Skip to content

Commit d65315a

Browse files
authored
Add JSX side effects option (#2546)
1 parent a6acbaa commit d65315a

File tree

11 files changed

+59
-2
lines changed

11 files changed

+59
-2
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@
9999
100100
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.
101101
102+
* Add the `--jsx-side-effects` API option ([#2539](https://github.com/evanw/esbuild/issues/2539), [#2546](https://github.com/evanw/esbuild/pull/2546))
103+
104+
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.
105+
106+
This feature was contributed by [@rtsao](https://github.com/rtsao).
107+
102108
## 0.15.7
103109
104110
* 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))

cmd/esbuild/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ var helpText = func(colors logger.Colors) string {
8282
--jsx-fragment=... What to use for JSX instead of React.Fragment
8383
--jsx-import-source=... Override the package name for the automatic runtime
8484
(default "react")
85+
--jsx-side-effects Do not remove unused JSX expressions
8586
--jsx=... Set to "automatic" to use React's automatic runtime
8687
or to "preserve" to disable transforming JSX to JS
8788
--keep-names Preserve "name" on functions and classes

internal/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type JSXOptions struct {
2020
AutomaticRuntime bool
2121
ImportSource string
2222
Development bool
23+
SideEffects bool
2324
}
2425

2526
type TSJSX uint8

internal/js_parser/js_parser.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12401,7 +12401,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
1240112401
CloseParenLoc: e.CloseLoc,
1240212402

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

1253112531
// Enable tree shaking
12532-
CanBeUnwrappedIfUnused: !p.options.ignoreDCEAnnotations,
12532+
CanBeUnwrappedIfUnused: !p.options.ignoreDCEAnnotations && !p.options.jsx.SideEffects,
1253312533
}}, exprOut{}
1253412534
}
1253512535
}

internal/js_parser/js_parser_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,16 @@ func expectPrintedJSX(t *testing.T, contents string, expected string) {
156156
})
157157
}
158158

159+
func expectPrintedJSXSideEffects(t *testing.T, contents string, expected string) {
160+
t.Helper()
161+
expectPrintedCommon(t, contents, expected, config.Options{
162+
JSX: config.JSXOptions{
163+
Parse: true,
164+
SideEffects: true,
165+
},
166+
})
167+
}
168+
159169
func expectPrintedMangleJSX(t *testing.T, contents string, expected string) {
160170
t.Helper()
161171
expectPrintedCommon(t, contents, expected, config.Options{
@@ -170,6 +180,7 @@ type JSXAutomaticTestOptions struct {
170180
Development bool
171181
ImportSource string
172182
OmitJSXRuntimeForTests bool
183+
SideEffects bool
173184
}
174185

175186
func expectParseErrorJSXAutomatic(t *testing.T, options JSXAutomaticTestOptions, contents string, expected string) {
@@ -181,6 +192,7 @@ func expectParseErrorJSXAutomatic(t *testing.T, options JSXAutomaticTestOptions,
181192
Parse: true,
182193
Development: options.Development,
183194
ImportSource: options.ImportSource,
195+
SideEffects: options.SideEffects,
184196
},
185197
})
186198
}
@@ -194,6 +206,7 @@ func expectPrintedJSXAutomatic(t *testing.T, options JSXAutomaticTestOptions, co
194206
Parse: true,
195207
Development: options.Development,
196208
ImportSource: options.ImportSource,
209+
SideEffects: options.SideEffects,
197210
},
198211
})
199212
}
@@ -4813,6 +4826,11 @@ NOTE: Both "__source" and "__self" are set automatically by esbuild when using R
48134826
expectPrintedJSXAutomatic(t, pri, "<div/>", "import { jsx } from \"my-jsx-lib/jsx-runtime\";\n/* @__PURE__ */ jsx(\"div\", {});\n")
48144827
expectPrintedJSXAutomatic(t, pri, "<div {...props} key=\"key\" />", "import { createElement } from \"my-jsx-lib\";\n/* @__PURE__ */ createElement(\"div\", {\n ...props,\n key: \"key\"\n});\n")
48154828

4829+
// Impure JSX call expressions
4830+
pi := JSXAutomaticTestOptions{SideEffects: true, ImportSource: "my-jsx-lib"}
4831+
expectPrintedJSXAutomatic(t, pi, "<a/>", "import { jsx } from \"my-jsx-lib/jsx-runtime\";\njsx(\"a\", {});\n")
4832+
expectPrintedJSXAutomatic(t, pi, "<></>", "import { Fragment, jsx } from \"my-jsx-lib/jsx-runtime\";\njsx(Fragment, {});\n")
4833+
48164834
// Dev, without runtime imports
48174835
d := JSXAutomaticTestOptions{Development: true, OmitJSXRuntimeForTests: true}
48184836
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")
@@ -4930,6 +4948,14 @@ NOTE: You can enable React's "automatic" JSX transform for this file by using a
49304948
expectParseErrorJSX(t, "// @jsxRuntime automatic @jsxFrag f\n<></>", "<stdin>: WARNING: The JSX fragment cannot be set when using React's \"automatic\" JSX transform\n")
49314949
}
49324950

4951+
func TestJSXSideEffects(t *testing.T) {
4952+
expectPrintedJSX(t, "<a/>", "/* @__PURE__ */ React.createElement(\"a\", null);\n")
4953+
expectPrintedJSX(t, "<></>", "/* @__PURE__ */ React.createElement(React.Fragment, null);\n")
4954+
4955+
expectPrintedJSXSideEffects(t, "<a/>", "React.createElement(\"a\", null);\n")
4956+
expectPrintedJSXSideEffects(t, "<></>", "React.createElement(React.Fragment, null);\n")
4957+
}
4958+
49334959
func TestPreserveOptionalChainParentheses(t *testing.T) {
49344960
expectPrinted(t, "a?.b.c", "a?.b.c;\n")
49354961
expectPrinted(t, "(a?.b).c", "(a?.b).c;\n")

lib/shared/common.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe
145145
let jsxFragment = getFlag(options, keys, 'jsxFragment', mustBeString);
146146
let jsxImportSource = getFlag(options, keys, 'jsxImportSource', mustBeString);
147147
let jsxDev = getFlag(options, keys, 'jsxDev', mustBeBoolean);
148+
let jsxSideEffects = getFlag(options, keys, 'jsxSideEffects', mustBeBoolean);
148149
let define = getFlag(options, keys, 'define', mustBeObject);
149150
let logOverride = getFlag(options, keys, 'logOverride', mustBeObject);
150151
let supported = getFlag(options, keys, 'supported', mustBeObject);
@@ -180,6 +181,7 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe
180181
if (jsxFragment) flags.push(`--jsx-fragment=${jsxFragment}`);
181182
if (jsxImportSource) flags.push(`--jsx-import-source=${jsxImportSource}`);
182183
if (jsxDev) flags.push(`--jsx-dev`);
184+
if (jsxSideEffects) flags.push(`--jsx-side-effects`);
183185

184186
if (define) {
185187
for (let key in define) {

lib/shared/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ interface CommonOptions {
6161
jsxImportSource?: string;
6262
/** Documentation: https://esbuild.github.io/api/#jsx-development */
6363
jsxDev?: boolean;
64+
/** Documentation: https://esbuild.github.io/api/#jsx-side-effects */
65+
jsxSideEffects?: boolean;
6466

6567
/** Documentation: https://esbuild.github.io/api/#define */
6668
define?: { [key: string]: string };

pkg/api/api.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ type BuildOptions struct {
281281
JSXFragment string // Documentation: https://esbuild.github.io/api/#jsx-fragment
282282
JSXImportSource string // Documentation: https://esbuild.github.io/api/#jsx-import-source
283283
JSXDev bool // Documentation: https://esbuild.github.io/api/#jsx-dev
284+
JSXSideEffects bool // Documentation: https://esbuild.github.io/api/#jsx-side-effects
284285

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

407409
TsconfigRaw string // Documentation: https://esbuild.github.io/api/#tsconfig-raw
408410
Banner string // Documentation: https://esbuild.github.io/api/#banner

pkg/api/api_impl.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,7 @@ func rebuildImpl(
903903
Fragment: validateJSXExpr(log, buildOpts.JSXFragment, "fragment"),
904904
Development: buildOpts.JSXDev,
905905
ImportSource: buildOpts.JSXImportSource,
906+
SideEffects: buildOpts.JSXSideEffects,
906907
},
907908
Defines: defines,
908909
InjectedDefines: injectedDefines,
@@ -1365,6 +1366,7 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult
13651366
Fragment: validateJSXExpr(log, transformOpts.JSXFragment, "fragment"),
13661367
Development: transformOpts.JSXDev,
13671368
ImportSource: transformOpts.JSXImportSource,
1369+
SideEffects: transformOpts.JSXSideEffects,
13681370
}
13691371

13701372
// Settings from "tsconfig.json" override those

pkg/cli/cli_impl.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,15 @@ func parseOptionsImpl(
657657
transformOpts.JSXDev = value
658658
}
659659

660+
case isBoolFlag(arg, "--jsx-side-effects"):
661+
if value, err := parseBoolFlag(arg, true); err != nil {
662+
return parseOptionsExtras{}, err
663+
} else if buildOpts != nil {
664+
buildOpts.JSXSideEffects = value
665+
} else {
666+
transformOpts.JSXSideEffects = value
667+
}
668+
660669
case strings.HasPrefix(arg, "--banner=") && transformOpts != nil:
661670
transformOpts.Banner = arg[len("--banner="):]
662671

@@ -754,6 +763,7 @@ func parseOptionsImpl(
754763
"bundle": true,
755764
"ignore-annotations": true,
756765
"jsx-dev": true,
766+
"jsx-side-effects": true,
757767
"keep-names": true,
758768
"minify-identifiers": true,
759769
"minify-syntax": true,

scripts/js-api-tests.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4587,6 +4587,11 @@ let transformTests = {
45874587
assert.strictEqual(code, `import { jsx } from "notreact/jsx-runtime";\nconsole.log(/* @__PURE__ */ jsx("div", {}));\n`)
45884588
},
45894589

4590+
async jsxSideEffects({ esbuild }) {
4591+
const { code } = await esbuild.transform(`<b/>`, { loader: 'jsx', jsxSideEffects: true })
4592+
assert.strictEqual(code, `React.createElement("b", null);\n`)
4593+
},
4594+
45904595
async ts({ esbuild }) {
45914596
const { code } = await esbuild.transform(`enum Foo { FOO }`, { loader: 'ts' })
45924597
assert.strictEqual(code, `var Foo = /* @__PURE__ */ ((Foo2) => {\n Foo2[Foo2["FOO"] = 0] = "FOO";\n return Foo2;\n})(Foo || {});\n`)

0 commit comments

Comments
 (0)