diff --git a/CHANGELOG.md b/CHANGELOG.md index ffa0f1127ca..007d0986121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ * Match `define` to strings in index expressions ([#2050](https://github.com/evanw/esbuild/issues/2050)) - With this release, configuring `--define:foo.bar=baz` now matches and replaces both `foo.bar` and `foo['bar']` expressions in the original source code. This is necessary for people who have enabled TypeScript's `noPropertyAccessFromIndexSignature` feature, which prevents you from using normal property access syntax on a type with an index signature such as in the following code: + With this release, configuring `--define:foo.bar=baz` now matches and replaces both `foo.bar` and `foo['bar']` expressions in the original source code. This is necessary for people who have enabled TypeScript's [`noPropertyAccessFromIndexSignature` feature](https://www.typescriptlang.org/tsconfig#noPropertyAccessFromIndexSignature), which prevents you from using normal property access syntax on a type with an index signature such as in the following code: ```ts declare let foo: { [key: string]: any } @@ -26,6 +26,27 @@ baz; ``` +* Add `--mangle-quoted` to mangle quoted properties ([#218](https://github.com/evanw/esbuild/issues/218)) + + The `--mangle-props=` flag tells esbuild to automatically rename all properties matching the provided regular expression to shorter names to save space. Previously esbuild never modified the contents of string literals. In particular, `--mangle-props=_` would mangle `foo._bar` but not `foo['_bar']`. There are some coding patterns where renaming quoted property names is desirable, such as when using TypeScript's [`noPropertyAccessFromIndexSignature` feature](https://www.typescriptlang.org/tsconfig#noPropertyAccessFromIndexSignature) or when using TypeScript's [discriminated union narrowing behavior](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions): + + ```ts + interface Foo { _foo: string } + interface Bar { _bar: number } + declare const value: Foo | Bar + console.log('_foo' in value ? value._foo : value._bar) + ``` + + The `'_foo' in value` check tells TypeScript to narrow the type of `value` to `Foo` in the true branch and to `Bar` in the false branch. Previously esbuild didn't mangle the property name `'_foo'` because it was inside a string literal. With this release, you can now use `--mangle-quoted` to also rename property names inside string literals: + + ```js + // Old output (with --mangle-props=_) + console.log("_foo" in value ? value.a : value.b); + + // New output (with --mangle-props=_ --mangle-quoted) + console.log("a" in value ? value.a : value.b); + ``` + * Parse and discard TypeScript `export as namespace` statements ([#2070](https://github.com/evanw/esbuild/issues/2070)) TypeScript `.d.ts` type declaration files can sometimes contain statements of the form `export as namespace foo;`. I believe these serve to declare that the module adds a property of that name to the global object. You aren't supposed to feed `.d.ts` files to esbuild so this normally doesn't matter, but sometimes esbuild can end up having to parse them. One such case is if you import a type-only package who's `main` field in `package.json` is a `.d.ts` file. diff --git a/cmd/esbuild/main.go b/cmd/esbuild/main.go index b0386eb6d77..1082d05ca9e 100644 --- a/cmd/esbuild/main.go +++ b/cmd/esbuild/main.go @@ -92,6 +92,7 @@ var helpText = func(colors logger.Colors) string { browser and "main,module" when platform is node) --mangle-cache=... Save "mangle props" decisions to a JSON file --mangle-props=... Rename all properties matching a regular expression + --mangle-quoted=... Enable renaming of quoted properties (true | false) --metafile=... Write metadata about the build to a JSON file --minify-whitespace Remove whitespace in output files --minify-identifiers Shorten identifiers in output files diff --git a/internal/bundler/bundler_default_test.go b/internal/bundler/bundler_default_test.go index eed4787959a..997d1ef22a7 100644 --- a/internal/bundler/bundler_default_test.go +++ b/internal/bundler/bundler_default_test.go @@ -5872,3 +5872,70 @@ func TestManglePropsSuperCall(t *testing.T) { }, }) } + +func TestMangleNoQuotedProps(t *testing.T) { + loader_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + x['_doNotMangleThis']; + x?.['_doNotMangleThis']; + x[y ? '_doNotMangleThis' : z]; + x?.[y ? '_doNotMangleThis' : z]; + x[y ? z : '_doNotMangleThis']; + x?.[y ? z : '_doNotMangleThis']; + ({ '_doNotMangleThis': x }); + (class { '_doNotMangleThis' = x }); + var { '_doNotMangleThis': x } = y; + '_doNotMangleThis' in x; + (y ? '_doNotMangleThis' : z) in x; + (y ? z : '_doNotMangleThis') in x; + `, + }, + entryPaths: []string{"/entry.js"}, + options: config.Options{ + Mode: config.ModePassThrough, + AbsOutputDir: "/out", + MangleProps: regexp.MustCompile("_"), + MangleQuoted: false, + }, + }) +} + +func TestMangleQuotedProps(t *testing.T) { + loader_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/keep.js": ` + foo("_keepThisProperty"); + foo((x, "_keepThisProperty")); + foo(x ? "_keepThisProperty" : "_keepThisPropertyToo"); + x[foo("_keepThisProperty")]; + x?.[foo("_keepThisProperty")]; + ({ [foo("_keepThisProperty")]: x }); + (class { [foo("_keepThisProperty")] = x }); + var { [foo("_keepThisProperty")]: x } = y; + foo("_keepThisProperty") in x; + `, + "/mangle.js": ` + x['_mangleThis']; + x?.['_mangleThis']; + x[y ? '_mangleThis' : z]; + x?.[y ? '_mangleThis' : z]; + x[y ? z : '_mangleThis']; + x?.[y ? z : '_mangleThis']; + ({ '_mangleThis': x }); + (class { '_mangleThis' = x }); + var { '_mangleThis': x } = y; + '_mangleThis' in x; + (y ? '_mangleThis' : z) in x; + (y ? z : '_mangleThis') in x; + `, + }, + entryPaths: []string{"/keep.js", "/mangle.js"}, + options: config.Options{ + Mode: config.ModePassThrough, + AbsOutputDir: "/out", + MangleProps: regexp.MustCompile("_"), + MangleQuoted: true, + }, + }) +} diff --git a/internal/config/config.go b/internal/config/config.go index 16254b8bc11..cec60bb7e28 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -281,6 +281,7 @@ type Options struct { IgnoreDCEAnnotations bool TreeShaking bool DropDebugger bool + MangleQuoted bool Platform Platform TargetFromAPI TargetFromAPI OutputFormat Format diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index f6edc3bb754..a65737fac4c 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -390,6 +390,7 @@ type optionsThatSupportStructuralEquality struct { ignoreDCEAnnotations bool treeShaking bool dropDebugger bool + mangleQuoted bool unusedImportsTS config.UnusedImportsTS useDefineForClassFields config.MaybeBool } @@ -420,6 +421,7 @@ func OptionsFromConfig(options *config.Options) Options { ignoreDCEAnnotations: options.IgnoreDCEAnnotations, treeShaking: options.TreeShaking, dropDebugger: options.DropDebugger, + mangleQuoted: options.MangleQuoted, unusedImportsTS: options.UnusedImportsTS, useDefineForClassFields: options.UseDefineForClassFields, }, @@ -8508,7 +8510,9 @@ func (p *parser) visitBinding(binding js_ast.Binding, opts bindingOpts) { if mangled, ok := property.Key.Data.(*js_ast.EMangledProp); ok { mangled.Ref = p.symbolForMangledProp(p.loadNameFromRef(mangled.Ref)) } else { - property.Key = p.visitExpr(property.Key) + property.Key, _ = p.visitExprInOut(property.Key, exprIn{ + shouldMangleStringsAsProps: true, + }) } } p.visitBinding(property.Value, opts) @@ -10371,7 +10375,9 @@ func (p *parser) visitClass(nameScopeLoc logger.Loc, class *js_ast.Class) js_ast k.Ref = p.symbolForMangledProp(p.loadNameFromRef(k.Ref)) default: - key := p.visitExpr(property.Key) + key, _ := p.visitExprInOut(property.Key, exprIn{ + shouldMangleStringsAsProps: true, + }) property.Key = key // "class {['x'] = y}" => "class {x = y}" @@ -11218,6 +11224,11 @@ type exprIn struct { // See also "thisArgFunc" and "thisArgWrapFunc" in "exprOut". storeThisArgForParentOptionalChain bool + // If true, string literals that match the current property mangling pattern + // should be turned into EMangledProp expressions, which will cause us to + // rename them in the linker. + shouldMangleStringsAsProps bool + // Certain substitutions of identifiers are disallowed for assignment targets. // For example, we shouldn't transform "undefined = 1" into "void 0 = 1". This // isn't something real-world code would do but it matters for conformance @@ -11488,6 +11499,14 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO } } + if in.shouldMangleStringsAsProps && p.options.mangleQuoted && !e.PreferTemplate { + if name := helpers.UTF16ToString(e.Value); p.isMangledProp(name) { + return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EMangledProp{ + Ref: p.symbolForMangledProp(name), + }}, exprOut{} + } + } + case *js_ast.ENumber: if p.legacyOctalLiterals != nil && p.isStrictMode() { if r, ok := p.legacyOctalLiterals[expr.Data]; ok { @@ -11801,7 +11820,10 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO isTemplateTag := e == p.templateTag isStmtExpr := e == p.stmtExprValue wasAnonymousNamedExpr := p.isAnonymousNamedExpr(e.Right) - e.Left, _ = p.visitExprInOut(e.Left, exprIn{assignTarget: e.Op.BinaryAssignTarget()}) + e.Left, _ = p.visitExprInOut(e.Left, exprIn{ + assignTarget: e.Op.BinaryAssignTarget(), + shouldMangleStringsAsProps: e.Op == js_ast.BinOpIn, + }) // Mark the control flow as dead if the branch is never taken switch e.Op { @@ -11838,6 +11860,11 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO e.Right = p.visitExpr(e.Right) } + case js_ast.BinOpComma: + e.Right, _ = p.visitExprInOut(e.Right, exprIn{ + shouldMangleStringsAsProps: in.shouldMangleStringsAsProps, + }) + default: e.Right = p.visitExpr(e.Right) } @@ -12515,7 +12542,9 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO index.Ref = p.symbolForMangledProp(p.loadNameFromRef(index.Ref)) default: - e.Index = p.visitExpr(e.Index) + e.Index, _ = p.visitExprInOut(e.Index, exprIn{ + shouldMangleStringsAsProps: true, + }) } // Lower "super[prop]" if necessary @@ -12719,18 +12748,23 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO e.Test = p.simplifyBooleanExpr(e.Test) } + // Propagate these flags into the branches + childIn := exprIn{ + shouldMangleStringsAsProps: in.shouldMangleStringsAsProps, + } + // Fold constants if boolean, sideEffects, ok := toBooleanWithSideEffects(e.Test.Data); !ok { - e.Yes = p.visitExpr(e.Yes) - e.No = p.visitExpr(e.No) + e.Yes, _ = p.visitExprInOut(e.Yes, childIn) + e.No, _ = p.visitExprInOut(e.No, childIn) } else { // Mark the control flow as dead if the branch is never taken if boolean { // "true ? live : dead" - e.Yes = p.visitExpr(e.Yes) + e.Yes, _ = p.visitExprInOut(e.Yes, childIn) old := p.isControlFlowDead p.isControlFlowDead = true - e.No = p.visitExpr(e.No) + e.No, _ = p.visitExprInOut(e.No, childIn) p.isControlFlowDead = old if p.options.minifySyntax { @@ -12752,9 +12786,9 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO // "false ? dead : live" old := p.isControlFlowDead p.isControlFlowDead = true - e.Yes = p.visitExpr(e.Yes) + e.Yes, _ = p.visitExprInOut(e.Yes, childIn) p.isControlFlowDead = old - e.No = p.visitExpr(e.No) + e.No, _ = p.visitExprInOut(e.No, childIn) if p.options.minifySyntax { // "(a, false) ? b : c" => "a, c" @@ -12848,7 +12882,9 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO if mangled, ok := key.Data.(*js_ast.EMangledProp); ok { mangled.Ref = p.symbolForMangledProp(p.loadNameFromRef(mangled.Ref)) } else { - key = p.visitExpr(property.Key) + key, _ = p.visitExprInOut(property.Key, exprIn{ + shouldMangleStringsAsProps: true, + }) property.Key = key } diff --git a/lib/shared/common.ts b/lib/shared/common.ts index 44f806f9860..9be8858b52a 100644 --- a/lib/shared/common.ts +++ b/lib/shared/common.ts @@ -123,6 +123,7 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe let globalName = getFlag(options, keys, 'globalName', mustBeString); let mangleProps = getFlag(options, keys, 'mangleProps', mustBeRegExp); let reserveProps = getFlag(options, keys, 'reserveProps', mustBeRegExp); + let mangleQuoted = getFlag(options, keys, 'mangleQuoted', mustBeBoolean); let minify = getFlag(options, keys, 'minify', mustBeBoolean); let minifySyntax = getFlag(options, keys, 'minifySyntax', mustBeBoolean); let minifyWhitespace = getFlag(options, keys, 'minifyWhitespace', mustBeBoolean); @@ -158,6 +159,7 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe if (drop) for (let what of drop) flags.push(`--drop:${what}`); if (mangleProps) flags.push(`--mangle-props=${mangleProps.source}`); if (reserveProps) flags.push(`--reserve-props=${reserveProps.source}`); + if (mangleQuoted !== void 0) flags.push(`--mangle-quoted=${mangleQuoted}`) if (jsx) flags.push(`--jsx=${jsx}`); if (jsxFactory) flags.push(`--jsx-factory=${jsxFactory}`); diff --git a/lib/shared/types.ts b/lib/shared/types.ts index ca281828a59..058f2d87204 100644 --- a/lib/shared/types.ts +++ b/lib/shared/types.ts @@ -27,6 +27,8 @@ interface CommonOptions { /** Documentation: https://esbuild.github.io/api/#mangle-props */ reserveProps?: RegExp; /** Documentation: https://esbuild.github.io/api/#mangle-props */ + mangleQuoted?: boolean; + /** Documentation: https://esbuild.github.io/api/#mangle-props */ mangleCache?: Record; /** Documentation: https://esbuild.github.io/api/#drop */ drop?: Drop[]; diff --git a/pkg/api/api.go b/pkg/api/api.go index 5ffd94b4c54..e342dfdb776 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -248,6 +248,13 @@ const ( DropDebugger ) +type MangleQuoted uint8 + +const ( + MangleQuotedFalse MangleQuoted = iota + MangleQuotedTrue +) + //////////////////////////////////////////////////////////////////////////////// // Build API @@ -265,6 +272,7 @@ type BuildOptions struct { MangleProps string // Documentation: https://esbuild.github.io/api/#mangle-props ReserveProps string // Documentation: https://esbuild.github.io/api/#mangle-props + MangleQuoted MangleQuoted // Documentation: https://esbuild.github.io/api/#mangle-props MangleCache map[string]interface{} // Documentation: https://esbuild.github.io/api/#mangle-props Drop Drop // Documentation: https://esbuild.github.io/api/#drop MinifyWhitespace bool // Documentation: https://esbuild.github.io/api/#minify @@ -381,6 +389,7 @@ type TransformOptions struct { MangleProps string // Documentation: https://esbuild.github.io/api/#mangle-props ReserveProps string // Documentation: https://esbuild.github.io/api/#mangle-props + MangleQuoted MangleQuoted // Documentation: https://esbuild.github.io/api/#mangle-props MangleCache map[string]interface{} // Documentation: https://esbuild.github.io/api/#mangle-props Drop Drop // Documentation: https://esbuild.github.io/api/#drop MinifyWhitespace bool // Documentation: https://esbuild.github.io/api/#minify diff --git a/pkg/api/api_impl.go b/pkg/api/api_impl.go index 4487a8e3381..b14b06547de 100644 --- a/pkg/api/api_impl.go +++ b/pkg/api/api_impl.go @@ -923,6 +923,7 @@ func rebuildImpl( MinifyIdentifiers: buildOpts.MinifyIdentifiers, MangleProps: validateRegex(log, "mangle props", buildOpts.MangleProps), ReserveProps: validateRegex(log, "reserve props", buildOpts.ReserveProps), + MangleQuoted: buildOpts.MangleQuoted == MangleQuotedTrue, DropDebugger: (buildOpts.Drop & DropDebugger) != 0, AllowOverwrite: buildOpts.AllowOverwrite, ASCIIOnly: validateASCIIOnly(buildOpts.Charset), @@ -1424,6 +1425,7 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult 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, diff --git a/pkg/cli/cli_impl.go b/pkg/cli/cli_impl.go index 5d0042c3ebb..64ecabca6b6 100644 --- a/pkg/cli/cli_impl.go +++ b/pkg/cli/cli_impl.go @@ -160,6 +160,23 @@ func parseOptionsImpl( transformOpts.MinifyIdentifiers = value } + case isBoolFlag(arg, "--mangle-quoted"): + if value, err := parseBoolFlag(arg, true); err != nil { + return parseOptionsExtras{}, err + } else { + var mangleQuoted *api.MangleQuoted + if buildOpts != nil { + mangleQuoted = &buildOpts.MangleQuoted + } else { + mangleQuoted = &transformOpts.MangleQuoted + } + if value { + *mangleQuoted = api.MangleQuotedTrue + } else { + *mangleQuoted = api.MangleQuotedFalse + } + } + case strings.HasPrefix(arg, "--mangle-props="): value := arg[len("--mangle-props="):] if buildOpts != nil { @@ -727,6 +744,7 @@ func parseOptionsImpl( "main-fields": true, "mangle-cache": true, "mangle-props": true, + "mangle-quoted": true, "metafile": true, "minify-identifiers": true, "minify-syntax": true, diff --git a/scripts/js-api-tests.js b/scripts/js-api-tests.js index 8ad5764c552..a3190ba9d11 100644 --- a/scripts/js-api-tests.js +++ b/scripts/js-api-tests.js @@ -3212,6 +3212,14 @@ let transformTests = { new Function(code)() }, + async mangleQuotedTransform({ esbuild }) { + var { code } = await esbuild.transform(`x.foo_ = 'foo_' in x`, { + mangleProps: /_/, + mangleQuoted: true, + }) + assert.strictEqual(code, 'x.a = "a" in x;\n') + }, + async mangleCacheTransform({ esbuild }) { var { code, mangleCache } = await esbuild.transform(`x = { x_: 0, y_: 1, z_: 2 }`, { mangleProps: /_/,