Skip to content
Open
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
202 changes: 183 additions & 19 deletions test/bundler/transpiler/react-compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1828,14 +1828,36 @@ describe("bundler", () => {
},
});

// Stub react packages for the three tests below. `c` counts its calls, and
// a compiled function calls it once per render. So the second number of
// each pair that `render` returns is 1 for a compiled function and 0 for a
// function that the compiler skipped.
const stubReactThatCountsCalls = {
"/node_modules/react/package.json": `{"name":"react","main":"./index.js"}`,
"/node_modules/react/jsx-runtime.js": `exports.jsx = exports.jsxs = (type, props) => ({ type, props });`,
"/node_modules/react/jsx-dev-runtime.js": `exports.jsxDEV = (type, props) => ({ type, props });`,
"/node_modules/react/compiler-runtime.js": `
let count = 0;
exports.c = size => {
count++;
return new Array(size).fill(Symbol.for("react.memo_cache_sentinel"));
};
exports.calls = () => count;
`,
};
const renderAndCountCalls = /* js */ `
function render(fn, props) {
const before = calls();
const result = fn(props);
return [result.props ?? result, calls() - before];
}
`;

// prune_non_escaping_scopes does not visit the test of a `?:`. A reactive
// scope that only the test reaches gets no node, but the local that scope
// reassigns still refers to it. Once that local has to be memoized, Babel
// raises `Invariant: Expected a node for all scopes` and skips that one
// function. The last two functions are the controls.
//
// A compiled function calls the fake `c` once per render, so the second
// number of each pair is 1 for a compiled function and 0 for a skipped one.
itBundled("react-compiler/AssignmentInConditionalTestSkipsOnlyThatFunction", {
files: {
"/entry.jsx": /* jsx */ `
Expand Down Expand Up @@ -1887,11 +1909,7 @@ describe("bundler", () => {
return <div>Hello {name}</div>;
}

function render(fn, props) {
const before = calls();
const result = fn(props);
return [result.props ?? result, calls() - before];
}
${renderAndCountCalls}
console.log(JSON.stringify({
CallInTest: render(CallInTest, { f: () => "x" }),
ArrayInTest: render(ArrayInTest, { a: 1 }),
Expand All @@ -1902,18 +1920,8 @@ describe("bundler", () => {
Plain: render(Plain, { name: "bun" }),
}));
`,
"/node_modules/react/package.json": `{"name":"react","main":"./index.js"}`,
...stubReactThatCountsCalls,
"/node_modules/react/index.js": `exports.createElement = () => null;`,
"/node_modules/react/jsx-runtime.js": `exports.jsx = exports.jsxs = (type, props) => ({ type, props });`,
"/node_modules/react/jsx-dev-runtime.js": `exports.jsxDEV = (type, props) => ({ type, props });`,
"/node_modules/react/compiler-runtime.js": `
let count = 0;
exports.c = size => {
count++;
return new Array(size).fill(Symbol.for("react.memo_cache_sentinel"));
};
exports.calls = () => count;
`,
},
reactCompiler: true,
target: "browser",
Expand All @@ -1931,6 +1939,162 @@ describe("bundler", () => {
},
});

// Nothing in these `try` bodies can throw, so PruneMaybeThrows removes the
// catch block. The phi that joins the local after the `try` keeps the operand
// of the removed block, and MergeConsecutiveBlocks has the invariant `Found a
// block with a single predecessor but where a phi has multiple operands`. It
// was an `assert_eq!` that ended the build. Babel 1.0.0 raises it and skips
// that one function. The last two functions are the controls.
itBundled("react-compiler/TryBodyThatCannotThrowSkipsOnlyThatFunction", {
files: {
"/entry.jsx": /* jsx */ `
import { useMemo } from "react";
import { calls } from "react/compiler-runtime";

function ConstantInTryBody() {
let found = "found";
try {
const unused = 1;
} catch {
found = "missing";
}
return <span>{found}</span>;
}
function LiteralsInTryBody(props) {
let ok = true;
try {
const list = [1, 2];
const object = { a: 1 };
} catch {
ok = false;
}
return <span>{ok ? props.a : "off"}</span>;
}
function ConstantInTryBodyWithCatchBinding() {
let message = "none";
try {
const unused = 1;
} catch (e) {
message = e.name;
}
return <span>{message}</span>;
}
function ConstantInTryBodyInUseMemo() {
const found = useMemo(() => {
let found = "found";
try {
const unused = 1;
} catch {
found = "missing";
}
return found;
}, []);
return <span>{found}</span>;
}
function CallInTryBody() {
let supported = true;
try {
window.localStorage.getItem("key");
} catch {
supported = false;
}
return <span>{supported ? "on" : "off"}</span>;
}
function Plain({ name }) {
return <div>Hello {name}</div>;
}

${renderAndCountCalls}
console.log(JSON.stringify({
ConstantInTryBody: render(ConstantInTryBody),
LiteralsInTryBody: render(LiteralsInTryBody, { a: "a" }),
ConstantInTryBodyWithCatchBinding: render(ConstantInTryBodyWithCatchBinding),
ConstantInTryBodyInUseMemo: render(ConstantInTryBodyInUseMemo),
CallInTryBody: render(CallInTryBody),
Plain: render(Plain, { name: "bun" }),
}));
`,
...stubReactThatCountsCalls,
"/node_modules/react/index.js": `exports.useMemo = fn => fn();`,
},
reactCompiler: true,
target: "browser",
backend: "cli",
run: {
stdout: JSON.stringify({
ConstantInTryBody: [{ children: "found" }, 0],
LiteralsInTryBody: [{ children: "a" }, 0],
ConstantInTryBodyWithCatchBinding: [{ children: "none" }, 0],
ConstantInTryBodyInUseMemo: [{ children: "found" }, 0],
CallInTryBody: [{ children: "off" }, 1],
Plain: [{ children: ["Hello ", "bun"] }, 1],
}),
},
});

// DropManualMemoization puts a StartMemoize marker after the load of
// `useMemo` / `useCallback` and a FinishMemoize marker after the call. A
// manual memo call in the arguments of another one nests the two pairs, and
// ValidateNoSetStateInRender has the invariant `Unexpected nested StartMemoize
// instructions`. It was an `assert!` that ended the build. Babel leaves each
// of these functions as written: 1.0.0 raises that invariant for the
// third-argument form and rejects a call in a dependency list one pass
// earlier. The last function is the control.
itBundled("react-compiler/NestedManualMemoCallSkipsOnlyThatFunction", {
files: {
"/entry.jsx": /* jsx */ `
import { useCallback, useMemo } from "react";
import * as React from "react";
import { calls } from "react/compiler-runtime";

function MemoInDependencyList({ a, b }) {
const x = useMemo(() => a + b, [a, useMemo(() => b * 2, [b])]);
return <div>{x}</div>;
}
function MemoInCallbackDependencyList(props) {
const f = useCallback(() => props.a, [useMemo(() => props.a, [props.a])]);
return <div>{f()}</div>;
}
function NamespaceMemoInDependencyList({ a, b }) {
const x = React.useMemo(() => a + b, [a, React.useMemo(() => b * 2, [b])]);
return <div>{x}</div>;
}
function MemoAsThirdArgument({ a, b }) {
const x = useMemo(() => a, [a], useMemo(() => b, [b]));
return <div>{x}</div>;
}
function SequentialMemos({ a, b }) {
const twice = useMemo(() => b * 2, [b]);
const x = useMemo(() => a + twice, [a, twice]);
return <div>{x}</div>;
}

${renderAndCountCalls}
console.log(JSON.stringify({
MemoInDependencyList: render(MemoInDependencyList, { a: 1, b: 2 }),
MemoInCallbackDependencyList: render(MemoInCallbackDependencyList, { a: 5 }),
NamespaceMemoInDependencyList: render(NamespaceMemoInDependencyList, { a: 1, b: 2 }),
MemoAsThirdArgument: render(MemoAsThirdArgument, { a: 1, b: 2 }),
SequentialMemos: render(SequentialMemos, { a: 1, b: 2 }),
}));
`,
...stubReactThatCountsCalls,
"/node_modules/react/index.js": `exports.useMemo = fn => fn(); exports.useCallback = fn => fn;`,
},
reactCompiler: true,
target: "browser",
backend: "cli",
run: {
stdout: JSON.stringify({
MemoInDependencyList: [{ children: 3 }, 0],
MemoInCallbackDependencyList: [{ children: 5 }, 0],
NamespaceMemoInDependencyList: [{ children: 3 }, 0],
MemoAsThirdArgument: [{ children: 1 }, 0],
SequentialMemos: [{ children: 5 }, 1],
}),
},
});

// A 0-arg call to an unknown import is non-reactive in InferReactivePlaces
// (no operand is reactive, callee isn't a hook), so its scope's deps prune
// to empty and it becomes a sentinel-only block. Babel does the same; this
Expand Down
Loading