Skip to content

Commit

Permalink
add the "--mangle-quoted" flag (#218)
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Mar 3, 2022
1 parent ce04765 commit 80994ef
Show file tree
Hide file tree
Showing 11 changed files with 179 additions and 12 deletions.
23 changes: 22 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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.
Expand Down
1 change: 1 addition & 0 deletions cmd/esbuild/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 67 additions & 0 deletions internal/bundler/bundler_default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
})
}
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ type Options struct {
IgnoreDCEAnnotations bool
TreeShaking bool
DropDebugger bool
MangleQuoted bool
Platform Platform
TargetFromAPI TargetFromAPI
OutputFormat Format
Expand Down
58 changes: 47 additions & 11 deletions internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ type optionsThatSupportStructuralEquality struct {
ignoreDCEAnnotations bool
treeShaking bool
dropDebugger bool
mangleQuoted bool
unusedImportsTS config.UnusedImportsTS
useDefineForClassFields config.MaybeBool
}
Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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}"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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"
Expand Down Expand Up @@ -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
}

Expand Down
2 changes: 2 additions & 0 deletions lib/shared/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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}`);
Expand Down
2 changes: 2 additions & 0 deletions lib/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string | false>;
/** Documentation: https://esbuild.github.io/api/#drop */
drop?: Drop[];
Expand Down
9 changes: 9 additions & 0 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,13 @@ const (
DropDebugger
)

type MangleQuoted uint8

const (
MangleQuotedFalse MangleQuoted = iota
MangleQuotedTrue
)

////////////////////////////////////////////////////////////////////////////////
// Build API

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions pkg/api/api_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand Down
18 changes: 18 additions & 0 deletions pkg/cli/cli_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions scripts/js-api-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: /_/,
Expand Down

0 comments on commit 80994ef

Please sign in to comment.