diff --git a/src/react_compiler/lowering/build_hir/mod.rs b/src/react_compiler/lowering/build_hir/mod.rs index 9226a7e551be..536a3ac1c53d 100644 --- a/src/react_compiler/lowering/build_hir/mod.rs +++ b/src/react_compiler/lowering/build_hir/mod.rs @@ -432,6 +432,11 @@ pub(super) fn gather_captured_context<'h>( continue; } } + // Module-level too, but not in `members` (the JSX runtime imports): + // `resolve_identifier` loads these as globals. + if module_scope.generated.contains(&ref_) { + continue; + } let pos = ref_loc.start; let loc = convert_loc(ref_loc); captured @@ -447,8 +452,11 @@ pub(super) fn gather_captured_context<'h>( // Sort captured entries by source position so context declarations appear // in source order, matching the TS compiler's position-ordered traversal. + // Nodes that the visit pass generates can share a position (React Fast + // Refresh lists hooks at `Loc::EMPTY`: `() => [useA, useB]`), and `captured` + // iterates in a random order, so the symbol index makes the order total. let mut sorted: Vec<_> = captured.into_iter().collect(); - sorted.sort_unstable_by_key(|(_, (pos, _))| *pos); + sorted.sort_unstable_by_key(|(ref_, (pos, _))| (*pos, ref_.inner_index())); sorted .into_iter() diff --git a/test/bundler/transpiler/react-compiler.test.ts b/test/bundler/transpiler/react-compiler.test.ts index a23e6050b820..16f51a924154 100644 --- a/test/bundler/transpiler/react-compiler.test.ts +++ b/test/bundler/transpiler/react-compiler.test.ts @@ -663,6 +663,24 @@ describe("bundler", () => { }); } + // A JSX runtime whose elements say which export made them, and with which type. + const namedJsxRuntime = { + "/node_modules/react/compiler-runtime.js": ` + exports.c = function (n) { return new Array(n).fill(Symbol.for("react.memo_cache_sentinel")); }; + `, + "/node_modules/react/index.js": `exports.createElement = () => null;`, + "/node_modules/react/jsx-runtime.js": ` + exports.jsx = (type, props) => ({ $: "jsx", type: typeof type === "symbol" ? "Fragment" : type, props }); + exports.jsxs = (type, props) => ({ $: "jsxs", type: typeof type === "symbol" ? "Fragment" : type, props }); + exports.Fragment = Symbol.for("fragment"); + `, + "/node_modules/react/jsx-dev-runtime.js": ` + exports.jsxDEV = (type, props) => ({ $: "jsxDEV", type: typeof type === "symbol" ? "Fragment" : type, props }); + exports.Fragment = Symbol.for("fragment"); + `, + "/node_modules/react/package.json": `{"name":"react","main":"./index.js"}`, + }; + // A user's local `jsx` / `jsxs` / `jsxDEV` / `Fragment` binding in the // component body scope must not capture the automatic JSX runtime import // when the React Compiler rewrites the component. @@ -678,20 +696,7 @@ describe("bundler", () => { } console.log(JSON.stringify(Comp({ a: "A", b: "B" }))) `, - "/node_modules/react/compiler-runtime.js": ` - exports.c = function (n) { return new Array(n).fill(Symbol.for("react.memo_cache_sentinel")); }; - `, - "/node_modules/react/index.js": `exports.createElement = () => null;`, - "/node_modules/react/jsx-runtime.js": ` - exports.jsx = (type, props) => ({ $: "jsx", type: typeof type === "symbol" ? "Fragment" : type, props }); - exports.jsxs = (type, props) => ({ $: "jsxs", type: typeof type === "symbol" ? "Fragment" : type, props }); - exports.Fragment = Symbol.for("fragment"); - `, - "/node_modules/react/jsx-dev-runtime.js": ` - exports.jsxDEV = (type, props) => ({ $: "jsxDEV", type: typeof type === "symbol" ? "Fragment" : type, props }); - exports.Fragment = Symbol.for("fragment"); - `, - "/node_modules/react/package.json": `{"name":"react","main":"./index.js"}`, + ...namedJsxRuntime, }, reactCompiler: true, target: "browser", @@ -702,6 +707,75 @@ describe("bundler", () => { }, }); + // The same runtime as ES modules. The bundler prints a CommonJS export as + // `$jsx` and an ES module export under its own name, `jsx`. + const namedJsxRuntimeEsm = Object.fromEntries( + Object.entries(namedJsxRuntime).map(([path, text]) => [ + path, + text.replace(/exports\.(\w+) =/g, "export const $1 ="), + ]), + ); + + // A JSX runtime import is module-level: the visit pass declares it on + // `module_scope.generated`. It is not context of `list`, so it does not + // reserve its name in `Comp`. The locals come after `list` on purpose: a + // name that the context of `list` reserves gives `_0` to a later local only. + const localsNamedLikeJsxRuntimeImports = /* jsx */ ` + export function Comp(props) { + function list() { + return <>{props.x}{props.y}>; + } + const jsx = props.a; + const jsxs = props.b; + const jsxDEV = props.c; + const Fragment = props.d; + return