Skip to content

Commit 78c7fe4

Browse files
committed
implement --supports to override syntax features
1 parent c090bc4 commit 78c7fe4

File tree

14 files changed

+422
-62
lines changed

14 files changed

+422
-62
lines changed

CHANGELOG.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,84 @@
22

33
## Unreleased
44

5+
* Add the ability to override support for individual syntax features ([2290](https://github.com/evanw/esbuild/issues/2290), [#2308](https://github.com/evanw/esbuild/issues/2308), [#2060](https://github.com/evanw/esbuild/issues/2060))
6+
7+
The `target` setting already lets you configure esbuild to restrict its output by only making use of syntax features that are known to be supported in the configured target environment. For example, setting `target` to `chrome50` causes esbuild to automatically transform optional chain expressions into the equivalent older JavaScript and prevents you from using BigInts, among many other things. However, sometimes you may want to customize this set of unsupported syntax features at the individual feature level.
8+
9+
Some examples of why you might want to do this:
10+
11+
* JavaScript runtimes often do a quick implementation of newer syntax features that is slower than the equivalent older JavaScript, and you can get a speedup by telling esbuild to pretend this syntax feature isn't supported. For example, V8 has a [long-standing performance bug regarding object spread](https://bugs.chromium.org/p/v8/issues/detail?id=11536) that can be avoided by manually copying properties instead of using object spread syntax. Right now esbuild hard-codes this optimization if you set `target` to a V8-based runtime.
12+
13+
* There are many less-used JavaScript runtimes in addition to the ones present in browsers, and these runtimes sometimes just decide not to implement parts of the specification, which might make sense for runtimes intended for embedded environments. For example, the developers behind Facebook's JavaScript runtime [Hermes](https://hermesengine.dev/) have decided to not implement classes despite it being a major JavaScript feature that was added seven years ago and that is used in virtually every large JavaScript project.
14+
15+
* You may be processing esbuild's output with another tool, and you may want esbuild to transform certain features and the other tool to transform certain other features. For example, if you are using esbuild to transform files individually to ES5 but you are then feeding the output into Webpack for bundling, you may want to preserve `import()` expressions even though they are a syntax error in ES5.
16+
17+
With this release, you can now use `--supported:feature=false` to force `feature` to be unsupported. This will cause esbuild to either rewrite code that uses the feature into older code that doesn't use the feature (if esbuild is able to), or to emit a build error (if esbuild is unable to). For example, you can use `--supported:arrow=false` to turn arrow functions into function expressions and `--supported:bigint=false` to make it an error to use a BigInt literal. You can also use `--supported:feature=true` to force it to be supported, which means esbuild will pass it through without transforming it. Keep in mind that this is an advanced feature. For most use cases you will probably want to just use `target` instead of using this.
18+
19+
The full set of currently-allowed features are as follows:
20+
21+
**JavaScript:**
22+
* `arbitrary-module-namespace-names`
23+
* `array-spread`
24+
* `arrow`
25+
* `async-await`
26+
* `async-generator`
27+
* `bigint`
28+
* `class`
29+
* `class-field`
30+
* `class-private-accessor`
31+
* `class-private-brand-check`
32+
* `class-private-field`
33+
* `class-private-method`
34+
* `class-private-static-accessor`
35+
* `class-private-static-field`
36+
* `class-private-static-method`
37+
* `class-static-blocks`
38+
* `class-static-field`
39+
* `const-and-let`
40+
* `default-argument`
41+
* `destructuring`
42+
* `dynamic-import`
43+
* `exponent-operator`
44+
* `export-star-as`
45+
* `for-await`
46+
* `for-of`
47+
* `generator`
48+
* `hashbang`
49+
* `import-assertions`
50+
* `import-meta`
51+
* `logical-assignment`
52+
* `nested-rest-binding`
53+
* `new-target`
54+
* `node-colon-prefix-import`
55+
* `node-colon-prefix-require`
56+
* `nullish-coalescing`
57+
* `object-accessors`
58+
* `object-extensions`
59+
* `object-rest-spread`
60+
* `optional-catch-binding`
61+
* `optional-chain`
62+
* `reg-exp-dot-all-flag`
63+
* `reg-exp-lookbehind-assertions`
64+
* `reg-exp-match-indices`
65+
* `reg-exp-named-capture-groups`
66+
* `reg-exp-sticky-and-unicode-flags`
67+
* `reg-exp-unicode-property-escapes`
68+
* `rest-argument`
69+
* `template-literal`
70+
* `top-level-await`
71+
* `typeof-exotic-object-is-object`
72+
* `unicode-escapes`
73+
74+
**CSS:**
75+
* `hex-rgba`
76+
* `rebecca-purple`
77+
* `modern-rgb-hsl`
78+
* `inset-property`
79+
* `nesting`
80+
81+
_Note that JavaScript feature transformation is very complex and allowing full customization of the set of supported syntax features could cause bugs in esbuild due to new interactions between multiple features that were never possible before. Consider this to be an experimental feature._
82+
583
* Allow `define` to match optional chain expressions ([#2324](https://github.com/evanw/esbuild/issues/2324))
684

785
Previously esbuild's `define` feature only matched member expressions that did not use optional chaining. With this release, esbuild will now also match those that use optional chaining:

cmd/esbuild/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ var helpText = func(colors logger.Colors) string {
113113
--sourcemap=external Do not link to the source map with a comment
114114
--sourcemap=inline Emit the source map with an inline data URL
115115
--sources-content=false Omit "sourcesContent" in generated source maps
116+
--supported:F=... Consider syntax F to be supported (true | false)
116117
--tree-shaking=... Force tree shaking on or off (false | true)
117118
--tsconfig=... Use this tsconfig.json file instead of other ones
118119
--version Print the current version (` + esbuildVersion + `) and exit

internal/compat/css_table.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package compat
22

3-
type CSSFeature uint32
3+
type CSSFeature uint8
44

55
const (
66
HexRGBA CSSFeature = 1 << iota
@@ -18,10 +18,30 @@ const (
1818
Nesting
1919
)
2020

21+
var StringToCSSFeature = map[string]CSSFeature{
22+
"hex-rgba": HexRGBA,
23+
"rebecca-purple": RebeccaPurple,
24+
"modern-rgb-hsl": Modern_RGB_HSL,
25+
"inset-property": InsetProperty,
26+
"nesting": Nesting,
27+
}
28+
29+
var CSSFeatureToString = map[CSSFeature]string{
30+
HexRGBA: "hex-rgba",
31+
RebeccaPurple: "rebecca-purple",
32+
Modern_RGB_HSL: "modern-rgb-hsl",
33+
InsetProperty: "inset-property",
34+
Nesting: "nesting",
35+
}
36+
2137
func (features CSSFeature) Has(feature CSSFeature) bool {
2238
return (features & feature) != 0
2339
}
2440

41+
func (features CSSFeature) ApplyOverrides(overrides CSSFeature, mask CSSFeature) CSSFeature {
42+
return (features & ^mask) | (overrides & mask)
43+
}
44+
2545
var cssTable = map[CSSFeature]map[Engine][]versionRange{
2646
// Data from: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
2747
HexRGBA: {

internal/compat/js_table.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,122 @@ const (
9696
UnicodeEscapes
9797
)
9898

99+
var StringToJSFeature = map[string]JSFeature{
100+
"arbitrary-module-namespace-names": ArbitraryModuleNamespaceNames,
101+
"array-spread": ArraySpread,
102+
"arrow": Arrow,
103+
"async-await": AsyncAwait,
104+
"async-generator": AsyncGenerator,
105+
"bigint": Bigint,
106+
"class": Class,
107+
"class-field": ClassField,
108+
"class-private-accessor": ClassPrivateAccessor,
109+
"class-private-brand-check": ClassPrivateBrandCheck,
110+
"class-private-field": ClassPrivateField,
111+
"class-private-method": ClassPrivateMethod,
112+
"class-private-static-accessor": ClassPrivateStaticAccessor,
113+
"class-private-static-field": ClassPrivateStaticField,
114+
"class-private-static-method": ClassPrivateStaticMethod,
115+
"class-static-blocks": ClassStaticBlocks,
116+
"class-static-field": ClassStaticField,
117+
"const-and-let": ConstAndLet,
118+
"default-argument": DefaultArgument,
119+
"destructuring": Destructuring,
120+
"dynamic-import": DynamicImport,
121+
"exponent-operator": ExponentOperator,
122+
"export-star-as": ExportStarAs,
123+
"for-await": ForAwait,
124+
"for-of": ForOf,
125+
"generator": Generator,
126+
"hashbang": Hashbang,
127+
"import-assertions": ImportAssertions,
128+
"import-meta": ImportMeta,
129+
"logical-assignment": LogicalAssignment,
130+
"nested-rest-binding": NestedRestBinding,
131+
"new-target": NewTarget,
132+
"node-colon-prefix-import": NodeColonPrefixImport,
133+
"node-colon-prefix-require": NodeColonPrefixRequire,
134+
"nullish-coalescing": NullishCoalescing,
135+
"object-accessors": ObjectAccessors,
136+
"object-extensions": ObjectExtensions,
137+
"object-rest-spread": ObjectRestSpread,
138+
"optional-catch-binding": OptionalCatchBinding,
139+
"optional-chain": OptionalChain,
140+
"reg-exp-dot-all-flag": RegExpDotAllFlag,
141+
"reg-exp-lookbehind-assertions": RegExpLookbehindAssertions,
142+
"reg-exp-match-indices": RegExpMatchIndices,
143+
"reg-exp-named-capture-groups": RegExpNamedCaptureGroups,
144+
"reg-exp-sticky-and-unicode-flags": RegExpStickyAndUnicodeFlags,
145+
"reg-exp-unicode-property-escapes": RegExpUnicodePropertyEscapes,
146+
"rest-argument": RestArgument,
147+
"template-literal": TemplateLiteral,
148+
"top-level-await": TopLevelAwait,
149+
"typeof-exotic-object-is-object": TypeofExoticObjectIsObject,
150+
"unicode-escapes": UnicodeEscapes,
151+
}
152+
153+
var JSFeatureToString = map[JSFeature]string{
154+
ArbitraryModuleNamespaceNames: "arbitrary-module-namespace-names",
155+
ArraySpread: "array-spread",
156+
Arrow: "arrow",
157+
AsyncAwait: "async-await",
158+
AsyncGenerator: "async-generator",
159+
Bigint: "bigint",
160+
Class: "class",
161+
ClassField: "class-field",
162+
ClassPrivateAccessor: "class-private-accessor",
163+
ClassPrivateBrandCheck: "class-private-brand-check",
164+
ClassPrivateField: "class-private-field",
165+
ClassPrivateMethod: "class-private-method",
166+
ClassPrivateStaticAccessor: "class-private-static-accessor",
167+
ClassPrivateStaticField: "class-private-static-field",
168+
ClassPrivateStaticMethod: "class-private-static-method",
169+
ClassStaticBlocks: "class-static-blocks",
170+
ClassStaticField: "class-static-field",
171+
ConstAndLet: "const-and-let",
172+
DefaultArgument: "default-argument",
173+
Destructuring: "destructuring",
174+
DynamicImport: "dynamic-import",
175+
ExponentOperator: "exponent-operator",
176+
ExportStarAs: "export-star-as",
177+
ForAwait: "for-await",
178+
ForOf: "for-of",
179+
Generator: "generator",
180+
Hashbang: "hashbang",
181+
ImportAssertions: "import-assertions",
182+
ImportMeta: "import-meta",
183+
LogicalAssignment: "logical-assignment",
184+
NestedRestBinding: "nested-rest-binding",
185+
NewTarget: "new-target",
186+
NodeColonPrefixImport: "node-colon-prefix-import",
187+
NodeColonPrefixRequire: "node-colon-prefix-require",
188+
NullishCoalescing: "nullish-coalescing",
189+
ObjectAccessors: "object-accessors",
190+
ObjectExtensions: "object-extensions",
191+
ObjectRestSpread: "object-rest-spread",
192+
OptionalCatchBinding: "optional-catch-binding",
193+
OptionalChain: "optional-chain",
194+
RegExpDotAllFlag: "reg-exp-dot-all-flag",
195+
RegExpLookbehindAssertions: "reg-exp-lookbehind-assertions",
196+
RegExpMatchIndices: "reg-exp-match-indices",
197+
RegExpNamedCaptureGroups: "reg-exp-named-capture-groups",
198+
RegExpStickyAndUnicodeFlags: "reg-exp-sticky-and-unicode-flags",
199+
RegExpUnicodePropertyEscapes: "reg-exp-unicode-property-escapes",
200+
RestArgument: "rest-argument",
201+
TemplateLiteral: "template-literal",
202+
TopLevelAwait: "top-level-await",
203+
TypeofExoticObjectIsObject: "typeof-exotic-object-is-object",
204+
UnicodeEscapes: "unicode-escapes",
205+
}
206+
99207
func (features JSFeature) Has(feature JSFeature) bool {
100208
return (features & feature) != 0
101209
}
102210

211+
func (features JSFeature) ApplyOverrides(overrides JSFeature, mask JSFeature) JSFeature {
212+
return (features & ^mask) | (overrides & mask)
213+
}
214+
103215
var jsTable = map[JSFeature]map[Engine][]versionRange{
104216
ArbitraryModuleNamespaceNames: {
105217
Chrome: {{start: v{90, 0, 0}}},

internal/config/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,11 @@ type Options struct {
255255
UnsupportedJSFeatures compat.JSFeature
256256
UnsupportedCSSFeatures compat.CSSFeature
257257

258+
UnsupportedJSFeatureOverrides compat.JSFeature
259+
UnsupportedJSFeatureOverridesMask compat.JSFeature
260+
UnsupportedCSSFeatureOverrides compat.CSSFeature
261+
UnsupportedCSSFeatureOverridesMask compat.CSSFeature
262+
258263
TS TSOptions
259264
Mode Mode
260265
PreserveSymlinks bool

internal/js_parser/js_parser.go

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -379,9 +379,11 @@ type Options struct {
379379
}
380380

381381
type optionsThatSupportStructuralEquality struct {
382-
originalTargetEnv string
383-
moduleTypeData js_ast.ModuleTypeData
384-
unsupportedJSFeatures compat.JSFeature
382+
originalTargetEnv string
383+
moduleTypeData js_ast.ModuleTypeData
384+
unsupportedJSFeatures compat.JSFeature
385+
unsupportedJSFeatureOverrides compat.JSFeature
386+
unsupportedJSFeatureOverridesMask compat.JSFeature
385387

386388
// Byte-sized values go here (gathered together here to keep this object compact)
387389
ts config.TSOptions
@@ -413,25 +415,27 @@ func OptionsFromConfig(options *config.Options) Options {
413415
reserveProps: options.ReserveProps,
414416

415417
optionsThatSupportStructuralEquality: optionsThatSupportStructuralEquality{
416-
unsupportedJSFeatures: options.UnsupportedJSFeatures,
417-
originalTargetEnv: options.OriginalTargetEnv,
418-
ts: options.TS,
419-
mode: options.Mode,
420-
platform: options.Platform,
421-
outputFormat: options.OutputFormat,
422-
moduleTypeData: options.ModuleTypeData,
423-
targetFromAPI: options.TargetFromAPI,
424-
asciiOnly: options.ASCIIOnly,
425-
keepNames: options.KeepNames,
426-
minifySyntax: options.MinifySyntax,
427-
minifyIdentifiers: options.MinifyIdentifiers,
428-
omitRuntimeForTests: options.OmitRuntimeForTests,
429-
ignoreDCEAnnotations: options.IgnoreDCEAnnotations,
430-
treeShaking: options.TreeShaking,
431-
dropDebugger: options.DropDebugger,
432-
mangleQuoted: options.MangleQuoted,
433-
unusedImportFlagsTS: options.UnusedImportFlagsTS,
434-
useDefineForClassFields: options.UseDefineForClassFields,
418+
unsupportedJSFeatures: options.UnsupportedJSFeatures,
419+
unsupportedJSFeatureOverrides: options.UnsupportedJSFeatureOverrides,
420+
unsupportedJSFeatureOverridesMask: options.UnsupportedJSFeatureOverridesMask,
421+
originalTargetEnv: options.OriginalTargetEnv,
422+
ts: options.TS,
423+
mode: options.Mode,
424+
platform: options.Platform,
425+
outputFormat: options.OutputFormat,
426+
moduleTypeData: options.ModuleTypeData,
427+
targetFromAPI: options.TargetFromAPI,
428+
asciiOnly: options.ASCIIOnly,
429+
keepNames: options.KeepNames,
430+
minifySyntax: options.MinifySyntax,
431+
minifyIdentifiers: options.MinifyIdentifiers,
432+
omitRuntimeForTests: options.OmitRuntimeForTests,
433+
ignoreDCEAnnotations: options.IgnoreDCEAnnotations,
434+
treeShaking: options.TreeShaking,
435+
dropDebugger: options.DropDebugger,
436+
mangleQuoted: options.MangleQuoted,
437+
unusedImportFlagsTS: options.UnusedImportFlagsTS,
438+
useDefineForClassFields: options.UseDefineForClassFields,
435439
},
436440
}
437441
}
@@ -15289,6 +15293,11 @@ func Parse(log logger.Log, source logger.Source, options Options) (result js_ast
1528915293
// TypeScript "target" setting is ignored.
1529015294
if options.targetFromAPI == config.TargetWasUnconfigured && options.tsTarget != nil {
1529115295
options.unsupportedJSFeatures |= options.tsTarget.UnsupportedJSFeatures
15296+
15297+
// Re-apply overrides to make sure they always win
15298+
options.unsupportedJSFeatures = options.unsupportedJSFeatures.ApplyOverrides(
15299+
options.unsupportedJSFeatureOverrides,
15300+
options.unsupportedJSFeatureOverridesMask)
1529215301
}
1529315302

1529415303
p := newParser(log, source, js_lexer.NewLexer(log, source, options.ts), &options)

internal/js_parser/js_parser_lower.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,31 @@ import (
1616

1717
func (p *parser) prettyPrintTargetEnvironment(feature compat.JSFeature) (where string, notes []logger.MsgData) {
1818
where = "the configured target environment"
19+
overrides := ""
20+
if p.options.unsupportedJSFeatureOverridesMask != 0 {
21+
count := 0
22+
mask := p.options.unsupportedJSFeatureOverridesMask
23+
for mask != 0 {
24+
if (mask & 1) != 0 {
25+
count++
26+
}
27+
mask >>= 1
28+
}
29+
s := "s"
30+
if count == 1 {
31+
s = ""
32+
}
33+
overrides = fmt.Sprintf(" + %d override%s", count, s)
34+
}
1935
if tsTarget := p.options.tsTarget; tsTarget != nil &&
2036
p.options.targetFromAPI == config.TargetWasUnconfigured &&
2137
tsTarget.UnsupportedJSFeatures.Has(feature) {
2238
tracker := logger.MakeLineColumnTracker(&tsTarget.Source)
23-
where = fmt.Sprintf("%s (%q)", where, tsTarget.Target)
39+
where = fmt.Sprintf("%s (%q%s)", where, tsTarget.Target, overrides)
2440
notes = []logger.MsgData{tracker.MsgData(tsTarget.Range, fmt.Sprintf(
2541
"The target environment was set to %q here:", tsTarget.Target))}
2642
} else if p.options.originalTargetEnv != "" {
27-
where = fmt.Sprintf("%s (%s)", where, p.options.originalTargetEnv)
43+
where = fmt.Sprintf("%s (%s%s)", where, p.options.originalTargetEnv, overrides)
2844
}
2945
return
3046
}

lib/shared/common.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe
142142
let jsxFragment = getFlag(options, keys, 'jsxFragment', mustBeString);
143143
let define = getFlag(options, keys, 'define', mustBeObject);
144144
let logOverride = getFlag(options, keys, 'logOverride', mustBeObject);
145+
let supported = getFlag(options, keys, 'supported', mustBeObject);
145146
let pure = getFlag(options, keys, 'pure', mustBeArray);
146147
let keepNames = getFlag(options, keys, 'keepNames', mustBeBoolean);
147148

@@ -183,6 +184,12 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe
183184
flags.push(`--log-override:${key}=${logOverride[key]}`);
184185
}
185186
}
187+
if (supported) {
188+
for (let key in supported) {
189+
if (key.indexOf('=') >= 0) throw new Error(`Invalid supported: ${key}`);
190+
flags.push(`--supported:${key}=${supported[key]}`);
191+
}
192+
}
186193
if (pure) for (let fn of pure) flags.push(`--pure:${fn}`);
187194
if (keepNames) flags.push(`--keep-names`);
188195
}

0 commit comments

Comments
 (0)