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 }