From 86900b418e10c900e1b1eaad3b3e0b19380e0fa1 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Wed, 26 May 2021 17:53:26 -0700 Subject: [PATCH] fix #277: respect "target" in "tsconfig.json" --- CHANGELOG.md | 4 + internal/bundler/bundler.go | 1 + internal/bundler/bundler_tsconfig_test.go | 81 +++++++++++++++++++ .../bundler/snapshots/snapshots_tsconfig.txt | 27 +++++++ internal/config/config.go | 8 ++ internal/js_parser/js_parser.go | 13 +++ internal/js_parser/js_parser_lower.go | 36 +++++---- internal/resolver/resolver.go | 3 + internal/resolver/tsconfig_json.go | 43 ++++++++++ pkg/api/api_impl.go | 3 + 10 files changed, 204 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6579f197d2..2f8cabbb97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ This release contains a potential fix for an unverified issue with non-determinism in esbuild. The regression was apparently introduced in 0.11.13 and may be related to parallelism that was introduced around the point where dynamic `import()` expressions are added to the list of entry points. Hopefully this fix should resolve the regression. +* Respect `target` in `tsconfig.json` ([#277](https://github.com/evanw/esbuild/issues/277)) + + Each JavaScript file that esbuild bundles will now be transformed according to the [`target`](https://www.typescriptlang.org/tsconfig#target) language level from the nearest enclosing `tsconfig.json` file. This is in addition to esbuild's own `--target` setting; the two settings are merged by transforming any JavaScript language feature that is unsupported in either esbuild's configured `--target` value or the `target` property in the `tsconfig.json` file. + ## 0.12.3 * Ensure JSX element names start with a capital letter ([#1309](https://github.com/evanw/esbuild/issues/1309)) diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index ee8b337d8b..abf3426469 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -1068,6 +1068,7 @@ func (s *scanner) maybeParseFile( if resolveResult.PreserveUnusedImportsTS { optionsClone.PreserveUnusedImportsTS = true } + optionsClone.TSTarget = resolveResult.TSTarget // Set the module type preference using node's module type rules if strings.HasSuffix(path.Text, ".mjs") { diff --git a/internal/bundler/bundler_tsconfig_test.go b/internal/bundler/bundler_tsconfig_test.go index 3704c62a8e..eff4b0dbd9 100644 --- a/internal/bundler/bundler_tsconfig_test.go +++ b/internal/bundler/bundler_tsconfig_test.go @@ -1097,3 +1097,84 @@ func TestTsconfigPreserveUnusedImportClause(t *testing.T) { }, }) } + +func TestTsconfigTarget(t *testing.T) { + tsconfig_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/Users/user/project/src/entry.ts": ` + import "./es2018" + import "./es2019" + import "./es2020" + import "./es4" + `, + "/Users/user/project/src/es2018/index.ts": ` + let x = { ...y } // es2018 syntax + try { y } catch {} // es2019 syntax + x?.y() // es2020 syntax + `, + "/Users/user/project/src/es2019/index.ts": ` + let x = { ...y } // es2018 syntax + try { y } catch {} // es2019 syntax + x?.y() // es2020 syntax + `, + "/Users/user/project/src/es2020/index.ts": ` + let x = { ...y } // es2018 syntax + try { y } catch {} // es2019 syntax + x?.y() // es2020 syntax + `, + "/Users/user/project/src/es4/index.ts": ` + `, + "/Users/user/project/src/es2018/tsconfig.json": `{ + "compilerOptions": { + "target": "ES2018" + } + }`, + "/Users/user/project/src/es2019/tsconfig.json": `{ + "compilerOptions": { + "target": "es2019" + } + }`, + "/Users/user/project/src/es2020/tsconfig.json": `{ + "compilerOptions": { + "target": "ESNext" + } + }`, + "/Users/user/project/src/es4/tsconfig.json": `{ + "compilerOptions": { + "target": "ES4" + } + }`, + }, + entryPaths: []string{"/Users/user/project/src/entry.ts"}, + options: config.Options{ + Mode: config.ModeBundle, + AbsOutputFile: "/Users/user/project/out.js", + }, + expectedScanLog: `Users/user/project/src/es4/tsconfig.json: warning: Unrecognized target environment "ES4" +`, + }) +} + +func TestTsconfigTargetError(t *testing.T) { + tsconfig_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/Users/user/project/src/entry.ts": ` + x = 123n + `, + "/Users/user/project/src/tsconfig.json": `{ + "compilerOptions": { + "target": "ES2019" + } + }`, + }, + entryPaths: []string{"/Users/user/project/src/entry.ts"}, + options: config.Options{ + Mode: config.ModeBundle, + AbsOutputFile: "/Users/user/project/out.js", + OriginalTargetEnv: "\"esnext\"", // This should not be reported as the cause of the error + }, + expectedScanLog: `Users/user/project/src/entry.ts: error: Big integer literals are not available in the configured target environment ("ES2019") +Users/user/project/src/tsconfig.json: note: The target environment was set to "ES2019" here +`, + }) +} diff --git a/internal/bundler/snapshots/snapshots_tsconfig.txt b/internal/bundler/snapshots/snapshots_tsconfig.txt index 4bef6fe347..525e9f5b46 100644 --- a/internal/bundler/snapshots/snapshots_tsconfig.txt +++ b/internal/bundler/snapshots/snapshots_tsconfig.txt @@ -334,6 +334,33 @@ TestTsconfigRemoveUnusedImports // Users/user/project/src/entry.ts console.log(1); +================================================================================ +TestTsconfigTarget +---------- /Users/user/project/out.js ---------- +// Users/user/project/src/es2018/index.ts +var x = { ...y }; +try { + y; +} catch (e) { +} +x == null ? void 0 : x.y(); + +// Users/user/project/src/es2019/index.ts +var x2 = { ...y }; +try { + y; +} catch { +} +x2 == null ? void 0 : x2.y(); + +// Users/user/project/src/es2020/index.ts +var x3 = { ...y }; +try { + y; +} catch { +} +x3?.y(); + ================================================================================ TestTsconfigWarningsInsideNodeModules ---------- /Users/user/project/out.js ---------- diff --git a/internal/config/config.go b/internal/config/config.go index ccdadfb50c..f7eb7eb319 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -226,6 +226,7 @@ type Options struct { UnsupportedJSFeatures compat.JSFeature UnsupportedCSSFeatures compat.CSSFeature + TSTarget *TSTarget // This is the original information that was used to generate the // unsupported feature sets above. It's used for error messages. @@ -271,6 +272,13 @@ type Options struct { Stdin *StdinInfo } +type TSTarget struct { + Source logger.Source + Range logger.Range + Target string + UnsupportedJSFeatures compat.JSFeature +} + type PathPlaceholder uint8 const ( diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 87aea292d6..4afbe13965 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -304,6 +304,7 @@ type thenCatchChain struct { type Options struct { injectedFiles []config.InjectedFile jsx config.JSXOptions + tsTarget *config.TSTarget // This pointer will always be different for each build but the contents // shouldn't ever behave different semantically. We ignore this field for the @@ -341,6 +342,7 @@ func OptionsFromConfig(options *config.Options) Options { injectedFiles: options.InjectedFiles, jsx: options.JSX, defines: options.Defines, + tsTarget: options.TSTarget, optionsThatSupportStructuralEquality: optionsThatSupportStructuralEquality{ unsupportedJSFeatures: options.UnsupportedJSFeatures, originalTargetEnv: options.OriginalTargetEnv, @@ -367,6 +369,12 @@ func (a *Options) Equal(b *Options) bool { return false } + // Compare "TSTarget" + if (a.tsTarget == nil && b.tsTarget != nil) || (a.tsTarget != nil && b.tsTarget == nil) || + (a.tsTarget != nil && b.tsTarget != nil && *a.tsTarget != *b.tsTarget) { + return false + } + // Compare "InjectedFiles" if len(a.injectedFiles) != len(b.injectedFiles) { return false @@ -13624,6 +13632,11 @@ func Parse(log logger.Log, source logger.Source, options Options) (result js_ast options.useDefineForClassFields = config.False } + // Include unsupported JavaScript features from the TypeScript "target" setting + if options.tsTarget != nil { + options.unsupportedJSFeatures |= options.tsTarget.UnsupportedJSFeatures + } + p := newParser(log, source, js_lexer.NewLexer(log, source), &options) // Consume a leading hashbang comment diff --git a/internal/js_parser/js_parser_lower.go b/internal/js_parser/js_parser_lower.go index 6e14742721..0319f9ee29 100644 --- a/internal/js_parser/js_parser_lower.go +++ b/internal/js_parser/js_parser_lower.go @@ -29,9 +29,15 @@ func (p *parser) markSyntaxFeature(feature compat.JSFeature, r logger.Range) (di } var name string + var notes []logger.MsgData where := "the configured target environment" - if p.options.originalTargetEnv != "" { + if tsTarget := p.options.tsTarget; tsTarget != nil && tsTarget.UnsupportedJSFeatures.Has(feature) { + tracker := logger.MakeLineColumnTracker(&tsTarget.Source) + where = fmt.Sprintf("%s (%q)", where, tsTarget.Target) + notes = []logger.MsgData{logger.RangeData(&tracker, tsTarget.Range, fmt.Sprintf( + "The target environment was set to %q here", tsTarget.Target))} + } else if p.options.originalTargetEnv != "" { where = fmt.Sprintf("%s (%s)", where, p.options.originalTargetEnv) } @@ -88,40 +94,40 @@ func (p *parser) markSyntaxFeature(feature compat.JSFeature, r logger.Range) (di name = "non-identifier array rest patterns" case compat.ImportAssertions: - p.log.AddRangeError(&p.tracker, r, - fmt.Sprintf("Using an arbitrary value as the second argument to \"import()\" is not possible in %s", where)) + p.log.AddRangeErrorWithNotes(&p.tracker, r, fmt.Sprintf( + "Using an arbitrary value as the second argument to \"import()\" is not possible in %s", where), notes) return case compat.TopLevelAwait: - p.log.AddRangeError(&p.tracker, r, - fmt.Sprintf("Top-level await is not available in %s", where)) + p.log.AddRangeErrorWithNotes(&p.tracker, r, fmt.Sprintf( + "Top-level await is not available in %s", where), notes) return case compat.ArbitraryModuleNamespaceNames: - p.log.AddRangeError(&p.tracker, r, - fmt.Sprintf("Using a string as a module namespace identifier name is not supported in %s", where)) + p.log.AddRangeErrorWithNotes(&p.tracker, r, fmt.Sprintf( + "Using a string as a module namespace identifier name is not supported in %s", where), notes) return case compat.BigInt: // Transforming these will never be supported - p.log.AddRangeError(&p.tracker, r, - fmt.Sprintf("Big integer literals are not available in %s", where)) + p.log.AddRangeErrorWithNotes(&p.tracker, r, fmt.Sprintf( + "Big integer literals are not available in %s", where), notes) return case compat.ImportMeta: // This can't be polyfilled - p.log.AddRangeWarning(&p.tracker, r, - fmt.Sprintf("\"import.meta\" is not available in %s and will be empty", where)) + p.log.AddRangeWarningWithNotes(&p.tracker, r, fmt.Sprintf( + "\"import.meta\" is not available in %s and will be empty", where), notes) return default: - p.log.AddRangeError(&p.tracker, r, - fmt.Sprintf("This feature is not available in %s", where)) + p.log.AddRangeErrorWithNotes(&p.tracker, r, fmt.Sprintf( + "This feature is not available in %s", where), notes) return } - p.log.AddRangeError(&p.tracker, r, - fmt.Sprintf("Transforming %s to %s is not supported yet", name, where)) + p.log.AddRangeErrorWithNotes(&p.tracker, r, fmt.Sprintf( + "Transforming %s to %s is not supported yet", name, where), notes) return } diff --git a/internal/resolver/resolver.go b/internal/resolver/resolver.go index ae3f045498..7ef94d69a2 100644 --- a/internal/resolver/resolver.go +++ b/internal/resolver/resolver.go @@ -103,6 +103,8 @@ type ResolveResult struct { // effects. This means they should be removed if unused. PrimarySideEffectsData *SideEffectsData + TSTarget *config.TSTarget + IsExternal bool // If true, the class field transform should use Object.defineProperty(). @@ -501,6 +503,7 @@ func (r resolverQuery) finalizeResolve(result *ResolveResult) { result.JSXFragment = dirInfo.enclosingTSConfigJSON.JSXFragmentFactory result.UseDefineForClassFieldsTS = dirInfo.enclosingTSConfigJSON.UseDefineForClassFields result.PreserveUnusedImportsTS = dirInfo.enclosingTSConfigJSON.PreserveImportsNotUsedAsValues + result.TSTarget = dirInfo.enclosingTSConfigJSON.TSTarget if r.debugLogs != nil { r.debugLogs.addNote(fmt.Sprintf("This import is under the effect of %q", diff --git a/internal/resolver/tsconfig_json.go b/internal/resolver/tsconfig_json.go index 35af999c99..e3dd4fca95 100644 --- a/internal/resolver/tsconfig_json.go +++ b/internal/resolver/tsconfig_json.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/evanw/esbuild/internal/cache" + "github.com/evanw/esbuild/internal/compat" "github.com/evanw/esbuild/internal/config" "github.com/evanw/esbuild/internal/js_ast" "github.com/evanw/esbuild/internal/js_lexer" @@ -35,6 +36,7 @@ type TSConfigJSON struct { JSXFactory []string JSXFragmentFactory []string + TSTarget *config.TSTarget UseDefineForClassFields config.MaybeBool PreserveImportsNotUsedAsValues bool } @@ -110,6 +112,47 @@ func ParseTSConfigJSON( } } + // Parse "target" + if valueJSON, _, ok := getProperty(compilerOptionsJSON, "target"); ok { + if value, ok := getString(valueJSON); ok { + constraints := make(map[compat.Engine][]int) + r := source.RangeOfString(valueJSON.Loc) + + // See https://www.typescriptlang.org/tsconfig#target + switch strings.ToLower(value) { + case "es5": + constraints[compat.ES] = []int{5} + case "es6", "es2015": + constraints[compat.ES] = []int{2015} + case "es7", "es2016": + constraints[compat.ES] = []int{2016} + case "es2017": + constraints[compat.ES] = []int{2017} + case "es2018": + constraints[compat.ES] = []int{2018} + case "es2019": + constraints[compat.ES] = []int{2019} + case "es2020": + constraints[compat.ES] = []int{2020} + case "esnext": + // Nothing to do in this case + default: + log.AddRangeWarning(&tracker, r, + fmt.Sprintf("Unrecognized target environment %q", value)) + } + + // These feature restrictions are merged with esbuild's own restrictions + if len(constraints) > 0 { + result.TSTarget = &config.TSTarget{ + Source: source, + Range: r, + Target: value, + UnsupportedJSFeatures: compat.UnsupportedJSFeatures(constraints), + } + } + } + } + // Parse "importsNotUsedAsValues" if valueJSON, _, ok := getProperty(compilerOptionsJSON, "importsNotUsedAsValues"); ok { if value, ok := getString(valueJSON); ok { diff --git a/pkg/api/api_impl.go b/pkg/api/api_impl.go index 59aaf3aff5..1baf95068a 100644 --- a/pkg/api/api_impl.go +++ b/pkg/api/api_impl.go @@ -1242,6 +1242,7 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult } // Settings from "tsconfig.json" override those + var tsTarget *config.TSTarget caches := cache.MakeCacheSet() if transformOpts.TsconfigRaw != "" { source := logger.Source{ @@ -1262,6 +1263,7 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult if result.PreserveImportsNotUsedAsValues { preserveUnusedImportsTS = true } + tsTarget = result.TSTarget } } @@ -1280,6 +1282,7 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult UnsupportedJSFeatures: jsFeatures, UnsupportedCSSFeatures: cssFeatures, OriginalTargetEnv: targetEnv, + TSTarget: tsTarget, JSX: jsx, Defines: defines, InjectedDefines: injectedDefines,