diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a55e3e7c13..d48b143f2b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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: diff --git a/cmd/esbuild/main.go b/cmd/esbuild/main.go index 2a53b920950..ac9d85fd3b7 100644 --- a/cmd/esbuild/main.go +++ b/cmd/esbuild/main.go @@ -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 diff --git a/internal/compat/css_table.go b/internal/compat/css_table.go index 0623f8ac387..81932fdc11e 100644 --- a/internal/compat/css_table.go +++ b/internal/compat/css_table.go @@ -1,6 +1,6 @@ package compat -type CSSFeature uint32 +type CSSFeature uint8 const ( HexRGBA CSSFeature = 1 << iota @@ -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: { diff --git a/internal/compat/js_table.go b/internal/compat/js_table.go index 890af962909..bf8612d4720 100644 --- a/internal/compat/js_table.go +++ b/internal/compat/js_table.go @@ -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}}}, diff --git a/internal/config/config.go b/internal/config/config.go index 29126d6fd33..28d7fe5ac38 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 5b30f626f15..0ceaf749a3f 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -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 @@ -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, }, } } @@ -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) diff --git a/internal/js_parser/js_parser_lower.go b/internal/js_parser/js_parser_lower.go index 7adf834b361..e3ea2d34162 100644 --- a/internal/js_parser/js_parser_lower.go +++ b/internal/js_parser/js_parser_lower.go @@ -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 } diff --git a/lib/shared/common.ts b/lib/shared/common.ts index 2ff84e05747..acf59266528 100644 --- a/lib/shared/common.ts +++ b/lib/shared/common.ts @@ -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); @@ -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`); } diff --git a/lib/shared/types.ts b/lib/shared/types.ts index d493fa42f21..9f4fd8c5f56 100644 --- a/lib/shared/types.ts +++ b/lib/shared/types.ts @@ -21,6 +21,8 @@ interface CommonOptions { globalName?: string; /** Documentation: https://esbuild.github.io/api/#target */ target?: string | string[]; + /** Documentation: https://esbuild.github.io/api/#supported */ + supported?: Record; /** Documentation: https://esbuild.github.io/api/#mangle-props */ mangleProps?: RegExp; diff --git a/pkg/api/api.go b/pkg/api/api.go index 1992fe5787b..9c281c2af05 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -271,8 +271,9 @@ type BuildOptions struct { SourceRoot string // Documentation: https://esbuild.github.io/api/#source-root SourcesContent SourcesContent // Documentation: https://esbuild.github.io/api/#sources-content - Target Target // Documentation: https://esbuild.github.io/api/#target - Engines []Engine // Documentation: https://esbuild.github.io/api/#target + Target Target // Documentation: https://esbuild.github.io/api/#target + Engines []Engine // Documentation: https://esbuild.github.io/api/#target + Supported map[string]bool // Documentation: https://esbuild.github.io/api/#supported MangleProps string // Documentation: https://esbuild.github.io/api/#mangle-props ReserveProps string // Documentation: https://esbuild.github.io/api/#mangle-props @@ -386,8 +387,9 @@ type TransformOptions struct { SourceRoot string // Documentation: https://esbuild.github.io/api/#source-root SourcesContent SourcesContent // Documentation: https://esbuild.github.io/api/#sources-content - Target Target // Documentation: https://esbuild.github.io/api/#target - Engines []Engine // Documentation: https://esbuild.github.io/api/#target + Target Target // Documentation: https://esbuild.github.io/api/#target + Engines []Engine // Documentation: https://esbuild.github.io/api/#target + Supported map[string]bool // Documentation: https://esbuild.github.io/api/#supported Format Format // Documentation: https://esbuild.github.io/api/#format GlobalName string // Documentation: https://esbuild.github.io/api/#global-name diff --git a/pkg/api/api_impl.go b/pkg/api/api_impl.go index 10157bb8e5c..b282fd2edc1 100644 --- a/pkg/api/api_impl.go +++ b/pkg/api/api_impl.go @@ -375,6 +375,30 @@ func validateFeatures(log logger.Log, target Target, engines []Engine) (config.T return targetFromAPI, compat.UnsupportedJSFeatures(constraints), compat.UnsupportedCSSFeatures(constraints), targetEnv } +func validateSupported(log logger.Log, supported map[string]bool) ( + jsFeature compat.JSFeature, + jsMask compat.JSFeature, + cssFeature compat.CSSFeature, + cssMask compat.CSSFeature, +) { + for k, v := range supported { + if js, ok := compat.StringToJSFeature[k]; ok { + jsMask |= js + if !v { + jsFeature |= js + } + } else if css, ok := compat.StringToCSSFeature[k]; ok { + cssMask |= css + if !v { + cssFeature |= css + } + } else { + log.AddError(nil, logger.Range{}, fmt.Sprintf("%q is not a valid feature name for the \"supported\" setting", k)) + } + } + return +} + func validateGlobalName(log logger.Log, text string) []string { if text != "" { source := logger.Source{ @@ -872,6 +896,7 @@ func rebuildImpl( panic(err.Error()) } targetFromAPI, jsFeatures, cssFeatures, targetEnv := validateFeatures(log, buildOpts.Target, buildOpts.Engines) + jsOverrides, jsMask, cssOverrides, cssMask := validateSupported(log, buildOpts.Supported) outJS, outCSS := validateOutputExtensions(log, buildOpts.OutExtensions) bannerJS, bannerCSS := validateBannerOrFooter(log, "banner", buildOpts.Banner) footerJS, footerCSS := validateBannerOrFooter(log, "footer", buildOpts.Footer) @@ -879,10 +904,14 @@ func rebuildImpl( defines, injectedDefines := validateDefines(log, buildOpts.Define, buildOpts.Pure, buildOpts.Platform, minify, buildOpts.Drop) mangleCache := cloneMangleCache(log, buildOpts.MangleCache) options := config.Options{ - TargetFromAPI: targetFromAPI, - UnsupportedJSFeatures: jsFeatures, - UnsupportedCSSFeatures: cssFeatures, - OriginalTargetEnv: targetEnv, + TargetFromAPI: targetFromAPI, + UnsupportedJSFeatures: jsFeatures.ApplyOverrides(jsOverrides, jsMask), + UnsupportedCSSFeatures: cssFeatures.ApplyOverrides(cssOverrides, cssMask), + UnsupportedJSFeatureOverrides: jsOverrides, + UnsupportedJSFeatureOverridesMask: jsMask, + UnsupportedCSSFeatureOverrides: cssOverrides, + UnsupportedCSSFeatureOverridesMask: cssMask, + OriginalTargetEnv: targetEnv, JSX: config.JSXOptions{ Preserve: buildOpts.JSXMode == JSXModePreserve, Factory: validateJSXExpr(log, buildOpts.JSXFactory, "factory"), @@ -1387,38 +1416,43 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult // Convert and validate the transformOpts targetFromAPI, jsFeatures, cssFeatures, targetEnv := validateFeatures(log, transformOpts.Target, transformOpts.Engines) + jsOverrides, jsMask, cssOverrides, cssMask := validateSupported(log, transformOpts.Supported) defines, injectedDefines := validateDefines(log, transformOpts.Define, transformOpts.Pure, PlatformNeutral, false /* minify */, transformOpts.Drop) mangleCache := cloneMangleCache(log, transformOpts.MangleCache) options := config.Options{ - TargetFromAPI: targetFromAPI, - UnsupportedJSFeatures: jsFeatures, - UnsupportedCSSFeatures: cssFeatures, - OriginalTargetEnv: targetEnv, - TSTarget: tsTarget, - TSAlwaysStrict: tsAlwaysStrict, - JSX: jsx, - Defines: defines, - InjectedDefines: injectedDefines, - 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, - UseDefineForClassFields: useDefineForClassFieldsTS, - UnusedImportFlagsTS: unusedImportFlagsTS, + TargetFromAPI: targetFromAPI, + UnsupportedJSFeatures: jsFeatures.ApplyOverrides(jsOverrides, jsMask), + UnsupportedCSSFeatures: cssFeatures.ApplyOverrides(cssOverrides, cssMask), + UnsupportedJSFeatureOverrides: jsOverrides, + UnsupportedJSFeatureOverridesMask: jsMask, + UnsupportedCSSFeatureOverrides: cssOverrides, + UnsupportedCSSFeatureOverridesMask: cssMask, + OriginalTargetEnv: targetEnv, + TSTarget: tsTarget, + TSAlwaysStrict: tsAlwaysStrict, + JSX: jsx, + Defines: defines, + InjectedDefines: injectedDefines, + 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, + UseDefineForClassFields: useDefineForClassFieldsTS, + UnusedImportFlagsTS: unusedImportFlagsTS, Stdin: &config.StdinInfo{ Loader: validateLoader(transformOpts.Loader), Contents: input, diff --git a/pkg/cli/cli_impl.go b/pkg/cli/cli_impl.go index 4b72c3e41e2..98651a12474 100644 --- a/pkg/cli/cli_impl.go +++ b/pkg/cli/cli_impl.go @@ -23,6 +23,7 @@ func newBuildOptions() api.BuildOptions { Footer: make(map[string]string), Loader: make(map[string]api.Loader), LogOverride: make(map[string]api.LogLevel), + Supported: make(map[string]bool), } } @@ -30,6 +31,7 @@ func newTransformOptions() api.TransformOptions { return api.TransformOptions{ Define: make(map[string]string), LogOverride: make(map[string]api.LogLevel), + Supported: make(map[string]bool), } } @@ -457,6 +459,24 @@ func parseOptionsImpl( transformOpts.LogOverride[value[:equals]] = logLevel } + case strings.HasPrefix(arg, "--supported:"): + value := arg[len("--supported:"):] + equals := strings.IndexByte(value, '=') + if equals == -1 { + return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote( + fmt.Sprintf("Missing \"=\" in %q", arg), + "You need to use \"=\" to specify both the name of the feature and whether it is supported or not. "+ + "For example, \"--supported:arrow=false\" marks arrow functions as unsupported.", + ) + } + if isSupported, err := parseBoolFlag(arg, true); err != nil { + return parseOptionsExtras{}, err + } else if buildOpts != nil { + buildOpts.Supported[value[:equals]] = isSupported + } else { + transformOpts.Supported[value[:equals]] = isSupported + } + case strings.HasPrefix(arg, "--pure:"): value := arg[len("--pure:"):] if buildOpts != nil { @@ -787,6 +807,7 @@ func parseOptionsImpl( "log-override": true, "out-extension": true, "pure": true, + "supported": true, } note := "" diff --git a/scripts/compat-table.js b/scripts/compat-table.js index 2ba767f2e04..e5744d7b522 100644 --- a/scripts/compat-table.js +++ b/scripts/compat-table.js @@ -344,6 +344,10 @@ function upper(text) { return text[0].toUpperCase() + text.slice(1) } +function jsFeatureString(feature) { + return feature.replace(/([A-Z])/g, '-$1').slice(1).toLowerCase() +} + function writeInnerMap(obj) { const keys = Object.keys(obj).sort() const maxLength = keys.reduce((a, b) => Math.max(a, b.length + 1), 0) @@ -381,10 +385,22 @@ const ( ${Object.keys(versions).sort().map((x, i) => `\t${x}${i ? '' : ' JSFeature = 1 << iota'}`).join('\n')} ) +var StringToJSFeature = map[string]JSFeature{ +${Object.keys(versions).sort().map(x => `\t"${jsFeatureString(x)}": ${x},`).join('\n')} +} + +var JSFeatureToString = map[JSFeature]string{ +${Object.keys(versions).sort().map(x => `\t${x}: "${jsFeatureString(x)}",`).join('\n')} +} + func (features JSFeature) Has(feature JSFeature) bool { \treturn (features & feature) != 0 } +func (features JSFeature) ApplyOverrides(overrides JSFeature, mask JSFeature) JSFeature { +\treturn (features & ^mask) | (overrides & mask) +} + var jsTable = map[JSFeature]map[Engine][]versionRange{ ${Object.keys(versions).sort().map(x => `\t${x}: ${writeInnerMap(versions[x])},`).join('\n')} } diff --git a/scripts/js-api-tests.js b/scripts/js-api-tests.js index 027b2f785ba..f8f41e32eef 100644 --- a/scripts/js-api-tests.js +++ b/scripts/js-api-tests.js @@ -4379,9 +4379,46 @@ let transformTests = { } }, + async supported({ esbuild }) { + const check = async (options, input, expected) => { + try { + assert.strictEqual((await esbuild.transform(input, options)).code, expected) + } catch (e) { + if (e.errors) assert.strictEqual(e.errors[0].text, expected) + else throw e + } + } + + await Promise.all([ + // JS: lower + check({ supported: { arrow: true } }, `x = () => y`, `x = () => y;\n`), + check({ supported: { arrow: false } }, `x = () => y`, `x = function() {\n return y;\n};\n`), + check({ supported: { arrow: true }, target: 'es5' }, `x = () => y`, `x = () => y;\n`), + check({ supported: { arrow: false }, target: 'es5' }, `x = () => y`, `x = function() {\n return y;\n};\n`), + check({ supported: { arrow: true }, target: 'es2022' }, `x = () => y`, `x = () => y;\n`), + check({ supported: { arrow: false }, target: 'es2022' }, `x = () => y`, `x = function() {\n return y;\n};\n`), + + // JS: error + check({ supported: { bigint: true } }, `x = 1n`, `x = 1n;\n`), + check({ supported: { bigint: false } }, `x = 1n`, `Big integer literals are not available in the configured target environment`), + check({ supported: { bigint: true }, target: 'es5' }, `x = 1n`, `x = 1n;\n`), + check({ supported: { bigint: false }, target: 'es5' }, `x = 1n`, `Big integer literals are not available in the configured target environment ("es5" + 1 override)`), + check({ supported: { bigint: true }, target: 'es2022' }, `x = 1n`, `x = 1n;\n`), + check({ supported: { bigint: false }, target: 'es2022' }, `x = 1n`, `Big integer literals are not available in the configured target environment ("es2022" + 1 override)`), + + // CSS: lower + check({ supported: { 'hex-rgba': true }, loader: 'css' }, `a { color: #1234 }`, `a {\n color: #1234;\n}\n`), + check({ supported: { 'hex-rgba': false }, loader: 'css' }, `a { color: #1234 }`, `a {\n color: rgba(17, 34, 51, 0.267);\n}\n`), + + // Check for "+ 2 overrides" + check({ supported: { bigint: false, arrow: true }, target: 'es2022' }, `x = 1n`, `Big integer literals are not available in the configured target environment ("es2022" + 2 overrides)`), + ]) + }, + async regExpFeatures({ esbuild }) { const check = async (target, input, expected) => assert.strictEqual((await esbuild.transform(input, { target })).code, expected) + await Promise.all([ // RegExpStickyAndUnicodeFlags check('es6', `x1 = /./y`, `x1 = /./y;\n`),