Skip to content

Commit

Permalink
fix #277: respect "target" in "tsconfig.json"
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed May 27, 2021
1 parent cecc78c commit 86900b4
Show file tree
Hide file tree
Showing 10 changed files with 204 additions and 15 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
1 change: 1 addition & 0 deletions internal/bundler/bundler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
81 changes: 81 additions & 0 deletions internal/bundler/bundler_tsconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
`,
})
}
27 changes: 27 additions & 0 deletions internal/bundler/snapshots/snapshots_tsconfig.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 ----------
8 changes: 8 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 (
Expand Down
13 changes: 13 additions & 0 deletions internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
36 changes: 21 additions & 15 deletions internal/js_parser/js_parser_lower.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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
}

Expand Down
3 changes: 3 additions & 0 deletions internal/resolver/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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().
Expand Down Expand Up @@ -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",
Expand Down
43 changes: 43 additions & 0 deletions internal/resolver/tsconfig_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -35,6 +36,7 @@ type TSConfigJSON struct {

JSXFactory []string
JSXFragmentFactory []string
TSTarget *config.TSTarget
UseDefineForClassFields config.MaybeBool
PreserveImportsNotUsedAsValues bool
}
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions pkg/api/api_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -1262,6 +1263,7 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult
if result.PreserveImportsNotUsedAsValues {
preserveUnusedImportsTS = true
}
tsTarget = result.TSTarget
}
}

Expand All @@ -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,
Expand Down

0 comments on commit 86900b4

Please sign in to comment.