mirror of
https://github.com/hanzoai/esbuild.git
synced 2026-09-24 16:58:50 +00:00
482 lines
15 KiB
Go
482 lines
15 KiB
Go
package resolver
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/evanw/esbuild/internal/cache"
|
|
"github.com/evanw/esbuild/internal/config"
|
|
"github.com/evanw/esbuild/internal/fs"
|
|
"github.com/evanw/esbuild/internal/helpers"
|
|
"github.com/evanw/esbuild/internal/js_ast"
|
|
"github.com/evanw/esbuild/internal/js_lexer"
|
|
"github.com/evanw/esbuild/internal/js_parser"
|
|
"github.com/evanw/esbuild/internal/logger"
|
|
)
|
|
|
|
type TSConfigJSON struct {
|
|
AbsPath string
|
|
|
|
// The absolute path of "compilerOptions.baseUrl"
|
|
BaseURL *string
|
|
|
|
// This is used if "Paths" is non-nil. It's equal to "BaseURL" except if
|
|
// "BaseURL" is missing, in which case it is as if "BaseURL" was ".". This
|
|
// is to implement the "paths without baseUrl" feature from TypeScript 4.1.
|
|
// More info: https://github.com/microsoft/TypeScript/issues/31869
|
|
BaseURLForPaths string
|
|
|
|
// The verbatim values of "compilerOptions.paths". The keys are patterns to
|
|
// match and the values are arrays of fallback paths to search. Each key and
|
|
// each fallback path can optionally have a single "*" wildcard character.
|
|
// If both the key and the value have a wildcard, the substring matched by
|
|
// the wildcard is substituted into the fallback path. The keys represent
|
|
// module-style path names and the fallback paths are relative to the
|
|
// "baseUrl" value in the "tsconfig.json" file.
|
|
Paths *TSConfigPaths
|
|
|
|
tsTargetKey tsTargetKey
|
|
TSStrict *config.TSAlwaysStrict
|
|
TSAlwaysStrict *config.TSAlwaysStrict
|
|
JSXSettings config.TSConfigJSX
|
|
Settings config.TSConfig
|
|
}
|
|
|
|
func (derived *TSConfigJSON) applyExtendedConfig(base TSConfigJSON) {
|
|
if base.tsTargetKey.Range.Len > 0 {
|
|
derived.tsTargetKey = base.tsTargetKey
|
|
}
|
|
if base.TSStrict != nil {
|
|
derived.TSStrict = base.TSStrict
|
|
}
|
|
if base.TSAlwaysStrict != nil {
|
|
derived.TSAlwaysStrict = base.TSAlwaysStrict
|
|
}
|
|
if base.BaseURL != nil {
|
|
derived.BaseURL = base.BaseURL
|
|
}
|
|
if base.Paths != nil {
|
|
derived.Paths = base.Paths
|
|
derived.BaseURLForPaths = base.BaseURLForPaths
|
|
}
|
|
derived.JSXSettings.ApplyExtendedConfig(base.JSXSettings)
|
|
derived.Settings.ApplyExtendedConfig(base.Settings)
|
|
}
|
|
|
|
func (config *TSConfigJSON) TSAlwaysStrictOrStrict() *config.TSAlwaysStrict {
|
|
if config.TSAlwaysStrict != nil {
|
|
return config.TSAlwaysStrict
|
|
}
|
|
|
|
// If "alwaysStrict" is absent, it defaults to "strict" instead
|
|
return config.TSStrict
|
|
}
|
|
|
|
// This information is only used for error messages
|
|
type tsTargetKey struct {
|
|
LowerValue string
|
|
Source logger.Source
|
|
Range logger.Range
|
|
}
|
|
|
|
type TSConfigPath struct {
|
|
Text string
|
|
Loc logger.Loc
|
|
}
|
|
|
|
type TSConfigPaths struct {
|
|
Map map[string][]TSConfigPath
|
|
|
|
// This may be different from the original "tsconfig.json" source if the
|
|
// "paths" value is from another file via an "extends" clause.
|
|
Source logger.Source
|
|
}
|
|
|
|
func ParseTSConfigJSON(
|
|
log logger.Log,
|
|
source logger.Source,
|
|
jsonCache *cache.JSONCache,
|
|
fs fs.FS,
|
|
fileDir string,
|
|
configDir string,
|
|
extends func(string, logger.Range) *TSConfigJSON,
|
|
) *TSConfigJSON {
|
|
// Unfortunately "tsconfig.json" isn't actually JSON. It's some other
|
|
// format that appears to be defined by the implementation details of the
|
|
// TypeScript compiler.
|
|
//
|
|
// Attempt to parse it anyway by modifying the JSON parser, but just for
|
|
// these particular files. This is likely not a completely accurate
|
|
// emulation of what the TypeScript compiler does (e.g. string escape
|
|
// behavior may also be different).
|
|
json, ok := jsonCache.Parse(log, source, js_parser.JSONOptions{Flavor: js_lexer.TSConfigJSON})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
var result TSConfigJSON
|
|
result.AbsPath = source.KeyPath.Text
|
|
tracker := logger.MakeLineColumnTracker(&source)
|
|
|
|
// Parse "extends"
|
|
if extends != nil {
|
|
if valueJSON, _, ok := getProperty(json, "extends"); ok {
|
|
if value, ok := getString(valueJSON); ok {
|
|
if base := extends(value, source.RangeOfString(valueJSON.Loc)); base != nil {
|
|
result.applyExtendedConfig(*base)
|
|
}
|
|
} else if array, ok := valueJSON.Data.(*js_ast.EArray); ok {
|
|
for _, item := range array.Items {
|
|
if str, ok := getString(item); ok {
|
|
if base := extends(str, source.RangeOfString(item.Loc)); base != nil {
|
|
result.applyExtendedConfig(*base)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse "compilerOptions"
|
|
if compilerOptionsJSON, _, ok := getProperty(json, "compilerOptions"); ok {
|
|
// Parse "baseUrl"
|
|
if valueJSON, _, ok := getProperty(compilerOptionsJSON, "baseUrl"); ok {
|
|
if value, ok := getString(valueJSON); ok {
|
|
value = getSubstitutedPathWithConfigDirTemplate(fs, value, configDir)
|
|
if !fs.IsAbs(value) {
|
|
value = fs.Join(fileDir, value)
|
|
}
|
|
result.BaseURL = &value
|
|
}
|
|
}
|
|
|
|
// Parse "jsx"
|
|
if valueJSON, _, ok := getProperty(compilerOptionsJSON, "jsx"); ok {
|
|
if value, ok := getString(valueJSON); ok {
|
|
switch strings.ToLower(value) {
|
|
case "preserve":
|
|
result.JSXSettings.JSX = config.TSJSXPreserve
|
|
case "react-native":
|
|
result.JSXSettings.JSX = config.TSJSXReactNative
|
|
case "react":
|
|
result.JSXSettings.JSX = config.TSJSXReact
|
|
case "react-jsx":
|
|
result.JSXSettings.JSX = config.TSJSXReactJSX
|
|
case "react-jsxdev":
|
|
result.JSXSettings.JSX = config.TSJSXReactJSXDev
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse "jsxFactory"
|
|
if valueJSON, _, ok := getProperty(compilerOptionsJSON, "jsxFactory"); ok {
|
|
if value, ok := getString(valueJSON); ok {
|
|
result.JSXSettings.JSXFactory = parseMemberExpressionForJSX(log, &source, &tracker, valueJSON.Loc, value)
|
|
}
|
|
}
|
|
|
|
// Parse "jsxFragmentFactory"
|
|
if valueJSON, _, ok := getProperty(compilerOptionsJSON, "jsxFragmentFactory"); ok {
|
|
if value, ok := getString(valueJSON); ok {
|
|
result.JSXSettings.JSXFragmentFactory = parseMemberExpressionForJSX(log, &source, &tracker, valueJSON.Loc, value)
|
|
}
|
|
}
|
|
|
|
// Parse "jsxImportSource"
|
|
if valueJSON, _, ok := getProperty(compilerOptionsJSON, "jsxImportSource"); ok {
|
|
if value, ok := getString(valueJSON); ok {
|
|
result.JSXSettings.JSXImportSource = &value
|
|
}
|
|
}
|
|
|
|
// Parse "experimentalDecorators"
|
|
if valueJSON, _, ok := getProperty(compilerOptionsJSON, "experimentalDecorators"); ok {
|
|
if value, ok := getBool(valueJSON); ok {
|
|
if value {
|
|
result.Settings.ExperimentalDecorators = config.True
|
|
} else {
|
|
result.Settings.ExperimentalDecorators = config.False
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse "useDefineForClassFields"
|
|
if valueJSON, _, ok := getProperty(compilerOptionsJSON, "useDefineForClassFields"); ok {
|
|
if value, ok := getBool(valueJSON); ok {
|
|
if value {
|
|
result.Settings.UseDefineForClassFields = config.True
|
|
} else {
|
|
result.Settings.UseDefineForClassFields = config.False
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse "target"
|
|
if valueJSON, keyLoc, ok := getProperty(compilerOptionsJSON, "target"); ok {
|
|
if value, ok := getString(valueJSON); ok {
|
|
lowerValue := strings.ToLower(value)
|
|
ok := true
|
|
|
|
// See https://www.typescriptlang.org/tsconfig#target
|
|
switch lowerValue {
|
|
case "es3", "es5", "es6", "es2015", "es2016", "es2017", "es2018", "es2019", "es2020", "es2021":
|
|
result.Settings.Target = config.TSTargetBelowES2022
|
|
case "es2022", "es2023", "es2024", "es2025", "es2026", "esnext":
|
|
result.Settings.Target = config.TSTargetAtOrAboveES2022
|
|
default:
|
|
ok = false
|
|
if !helpers.IsInsideNodeModules(source.KeyPath.Text) {
|
|
log.AddID(logger.MsgID_TSConfigJSON_InvalidTarget, logger.Warning, &tracker, source.RangeOfString(valueJSON.Loc),
|
|
fmt.Sprintf("Unrecognized target environment %q", value))
|
|
}
|
|
}
|
|
|
|
if ok {
|
|
result.tsTargetKey = tsTargetKey{
|
|
Source: source,
|
|
Range: source.RangeOfString(keyLoc),
|
|
LowerValue: lowerValue,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse "strict"
|
|
if valueJSON, keyLoc, ok := getProperty(compilerOptionsJSON, "strict"); ok {
|
|
if value, ok := getBool(valueJSON); ok {
|
|
valueRange := js_lexer.RangeOfIdentifier(source, valueJSON.Loc)
|
|
result.TSStrict = &config.TSAlwaysStrict{
|
|
Name: "strict",
|
|
Value: value,
|
|
Source: source,
|
|
Range: logger.Range{Loc: keyLoc, Len: valueRange.End() - keyLoc.Start},
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse "alwaysStrict"
|
|
if valueJSON, keyLoc, ok := getProperty(compilerOptionsJSON, "alwaysStrict"); ok {
|
|
if value, ok := getBool(valueJSON); ok {
|
|
valueRange := js_lexer.RangeOfIdentifier(source, valueJSON.Loc)
|
|
result.TSAlwaysStrict = &config.TSAlwaysStrict{
|
|
Name: "alwaysStrict",
|
|
Value: value,
|
|
Source: source,
|
|
Range: logger.Range{Loc: keyLoc, Len: valueRange.End() - keyLoc.Start},
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse "importsNotUsedAsValues"
|
|
if valueJSON, _, ok := getProperty(compilerOptionsJSON, "importsNotUsedAsValues"); ok {
|
|
if value, ok := getString(valueJSON); ok {
|
|
switch value {
|
|
case "remove":
|
|
result.Settings.ImportsNotUsedAsValues = config.TSImportsNotUsedAsValues_Remove
|
|
case "preserve":
|
|
result.Settings.ImportsNotUsedAsValues = config.TSImportsNotUsedAsValues_Preserve
|
|
case "error":
|
|
result.Settings.ImportsNotUsedAsValues = config.TSImportsNotUsedAsValues_Error
|
|
default:
|
|
log.AddID(logger.MsgID_TSConfigJSON_InvalidImportsNotUsedAsValues, logger.Warning, &tracker, source.RangeOfString(valueJSON.Loc),
|
|
fmt.Sprintf("Invalid value %q for \"importsNotUsedAsValues\"", value))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse "preserveValueImports"
|
|
if valueJSON, _, ok := getProperty(compilerOptionsJSON, "preserveValueImports"); ok {
|
|
if value, ok := getBool(valueJSON); ok {
|
|
if value {
|
|
result.Settings.PreserveValueImports = config.True
|
|
} else {
|
|
result.Settings.PreserveValueImports = config.False
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse "verbatimModuleSyntax"
|
|
if valueJSON, _, ok := getProperty(compilerOptionsJSON, "verbatimModuleSyntax"); ok {
|
|
if value, ok := getBool(valueJSON); ok {
|
|
if value {
|
|
result.Settings.VerbatimModuleSyntax = config.True
|
|
} else {
|
|
result.Settings.VerbatimModuleSyntax = config.False
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse "paths"
|
|
if valueJSON, _, ok := getProperty(compilerOptionsJSON, "paths"); ok {
|
|
if paths, ok := valueJSON.Data.(*js_ast.EObject); ok {
|
|
result.BaseURLForPaths = fileDir
|
|
result.Paths = &TSConfigPaths{Source: source, Map: make(map[string][]TSConfigPath)}
|
|
for _, prop := range paths.Properties {
|
|
if key, ok := getString(prop.Key); ok {
|
|
if !isValidTSConfigPathPattern(key, log, &source, &tracker, prop.Key.Loc) {
|
|
continue
|
|
}
|
|
|
|
// The "paths" field is an object which maps a pattern to an
|
|
// array of remapping patterns to try, in priority order. See
|
|
// the documentation for examples of how this is used:
|
|
// https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping.
|
|
//
|
|
// One particular example:
|
|
//
|
|
// {
|
|
// "compilerOptions": {
|
|
// "baseUrl": "projectRoot",
|
|
// "paths": {
|
|
// "*": [
|
|
// "*",
|
|
// "generated/*"
|
|
// ]
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// Matching "folder1/file2" should first check "projectRoot/folder1/file2"
|
|
// and then, if that didn't work, also check "projectRoot/generated/folder1/file2".
|
|
if array, ok := prop.ValueOrNil.Data.(*js_ast.EArray); ok {
|
|
for _, item := range array.Items {
|
|
if str, ok := getString(item); ok {
|
|
if isValidTSConfigPathPattern(str, log, &source, &tracker, item.Loc) {
|
|
str = getSubstitutedPathWithConfigDirTemplate(fs, str, configDir)
|
|
result.Paths.Map[key] = append(result.Paths.Map[key], TSConfigPath{Text: str, Loc: item.Loc})
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
log.AddID(logger.MsgID_TSConfigJSON_InvalidPaths, logger.Warning, &tracker, source.RangeOfString(prop.ValueOrNil.Loc), fmt.Sprintf(
|
|
"Substitutions for pattern %q should be an array", key))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Warn about compiler options not wrapped in "compilerOptions".
|
|
// For example: https://github.com/evanw/esbuild/issues/3301
|
|
if obj, ok := json.Data.(*js_ast.EObject); ok {
|
|
loop:
|
|
for _, prop := range obj.Properties {
|
|
if key, ok := prop.Key.Data.(*js_ast.EString); ok && key.Value != nil {
|
|
key := helpers.UTF16ToString(key.Value)
|
|
switch key {
|
|
case "alwaysStrict",
|
|
"baseUrl",
|
|
"experimentalDecorators",
|
|
"importsNotUsedAsValues",
|
|
"jsx",
|
|
"jsxFactory",
|
|
"jsxFragmentFactory",
|
|
"jsxImportSource",
|
|
"paths",
|
|
"preserveValueImports",
|
|
"strict",
|
|
"target",
|
|
"useDefineForClassFields",
|
|
"verbatimModuleSyntax":
|
|
log.AddIDWithNotes(logger.MsgID_TSConfigJSON_InvalidTopLevelOption, logger.Warning, &tracker, source.RangeOfString(prop.Key.Loc),
|
|
fmt.Sprintf("Expected the %q option to be nested inside a \"compilerOptions\" object", key),
|
|
[]logger.MsgData{})
|
|
break loop
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return &result
|
|
}
|
|
|
|
// See: https://github.com/microsoft/TypeScript/pull/58042
|
|
func getSubstitutedPathWithConfigDirTemplate(fs fs.FS, value string, basePath string) string {
|
|
if strings.HasPrefix(value, "${configDir}") {
|
|
return fs.Join(basePath, "./"+value[12:])
|
|
}
|
|
return value
|
|
}
|
|
|
|
func parseMemberExpressionForJSX(log logger.Log, source *logger.Source, tracker *logger.LineColumnTracker, loc logger.Loc, text string) []string {
|
|
if text == "" {
|
|
return nil
|
|
}
|
|
parts := strings.Split(text, ".")
|
|
for _, part := range parts {
|
|
if !js_ast.IsIdentifier(part) {
|
|
warnRange := source.RangeOfString(loc)
|
|
log.AddID(logger.MsgID_TSConfigJSON_InvalidJSX, logger.Warning, tracker, warnRange, fmt.Sprintf("Invalid JSX member expression: %q", text))
|
|
return nil
|
|
}
|
|
}
|
|
return parts
|
|
}
|
|
|
|
func isValidTSConfigPathPattern(text string, log logger.Log, source *logger.Source, tracker *logger.LineColumnTracker, loc logger.Loc) bool {
|
|
foundAsterisk := false
|
|
for i := 0; i < len(text); i++ {
|
|
if text[i] == '*' {
|
|
if foundAsterisk {
|
|
r := source.RangeOfString(loc)
|
|
log.AddID(logger.MsgID_TSConfigJSON_InvalidPaths, logger.Warning, tracker, r, fmt.Sprintf(
|
|
"Invalid pattern %q, must have at most one \"*\" character", text))
|
|
return false
|
|
}
|
|
foundAsterisk = true
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isSlash(c byte) bool {
|
|
return c == '/' || c == '\\'
|
|
}
|
|
|
|
func isValidTSConfigPathNoBaseURLPattern(text string, log logger.Log, source *logger.Source, tracker **logger.LineColumnTracker, loc logger.Loc) bool {
|
|
var c0 byte
|
|
var c1 byte
|
|
var c2 byte
|
|
n := len(text)
|
|
|
|
if n > 0 {
|
|
c0 = text[0]
|
|
if n > 1 {
|
|
c1 = text[1]
|
|
if n > 2 {
|
|
c2 = text[2]
|
|
}
|
|
}
|
|
}
|
|
|
|
// Relative "." or ".."
|
|
if c0 == '.' && (n == 1 || (n == 2 && c1 == '.')) {
|
|
return true
|
|
}
|
|
|
|
// Relative "./" or "../" or ".\\" or "..\\"
|
|
if c0 == '.' && (isSlash(c1) || (c1 == '.' && isSlash(c2))) {
|
|
return true
|
|
}
|
|
|
|
// Absolute POSIX "/" or UNC "\\"
|
|
if isSlash(c0) {
|
|
return true
|
|
}
|
|
|
|
// Absolute DOS "c:/" or "c:\\"
|
|
if ((c0 >= 'a' && c0 <= 'z') || (c0 >= 'A' && c0 <= 'Z')) && c1 == ':' && isSlash(c2) {
|
|
return true
|
|
}
|
|
|
|
r := source.RangeOfString(loc)
|
|
if *tracker == nil {
|
|
t := logger.MakeLineColumnTracker(source)
|
|
*tracker = &t
|
|
}
|
|
log.AddID(logger.MsgID_TSConfigJSON_InvalidPaths, logger.Warning, *tracker, r, fmt.Sprintf(
|
|
"Non-relative path %q is not allowed when \"baseUrl\" is not set (did you forget a leading \"./\"?)", text))
|
|
return false
|
|
}
|