Skip to content

Commit

Permalink
implement --supports to override syntax features
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jun 18, 2022
1 parent c090bc4 commit 78c7fe4
Show file tree
Hide file tree
Showing 14 changed files with 422 additions and 62 deletions.
78 changes: 78 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,84 @@

## Unreleased

* 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))

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.

Some examples of why you might want to do this:

* 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.

* 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.

* 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.

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.

The full set of currently-allowed features are as follows:

**JavaScript:**
* `arbitrary-module-namespace-names`
* `array-spread`
* `arrow`
* `async-await`
* `async-generator`
* `bigint`
* `class`
* `class-field`
* `class-private-accessor`
* `class-private-brand-check`
* `class-private-field`
* `class-private-method`
* `class-private-static-accessor`
* `class-private-static-field`
* `class-private-static-method`
* `class-static-blocks`
* `class-static-field`
* `const-and-let`
* `default-argument`
* `destructuring`
* `dynamic-import`
* `exponent-operator`
* `export-star-as`
* `for-await`
* `for-of`
* `generator`
* `hashbang`
* `import-assertions`
* `import-meta`
* `logical-assignment`
* `nested-rest-binding`
* `new-target`
* `node-colon-prefix-import`
* `node-colon-prefix-require`
* `nullish-coalescing`
* `object-accessors`
* `object-extensions`
* `object-rest-spread`
* `optional-catch-binding`
* `optional-chain`
* `reg-exp-dot-all-flag`
* `reg-exp-lookbehind-assertions`
* `reg-exp-match-indices`
* `reg-exp-named-capture-groups`
* `reg-exp-sticky-and-unicode-flags`
* `reg-exp-unicode-property-escapes`
* `rest-argument`
* `template-literal`
* `top-level-await`
* `typeof-exotic-object-is-object`
* `unicode-escapes`

**CSS:**
* `hex-rgba`
* `rebecca-purple`
* `modern-rgb-hsl`
* `inset-property`
* `nesting`

_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._

* Allow `define` to match optional chain expressions ([#2324](https://github.com/evanw/esbuild/issues/2324))

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:
Expand Down
1 change: 1 addition & 0 deletions cmd/esbuild/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ var helpText = func(colors logger.Colors) string {
--sourcemap=external Do not link to the source map with a comment
--sourcemap=inline Emit the source map with an inline data URL
--sources-content=false Omit "sourcesContent" in generated source maps
--supported:F=... Consider syntax F to be supported (true | false)
--tree-shaking=... Force tree shaking on or off (false | true)
--tsconfig=... Use this tsconfig.json file instead of other ones
--version Print the current version (` + esbuildVersion + `) and exit
Expand Down
22 changes: 21 additions & 1 deletion internal/compat/css_table.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package compat

type CSSFeature uint32
type CSSFeature uint8

const (
HexRGBA CSSFeature = 1 << iota
Expand All @@ -18,10 +18,30 @@ const (
Nesting
)

var StringToCSSFeature = map[string]CSSFeature{
"hex-rgba": HexRGBA,
"rebecca-purple": RebeccaPurple,
"modern-rgb-hsl": Modern_RGB_HSL,
"inset-property": InsetProperty,
"nesting": Nesting,
}

var CSSFeatureToString = map[CSSFeature]string{
HexRGBA: "hex-rgba",
RebeccaPurple: "rebecca-purple",
Modern_RGB_HSL: "modern-rgb-hsl",
InsetProperty: "inset-property",
Nesting: "nesting",
}

func (features CSSFeature) Has(feature CSSFeature) bool {
return (features & feature) != 0
}

func (features CSSFeature) ApplyOverrides(overrides CSSFeature, mask CSSFeature) CSSFeature {
return (features & ^mask) | (overrides & mask)
}

var cssTable = map[CSSFeature]map[Engine][]versionRange{
// Data from: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
HexRGBA: {
Expand Down
112 changes: 112 additions & 0 deletions internal/compat/js_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,122 @@ const (
UnicodeEscapes
)

var StringToJSFeature = map[string]JSFeature{
"arbitrary-module-namespace-names": ArbitraryModuleNamespaceNames,
"array-spread": ArraySpread,
"arrow": Arrow,
"async-await": AsyncAwait,
"async-generator": AsyncGenerator,
"bigint": Bigint,
"class": Class,
"class-field": ClassField,
"class-private-accessor": ClassPrivateAccessor,
"class-private-brand-check": ClassPrivateBrandCheck,
"class-private-field": ClassPrivateField,
"class-private-method": ClassPrivateMethod,
"class-private-static-accessor": ClassPrivateStaticAccessor,
"class-private-static-field": ClassPrivateStaticField,
"class-private-static-method": ClassPrivateStaticMethod,
"class-static-blocks": ClassStaticBlocks,
"class-static-field": ClassStaticField,
"const-and-let": ConstAndLet,
"default-argument": DefaultArgument,
"destructuring": Destructuring,
"dynamic-import": DynamicImport,
"exponent-operator": ExponentOperator,
"export-star-as": ExportStarAs,
"for-await": ForAwait,
"for-of": ForOf,
"generator": Generator,
"hashbang": Hashbang,
"import-assertions": ImportAssertions,
"import-meta": ImportMeta,
"logical-assignment": LogicalAssignment,
"nested-rest-binding": NestedRestBinding,
"new-target": NewTarget,
"node-colon-prefix-import": NodeColonPrefixImport,
"node-colon-prefix-require": NodeColonPrefixRequire,
"nullish-coalescing": NullishCoalescing,
"object-accessors": ObjectAccessors,
"object-extensions": ObjectExtensions,
"object-rest-spread": ObjectRestSpread,
"optional-catch-binding": OptionalCatchBinding,
"optional-chain": OptionalChain,
"reg-exp-dot-all-flag": RegExpDotAllFlag,
"reg-exp-lookbehind-assertions": RegExpLookbehindAssertions,
"reg-exp-match-indices": RegExpMatchIndices,
"reg-exp-named-capture-groups": RegExpNamedCaptureGroups,
"reg-exp-sticky-and-unicode-flags": RegExpStickyAndUnicodeFlags,
"reg-exp-unicode-property-escapes": RegExpUnicodePropertyEscapes,
"rest-argument": RestArgument,
"template-literal": TemplateLiteral,
"top-level-await": TopLevelAwait,
"typeof-exotic-object-is-object": TypeofExoticObjectIsObject,
"unicode-escapes": UnicodeEscapes,
}

var JSFeatureToString = map[JSFeature]string{
ArbitraryModuleNamespaceNames: "arbitrary-module-namespace-names",
ArraySpread: "array-spread",
Arrow: "arrow",
AsyncAwait: "async-await",
AsyncGenerator: "async-generator",
Bigint: "bigint",
Class: "class",
ClassField: "class-field",
ClassPrivateAccessor: "class-private-accessor",
ClassPrivateBrandCheck: "class-private-brand-check",
ClassPrivateField: "class-private-field",
ClassPrivateMethod: "class-private-method",
ClassPrivateStaticAccessor: "class-private-static-accessor",
ClassPrivateStaticField: "class-private-static-field",
ClassPrivateStaticMethod: "class-private-static-method",
ClassStaticBlocks: "class-static-blocks",
ClassStaticField: "class-static-field",
ConstAndLet: "const-and-let",
DefaultArgument: "default-argument",
Destructuring: "destructuring",
DynamicImport: "dynamic-import",
ExponentOperator: "exponent-operator",
ExportStarAs: "export-star-as",
ForAwait: "for-await",
ForOf: "for-of",
Generator: "generator",
Hashbang: "hashbang",
ImportAssertions: "import-assertions",
ImportMeta: "import-meta",
LogicalAssignment: "logical-assignment",
NestedRestBinding: "nested-rest-binding",
NewTarget: "new-target",
NodeColonPrefixImport: "node-colon-prefix-import",
NodeColonPrefixRequire: "node-colon-prefix-require",
NullishCoalescing: "nullish-coalescing",
ObjectAccessors: "object-accessors",
ObjectExtensions: "object-extensions",
ObjectRestSpread: "object-rest-spread",
OptionalCatchBinding: "optional-catch-binding",
OptionalChain: "optional-chain",
RegExpDotAllFlag: "reg-exp-dot-all-flag",
RegExpLookbehindAssertions: "reg-exp-lookbehind-assertions",
RegExpMatchIndices: "reg-exp-match-indices",
RegExpNamedCaptureGroups: "reg-exp-named-capture-groups",
RegExpStickyAndUnicodeFlags: "reg-exp-sticky-and-unicode-flags",
RegExpUnicodePropertyEscapes: "reg-exp-unicode-property-escapes",
RestArgument: "rest-argument",
TemplateLiteral: "template-literal",
TopLevelAwait: "top-level-await",
TypeofExoticObjectIsObject: "typeof-exotic-object-is-object",
UnicodeEscapes: "unicode-escapes",
}

func (features JSFeature) Has(feature JSFeature) bool {
return (features & feature) != 0
}

func (features JSFeature) ApplyOverrides(overrides JSFeature, mask JSFeature) JSFeature {
return (features & ^mask) | (overrides & mask)
}

var jsTable = map[JSFeature]map[Engine][]versionRange{
ArbitraryModuleNamespaceNames: {
Chrome: {{start: v{90, 0, 0}}},
Expand Down
5 changes: 5 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,11 @@ type Options struct {
UnsupportedJSFeatures compat.JSFeature
UnsupportedCSSFeatures compat.CSSFeature

UnsupportedJSFeatureOverrides compat.JSFeature
UnsupportedJSFeatureOverridesMask compat.JSFeature
UnsupportedCSSFeatureOverrides compat.CSSFeature
UnsupportedCSSFeatureOverridesMask compat.CSSFeature

TS TSOptions
Mode Mode
PreserveSymlinks bool
Expand Down
53 changes: 31 additions & 22 deletions internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,9 +379,11 @@ type Options struct {
}

type optionsThatSupportStructuralEquality struct {
originalTargetEnv string
moduleTypeData js_ast.ModuleTypeData
unsupportedJSFeatures compat.JSFeature
originalTargetEnv string
moduleTypeData js_ast.ModuleTypeData
unsupportedJSFeatures compat.JSFeature
unsupportedJSFeatureOverrides compat.JSFeature
unsupportedJSFeatureOverridesMask compat.JSFeature

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

optionsThatSupportStructuralEquality: optionsThatSupportStructuralEquality{
unsupportedJSFeatures: options.UnsupportedJSFeatures,
originalTargetEnv: options.OriginalTargetEnv,
ts: options.TS,
mode: options.Mode,
platform: options.Platform,
outputFormat: options.OutputFormat,
moduleTypeData: options.ModuleTypeData,
targetFromAPI: options.TargetFromAPI,
asciiOnly: options.ASCIIOnly,
keepNames: options.KeepNames,
minifySyntax: options.MinifySyntax,
minifyIdentifiers: options.MinifyIdentifiers,
omitRuntimeForTests: options.OmitRuntimeForTests,
ignoreDCEAnnotations: options.IgnoreDCEAnnotations,
treeShaking: options.TreeShaking,
dropDebugger: options.DropDebugger,
mangleQuoted: options.MangleQuoted,
unusedImportFlagsTS: options.UnusedImportFlagsTS,
useDefineForClassFields: options.UseDefineForClassFields,
unsupportedJSFeatures: options.UnsupportedJSFeatures,
unsupportedJSFeatureOverrides: options.UnsupportedJSFeatureOverrides,
unsupportedJSFeatureOverridesMask: options.UnsupportedJSFeatureOverridesMask,
originalTargetEnv: options.OriginalTargetEnv,
ts: options.TS,
mode: options.Mode,
platform: options.Platform,
outputFormat: options.OutputFormat,
moduleTypeData: options.ModuleTypeData,
targetFromAPI: options.TargetFromAPI,
asciiOnly: options.ASCIIOnly,
keepNames: options.KeepNames,
minifySyntax: options.MinifySyntax,
minifyIdentifiers: options.MinifyIdentifiers,
omitRuntimeForTests: options.OmitRuntimeForTests,
ignoreDCEAnnotations: options.IgnoreDCEAnnotations,
treeShaking: options.TreeShaking,
dropDebugger: options.DropDebugger,
mangleQuoted: options.MangleQuoted,
unusedImportFlagsTS: options.UnusedImportFlagsTS,
useDefineForClassFields: options.UseDefineForClassFields,
},
}
}
Expand Down Expand Up @@ -15289,6 +15293,11 @@ func Parse(log logger.Log, source logger.Source, options Options) (result js_ast
// TypeScript "target" setting is ignored.
if options.targetFromAPI == config.TargetWasUnconfigured && options.tsTarget != nil {
options.unsupportedJSFeatures |= options.tsTarget.UnsupportedJSFeatures

// Re-apply overrides to make sure they always win
options.unsupportedJSFeatures = options.unsupportedJSFeatures.ApplyOverrides(
options.unsupportedJSFeatureOverrides,
options.unsupportedJSFeatureOverridesMask)
}

p := newParser(log, source, js_lexer.NewLexer(log, source, options.ts), &options)
Expand Down
20 changes: 18 additions & 2 deletions internal/js_parser/js_parser_lower.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,31 @@ import (

func (p *parser) prettyPrintTargetEnvironment(feature compat.JSFeature) (where string, notes []logger.MsgData) {
where = "the configured target environment"
overrides := ""
if p.options.unsupportedJSFeatureOverridesMask != 0 {
count := 0
mask := p.options.unsupportedJSFeatureOverridesMask
for mask != 0 {
if (mask & 1) != 0 {
count++
}
mask >>= 1
}
s := "s"
if count == 1 {
s = ""
}
overrides = fmt.Sprintf(" + %d override%s", count, s)
}
if tsTarget := p.options.tsTarget; tsTarget != nil &&
p.options.targetFromAPI == config.TargetWasUnconfigured &&
tsTarget.UnsupportedJSFeatures.Has(feature) {
tracker := logger.MakeLineColumnTracker(&tsTarget.Source)
where = fmt.Sprintf("%s (%q)", where, tsTarget.Target)
where = fmt.Sprintf("%s (%q%s)", where, tsTarget.Target, overrides)
notes = []logger.MsgData{tracker.MsgData(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)
where = fmt.Sprintf("%s (%s%s)", where, p.options.originalTargetEnv, overrides)
}
return
}
Expand Down
7 changes: 7 additions & 0 deletions lib/shared/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe
let jsxFragment = getFlag(options, keys, 'jsxFragment', mustBeString);
let define = getFlag(options, keys, 'define', mustBeObject);
let logOverride = getFlag(options, keys, 'logOverride', mustBeObject);
let supported = getFlag(options, keys, 'supported', mustBeObject);
let pure = getFlag(options, keys, 'pure', mustBeArray);
let keepNames = getFlag(options, keys, 'keepNames', mustBeBoolean);

Expand Down Expand Up @@ -183,6 +184,12 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe
flags.push(`--log-override:${key}=${logOverride[key]}`);
}
}
if (supported) {
for (let key in supported) {
if (key.indexOf('=') >= 0) throw new Error(`Invalid supported: ${key}`);
flags.push(`--supported:${key}=${supported[key]}`);
}
}
if (pure) for (let fn of pure) flags.push(`--pure:${fn}`);
if (keepNames) flags.push(`--keep-names`);
}
Expand Down
Loading

0 comments on commit 78c7fe4

Please sign in to comment.