diff --git a/CHANGELOG.md b/CHANGELOG.md index f1fb7ce1a3d..a471bf64baa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,14 @@ The community has requested that esbuild remove support for TypeScript's `moduleSuffixes` feature, so it has been removed in this release. Instead you can use esbuild's `--resolve-extensions=` feature to select which module suffix you want to build with. + * Apply `--tsconfig=` overrides to `stdin` ([#385](https://github.com/evanw/esbuild/issues/385), [#2543](https://github.com/evanw/esbuild/issues/2543)) + + When you override esbuild's automatic `tsconfig.json` file detection with `--tsconfig=` to pass a specific `tsconfig.json` file, esbuild previously didn't apply these settings to source code passed via the `stdin` API option. This release changes esbuild's behavior so that settings from `tsconfig.json` also apply to `stdin` source code as well. + + * Support `--tsconfig-raw=` in build API calls ([#943](https://github.com/evanw/esbuild/issues/943), [#2440](https://github.com/evanw/esbuild/issues/2440)) + + Previously if you wanted to override esbuild's automatic `tsconfig.json` file detection, you had to create a new `tsconfig.json` file and pass the file name to esbuild via the `--tsconfig=` flag. With this release, you can now optionally use `--tsconfig-raw=` instead to pass the contents of `tsconfig.json` to esbuild directly instead of passing the file name. For example, you can now use `--tsconfig-raw={"compilerOptions":{"experimentalDecorators":true}}` to enable TypeScript experimental decorators directly using a command-line flag (assuming you escape the quotes correctly using your current shell's quoting rules). The `--tsconfig-raw=` flag previously only worked with transform API calls but with this release, it now works with build API calls too. + These changes are intended to improve esbuild's compatibility with `tsc` and reduce the number of unfortunate behaviors regarding `tsconfig.json` and esbuild. * Add a workaround for bugs in Safari 16.2 and earlier ([#3072](https://github.com/evanw/esbuild/issues/3072)) diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index 11de552cba3..9d53da34ee1 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -1141,10 +1141,13 @@ func ScanBundle( log.AddError(nil, logger.Range{}, fmt.Sprintf("Failed to read from randomness source: %s", err.Error())) } + // This may mutate "options" by the "tsconfig.json" override settings + res := resolver.NewResolver(fs, log, caches, &options) + s := scanner{ log: log, fs: fs, - res: resolver.NewResolver(fs, log, caches, options), + res: res, caches: caches, options: options, timer: timer, diff --git a/internal/bundler_tests/bundler_test.go b/internal/bundler_tests/bundler_test.go index 9719a138209..2e8fafcc0c7 100644 --- a/internal/bundler_tests/bundler_test.go +++ b/internal/bundler_tests/bundler_test.go @@ -153,7 +153,7 @@ func (s *suite) __expectBundledImpl(t *testing.T, args bundled, fsKind fs.MockKi args.options.AbsOutputFile = unix2win(args.options.AbsOutputFile) args.options.AbsOutputBase = unix2win(args.options.AbsOutputBase) args.options.AbsOutputDir = unix2win(args.options.AbsOutputDir) - args.options.TsConfigOverride = unix2win(args.options.TsConfigOverride) + args.options.TSConfigPath = unix2win(args.options.TSConfigPath) } // Run the bundler diff --git a/internal/bundler_tests/bundler_tsconfig_test.go b/internal/bundler_tests/bundler_tsconfig_test.go index fc4f2559eae..12c427f6026 100644 --- a/internal/bundler_tests/bundler_tsconfig_test.go +++ b/internal/bundler_tests/bundler_tsconfig_test.go @@ -1103,9 +1103,9 @@ func TestTsconfigJsonOverrideMissing(t *testing.T) { }, entryPaths: []string{"/Users/user/project/src/app/entry.ts"}, options: config.Options{ - Mode: config.ModeBundle, - AbsOutputFile: "/Users/user/project/out.js", - TsConfigOverride: "/Users/user/project/other/config-for-ts.json", + Mode: config.ModeBundle, + AbsOutputFile: "/Users/user/project/out.js", + TSConfigPath: "/Users/user/project/other/config-for-ts.json", }, }) } @@ -1148,9 +1148,9 @@ func TestTsconfigJsonOverrideNodeModules(t *testing.T) { }, entryPaths: []string{"/Users/user/project/src/app/entry.ts"}, options: config.Options{ - Mode: config.ModeBundle, - AbsOutputFile: "/Users/user/project/out.js", - TsConfigOverride: "/Users/user/project/other/config-for-ts.json", + Mode: config.ModeBundle, + AbsOutputFile: "/Users/user/project/out.js", + TSConfigPath: "/Users/user/project/other/config-for-ts.json", }, }) } @@ -1162,9 +1162,9 @@ func TestTsconfigJsonOverrideInvalid(t *testing.T) { }, entryPaths: []string{"/entry.ts"}, options: config.Options{ - Mode: config.ModeBundle, - AbsOutputFile: "/out.js", - TsConfigOverride: "/this/file/doesn't/exist/tsconfig.json", + Mode: config.ModeBundle, + AbsOutputFile: "/out.js", + TSConfigPath: "/this/file/doesn't/exist/tsconfig.json", }, expectedScanLog: `ERROR: Cannot find tsconfig file "this/file/doesn't/exist/tsconfig.json" `, @@ -1918,10 +1918,10 @@ func TestTsConfigExtendsDotWithoutSlash(t *testing.T) { }, entryPaths: []string{"/Users/user/project/src/main.tsx"}, options: config.Options{ - Mode: config.ModeBundle, - AbsOutputDir: "/Users/user/project/out", - OutputFormat: config.FormatESModule, - TsConfigOverride: "/Users/user/project/src/foo.json", + Mode: config.ModeBundle, + AbsOutputDir: "/Users/user/project/out", + OutputFormat: config.FormatESModule, + TSConfigPath: "/Users/user/project/src/foo.json", }, }) } @@ -1967,10 +1967,10 @@ func TestTsConfigExtendsDotWithSlash(t *testing.T) { }, entryPaths: []string{"/Users/user/project/src/main.tsx"}, options: config.Options{ - Mode: config.ModeBundle, - AbsOutputDir: "/Users/user/project/out", - OutputFormat: config.FormatESModule, - TsConfigOverride: "/Users/user/project/src/foo.json", + Mode: config.ModeBundle, + AbsOutputDir: "/Users/user/project/out", + OutputFormat: config.FormatESModule, + TSConfigPath: "/Users/user/project/src/foo.json", }, expectedScanLog: `Users/user/project/src/foo.json: WARNING: Cannot find base config file "./" `, diff --git a/internal/config/config.go b/internal/config/config.go index 247094967bb..b8462a44ff9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -351,7 +351,8 @@ type Options struct { OutputExtensionJS string OutputExtensionCSS string GlobalName []string - TsConfigOverride string + TSConfigPath string + TSConfigRaw string ExtensionToLoader map[string]Loader PublicPath string diff --git a/internal/resolver/resolver.go b/internal/resolver/resolver.go index 3f67bbc1c79..0fb17ec0ba7 100644 --- a/internal/resolver/resolver.go +++ b/internal/resolver/resolver.go @@ -163,6 +163,8 @@ type Resolver struct { log logger.Log caches *cache.CacheSet + tsConfigOverride *TSConfigJSON + // These are sets that represent various conditions for the "exports" field // in package.json. esmConditionsDefault map[string]bool @@ -220,7 +222,7 @@ type resolverQuery struct { kind ast.ImportKind } -func NewResolver(fs fs.FS, log logger.Log, caches *cache.CacheSet, options config.Options) *Resolver { +func NewResolver(fs fs.FS, log logger.Log, caches *cache.CacheSet, options *config.Options) *Resolver { // Filter out non-CSS extensions for CSS "@import" imports atImportExtensionOrder := make([]string, 0, len(options.ExtensionOrder)) for _, ext := range options.ExtensionOrder { @@ -250,10 +252,10 @@ func NewResolver(fs fs.FS, log logger.Log, caches *cache.CacheSet, options confi fs.Cwd() - return &Resolver{ + res := &Resolver{ fs: fs, log: log, - options: options, + options: *options, caches: caches, dirCache: make(map[string]*dirInfo), atImportExtensionOrder: atImportExtensionOrder, @@ -261,6 +263,47 @@ func NewResolver(fs fs.FS, log logger.Log, caches *cache.CacheSet, options confi esmConditionsImport: esmConditionsImport, esmConditionsRequire: esmConditionsRequire, } + + // Handle the "tsconfig.json" override when the resolver is created. This + // isn't done when we validate the build options both because the code for + // "tsconfig.json" handling is already in the resolver, and because we want + // watch mode to pick up changes to "tsconfig.json" and rebuild. + var debugMeta DebugMeta + if options.TSConfigPath != "" || options.TSConfigRaw != "" { + r := resolverQuery{ + Resolver: res, + debugMeta: &debugMeta, + } + var err error + if options.TSConfigPath != "" { + res.tsConfigOverride, err = r.parseTSConfig(options.TSConfigPath, make(map[string]bool)) + } else { + source := logger.Source{ + KeyPath: logger.Path{Text: fs.Join(fs.Cwd(), ""), Namespace: "file"}, + PrettyPath: "", + Contents: options.TSConfigRaw, + } + res.tsConfigOverride, err = r.parseTSConfigFromSource(source, make(map[string]bool)) + } + if err != nil { + if err == syscall.ENOENT { + r.log.AddError(nil, logger.Range{}, fmt.Sprintf("Cannot find tsconfig file %q", + PrettyPath(r.fs, logger.Path{Text: options.TSConfigPath, Namespace: "file"}))) + } else if err != errParseErrorAlreadyLogged { + r.log.AddError(nil, logger.Range{}, fmt.Sprintf("Cannot read file %q: %s", + PrettyPath(r.fs, logger.Path{Text: options.TSConfigPath, Namespace: "file"}), err.Error())) + } + } + } + + // Mutate the provided options by settings from "tsconfig.json" if present + if res.tsConfigOverride != nil { + options.TS.Config = res.tsConfigOverride.Settings + res.tsConfigOverride.JSXSettings.ApplyTo(&options.JSX) + options.TSAlwaysStrict = res.tsConfigOverride.TSAlwaysStrictOrStrict() + } + + return res } func (res *Resolver) Resolve(sourceDir string, importPath string, kind ast.ImportKind) (*ResolveResult, DebugMeta) { @@ -662,44 +705,46 @@ func (r resolverQuery) finalizeResolve(result *ResolveResult) { } // Copy various fields from the nearest enclosing "tsconfig.json" file if present - if path == &result.PathPair.Primary && dirInfo.enclosingTSConfigJSON != nil { - // Except don't do this if we're inside a "node_modules" directory. Package - // authors often publish their "tsconfig.json" files to npm because of - // npm's default-include publishing model and because these authors - // probably don't know about ".npmignore" files. - // - // People trying to use these packages with esbuild have historically - // complained that esbuild is respecting "tsconfig.json" in these cases. - // The assumption is that the package author published these files by - // accident. - // - // Ignoring "tsconfig.json" files inside "node_modules" directories breaks - // the use case of publishing TypeScript code and having it be transpiled - // for you, but that's the uncommon case and likely doesn't work with - // many other tools anyway. So now these files are ignored. - if helpers.IsInsideNodeModules(result.PathPair.Primary.Text) { - if r.debugLogs != nil { - r.debugLogs.addNote(fmt.Sprintf("Ignoring %q because %q is inside \"node_modules\"", - dirInfo.enclosingTSConfigJSON.AbsPath, - result.PathPair.Primary.Text)) - } - } else { - result.TSConfig = &dirInfo.enclosingTSConfigJSON.Settings - result.TSConfigJSX = dirInfo.enclosingTSConfigJSON.JSXSettings - result.TSAlwaysStrict = dirInfo.enclosingTSConfigJSON.TSAlwaysStrictOrStrict() - - if r.debugLogs != nil { - r.debugLogs.addNote(fmt.Sprintf("This import is under the effect of %q", - dirInfo.enclosingTSConfigJSON.AbsPath)) - if result.TSConfigJSX.JSXFactory != nil { - r.debugLogs.addNote(fmt.Sprintf("\"jsxFactory\" is %q due to %q", - strings.Join(result.TSConfigJSX.JSXFactory, "."), - dirInfo.enclosingTSConfigJSON.AbsPath)) + if path == &result.PathPair.Primary { + if tsConfigJSON := r.tsConfigForDir(dirInfo); tsConfigJSON != nil { + // Except don't do this if we're inside a "node_modules" directory. Package + // authors often publish their "tsconfig.json" files to npm because of + // npm's default-include publishing model and because these authors + // probably don't know about ".npmignore" files. + // + // People trying to use these packages with esbuild have historically + // complained that esbuild is respecting "tsconfig.json" in these cases. + // The assumption is that the package author published these files by + // accident. + // + // Ignoring "tsconfig.json" files inside "node_modules" directories breaks + // the use case of publishing TypeScript code and having it be transpiled + // for you, but that's the uncommon case and likely doesn't work with + // many other tools anyway. So now these files are ignored. + if helpers.IsInsideNodeModules(result.PathPair.Primary.Text) { + if r.debugLogs != nil { + r.debugLogs.addNote(fmt.Sprintf("Ignoring %q because %q is inside \"node_modules\"", + tsConfigJSON.AbsPath, + result.PathPair.Primary.Text)) } - if result.TSConfigJSX.JSXFragmentFactory != nil { - r.debugLogs.addNote(fmt.Sprintf("\"jsxFragment\" is %q due to %q", - strings.Join(result.TSConfigJSX.JSXFragmentFactory, "."), - dirInfo.enclosingTSConfigJSON.AbsPath)) + } else { + result.TSConfig = &tsConfigJSON.Settings + result.TSConfigJSX = tsConfigJSON.JSXSettings + result.TSAlwaysStrict = tsConfigJSON.TSAlwaysStrictOrStrict() + + if r.debugLogs != nil { + r.debugLogs.addNote(fmt.Sprintf("This import is under the effect of %q", + tsConfigJSON.AbsPath)) + if result.TSConfigJSX.JSXFactory != nil { + r.debugLogs.addNote(fmt.Sprintf("\"jsxFactory\" is %q due to %q", + strings.Join(result.TSConfigJSX.JSXFactory, "."), + tsConfigJSON.AbsPath)) + } + if result.TSConfigJSX.JSXFragmentFactory != nil { + r.debugLogs.addNote(fmt.Sprintf("\"jsxFragment\" is %q due to %q", + strings.Join(result.TSConfigJSX.JSXFragmentFactory, "."), + tsConfigJSON.AbsPath)) + } } } } @@ -755,8 +800,8 @@ func (r resolverQuery) resolveWithoutSymlinks(sourceDir string, sourceDirInfo *d } // First, check path overrides from the nearest enclosing TypeScript "tsconfig.json" file - if sourceDirInfo != nil && sourceDirInfo.enclosingTSConfigJSON != nil && sourceDirInfo.enclosingTSConfigJSON.Paths != nil { - if absolute, ok, diffCase := r.matchTSConfigPaths(sourceDirInfo.enclosingTSConfigJSON, importPath); ok { + if tsConfigJSON := r.tsConfigForDir(sourceDirInfo); tsConfigJSON != nil && tsConfigJSON.Paths != nil { + if absolute, ok, diffCase := r.matchTSConfigPaths(tsConfigJSON, importPath); ok { return &ResolveResult{PathPair: absolute, DifferentCase: diffCase} } } @@ -902,6 +947,16 @@ type dirInfo struct { hasNodeModules bool // Is there a "node_modules" subdirectory? } +func (r resolverQuery) tsConfigForDir(dirInfo *dirInfo) *TSConfigJSON { + if r.tsConfigOverride != nil { + return r.tsConfigOverride + } + if dirInfo != nil { + return dirInfo.enclosingTSConfigJSON + } + return nil +} + func (r resolverQuery) dirInfoCached(path string) *dirInfo { // First, check the cache cached, ok := r.dirCache[path] @@ -945,7 +1000,6 @@ func (r resolverQuery) parseTSConfig(file string, visited map[string]bool) (*TSC if visited[file] { return nil, errParseErrorImportCycle } - isExtends := len(visited) != 0 visited[file] = true contents, err, originalError := r.caches.FSCache.ReadFile(r.fs, file) @@ -965,8 +1019,13 @@ func (r resolverQuery) parseTSConfig(file string, visited map[string]bool) (*TSC PrettyPath: PrettyPath(r.fs, keyPath), Contents: contents, } + return r.parseTSConfigFromSource(source, visited) +} + +func (r resolverQuery) parseTSConfigFromSource(source logger.Source, visited map[string]bool) (*TSConfigJSON, error) { tracker := logger.MakeLineColumnTracker(&source) - fileDir := r.fs.Dir(file) + fileDir := r.fs.Dir(source.KeyPath.Text) + isExtends := len(visited) > 1 result := ParseTSConfigJSON(r.log, source, &r.caches.JSONCache, func(extends string, extendsRange logger.Range) *TSConfigJSON { // Note: This doesn't use the normal node module resolution algorithm @@ -1163,7 +1222,7 @@ func (r resolverQuery) parseTSConfig(file string, visited map[string]bool) (*TSC // Suppress warnings about missing base config files inside "node_modules" pnpError: - if !helpers.IsInsideNodeModules(file) { + if !helpers.IsInsideNodeModules(source.KeyPath.Text) { r.log.AddID(logger.MsgID_TSConfigJSON_Missing, logger.Warning, &tracker, extendsRange, fmt.Sprintf("Cannot find base config file %q", extends)) } @@ -1178,7 +1237,7 @@ func (r resolverQuery) parseTSConfig(file string, visited map[string]bool) (*TSC // Warn when people try to set esbuild's target via "tsconfig.json" and esbuild's target is unset if result.Settings.Target != config.TSTargetUnspecified && r.options.OriginalTargetEnv == "" && // Don't warn if the target is "ESNext" since esbuild's target also defaults to "esnext" (so that case is harmless) - result.tsTargetKey.LowerValue != "esnext" && !helpers.IsInsideNodeModules(file) { + result.tsTargetKey.LowerValue != "esnext" && !helpers.IsInsideNodeModules(source.KeyPath.Text) { var example string switch logger.API { case logger.CLIAPI: @@ -1320,17 +1379,12 @@ func (r resolverQuery) dirInfoUncached(path string) *dirInfo { } // Record if this directory has a tsconfig.json or jsconfig.json file - { + if r.tsConfigOverride == nil { var tsConfigPath string - if forceTsConfig := r.options.TsConfigOverride; forceTsConfig == "" { - if entry, _ := entries.Get("tsconfig.json"); entry != nil && entry.Kind(r.fs) == fs.FileEntry { - tsConfigPath = r.fs.Join(path, "tsconfig.json") - } else if entry, _ := entries.Get("jsconfig.json"); entry != nil && entry.Kind(r.fs) == fs.FileEntry { - tsConfigPath = r.fs.Join(path, "jsconfig.json") - } - } else if parentInfo == nil { - // If there is a tsconfig.json override, mount it at the root directory - tsConfigPath = forceTsConfig + if entry, _ := entries.Get("tsconfig.json"); entry != nil && entry.Kind(r.fs) == fs.FileEntry { + tsConfigPath = r.fs.Join(path, "tsconfig.json") + } else if entry, _ := entries.Get("jsconfig.json"); entry != nil && entry.Kind(r.fs) == fs.FileEntry { + tsConfigPath = r.fs.Join(path, "jsconfig.json") } if tsConfigPath != "" { var err error @@ -2026,17 +2080,17 @@ func (r resolverQuery) loadNodeModules(importPath string, dirInfo *dirInfo, forb } // First, check path overrides from the nearest enclosing TypeScript "tsconfig.json" file - if dirInfo.enclosingTSConfigJSON != nil { + if tsConfigJSON := r.tsConfigForDir(dirInfo); tsConfigJSON != nil { // Try path substitutions first - if dirInfo.enclosingTSConfigJSON.Paths != nil { - if absolute, ok, diffCase := r.matchTSConfigPaths(dirInfo.enclosingTSConfigJSON, importPath); ok { + if tsConfigJSON.Paths != nil { + if absolute, ok, diffCase := r.matchTSConfigPaths(tsConfigJSON, importPath); ok { return absolute, true, diffCase } } // Try looking up the path relative to the base URL - if dirInfo.enclosingTSConfigJSON.BaseURL != nil { - basePath := r.fs.Join(*dirInfo.enclosingTSConfigJSON.BaseURL, importPath) + if tsConfigJSON.BaseURL != nil { + basePath := r.fs.Join(*tsConfigJSON.BaseURL, importPath) if absolute, ok, diffCase := r.loadAsFileOrDirectory(basePath); ok { return absolute, true, diffCase } diff --git a/internal/resolver/tsconfig_json.go b/internal/resolver/tsconfig_json.go index 856600e41e8..386813a7623 100644 --- a/internal/resolver/tsconfig_json.go +++ b/internal/resolver/tsconfig_json.go @@ -117,8 +117,6 @@ func ParseTSConfigJSON( if valueJSON, _, ok := getProperty(compilerOptionsJSON, "jsx"); ok { if value, ok := getString(valueJSON); ok { switch strings.ToLower(value) { - case "none": - result.JSXSettings.JSX = config.TSJSXNone case "preserve": result.JSXSettings.JSX = config.TSJSXPreserve case "react-native": diff --git a/internal/resolver/yarnpnp_test.go b/internal/resolver/yarnpnp_test.go index 373675fb44c..8dd87f5e58c 100644 --- a/internal/resolver/yarnpnp_test.go +++ b/internal/resolver/yarnpnp_test.go @@ -68,7 +68,7 @@ func TestYarnPnP(t *testing.T) { func(current pnpTest) { t.Run(current.It, func(t *testing.T) { fs := fs.MockFS(nil, fs.MockUnix, "/") - r := resolverQuery{Resolver: NewResolver(fs, logger.NewDeferLog(logger.DeferLogNoVerboseOrDebug, nil), nil, config.Options{})} + r := resolverQuery{Resolver: NewResolver(fs, logger.NewDeferLog(logger.DeferLogNoVerboseOrDebug, nil), nil, &config.Options{})} result := r.resolveToUnqualified(current.Imported, current.Importer, manifest) var observed string diff --git a/lib/shared/common.ts b/lib/shared/common.ts index dc98695e4f0..d3964a64aa8 100644 --- a/lib/shared/common.ts +++ b/lib/shared/common.ts @@ -161,6 +161,7 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe let pure = getFlag(options, keys, 'pure', mustBeArray) let keepNames = getFlag(options, keys, 'keepNames', mustBeBoolean) let platform = getFlag(options, keys, 'platform', mustBeString) + let tsconfigRaw = getFlag(options, keys, 'tsconfigRaw', mustBeStringOrObject) if (legalComments) flags.push(`--legal-comments=${legalComments}`) if (sourceRoot !== void 0) flags.push(`--source-root=${sourceRoot}`) @@ -172,6 +173,7 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe if (format) flags.push(`--format=${format}`) if (globalName) flags.push(`--global-name=${globalName}`) if (platform) flags.push(`--platform=${platform}`) + if (tsconfigRaw) flags.push(`--tsconfig-raw=${typeof tsconfigRaw === 'string' ? tsconfigRaw : JSON.stringify(tsconfigRaw)}`) if (minify) flags.push('--minify') if (minifySyntax) flags.push('--minify-syntax') @@ -422,7 +424,6 @@ function flagsForTransformOptions( pushCommonFlags(flags, options, keys) let sourcemap = getFlag(options, keys, 'sourcemap', mustBeStringOrBoolean) - let tsconfigRaw = getFlag(options, keys, 'tsconfigRaw', mustBeStringOrObject) let sourcefile = getFlag(options, keys, 'sourcefile', mustBeString) let loader = getFlag(options, keys, 'loader', mustBeString) let banner = getFlag(options, keys, 'banner', mustBeString) @@ -431,7 +432,6 @@ function flagsForTransformOptions( checkForInvalidFlags(options, keys, `in ${callName}() call`) if (sourcemap) flags.push(`--sourcemap=${sourcemap === true ? 'external' : sourcemap}`) - if (tsconfigRaw) flags.push(`--tsconfig-raw=${typeof tsconfigRaw === 'string' ? tsconfigRaw : JSON.stringify(tsconfigRaw)}`) if (sourcefile) flags.push(`--sourcefile=${sourcefile}`) if (loader) flags.push(`--loader=${loader}`) if (banner) flags.push(`--banner=${banner}`) diff --git a/lib/shared/types.ts b/lib/shared/types.ts index ec0f6001f9a..f3d434c7e35 100644 --- a/lib/shared/types.ts +++ b/lib/shared/types.ts @@ -79,6 +79,26 @@ interface CommonOptions { logLimit?: number /** Documentation: https://esbuild.github.io/api/#log-override */ logOverride?: Record + + /** Documentation: https://esbuild.github.io/api/#tsconfig-raw */ + tsconfigRaw?: string | { + compilerOptions?: { + alwaysStrict?: boolean + baseUrl?: boolean + experimentalDecorators?: boolean + importsNotUsedAsValues?: 'remove' | 'preserve' | 'error' + jsx?: 'preserve' | 'react-native' | 'react' | 'react-jsx' | 'react-jsxdev' + jsxFactory?: string + jsxFragmentFactory?: string + jsxImportSource?: string + paths?: Record + preserveValueImports?: boolean + strict?: boolean + target?: string + useDefineForClassFields?: boolean + verbatimModuleSyntax?: boolean + } + } } export interface BuildOptions extends CommonOptions { @@ -233,23 +253,13 @@ export interface ServeResult { } export interface TransformOptions extends CommonOptions { - tsconfigRaw?: string | { - compilerOptions?: { - alwaysStrict?: boolean, - importsNotUsedAsValues?: 'remove' | 'preserve' | 'error', - jsx?: 'react' | 'react-jsx' | 'react-jsxdev' | 'preserve', - jsxFactory?: string, - jsxFragmentFactory?: string, - jsxImportSource?: string, - preserveValueImports?: boolean, - target?: string, - useDefineForClassFields?: boolean, - }, - } - + /** Documentation: https://esbuild.github.io/api/#sourcefile */ sourcefile?: string + /** Documentation: https://esbuild.github.io/api/#loader */ loader?: Loader + /** Documentation: https://esbuild.github.io/api/#banner */ banner?: string + /** Documentation: https://esbuild.github.io/api/#footer */ footer?: string } diff --git a/pkg/api/api.go b/pkg/api/api.go index b1a383d2c95..cbbc7d0cb3f 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -320,6 +320,7 @@ type BuildOptions struct { Loader map[string]Loader // Documentation: https://esbuild.github.io/api/#loader ResolveExtensions []string // Documentation: https://esbuild.github.io/api/#resolve-extensions Tsconfig string // Documentation: https://esbuild.github.io/api/#tsconfig + TsconfigRaw string // Documentation: https://esbuild.github.io/api/#tsconfig-raw OutExtension map[string]string // Documentation: https://esbuild.github.io/api/#out-extension PublicPath string // Documentation: https://esbuild.github.io/api/#public-path Inject []string // Documentation: https://esbuild.github.io/api/#inject diff --git a/pkg/api/api_impl.go b/pkg/api/api_impl.go index 4a4ef5bab08..452a97438a9 100644 --- a/pkg/api/api_impl.go +++ b/pkg/api/api_impl.go @@ -1303,7 +1303,8 @@ func validateBuildOptions( ExternalSettings: validateExternals(log, realFS, buildOpts.External), ExternalPackages: buildOpts.Packages == PackagesExternal, PackageAliases: validateAlias(log, realFS, buildOpts.Alias), - TsConfigOverride: validatePath(log, realFS, buildOpts.Tsconfig, "tsconfig path"), + TSConfigPath: validatePath(log, realFS, buildOpts.Tsconfig, "tsconfig path"), + TSConfigRaw: buildOpts.TsconfigRaw, MainFields: buildOpts.MainFields, PublicPath: buildOpts.PublicPath, KeepNames: buildOpts.KeepNames, @@ -1416,6 +1417,11 @@ func validateBuildOptions( log.AddError(nil, logger.Range{}, "Splitting currently only works with the \"esm\" format") } + // Code splitting is experimental and currently only enabled for ES6 modules + if options.TSConfigPath != "" && options.TSConfigRaw != "" { + log.AddError(nil, logger.Range{}, "Cannot provide \"tsconfig\" as both a raw string and a path") + } + // If we aren't writing the output to the file system, then we can allow the // output paths to be the same as the input paths. This helps when serving. if !buildOpts.Write { @@ -1668,34 +1674,7 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult LogLevel: validateLogLevel(transformOpts.LogLevel), Overrides: validateLogOverrides(transformOpts.LogOverride), }) - - // Settings from the user come first - var tsConfig config.TSConfig - jsx := config.JSXOptions{ - Preserve: transformOpts.JSX == JSXPreserve, - AutomaticRuntime: transformOpts.JSX == JSXAutomatic, - Factory: validateJSXExpr(log, transformOpts.JSXFactory, "factory"), - Fragment: validateJSXExpr(log, transformOpts.JSXFragment, "fragment"), - Development: transformOpts.JSXDev, - ImportSource: transformOpts.JSXImportSource, - SideEffects: transformOpts.JSXSideEffects, - } - - // Settings from "tsconfig.json" override those - var tsAlwaysStrict *config.TSAlwaysStrict caches := cache.MakeCacheSet() - if transformOpts.TsconfigRaw != "" { - source := logger.Source{ - KeyPath: logger.Path{Text: "tsconfig.json"}, - PrettyPath: "tsconfig.json", - Contents: transformOpts.TsconfigRaw, - } - if result := resolver.ParseTSConfigJSON(log, source, &caches.JSONCache, nil); result != nil { - tsConfig = result.Settings - result.JSXSettings.ApplyTo(&jsx) - tsAlwaysStrict = result.TSAlwaysStrictOrStrict() - } - } // Apply default values if transformOpts.Sourcefile == "" { @@ -1719,30 +1698,37 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult UnsupportedCSSFeatureOverrides: cssOverrides, UnsupportedCSSFeatureOverridesMask: cssMask, OriginalTargetEnv: targetEnv, - TS: config.TSOptions{Config: tsConfig}, - TSAlwaysStrict: tsAlwaysStrict, - JSX: jsx, - Defines: defines, - InjectedDefines: injectedDefines, - Platform: platform, - SourceMap: validateSourceMap(transformOpts.Sourcemap), - LegalComments: validateLegalComments(transformOpts.LegalComments, false /* bundle */), - SourceRoot: transformOpts.SourceRoot, - ExcludeSourcesContent: transformOpts.SourcesContent == SourcesContentExclude, - OutputFormat: validateFormat(transformOpts.Format), - GlobalName: validateGlobalName(log, transformOpts.GlobalName), - MinifySyntax: transformOpts.MinifySyntax, - MinifyWhitespace: transformOpts.MinifyWhitespace, - MinifyIdentifiers: transformOpts.MinifyIdentifiers, - MangleProps: validateRegex(log, "mangle props", transformOpts.MangleProps), - ReserveProps: validateRegex(log, "reserve props", transformOpts.ReserveProps), - MangleQuoted: transformOpts.MangleQuoted == MangleQuotedTrue, - DropDebugger: (transformOpts.Drop & DropDebugger) != 0, - ASCIIOnly: validateASCIIOnly(transformOpts.Charset), - IgnoreDCEAnnotations: transformOpts.IgnoreAnnotations, - TreeShaking: validateTreeShaking(transformOpts.TreeShaking, false /* bundle */, transformOpts.Format), - AbsOutputFile: transformOpts.Sourcefile + "-out", - KeepNames: transformOpts.KeepNames, + TSConfigRaw: transformOpts.TsconfigRaw, + JSX: config.JSXOptions{ + Preserve: transformOpts.JSX == JSXPreserve, + AutomaticRuntime: transformOpts.JSX == JSXAutomatic, + Factory: validateJSXExpr(log, transformOpts.JSXFactory, "factory"), + Fragment: validateJSXExpr(log, transformOpts.JSXFragment, "fragment"), + Development: transformOpts.JSXDev, + ImportSource: transformOpts.JSXImportSource, + SideEffects: transformOpts.JSXSideEffects, + }, + Defines: defines, + InjectedDefines: injectedDefines, + Platform: platform, + SourceMap: validateSourceMap(transformOpts.Sourcemap), + LegalComments: validateLegalComments(transformOpts.LegalComments, false /* bundle */), + SourceRoot: transformOpts.SourceRoot, + ExcludeSourcesContent: transformOpts.SourcesContent == SourcesContentExclude, + OutputFormat: validateFormat(transformOpts.Format), + GlobalName: validateGlobalName(log, transformOpts.GlobalName), + MinifySyntax: transformOpts.MinifySyntax, + MinifyWhitespace: transformOpts.MinifyWhitespace, + MinifyIdentifiers: transformOpts.MinifyIdentifiers, + MangleProps: validateRegex(log, "mangle props", transformOpts.MangleProps), + ReserveProps: validateRegex(log, "reserve props", transformOpts.ReserveProps), + MangleQuoted: transformOpts.MangleQuoted == MangleQuotedTrue, + DropDebugger: (transformOpts.Drop & DropDebugger) != 0, + ASCIIOnly: validateASCIIOnly(transformOpts.Charset), + IgnoreDCEAnnotations: transformOpts.IgnoreAnnotations, + TreeShaking: validateTreeShaking(transformOpts.TreeShaking, false /* bundle */, transformOpts.Format), + AbsOutputFile: transformOpts.Sourcefile + "-out", + KeepNames: transformOpts.KeepNames, Stdin: &config.StdinInfo{ Loader: validateLoader(transformOpts.Loader), Contents: input, @@ -2054,7 +2040,8 @@ func loadPlugins(initialOptions *BuildOptions, fs fs.FS, log logger.Log, caches // Make a new resolver so it has its own log log := logger.NewDeferLog(logger.DeferLogNoVerboseOrDebug, validateLogOverrides(initialOptions.LogOverride)) - resolver := resolver.NewResolver(fs, log, caches, *optionsForResolve) + optionsClone := *optionsForResolve + resolver := resolver.NewResolver(fs, log, caches, &optionsClone) // Make sure the resolve directory is an absolute path, which can fail absResolveDir := validatePath(log, fs, options.ResolveDir, "resolve directory") diff --git a/pkg/cli/cli_impl.go b/pkg/cli/cli_impl.go index e7b230c10bf..0d9bbbd1459 100644 --- a/pkg/cli/cli_impl.go +++ b/pkg/cli/cli_impl.go @@ -414,8 +414,12 @@ func parseOptionsImpl( case strings.HasPrefix(arg, "--tsconfig=") && buildOpts != nil: buildOpts.Tsconfig = arg[len("--tsconfig="):] - case strings.HasPrefix(arg, "--tsconfig-raw=") && transformOpts != nil: - transformOpts.TsconfigRaw = arg[len("--tsconfig-raw="):] + case strings.HasPrefix(arg, "--tsconfig-raw="): + if buildOpts != nil { + buildOpts.TsconfigRaw = arg[len("--tsconfig-raw="):] + } else { + transformOpts.TsconfigRaw = arg[len("--tsconfig-raw="):] + } case strings.HasPrefix(arg, "--entry-names=") && buildOpts != nil: buildOpts.EntryNames = arg[len("--entry-names="):] diff --git a/scripts/js-api-tests.js b/scripts/js-api-tests.js index 401e43a5083..279b1056570 100644 --- a/scripts/js-api-tests.js +++ b/scripts/js-api-tests.js @@ -2180,6 +2180,73 @@ console.log("success"); `) }, + async forceTsConfigRaw({ esbuild, testDir }) { + // ./a/tsconfig.json + // ./a/b/test-impl.js + // ./a/b/c/in.js + const aDir = path.join(testDir, 'a') + const bDir = path.join(aDir, 'b') + const cDir = path.join(bDir, 'c') + await mkdirAsync(aDir).catch(x => x) + await mkdirAsync(bDir).catch(x => x) + await mkdirAsync(cDir).catch(x => x) + const input = path.join(cDir, 'in.js') + const forced = path.join(bDir, 'test-impl.js') + const tsconfigIgnore = path.join(aDir, 'tsconfig.json') + const output = path.join(testDir, 'out.js') + await writeFileAsync(input, 'import "test"') + await writeFileAsync(forced, 'console.log("success")') + await writeFileAsync(tsconfigIgnore, '{"compilerOptions": {"baseUrl": "./b", "paths": {"test": ["./ignore.js"]}}}') + await esbuild.build({ + entryPoints: [input], + bundle: true, + outfile: output, + absWorkingDir: testDir, // "paths" are resolved relative to the current working directory + tsconfigRaw: { + compilerOptions: { + baseUrl: './a/b', + paths: { + test: ['./test-impl.js'], + }, + }, + }, + format: 'esm', + }) + const result = await readFileAsync(output, 'utf8') + assert.strictEqual(result, `// a/b/test-impl.js +console.log("success"); +`) + }, + + async forceTsConfigRawStdin({ esbuild, testDir }) { + const input = path.join(testDir, 'in.js') + const test = path.join(testDir, 'test-impl.js') + const output = path.join(testDir, 'out.js') + await writeFileAsync(input, 'import "test"') + await writeFileAsync(test, 'console.log("success")') + await esbuild.build({ + stdin: { + contents: `import "test"`, + resolveDir: '.', + }, + bundle: true, + outfile: output, + absWorkingDir: testDir, + tsconfigRaw: { + compilerOptions: { + paths: { + test: ['./test-impl.js'], + }, + }, + }, + format: 'esm', + }) + const result = await readFileAsync(output, 'utf8') + assert.strictEqual(result, `// test-impl.js +console.log("success"); +`) + }, + async es5({ esbuild, testDir }) { const input = path.join(testDir, 'in.js') const cjs = path.join(testDir, 'cjs.js')