Skip to content

Commit

Permalink
Add JSX side effects option (#2546)
Browse files Browse the repository at this point in the history
  • Loading branch information
rtsao authored Sep 18, 2022
1 parent a6acbaa commit d65315a
Show file tree
Hide file tree
Showing 11 changed files with 59 additions and 2 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@
This release changes esbuild's parsing of `@keyframes` to now consider this case to be an unrecognized CSS rule. That means it will be passed through unmodified (so you can now use esbuild to bundle this Firefox-specific CSS) but the CSS will not be pretty-printed or minified. I don't think it makes sense for esbuild to have special code to handle this Firefox-specific syntax at this time. This decision can be revisited in the future if other browsers add support for this feature.
* Add the `--jsx-side-effects` API option ([#2539](https://github.com/evanw/esbuild/issues/2539), [#2546](https://github.com/evanw/esbuild/pull/2546))
By default esbuild assumes that JSX expressions are side-effect free, which means they are annoated with `/* @__PURE__ */` comments and are removed during bundling when they are unused. This follows the common use of JSX for virtual DOM and applies to the vast majority of JSX libraries. However, some people have written JSX libraries that don't have this property. JSX expressions can have arbitrary side effects and can't be removed. If you are using such a library, you can now pass `--jsx-side-effects` to tell esbuild that JSX expressions have side effects so it won't remove them when they are unused.
This feature was contributed by [@rtsao](https://github.com/rtsao).
## 0.15.7
* Add `--watch=forever` to allow esbuild to never terminate ([#1511](https://github.com/evanw/esbuild/issues/1511), [#1885](https://github.com/evanw/esbuild/issues/1885))
Expand Down
1 change: 1 addition & 0 deletions cmd/esbuild/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ var helpText = func(colors logger.Colors) string {
--jsx-fragment=... What to use for JSX instead of React.Fragment
--jsx-import-source=... Override the package name for the automatic runtime
(default "react")
--jsx-side-effects Do not remove unused JSX expressions
--jsx=... Set to "automatic" to use React's automatic runtime
or to "preserve" to disable transforming JSX to JS
--keep-names Preserve "name" on functions and classes
Expand Down
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type JSXOptions struct {
AutomaticRuntime bool
ImportSource string
Development bool
SideEffects bool
}

type TSJSX uint8
Expand Down
4 changes: 2 additions & 2 deletions internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -12401,7 +12401,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
CloseParenLoc: e.CloseLoc,

// Enable tree shaking
CanBeUnwrappedIfUnused: !p.options.ignoreDCEAnnotations,
CanBeUnwrappedIfUnused: !p.options.ignoreDCEAnnotations && !p.options.jsx.SideEffects,
}}, exprOut{}
} else {
// Arguments to jsx()
Expand Down Expand Up @@ -12529,7 +12529,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
CloseParenLoc: e.CloseLoc,

// Enable tree shaking
CanBeUnwrappedIfUnused: !p.options.ignoreDCEAnnotations,
CanBeUnwrappedIfUnused: !p.options.ignoreDCEAnnotations && !p.options.jsx.SideEffects,
}}, exprOut{}
}
}
Expand Down
26 changes: 26 additions & 0 deletions internal/js_parser/js_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,16 @@ func expectPrintedJSX(t *testing.T, contents string, expected string) {
})
}

func expectPrintedJSXSideEffects(t *testing.T, contents string, expected string) {
t.Helper()
expectPrintedCommon(t, contents, expected, config.Options{
JSX: config.JSXOptions{
Parse: true,
SideEffects: true,
},
})
}

func expectPrintedMangleJSX(t *testing.T, contents string, expected string) {
t.Helper()
expectPrintedCommon(t, contents, expected, config.Options{
Expand All @@ -170,6 +180,7 @@ type JSXAutomaticTestOptions struct {
Development bool
ImportSource string
OmitJSXRuntimeForTests bool
SideEffects bool
}

func expectParseErrorJSXAutomatic(t *testing.T, options JSXAutomaticTestOptions, contents string, expected string) {
Expand All @@ -181,6 +192,7 @@ func expectParseErrorJSXAutomatic(t *testing.T, options JSXAutomaticTestOptions,
Parse: true,
Development: options.Development,
ImportSource: options.ImportSource,
SideEffects: options.SideEffects,
},
})
}
Expand All @@ -194,6 +206,7 @@ func expectPrintedJSXAutomatic(t *testing.T, options JSXAutomaticTestOptions, co
Parse: true,
Development: options.Development,
ImportSource: options.ImportSource,
SideEffects: options.SideEffects,
},
})
}
Expand Down Expand Up @@ -4813,6 +4826,11 @@ NOTE: Both "__source" and "__self" are set automatically by esbuild when using R
expectPrintedJSXAutomatic(t, pri, "<div/>", "import { jsx } from \"my-jsx-lib/jsx-runtime\";\n/* @__PURE__ */ jsx(\"div\", {});\n")
expectPrintedJSXAutomatic(t, pri, "<div {...props} key=\"key\" />", "import { createElement } from \"my-jsx-lib\";\n/* @__PURE__ */ createElement(\"div\", {\n ...props,\n key: \"key\"\n});\n")

// Impure JSX call expressions
pi := JSXAutomaticTestOptions{SideEffects: true, ImportSource: "my-jsx-lib"}
expectPrintedJSXAutomatic(t, pi, "<a/>", "import { jsx } from \"my-jsx-lib/jsx-runtime\";\njsx(\"a\", {});\n")
expectPrintedJSXAutomatic(t, pi, "<></>", "import { Fragment, jsx } from \"my-jsx-lib/jsx-runtime\";\njsx(Fragment, {});\n")

// Dev, without runtime imports
d := JSXAutomaticTestOptions{Development: true, OmitJSXRuntimeForTests: true}
expectPrintedJSXAutomatic(t, d, "<div>></div>", "/* @__PURE__ */ jsxDEV(\"div\", {\n children: \">\"\n}, void 0, false, {\n fileName: \"<stdin>\",\n lineNumber: 1,\n columnNumber: 1\n}, this);\n")
Expand Down Expand Up @@ -4930,6 +4948,14 @@ NOTE: You can enable React's "automatic" JSX transform for this file by using a
expectParseErrorJSX(t, "// @jsxRuntime automatic @jsxFrag f\n<></>", "<stdin>: WARNING: The JSX fragment cannot be set when using React's \"automatic\" JSX transform\n")
}

func TestJSXSideEffects(t *testing.T) {
expectPrintedJSX(t, "<a/>", "/* @__PURE__ */ React.createElement(\"a\", null);\n")
expectPrintedJSX(t, "<></>", "/* @__PURE__ */ React.createElement(React.Fragment, null);\n")

expectPrintedJSXSideEffects(t, "<a/>", "React.createElement(\"a\", null);\n")
expectPrintedJSXSideEffects(t, "<></>", "React.createElement(React.Fragment, null);\n")
}

func TestPreserveOptionalChainParentheses(t *testing.T) {
expectPrinted(t, "a?.b.c", "a?.b.c;\n")
expectPrinted(t, "(a?.b).c", "(a?.b).c;\n")
Expand Down
2 changes: 2 additions & 0 deletions lib/shared/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe
let jsxFragment = getFlag(options, keys, 'jsxFragment', mustBeString);
let jsxImportSource = getFlag(options, keys, 'jsxImportSource', mustBeString);
let jsxDev = getFlag(options, keys, 'jsxDev', mustBeBoolean);
let jsxSideEffects = getFlag(options, keys, 'jsxSideEffects', mustBeBoolean);
let define = getFlag(options, keys, 'define', mustBeObject);
let logOverride = getFlag(options, keys, 'logOverride', mustBeObject);
let supported = getFlag(options, keys, 'supported', mustBeObject);
Expand Down Expand Up @@ -180,6 +181,7 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe
if (jsxFragment) flags.push(`--jsx-fragment=${jsxFragment}`);
if (jsxImportSource) flags.push(`--jsx-import-source=${jsxImportSource}`);
if (jsxDev) flags.push(`--jsx-dev`);
if (jsxSideEffects) flags.push(`--jsx-side-effects`);

if (define) {
for (let key in define) {
Expand Down
2 changes: 2 additions & 0 deletions lib/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ interface CommonOptions {
jsxImportSource?: string;
/** Documentation: https://esbuild.github.io/api/#jsx-development */
jsxDev?: boolean;
/** Documentation: https://esbuild.github.io/api/#jsx-side-effects */
jsxSideEffects?: boolean;

/** Documentation: https://esbuild.github.io/api/#define */
define?: { [key: string]: string };
Expand Down
2 changes: 2 additions & 0 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ type BuildOptions struct {
JSXFragment string // Documentation: https://esbuild.github.io/api/#jsx-fragment
JSXImportSource string // Documentation: https://esbuild.github.io/api/#jsx-import-source
JSXDev bool // Documentation: https://esbuild.github.io/api/#jsx-dev
JSXSideEffects bool // Documentation: https://esbuild.github.io/api/#jsx-side-effects

Define map[string]string // Documentation: https://esbuild.github.io/api/#define
Pure []string // Documentation: https://esbuild.github.io/api/#pure
Expand Down Expand Up @@ -403,6 +404,7 @@ type TransformOptions struct {
JSXFragment string // Documentation: https://esbuild.github.io/api/#jsx-fragment
JSXImportSource string // Documentation: https://esbuild.github.io/api/#jsx-import-source
JSXDev bool // Documentation: https://esbuild.github.io/api/#jsx-dev
JSXSideEffects bool // Documentation: https://esbuild.github.io/api/#jsx-side-effects

TsconfigRaw string // Documentation: https://esbuild.github.io/api/#tsconfig-raw
Banner string // Documentation: https://esbuild.github.io/api/#banner
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 @@ -903,6 +903,7 @@ func rebuildImpl(
Fragment: validateJSXExpr(log, buildOpts.JSXFragment, "fragment"),
Development: buildOpts.JSXDev,
ImportSource: buildOpts.JSXImportSource,
SideEffects: buildOpts.JSXSideEffects,
},
Defines: defines,
InjectedDefines: injectedDefines,
Expand Down Expand Up @@ -1365,6 +1366,7 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult
Fragment: validateJSXExpr(log, transformOpts.JSXFragment, "fragment"),
Development: transformOpts.JSXDev,
ImportSource: transformOpts.JSXImportSource,
SideEffects: transformOpts.JSXSideEffects,
}

// Settings from "tsconfig.json" override those
Expand Down
10 changes: 10 additions & 0 deletions pkg/cli/cli_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,15 @@ func parseOptionsImpl(
transformOpts.JSXDev = value
}

case isBoolFlag(arg, "--jsx-side-effects"):
if value, err := parseBoolFlag(arg, true); err != nil {
return parseOptionsExtras{}, err
} else if buildOpts != nil {
buildOpts.JSXSideEffects = value
} else {
transformOpts.JSXSideEffects = value
}

case strings.HasPrefix(arg, "--banner=") && transformOpts != nil:
transformOpts.Banner = arg[len("--banner="):]

Expand Down Expand Up @@ -754,6 +763,7 @@ func parseOptionsImpl(
"bundle": true,
"ignore-annotations": true,
"jsx-dev": true,
"jsx-side-effects": true,
"keep-names": true,
"minify-identifiers": true,
"minify-syntax": true,
Expand Down
5 changes: 5 additions & 0 deletions scripts/js-api-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -4587,6 +4587,11 @@ let transformTests = {
assert.strictEqual(code, `import { jsx } from "notreact/jsx-runtime";\nconsole.log(/* @__PURE__ */ jsx("div", {}));\n`)
},

async jsxSideEffects({ esbuild }) {
const { code } = await esbuild.transform(`<b/>`, { loader: 'jsx', jsxSideEffects: true })
assert.strictEqual(code, `React.createElement("b", null);\n`)
},

async ts({ esbuild }) {
const { code } = await esbuild.transform(`enum Foo { FOO }`, { loader: 'ts' })
assert.strictEqual(code, `var Foo = /* @__PURE__ */ ((Foo2) => {\n Foo2[Foo2["FOO"] = 0] = "FOO";\n return Foo2;\n})(Foo || {});\n`)
Expand Down

0 comments on commit d65315a

Please sign in to comment.