diff --git a/CHANGELOG.md b/CHANGELOG.md index 844b6006c5f..5fc97414854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/cmd/esbuild/main.go b/cmd/esbuild/main.go index 976e41b760e..1208cb916d5 100644 --- a/cmd/esbuild/main.go +++ b/cmd/esbuild/main.go @@ -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 diff --git a/internal/config/config.go b/internal/config/config.go index 49e29653dd4..1a996b13eff 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -20,6 +20,7 @@ type JSXOptions struct { AutomaticRuntime bool ImportSource string Development bool + SideEffects bool } type TSJSX uint8 diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index fa3e699af2a..6dddc9e651a 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -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() @@ -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{} } } diff --git a/internal/js_parser/js_parser_test.go b/internal/js_parser/js_parser_test.go index 56424620096..06a8a92a046 100644 --- a/internal/js_parser/js_parser_test.go +++ b/internal/js_parser/js_parser_test.go @@ -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{ @@ -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) { @@ -181,6 +192,7 @@ func expectParseErrorJSXAutomatic(t *testing.T, options JSXAutomaticTestOptions, Parse: true, Development: options.Development, ImportSource: options.ImportSource, + SideEffects: options.SideEffects, }, }) } @@ -194,6 +206,7 @@ func expectPrintedJSXAutomatic(t *testing.T, options JSXAutomaticTestOptions, co Parse: true, Development: options.Development, ImportSource: options.ImportSource, + SideEffects: options.SideEffects, }, }) } @@ -4813,6 +4826,11 @@ NOTE: Both "__source" and "__self" are set automatically by esbuild when using R expectPrintedJSXAutomatic(t, pri, "
", "import { jsx } from \"my-jsx-lib/jsx-runtime\";\n/* @__PURE__ */ jsx(\"div\", {});\n") expectPrintedJSXAutomatic(t, pri, "
", "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, "", "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, "
>
", "/* @__PURE__ */ jsxDEV(\"div\", {\n children: \">\"\n}, void 0, false, {\n fileName: \"\",\n lineNumber: 1,\n columnNumber: 1\n}, this);\n") @@ -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<>", ": WARNING: The JSX fragment cannot be set when using React's \"automatic\" JSX transform\n") } +func TestJSXSideEffects(t *testing.T) { + expectPrintedJSX(t, "
", "/* @__PURE__ */ React.createElement(\"a\", null);\n") + expectPrintedJSX(t, "<>", "/* @__PURE__ */ React.createElement(React.Fragment, null);\n") + + expectPrintedJSXSideEffects(t, "", "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") diff --git a/lib/shared/common.ts b/lib/shared/common.ts index 9b4ed7f087b..a5302f400b2 100644 --- a/lib/shared/common.ts +++ b/lib/shared/common.ts @@ -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); @@ -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) { diff --git a/lib/shared/types.ts b/lib/shared/types.ts index e3d264b3474..d55b32bb672 100644 --- a/lib/shared/types.ts +++ b/lib/shared/types.ts @@ -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 }; diff --git a/pkg/api/api.go b/pkg/api/api.go index 5a7af90b3be..3aefb20e85a 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -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 @@ -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 diff --git a/pkg/api/api_impl.go b/pkg/api/api_impl.go index f2d5c30a216..451ec4fad2f 100644 --- a/pkg/api/api_impl.go +++ b/pkg/api/api_impl.go @@ -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, @@ -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 diff --git a/pkg/cli/cli_impl.go b/pkg/cli/cli_impl.go index 08064021362..09da40503af 100644 --- a/pkg/cli/cli_impl.go +++ b/pkg/cli/cli_impl.go @@ -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="):] @@ -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, diff --git a/scripts/js-api-tests.js b/scripts/js-api-tests.js index 654f08ff15f..00cdf76b8b3 100644 --- a/scripts/js-api-tests.js +++ b/scripts/js-api-tests.js @@ -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(``, { 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`)