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

     1  package resolver
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/evanw/esbuild/internal/cache"
     8  	"github.com/evanw/esbuild/internal/config"
     9  	"github.com/evanw/esbuild/internal/helpers"
    10  	"github.com/evanw/esbuild/internal/js_ast"
    11  	"github.com/evanw/esbuild/internal/js_lexer"
    12  	"github.com/evanw/esbuild/internal/js_parser"
    13  	"github.com/evanw/esbuild/internal/logger"
    14  )
    15  
    16  type TSConfigJSON struct {
    17  	AbsPath string
    18  
    19  	// The absolute path of "compilerOptions.baseUrl"
    20  	BaseURL *string
    21  
    22  	// This is used if "Paths" is non-nil. It's equal to "BaseURL" except if
    23  	// "BaseURL" is missing, in which case it is as if "BaseURL" was ".". This
    24  	// is to implement the "paths without baseUrl" feature from TypeScript 4.1.
    25  	// More info: https://github.com/microsoft/TypeScript/issues/31869
    26  	BaseURLForPaths string
    27  
    28  	// The verbatim values of "compilerOptions.paths". The keys are patterns to
    29  	// match and the values are arrays of fallback paths to search. Each key and
    30  	// each fallback path can optionally have a single "*" wildcard character.
    31  	// If both the key and the value have a wildcard, the substring matched by
    32  	// the wildcard is substituted into the fallback path. The keys represent
    33  	// module-style path names and the fallback paths are relative to the
    34  	// "baseUrl" value in the "tsconfig.json" file.
    35  	Paths *TSConfigPaths
    36  
    37  	tsTargetKey    tsTargetKey
    38  	TSStrict       *config.TSAlwaysStrict
    39  	TSAlwaysStrict *config.TSAlwaysStrict
    40  	JSXSettings    config.TSConfigJSX
    41  	Settings       config.TSConfig
    42  }
    43  
    44  func (derived *TSConfigJSON) applyExtendedConfig(base TSConfigJSON) {
    45  	if base.tsTargetKey.Range.Len > 0 {
    46  		derived.tsTargetKey = base.tsTargetKey
    47  	}
    48  	if base.TSStrict != nil {
    49  		derived.TSStrict = base.TSStrict
    50  	}
    51  	if base.TSAlwaysStrict != nil {
    52  		derived.TSAlwaysStrict = base.TSAlwaysStrict
    53  	}
    54  	if base.BaseURL != nil {
    55  		derived.BaseURL = base.BaseURL
    56  	}
    57  	if base.Paths != nil {
    58  		derived.Paths = base.Paths
    59  		derived.BaseURLForPaths = base.BaseURLForPaths
    60  	}
    61  	derived.JSXSettings.ApplyExtendedConfig(base.JSXSettings)
    62  	derived.Settings.ApplyExtendedConfig(base.Settings)
    63  }
    64  
    65  func (config *TSConfigJSON) TSAlwaysStrictOrStrict() *config.TSAlwaysStrict {
    66  	if config.TSAlwaysStrict != nil {
    67  		return config.TSAlwaysStrict
    68  	}
    69  
    70  	// If "alwaysStrict" is absent, it defaults to "strict" instead
    71  	return config.TSStrict
    72  }
    73  
    74  // This information is only used for error messages
    75  type tsTargetKey struct {
    76  	LowerValue string
    77  	Source     logger.Source
    78  	Range      logger.Range
    79  }
    80  
    81  type TSConfigPath struct {
    82  	Text string
    83  	Loc  logger.Loc
    84  }
    85  
    86  type TSConfigPaths struct {
    87  	Map map[string][]TSConfigPath
    88  
    89  	// This may be different from the original "tsconfig.json" source if the
    90  	// "paths" value is from another file via an "extends" clause.
    91  	Source logger.Source
    92  }
    93  
    94  func ParseTSConfigJSON(
    95  	log logger.Log,
    96  	source logger.Source,
    97  	jsonCache *cache.JSONCache,
    98  	extends func(string, logger.Range) *TSConfigJSON,
    99  ) *TSConfigJSON {
   100  	// Unfortunately "tsconfig.json" isn't actually JSON. It's some other
   101  	// format that appears to be defined by the implementation details of the
   102  	// TypeScript compiler.
   103  	//
   104  	// Attempt to parse it anyway by modifying the JSON parser, but just for
   105  	// these particular files. This is likely not a completely accurate
   106  	// emulation of what the TypeScript compiler does (e.g. string escape
   107  	// behavior may also be different).
   108  	json, ok := jsonCache.Parse(log, source, js_parser.JSONOptions{Flavor: js_lexer.TSConfigJSON})
   109  	if !ok {
   110  		return nil
   111  	}
   112  
   113  	var result TSConfigJSON
   114  	result.AbsPath = source.KeyPath.Text
   115  	tracker := logger.MakeLineColumnTracker(&source)
   116  
   117  	// Parse "extends"
   118  	if extends != nil {
   119  		if valueJSON, _, ok := getProperty(json, "extends"); ok {
   120  			if value, ok := getString(valueJSON); ok {
   121  				if base := extends(value, source.RangeOfString(valueJSON.Loc)); base != nil {
   122  					result.applyExtendedConfig(*base)
   123  				}
   124  			} else if array, ok := valueJSON.Data.(*js_ast.EArray); ok {
   125  				for _, item := range array.Items {
   126  					if str, ok := getString(item); ok {
   127  						if base := extends(str, source.RangeOfString(item.Loc)); base != nil {
   128  							result.applyExtendedConfig(*base)
   129  						}
   130  					}
   131  				}
   132  			}
   133  		}
   134  	}
   135  
   136  	// Parse "compilerOptions"
   137  	if compilerOptionsJSON, _, ok := getProperty(json, "compilerOptions"); ok {
   138  		// Parse "baseUrl"
   139  		if valueJSON, _, ok := getProperty(compilerOptionsJSON, "baseUrl"); ok {
   140  			if value, ok := getString(valueJSON); ok {
   141  				result.BaseURL = &value
   142  			}
   143  		}
   144  
   145  		// Parse "jsx"
   146  		if valueJSON, _, ok := getProperty(compilerOptionsJSON, "jsx"); ok {
   147  			if value, ok := getString(valueJSON); ok {
   148  				switch strings.ToLower(value) {
   149  				case "preserve":
   150  					result.JSXSettings.JSX = config.TSJSXPreserve
   151  				case "react-native":
   152  					result.JSXSettings.JSX = config.TSJSXReactNative
   153  				case "react":
   154  					result.JSXSettings.JSX = config.TSJSXReact
   155  				case "react-jsx":
   156  					result.JSXSettings.JSX = config.TSJSXReactJSX
   157  				case "react-jsxdev":
   158  					result.JSXSettings.JSX = config.TSJSXReactJSXDev
   159  				}
   160  			}
   161  		}
   162  
   163  		// Parse "jsxFactory"
   164  		if valueJSON, _, ok := getProperty(compilerOptionsJSON, "jsxFactory"); ok {
   165  			if value, ok := getString(valueJSON); ok {
   166  				result.JSXSettings.JSXFactory = parseMemberExpressionForJSX(log, &source, &tracker, valueJSON.Loc, value)
   167  			}
   168  		}
   169  
   170  		// Parse "jsxFragmentFactory"
   171  		if valueJSON, _, ok := getProperty(compilerOptionsJSON, "jsxFragmentFactory"); ok {
   172  			if value, ok := getString(valueJSON); ok {
   173  				result.JSXSettings.JSXFragmentFactory = parseMemberExpressionForJSX(log, &source, &tracker, valueJSON.Loc, value)
   174  			}
   175  		}
   176  
   177  		// Parse "jsxImportSource"
   178  		if valueJSON, _, ok := getProperty(compilerOptionsJSON, "jsxImportSource"); ok {
   179  			if value, ok := getString(valueJSON); ok {
   180  				result.JSXSettings.JSXImportSource = &value
   181  			}
   182  		}
   183  
   184  		// Parse "experimentalDecorators"
   185  		if valueJSON, _, ok := getProperty(compilerOptionsJSON, "experimentalDecorators"); ok {
   186  			if value, ok := getBool(valueJSON); ok {
   187  				if value {
   188  					result.Settings.ExperimentalDecorators = config.True
   189  				} else {
   190  					result.Settings.ExperimentalDecorators = config.False
   191  				}
   192  			}
   193  		}
   194  
   195  		// Parse "useDefineForClassFields"
   196  		if valueJSON, _, ok := getProperty(compilerOptionsJSON, "useDefineForClassFields"); ok {
   197  			if value, ok := getBool(valueJSON); ok {
   198  				if value {
   199  					result.Settings.UseDefineForClassFields = config.True
   200  				} else {
   201  					result.Settings.UseDefineForClassFields = config.False
   202  				}
   203  			}
   204  		}
   205  
   206  		// Parse "target"
   207  		if valueJSON, keyLoc, ok := getProperty(compilerOptionsJSON, "target"); ok {
   208  			if value, ok := getString(valueJSON); ok {
   209  				lowerValue := strings.ToLower(value)
   210  				ok := true
   211  
   212  				// See https://www.typescriptlang.org/tsconfig#target
   213  				switch lowerValue {
   214  				case "es3", "es5", "es6", "es2015", "es2016", "es2017", "es2018", "es2019", "es2020", "es2021":
   215  					result.Settings.Target = config.TSTargetBelowES2022
   216  				case "es2022", "es2023", "esnext":
   217  					result.Settings.Target = config.TSTargetAtOrAboveES2022
   218  				default:
   219  					ok = false
   220  					if !helpers.IsInsideNodeModules(source.KeyPath.Text) {
   221  						log.AddID(logger.MsgID_TSConfigJSON_InvalidTarget, logger.Warning, &tracker, source.RangeOfString(valueJSON.Loc),
   222  							fmt.Sprintf("Unrecognized target environment %q", value))
   223  					}
   224  				}
   225  
   226  				if ok {
   227  					result.tsTargetKey = tsTargetKey{
   228  						Source:     source,
   229  						Range:      source.RangeOfString(keyLoc),
   230  						LowerValue: lowerValue,
   231  					}
   232  				}
   233  			}
   234  		}
   235  
   236  		// Parse "strict"
   237  		if valueJSON, keyLoc, ok := getProperty(compilerOptionsJSON, "strict"); ok {
   238  			if value, ok := getBool(valueJSON); ok {
   239  				valueRange := js_lexer.RangeOfIdentifier(source, valueJSON.Loc)
   240  				result.TSStrict = &config.TSAlwaysStrict{
   241  					Name:   "strict",
   242  					Value:  value,
   243  					Source: source,
   244  					Range:  logger.Range{Loc: keyLoc, Len: valueRange.End() - keyLoc.Start},
   245  				}
   246  			}
   247  		}
   248  
   249  		// Parse "alwaysStrict"
   250  		if valueJSON, keyLoc, ok := getProperty(compilerOptionsJSON, "alwaysStrict"); ok {
   251  			if value, ok := getBool(valueJSON); ok {
   252  				valueRange := js_lexer.RangeOfIdentifier(source, valueJSON.Loc)
   253  				result.TSAlwaysStrict = &config.TSAlwaysStrict{
   254  					Name:   "alwaysStrict",
   255  					Value:  value,
   256  					Source: source,
   257  					Range:  logger.Range{Loc: keyLoc, Len: valueRange.End() - keyLoc.Start},
   258  				}
   259  			}
   260  		}
   261  
   262  		// Parse "importsNotUsedAsValues"
   263  		if valueJSON, _, ok := getProperty(compilerOptionsJSON, "importsNotUsedAsValues"); ok {
   264  			if value, ok := getString(valueJSON); ok {
   265  				switch value {
   266  				case "remove":
   267  					result.Settings.ImportsNotUsedAsValues = config.TSImportsNotUsedAsValues_Remove
   268  				case "preserve":
   269  					result.Settings.ImportsNotUsedAsValues = config.TSImportsNotUsedAsValues_Preserve
   270  				case "error":
   271  					result.Settings.ImportsNotUsedAsValues = config.TSImportsNotUsedAsValues_Error
   272  				default:
   273  					log.AddID(logger.MsgID_TSConfigJSON_InvalidImportsNotUsedAsValues, logger.Warning, &tracker, source.RangeOfString(valueJSON.Loc),
   274  						fmt.Sprintf("Invalid value %q for \"importsNotUsedAsValues\"", value))
   275  				}
   276  			}
   277  		}
   278  
   279  		// Parse "preserveValueImports"
   280  		if valueJSON, _, ok := getProperty(compilerOptionsJSON, "preserveValueImports"); ok {
   281  			if value, ok := getBool(valueJSON); ok {
   282  				if value {
   283  					result.Settings.PreserveValueImports = config.True
   284  				} else {
   285  					result.Settings.PreserveValueImports = config.False
   286  				}
   287  			}
   288  		}
   289  
   290  		// Parse "verbatimModuleSyntax"
   291  		if valueJSON, _, ok := getProperty(compilerOptionsJSON, "verbatimModuleSyntax"); ok {
   292  			if value, ok := getBool(valueJSON); ok {
   293  				if value {
   294  					result.Settings.VerbatimModuleSyntax = config.True
   295  				} else {
   296  					result.Settings.VerbatimModuleSyntax = config.False
   297  				}
   298  			}
   299  		}
   300  
   301  		// Parse "paths"
   302  		if valueJSON, _, ok := getProperty(compilerOptionsJSON, "paths"); ok {
   303  			if paths, ok := valueJSON.Data.(*js_ast.EObject); ok {
   304  				hasBaseURL := result.BaseURL != nil
   305  				if hasBaseURL {
   306  					result.BaseURLForPaths = *result.BaseURL
   307  				} else {
   308  					result.BaseURLForPaths = "."
   309  				}
   310  				result.Paths = &TSConfigPaths{Source: source, Map: make(map[string][]TSConfigPath)}
   311  				for _, prop := range paths.Properties {
   312  					if key, ok := getString(prop.Key); ok {
   313  						if !isValidTSConfigPathPattern(key, log, &source, &tracker, prop.Key.Loc) {
   314  							continue
   315  						}
   316  
   317  						// The "paths" field is an object which maps a pattern to an
   318  						// array of remapping patterns to try, in priority order. See
   319  						// the documentation for examples of how this is used:
   320  						// https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping.
   321  						//
   322  						// One particular example:
   323  						//
   324  						//   {
   325  						//     "compilerOptions": {
   326  						//       "baseUrl": "projectRoot",
   327  						//       "paths": {
   328  						//         "*": [
   329  						//           "*",
   330  						//           "generated/*"
   331  						//         ]
   332  						//       }
   333  						//     }
   334  						//   }
   335  						//
   336  						// Matching "folder1/file2" should first check "projectRoot/folder1/file2"
   337  						// and then, if that didn't work, also check "projectRoot/generated/folder1/file2".
   338  						if array, ok := prop.ValueOrNil.Data.(*js_ast.EArray); ok {
   339  							for _, item := range array.Items {
   340  								if str, ok := getString(item); ok {
   341  									if isValidTSConfigPathPattern(str, log, &source, &tracker, item.Loc) {
   342  										result.Paths.Map[key] = append(result.Paths.Map[key], TSConfigPath{Text: str, Loc: item.Loc})
   343  									}
   344  								}
   345  							}
   346  						} else {
   347  							log.AddID(logger.MsgID_TSConfigJSON_InvalidPaths, logger.Warning, &tracker, source.RangeOfString(prop.ValueOrNil.Loc), fmt.Sprintf(
   348  								"Substitutions for pattern %q should be an array", key))
   349  						}
   350  					}
   351  				}
   352  			}
   353  		}
   354  	}
   355  
   356  	// Warn about compiler options not wrapped in "compilerOptions".
   357  	// For example: https://github.com/evanw/esbuild/issues/3301
   358  	if obj, ok := json.Data.(*js_ast.EObject); ok {
   359  	loop:
   360  		for _, prop := range obj.Properties {
   361  			if key, ok := prop.Key.Data.(*js_ast.EString); ok && key.Value != nil {
   362  				key := helpers.UTF16ToString(key.Value)
   363  				switch key {
   364  				case "alwaysStrict",
   365  					"baseUrl",
   366  					"experimentalDecorators",
   367  					"importsNotUsedAsValues",
   368  					"jsx",
   369  					"jsxFactory",
   370  					"jsxFragmentFactory",
   371  					"jsxImportSource",
   372  					"paths",
   373  					"preserveValueImports",
   374  					"strict",
   375  					"target",
   376  					"useDefineForClassFields",
   377  					"verbatimModuleSyntax":
   378  					log.AddIDWithNotes(logger.MsgID_TSConfigJSON_InvalidTopLevelOption, logger.Warning, &tracker, source.RangeOfString(prop.Key.Loc),
   379  						fmt.Sprintf("Expected the %q option to be nested inside a \"compilerOptions\" object", key),
   380  						[]logger.MsgData{})
   381  					break loop
   382  				}
   383  			}
   384  		}
   385  	}
   386  
   387  	return &result
   388  }
   389  
   390  func parseMemberExpressionForJSX(log logger.Log, source *logger.Source, tracker *logger.LineColumnTracker, loc logger.Loc, text string) []string {
   391  	if text == "" {
   392  		return nil
   393  	}
   394  	parts := strings.Split(text, ".")
   395  	for _, part := range parts {
   396  		if !js_ast.IsIdentifier(part) {
   397  			warnRange := source.RangeOfString(loc)
   398  			log.AddID(logger.MsgID_TSConfigJSON_InvalidJSX, logger.Warning, tracker, warnRange, fmt.Sprintf("Invalid JSX member expression: %q", text))
   399  			return nil
   400  		}
   401  	}
   402  	return parts
   403  }
   404  
   405  func isValidTSConfigPathPattern(text string, log logger.Log, source *logger.Source, tracker *logger.LineColumnTracker, loc logger.Loc) bool {
   406  	foundAsterisk := false
   407  	for i := 0; i < len(text); i++ {
   408  		if text[i] == '*' {
   409  			if foundAsterisk {
   410  				r := source.RangeOfString(loc)
   411  				log.AddID(logger.MsgID_TSConfigJSON_InvalidPaths, logger.Warning, tracker, r, fmt.Sprintf(
   412  					"Invalid pattern %q, must have at most one \"*\" character", text))
   413  				return false
   414  			}
   415  			foundAsterisk = true
   416  		}
   417  	}
   418  	return true
   419  }
   420  
   421  func isSlash(c byte) bool {
   422  	return c == '/' || c == '\\'
   423  }
   424  
   425  func isValidTSConfigPathNoBaseURLPattern(text string, log logger.Log, source *logger.Source, tracker **logger.LineColumnTracker, loc logger.Loc) bool {
   426  	var c0 byte
   427  	var c1 byte
   428  	var c2 byte
   429  	n := len(text)
   430  
   431  	if n > 0 {
   432  		c0 = text[0]
   433  		if n > 1 {
   434  			c1 = text[1]
   435  			if n > 2 {
   436  				c2 = text[2]
   437  			}
   438  		}
   439  	}
   440  
   441  	// Relative "." or ".."
   442  	if c0 == '.' && (n == 1 || (n == 2 && c1 == '.')) {
   443  		return true
   444  	}
   445  
   446  	// Relative "./" or "../" or ".\\" or "..\\"
   447  	if c0 == '.' && (isSlash(c1) || (c1 == '.' && isSlash(c2))) {
   448  		return true
   449  	}
   450  
   451  	// Absolute POSIX "/" or UNC "\\"
   452  	if isSlash(c0) {
   453  		return true
   454  	}
   455  
   456  	// Absolute DOS "c:/" or "c:\\"
   457  	if ((c0 >= 'a' && c0 <= 'z') || (c0 >= 'A' && c0 <= 'Z')) && c1 == ':' && isSlash(c2) {
   458  		return true
   459  	}
   460  
   461  	r := source.RangeOfString(loc)
   462  	if *tracker == nil {
   463  		t := logger.MakeLineColumnTracker(source)
   464  		*tracker = &t
   465  	}
   466  	log.AddID(logger.MsgID_TSConfigJSON_InvalidPaths, logger.Warning, *tracker, r, fmt.Sprintf(
   467  		"Non-relative path %q is not allowed when \"baseUrl\" is not set (did you forget a leading \"./\"?)", text))
   468  	return false
   469  }