diff --git a/test/bundler/transpiler/react-compiler.test.ts b/test/bundler/transpiler/react-compiler.test.ts
index 85ee0a405ef6..bf4f2ce72581 100644
--- a/test/bundler/transpiler/react-compiler.test.ts
+++ b/test/bundler/transpiler/react-compiler.test.ts
@@ -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 */ `
@@ -1887,11 +1909,7 @@ describe("bundler", () => {
return
Hello {name}
;
}
- 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 }),
@@ -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",
@@ -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 {found};
+ }
+ function LiteralsInTryBody(props) {
+ let ok = true;
+ try {
+ const list = [1, 2];
+ const object = { a: 1 };
+ } catch {
+ ok = false;
+ }
+ return {ok ? props.a : "off"};
+ }
+ function ConstantInTryBodyWithCatchBinding() {
+ let message = "none";
+ try {
+ const unused = 1;
+ } catch (e) {
+ message = e.name;
+ }
+ return {message};
+ }
+ function ConstantInTryBodyInUseMemo() {
+ const found = useMemo(() => {
+ let found = "found";
+ try {
+ const unused = 1;
+ } catch {
+ found = "missing";
+ }
+ return found;
+ }, []);
+ return {found};
+ }
+ function CallInTryBody() {
+ let supported = true;
+ try {
+ window.localStorage.getItem("key");
+ } catch {
+ supported = false;
+ }
+ return {supported ? "on" : "off"};
+ }
+ function Plain({ name }) {
+ return Hello {name}
;
+ }
+
+ ${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 {x}
;
+ }
+ function MemoInCallbackDependencyList(props) {
+ const f = useCallback(() => props.a, [useMemo(() => props.a, [props.a])]);
+ return {f()}
;
+ }
+ function NamespaceMemoInDependencyList({ a, b }) {
+ const x = React.useMemo(() => a + b, [a, React.useMemo(() => b * 2, [b])]);
+ return {x}
;
+ }
+ function MemoAsThirdArgument({ a, b }) {
+ const x = useMemo(() => a, [a], useMemo(() => b, [b]));
+ return {x}
;
+ }
+ function SequentialMemos({ a, b }) {
+ const twice = useMemo(() => b * 2, [b]);
+ const x = useMemo(() => a + twice, [a, twice]);
+ return {x}
;
+ }
+
+ ${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