github.com/evanw/esbuild@v0.21.4/internal/logger/logger.go (about)

     1  package logger
     2  
     3  // Logging is either done to stderr (via "NewStderrLog") or to an in-memory
     4  // array (via "NewDeferLog"). In-memory arrays are used to capture messages
     5  // from parsing individual files because during incremental builds, log
     6  // messages for a given file can be replayed from memory if the file ends up
     7  // not being reparsed.
     8  //
     9  // Errors are streamed asynchronously as they happen, each error contains the
    10  // contents of the line with the error, and the error count is limited by
    11  // default.
    12  
    13  import (
    14  	"encoding/binary"
    15  	"fmt"
    16  	"os"
    17  	"runtime"
    18  	"sort"
    19  	"strings"
    20  	"sync"
    21  	"time"
    22  	"unicode/utf8"
    23  )
    24  
    25  const defaultTerminalWidth = 80
    26  
    27  type Log struct {
    28  	AddMsg    func(Msg)
    29  	HasErrors func() bool
    30  	Peek      func() []Msg
    31  
    32  	Done func() []Msg
    33  
    34  	Level     LogLevel
    35  	Overrides map[MsgID]LogLevel
    36  }
    37  
    38  type LogLevel int8
    39  
    40  const (
    41  	LevelNone LogLevel = iota
    42  	LevelVerbose
    43  	LevelDebug
    44  	LevelInfo
    45  	LevelWarning
    46  	LevelError
    47  	LevelSilent
    48  )
    49  
    50  type MsgKind uint8
    51  
    52  const (
    53  	Error MsgKind = iota
    54  	Warning
    55  	Info
    56  	Note
    57  	Debug
    58  	Verbose
    59  )
    60  
    61  func (kind MsgKind) String() string {
    62  	switch kind {
    63  	case Error:
    64  		return "ERROR"
    65  	case Warning:
    66  		return "WARNING"
    67  	case Info:
    68  		return "INFO"
    69  	case Note:
    70  		return "NOTE"
    71  	case Debug:
    72  		return "DEBUG"
    73  	case Verbose:
    74  		return "VERBOSE"
    75  	default:
    76  		panic("Internal error")
    77  	}
    78  }
    79  
    80  func (kind MsgKind) Icon() string {
    81  	// Special-case Windows command prompt, which only supports a few characters
    82  	if isProbablyWindowsCommandPrompt() {
    83  		switch kind {
    84  		case Error:
    85  			return "X"
    86  		case Warning:
    87  			return "▲"
    88  		case Info:
    89  			return "►"
    90  		case Note:
    91  			return "→"
    92  		case Debug:
    93  			return "●"
    94  		case Verbose:
    95  			return "♦"
    96  		default:
    97  			panic("Internal error")
    98  		}
    99  	}
   100  
   101  	switch kind {
   102  	case Error:
   103  		return "✘"
   104  	case Warning:
   105  		return "▲"
   106  	case Info:
   107  		return "▶"
   108  	case Note:
   109  		return "→"
   110  	case Debug:
   111  		return "●"
   112  	case Verbose:
   113  		return "⬥"
   114  	default:
   115  		panic("Internal error")
   116  	}
   117  }
   118  
   119  var windowsCommandPrompt struct {
   120  	mutex         sync.Mutex
   121  	once          bool
   122  	isProbablyCMD bool
   123  }
   124  
   125  func isProbablyWindowsCommandPrompt() bool {
   126  	windowsCommandPrompt.mutex.Lock()
   127  	defer windowsCommandPrompt.mutex.Unlock()
   128  
   129  	if !windowsCommandPrompt.once {
   130  		windowsCommandPrompt.once = true
   131  
   132  		// Assume we are running in Windows Command Prompt if we're on Windows. If
   133  		// so, we can't use emoji because it won't be supported. Except we can
   134  		// still use emoji if the WT_SESSION environment variable is present
   135  		// because that means we're running in the new Windows Terminal instead.
   136  		if runtime.GOOS == "windows" {
   137  			windowsCommandPrompt.isProbablyCMD = true
   138  			if _, ok := os.LookupEnv("WT_SESSION"); ok {
   139  				windowsCommandPrompt.isProbablyCMD = false
   140  			}
   141  		}
   142  	}
   143  
   144  	return windowsCommandPrompt.isProbablyCMD
   145  }
   146  
   147  type Msg struct {
   148  	Notes      []MsgData
   149  	PluginName string
   150  	Data       MsgData
   151  	Kind       MsgKind
   152  	ID         MsgID
   153  }
   154  
   155  type MsgData struct {
   156  	// Optional user-specified data that is passed through unmodified
   157  	UserDetail interface{}
   158  
   159  	Location *MsgLocation
   160  	Text     string
   161  
   162  	DisableMaximumWidth bool
   163  }
   164  
   165  type MsgLocation struct {
   166  	File       string
   167  	Namespace  string
   168  	LineText   string
   169  	Suggestion string
   170  	Line       int // 1-based
   171  	Column     int // 0-based, in bytes
   172  	Length     int // in bytes
   173  }
   174  
   175  type Loc struct {
   176  	// This is the 0-based index of this location from the start of the file, in bytes
   177  	Start int32
   178  }
   179  
   180  type Range struct {
   181  	Loc Loc
   182  	Len int32
   183  }
   184  
   185  func (r Range) End() int32 {
   186  	return r.Loc.Start + r.Len
   187  }
   188  
   189  func (a *Range) ExpandBy(b Range) {
   190  	if a.Len == 0 {
   191  		*a = b
   192  	} else {
   193  		end := a.End()
   194  		if n := b.End(); n > end {
   195  			end = n
   196  		}
   197  		if b.Loc.Start < a.Loc.Start {
   198  			a.Loc.Start = b.Loc.Start
   199  		}
   200  		a.Len = end - a.Loc.Start
   201  	}
   202  }
   203  
   204  type Span struct {
   205  	Text  string
   206  	Range Range
   207  }
   208  
   209  // This type is just so we can use Go's native sort function
   210  type SortableMsgs []Msg
   211  
   212  func (a SortableMsgs) Len() int          { return len(a) }
   213  func (a SortableMsgs) Swap(i int, j int) { a[i], a[j] = a[j], a[i] }
   214  
   215  func (a SortableMsgs) Less(i int, j int) bool {
   216  	ai := a[i]
   217  	aj := a[j]
   218  	aiLoc := ai.Data.Location
   219  	ajLoc := aj.Data.Location
   220  	if aiLoc == nil || ajLoc == nil {
   221  		return aiLoc == nil && ajLoc != nil
   222  	}
   223  	if aiLoc.File != ajLoc.File {
   224  		return aiLoc.File < ajLoc.File
   225  	}
   226  	if aiLoc.Line != ajLoc.Line {
   227  		return aiLoc.Line < ajLoc.Line
   228  	}
   229  	if aiLoc.Column != ajLoc.Column {
   230  		return aiLoc.Column < ajLoc.Column
   231  	}
   232  	if ai.Kind != aj.Kind {
   233  		return ai.Kind < aj.Kind
   234  	}
   235  	return ai.Data.Text < aj.Data.Text
   236  }
   237  
   238  // This is used to represent both file system paths (Namespace == "file") and
   239  // abstract module paths (Namespace != "file"). Abstract module paths represent
   240  // "virtual modules" when used for an input file and "package paths" when used
   241  // to represent an external module.
   242  type Path struct {
   243  	Text      string
   244  	Namespace string
   245  
   246  	// This feature was added to support ancient CSS libraries that append things
   247  	// like "?#iefix" and "#icons" to some of their import paths as a hack for IE6.
   248  	// The intent is for these suffix parts to be ignored but passed through to
   249  	// the output. This is supported by other bundlers, so we also support this.
   250  	IgnoredSuffix string
   251  
   252  	// Import attributes (the "with" keyword after an import) can affect path
   253  	// resolution. In other words, two paths in the same file that are otherwise
   254  	// equal but that have different import attributes may resolve to different
   255  	// paths.
   256  	ImportAttributes ImportAttributes
   257  
   258  	Flags PathFlags
   259  }
   260  
   261  // We rely on paths as map keys. Go doesn't support custom hash codes and
   262  // only implements hash codes for certain types. In particular, hash codes
   263  // are implemented for strings but not for arrays of strings. So we have to
   264  // pack these import attributes into a string.
   265  type ImportAttributes struct {
   266  	packedData string
   267  }
   268  
   269  type ImportAttribute struct {
   270  	Key   string
   271  	Value string
   272  }
   273  
   274  // This returns a sorted array instead of a map to make determinism easier
   275  func (attrs ImportAttributes) DecodeIntoArray() (result []ImportAttribute) {
   276  	if attrs.packedData == "" {
   277  		return nil
   278  	}
   279  	bytes := []byte(attrs.packedData)
   280  	for len(bytes) > 0 {
   281  		kn := 4 + binary.LittleEndian.Uint32(bytes[:4])
   282  		k := string(bytes[4:kn])
   283  		bytes = bytes[kn:]
   284  		vn := 4 + binary.LittleEndian.Uint32(bytes[:4])
   285  		v := string(bytes[4:vn])
   286  		bytes = bytes[vn:]
   287  		result = append(result, ImportAttribute{Key: k, Value: v})
   288  	}
   289  	return result
   290  }
   291  
   292  func (attrs ImportAttributes) DecodeIntoMap() (result map[string]string) {
   293  	if array := attrs.DecodeIntoArray(); len(array) > 0 {
   294  		result = make(map[string]string, len(array))
   295  		for _, attr := range array {
   296  			result[attr.Key] = attr.Value
   297  		}
   298  	}
   299  	return
   300  }
   301  
   302  func EncodeImportAttributes(value map[string]string) ImportAttributes {
   303  	if len(value) == 0 {
   304  		return ImportAttributes{}
   305  	}
   306  	keys := make([]string, 0, len(value))
   307  	for k := range value {
   308  		keys = append(keys, k)
   309  	}
   310  	sort.Strings(keys)
   311  	var sb strings.Builder
   312  	var n [4]byte
   313  	for _, k := range keys {
   314  		v := value[k]
   315  		binary.LittleEndian.PutUint32(n[:], uint32(len(k)))
   316  		sb.Write(n[:])
   317  		sb.WriteString(k)
   318  		binary.LittleEndian.PutUint32(n[:], uint32(len(v)))
   319  		sb.Write(n[:])
   320  		sb.WriteString(v)
   321  	}
   322  	return ImportAttributes{packedData: sb.String()}
   323  }
   324  
   325  type PathFlags uint8
   326  
   327  const (
   328  	// This corresponds to a value of "false' in the "browser" package.json field
   329  	PathDisabled PathFlags = 1 << iota
   330  )
   331  
   332  func (p Path) IsDisabled() bool {
   333  	return (p.Flags & PathDisabled) != 0
   334  }
   335  
   336  var noColorResult bool
   337  var noColorOnce sync.Once
   338  
   339  func hasNoColorEnvironmentVariable() bool {
   340  	noColorOnce.Do(func() {
   341  		// Read "NO_COLOR" from the environment. This is a convention that some
   342  		// software follows. See https://no-color.org/ for more information.
   343  		if _, ok := os.LookupEnv("NO_COLOR"); ok {
   344  			noColorResult = true
   345  		}
   346  	})
   347  	return noColorResult
   348  }
   349  
   350  // This has a custom implementation instead of using "filepath.Dir/Base/Ext"
   351  // because it should work the same on Unix and Windows. These names end up in
   352  // the generated output and the generated output should not depend on the OS.
   353  func PlatformIndependentPathDirBaseExt(path string) (dir string, base string, ext string) {
   354  	absRootSlash := -1
   355  
   356  	// Make sure we don't strip off the slash for the root of the file system
   357  	if len(path) > 0 && (path[0] == '/' || path[0] == '\\') {
   358  		absRootSlash = 0 // Unix
   359  	} else if len(path) > 2 && path[1] == ':' && (path[2] == '/' || path[2] == '\\') {
   360  		if c := path[0]; (c >= 'a' && c < 'z') || (c >= 'A' && c <= 'Z') {
   361  			absRootSlash = 2 // Windows
   362  		}
   363  	}
   364  
   365  	for {
   366  		i := strings.LastIndexAny(path, "/\\")
   367  
   368  		// Stop if there are no more slashes
   369  		if i < 0 {
   370  			base = path
   371  			break
   372  		}
   373  
   374  		// Stop if we found a non-trailing slash
   375  		if i == absRootSlash {
   376  			dir, base = path[:i+1], path[i+1:]
   377  			break
   378  		}
   379  		if i+1 != len(path) {
   380  			dir, base = path[:i], path[i+1:]
   381  			break
   382  		}
   383  
   384  		// Ignore trailing slashes
   385  		path = path[:i]
   386  	}
   387  
   388  	// Strip off the extension
   389  	if dot := strings.LastIndexByte(base, '.'); dot >= 0 {
   390  		ext = base[dot:]
   391  
   392  		// We default to the "local-css" loader for ".module.css" files. Make sure
   393  		// the string names generated by this don't all have "_module_" in them.
   394  		if ext == ".css" {
   395  			if dot2 := strings.LastIndexByte(base[:dot], '.'); dot2 >= 0 && base[dot2:] == ".module.css" {
   396  				dot = dot2
   397  				ext = base[dot:]
   398  			}
   399  		}
   400  
   401  		base = base[:dot]
   402  	}
   403  	return
   404  }
   405  
   406  type Source struct {
   407  	// This is used for error messages and the metadata JSON file.
   408  	//
   409  	// This is a mostly platform-independent path. It's relative to the current
   410  	// working directory and always uses standard path separators. Use this for
   411  	// referencing a file in all output data. These paths still use the original
   412  	// case of the path so they may still work differently on file systems that
   413  	// are case-insensitive vs. case-sensitive.
   414  	PrettyPath string
   415  
   416  	// An identifier that is mixed in to automatically-generated symbol names to
   417  	// improve readability. For example, if the identifier is "util" then the
   418  	// symbol for an "export default" statement will be called "util_default".
   419  	IdentifierName string
   420  
   421  	Contents string
   422  
   423  	// This is used as a unique key to identify this source file. It should never
   424  	// be shown to the user (e.g. never print this to the terminal).
   425  	//
   426  	// If it's marked as an absolute path, it's a platform-dependent path that
   427  	// includes environment-specific things such as Windows backslash path
   428  	// separators and potentially the user's home directory. Only use this for
   429  	// passing to syscalls for reading and writing to the file system. Do not
   430  	// include this in any output data.
   431  	//
   432  	// If it's marked as not an absolute path, it's an opaque string that is used
   433  	// to refer to an automatically-generated module.
   434  	KeyPath Path
   435  
   436  	Index uint32
   437  }
   438  
   439  func (s *Source) TextForRange(r Range) string {
   440  	return s.Contents[r.Loc.Start : r.Loc.Start+r.Len]
   441  }
   442  
   443  func (s *Source) LocBeforeWhitespace(loc Loc) Loc {
   444  	for loc.Start > 0 {
   445  		c, width := utf8.DecodeLastRuneInString(s.Contents[:loc.Start])
   446  		if c != ' ' && c != '\t' && c != '\r' && c != '\n' {
   447  			break
   448  		}
   449  		loc.Start -= int32(width)
   450  	}
   451  	return loc
   452  }
   453  
   454  func (s *Source) RangeOfOperatorBefore(loc Loc, op string) Range {
   455  	text := s.Contents[:loc.Start]
   456  	index := strings.LastIndex(text, op)
   457  	if index >= 0 {
   458  		return Range{Loc: Loc{Start: int32(index)}, Len: int32(len(op))}
   459  	}
   460  	return Range{Loc: loc}
   461  }
   462  
   463  func (s *Source) RangeOfOperatorAfter(loc Loc, op string) Range {
   464  	text := s.Contents[loc.Start:]
   465  	index := strings.Index(text, op)
   466  	if index >= 0 {
   467  		return Range{Loc: Loc{Start: loc.Start + int32(index)}, Len: int32(len(op))}
   468  	}
   469  	return Range{Loc: loc}
   470  }
   471  
   472  func (s *Source) RangeOfString(loc Loc) Range {
   473  	text := s.Contents[loc.Start:]
   474  	if len(text) == 0 {
   475  		return Range{Loc: loc, Len: 0}
   476  	}
   477  
   478  	quote := text[0]
   479  	if quote == '"' || quote == '\'' {
   480  		// Search for the matching quote character
   481  		for i := 1; i < len(text); i++ {
   482  			c := text[i]
   483  			if c == quote {
   484  				return Range{Loc: loc, Len: int32(i + 1)}
   485  			} else if c == '\\' {
   486  				i += 1
   487  			}
   488  		}
   489  	}
   490  
   491  	if quote == '`' {
   492  		// Search for the matching quote character
   493  		for i := 1; i < len(text); i++ {
   494  			c := text[i]
   495  			if c == quote {
   496  				return Range{Loc: loc, Len: int32(i + 1)}
   497  			} else if c == '\\' {
   498  				i += 1
   499  			} else if c == '$' && i+1 < len(text) && text[i+1] == '{' {
   500  				break // Only return the range for no-substitution template literals
   501  			}
   502  		}
   503  	}
   504  
   505  	return Range{Loc: loc, Len: 0}
   506  }
   507  
   508  func (s *Source) RangeOfNumber(loc Loc) (r Range) {
   509  	text := s.Contents[loc.Start:]
   510  	r = Range{Loc: loc, Len: 0}
   511  
   512  	if len(text) > 0 {
   513  		if c := text[0]; c >= '0' && c <= '9' {
   514  			r.Len = 1
   515  			for int(r.Len) < len(text) {
   516  				c := text[r.Len]
   517  				if (c < '0' || c > '9') && (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && c != '.' && c != '_' {
   518  					break
   519  				}
   520  				r.Len++
   521  			}
   522  		}
   523  	}
   524  	return
   525  }
   526  
   527  func (s *Source) RangeOfLegacyOctalEscape(loc Loc) (r Range) {
   528  	text := s.Contents[loc.Start:]
   529  	r = Range{Loc: loc, Len: 0}
   530  
   531  	if len(text) >= 2 && text[0] == '\\' {
   532  		r.Len = 2
   533  		for r.Len < 4 && int(r.Len) < len(text) {
   534  			c := text[r.Len]
   535  			if c < '0' || c > '9' {
   536  				break
   537  			}
   538  			r.Len++
   539  		}
   540  	}
   541  	return
   542  }
   543  
   544  func (s *Source) CommentTextWithoutIndent(r Range) string {
   545  	text := s.Contents[r.Loc.Start:r.End()]
   546  	if len(text) < 2 || !strings.HasPrefix(text, "/*") {
   547  		return text
   548  	}
   549  	prefix := s.Contents[:r.Loc.Start]
   550  
   551  	// Figure out the initial indent
   552  	indent := 0
   553  seekBackwardToNewline:
   554  	for len(prefix) > 0 {
   555  		c, size := utf8.DecodeLastRuneInString(prefix)
   556  		switch c {
   557  		case '\r', '\n', '\u2028', '\u2029':
   558  			break seekBackwardToNewline
   559  		}
   560  		prefix = prefix[:len(prefix)-size]
   561  		indent++
   562  	}
   563  
   564  	// Split the comment into lines
   565  	var lines []string
   566  	start := 0
   567  	for i, c := range text {
   568  		switch c {
   569  		case '\r', '\n':
   570  			// Don't double-append for Windows style "\r\n" newlines
   571  			if start <= i {
   572  				lines = append(lines, text[start:i])
   573  			}
   574  
   575  			start = i + 1
   576  
   577  			// Ignore the second part of Windows style "\r\n" newlines
   578  			if c == '\r' && start < len(text) && text[start] == '\n' {
   579  				start++
   580  			}
   581  
   582  		case '\u2028', '\u2029':
   583  			lines = append(lines, text[start:i])
   584  			start = i + 3
   585  		}
   586  	}
   587  	lines = append(lines, text[start:])
   588  
   589  	// Find the minimum indent over all lines after the first line
   590  	for _, line := range lines[1:] {
   591  		lineIndent := 0
   592  		for _, c := range line {
   593  			if c != ' ' && c != '\t' {
   594  				break
   595  			}
   596  			lineIndent++
   597  		}
   598  		if indent > lineIndent {
   599  			indent = lineIndent
   600  		}
   601  	}
   602  
   603  	// Trim the indent off of all lines after the first line
   604  	for i, line := range lines {
   605  		if i > 0 {
   606  			lines[i] = line[indent:]
   607  		}
   608  	}
   609  	return strings.Join(lines, "\n")
   610  }
   611  
   612  func plural(prefix string, count int, shown int, someAreMissing bool) string {
   613  	var text string
   614  	if count == 1 {
   615  		text = fmt.Sprintf("%d %s", count, prefix)
   616  	} else {
   617  		text = fmt.Sprintf("%d %ss", count, prefix)
   618  	}
   619  	if shown < count {
   620  		text = fmt.Sprintf("%d of %s", shown, text)
   621  	} else if someAreMissing && count > 1 {
   622  		text = "all " + text
   623  	}
   624  	return text
   625  }
   626  
   627  func errorAndWarningSummary(errors int, warnings int, shownErrors int, shownWarnings int) string {
   628  	someAreMissing := shownWarnings < warnings || shownErrors < errors
   629  	switch {
   630  	case errors == 0:
   631  		return plural("warning", warnings, shownWarnings, someAreMissing)
   632  	case warnings == 0:
   633  		return plural("error", errors, shownErrors, someAreMissing)
   634  	default:
   635  		return fmt.Sprintf("%s and %s",
   636  			plural("warning", warnings, shownWarnings, someAreMissing),
   637  			plural("error", errors, shownErrors, someAreMissing))
   638  	}
   639  }
   640  
   641  type APIKind uint8
   642  
   643  const (
   644  	GoAPI APIKind = iota
   645  	CLIAPI
   646  	JSAPI
   647  )
   648  
   649  // This can be used to customize error messages for the current API kind
   650  var API APIKind
   651  
   652  type TerminalInfo struct {
   653  	IsTTY           bool
   654  	UseColorEscapes bool
   655  	Width           int
   656  	Height          int
   657  }
   658  
   659  func NewStderrLog(options OutputOptions) Log {
   660  	var mutex sync.Mutex
   661  	var msgs SortableMsgs
   662  	terminalInfo := GetTerminalInfo(os.Stderr)
   663  	errors := 0
   664  	warnings := 0
   665  	shownErrors := 0
   666  	shownWarnings := 0
   667  	hasErrors := false
   668  	remainingMessagesBeforeLimit := options.MessageLimit
   669  	if remainingMessagesBeforeLimit == 0 {
   670  		remainingMessagesBeforeLimit = 0x7FFFFFFF
   671  	}
   672  	var deferredWarnings []Msg
   673  
   674  	finalizeLog := func() {
   675  		// Print the deferred warning now if there was no error after all
   676  		for remainingMessagesBeforeLimit > 0 && len(deferredWarnings) > 0 {
   677  			shownWarnings++
   678  			writeStringWithColor(os.Stderr, deferredWarnings[0].String(options, terminalInfo))
   679  			deferredWarnings = deferredWarnings[1:]
   680  			remainingMessagesBeforeLimit--
   681  		}
   682  
   683  		// Print out a summary
   684  		if options.MessageLimit > 0 && errors+warnings > options.MessageLimit {
   685  			writeStringWithColor(os.Stderr, fmt.Sprintf("%s shown (disable the message limit with --log-limit=0)\n",
   686  				errorAndWarningSummary(errors, warnings, shownErrors, shownWarnings)))
   687  		} else if options.LogLevel <= LevelInfo && (warnings != 0 || errors != 0) {
   688  			writeStringWithColor(os.Stderr, fmt.Sprintf("%s\n",
   689  				errorAndWarningSummary(errors, warnings, shownErrors, shownWarnings)))
   690  		}
   691  	}
   692  
   693  	switch options.Color {
   694  	case ColorNever:
   695  		terminalInfo.UseColorEscapes = false
   696  	case ColorAlways:
   697  		terminalInfo.UseColorEscapes = SupportsColorEscapes
   698  	}
   699  
   700  	return Log{
   701  		Level:     options.LogLevel,
   702  		Overrides: options.Overrides,
   703  
   704  		AddMsg: func(msg Msg) {
   705  			mutex.Lock()
   706  			defer mutex.Unlock()
   707  			msgs = append(msgs, msg)
   708  
   709  			switch msg.Kind {
   710  			case Verbose:
   711  				if options.LogLevel <= LevelVerbose {
   712  					writeStringWithColor(os.Stderr, msg.String(options, terminalInfo))
   713  				}
   714  
   715  			case Debug:
   716  				if options.LogLevel <= LevelDebug {
   717  					writeStringWithColor(os.Stderr, msg.String(options, terminalInfo))
   718  				}
   719  
   720  			case Info:
   721  				if options.LogLevel <= LevelInfo {
   722  					writeStringWithColor(os.Stderr, msg.String(options, terminalInfo))
   723  				}
   724  
   725  			case Error:
   726  				hasErrors = true
   727  				if options.LogLevel <= LevelError {
   728  					errors++
   729  				}
   730  
   731  			case Warning:
   732  				if options.LogLevel <= LevelWarning {
   733  					warnings++
   734  				}
   735  			}
   736  
   737  			// Be silent if we're past the limit so we don't flood the terminal
   738  			if remainingMessagesBeforeLimit == 0 {
   739  				return
   740  			}
   741  
   742  			switch msg.Kind {
   743  			case Error:
   744  				if options.LogLevel <= LevelError {
   745  					shownErrors++
   746  					writeStringWithColor(os.Stderr, msg.String(options, terminalInfo))
   747  					remainingMessagesBeforeLimit--
   748  				}
   749  
   750  			case Warning:
   751  				if options.LogLevel <= LevelWarning {
   752  					if remainingMessagesBeforeLimit > (options.MessageLimit+1)/2 {
   753  						shownWarnings++
   754  						writeStringWithColor(os.Stderr, msg.String(options, terminalInfo))
   755  						remainingMessagesBeforeLimit--
   756  					} else {
   757  						// If we have less than half of the slots left, wait for potential
   758  						// future errors instead of using up all of the slots with warnings.
   759  						// We want the log for a failed build to always have at least one
   760  						// error in it.
   761  						deferredWarnings = append(deferredWarnings, msg)
   762  					}
   763  				}
   764  			}
   765  		},
   766  
   767  		HasErrors: func() bool {
   768  			mutex.Lock()
   769  			defer mutex.Unlock()
   770  			return hasErrors
   771  		},
   772  
   773  		Peek: func() []Msg {
   774  			mutex.Lock()
   775  			defer mutex.Unlock()
   776  			sort.Stable(msgs)
   777  			return append([]Msg{}, msgs...)
   778  		},
   779  
   780  		Done: func() []Msg {
   781  			mutex.Lock()
   782  			defer mutex.Unlock()
   783  			finalizeLog()
   784  			sort.Stable(msgs)
   785  			return msgs
   786  		},
   787  	}
   788  }
   789  
   790  func PrintErrorToStderr(osArgs []string, text string) {
   791  	PrintMessageToStderr(osArgs, Msg{Kind: Error, Data: MsgData{Text: text}})
   792  }
   793  
   794  func PrintErrorWithNoteToStderr(osArgs []string, text string, note string) {
   795  	msg := Msg{
   796  		Kind: Error,
   797  		Data: MsgData{Text: text},
   798  	}
   799  	if note != "" {
   800  		msg.Notes = []MsgData{{Text: note}}
   801  	}
   802  	PrintMessageToStderr(osArgs, msg)
   803  }
   804  
   805  func OutputOptionsForArgs(osArgs []string) OutputOptions {
   806  	options := OutputOptions{IncludeSource: true}
   807  
   808  	// Implement a mini argument parser so these options always work even if we
   809  	// haven't yet gotten to the general-purpose argument parsing code
   810  	for _, arg := range osArgs {
   811  		switch arg {
   812  		case "--color=false":
   813  			options.Color = ColorNever
   814  		case "--color=true", "--color":
   815  			options.Color = ColorAlways
   816  		case "--log-level=info":
   817  			options.LogLevel = LevelInfo
   818  		case "--log-level=warning":
   819  			options.LogLevel = LevelWarning
   820  		case "--log-level=error":
   821  			options.LogLevel = LevelError
   822  		case "--log-level=silent":
   823  			options.LogLevel = LevelSilent
   824  		}
   825  	}
   826  
   827  	return options
   828  }
   829  
   830  func PrintMessageToStderr(osArgs []string, msg Msg) {
   831  	log := NewStderrLog(OutputOptionsForArgs(osArgs))
   832  	log.AddMsg(msg)
   833  	log.Done()
   834  }
   835  
   836  type Colors struct {
   837  	Reset     string
   838  	Bold      string
   839  	Dim       string
   840  	Underline string
   841  
   842  	Red   string
   843  	Green string
   844  	Blue  string
   845  
   846  	Cyan    string
   847  	Magenta string
   848  	Yellow  string
   849  
   850  	RedBgRed     string
   851  	RedBgWhite   string
   852  	GreenBgGreen string
   853  	GreenBgWhite string
   854  	BlueBgBlue   string
   855  	BlueBgWhite  string
   856  
   857  	CyanBgCyan       string
   858  	CyanBgBlack      string
   859  	MagentaBgMagenta string
   860  	MagentaBgBlack   string
   861  	YellowBgYellow   string
   862  	YellowBgBlack    string
   863  }
   864  
   865  var TerminalColors = Colors{
   866  	Reset:     "\033[0m",
   867  	Bold:      "\033[1m",
   868  	Dim:       "\033[37m",
   869  	Underline: "\033[4m",
   870  
   871  	Red:   "\033[31m",
   872  	Green: "\033[32m",
   873  	Blue:  "\033[34m",
   874  
   875  	Cyan:    "\033[36m",
   876  	Magenta: "\033[35m",
   877  	Yellow:  "\033[33m",
   878  
   879  	RedBgRed:     "\033[41;31m",
   880  	RedBgWhite:   "\033[41;97m",
   881  	GreenBgGreen: "\033[42;32m",
   882  	GreenBgWhite: "\033[42;97m",
   883  	BlueBgBlue:   "\033[44;34m",
   884  	BlueBgWhite:  "\033[44;97m",
   885  
   886  	CyanBgCyan:       "\033[46;36m",
   887  	CyanBgBlack:      "\033[46;30m",
   888  	MagentaBgMagenta: "\033[45;35m",
   889  	MagentaBgBlack:   "\033[45;30m",
   890  	YellowBgYellow:   "\033[43;33m",
   891  	YellowBgBlack:    "\033[43;30m",
   892  }
   893  
   894  func PrintText(file *os.File, level LogLevel, osArgs []string, callback func(Colors) string) {
   895  	options := OutputOptionsForArgs(osArgs)
   896  
   897  	// Skip logging these if these logs are disabled
   898  	if options.LogLevel > level {
   899  		return
   900  	}
   901  
   902  	PrintTextWithColor(file, options.Color, callback)
   903  }
   904  
   905  func PrintTextWithColor(file *os.File, useColor UseColor, callback func(Colors) string) {
   906  	var useColorEscapes bool
   907  	switch useColor {
   908  	case ColorNever:
   909  		useColorEscapes = false
   910  	case ColorAlways:
   911  		useColorEscapes = SupportsColorEscapes
   912  	case ColorIfTerminal:
   913  		useColorEscapes = GetTerminalInfo(file).UseColorEscapes
   914  	}
   915  
   916  	var colors Colors
   917  	if useColorEscapes {
   918  		colors = TerminalColors
   919  	}
   920  	writeStringWithColor(file, callback(colors))
   921  }
   922  
   923  type SummaryTableEntry struct {
   924  	Dir         string
   925  	Base        string
   926  	Size        string
   927  	Bytes       int
   928  	IsSourceMap bool
   929  }
   930  
   931  // This type is just so we can use Go's native sort function
   932  type SummaryTable []SummaryTableEntry
   933  
   934  func (t SummaryTable) Len() int          { return len(t) }
   935  func (t SummaryTable) Swap(i int, j int) { t[i], t[j] = t[j], t[i] }
   936  
   937  func (t SummaryTable) Less(i int, j int) bool {
   938  	ti := t[i]
   939  	tj := t[j]
   940  
   941  	// Sort source maps last
   942  	if !ti.IsSourceMap && tj.IsSourceMap {
   943  		return true
   944  	}
   945  	if ti.IsSourceMap && !tj.IsSourceMap {
   946  		return false
   947  	}
   948  
   949  	// Sort by size first
   950  	if ti.Bytes > tj.Bytes {
   951  		return true
   952  	}
   953  	if ti.Bytes < tj.Bytes {
   954  		return false
   955  	}
   956  
   957  	// Sort alphabetically by directory first
   958  	if ti.Dir < tj.Dir {
   959  		return true
   960  	}
   961  	if ti.Dir > tj.Dir {
   962  		return false
   963  	}
   964  
   965  	// Then sort alphabetically by file name
   966  	return ti.Base < tj.Base
   967  }
   968  
   969  // Show a warning icon next to output files that are 1mb or larger
   970  const sizeWarningThreshold = 1024 * 1024
   971  
   972  func PrintSummary(useColor UseColor, table SummaryTable, start *time.Time) {
   973  	PrintTextWithColor(os.Stderr, useColor, func(colors Colors) string {
   974  		isProbablyWindowsCommandPrompt := isProbablyWindowsCommandPrompt()
   975  		sb := strings.Builder{}
   976  
   977  		if len(table) > 0 {
   978  			info := GetTerminalInfo(os.Stderr)
   979  
   980  			// Truncate the table in case it's really long
   981  			maxLength := info.Height / 2
   982  			if info.Height == 0 {
   983  				maxLength = 20
   984  			} else if maxLength < 5 {
   985  				maxLength = 5
   986  			}
   987  			length := len(table)
   988  			sort.Sort(table)
   989  			if length > maxLength {
   990  				table = table[:maxLength]
   991  			}
   992  
   993  			// Compute the maximum width of the size column
   994  			spacingBetweenColumns := 2
   995  			hasSizeWarning := false
   996  			maxPath := 0
   997  			maxSize := 0
   998  			for _, entry := range table {
   999  				path := len(entry.Dir) + len(entry.Base)
  1000  				size := len(entry.Size) + spacingBetweenColumns
  1001  				if path > maxPath {
  1002  					maxPath = path
  1003  				}
  1004  				if size > maxSize {
  1005  					maxSize = size
  1006  				}
  1007  				if !entry.IsSourceMap && entry.Bytes >= sizeWarningThreshold {
  1008  					hasSizeWarning = true
  1009  				}
  1010  			}
  1011  
  1012  			margin := "  "
  1013  			layoutWidth := info.Width
  1014  			if layoutWidth < 1 {
  1015  				layoutWidth = defaultTerminalWidth
  1016  			}
  1017  			layoutWidth -= 2 * len(margin)
  1018  			if hasSizeWarning {
  1019  				// Add space for the warning icon
  1020  				layoutWidth -= 2
  1021  			}
  1022  			if layoutWidth > maxPath+maxSize {
  1023  				layoutWidth = maxPath + maxSize
  1024  			}
  1025  			sb.WriteByte('\n')
  1026  
  1027  			for _, entry := range table {
  1028  				dir, base := entry.Dir, entry.Base
  1029  				pathWidth := layoutWidth - maxSize
  1030  
  1031  				// Truncate the path with "..." to fit on one line
  1032  				if len(dir)+len(base) > pathWidth {
  1033  					// Trim the directory from the front, leaving the trailing slash
  1034  					if len(dir) > 0 {
  1035  						n := pathWidth - len(base) - 3
  1036  						if n < 1 {
  1037  							n = 1
  1038  						}
  1039  						dir = "..." + dir[len(dir)-n:]
  1040  					}
  1041  
  1042  					// Trim the file name from the back
  1043  					if len(dir)+len(base) > pathWidth {
  1044  						n := pathWidth - len(dir) - 3
  1045  						if n < 0 {
  1046  							n = 0
  1047  						}
  1048  						base = base[:n] + "..."
  1049  					}
  1050  				}
  1051  
  1052  				spacer := layoutWidth - len(entry.Size) - len(dir) - len(base)
  1053  				if spacer < 0 {
  1054  					spacer = 0
  1055  				}
  1056  
  1057  				// Put a warning next to the size if it's above a certain threshold
  1058  				sizeColor := colors.Cyan
  1059  				sizeWarning := ""
  1060  				if !entry.IsSourceMap && entry.Bytes >= sizeWarningThreshold {
  1061  					sizeColor = colors.Yellow
  1062  
  1063  					// Emoji don't work in Windows Command Prompt
  1064  					if !isProbablyWindowsCommandPrompt {
  1065  						sizeWarning = " ⚠️"
  1066  					}
  1067  				}
  1068  
  1069  				sb.WriteString(fmt.Sprintf("%s%s%s%s%s%s%s%s%s%s%s%s\n",
  1070  					margin,
  1071  					colors.Dim,
  1072  					dir,
  1073  					colors.Reset,
  1074  					colors.Bold,
  1075  					base,
  1076  					colors.Reset,
  1077  					strings.Repeat(" ", spacer),
  1078  					sizeColor,
  1079  					entry.Size,
  1080  					sizeWarning,
  1081  					colors.Reset,
  1082  				))
  1083  			}
  1084  
  1085  			// Say how many remaining files are not shown
  1086  			if length > maxLength {
  1087  				plural := "s"
  1088  				if length == maxLength+1 {
  1089  					plural = ""
  1090  				}
  1091  				sb.WriteString(fmt.Sprintf("%s%s...and %d more output file%s...%s\n", margin, colors.Dim, length-maxLength, plural, colors.Reset))
  1092  			}
  1093  		}
  1094  		sb.WriteByte('\n')
  1095  
  1096  		lightningSymbol := "⚡ "
  1097  
  1098  		// Emoji don't work in Windows Command Prompt
  1099  		if isProbablyWindowsCommandPrompt {
  1100  			lightningSymbol = ""
  1101  		}
  1102  
  1103  		// Printing the time taken is optional
  1104  		if start != nil {
  1105  			sb.WriteString(fmt.Sprintf("%s%sDone in %dms%s\n",
  1106  				lightningSymbol,
  1107  				colors.Green,
  1108  				time.Since(*start).Milliseconds(),
  1109  				colors.Reset,
  1110  			))
  1111  		}
  1112  
  1113  		return sb.String()
  1114  	})
  1115  }
  1116  
  1117  type DeferLogKind uint8
  1118  
  1119  const (
  1120  	DeferLogAll DeferLogKind = iota
  1121  	DeferLogNoVerboseOrDebug
  1122  )
  1123  
  1124  func NewDeferLog(kind DeferLogKind, overrides map[MsgID]LogLevel) Log {
  1125  	var msgs SortableMsgs
  1126  	var mutex sync.Mutex
  1127  	var hasErrors bool
  1128  
  1129  	return Log{
  1130  		Level:     LevelInfo,
  1131  		Overrides: overrides,
  1132  
  1133  		AddMsg: func(msg Msg) {
  1134  			if kind == DeferLogNoVerboseOrDebug && (msg.Kind == Verbose || msg.Kind == Debug) {
  1135  				return
  1136  			}
  1137  			mutex.Lock()
  1138  			defer mutex.Unlock()
  1139  			if msg.Kind == Error {
  1140  				hasErrors = true
  1141  			}
  1142  			msgs = append(msgs, msg)
  1143  		},
  1144  
  1145  		HasErrors: func() bool {
  1146  			mutex.Lock()
  1147  			defer mutex.Unlock()
  1148  			return hasErrors
  1149  		},
  1150  
  1151  		Peek: func() []Msg {
  1152  			mutex.Lock()
  1153  			defer mutex.Unlock()
  1154  			return append([]Msg{}, msgs...)
  1155  		},
  1156  
  1157  		Done: func() []Msg {
  1158  			mutex.Lock()
  1159  			defer mutex.Unlock()
  1160  			sort.Stable(msgs)
  1161  			return msgs
  1162  		},
  1163  	}
  1164  }
  1165  
  1166  type UseColor uint8
  1167  
  1168  const (
  1169  	ColorIfTerminal UseColor = iota
  1170  	ColorNever
  1171  	ColorAlways
  1172  )
  1173  
  1174  type OutputOptions struct {
  1175  	MessageLimit  int
  1176  	IncludeSource bool
  1177  	Color         UseColor
  1178  	LogLevel      LogLevel
  1179  	Overrides     map[MsgID]LogLevel
  1180  }
  1181  
  1182  func (msg Msg) String(options OutputOptions, terminalInfo TerminalInfo) string {
  1183  	// Format the message
  1184  	text := msgString(options.IncludeSource, terminalInfo, msg.ID, msg.Kind, msg.Data, msg.PluginName)
  1185  
  1186  	// Format the notes
  1187  	var oldData MsgData
  1188  	for i, note := range msg.Notes {
  1189  		if options.IncludeSource && (i == 0 || strings.IndexByte(oldData.Text, '\n') >= 0 || oldData.Location != nil) {
  1190  			text += "\n"
  1191  		}
  1192  		text += msgString(options.IncludeSource, terminalInfo, MsgID_None, Note, note, "")
  1193  		oldData = note
  1194  	}
  1195  
  1196  	// Add extra spacing between messages if source code is present
  1197  	if options.IncludeSource {
  1198  		text += "\n"
  1199  	}
  1200  	return text
  1201  }
  1202  
  1203  // The number of margin characters in addition to the line number
  1204  const extraMarginChars = 9
  1205  
  1206  func marginWithLineText(maxMargin int, line int) string {
  1207  	number := fmt.Sprintf("%d", line)
  1208  	return fmt.Sprintf("      %s%s │ ", strings.Repeat(" ", maxMargin-len(number)), number)
  1209  }
  1210  
  1211  func emptyMarginText(maxMargin int, isLast bool) string {
  1212  	space := strings.Repeat(" ", maxMargin)
  1213  	if isLast {
  1214  		return fmt.Sprintf("      %s ╵ ", space)
  1215  	}
  1216  	return fmt.Sprintf("      %s │ ", space)
  1217  }
  1218  
  1219  func msgString(includeSource bool, terminalInfo TerminalInfo, id MsgID, kind MsgKind, data MsgData, pluginName string) string {
  1220  	if !includeSource {
  1221  		if loc := data.Location; loc != nil {
  1222  			return fmt.Sprintf("%s: %s: %s\n", loc.File, kind.String(), data.Text)
  1223  		}
  1224  		return fmt.Sprintf("%s: %s\n", kind.String(), data.Text)
  1225  	}
  1226  
  1227  	var colors Colors
  1228  	if terminalInfo.UseColorEscapes {
  1229  		colors = TerminalColors
  1230  	}
  1231  
  1232  	var iconColor string
  1233  	var kindColorBrackets string
  1234  	var kindColorText string
  1235  
  1236  	location := ""
  1237  
  1238  	if data.Location != nil {
  1239  		maxMargin := len(fmt.Sprintf("%d", data.Location.Line))
  1240  		d := detailStruct(data, terminalInfo, maxMargin)
  1241  
  1242  		if d.Suggestion != "" {
  1243  			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",
  1244  				d.Path, d.Line, d.Column,
  1245  				colors.Dim, d.SourceBefore, colors.Green, d.SourceMarked, colors.Dim, d.SourceAfter,
  1246  				emptyMarginText(maxMargin, false), d.Indent, colors.Green, d.Marker, colors.Dim,
  1247  				emptyMarginText(maxMargin, true), d.Indent, colors.Green, d.Suggestion, colors.Reset,
  1248  				d.ContentAfter,
  1249  			)
  1250  		} else {
  1251  			location = fmt.Sprintf("\n    %s:%d:%d:\n%s%s%s%s%s%s\n%s%s%s%s%s\n%s",
  1252  				d.Path, d.Line, d.Column,
  1253  				colors.Dim, d.SourceBefore, colors.Green, d.SourceMarked, colors.Dim, d.SourceAfter,
  1254  				emptyMarginText(maxMargin, true), d.Indent, colors.Green, d.Marker, colors.Reset,
  1255  				d.ContentAfter,
  1256  			)
  1257  		}
  1258  	}
  1259  
  1260  	switch kind {
  1261  	case Verbose:
  1262  		iconColor = colors.Cyan
  1263  		kindColorBrackets = colors.CyanBgCyan
  1264  		kindColorText = colors.CyanBgBlack
  1265  
  1266  	case Debug:
  1267  		iconColor = colors.Green
  1268  		kindColorBrackets = colors.GreenBgGreen
  1269  		kindColorText = colors.GreenBgWhite
  1270  
  1271  	case Info:
  1272  		iconColor = colors.Blue
  1273  		kindColorBrackets = colors.BlueBgBlue
  1274  		kindColorText = colors.BlueBgWhite
  1275  
  1276  	case Error:
  1277  		iconColor = colors.Red
  1278  		kindColorBrackets = colors.RedBgRed
  1279  		kindColorText = colors.RedBgWhite
  1280  
  1281  	case Warning:
  1282  		iconColor = colors.Yellow
  1283  		kindColorBrackets = colors.YellowBgYellow
  1284  		kindColorText = colors.YellowBgBlack
  1285  
  1286  	case Note:
  1287  		sb := strings.Builder{}
  1288  
  1289  		for _, line := range strings.Split(data.Text, "\n") {
  1290  			// Special-case word wrapping
  1291  			if wrapWidth := terminalInfo.Width; wrapWidth > 2 {
  1292  				if !data.DisableMaximumWidth && wrapWidth > 100 {
  1293  					wrapWidth = 100 // Enforce a maximum paragraph width for readability
  1294  				}
  1295  				for _, run := range wrapWordsInString(line, wrapWidth-2) {
  1296  					sb.WriteString("  ")
  1297  					sb.WriteString(linkifyText(run, colors.Underline, colors.Reset))
  1298  					sb.WriteByte('\n')
  1299  				}
  1300  				continue
  1301  			}
  1302  
  1303  			// Otherwise, just write an indented line
  1304  			sb.WriteString("  ")
  1305  			sb.WriteString(linkifyText(line, colors.Underline, colors.Reset))
  1306  			sb.WriteByte('\n')
  1307  		}
  1308  
  1309  		sb.WriteString(location)
  1310  		return sb.String()
  1311  	}
  1312  
  1313  	if pluginName != "" {
  1314  		pluginName = fmt.Sprintf(" %s%s[plugin %s]%s", colors.Bold, colors.Magenta, pluginName, colors.Reset)
  1315  	}
  1316  
  1317  	msgID := MsgIDToString(id)
  1318  	if msgID != "" {
  1319  		msgID = fmt.Sprintf(" [%s]", msgID)
  1320  	}
  1321  
  1322  	return fmt.Sprintf("%s%s %s[%s%s%s]%s %s%s%s%s%s\n%s",
  1323  		iconColor, kind.Icon(),
  1324  		kindColorBrackets, kindColorText, kind.String(), kindColorBrackets, colors.Reset,
  1325  		colors.Bold, data.Text, colors.Reset, pluginName, msgID,
  1326  		location,
  1327  	)
  1328  }
  1329  
  1330  func linkifyText(text string, underline string, reset string) string {
  1331  	if underline == "" {
  1332  		return text
  1333  	}
  1334  
  1335  	https := strings.Index(text, "https://")
  1336  	if https == -1 {
  1337  		return text
  1338  	}
  1339  
  1340  	sb := strings.Builder{}
  1341  	for {
  1342  		https := strings.Index(text, "https://")
  1343  		if https == -1 {
  1344  			break
  1345  		}
  1346  
  1347  		end := strings.IndexByte(text[https:], ' ')
  1348  		if end == -1 {
  1349  			end = len(text)
  1350  		} else {
  1351  			end += https
  1352  		}
  1353  
  1354  		// Remove trailing punctuation
  1355  		if end > https {
  1356  			switch text[end-1] {
  1357  			case '.', ',', '?', '!', ')', ']', '}':
  1358  				end--
  1359  			}
  1360  		}
  1361  
  1362  		sb.WriteString(text[:https])
  1363  		sb.WriteString(underline)
  1364  		sb.WriteString(text[https:end])
  1365  		sb.WriteString(reset)
  1366  		text = text[end:]
  1367  	}
  1368  
  1369  	sb.WriteString(text)
  1370  	return sb.String()
  1371  }
  1372  
  1373  func wrapWordsInString(text string, width int) []string {
  1374  	runs := []string{}
  1375  
  1376  outer:
  1377  	for text != "" {
  1378  		i := 0
  1379  		x := 0
  1380  		wordEndI := 0
  1381  
  1382  		// Skip over any leading spaces
  1383  		for i < len(text) && text[i] == ' ' {
  1384  			i++
  1385  			x++
  1386  		}
  1387  
  1388  		// Find out how many words will fit in this run
  1389  		for i < len(text) {
  1390  			oldWordEndI := wordEndI
  1391  			wordStartI := i
  1392  
  1393  			// Find the end of the word
  1394  			for i < len(text) {
  1395  				c, width := utf8.DecodeRuneInString(text[i:])
  1396  				if c == ' ' {
  1397  					break
  1398  				}
  1399  				i += width
  1400  				x += 1 // Naively assume that each unicode code point is a single column
  1401  			}
  1402  			wordEndI = i
  1403  
  1404  			// Split into a new run if this isn't the first word in the run and the end is past the width
  1405  			if wordStartI > 0 && x > width {
  1406  				runs = append(runs, text[:oldWordEndI])
  1407  				text = text[wordStartI:]
  1408  				continue outer
  1409  			}
  1410  
  1411  			// Skip over any spaces after the word
  1412  			for i < len(text) && text[i] == ' ' {
  1413  				i++
  1414  				x++
  1415  			}
  1416  		}
  1417  
  1418  		// If we get here, this is the last run (i.e. everything fits)
  1419  		break
  1420  	}
  1421  
  1422  	// Remove any trailing spaces on the last run
  1423  	for len(text) > 0 && text[len(text)-1] == ' ' {
  1424  		text = text[:len(text)-1]
  1425  	}
  1426  	runs = append(runs, text)
  1427  	return runs
  1428  }
  1429  
  1430  type MsgDetail struct {
  1431  	SourceBefore string
  1432  	SourceMarked string
  1433  	SourceAfter  string
  1434  
  1435  	Indent     string
  1436  	Marker     string
  1437  	Suggestion string
  1438  
  1439  	ContentAfter string
  1440  
  1441  	Path   string
  1442  	Line   int
  1443  	Column int
  1444  }
  1445  
  1446  // It's not common for large files to have many warnings. But when it happens,
  1447  // we want to make sure that it's not too slow. Source code locations are
  1448  // represented as byte offsets for compactness but transforming these to
  1449  // line/column locations for warning messages requires scanning through the
  1450  // file. A naive approach for this would cause O(n^2) scanning time for n
  1451  // warnings distributed throughout the file.
  1452  //
  1453  // Warnings are typically generated sequentially as the file is scanned. So
  1454  // one way of optimizing this is to just start scanning from where we left
  1455  // off last time instead of always starting from the beginning of the file.
  1456  // That's what this object does.
  1457  //
  1458  // Another option could be to eagerly populate an array of line/column offsets
  1459  // and then use binary search for each query. This might slow down the common
  1460  // case of a file with only at most a few warnings though, so think before
  1461  // optimizing too much. Performance in the zero or one warning case is by far
  1462  // the most important.
  1463  type LineColumnTracker struct {
  1464  	contents     string
  1465  	prettyPath   string
  1466  	offset       int32
  1467  	line         int32
  1468  	lineStart    int32
  1469  	lineEnd      int32
  1470  	hasLineStart bool
  1471  	hasLineEnd   bool
  1472  	hasSource    bool
  1473  }
  1474  
  1475  func MakeLineColumnTracker(source *Source) LineColumnTracker {
  1476  	if source == nil {
  1477  		return LineColumnTracker{
  1478  			hasSource: false,
  1479  		}
  1480  	}
  1481  
  1482  	return LineColumnTracker{
  1483  		contents:     source.Contents,
  1484  		prettyPath:   source.PrettyPath,
  1485  		hasLineStart: true,
  1486  		hasSource:    true,
  1487  	}
  1488  }
  1489  
  1490  func (tracker *LineColumnTracker) MsgData(r Range, text string) MsgData {
  1491  	return MsgData{
  1492  		Text:     text,
  1493  		Location: tracker.MsgLocationOrNil(r),
  1494  	}
  1495  }
  1496  
  1497  func (t *LineColumnTracker) scanTo(offset int32) {
  1498  	contents := t.contents
  1499  	i := t.offset
  1500  
  1501  	// Scan forward
  1502  	if i < offset {
  1503  		for {
  1504  			r, size := utf8.DecodeRuneInString(contents[i:])
  1505  			i += int32(size)
  1506  
  1507  			switch r {
  1508  			case '\n':
  1509  				t.hasLineStart = true
  1510  				t.hasLineEnd = false
  1511  				t.lineStart = i
  1512  				if i == int32(size) || contents[i-int32(size)-1] != '\r' {
  1513  					t.line++
  1514  				}
  1515  
  1516  			case '\r', '\u2028', '\u2029':
  1517  				t.hasLineStart = true
  1518  				t.hasLineEnd = false
  1519  				t.lineStart = i
  1520  				t.line++
  1521  			}
  1522  
  1523  			if i >= offset {
  1524  				t.offset = i
  1525  				return
  1526  			}
  1527  		}
  1528  	}
  1529  
  1530  	// Scan backward
  1531  	if i > offset {
  1532  		for {
  1533  			r, size := utf8.DecodeLastRuneInString(contents[:i])
  1534  			i -= int32(size)
  1535  
  1536  			switch r {
  1537  			case '\n':
  1538  				t.hasLineStart = false
  1539  				t.hasLineEnd = true
  1540  				t.lineEnd = i
  1541  				if i == 0 || contents[i-1] != '\r' {
  1542  					t.line--
  1543  				}
  1544  
  1545  			case '\r', '\u2028', '\u2029':
  1546  				t.hasLineStart = false
  1547  				t.hasLineEnd = true
  1548  				t.lineEnd = i
  1549  				t.line--
  1550  			}
  1551  
  1552  			if i <= offset {
  1553  				t.offset = i
  1554  				return
  1555  			}
  1556  		}
  1557  	}
  1558  }
  1559  
  1560  func (t *LineColumnTracker) computeLineAndColumn(offset int) (lineCount int, columnCount int, lineStart int, lineEnd int) {
  1561  	t.scanTo(int32(offset))
  1562  
  1563  	// Scan for the start of the line
  1564  	if !t.hasLineStart {
  1565  		contents := t.contents
  1566  		i := t.offset
  1567  		for i > 0 {
  1568  			r, size := utf8.DecodeLastRuneInString(contents[:i])
  1569  			if r == '\n' || r == '\r' || r == '\u2028' || r == '\u2029' {
  1570  				break
  1571  			}
  1572  			i -= int32(size)
  1573  		}
  1574  		t.hasLineStart = true
  1575  		t.lineStart = i
  1576  	}
  1577  
  1578  	// Scan for the end of the line
  1579  	if !t.hasLineEnd {
  1580  		contents := t.contents
  1581  		i := t.offset
  1582  		n := int32(len(contents))
  1583  		for i < n {
  1584  			r, size := utf8.DecodeRuneInString(contents[i:])
  1585  			if r == '\n' || r == '\r' || r == '\u2028' || r == '\u2029' {
  1586  				break
  1587  			}
  1588  			i += int32(size)
  1589  		}
  1590  		t.hasLineEnd = true
  1591  		t.lineEnd = i
  1592  	}
  1593  
  1594  	return int(t.line), offset - int(t.lineStart), int(t.lineStart), int(t.lineEnd)
  1595  }
  1596  
  1597  func (tracker *LineColumnTracker) MsgLocationOrNil(r Range) *MsgLocation {
  1598  	if tracker == nil || !tracker.hasSource {
  1599  		return nil
  1600  	}
  1601  
  1602  	// Convert the index into a line and column number
  1603  	lineCount, columnCount, lineStart, lineEnd := tracker.computeLineAndColumn(int(r.Loc.Start))
  1604  
  1605  	return &MsgLocation{
  1606  		File:     tracker.prettyPath,
  1607  		Line:     lineCount + 1, // 0-based to 1-based
  1608  		Column:   columnCount,
  1609  		Length:   int(r.Len),
  1610  		LineText: tracker.contents[lineStart:lineEnd],
  1611  	}
  1612  }
  1613  
  1614  func detailStruct(data MsgData, terminalInfo TerminalInfo, maxMargin int) MsgDetail {
  1615  	// Only highlight the first line of the line text
  1616  	loc := *data.Location
  1617  	endOfFirstLine := len(loc.LineText)
  1618  
  1619  	// Note: This uses "IndexByte" because Go implements this with SIMD, which
  1620  	// can matter a lot for really long lines. Some people pass huge >100mb
  1621  	// minified files as line text for the log message.
  1622  	if i := strings.IndexByte(loc.LineText, '\n'); i >= 0 {
  1623  		endOfFirstLine = i
  1624  	}
  1625  
  1626  	firstLine := loc.LineText[:endOfFirstLine]
  1627  	afterFirstLine := loc.LineText[endOfFirstLine:]
  1628  	if afterFirstLine != "" && !strings.HasSuffix(afterFirstLine, "\n") {
  1629  		afterFirstLine += "\n"
  1630  	}
  1631  
  1632  	// Clamp values in range
  1633  	if loc.Line < 0 {
  1634  		loc.Line = 0
  1635  	}
  1636  	if loc.Column < 0 {
  1637  		loc.Column = 0
  1638  	}
  1639  	if loc.Length < 0 {
  1640  		loc.Length = 0
  1641  	}
  1642  	if loc.Column > endOfFirstLine {
  1643  		loc.Column = endOfFirstLine
  1644  	}
  1645  	if loc.Length > endOfFirstLine-loc.Column {
  1646  		loc.Length = endOfFirstLine - loc.Column
  1647  	}
  1648  
  1649  	spacesPerTab := 2
  1650  	lineText := renderTabStops(firstLine, spacesPerTab)
  1651  	textUpToLoc := renderTabStops(firstLine[:loc.Column], spacesPerTab)
  1652  	markerStart := len(textUpToLoc)
  1653  	markerEnd := markerStart
  1654  	indent := strings.Repeat(" ", estimateWidthInTerminal(textUpToLoc))
  1655  	marker := "^"
  1656  
  1657  	// Extend markers to cover the full range of the error
  1658  	if loc.Length > 0 {
  1659  		markerEnd = len(renderTabStops(firstLine[:loc.Column+loc.Length], spacesPerTab))
  1660  	}
  1661  
  1662  	// Clip the marker to the bounds of the line
  1663  	if markerStart > len(lineText) {
  1664  		markerStart = len(lineText)
  1665  	}
  1666  	if markerEnd > len(lineText) {
  1667  		markerEnd = len(lineText)
  1668  	}
  1669  	if markerEnd < markerStart {
  1670  		markerEnd = markerStart
  1671  	}
  1672  
  1673  	// Trim the line to fit the terminal width
  1674  	width := terminalInfo.Width
  1675  	if width < 1 {
  1676  		width = defaultTerminalWidth
  1677  	}
  1678  	width -= maxMargin + extraMarginChars
  1679  	if width < 1 {
  1680  		width = 1
  1681  	}
  1682  	if loc.Column == endOfFirstLine {
  1683  		// If the marker is at the very end of the line, the marker will be a "^"
  1684  		// character that extends one column past the end of the line. In this case
  1685  		// we should reserve a column at the end so the marker doesn't wrap.
  1686  		width -= 1
  1687  	}
  1688  	if len(lineText) > width {
  1689  		// Try to center the error
  1690  		sliceStart := (markerStart + markerEnd - width) / 2
  1691  		if sliceStart > markerStart-width/5 {
  1692  			sliceStart = markerStart - width/5
  1693  		}
  1694  		if sliceStart < 0 {
  1695  			sliceStart = 0
  1696  		}
  1697  		if sliceStart > len(lineText)-width {
  1698  			sliceStart = len(lineText) - width
  1699  		}
  1700  		sliceEnd := sliceStart + width
  1701  
  1702  		// Slice the line
  1703  		slicedLine := lineText[sliceStart:sliceEnd]
  1704  		markerStart -= sliceStart
  1705  		markerEnd -= sliceStart
  1706  		if markerStart < 0 {
  1707  			markerStart = 0
  1708  		}
  1709  		if markerEnd > len(slicedLine) {
  1710  			markerEnd = len(slicedLine)
  1711  		}
  1712  
  1713  		// Truncate the ends with "..."
  1714  		if len(slicedLine) > 3 && sliceStart > 0 {
  1715  			slicedLine = "..." + slicedLine[3:]
  1716  			if markerStart < 3 {
  1717  				markerStart = 3
  1718  			}
  1719  		}
  1720  		if len(slicedLine) > 3 && sliceEnd < len(lineText) {
  1721  			slicedLine = slicedLine[:len(slicedLine)-3] + "..."
  1722  			if markerEnd > len(slicedLine)-3 {
  1723  				markerEnd = len(slicedLine) - 3
  1724  			}
  1725  			if markerEnd < markerStart {
  1726  				markerEnd = markerStart
  1727  			}
  1728  		}
  1729  
  1730  		// Now we can compute the indent
  1731  		lineText = slicedLine
  1732  		indent = strings.Repeat(" ", estimateWidthInTerminal(lineText[:markerStart]))
  1733  	}
  1734  
  1735  	// If marker is still multi-character after clipping, make the marker wider
  1736  	if markerEnd-markerStart > 1 {
  1737  		marker = strings.Repeat("~", estimateWidthInTerminal(lineText[markerStart:markerEnd]))
  1738  	}
  1739  
  1740  	// Put a margin before the marker indent
  1741  	margin := marginWithLineText(maxMargin, loc.Line)
  1742  
  1743  	return MsgDetail{
  1744  		Path:   loc.File,
  1745  		Line:   loc.Line,
  1746  		Column: loc.Column,
  1747  
  1748  		SourceBefore: margin + lineText[:markerStart],
  1749  		SourceMarked: lineText[markerStart:markerEnd],
  1750  		SourceAfter:  lineText[markerEnd:],
  1751  
  1752  		Indent:     indent,
  1753  		Marker:     marker,
  1754  		Suggestion: loc.Suggestion,
  1755  
  1756  		ContentAfter: afterFirstLine,
  1757  	}
  1758  }
  1759  
  1760  // Estimate the number of columns this string will take when printed
  1761  func estimateWidthInTerminal(text string) int {
  1762  	// For now just assume each code point is one column. This is wrong but is
  1763  	// less wrong than assuming each code unit is one column.
  1764  	width := 0
  1765  	for text != "" {
  1766  		c, size := utf8.DecodeRuneInString(text)
  1767  		text = text[size:]
  1768  
  1769  		// Ignore the Zero Width No-Break Space character (UTF-8 BOM)
  1770  		if c != 0xFEFF {
  1771  			width++
  1772  		}
  1773  	}
  1774  	return width
  1775  }
  1776  
  1777  func renderTabStops(withTabs string, spacesPerTab int) string {
  1778  	if !strings.ContainsRune(withTabs, '\t') {
  1779  		return withTabs
  1780  	}
  1781  
  1782  	withoutTabs := strings.Builder{}
  1783  	count := 0
  1784  
  1785  	for _, c := range withTabs {
  1786  		if c == '\t' {
  1787  			spaces := spacesPerTab - count%spacesPerTab
  1788  			for i := 0; i < spaces; i++ {
  1789  				withoutTabs.WriteRune(' ')
  1790  				count++
  1791  			}
  1792  		} else {
  1793  			withoutTabs.WriteRune(c)
  1794  			count++
  1795  		}
  1796  	}
  1797  
  1798  	return withoutTabs.String()
  1799  }
  1800  
  1801  func (log Log) AddError(tracker *LineColumnTracker, r Range, text string) {
  1802  	log.AddMsg(Msg{
  1803  		Kind: Error,
  1804  		Data: tracker.MsgData(r, text),
  1805  	})
  1806  }
  1807  
  1808  func (log Log) AddID(id MsgID, kind MsgKind, tracker *LineColumnTracker, r Range, text string) {
  1809  	if override, ok := allowOverride(log.Overrides, id, kind); ok {
  1810  		log.AddMsg(Msg{
  1811  			ID:   id,
  1812  			Kind: override,
  1813  			Data: tracker.MsgData(r, text),
  1814  		})
  1815  	}
  1816  }
  1817  
  1818  func (log Log) AddErrorWithNotes(tracker *LineColumnTracker, r Range, text string, notes []MsgData) {
  1819  	log.AddMsg(Msg{
  1820  		Kind:  Error,
  1821  		Data:  tracker.MsgData(r, text),
  1822  		Notes: notes,
  1823  	})
  1824  }
  1825  
  1826  func (log Log) AddIDWithNotes(id MsgID, kind MsgKind, tracker *LineColumnTracker, r Range, text string, notes []MsgData) {
  1827  	if override, ok := allowOverride(log.Overrides, id, kind); ok {
  1828  		log.AddMsg(Msg{
  1829  			ID:    id,
  1830  			Kind:  override,
  1831  			Data:  tracker.MsgData(r, text),
  1832  			Notes: notes,
  1833  		})
  1834  	}
  1835  }
  1836  
  1837  func (log Log) AddMsgID(id MsgID, msg Msg) {
  1838  	if override, ok := allowOverride(log.Overrides, id, msg.Kind); ok {
  1839  		msg.ID = id
  1840  		msg.Kind = override
  1841  		log.AddMsg(msg)
  1842  	}
  1843  }
  1844  
  1845  func allowOverride(overrides map[MsgID]LogLevel, id MsgID, kind MsgKind) (MsgKind, bool) {
  1846  	if logLevel, ok := overrides[id]; ok {
  1847  		switch logLevel {
  1848  		case LevelVerbose:
  1849  			return Verbose, true
  1850  		case LevelDebug:
  1851  			return Debug, true
  1852  		case LevelInfo:
  1853  			return Info, true
  1854  		case LevelWarning:
  1855  			return Warning, true
  1856  		case LevelError:
  1857  			return Error, true
  1858  		default:
  1859  			// Setting the log level to "silent" silences this log message
  1860  			return MsgKind(0), false
  1861  		}
  1862  	}
  1863  	return kind, true
  1864  }
  1865  
  1866  type StringInJSTableEntry struct {
  1867  	innerLine   int32
  1868  	innerColumn int32
  1869  	innerLoc    Loc
  1870  	outerLoc    Loc
  1871  }
  1872  
  1873  // For Yarn PnP we sometimes parse JSON embedded in a JS string. This generates
  1874  // a table that remaps locations inside the embedded JSON string literal into
  1875  // locations in the actual JS file, which makes them easier to understand.
  1876  func GenerateStringInJSTable(outerContents string, outerStringLiteralLoc Loc, innerContents string) (table []StringInJSTableEntry) {
  1877  	i := int32(0)
  1878  	n := int32(len(innerContents))
  1879  	line := int32(1)
  1880  	column := int32(0)
  1881  	loc := Loc{Start: outerStringLiteralLoc.Start + 1}
  1882  
  1883  	for i < n {
  1884  		// Ignore line continuations. A line continuation is not an escaped newline.
  1885  		for {
  1886  			if c, _ := utf8.DecodeRuneInString(outerContents[loc.Start:]); c != '\\' {
  1887  				break
  1888  			}
  1889  			c, width := utf8.DecodeRuneInString(outerContents[loc.Start+1:])
  1890  			switch c {
  1891  			case '\n', '\r', '\u2028', '\u2029':
  1892  				loc.Start += 1 + int32(width)
  1893  				if c == '\r' && outerContents[loc.Start] == '\n' {
  1894  					// Make sure Windows CRLF counts as a single newline
  1895  					loc.Start++
  1896  				}
  1897  				continue
  1898  			}
  1899  			break
  1900  		}
  1901  
  1902  		c, width := utf8.DecodeRuneInString(innerContents[i:])
  1903  
  1904  		// Compress the table using run-length encoding
  1905  		table = append(table, StringInJSTableEntry{innerLine: line, innerColumn: column, innerLoc: Loc{Start: i}, outerLoc: loc})
  1906  		if len(table) > 1 {
  1907  			if last := table[len(table)-2]; line == last.innerLine && loc.Start-column == last.outerLoc.Start-last.innerColumn {
  1908  				table = table[:len(table)-1]
  1909  			}
  1910  		}
  1911  
  1912  		// Advance the inner line/column
  1913  		switch c {
  1914  		case '\n', '\r', '\u2028', '\u2029':
  1915  			line++
  1916  			column = 0
  1917  
  1918  			// Handle newlines on Windows
  1919  			if c == '\r' && i+1 < n && innerContents[i+1] == '\n' {
  1920  				i++
  1921  			}
  1922  
  1923  		default:
  1924  			column += int32(width)
  1925  		}
  1926  		i += int32(width)
  1927  
  1928  		// Advance the outer loc, assuming the string syntax is already valid
  1929  		c, width = utf8.DecodeRuneInString(outerContents[loc.Start:])
  1930  		if c == '\r' && outerContents[loc.Start+1] == '\n' {
  1931  			// Handle newlines on Windows in template literal strings
  1932  			loc.Start += 2
  1933  		} else if c != '\\' {
  1934  			loc.Start += int32(width)
  1935  		} else {
  1936  			// Handle an escape sequence
  1937  			c, width = utf8.DecodeRuneInString(outerContents[loc.Start+1:])
  1938  			switch c {
  1939  			case 'x':
  1940  				// 2-digit hexadecimal
  1941  				loc.Start += 1 + 2
  1942  
  1943  			case 'u':
  1944  				loc.Start++
  1945  				if outerContents[loc.Start] == '{' {
  1946  					// Variable-length
  1947  					for outerContents[loc.Start] != '}' {
  1948  						loc.Start++
  1949  					}
  1950  					loc.Start++
  1951  				} else {
  1952  					// Fixed-length
  1953  					loc.Start += 4
  1954  				}
  1955  
  1956  			case '\n', '\r', '\u2028', '\u2029':
  1957  				// This will be handled by the next iteration
  1958  				break
  1959  
  1960  			default:
  1961  				loc.Start += 1 + int32(width)
  1962  			}
  1963  		}
  1964  	}
  1965  
  1966  	return
  1967  }
  1968  
  1969  func RemapStringInJSLoc(table []StringInJSTableEntry, innerLoc Loc) Loc {
  1970  	count := len(table)
  1971  	index := 0
  1972  
  1973  	// Binary search to find the previous entry
  1974  	for count > 0 {
  1975  		step := count / 2
  1976  		i := index + step
  1977  		if i+1 < len(table) {
  1978  			if entry := table[i+1]; entry.innerLoc.Start < innerLoc.Start {
  1979  				index = i + 1
  1980  				count -= step + 1
  1981  				continue
  1982  			}
  1983  		}
  1984  		count = step
  1985  	}
  1986  
  1987  	entry := table[index]
  1988  	entry.outerLoc.Start += innerLoc.Start - entry.innerLoc.Start // Undo run-length compression
  1989  	return entry.outerLoc
  1990  }
  1991  
  1992  func NewStringInJSLog(log Log, outerTracker *LineColumnTracker, table []StringInJSTableEntry) Log {
  1993  	oldAddMsg := log.AddMsg
  1994  
  1995  	remapLineAndColumnToLoc := func(line int32, column int32) Loc {
  1996  		count := len(table)
  1997  		index := 0
  1998  
  1999  		// Binary search to find the previous entry
  2000  		for count > 0 {
  2001  			step := count / 2
  2002  			i := index + step
  2003  			if i+1 < len(table) {
  2004  				if entry := table[i+1]; entry.innerLine < line || (entry.innerLine == line && entry.innerColumn < column) {
  2005  					index = i + 1
  2006  					count -= step + 1
  2007  					continue
  2008  				}
  2009  			}
  2010  			count = step
  2011  		}
  2012  
  2013  		entry := table[index]
  2014  		entry.outerLoc.Start += column - entry.innerColumn // Undo run-length compression
  2015  		return entry.outerLoc
  2016  	}
  2017  
  2018  	remapData := func(data MsgData) MsgData {
  2019  		if data.Location == nil {
  2020  			return data
  2021  		}
  2022  
  2023  		// Generate a range in the outer source using the line/column/length in the inner source
  2024  		r := Range{Loc: remapLineAndColumnToLoc(int32(data.Location.Line), int32(data.Location.Column))}
  2025  		if data.Location.Length != 0 {
  2026  			r.Len = remapLineAndColumnToLoc(int32(data.Location.Line), int32(data.Location.Column+data.Location.Length)).Start - r.Loc.Start
  2027  		}
  2028  
  2029  		// Use that range to look up the line in the outer source
  2030  		location := outerTracker.MsgData(r, data.Text).Location
  2031  		location.Suggestion = data.Location.Suggestion
  2032  		data.Location = location
  2033  		return data
  2034  	}
  2035  
  2036  	log.AddMsg = func(msg Msg) {
  2037  		msg.Data = remapData(msg.Data)
  2038  		for i, note := range msg.Notes {
  2039  			msg.Notes[i] = remapData(note)
  2040  		}
  2041  		oldAddMsg(msg)
  2042  	}
  2043  
  2044  	return log
  2045  }