Skip to content

Commit

Permalink
fix #2574: support /* @__KEY__ */ comments
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jun 10, 2023
1 parent 6eb0e71 commit b2176c8
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 49 deletions.
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,35 @@

If esbuild bundles input files with source maps and those source maps contain a `sourcesContent` array with `null` entries, esbuild previously copied those `null` entries over to the output source map. With this release, esbuild will now attempt to fill in those `null` entries by looking for a file on the file system with the corresponding name from the `sources` array. This matches esbuild's existing behavior that automatically generates the `sourcesContent` array from the file system if the entire `sourcesContent` array is missing.

* Support `/* @__KEY__ */` comments for mangling property names ([#2574](https://github.com/evanw/esbuild/issues/2574))

Property mangling is an advanced feature that enables esbuild to minify certain property names, even though it's not possible to automatically determine that it's safe to do so. The safe property names are configured via regular expression such as `--mangle-props=_$` (mangle all properties ending in `_`).

Sometimes it's desirable to also minify strings containing property names, even though it's not possible to automatically determine which strings are property names. This release makes it possible to do this by annotating those strings with `/* @__KEY__ */`. This is a convention that Terser added earlier this year, and which esbuild is now following too: https://github.com/terser/terser/pull/1365. Using it looks like this:

```js
// Original code
console.log(
[obj.mangle_, obj.keep],
[obj.get('mangle_'), obj.get('keep')],
[obj.get(/* @__KEY__ */ 'mangle_'), obj.get(/* @__KEY__ */ 'keep')],
)

// Old output (with --mangle-props=_$)
console.log(
[obj.a, obj.keep],
[obj.get("mangle_"), obj.get("keep")],
[obj.get(/* @__KEY__ */ "mangle_"), obj.get(/* @__KEY__ */ "keep")]
);

// New output (with --mangle-props=_$)
console.log(
[obj.a, obj.keep],
[obj.get("mangle_"), obj.get("keep")],
[obj.get(/* @__KEY__ */ "a"), obj.get(/* @__KEY__ */ "keep")]
);
```

## 0.18.0

**This release deliberately contains backwards-incompatible changes.** To avoid automatically picking up releases like this, you should either be pinning the exact version of `esbuild` in your `package.json` file (recommended) or be using a version range syntax that only accepts patch upgrades such as `^0.17.0` or `~0.17.0`. See npm's documentation about [semver](https://docs.npmjs.com/cli/v6/using-npm/semver/) for more information.
Expand Down
73 changes: 73 additions & 0 deletions internal/bundler_tests/bundler_default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7394,6 +7394,79 @@ func TestMangleQuotedPropsMinifySyntax(t *testing.T) {
})
}

func TestPreserveKeyComment(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
x(/* __KEY__ */ 'notKey', /* __KEY__ */ ` + "`" + `notKey` + "`" + `)
x(/* @__KEY__ */ 'key', /* @__KEY__ */ ` + "`" + `key` + "`" + `)
x(/* #__KEY__ */ 'alsoKey', /* #__KEY__ */ ` + "`" + `alsoKey` + "`" + `)
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModePassThrough,
AbsOutputDir: "/out",
},
})
}

func TestManglePropsKeyComment(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
x(/* __KEY__ */ '_doNotMangleThis', /* __KEY__ */ ` + "`" + `_doNotMangleThis` + "`" + `)
x._mangleThis(/* @__KEY__ */ '_mangleThis', /* @__KEY__ */ ` + "`" + `_mangleThis` + "`" + `)
x._mangleThisToo(/* #__KEY__ */ '_mangleThisToo', /* #__KEY__ */ ` + "`" + `_mangleThisToo` + "`" + `)
x._someKey = /* #__KEY__ */ '_someKey' in y
x([
` + "`" + `foo.${/* @__KEY__ */ '_mangleThis'} = bar.${/* @__KEY__ */ '_mangleThisToo'}` + "`" + `,
` + "`" + `foo.${/* @__KEY__ */ 'notMangled'} = bar.${/* @__KEY__ */ 'notMangledEither'}` + "`" + `,
])
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModePassThrough,
AbsOutputDir: "/out",
MangleProps: regexp.MustCompile("_"),
},
})
}

func TestManglePropsKeyCommentMinify(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
x = class {
_mangleThis = 1;
[/* @__KEY__ */ '_mangleThisToo'] = 2;
'_doNotMangleThis' = 3;
}
x = {
_mangleThis: 1,
[/* @__KEY__ */ '_mangleThisToo']: 2,
'_doNotMangleThis': 3,
}
x._mangleThis = 1
x[/* @__KEY__ */ '_mangleThisToo'] = 2
x['_doNotMangleThis'] = 3
x([
` + "`" + `foo.${/* @__KEY__ */ '_mangleThis'} = bar.${/* @__KEY__ */ '_mangleThisToo'}` + "`" + `,
` + "`" + `foo.${/* @__KEY__ */ 'notMangled'} = bar.${/* @__KEY__ */ 'notMangledEither'}` + "`" + `,
])
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModePassThrough,
AbsOutputDir: "/out",
MangleProps: regexp.MustCompile("_"),
MinifySyntax: true,
},
})
}

func TestIndirectRequireMessage(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
Expand Down
49 changes: 47 additions & 2 deletions internal/bundler_tests/snapshots/snapshots_default.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3123,6 +3123,39 @@ export default [
/* @__PURE__ */ React.createElement("foo", { "KEEP:THIS_": true })
];

================================================================================
TestManglePropsKeyComment
---------- /out/entry.js ----------
x(
/* __KEY__ */
"_doNotMangleThis",
/* __KEY__ */
`_doNotMangleThis`
);
x.a(/* @__KEY__ */ "a", /* @__KEY__ */ "a");
x.b(/* @__KEY__ */ "b", /* @__KEY__ */ "b");
x.c = /* @__KEY__ */ "c" in y;
x([
`foo.${/* @__KEY__ */ "a"} = bar.${/* @__KEY__ */ "b"}`,
`foo.${/* @__KEY__ */ "notMangled"} = bar.${/* @__KEY__ */ "notMangledEither"}`
]);

================================================================================
TestManglePropsKeyCommentMinify
---------- /out/entry.js ----------
x = class {
a = 1;
b = 2;
_doNotMangleThis = 3;
}, x = {
a: 1,
b: 2,
_doNotMangleThis: 3
}, x.a = 1, x.b = 2, x._doNotMangleThis = 3, x([
"foo.a = bar.b",
"foo.notMangled = bar.notMangledEither"
]);

================================================================================
TestManglePropsKeywordPropertyMinify
---------- /out/entry.js ----------
Expand Down Expand Up @@ -3381,10 +3414,10 @@ var { [foo("_keepThisProperty")]: x } = y;
foo("_keepThisProperty") in x;

---------- /out/mangle.js ----------
x.a, x?.a, x[y ? "a" : z], x?.[y ? "a" : z], x[y ? z : "a"], x?.[y ? z : "a"], x[y, "a"], x?.[y, "a"], "a" + "", (y, "a") + "", class {
x.a, x?.a, x[y ? "a" : z], x?.[y ? "a" : z], x[y ? z : "a"], x?.[y ? z : "a"], x[y, "a"], x?.[y, "a"], (y, "a") + "", class {
a = x;
}, class {
["a"] = x;
a = x;
}, class {
[(y, "a")] = x;
};
Expand Down Expand Up @@ -4732,6 +4765,18 @@ import "alias/pkg_foo_bar/baz";
import "alias/pkg/bar/baz";
import "alias/pkg/baz";

================================================================================
TestPreserveKeyComment
---------- /out/entry.js ----------
x(
/* __KEY__ */
"notKey",
/* __KEY__ */
`notKey`
);
x(/* @__KEY__ */ "key", /* @__KEY__ */ `key`);
x(/* @__KEY__ */ "alsoKey", /* @__KEY__ */ `alsoKey`);

================================================================================
TestQuotedProperty
---------- /out/entry.js ----------
Expand Down
10 changes: 6 additions & 4 deletions internal/js_ast/js_ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,8 @@ type EPrivateIdentifier struct {
// This represents an internal property name that can be mangled. The symbol
// referenced by this expression should be a "SymbolMangledProp" symbol.
type EMangledProp struct {
Ref Ref
Ref Ref
HasPropertyKeyComment bool // If true, a preceding comment contains "@__KEY__"
}

type EJSXElement struct {
Expand Down Expand Up @@ -746,9 +747,10 @@ type ESpread struct{ Value Expr }
// This is used for both strings and no-substitution template literals to reduce
// the number of cases that need to be checked for string optimization code
type EString struct {
Value []uint16
LegacyOctalLoc logger.Loc
PreferTemplate bool
Value []uint16
LegacyOctalLoc logger.Loc
PreferTemplate bool
HasPropertyKeyComment bool // If true, a preceding comment contains "@__KEY__"
}

type TemplatePart struct {
Expand Down
21 changes: 17 additions & 4 deletions internal/js_lexer/js_lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ type Lexer struct {
Token T
ts config.TSOptions
HasNewlineBefore bool
HasPureCommentBefore bool
HasCommentBefore CommentBefore
IsLegacyOctalLiteral bool
PrevTokenWasAwaitKeyword bool
rescanCloseBraceAsTemplateToken bool
Expand All @@ -292,6 +292,13 @@ type Lexer struct {
IsLogDisabled bool
}

type CommentBefore uint8

const (
PureCommentBefore CommentBefore = 1 << iota
KeyCommentBefore
)

type LexerPanic struct{}

func NewLexer(log logger.Log, source logger.Source, ts config.TSOptions) Lexer {
Expand Down Expand Up @@ -982,7 +989,7 @@ func (lexer *Lexer) NextInsideJSXElement() {

func (lexer *Lexer) Next() {
lexer.HasNewlineBefore = lexer.end == 0
lexer.HasPureCommentBefore = false
lexer.HasCommentBefore = 0
lexer.PrevTokenWasAwaitKeyword = false
lexer.LegalCommentsBeforeToken = lexer.LegalCommentsBeforeToken[:0]
lexer.CommentsBeforeToken = lexer.CommentsBeforeToken[:0]
Expand Down Expand Up @@ -2587,7 +2594,10 @@ func (lexer *Lexer) scanCommentText() {
rest := text[i+1 : endOfCommentText]
if hasPrefixWithWordBoundary(rest, "__PURE__") {
omitFromGeneralCommentPreservation = true
lexer.HasPureCommentBefore = true
lexer.HasCommentBefore |= PureCommentBefore
} else if hasPrefixWithWordBoundary(rest, "__KEY__") {
omitFromGeneralCommentPreservation = true
lexer.HasCommentBefore |= KeyCommentBefore
} else if i == 2 && strings.HasPrefix(rest, " sourceMappingURL=") {
if arg, ok := scanForPragmaArg(pragmaNoSpaceFirst, lexer.start+i+1, " sourceMappingURL=", rest); ok {
omitFromGeneralCommentPreservation = true
Expand All @@ -2599,7 +2609,10 @@ func (lexer *Lexer) scanCommentText() {
rest := text[i+1 : endOfCommentText]
if hasPrefixWithWordBoundary(rest, "__PURE__") {
omitFromGeneralCommentPreservation = true
lexer.HasPureCommentBefore = true
lexer.HasCommentBefore |= PureCommentBefore
} else if hasPrefixWithWordBoundary(rest, "__KEY__") {
omitFromGeneralCommentPreservation = true
lexer.HasCommentBefore |= KeyCommentBefore
} else if hasPrefixWithWordBoundary(rest, "preserve") || hasPrefixWithWordBoundary(rest, "license") {
hasLegalAnnotation = true
} else if hasPrefixWithWordBoundary(rest, "jsx") {
Expand Down
Loading

0 comments on commit b2176c8

Please sign in to comment.