mirror of
https://github.com/hanzoai/esbuild.git
synced 2026-09-24 16:58:50 +00:00
fix #4460: add --log-style=visualstudio
This commit is contained in:
@@ -63,6 +63,19 @@
|
||||
|
||||
See [microsoft/TypeScript#63527](https://github.com/microsoft/TypeScript/issues/63527) for more information.
|
||||
|
||||
* Add support for formatting errors for Visual Studio ([#4460](https://github.com/evanw/esbuild/issues/4460))
|
||||
|
||||
Visual Studio has a specific style that it expects log messages to be in for them to show up in the UI when esbuild is run as a custom build step. The current log style that esbuild uses doesn't conform to this specific style.
|
||||
|
||||
With this release, esbuild has a new log style for Visual Studio (and other tools in the MSBuild ecosystem) that can be enabled with `--log-style=visualstudio`. Here is an example log message in this style:
|
||||
|
||||
```
|
||||
$ esbuild example.ts --log-style=visualstudio
|
||||
/Users/evan/dev/esbuild/example.ts(1,29): warning: Operator "*" should not directly follow a TypeScript type cast after the "+" operator
|
||||
```
|
||||
|
||||
This visual style is also available via the JS and Go APIs, and can now be used with the existing `formatMessages` API.
|
||||
|
||||
## 0.28.1
|
||||
|
||||
* Disallow ``\`` in local development server HTTP requests ([GHSA-g7r4-m6w7-qqqr](https://github.com/evanw/esbuild/security/advisories/GHSA-g7r4-m6w7-qqqr))
|
||||
|
||||
@@ -1245,6 +1245,21 @@ func (service *serviceType) handleFormatMessagesRequest(id uint32, request map[s
|
||||
if value, ok := request["terminalWidth"].(int); ok {
|
||||
options.TerminalWidth = value
|
||||
}
|
||||
if logStyle, ok := request["logStyle"].(string); ok {
|
||||
switch logStyle {
|
||||
case "default":
|
||||
options.LogStyle = api.LogStyleDefault
|
||||
case "visualstudio":
|
||||
options.LogStyle = api.LogStyleVisualStudio
|
||||
default:
|
||||
return encodePacket(packet{
|
||||
id: id,
|
||||
value: map[string]interface{}{
|
||||
"error": fmt.Sprintf("Invalid log style: %q", logStyle),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
result := api.FormatMessages(msgs, options)
|
||||
|
||||
|
||||
+19
-444
@@ -47,6 +47,13 @@ const (
|
||||
LevelSilent
|
||||
)
|
||||
|
||||
type LogStyle int8
|
||||
|
||||
const (
|
||||
StyleDefault LogStyle = iota
|
||||
StyleVisualStudio
|
||||
)
|
||||
|
||||
type MsgKind uint8
|
||||
|
||||
const (
|
||||
@@ -839,6 +846,7 @@ func OutputOptionsForArgs(osArgs []string) OutputOptions {
|
||||
options.Color = ColorNever
|
||||
case "--color=true", "--color":
|
||||
options.Color = ColorAlways
|
||||
|
||||
case "--log-level=info":
|
||||
options.LogLevel = LevelInfo
|
||||
case "--log-level=warning":
|
||||
@@ -847,6 +855,11 @@ func OutputOptionsForArgs(osArgs []string) OutputOptions {
|
||||
options.LogLevel = LevelError
|
||||
case "--log-level=silent":
|
||||
options.LogLevel = LevelSilent
|
||||
|
||||
case "--log-style=default":
|
||||
options.LogStyle = StyleDefault
|
||||
case "--log-level=visualstudio":
|
||||
options.LogStyle = StyleVisualStudio
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1202,275 +1215,24 @@ type OutputOptions struct {
|
||||
IncludeSource bool
|
||||
Color UseColor
|
||||
LogLevel LogLevel
|
||||
LogStyle LogStyle
|
||||
PathStyle PathStyle
|
||||
Overrides map[MsgID]LogLevel
|
||||
}
|
||||
|
||||
func (msg Msg) String(options OutputOptions, terminalInfo TerminalInfo) string {
|
||||
// Format the message
|
||||
var text strings.Builder
|
||||
text.WriteString(msgString(options.IncludeSource, options.PathStyle, terminalInfo, msg.ID, msg.Kind, msg.Data, msg.PluginName))
|
||||
switch options.LogStyle {
|
||||
case StyleVisualStudio:
|
||||
return msgToStringVisualStudio(msg, options, terminalInfo)
|
||||
|
||||
// Format the notes
|
||||
var oldData MsgData
|
||||
for i, note := range msg.Notes {
|
||||
if options.IncludeSource && (i == 0 || strings.IndexByte(oldData.Text, '\n') >= 0 || oldData.Location != nil) {
|
||||
text.WriteString("\n")
|
||||
}
|
||||
text.WriteString(msgString(options.IncludeSource, options.PathStyle, terminalInfo, MsgID_None, Note, note, ""))
|
||||
oldData = note
|
||||
default:
|
||||
return msgToStringDefault(msg, options, terminalInfo)
|
||||
}
|
||||
|
||||
// Add extra spacing between messages if source code is present
|
||||
if options.IncludeSource {
|
||||
text.WriteString("\n")
|
||||
}
|
||||
return text.String()
|
||||
}
|
||||
|
||||
// The number of margin characters in addition to the line number
|
||||
const extraMarginChars = 9
|
||||
|
||||
func marginWithLineText(maxMargin int, line int) string {
|
||||
number := fmt.Sprintf("%d", line)
|
||||
return fmt.Sprintf(" %s%s │ ", strings.Repeat(" ", maxMargin-len(number)), number)
|
||||
}
|
||||
|
||||
func emptyMarginText(maxMargin int, isLast bool) string {
|
||||
space := strings.Repeat(" ", maxMargin)
|
||||
if isLast {
|
||||
return fmt.Sprintf(" %s ╵ ", space)
|
||||
}
|
||||
return fmt.Sprintf(" %s │ ", space)
|
||||
}
|
||||
|
||||
func msgString(includeSource bool, pathStyle PathStyle, terminalInfo TerminalInfo, id MsgID, kind MsgKind, data MsgData, pluginName string) string {
|
||||
if !includeSource {
|
||||
if loc := data.Location; loc != nil {
|
||||
return fmt.Sprintf("%s: %s: %s\n", loc.File.Select(pathStyle), kind.String(), data.Text)
|
||||
}
|
||||
return fmt.Sprintf("%s: %s\n", kind.String(), data.Text)
|
||||
}
|
||||
|
||||
var colors Colors
|
||||
if terminalInfo.UseColorEscapes {
|
||||
colors = TerminalColors
|
||||
}
|
||||
|
||||
var iconColor string
|
||||
var kindColorBrackets string
|
||||
var kindColorText string
|
||||
|
||||
location := ""
|
||||
|
||||
if data.Location != nil {
|
||||
maxMargin := len(fmt.Sprintf("%d", data.Location.Line))
|
||||
d := detailStruct(data, pathStyle, terminalInfo, maxMargin)
|
||||
|
||||
if d.Suggestion != "" {
|
||||
location = fmt.Sprintf("\n %s:%d:%d:\n%s%s%s%s%s%s\n%s%s%s%s%s\n%s%s%s%s%s\n%s",
|
||||
d.Path, d.Line, d.Column,
|
||||
colors.Dim, d.SourceBefore, colors.Green, d.SourceMarked, colors.Dim, d.SourceAfter,
|
||||
emptyMarginText(maxMargin, false), d.Indent, colors.Green, d.Marker, colors.Dim,
|
||||
emptyMarginText(maxMargin, true), d.Indent, colors.Green, d.Suggestion, colors.Reset,
|
||||
d.ContentAfter,
|
||||
)
|
||||
} else {
|
||||
location = fmt.Sprintf("\n %s:%d:%d:\n%s%s%s%s%s%s\n%s%s%s%s%s\n%s",
|
||||
d.Path, d.Line, d.Column,
|
||||
colors.Dim, d.SourceBefore, colors.Green, d.SourceMarked, colors.Dim, d.SourceAfter,
|
||||
emptyMarginText(maxMargin, true), d.Indent, colors.Green, d.Marker, colors.Reset,
|
||||
d.ContentAfter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case Verbose:
|
||||
iconColor = colors.Cyan
|
||||
kindColorBrackets = colors.CyanBgCyan
|
||||
kindColorText = colors.CyanBgBlack
|
||||
|
||||
case Debug:
|
||||
iconColor = colors.Green
|
||||
kindColorBrackets = colors.GreenBgGreen
|
||||
kindColorText = colors.GreenBgWhite
|
||||
|
||||
case Info:
|
||||
iconColor = colors.Blue
|
||||
kindColorBrackets = colors.BlueBgBlue
|
||||
kindColorText = colors.BlueBgWhite
|
||||
|
||||
case Error:
|
||||
iconColor = colors.Red
|
||||
kindColorBrackets = colors.RedBgRed
|
||||
kindColorText = colors.RedBgWhite
|
||||
|
||||
case Warning:
|
||||
iconColor = colors.Yellow
|
||||
kindColorBrackets = colors.YellowBgYellow
|
||||
kindColorText = colors.YellowBgBlack
|
||||
|
||||
case Note:
|
||||
sb := strings.Builder{}
|
||||
|
||||
for _, line := range strings.Split(data.Text, "\n") {
|
||||
// Special-case word wrapping
|
||||
if wrapWidth := terminalInfo.Width; wrapWidth > 2 {
|
||||
if !data.DisableMaximumWidth && wrapWidth > 100 {
|
||||
wrapWidth = 100 // Enforce a maximum paragraph width for readability
|
||||
}
|
||||
for _, run := range wrapWordsInString(line, wrapWidth-2) {
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString(linkifyText(run, colors.Underline, colors.Reset))
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Otherwise, just write an indented line
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString(linkifyText(line, colors.Underline, colors.Reset))
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
|
||||
sb.WriteString(location)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
if pluginName != "" {
|
||||
pluginName = fmt.Sprintf(" %s%s[plugin %s]%s", colors.Bold, colors.Magenta, pluginName, colors.Reset)
|
||||
}
|
||||
|
||||
msgID := MsgIDToString(id)
|
||||
if msgID != "" {
|
||||
msgID = fmt.Sprintf(" [%s]", msgID)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s%s %s[%s%s%s]%s %s%s%s%s%s\n%s",
|
||||
iconColor, kind.Icon(),
|
||||
kindColorBrackets, kindColorText, kind.String(), kindColorBrackets, colors.Reset,
|
||||
colors.Bold, data.Text, colors.Reset, pluginName, msgID,
|
||||
location,
|
||||
)
|
||||
}
|
||||
|
||||
func linkifyText(text string, underline string, reset string) string {
|
||||
if underline == "" {
|
||||
return text
|
||||
}
|
||||
|
||||
https := strings.Index(text, "https://")
|
||||
if https == -1 {
|
||||
return text
|
||||
}
|
||||
|
||||
sb := strings.Builder{}
|
||||
for {
|
||||
https := strings.Index(text, "https://")
|
||||
if https == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
end := strings.IndexByte(text[https:], ' ')
|
||||
if end == -1 {
|
||||
end = len(text)
|
||||
} else {
|
||||
end += https
|
||||
}
|
||||
|
||||
// Remove trailing punctuation
|
||||
if end > https {
|
||||
switch text[end-1] {
|
||||
case '.', ',', '?', '!', ')', ']', '}':
|
||||
end--
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString(text[:https])
|
||||
sb.WriteString(underline)
|
||||
sb.WriteString(text[https:end])
|
||||
sb.WriteString(reset)
|
||||
text = text[end:]
|
||||
}
|
||||
|
||||
sb.WriteString(text)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func wrapWordsInString(text string, width int) []string {
|
||||
runs := []string{}
|
||||
|
||||
outer:
|
||||
for text != "" {
|
||||
i := 0
|
||||
x := 0
|
||||
wordEndI := 0
|
||||
|
||||
// Skip over any leading spaces
|
||||
for i < len(text) && text[i] == ' ' {
|
||||
i++
|
||||
x++
|
||||
}
|
||||
|
||||
// Find out how many words will fit in this run
|
||||
for i < len(text) {
|
||||
oldWordEndI := wordEndI
|
||||
wordStartI := i
|
||||
|
||||
// Find the end of the word
|
||||
for i < len(text) {
|
||||
c, width := utf8.DecodeRuneInString(text[i:])
|
||||
if c == ' ' {
|
||||
break
|
||||
}
|
||||
i += width
|
||||
x += 1 // Naively assume that each unicode code point is a single column
|
||||
}
|
||||
wordEndI = i
|
||||
|
||||
// Split into a new run if this isn't the first word in the run and the end is past the width
|
||||
if wordStartI > 0 && x > width {
|
||||
runs = append(runs, text[:oldWordEndI])
|
||||
text = text[wordStartI:]
|
||||
continue outer
|
||||
}
|
||||
|
||||
// Skip over any spaces after the word
|
||||
for i < len(text) && text[i] == ' ' {
|
||||
i++
|
||||
x++
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, this is the last run (i.e. everything fits)
|
||||
break
|
||||
}
|
||||
|
||||
// Remove any trailing spaces on the last run
|
||||
for len(text) > 0 && text[len(text)-1] == ' ' {
|
||||
text = text[:len(text)-1]
|
||||
}
|
||||
runs = append(runs, text)
|
||||
return runs
|
||||
}
|
||||
|
||||
type MsgDetail struct {
|
||||
SourceBefore string
|
||||
SourceMarked string
|
||||
SourceAfter string
|
||||
|
||||
Indent string
|
||||
Marker string
|
||||
Suggestion string
|
||||
|
||||
ContentAfter string
|
||||
|
||||
Path string
|
||||
Line int
|
||||
Column int
|
||||
}
|
||||
|
||||
// It's not common for large files to have many warnings. But when it happens,
|
||||
// we want to make sure that it's not too slow. Source code locations are
|
||||
// represented as byte offsets for compactness but transforming these to
|
||||
@@ -1639,193 +1401,6 @@ func (tracker *LineColumnTracker) MsgLocationOrNil(r Range) *MsgLocation {
|
||||
}
|
||||
}
|
||||
|
||||
func detailStruct(data MsgData, pathStyle PathStyle, terminalInfo TerminalInfo, maxMargin int) MsgDetail {
|
||||
// Only highlight the first line of the line text
|
||||
loc := *data.Location
|
||||
endOfFirstLine := len(loc.LineText)
|
||||
|
||||
// Note: This uses "IndexByte" because Go implements this with SIMD, which
|
||||
// can matter a lot for really long lines. Some people pass huge >100mb
|
||||
// minified files as line text for the log message.
|
||||
if i := strings.IndexByte(loc.LineText, '\n'); i >= 0 {
|
||||
endOfFirstLine = i
|
||||
}
|
||||
|
||||
firstLine := loc.LineText[:endOfFirstLine]
|
||||
afterFirstLine := loc.LineText[endOfFirstLine:]
|
||||
if afterFirstLine != "" && !strings.HasSuffix(afterFirstLine, "\n") {
|
||||
afterFirstLine += "\n"
|
||||
}
|
||||
|
||||
// Clamp values in range
|
||||
if loc.Line < 0 {
|
||||
loc.Line = 0
|
||||
}
|
||||
if loc.Column < 0 {
|
||||
loc.Column = 0
|
||||
}
|
||||
if loc.Length < 0 {
|
||||
loc.Length = 0
|
||||
}
|
||||
if loc.Column > endOfFirstLine {
|
||||
loc.Column = endOfFirstLine
|
||||
}
|
||||
if loc.Length > endOfFirstLine-loc.Column {
|
||||
loc.Length = endOfFirstLine - loc.Column
|
||||
}
|
||||
|
||||
spacesPerTab := 2
|
||||
lineText := renderTabStops(firstLine, spacesPerTab)
|
||||
textUpToLoc := renderTabStops(firstLine[:loc.Column], spacesPerTab)
|
||||
markerStart := len(textUpToLoc)
|
||||
markerEnd := markerStart
|
||||
indent := strings.Repeat(" ", estimateWidthInTerminal(textUpToLoc))
|
||||
marker := "^"
|
||||
|
||||
// Extend markers to cover the full range of the error
|
||||
if loc.Length > 0 {
|
||||
markerEnd = len(renderTabStops(firstLine[:loc.Column+loc.Length], spacesPerTab))
|
||||
}
|
||||
|
||||
// Clip the marker to the bounds of the line
|
||||
if markerStart > len(lineText) {
|
||||
markerStart = len(lineText)
|
||||
}
|
||||
if markerEnd > len(lineText) {
|
||||
markerEnd = len(lineText)
|
||||
}
|
||||
if markerEnd < markerStart {
|
||||
markerEnd = markerStart
|
||||
}
|
||||
|
||||
// Trim the line to fit the terminal width
|
||||
width := terminalInfo.Width
|
||||
if width < 1 {
|
||||
width = defaultTerminalWidth
|
||||
}
|
||||
width -= maxMargin + extraMarginChars
|
||||
if width < 1 {
|
||||
width = 1
|
||||
}
|
||||
if loc.Column == endOfFirstLine {
|
||||
// If the marker is at the very end of the line, the marker will be a "^"
|
||||
// character that extends one column past the end of the line. In this case
|
||||
// we should reserve a column at the end so the marker doesn't wrap.
|
||||
width -= 1
|
||||
}
|
||||
if len(lineText) > width {
|
||||
// Try to center the error
|
||||
sliceStart := (markerStart + markerEnd - width) / 2
|
||||
if sliceStart > markerStart-width/5 {
|
||||
sliceStart = markerStart - width/5
|
||||
}
|
||||
if sliceStart < 0 {
|
||||
sliceStart = 0
|
||||
}
|
||||
if sliceStart > len(lineText)-width {
|
||||
sliceStart = len(lineText) - width
|
||||
}
|
||||
sliceEnd := sliceStart + width
|
||||
|
||||
// Slice the line
|
||||
slicedLine := lineText[sliceStart:sliceEnd]
|
||||
markerStart -= sliceStart
|
||||
markerEnd -= sliceStart
|
||||
if markerStart < 0 {
|
||||
markerStart = 0
|
||||
}
|
||||
if markerEnd > len(slicedLine) {
|
||||
markerEnd = len(slicedLine)
|
||||
}
|
||||
|
||||
// Truncate the ends with "..."
|
||||
if len(slicedLine) > 3 && sliceStart > 0 {
|
||||
slicedLine = "..." + slicedLine[3:]
|
||||
if markerStart < 3 {
|
||||
markerStart = 3
|
||||
}
|
||||
}
|
||||
if len(slicedLine) > 3 && sliceEnd < len(lineText) {
|
||||
slicedLine = slicedLine[:len(slicedLine)-3] + "..."
|
||||
if markerEnd > len(slicedLine)-3 {
|
||||
markerEnd = len(slicedLine) - 3
|
||||
}
|
||||
if markerEnd < markerStart {
|
||||
markerEnd = markerStart
|
||||
}
|
||||
}
|
||||
|
||||
// Now we can compute the indent
|
||||
lineText = slicedLine
|
||||
indent = strings.Repeat(" ", estimateWidthInTerminal(lineText[:markerStart]))
|
||||
}
|
||||
|
||||
// If marker is still multi-character after clipping, make the marker wider
|
||||
if markerEnd-markerStart > 1 {
|
||||
marker = strings.Repeat("~", estimateWidthInTerminal(lineText[markerStart:markerEnd]))
|
||||
}
|
||||
|
||||
// Put a margin before the marker indent
|
||||
margin := marginWithLineText(maxMargin, loc.Line)
|
||||
|
||||
return MsgDetail{
|
||||
Path: loc.File.Select(pathStyle),
|
||||
Line: loc.Line,
|
||||
Column: loc.Column,
|
||||
|
||||
SourceBefore: margin + lineText[:markerStart],
|
||||
SourceMarked: lineText[markerStart:markerEnd],
|
||||
SourceAfter: lineText[markerEnd:],
|
||||
|
||||
Indent: indent,
|
||||
Marker: marker,
|
||||
Suggestion: loc.Suggestion,
|
||||
|
||||
ContentAfter: afterFirstLine,
|
||||
}
|
||||
}
|
||||
|
||||
// Estimate the number of columns this string will take when printed
|
||||
func estimateWidthInTerminal(text string) int {
|
||||
// For now just assume each code point is one column. This is wrong but is
|
||||
// less wrong than assuming each code unit is one column.
|
||||
width := 0
|
||||
for text != "" {
|
||||
c, size := utf8.DecodeRuneInString(text)
|
||||
text = text[size:]
|
||||
|
||||
// Ignore the Zero Width No-Break Space character (UTF-8 BOM)
|
||||
if c != 0xFEFF {
|
||||
width++
|
||||
}
|
||||
}
|
||||
return width
|
||||
}
|
||||
|
||||
func renderTabStops(withTabs string, spacesPerTab int) string {
|
||||
if !strings.ContainsRune(withTabs, '\t') {
|
||||
return withTabs
|
||||
}
|
||||
|
||||
withoutTabs := strings.Builder{}
|
||||
count := 0
|
||||
|
||||
for _, c := range withTabs {
|
||||
if c == '\t' {
|
||||
spaces := spacesPerTab - count%spacesPerTab
|
||||
for i := 0; i < spaces; i++ {
|
||||
withoutTabs.WriteRune(' ')
|
||||
count++
|
||||
}
|
||||
} else {
|
||||
withoutTabs.WriteRune(c)
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
return withoutTabs.String()
|
||||
}
|
||||
|
||||
func (log Log) AddError(tracker *LineColumnTracker, r Range, text string) {
|
||||
log.AddMsg(Msg{
|
||||
Kind: Error,
|
||||
|
||||
@@ -0,0 +1,460 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func msgToStringDefault(msg Msg, options OutputOptions, terminalInfo TerminalInfo) string {
|
||||
// Format the message
|
||||
var text strings.Builder
|
||||
text.WriteString(msgString(options.IncludeSource, options.PathStyle, terminalInfo, msg.ID, msg.Kind, msg.Data, msg.PluginName))
|
||||
|
||||
// Format the notes
|
||||
var oldData MsgData
|
||||
for i, note := range msg.Notes {
|
||||
if options.IncludeSource && (i == 0 || strings.IndexByte(oldData.Text, '\n') >= 0 || oldData.Location != nil) {
|
||||
text.WriteString("\n")
|
||||
}
|
||||
text.WriteString(msgString(options.IncludeSource, options.PathStyle, terminalInfo, MsgID_None, Note, note, ""))
|
||||
oldData = note
|
||||
}
|
||||
|
||||
// Add extra spacing between messages if source code is present
|
||||
if options.IncludeSource {
|
||||
text.WriteString("\n")
|
||||
}
|
||||
return text.String()
|
||||
}
|
||||
|
||||
func marginWithLineText(maxMargin int, line int) string {
|
||||
number := fmt.Sprintf("%d", line)
|
||||
return fmt.Sprintf(" %s%s │ ", strings.Repeat(" ", maxMargin-len(number)), number)
|
||||
}
|
||||
|
||||
func emptyMarginText(maxMargin int, isLast bool) string {
|
||||
space := strings.Repeat(" ", maxMargin)
|
||||
if isLast {
|
||||
return fmt.Sprintf(" %s ╵ ", space)
|
||||
}
|
||||
return fmt.Sprintf(" %s │ ", space)
|
||||
}
|
||||
|
||||
func msgString(includeSource bool, pathStyle PathStyle, terminalInfo TerminalInfo, id MsgID, kind MsgKind, data MsgData, pluginName string) string {
|
||||
if !includeSource {
|
||||
if loc := data.Location; loc != nil {
|
||||
return fmt.Sprintf("%s: %s: %s\n", loc.File.Select(pathStyle), kind.String(), data.Text)
|
||||
}
|
||||
return fmt.Sprintf("%s: %s\n", kind.String(), data.Text)
|
||||
}
|
||||
|
||||
var colors Colors
|
||||
if terminalInfo.UseColorEscapes {
|
||||
colors = TerminalColors
|
||||
}
|
||||
|
||||
var iconColor string
|
||||
var kindColorBrackets string
|
||||
var kindColorText string
|
||||
|
||||
location := ""
|
||||
|
||||
if data.Location != nil {
|
||||
maxMargin := len(fmt.Sprintf("%d", data.Location.Line))
|
||||
d := detailStruct(data, pathStyle, terminalInfo, maxMargin)
|
||||
|
||||
if d.Suggestion != "" {
|
||||
location = fmt.Sprintf("\n %s:%d:%d:\n%s%s%s%s%s%s\n%s%s%s%s%s\n%s%s%s%s%s\n%s",
|
||||
d.Path, d.Line, d.Column,
|
||||
colors.Dim, d.SourceBefore, colors.Green, d.SourceMarked, colors.Dim, d.SourceAfter,
|
||||
emptyMarginText(maxMargin, false), d.Indent, colors.Green, d.Marker, colors.Dim,
|
||||
emptyMarginText(maxMargin, true), d.Indent, colors.Green, d.Suggestion, colors.Reset,
|
||||
d.ContentAfter,
|
||||
)
|
||||
} else {
|
||||
location = fmt.Sprintf("\n %s:%d:%d:\n%s%s%s%s%s%s\n%s%s%s%s%s\n%s",
|
||||
d.Path, d.Line, d.Column,
|
||||
colors.Dim, d.SourceBefore, colors.Green, d.SourceMarked, colors.Dim, d.SourceAfter,
|
||||
emptyMarginText(maxMargin, true), d.Indent, colors.Green, d.Marker, colors.Reset,
|
||||
d.ContentAfter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case Verbose:
|
||||
iconColor = colors.Cyan
|
||||
kindColorBrackets = colors.CyanBgCyan
|
||||
kindColorText = colors.CyanBgBlack
|
||||
|
||||
case Debug:
|
||||
iconColor = colors.Green
|
||||
kindColorBrackets = colors.GreenBgGreen
|
||||
kindColorText = colors.GreenBgWhite
|
||||
|
||||
case Info:
|
||||
iconColor = colors.Blue
|
||||
kindColorBrackets = colors.BlueBgBlue
|
||||
kindColorText = colors.BlueBgWhite
|
||||
|
||||
case Error:
|
||||
iconColor = colors.Red
|
||||
kindColorBrackets = colors.RedBgRed
|
||||
kindColorText = colors.RedBgWhite
|
||||
|
||||
case Warning:
|
||||
iconColor = colors.Yellow
|
||||
kindColorBrackets = colors.YellowBgYellow
|
||||
kindColorText = colors.YellowBgBlack
|
||||
|
||||
case Note:
|
||||
sb := strings.Builder{}
|
||||
|
||||
for _, line := range strings.Split(data.Text, "\n") {
|
||||
// Special-case word wrapping
|
||||
if wrapWidth := terminalInfo.Width; wrapWidth > 2 {
|
||||
if !data.DisableMaximumWidth && wrapWidth > 100 {
|
||||
wrapWidth = 100 // Enforce a maximum paragraph width for readability
|
||||
}
|
||||
for _, run := range wrapWordsInString(line, wrapWidth-2) {
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString(linkifyText(run, colors.Underline, colors.Reset))
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Otherwise, just write an indented line
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString(linkifyText(line, colors.Underline, colors.Reset))
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
|
||||
sb.WriteString(location)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
if pluginName != "" {
|
||||
pluginName = fmt.Sprintf(" %s%s[plugin %s]%s", colors.Bold, colors.Magenta, pluginName, colors.Reset)
|
||||
}
|
||||
|
||||
msgID := MsgIDToString(id)
|
||||
if msgID != "" {
|
||||
msgID = fmt.Sprintf(" [%s]", msgID)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s%s %s[%s%s%s]%s %s%s%s%s%s\n%s",
|
||||
iconColor, kind.Icon(),
|
||||
kindColorBrackets, kindColorText, kind.String(), kindColorBrackets, colors.Reset,
|
||||
colors.Bold, data.Text, colors.Reset, pluginName, msgID,
|
||||
location,
|
||||
)
|
||||
}
|
||||
|
||||
func linkifyText(text string, underline string, reset string) string {
|
||||
if underline == "" {
|
||||
return text
|
||||
}
|
||||
|
||||
https := strings.Index(text, "https://")
|
||||
if https == -1 {
|
||||
return text
|
||||
}
|
||||
|
||||
sb := strings.Builder{}
|
||||
for {
|
||||
https := strings.Index(text, "https://")
|
||||
if https == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
end := strings.IndexByte(text[https:], ' ')
|
||||
if end == -1 {
|
||||
end = len(text)
|
||||
} else {
|
||||
end += https
|
||||
}
|
||||
|
||||
// Remove trailing punctuation
|
||||
if end > https {
|
||||
switch text[end-1] {
|
||||
case '.', ',', '?', '!', ')', ']', '}':
|
||||
end--
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString(text[:https])
|
||||
sb.WriteString(underline)
|
||||
sb.WriteString(text[https:end])
|
||||
sb.WriteString(reset)
|
||||
text = text[end:]
|
||||
}
|
||||
|
||||
sb.WriteString(text)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func wrapWordsInString(text string, width int) []string {
|
||||
runs := []string{}
|
||||
|
||||
outer:
|
||||
for text != "" {
|
||||
i := 0
|
||||
x := 0
|
||||
wordEndI := 0
|
||||
|
||||
// Skip over any leading spaces
|
||||
for i < len(text) && text[i] == ' ' {
|
||||
i++
|
||||
x++
|
||||
}
|
||||
|
||||
// Find out how many words will fit in this run
|
||||
for i < len(text) {
|
||||
oldWordEndI := wordEndI
|
||||
wordStartI := i
|
||||
|
||||
// Find the end of the word
|
||||
for i < len(text) {
|
||||
c, width := utf8.DecodeRuneInString(text[i:])
|
||||
if c == ' ' {
|
||||
break
|
||||
}
|
||||
i += width
|
||||
x += 1 // Naively assume that each unicode code point is a single column
|
||||
}
|
||||
wordEndI = i
|
||||
|
||||
// Split into a new run if this isn't the first word in the run and the end is past the width
|
||||
if wordStartI > 0 && x > width {
|
||||
runs = append(runs, text[:oldWordEndI])
|
||||
text = text[wordStartI:]
|
||||
continue outer
|
||||
}
|
||||
|
||||
// Skip over any spaces after the word
|
||||
for i < len(text) && text[i] == ' ' {
|
||||
i++
|
||||
x++
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, this is the last run (i.e. everything fits)
|
||||
break
|
||||
}
|
||||
|
||||
// Remove any trailing spaces on the last run
|
||||
for len(text) > 0 && text[len(text)-1] == ' ' {
|
||||
text = text[:len(text)-1]
|
||||
}
|
||||
runs = append(runs, text)
|
||||
return runs
|
||||
}
|
||||
|
||||
type msgDetail struct {
|
||||
SourceBefore string
|
||||
SourceMarked string
|
||||
SourceAfter string
|
||||
|
||||
Indent string
|
||||
Marker string
|
||||
Suggestion string
|
||||
|
||||
ContentAfter string
|
||||
|
||||
Path string
|
||||
Line int
|
||||
Column int
|
||||
}
|
||||
|
||||
func detailStruct(data MsgData, pathStyle PathStyle, terminalInfo TerminalInfo, maxMargin int) msgDetail {
|
||||
// Only highlight the first line of the line text
|
||||
loc := *data.Location
|
||||
endOfFirstLine := len(loc.LineText)
|
||||
|
||||
// Note: This uses "IndexByte" because Go implements this with SIMD, which
|
||||
// can matter a lot for really long lines. Some people pass huge >100mb
|
||||
// minified files as line text for the log message.
|
||||
if i := strings.IndexByte(loc.LineText, '\n'); i >= 0 {
|
||||
endOfFirstLine = i
|
||||
}
|
||||
|
||||
firstLine := loc.LineText[:endOfFirstLine]
|
||||
afterFirstLine := loc.LineText[endOfFirstLine:]
|
||||
if afterFirstLine != "" && !strings.HasSuffix(afterFirstLine, "\n") {
|
||||
afterFirstLine += "\n"
|
||||
}
|
||||
|
||||
// Clamp values in range
|
||||
if loc.Line < 0 {
|
||||
loc.Line = 0
|
||||
}
|
||||
if loc.Column < 0 {
|
||||
loc.Column = 0
|
||||
}
|
||||
if loc.Length < 0 {
|
||||
loc.Length = 0
|
||||
}
|
||||
if loc.Column > endOfFirstLine {
|
||||
loc.Column = endOfFirstLine
|
||||
}
|
||||
if loc.Length > endOfFirstLine-loc.Column {
|
||||
loc.Length = endOfFirstLine - loc.Column
|
||||
}
|
||||
|
||||
spacesPerTab := 2
|
||||
lineText := renderTabStops(firstLine, spacesPerTab)
|
||||
textUpToLoc := renderTabStops(firstLine[:loc.Column], spacesPerTab)
|
||||
markerStart := len(textUpToLoc)
|
||||
markerEnd := markerStart
|
||||
indent := strings.Repeat(" ", estimateWidthInTerminal(textUpToLoc))
|
||||
marker := "^"
|
||||
|
||||
// Extend markers to cover the full range of the error
|
||||
if loc.Length > 0 {
|
||||
markerEnd = len(renderTabStops(firstLine[:loc.Column+loc.Length], spacesPerTab))
|
||||
}
|
||||
|
||||
// Clip the marker to the bounds of the line
|
||||
if markerStart > len(lineText) {
|
||||
markerStart = len(lineText)
|
||||
}
|
||||
if markerEnd > len(lineText) {
|
||||
markerEnd = len(lineText)
|
||||
}
|
||||
if markerEnd < markerStart {
|
||||
markerEnd = markerStart
|
||||
}
|
||||
|
||||
// Trim the line to fit the terminal width
|
||||
width := terminalInfo.Width
|
||||
if width < 1 {
|
||||
width = defaultTerminalWidth
|
||||
}
|
||||
width -= maxMargin + extraMarginChars
|
||||
if width < 1 {
|
||||
width = 1
|
||||
}
|
||||
if loc.Column == endOfFirstLine {
|
||||
// If the marker is at the very end of the line, the marker will be a "^"
|
||||
// character that extends one column past the end of the line. In this case
|
||||
// we should reserve a column at the end so the marker doesn't wrap.
|
||||
width -= 1
|
||||
}
|
||||
if len(lineText) > width {
|
||||
// Try to center the error
|
||||
sliceStart := (markerStart + markerEnd - width) / 2
|
||||
if sliceStart > markerStart-width/5 {
|
||||
sliceStart = markerStart - width/5
|
||||
}
|
||||
if sliceStart < 0 {
|
||||
sliceStart = 0
|
||||
}
|
||||
if sliceStart > len(lineText)-width {
|
||||
sliceStart = len(lineText) - width
|
||||
}
|
||||
sliceEnd := sliceStart + width
|
||||
|
||||
// Slice the line
|
||||
slicedLine := lineText[sliceStart:sliceEnd]
|
||||
markerStart -= sliceStart
|
||||
markerEnd -= sliceStart
|
||||
if markerStart < 0 {
|
||||
markerStart = 0
|
||||
}
|
||||
if markerEnd > len(slicedLine) {
|
||||
markerEnd = len(slicedLine)
|
||||
}
|
||||
|
||||
// Truncate the ends with "..."
|
||||
if len(slicedLine) > 3 && sliceStart > 0 {
|
||||
slicedLine = "..." + slicedLine[3:]
|
||||
if markerStart < 3 {
|
||||
markerStart = 3
|
||||
}
|
||||
}
|
||||
if len(slicedLine) > 3 && sliceEnd < len(lineText) {
|
||||
slicedLine = slicedLine[:len(slicedLine)-3] + "..."
|
||||
if markerEnd > len(slicedLine)-3 {
|
||||
markerEnd = len(slicedLine) - 3
|
||||
}
|
||||
if markerEnd < markerStart {
|
||||
markerEnd = markerStart
|
||||
}
|
||||
}
|
||||
|
||||
// Now we can compute the indent
|
||||
lineText = slicedLine
|
||||
indent = strings.Repeat(" ", estimateWidthInTerminal(lineText[:markerStart]))
|
||||
}
|
||||
|
||||
// If marker is still multi-character after clipping, make the marker wider
|
||||
if markerEnd-markerStart > 1 {
|
||||
marker = strings.Repeat("~", estimateWidthInTerminal(lineText[markerStart:markerEnd]))
|
||||
}
|
||||
|
||||
// Put a margin before the marker indent
|
||||
margin := marginWithLineText(maxMargin, loc.Line)
|
||||
|
||||
return msgDetail{
|
||||
Path: loc.File.Select(pathStyle),
|
||||
|
||||
// Note: We want to deliberately print the unclamped line and column, as it
|
||||
// may come from another tool that either didn't set "LineText" at all or
|
||||
// at least didn't calculate the column number correctly.
|
||||
Line: data.Location.Line,
|
||||
Column: data.Location.Column,
|
||||
|
||||
SourceBefore: margin + lineText[:markerStart],
|
||||
SourceMarked: lineText[markerStart:markerEnd],
|
||||
SourceAfter: lineText[markerEnd:],
|
||||
|
||||
Indent: indent,
|
||||
Marker: marker,
|
||||
Suggestion: loc.Suggestion,
|
||||
|
||||
ContentAfter: afterFirstLine,
|
||||
}
|
||||
}
|
||||
|
||||
// Estimate the number of columns this string will take when printed
|
||||
func estimateWidthInTerminal(text string) int {
|
||||
// For now just assume each code point is one column. This is wrong but is
|
||||
// less wrong than assuming each code unit is one column.
|
||||
width := 0
|
||||
for text != "" {
|
||||
c, size := utf8.DecodeRuneInString(text)
|
||||
text = text[size:]
|
||||
|
||||
// Ignore the Zero Width No-Break Space character (UTF-8 BOM)
|
||||
if c != 0xFEFF {
|
||||
width++
|
||||
}
|
||||
}
|
||||
return width
|
||||
}
|
||||
|
||||
func renderTabStops(withTabs string, spacesPerTab int) string {
|
||||
if !strings.ContainsRune(withTabs, '\t') {
|
||||
return withTabs
|
||||
}
|
||||
|
||||
withoutTabs := strings.Builder{}
|
||||
count := 0
|
||||
|
||||
for _, c := range withTabs {
|
||||
if c == '\t' {
|
||||
spaces := spacesPerTab - count%spacesPerTab
|
||||
for i := 0; i < spaces; i++ {
|
||||
withoutTabs.WriteRune(' ')
|
||||
count++
|
||||
}
|
||||
} else {
|
||||
withoutTabs.WriteRune(c)
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
return withoutTabs.String()
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// This log style is intended to help integrate esbuild with Visual Studio's
|
||||
// built-in problem matcher. More information:
|
||||
//
|
||||
// - https://learn.microsoft.com/en-us/cpp/build/formatting-the-output-of-a-custom-build-step-or-build-event
|
||||
// - https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-diagnostic-format-for-tasks
|
||||
func msgToStringVisualStudio(msg Msg, options OutputOptions, terminalInfo TerminalInfo) string {
|
||||
var text strings.Builder
|
||||
|
||||
// Write the origin
|
||||
if loc := msg.Data.Location; loc != nil {
|
||||
// Potentially this needs to always be an absolute path?
|
||||
text.WriteString(loc.File.Abs)
|
||||
|
||||
// Note: Need to adjust the column from 0-based to 1-based
|
||||
if loc.Line > 0 {
|
||||
text.WriteString(fmt.Sprintf("(%d,%d)", loc.Line, loc.Column+1))
|
||||
}
|
||||
text.WriteString(": ")
|
||||
} else {
|
||||
// If there is no file, then we must write a tool name
|
||||
text.WriteString("esbuild: ")
|
||||
}
|
||||
|
||||
// The only valid options for the category seem to be "error" and "warning"
|
||||
if msg.Kind == Error {
|
||||
text.WriteString("error: ")
|
||||
} else {
|
||||
text.WriteString("warning: ")
|
||||
}
|
||||
|
||||
text.WriteString(msg.Data.Text)
|
||||
text.WriteByte('\n')
|
||||
return text.String()
|
||||
}
|
||||
@@ -128,11 +128,13 @@ function pushLogFlags(flags: string[], options: CommonOptions, keys: OptionKeys,
|
||||
let color = getFlag(options, keys, 'color', mustBeBoolean)
|
||||
let logLevel = getFlag(options, keys, 'logLevel', mustBeString)
|
||||
let logLimit = getFlag(options, keys, 'logLimit', mustBeInteger)
|
||||
let logStyle = getFlag(options, keys, 'logStyle', mustBeString)
|
||||
|
||||
if (color !== void 0) flags.push(`--color=${color}`)
|
||||
else if (isTTY) flags.push(`--color=true`); // This is needed to fix "execFileSync" which buffers stderr
|
||||
flags.push(`--log-level=${logLevel || logLevelDefault}`)
|
||||
flags.push(`--log-limit=${logLimit || 0}`)
|
||||
if (logStyle) flags.push(`--log-style=${logStyle}`)
|
||||
}
|
||||
|
||||
function validateStringValue(value: unknown, what: string, key?: string): string {
|
||||
@@ -791,6 +793,7 @@ export function createChannel(streamIn: StreamIn): StreamOut {
|
||||
let kind = getFlag(options, keys, 'kind', mustBeString)
|
||||
let color = getFlag(options, keys, 'color', mustBeBoolean)
|
||||
let terminalWidth = getFlag(options, keys, 'terminalWidth', mustBeInteger)
|
||||
let logStyle = getFlag(options, keys, 'logStyle', mustBeString)
|
||||
checkForInvalidFlags(options, keys, `in ${callName}() call`)
|
||||
if (kind === void 0) throw new Error(`Missing "kind" in ${callName}() call`)
|
||||
if (kind !== 'error' && kind !== 'warning') throw new Error(`Expected "kind" to be "error" or "warning" in ${callName}() call`)
|
||||
@@ -801,6 +804,7 @@ export function createChannel(streamIn: StreamIn): StreamOut {
|
||||
}
|
||||
if (color !== void 0) request.color = color
|
||||
if (terminalWidth !== void 0) request.terminalWidth = terminalWidth
|
||||
if (logStyle !== void 0) request.logStyle = logStyle
|
||||
sendRequest<protocol.FormatMsgsRequest, protocol.FormatMsgsResponse>(refs, request, (error, response) => {
|
||||
if (error) return callback(new Error(error), null)
|
||||
callback(null, response!.messages)
|
||||
|
||||
@@ -135,6 +135,7 @@ export interface FormatMsgsRequest {
|
||||
isWarning: boolean
|
||||
color?: boolean
|
||||
terminalWidth?: number
|
||||
logStyle?: string
|
||||
}
|
||||
|
||||
export interface FormatMsgsResponse {
|
||||
|
||||
@@ -2,6 +2,7 @@ export type Platform = 'browser' | 'node' | 'neutral'
|
||||
export type Format = 'iife' | 'cjs' | 'esm'
|
||||
export type Loader = 'base64' | 'binary' | 'copy' | 'css' | 'dataurl' | 'default' | 'empty' | 'file' | 'js' | 'json' | 'jsx' | 'local-css' | 'text' | 'ts' | 'tsx'
|
||||
export type LogLevel = 'verbose' | 'debug' | 'info' | 'warning' | 'error' | 'silent'
|
||||
export type LogStyle = 'default' | 'visualstudio'
|
||||
export type Charset = 'ascii' | 'utf8'
|
||||
export type Drop = 'console' | 'debugger'
|
||||
export type AbsPaths = 'code' | 'log' | 'metafile'
|
||||
@@ -86,6 +87,8 @@ interface CommonOptions {
|
||||
logLimit?: number
|
||||
/** Documentation: https://esbuild.github.io/api/#log-override */
|
||||
logOverride?: Record<string, LogLevel>
|
||||
/** Documentation: https://esbuild.github.io/api/#log-style */
|
||||
logStyle?: LogStyle
|
||||
|
||||
/** Documentation: https://esbuild.github.io/api/#tsconfig-raw */
|
||||
tsconfigRaw?: string | TsconfigRaw
|
||||
@@ -509,6 +512,7 @@ export interface FormatMessagesOptions {
|
||||
kind: 'error' | 'warning'
|
||||
color?: boolean
|
||||
terminalWidth?: number
|
||||
logStyle?: LogStyle
|
||||
}
|
||||
|
||||
export interface AnalyzeMetafileOptions {
|
||||
|
||||
@@ -236,6 +236,13 @@ const (
|
||||
LogLevelError
|
||||
)
|
||||
|
||||
type LogStyle uint8
|
||||
|
||||
const (
|
||||
LogStyleDefault LogStyle = iota
|
||||
LogStyleVisualStudio
|
||||
)
|
||||
|
||||
type Charset uint8
|
||||
|
||||
const (
|
||||
@@ -280,6 +287,7 @@ const (
|
||||
type BuildOptions struct {
|
||||
Color StderrColor // Documentation: https://esbuild.github.io/api/#color
|
||||
LogLevel LogLevel // Documentation: https://esbuild.github.io/api/#log-level
|
||||
LogStyle LogStyle // Documentation: https://esbuild.github.io/api/#log-style
|
||||
LogLimit int // Documentation: https://esbuild.github.io/api/#log-limit
|
||||
LogOverride map[string]LogLevel // Documentation: https://esbuild.github.io/api/#log-override
|
||||
AbsPaths AbsPaths // Documentation: https://esbuild.github.io/api/#abs-path
|
||||
@@ -412,6 +420,7 @@ func Build(options BuildOptions) BuildResult {
|
||||
type TransformOptions struct {
|
||||
Color StderrColor // Documentation: https://esbuild.github.io/api/#color
|
||||
LogLevel LogLevel // Documentation: https://esbuild.github.io/api/#log-level
|
||||
LogStyle LogStyle // Documentation: https://esbuild.github.io/api/#log-style
|
||||
LogLimit int // Documentation: https://esbuild.github.io/api/#log-limit
|
||||
LogOverride map[string]LogLevel // Documentation: https://esbuild.github.io/api/#log-override
|
||||
AbsPaths AbsPaths // Documentation: https://esbuild.github.io/api/#abs-path
|
||||
@@ -717,6 +726,7 @@ type FormatMessagesOptions struct {
|
||||
TerminalWidth int
|
||||
Kind MessageKind
|
||||
Color bool
|
||||
LogStyle LogStyle
|
||||
}
|
||||
|
||||
func FormatMessages(msgs []Message, opts FormatMessagesOptions) []string {
|
||||
|
||||
@@ -205,6 +205,17 @@ func validateLogLevel(value LogLevel) logger.LogLevel {
|
||||
}
|
||||
}
|
||||
|
||||
func validateLogStyle(value LogStyle) logger.LogStyle {
|
||||
switch value {
|
||||
case LogStyleDefault:
|
||||
return logger.StyleDefault
|
||||
case LogStyleVisualStudio:
|
||||
return logger.StyleVisualStudio
|
||||
default:
|
||||
panic("Invalid log style")
|
||||
}
|
||||
}
|
||||
|
||||
func validateASCIIOnly(value Charset) bool {
|
||||
switch value {
|
||||
case CharsetDefault, CharsetASCII:
|
||||
@@ -889,6 +900,7 @@ func contextImpl(buildOpts BuildOptions) (*internalContext, []Message) {
|
||||
MessageLimit: buildOpts.LogLimit,
|
||||
Color: validateColor(buildOpts.Color),
|
||||
LogLevel: validateLogLevel(buildOpts.LogLevel),
|
||||
LogStyle: validateLogStyle(buildOpts.LogStyle),
|
||||
PathStyle: extractPathStyle(buildOpts.AbsPaths, LogAbsPath),
|
||||
Overrides: validateLogOverrides(buildOpts.LogOverride),
|
||||
}
|
||||
@@ -1695,6 +1707,7 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult
|
||||
MessageLimit: transformOpts.LogLimit,
|
||||
Color: validateColor(transformOpts.Color),
|
||||
LogLevel: validateLogLevel(transformOpts.LogLevel),
|
||||
LogStyle: validateLogStyle(transformOpts.LogStyle),
|
||||
PathStyle: extractPathStyle(transformOpts.AbsPaths, LogAbsPath),
|
||||
Overrides: validateLogOverrides(transformOpts.LogOverride),
|
||||
})
|
||||
@@ -2204,6 +2217,7 @@ func formatMsgsImpl(msgs []Message, opts FormatMessagesOptions) []string {
|
||||
strings[i] = msg.String(
|
||||
logger.OutputOptions{
|
||||
IncludeSource: true,
|
||||
LogStyle: validateLogStyle(opts.LogStyle),
|
||||
},
|
||||
logger.TerminalInfo{
|
||||
UseColorEscapes: opts.Color,
|
||||
|
||||
@@ -836,6 +836,19 @@ func parseOptionsImpl(
|
||||
transformOpts.LogLevel = logLevel
|
||||
}
|
||||
|
||||
// Make sure this stays in sync with "PrintErrorToStderr"
|
||||
case strings.HasPrefix(arg, "--log-style="):
|
||||
value := arg[len("--log-style="):]
|
||||
logStyle, err := parseLogStyle(value, arg)
|
||||
if err != nil {
|
||||
return parseOptionsExtras{}, err
|
||||
}
|
||||
if buildOpts != nil {
|
||||
buildOpts.LogStyle = logStyle
|
||||
} else {
|
||||
transformOpts.LogStyle = logStyle
|
||||
}
|
||||
|
||||
case strings.HasPrefix(arg, "'--"):
|
||||
return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(
|
||||
fmt.Sprintf("Unexpected single quote character before flag: %s", arg),
|
||||
@@ -1562,3 +1575,17 @@ func parseLogLevel(value string, arg string) (api.LogLevel, *cli_helpers.ErrorWi
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func parseLogStyle(value string, arg string) (api.LogStyle, *cli_helpers.ErrorWithNote) {
|
||||
switch value {
|
||||
case "default":
|
||||
return api.LogStyleDefault, nil
|
||||
case "visualstudio":
|
||||
return api.LogStyleVisualStudio, nil
|
||||
default:
|
||||
return api.LogStyleDefault, cli_helpers.MakeErrorWithNote(
|
||||
fmt.Sprintf("Invalid value %q in %q", value, arg),
|
||||
"Valid values are \"default\" or \"visualstudio\".",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+18
-1
@@ -7590,12 +7590,29 @@ let formatTests = {
|
||||
const messages = await esbuild.formatMessages([
|
||||
{ text: 'This is an error' },
|
||||
{ text: 'Another error', location: { file: 'file.js' } },
|
||||
{ text: 'Error with line and column', location: { file: 'file.js', line: 3, column: 6 } },
|
||||
], {
|
||||
kind: 'error',
|
||||
})
|
||||
assert.strictEqual(messages.length, 2)
|
||||
assert.strictEqual(messages.length, 3)
|
||||
assert.strictEqual(messages[0], `${errorIcon} [ERROR] This is an error\n\n`)
|
||||
assert.strictEqual(messages[1], `${errorIcon} [ERROR] Another error\n\n file.js:0:0:\n 0 │ \n ╵ ^\n\n`)
|
||||
assert.strictEqual(messages[2], `${errorIcon} [ERROR] Error with line and column\n\n file.js:3:6:\n 3 │ \n ╵ ^\n\n`)
|
||||
},
|
||||
|
||||
async formatMessagesVisualStudio({ esbuild }) {
|
||||
const messages = await esbuild.formatMessages([
|
||||
{ text: 'This is an error' },
|
||||
{ text: 'Another error', location: { file: 'file.js' } },
|
||||
{ text: 'Error with line and column', location: { file: 'file.js', line: 3, column: 6 } },
|
||||
], {
|
||||
kind: 'error',
|
||||
logStyle: 'visualstudio',
|
||||
})
|
||||
assert.strictEqual(messages.length, 3)
|
||||
assert.strictEqual(messages[0], `esbuild: error: This is an error\n`)
|
||||
assert.strictEqual(messages[1], `file.js: error: Another error\n`)
|
||||
assert.strictEqual(messages[2], `file.js(3,7): error: Error with line and column\n`)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user