Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/react_compiler/lowering/build_hir/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down
102 changes: 88 additions & 14 deletions test/bundler/transpiler/react-compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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",
Expand All @@ -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 <><i>{props.x}</i><b>{props.y}</b></>;
}
const jsx = props.a;
const jsxs = props.b;
const jsxDEV = props.c;
const Fragment = props.d;
return <div title={jsx + jsxs + jsxDEV + Fragment}>{list()}</div>;
}
console.log(JSON.stringify(Comp({ a: "A", b: "B", c: "C", d: "D", x: "X", y: "Y" })));
`;
for (const development of [true, false]) {
const mode = development ? "Dev" : "Prod";
const [jsx, jsxs] = development ? ["jsxDEV", "jsxDEV"] : ["jsx", "jsxs"];
const element = (by: string, type: string, props: object) => ({ $: by, type, props });
const options: Partial<BundlerTestInput> = {
reactCompiler: true,
target: "browser",
backend: "api",
jsx: { development },
run: {
stdout: JSON.stringify(
element(jsx, "div", {
title: "ABCD",
children: element(jsxs, "Fragment", {
children: [element(jsx, "i", { children: "X" }), element(jsx, "b", { children: "Y" })],
}),
}),
),
},
};

itBundled(`react-compiler/JsxRuntimeImportIsNotContextOfANestedFunction-${mode}`, {
...options,
files: { "/entry.jsx": localsNamedLikeJsxRuntimeImports, ...namedJsxRuntime },
onAfterBundle(api) {
const out = api.readFile("/out.js");
// `Comp` is compiled: it reads its memo cache.
expect(out).toMatch(/\$\[\d+\]/);
// The imports print as `$jsx`, so the bundler does not number a local.
// A suffix here can only be the `_0` of the compiler.
const locals = Array.from(out.matchAll(/(\w+) = props\.([abcd])\b/g), m => [m[2], m[1]]);
expect(Object.fromEntries(locals)).toEqual({ a: "jsx", b: "jsxs", c: "jsxDEV", d: "Fragment" });
},
});

// Here the imports print under their own names, and the locals keep theirs
// in the compiler. The bundler has to number the locals, or `list` calls one.
itBundled(`react-compiler/JsxRuntimeImportIsNotContextOfANestedFunction-${mode}-EsmRuntime`, {
...options,
files: { "/entry.jsx": localsNamedLikeJsxRuntimeImports, ...namedJsxRuntimeEsm },
});
}

// Regression: codegen.rs PropertyDelete/ComputedDelete/UnaryExpression emitted
// `E::Unary` with `UnaryFlags::empty()`. The parser sets
// `WAS_ORIGINALLY_DELETE_OF_IDENTIFIER_OR_PROPERTY_ACCESS` for `delete <dot|index>`;
Expand Down
Loading