Skip to content

Commit 6c4aa2c

Browse files
committed
fix #3454: crash with jsx-dev before super() call
1 parent 4a1e576 commit 6c4aa2c

5 files changed

Lines changed: 340 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,33 @@
6060
}
6161
```
6262

63+
* Avoid referencing `this` from JSX elements in derived class constructors ([#3454](https://github.com/evanw/esbuild/issues/3454))
64+
65+
When you enable `--jsx=automatic` and `--jsx-dev`, the JSX transform is supposed to insert `this` as the last argument to the `jsxDEV` function. I'm not sure exactly why this is and I can't find any specification for it, but in any case this causes the generated code to crash when you use a JSX element in a derived class constructor before the call to `super()` as `this` is not allowed to be accessed at that point. For example
66+
67+
```js
68+
// Original code
69+
class ChildComponent extends ParentComponent {
70+
constructor() {
71+
super(<div />)
72+
}
73+
}
74+
75+
// Problematic output (with --loader=jsx --jsx=automatic --jsx-dev)
76+
import { jsxDEV } from "react/jsx-dev-runtime";
77+
class ChildComponent extends ParentComponent {
78+
constructor() {
79+
super(/* @__PURE__ */ jsxDEV("div", {}, void 0, false, {
80+
fileName: "<stdin>",
81+
lineNumber: 3,
82+
columnNumber: 15
83+
}, this)); // The reference to "this" crashes here
84+
}
85+
}
86+
```
87+
88+
The TypeScript compiler doesn't handle this at all while the Babel compiler just omits `this` for the entire constructor (even after the call to `super()`). There seems to be no specification so I can't be sure that this change doesn't break anything important. But given that Babel is pretty loose with this and TypeScript doesn't handle this at all, I'm guessing this value isn't too important. React's blog post seems to indicate that this value was intended to be used for a React-specific migration warning at some point, so it could even be that this value is irrelevant now. Anyway the crash in this case should now be fixed.
89+
6390
* Allow package subpath imports to map to node built-ins ([#3485](https://github.com/evanw/esbuild/issues/3485))
6491
6592
You are now able to use a [subpath import](https://nodejs.org/api/packages.html#subpath-imports) in your package to resolve to a node built-in module. For example, with a `package.json` file like this:

internal/bundler_tests/bundler_default_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8534,3 +8534,57 @@ func TestDecoratorPrintingCJS(t *testing.T) {
85348534
`,
85358535
})
85368536
}
8537+
8538+
// React's development-mode transform has a special "__self" value that's sort
8539+
// of supposed to be set to "this". Except there's no specification for it
8540+
// AFAIK and the value of "this" isn't always allowed to be accessed. For
8541+
// example, accessing it before "super()" in a constructor call will crash.
8542+
//
8543+
// From what I understand the React team wanted to have it in case they need it
8544+
// for some run-time warnings, but having it be accurate in all cases doesn't
8545+
// really matter. For example, I'm not sure if it needs to even be any value in
8546+
// particular for top-level JSX elements (top-level "this" can technically be
8547+
// the module's exports object, which could materialize a lot of code to
8548+
// generate one when bundling, so Facebook probably doesn't want that to
8549+
// happen?).
8550+
//
8551+
// Anyway, this test case documents what esbuild does in case a specification
8552+
// is produced in the future and it turns out esbuild should be doing something
8553+
// else.
8554+
func TestJSXDevSelfEdgeCases(t *testing.T) {
8555+
default_suite.expectBundled(t, bundled{
8556+
files: map[string]string{
8557+
"/function-this.jsx": `export function Foo() { return <div/> }`,
8558+
"/class-this.jsx": `export class Foo { foo() { return <div/> } }`,
8559+
"/normal-constructor.jsx": `export class Foo { constructor() { this.foo = <div/> } }`,
8560+
"/derived-constructor.jsx": `export class Foo extends Object { constructor() { super(<div/>); this.foo = <div/> } }`,
8561+
"/normal-constructor-arg.jsx": `export class Foo { constructor(foo = <div/>) {} }`,
8562+
"/derived-constructor-arg.jsx": `export class Foo extends Object { constructor(foo = <div/>) { super() } }`,
8563+
"/normal-constructor-field.tsx": `export class Foo { foo = <div/> }`,
8564+
"/derived-constructor-field.tsx": `export class Foo extends Object { foo = <div/> }`,
8565+
"/static-field.jsx": `export class Foo { static foo = <div/> }`,
8566+
"/top-level-this-esm.jsx": `export let foo = <div/>; if (Foo) { foo = <Foo>nested top-level this</Foo> }`,
8567+
"/top-level-this-cjs.jsx": `exports.foo = <div/>`,
8568+
"/typescript-namespace.tsx": `export namespace Foo { export let foo = <div/> }`,
8569+
"/typescript-enum.tsx": `export enum Foo { foo = <div/> }`,
8570+
"/tsconfig.json": `{ "compilerOptions": { "useDefineForClassFields": false } }`,
8571+
},
8572+
entryPaths: []string{"*"},
8573+
options: config.Options{
8574+
Mode: config.ModeBundle,
8575+
AbsOutputDir: "/out",
8576+
JSX: config.JSXOptions{
8577+
AutomaticRuntime: true,
8578+
Development: true,
8579+
},
8580+
UnsupportedJSFeatures: compat.ClassStaticField,
8581+
ExternalSettings: config.ExternalSettings{
8582+
PreResolve: config.ExternalMatchers{
8583+
Exact: map[string]bool{
8584+
"react/jsx-dev-runtime": true,
8585+
},
8586+
},
8587+
},
8588+
},
8589+
})
8590+
}

internal/bundler_tests/snapshots/snapshots_default.txt

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2287,6 +2287,226 @@ console.log(/* @__PURE__ */ React.createElement("[", null));
22872287
// string-template.jsx
22882288
console.log(/* @__PURE__ */ React.createElement("]", null));
22892289

2290+
================================================================================
2291+
TestJSXDevSelfEdgeCases
2292+
---------- /out/class-this.js ----------
2293+
// class-this.jsx
2294+
import { jsxDEV } from "react/jsx-dev-runtime";
2295+
var Foo = class {
2296+
foo() {
2297+
return /* @__PURE__ */ jsxDEV("div", {}, void 0, false, {
2298+
fileName: "class-this.jsx",
2299+
lineNumber: 1,
2300+
columnNumber: 35
2301+
}, this);
2302+
}
2303+
};
2304+
export {
2305+
Foo
2306+
};
2307+
2308+
---------- /out/derived-constructor-arg.js ----------
2309+
// derived-constructor-arg.jsx
2310+
import { jsxDEV } from "react/jsx-dev-runtime";
2311+
var Foo = class extends Object {
2312+
constructor(foo = /* @__PURE__ */ jsxDEV("div", {}, void 0, false, {
2313+
fileName: "derived-constructor-arg.jsx",
2314+
lineNumber: 1,
2315+
columnNumber: 53
2316+
})) {
2317+
super();
2318+
}
2319+
};
2320+
export {
2321+
Foo
2322+
};
2323+
2324+
---------- /out/derived-constructor-field.js ----------
2325+
// derived-constructor-field.tsx
2326+
import { jsxDEV } from "react/jsx-dev-runtime";
2327+
var Foo = class extends Object {
2328+
constructor() {
2329+
super(...arguments);
2330+
this.foo = /* @__PURE__ */ jsxDEV("div", {}, void 0, false, {
2331+
fileName: "derived-constructor-field.tsx",
2332+
lineNumber: 1,
2333+
columnNumber: 41
2334+
}, this);
2335+
}
2336+
};
2337+
export {
2338+
Foo
2339+
};
2340+
2341+
---------- /out/derived-constructor.js ----------
2342+
// derived-constructor.jsx
2343+
import { jsxDEV } from "react/jsx-dev-runtime";
2344+
var Foo = class extends Object {
2345+
constructor() {
2346+
super(/* @__PURE__ */ jsxDEV("div", {}, void 0, false, {
2347+
fileName: "derived-constructor.jsx",
2348+
lineNumber: 1,
2349+
columnNumber: 57
2350+
}));
2351+
this.foo = /* @__PURE__ */ jsxDEV("div", {}, void 0, false, {
2352+
fileName: "derived-constructor.jsx",
2353+
lineNumber: 1,
2354+
columnNumber: 77
2355+
});
2356+
}
2357+
};
2358+
export {
2359+
Foo
2360+
};
2361+
2362+
---------- /out/function-this.js ----------
2363+
// function-this.jsx
2364+
import { jsxDEV } from "react/jsx-dev-runtime";
2365+
function Foo() {
2366+
return /* @__PURE__ */ jsxDEV("div", {}, void 0, false, {
2367+
fileName: "function-this.jsx",
2368+
lineNumber: 1,
2369+
columnNumber: 32
2370+
}, this);
2371+
}
2372+
export {
2373+
Foo
2374+
};
2375+
2376+
---------- /out/normal-constructor-arg.js ----------
2377+
// normal-constructor-arg.jsx
2378+
import { jsxDEV } from "react/jsx-dev-runtime";
2379+
var Foo = class {
2380+
constructor(foo = /* @__PURE__ */ jsxDEV("div", {}, void 0, false, {
2381+
fileName: "normal-constructor-arg.jsx",
2382+
lineNumber: 1,
2383+
columnNumber: 38
2384+
}, this)) {
2385+
}
2386+
};
2387+
export {
2388+
Foo
2389+
};
2390+
2391+
---------- /out/normal-constructor-field.js ----------
2392+
// normal-constructor-field.tsx
2393+
import { jsxDEV } from "react/jsx-dev-runtime";
2394+
var Foo = class {
2395+
constructor() {
2396+
this.foo = /* @__PURE__ */ jsxDEV("div", {}, void 0, false, {
2397+
fileName: "normal-constructor-field.tsx",
2398+
lineNumber: 1,
2399+
columnNumber: 26
2400+
}, this);
2401+
}
2402+
};
2403+
export {
2404+
Foo
2405+
};
2406+
2407+
---------- /out/normal-constructor.js ----------
2408+
// normal-constructor.jsx
2409+
import { jsxDEV } from "react/jsx-dev-runtime";
2410+
var Foo = class {
2411+
constructor() {
2412+
this.foo = /* @__PURE__ */ jsxDEV("div", {}, void 0, false, {
2413+
fileName: "normal-constructor.jsx",
2414+
lineNumber: 1,
2415+
columnNumber: 47
2416+
}, this);
2417+
}
2418+
};
2419+
export {
2420+
Foo
2421+
};
2422+
2423+
---------- /out/static-field.js ----------
2424+
// static-field.jsx
2425+
import { jsxDEV } from "react/jsx-dev-runtime";
2426+
var _Foo = class _Foo {
2427+
};
2428+
__publicField(_Foo, "foo", /* @__PURE__ */ jsxDEV("div", {}, void 0, false, {
2429+
fileName: "static-field.jsx",
2430+
lineNumber: 1,
2431+
columnNumber: 33
2432+
}, _Foo));
2433+
var Foo = _Foo;
2434+
export {
2435+
Foo
2436+
};
2437+
2438+
---------- /out/top-level-this-cjs.js ----------
2439+
// top-level-this-cjs.jsx
2440+
import { jsxDEV } from "react/jsx-dev-runtime";
2441+
var require_top_level_this_cjs = __commonJS({
2442+
"top-level-this-cjs.jsx"(exports) {
2443+
exports.foo = /* @__PURE__ */ jsxDEV("div", {}, void 0, false, {
2444+
fileName: "top-level-this-cjs.jsx",
2445+
lineNumber: 1,
2446+
columnNumber: 15
2447+
});
2448+
}
2449+
});
2450+
export default require_top_level_this_cjs();
2451+
2452+
---------- /out/top-level-this-esm.js ----------
2453+
// top-level-this-esm.jsx
2454+
import { jsxDEV } from "react/jsx-dev-runtime";
2455+
var foo = /* @__PURE__ */ jsxDEV("div", {}, void 0, false, {
2456+
fileName: "top-level-this-esm.jsx",
2457+
lineNumber: 1,
2458+
columnNumber: 18
2459+
});
2460+
if (Foo) {
2461+
foo = /* @__PURE__ */ jsxDEV(Foo, { children: "nested top-level this" }, void 0, false, {
2462+
fileName: "top-level-this-esm.jsx",
2463+
lineNumber: 1,
2464+
columnNumber: 43
2465+
});
2466+
}
2467+
export {
2468+
foo
2469+
};
2470+
2471+
---------- /out/tsconfig.js ----------
2472+
// tsconfig.json
2473+
var compilerOptions = { useDefineForClassFields: false };
2474+
var tsconfig_default = { compilerOptions };
2475+
export {
2476+
compilerOptions,
2477+
tsconfig_default as default
2478+
};
2479+
2480+
---------- /out/typescript-enum.js ----------
2481+
// typescript-enum.tsx
2482+
import { jsxDEV } from "react/jsx-dev-runtime";
2483+
var Foo = /* @__PURE__ */ ((Foo2) => {
2484+
Foo2[Foo2["foo"] = /* @__PURE__ */ jsxDEV("div", {}, void 0, false, {
2485+
fileName: "typescript-enum.tsx",
2486+
lineNumber: 1,
2487+
columnNumber: 25
2488+
})] = "foo";
2489+
return Foo2;
2490+
})(Foo || {});
2491+
export {
2492+
Foo
2493+
};
2494+
2495+
---------- /out/typescript-namespace.js ----------
2496+
// typescript-namespace.tsx
2497+
import { jsxDEV } from "react/jsx-dev-runtime";
2498+
var Foo;
2499+
((Foo2) => {
2500+
Foo2.foo = /* @__PURE__ */ jsxDEV("div", {}, void 0, false, {
2501+
fileName: "typescript-namespace.tsx",
2502+
lineNumber: 1,
2503+
columnNumber: 41
2504+
});
2505+
})(Foo || (Foo = {}));
2506+
export {
2507+
Foo
2508+
};
2509+
22902510
================================================================================
22912511
TestJSXImportMetaProperty
22922512
---------- /out/factory.js ----------

internal/bundler_tests/snapshots/snapshots_tsconfig.txt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -664,17 +664,17 @@ console.log(/* @__PURE__ */ jsxDEV(Fragment, { children: [
664664
fileName: "Users/user/project/entry.tsx",
665665
lineNumber: 2,
666666
columnNumber: 19
667-
}, this),
667+
}),
668668
/* @__PURE__ */ jsxDEV("div", {}, void 0, false, {
669669
fileName: "Users/user/project/entry.tsx",
670670
lineNumber: 2,
671671
columnNumber: 25
672-
}, this)
672+
})
673673
] }, void 0, true, {
674674
fileName: "Users/user/project/entry.tsx",
675675
lineNumber: 2,
676676
columnNumber: 17
677-
}, this));
677+
}));
678678

679679
================================================================================
680680
TestTsconfigReactJSXWithDevInMainConfig
@@ -686,17 +686,17 @@ console.log(/* @__PURE__ */ jsxDEV(Fragment, { children: [
686686
fileName: "Users/user/project/entry.tsx",
687687
lineNumber: 2,
688688
columnNumber: 19
689-
}, this),
689+
}),
690690
/* @__PURE__ */ jsxDEV("div", {}, void 0, false, {
691691
fileName: "Users/user/project/entry.tsx",
692692
lineNumber: 2,
693693
columnNumber: 25
694-
}, this)
694+
})
695695
] }, void 0, true, {
696696
fileName: "Users/user/project/entry.tsx",
697697
lineNumber: 2,
698698
columnNumber: 17
699-
}, this));
699+
}));
700700

701701
================================================================================
702702
TestTsconfigRemoveUnusedImports

0 commit comments

Comments
 (0)