Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JSX side effects option #2546

Merged
merged 3 commits into from Sep 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
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
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
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
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
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
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
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
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
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
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
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