@samitouri / QOS-React-1 / commits / cd0c4879a2

[compiler] Fix `for` loops in try/catch (#35686)

This is a combination of a) a subagent for investigating compiler errors and b) testing that agent by fixing bugs with for loops within try/catch. My recent diffs to support maybe-throw within value blocks was incomplete and handled many cases, like optionals/logicals/etc within try/catch. However, the handling for for loops was making more assumptions and needed additional fixes. Key changes: * `maybe-throw` terminal `handler` is now nullable. PruneMaybeThrows nulls the handler for blocks that cannot throw, rather than changing to a `goto`. This preserves more information, and makes it easier for BuildReactiveFunction's visitValueBlock() to reconstruct the value blocks * Updates BuildReactiveFunction's handling of `for` init/test/update (and similar for `for..of` and `for..in`) to correctly extract value blocks. The previous logic made assumptions about the shape of the SequenceExpression which were incorrect in some cases within try/catch. The new helper extracts a flattened SequenceExpression. Supporting changes: * The agent itself (tested via this diff) * Updated the script for invoking snap to keep `compiler/` as the working directory, allowing relative paths to work more easily * Add an `--update` (`-u`) flag to `yarn snap minimize`, which updates the fixture in place w the minimized version

Joseph Savona committed Feb 3, 2026 at 18:04 UTC cd0c4879a2959db91f9bd51a09dafefedd95fb17
24 files changed +960 -370
compiler/.claude/agents/investigate-error.md new
+113
@@ -0,0 +1,113 @@
1 +---
2 +name: investigate-error
3 +description: Investigates React compiler errors to determine the root cause and identify potential mitigation(s). Use this agent when the user asks to 'investigate a bug', 'debug why this fixture errors', 'understand why the compiler is failing', 'find the root cause of a compiler issue', or when they provide a snippet of code and ask to debug. Use automatically when encountering a failing test case, in order to understand the root cause.
4 +model: opus
5 +color: pink
6 +---
7 +
8 +You are an expert React Compiler debugging specialist with deep knowledge of compiler internals, intermediate representations, and optimization passes. Your mission is to systematically investigate compiler bugs to identify root causes and provide actionable information for fixes.
9 +
10 +## Your Investigation Process
11 +
12 +### Step 1: Create Test Fixture
13 +Create a new fixture file at `packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/<fixture-name>.js` containing the problematic code. Use a descriptive name that reflects the issue (e.g., `bug-optional-chain-in-effect.js`).
14 +
15 +### Step 2: Run Debug Compilation
16 +Execute `yarn snap -d -p <fixture-name>` to compile the fixture with full debug output. This shows the state of the program after each compilation pass.
17 +
18 +### Step 3: Analyze Compilation Results
19 +
20 +### Step 3a: If the fixture compiles successfully
21 +- Compare the output against the user's expected behavior
22 +- Review each compilation pass output from the `-d` flag
23 +- Identify the first pass where the output diverges from expected behavior
24 +- Proceed to binary search simplification
25 +
26 +### Step 3b: If the fixture errors
27 +Execute `yarn snap minimize --update <path-to-fixture>` to remove non-critical aspects of the failing test case. This **updates the fixture in place**.
28 +
29 +Re-read the fixture file to see the latest, minimal reproduction of the error.
30 +
31 +### Step 4: Iteratively adjust the fixture until it stops erroring
32 +After the previous step the fixture will have all extraneous aspects removed. Try to make further edits to determine the specific feature that is causing the error.
33 +
34 +Ideas:
35 +* Replace immediately-invoked function expressions with labeled blocks
36 +* Remove statements
37 +* Simplify calls (remove arguments, replace the call with its lone argument)
38 +* Simplify control flow statements by picking a single branch. Try using a labeled block with just the selected block
39 +* Replace optional member/call expressions with non-optional versions
40 +* Remove items in array/object expressions
41 +* Remove properties from member expressions
42 +
43 +Try to make the minimal possible edit to get the fixture stop erroring.
44 +
45 +### Step 5: Compare Debug Outputs
46 +With both minimal versions (failing and non-failing):
47 +- Run `yarn snap -d -p <fixture-name>` on both
48 +- Compare the debug output pass-by-pass
49 +- Identify the exact pass where behavior diverges
50 +- Note specific differences in HIR, effects, or generated code
51 +
52 +### Step 6: Investigate Compiler Logic
53 +- Read the documentation for the problematic pass in `packages/babel-plugin-react-compiler/docs/passes/`
54 +- Examine the pass implementation in `packages/babel-plugin-react-compiler/src/`
55 +- Key directories to investigate:
56 + - `src/HIR/` - IR definitions and utilities
57 + - `src/Inference/` - Effect inference (aliasing, mutation)
58 + - `src/Validation/` - Validation passes
59 + - `src/Optimization/` - Optimization passes
60 + - `src/ReactiveScopes/` - Reactive scope analysis
61 +- Identify specific code locations that may be handling the pattern incorrectly
62 +
63 +## Output Format
64 +
65 +Provide a structured investigation report:
66 +
67 +```
68 +## Investigation Summary
69 +
70 +### Bug Description
71 +[Brief description of the issue]
72 +
73 +### Minimal Failing Fixture
74 +```javascript
75 +// packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/<name>.js
76 +[minimal code that reproduces the error]
77 +```
78 +
79 +### Minimal Non-Failing Fixture
80 +```javascript
81 +// The simplest change that makes it work
82 +[code that compiles correctly]
83 +```
84 +
85 +### Problematic Compiler Pass
86 +[Name of the pass where the issue occurs]
87 +
88 +### Root Cause Analysis
89 +[Explanation of what the compiler is doing wrong]
90 +
91 +### Suspect Code Locations
92 +- `packages/babel-plugin-react-compiler/src/<path>:<line>:<column>` - [description of what may be incorrect]
93 +- [additional locations if applicable]
94 +
95 +### Suggested Fix Direction
96 +[Brief suggestion of how the bug might be fixed]
97 +```
98 +
99 +## Key Debugging Tips
100 +
101 +1. The debug output (`-d` flag) shows the program state after each pass - use this to pinpoint where things go wrong
102 +2. Look for `@aliasingEffects=` on FunctionExpressions to understand data flow
103 +3. Check for `Impure`, `Render`, `Capture` effects on instructions
104 +4. The pass ordering in `Pipeline.ts` shows when effects are populated vs validated
105 +5. Todo errors indicate unsupported but known patterns; Invariant errors indicate unexpected states
106 +
107 +## Important Reminders
108 +
109 +- Always create the fixture file before running tests
110 +- Use descriptive fixture names that indicate the bug being investigated
111 +- Keep both failing and non-failing minimal versions for your report
112 +- Provide specific file:line:column references when identifying suspect code
113 +- Read the relevant pass documentation before making conclusions about the cause
compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
+1 -1
@@ -612,7 +612,7 @@ export type TryTerminal = {
612 export type MaybeThrowTerminal = {
613 kind: 'maybe-throw';
614 continuation: BlockId;
615 - handler: BlockId;
615 + handler: BlockId | null;
616 id: InstructionId;
617 loc: SourceLocation;
618 fallthrough?: never;
compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts
+3 -1
@@ -291,7 +291,9 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
291 break;
292 }
293 case 'maybe-throw': {
294 - value = `[${terminal.id}] MaybeThrow continuation=bb${terminal.continuation} handler=bb${terminal.handler}`;
294 + const handlerStr =
295 + terminal.handler !== null ? `bb${terminal.handler}` : '(none)';
296 + value = `[${terminal.id}] MaybeThrow continuation=bb${terminal.continuation} handler=${handlerStr}`;
297 if (terminal.effects != null) {
298 value += `\n ${terminal.effects.map(printAliasingEffect).join('\n ')}`;
299 }
compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts
+4 -2
@@ -909,7 +909,7 @@ export function mapTerminalSuccessors(
909 }
910 case 'maybe-throw': {
911 const continuation = fn(terminal.continuation);
912 - const handler = fn(terminal.handler);
912 + const handler = terminal.handler !== null ? fn(terminal.handler) : null;
913 return {
914 kind: 'maybe-throw',
915 continuation,
@@ -1083,7 +1083,9 @@ export function* eachTerminalSuccessor(terminal: Terminal): Iterable<BlockId> {
1083 }
1084 case 'maybe-throw': {
1085 yield terminal.continuation;
1086 - yield terminal.handler;
1086 + if (terminal.handler !== null) {
1087 + yield terminal.handler;
1088 + }
1089 break;
1090 }
1091 case 'try': {
compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts
+1 -1
@@ -508,7 +508,7 @@ function inferBlock(
508 const terminal = block.terminal;
509 if (terminal.kind === 'try' && terminal.handlerBinding != null) {
510 context.catchHandlers.set(terminal.handler, terminal.handlerBinding);
511 - } else if (terminal.kind === 'maybe-throw') {
511 + } else if (terminal.kind === 'maybe-throw' && terminal.handler !== null) {
512 const handlerParam = context.catchHandlers.get(terminal.handler);
513 if (handlerParam != null) {
514 CompilerError.invariant(state.kind(handlerParam) != null, {
compiler/packages/babel-plugin-react-compiler/src/Optimization/PruneMaybeThrows.ts
+10 -11
@@ -9,7 +9,6 @@ import {CompilerError} from '..';
9 import {
10 BlockId,
11 GeneratedSource,
12 - GotoVariant,
12 HIRFunction,
13 Instruction,
14 assertConsistentIdentifiers,
@@ -25,9 +24,15 @@ import {
24 } from '../HIR/HIRBuilder';
25 import {printPlace} from '../HIR/PrintHIR';
26
28 -/*
29 - * This pass prunes `maybe-throw` terminals for blocks that can provably *never* throw.
30 - * For now this is very conservative, and only affects blocks with primitives or
27 +/**
28 + * This pass updates `maybe-throw` terminals for blocks that can provably *never* throw,
29 + * nulling out the handler to indicate that control will always continue. Note that
30 + * rewriting to a `goto` disrupts the structure of the HIR, making it more difficult to
31 + * reconstruct an ast during BuildReactiveFunction. Preserving the maybe-throw makes the
32 + * continuations clear, while nulling out the handler tells us that control cannot flow
33 + * to the handler.
34 + *
35 + * For now the analysis is very conservative, and only affects blocks with primitives or
36 * array/object literals. Even a variable reference could throw bc of the TDZ.
37 */
38 export function pruneMaybeThrows(fn: HIRFunction): void {
@@ -82,13 +87,7 @@ function pruneMaybeThrowsImpl(fn: HIRFunction): Map<BlockId, BlockId> | null {
87 if (!canThrow) {
88 const source = terminalMapping.get(block.id) ?? block.id;
89 terminalMapping.set(terminal.continuation, source);
85 - block.terminal = {
86 - kind: 'goto',
87 - block: terminal.continuation,
88 - variant: GotoVariant.Break,
89 - id: terminal.id,
90 - loc: terminal.loc,
91 - };
90 + terminal.handler = null;
91 }
92 }
93 return terminalMapping.size > 0 ? terminalMapping : null;
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts
+59 -92
@@ -141,6 +141,61 @@ class Driver {
141 return {block: blockId, place, value: sequence, id: instr.id};
142 }
143
144 + /*
145 + * Converts the result of visitValueBlock into a SequenceExpression that includes
146 + * the instruction with its lvalue. This is needed for for/for-of/for-in init/test
147 + * blocks where the instruction's lvalue assignment must be preserved.
148 + *
149 + * This also flattens nested SequenceExpressions that can occur from MaybeThrow
150 + * handling in try-catch blocks.
151 + */
152 + valueBlockResultToSequence(
153 + result: {
154 + block: BlockId;
155 + value: ReactiveValue;
156 + place: Place;
157 + id: InstructionId;
158 + },
159 + loc: SourceLocation,
160 + ): ReactiveSequenceValue {
161 + // Collect all instructions from potentially nested SequenceExpressions
162 + const instructions: Array<ReactiveInstruction> = [];
163 + let innerValue: ReactiveValue = result.value;
164 +
165 + // Flatten nested SequenceExpressions
166 + while (innerValue.kind === 'SequenceExpression') {
167 + instructions.push(...innerValue.instructions);
168 + innerValue = innerValue.value;
169 + }
170 +
171 + /*
172 + * Only add the final instruction if the innermost value is not just a LoadLocal
173 + * of the same place we're storing to (which would be a no-op).
174 + * This happens when MaybeThrow blocks cause the sequence to already contain
175 + * all the necessary instructions.
176 + */
177 + const isLoadOfSamePlace =
178 + innerValue.kind === 'LoadLocal' &&
179 + innerValue.place.identifier.id === result.place.identifier.id;
180 +
181 + if (!isLoadOfSamePlace) {
182 + instructions.push({
183 + id: result.id,
184 + lvalue: result.place,
185 + value: innerValue,
186 + loc,
187 + });
188 + }
189 +
190 + return {
191 + kind: 'SequenceExpression',
192 + instructions,
193 + id: result.id,
194 + value: {kind: 'Primitive', value: undefined, loc},
195 + loc,
196 + };
197 + }
198 +
199 traverseBlock(block: BasicBlock): ReactiveBlock {
200 const blockValue: ReactiveBlock = [];
201 this.visitBlock(block, blockValue);
@@ -441,29 +496,7 @@ class Driver {
496 scheduleIds.push(scheduleId);
497
498 const init = this.visitValueBlock(terminal.init, terminal.loc);
444 - const initBlock = this.cx.ir.blocks.get(init.block)!;
445 - let initValue = init.value;
446 - if (initValue.kind === 'SequenceExpression') {
447 - const last = initBlock.instructions.at(-1)!;
448 - initValue.instructions.push(last);
449 - initValue.value = {
450 - kind: 'Primitive',
451 - value: undefined,
452 - loc: terminal.loc,
453 - };
454 - } else {
455 - initValue = {
456 - kind: 'SequenceExpression',
457 - instructions: [initBlock.instructions.at(-1)!],
458 - id: terminal.id,
459 - loc: terminal.loc,
460 - value: {
461 - kind: 'Primitive',
462 - value: undefined,
463 - loc: terminal.loc,
464 - },
465 - };
466 - }
499 + const initValue = this.valueBlockResultToSequence(init, terminal.loc);
500
501 const testValue = this.visitValueBlock(
502 terminal.test,
@@ -524,54 +557,10 @@ class Driver {
557 scheduleIds.push(scheduleId);
558
559 const init = this.visitValueBlock(terminal.init, terminal.loc);
527 - const initBlock = this.cx.ir.blocks.get(init.block)!;
528 - let initValue = init.value;
529 - if (initValue.kind === 'SequenceExpression') {
530 - const last = initBlock.instructions.at(-1)!;
531 - initValue.instructions.push(last);
532 - initValue.value = {
533 - kind: 'Primitive',
534 - value: undefined,
535 - loc: terminal.loc,
536 - };
537 - } else {
538 - initValue = {
539 - kind: 'SequenceExpression',
540 - instructions: [initBlock.instructions.at(-1)!],
541 - id: terminal.id,
542 - loc: terminal.loc,
543 - value: {
544 - kind: 'Primitive',
545 - value: undefined,
546 - loc: terminal.loc,
547 - },
548 - };
549 - }
560 + const initValue = this.valueBlockResultToSequence(init, terminal.loc);
561
562 const test = this.visitValueBlock(terminal.test, terminal.loc);
552 - const testBlock = this.cx.ir.blocks.get(test.block)!;
553 - let testValue = test.value;
554 - if (testValue.kind === 'SequenceExpression') {
555 - const last = testBlock.instructions.at(-1)!;
556 - testValue.instructions.push(last);
557 - testValue.value = {
558 - kind: 'Primitive',
559 - value: undefined,
560 - loc: terminal.loc,
561 - };
562 - } else {
563 - testValue = {
564 - kind: 'SequenceExpression',
565 - instructions: [testBlock.instructions.at(-1)!],
566 - id: terminal.id,
567 - loc: terminal.loc,
568 - value: {
569 - kind: 'Primitive',
570 - value: undefined,
571 - loc: terminal.loc,
572 - },
573 - };
574 - }
563 + const testValue = this.valueBlockResultToSequence(test, terminal.loc);
564
565 let loopBody: ReactiveBlock;
566 if (loopId) {
@@ -621,29 +610,7 @@ class Driver {
610 scheduleIds.push(scheduleId);
611
612 const init = this.visitValueBlock(terminal.init, terminal.loc);
624 - const initBlock = this.cx.ir.blocks.get(init.block)!;
625 - let initValue = init.value;
626 - if (initValue.kind === 'SequenceExpression') {
627 - const last = initBlock.instructions.at(-1)!;
628 - initValue.instructions.push(last);
629 - initValue.value = {
630 - kind: 'Primitive',
631 - value: undefined,
632 - loc: terminal.loc,
633 - };
634 - } else {
635 - initValue = {
636 - kind: 'SequenceExpression',
637 - instructions: [initBlock.instructions.at(-1)!],
638 - id: terminal.id,
639 - loc: terminal.loc,
640 - value: {
641 - kind: 'Primitive',
642 - value: undefined,
643 - loc: terminal.loc,
644 - },
645 - };
646 - }
613 + const initValue = this.valueBlockResultToSequence(init, terminal.loc);
614
615 let loopBody: ReactiveBlock;
616 if (loopId) {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-break-target.expect.md deleted
-32
@@ -1,32 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -import {useMemo} from 'react';
6 -
7 -export default function useFoo(text) {
8 - return useMemo(() => {
9 - try {
10 - let formattedText = '';
11 - try {
12 - formattedText = format(text);
13 - } catch {
14 - console.log('error');
15 - }
16 - return formattedText || '';
17 - } catch (e) {}
18 - }, [text]);
19 -}
20 -
21 -```
22 -
23 -
24 -## Error
25 -
26 -```
27 -Found 1 error:
28 -
29 -Invariant: Expected a break target
30 -```
31 -
32 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-break-target.js deleted
-15
@@ -1,15 +0,0 @@
1 -import {useMemo} from 'react';
2 -
3 -export default function useFoo(text) {
4 - return useMemo(() => {
5 - try {
6 - let formattedText = '';
7 - try {
8 - formattedText = format(text);
9 - } catch {
10 - console.log('error');
11 - }
12 - return formattedText || '';
13 - } catch (e) {}
14 - }, [text]);
15 -}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-declaration-for-all-identifiers.expect.md deleted
-35
@@ -1,35 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -function Foo() {
6 - try {
7 - // NOTE: this fixture previously failed during LeaveSSA;
8 - // double-check this code when supporting value blocks in try/catch
9 - for (let i = 0; i < 2; i++) {}
10 - } catch {}
11 -}
12 -
13 -```
14 -
15 -
16 -## Error
17 -
18 -```
19 -Found 1 error:
20 -
21 -Invariant: Expected a variable declaration
22 -
23 -Got ExpressionStatement.
24 -
25 -error.todo-repro-declaration-for-all-identifiers.ts:5:4
26 - 3 | // NOTE: this fixture previously failed during LeaveSSA;
27 - 4 | // double-check this code when supporting value blocks in try/catch
28 -> 5 | for (let i = 0; i < 2; i++) {}
29 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Expected a variable declaration
30 - 6 | } catch {}
31 - 7 | }
32 - 8 |
33 -```
34 -
35 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-declaration-for-all-identifiers.js deleted
-7
@@ -1,7 +0,0 @@
1 -function Foo() {
2 - try {
3 - // NOTE: this fixture previously failed during LeaveSSA;
4 - // double-check this code when supporting value blocks in try/catch
5 - for (let i = 0; i < 2; i++) {}
6 - } catch {}
7 -}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-declaration-for-all-identifiers.expect.md new
+50
@@ -0,0 +1,50 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Foo() {
6 + try {
7 + for (let i = 0; i < 2; i++) {}
8 + } catch {}
9 + return <span>ok</span>;
10 +}
11 +
12 +export const FIXTURE_ENTRYPOINT = {
13 + fn: Foo,
14 + params: [],
15 + sequentialRenders: [{}, {}, {}],
16 +};
17 +
18 +```
19 +
20 +## Code
21 +
22 +```javascript
23 +import { c as _c } from "react/compiler-runtime";
24 +function Foo() {
25 + const $ = _c(1);
26 + try {
27 + for (let i = 0; i < 2; i++) {}
28 + } catch {}
29 + let t0;
30 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
31 + t0 = <span>ok</span>;
32 + $[0] = t0;
33 + } else {
34 + t0 = $[0];
35 + }
36 + return t0;
37 +}
38 +
39 +export const FIXTURE_ENTRYPOINT = {
40 + fn: Foo,
41 + params: [],
42 + sequentialRenders: [{}, {}, {}],
43 +};
44 +
45 +```
46 +
47 +### Eval output
48 +(kind: ok) <span>ok</span>
49 +<span>ok</span>
50 +<span>ok</span>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-declaration-for-all-identifiers.js new
+12
@@ -0,0 +1,12 @@
1 +function Foo() {
2 + try {
3 + for (let i = 0; i < 2; i++) {}
4 + } catch {}
5 + return <span>ok</span>;
6 +}
7 +
8 +export const FIXTURE_ENTRYPOINT = {
9 + fn: Foo,
10 + params: [],
11 + sequentialRenders: [{}, {}, {}],
12 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-in-in-try.expect.md new
+102
@@ -0,0 +1,102 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Foo({obj}) {
6 + const keys = [];
7 + try {
8 + for (const key in obj) {
9 + keys.push(key);
10 + }
11 + } catch (e) {
12 + return <span>Error</span>;
13 + }
14 + return <span>{keys.join(', ')}</span>;
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Foo,
19 + params: [{obj: {a: 1, b: 2}}],
20 + sequentialRenders: [
21 + {obj: {a: 1, b: 2}},
22 + {obj: {a: 1, b: 2}},
23 + {obj: {x: 'hello', y: 'world'}},
24 + {obj: {}},
25 + {obj: {single: 'value'}},
26 + ],
27 +};
28 +
29 +```
30 +
31 +## Code
32 +
33 +```javascript
34 +import { c as _c } from "react/compiler-runtime";
35 +function Foo(t0) {
36 + const $ = _c(6);
37 + const { obj } = t0;
38 + let keys;
39 + let t1;
40 + if ($[0] !== obj) {
41 + t1 = Symbol.for("react.early_return_sentinel");
42 + bb0: {
43 + keys = [];
44 + try {
45 + for (const key in obj) {
46 + keys.push(key);
47 + }
48 + } catch (t2) {
49 + let t3;
50 + if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
51 + t3 = <span>Error</span>;
52 + $[3] = t3;
53 + } else {
54 + t3 = $[3];
55 + }
56 + t1 = t3;
57 + break bb0;
58 + }
59 + }
60 + $[0] = obj;
61 + $[1] = keys;
62 + $[2] = t1;
63 + } else {
64 + keys = $[1];
65 + t1 = $[2];
66 + }
67 + if (t1 !== Symbol.for("react.early_return_sentinel")) {
68 + return t1;
69 + }
70 +
71 + const t2 = keys.join(", ");
72 + let t3;
73 + if ($[4] !== t2) {
74 + t3 = <span>{t2}</span>;
75 + $[4] = t2;
76 + $[5] = t3;
77 + } else {
78 + t3 = $[5];
79 + }
80 + return t3;
81 +}
82 +
83 +export const FIXTURE_ENTRYPOINT = {
84 + fn: Foo,
85 + params: [{ obj: { a: 1, b: 2 } }],
86 + sequentialRenders: [
87 + { obj: { a: 1, b: 2 } },
88 + { obj: { a: 1, b: 2 } },
89 + { obj: { x: "hello", y: "world" } },
90 + { obj: {} },
91 + { obj: { single: "value" } },
92 + ],
93 +};
94 +
95 +```
96 +
97 +### Eval output
98 +(kind: ok) <span>a, b</span>
99 +<span>a, b</span>
100 +<span>x, y</span>
101 +<span></span>
102 +<span>single</span>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-in-in-try.js new
+23
@@ -0,0 +1,23 @@
1 +function Foo({obj}) {
2 + const keys = [];
3 + try {
4 + for (const key in obj) {
5 + keys.push(key);
6 + }
7 + } catch (e) {
8 + return <span>Error</span>;
9 + }
10 + return <span>{keys.join(', ')}</span>;
11 +}
12 +
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: Foo,
15 + params: [{obj: {a: 1, b: 2}}],
16 + sequentialRenders: [
17 + {obj: {a: 1, b: 2}},
18 + {obj: {a: 1, b: 2}},
19 + {obj: {x: 'hello', y: 'world'}},
20 + {obj: {}},
21 + {obj: {single: 'value'}},
22 + ],
23 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-loop-in-try.expect.md new
+95
@@ -0,0 +1,95 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Foo({items}) {
6 + const results = [];
7 + try {
8 + for (let i = 0; i < items.length; i++) {
9 + results.push(items[i]);
10 + }
11 + } catch (e) {
12 + return <span>Error</span>;
13 + }
14 + return <span>{results.join(', ')}</span>;
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Foo,
19 + params: [{items: ['a', 'b', 'c']}],
20 + sequentialRenders: [
21 + {items: ['a', 'b', 'c']},
22 + {items: ['a', 'b', 'c']},
23 + {items: ['x', 'y']},
24 + {items: []},
25 + {items: ['single']},
26 + ],
27 +};
28 +
29 +```
30 +
31 +## Code
32 +
33 +```javascript
34 +import { c as _c } from "react/compiler-runtime";
35 +function Foo(t0) {
36 + const $ = _c(5);
37 + const { items } = t0;
38 + let results;
39 + let t1;
40 + if ($[0] !== items) {
41 + t1 = Symbol.for("react.early_return_sentinel");
42 + bb0: {
43 + results = [];
44 + try {
45 + for (let i = 0; i < items.length; i++) {
46 + results.push(items[i]);
47 + }
48 + } catch (t2) {
49 + t1 = <span>Error</span>;
50 + break bb0;
51 + }
52 + }
53 + $[0] = items;
54 + $[1] = results;
55 + $[2] = t1;
56 + } else {
57 + results = $[1];
58 + t1 = $[2];
59 + }
60 + if (t1 !== Symbol.for("react.early_return_sentinel")) {
61 + return t1;
62 + }
63 +
64 + const t2 = results.join(", ");
65 + let t3;
66 + if ($[3] !== t2) {
67 + t3 = <span>{t2}</span>;
68 + $[3] = t2;
69 + $[4] = t3;
70 + } else {
71 + t3 = $[4];
72 + }
73 + return t3;
74 +}
75 +
76 +export const FIXTURE_ENTRYPOINT = {
77 + fn: Foo,
78 + params: [{ items: ["a", "b", "c"] }],
79 + sequentialRenders: [
80 + { items: ["a", "b", "c"] },
81 + { items: ["a", "b", "c"] },
82 + { items: ["x", "y"] },
83 + { items: [] },
84 + { items: ["single"] },
85 + ],
86 +};
87 +
88 +```
89 +
90 +### Eval output
91 +(kind: ok) <span>a, b, c</span>
92 +<span>a, b, c</span>
93 +<span>x, y</span>
94 +<span></span>
95 +<span>single</span>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-loop-in-try.js new
+23
@@ -0,0 +1,23 @@
1 +function Foo({items}) {
2 + const results = [];
3 + try {
4 + for (let i = 0; i < items.length; i++) {
5 + results.push(items[i]);
6 + }
7 + } catch (e) {
8 + return <span>Error</span>;
9 + }
10 + return <span>{results.join(', ')}</span>;
11 +}
12 +
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: Foo,
15 + params: [{items: ['a', 'b', 'c']}],
16 + sequentialRenders: [
17 + {items: ['a', 'b', 'c']},
18 + {items: ['a', 'b', 'c']},
19 + {items: ['x', 'y']},
20 + {items: []},
21 + {items: ['single']},
22 + ],
23 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-of-in-try.expect.md new
+102
@@ -0,0 +1,102 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Foo({obj}) {
6 + const items = [];
7 + try {
8 + for (const [key, value] of Object.entries(obj)) {
9 + items.push(`${key}: ${value}`);
10 + }
11 + } catch (e) {
12 + return <span>Error</span>;
13 + }
14 + return <span>{items.join(', ')}</span>;
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Foo,
19 + params: [{obj: {a: 1, b: 2}}],
20 + sequentialRenders: [
21 + {obj: {a: 1, b: 2}},
22 + {obj: {a: 1, b: 2}},
23 + {obj: {x: 'hello', y: 'world'}},
24 + {obj: {}},
25 + {obj: {single: 'value'}},
26 + ],
27 +};
28 +
29 +```
30 +
31 +## Code
32 +
33 +```javascript
34 +import { c as _c } from "react/compiler-runtime";
35 +function Foo(t0) {
36 + const $ = _c(6);
37 + const { obj } = t0;
38 + let items;
39 + let t1;
40 + if ($[0] !== obj) {
41 + t1 = Symbol.for("react.early_return_sentinel");
42 + bb0: {
43 + items = [];
44 + try {
45 + for (const [key, value] of Object.entries(obj)) {
46 + items.push(`${key}: ${value}`);
47 + }
48 + } catch (t2) {
49 + let t3;
50 + if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
51 + t3 = <span>Error</span>;
52 + $[3] = t3;
53 + } else {
54 + t3 = $[3];
55 + }
56 + t1 = t3;
57 + break bb0;
58 + }
59 + }
60 + $[0] = obj;
61 + $[1] = items;
62 + $[2] = t1;
63 + } else {
64 + items = $[1];
65 + t1 = $[2];
66 + }
67 + if (t1 !== Symbol.for("react.early_return_sentinel")) {
68 + return t1;
69 + }
70 +
71 + const t2 = items.join(", ");
72 + let t3;
73 + if ($[4] !== t2) {
74 + t3 = <span>{t2}</span>;
75 + $[4] = t2;
76 + $[5] = t3;
77 + } else {
78 + t3 = $[5];
79 + }
80 + return t3;
81 +}
82 +
83 +export const FIXTURE_ENTRYPOINT = {
84 + fn: Foo,
85 + params: [{ obj: { a: 1, b: 2 } }],
86 + sequentialRenders: [
87 + { obj: { a: 1, b: 2 } },
88 + { obj: { a: 1, b: 2 } },
89 + { obj: { x: "hello", y: "world" } },
90 + { obj: {} },
91 + { obj: { single: "value" } },
92 + ],
93 +};
94 +
95 +```
96 +
97 +### Eval output
98 +(kind: ok) <span>a: 1, b: 2</span>
99 +<span>a: 1, b: 2</span>
100 +<span>x: hello, y: world</span>
101 +<span></span>
102 +<span>single: value</span>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-for-of-in-try.js new
+23
@@ -0,0 +1,23 @@
1 +function Foo({obj}) {
2 + const items = [];
3 + try {
4 + for (const [key, value] of Object.entries(obj)) {
5 + items.push(`${key}: ${value}`);
6 + }
7 + } catch (e) {
8 + return <span>Error</span>;
9 + }
10 + return <span>{items.join(', ')}</span>;
11 +}
12 +
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: Foo,
15 + params: [{obj: {a: 1, b: 2}}],
16 + sequentialRenders: [
17 + {obj: {a: 1, b: 2}},
18 + {obj: {a: 1, b: 2}},
19 + {obj: {x: 'hello', y: 'world'}},
20 + {obj: {}},
21 + {obj: {single: 'value'}},
22 + ],
23 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-nested-try-catch-in-usememo.expect.md new
+114
@@ -0,0 +1,114 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @compilationMode:"infer"
6 +import {useMemo} from 'react';
7 +
8 +function useFoo(text) {
9 + return useMemo(() => {
10 + try {
11 + let formattedText = '';
12 + try {
13 + formattedText = format(text);
14 + } catch {
15 + formattedText = text;
16 + }
17 + return formattedText || '';
18 + } catch (e) {
19 + return '';
20 + }
21 + }, [text]);
22 +}
23 +
24 +function format(text) {
25 + return text.toUpperCase();
26 +}
27 +
28 +function Foo({text}) {
29 + const result = useFoo(text);
30 + return <span>{result}</span>;
31 +}
32 +
33 +export const FIXTURE_ENTRYPOINT = {
34 + fn: Foo,
35 + params: [{text: 'hello'}],
36 + sequentialRenders: [
37 + {text: 'hello'},
38 + {text: 'hello'},
39 + {text: 'world'},
40 + {text: ''},
41 + ],
42 +};
43 +
44 +```
45 +
46 +## Code
47 +
48 +```javascript
49 +import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer"
50 +import { useMemo } from "react";
51 +
52 +function useFoo(text) {
53 + const $ = _c(2);
54 + let t0;
55 + try {
56 + let formattedText;
57 + try {
58 + let t2;
59 + if ($[0] !== text) {
60 + t2 = format(text);
61 + $[0] = text;
62 + $[1] = t2;
63 + } else {
64 + t2 = $[1];
65 + }
66 + formattedText = t2;
67 + } catch {
68 + formattedText = text;
69 + }
70 +
71 + t0 = formattedText || "";
72 + } catch (t1) {
73 + t0 = "";
74 + }
75 + return t0;
76 +}
77 +
78 +function format(text) {
79 + return text.toUpperCase();
80 +}
81 +
82 +function Foo(t0) {
83 + const $ = _c(2);
84 + const { text } = t0;
85 + const result = useFoo(text);
86 + let t1;
87 + if ($[0] !== result) {
88 + t1 = <span>{result}</span>;
89 + $[0] = result;
90 + $[1] = t1;
91 + } else {
92 + t1 = $[1];
93 + }
94 + return t1;
95 +}
96 +
97 +export const FIXTURE_ENTRYPOINT = {
98 + fn: Foo,
99 + params: [{ text: "hello" }],
100 + sequentialRenders: [
101 + { text: "hello" },
102 + { text: "hello" },
103 + { text: "world" },
104 + { text: "" },
105 + ],
106 +};
107 +
108 +```
109 +
110 +### Eval output
111 +(kind: ok) <span>HELLO</span>
112 +<span>HELLO</span>
113 +<span>WORLD</span>
114 +<span></span>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-nested-try-catch-in-usememo.js new
+38
@@ -0,0 +1,38 @@
1 +// @compilationMode:"infer"
2 +import {useMemo} from 'react';
3 +
4 +function useFoo(text) {
5 + return useMemo(() => {
6 + try {
7 + let formattedText = '';
8 + try {
9 + formattedText = format(text);
10 + } catch {
11 + formattedText = text;
12 + }
13 + return formattedText || '';
14 + } catch (e) {
15 + return '';
16 + }
17 + }, [text]);
18 +}
19 +
20 +function format(text) {
21 + return text.toUpperCase();
22 +}
23 +
24 +function Foo({text}) {
25 + const result = useFoo(text);
26 + return <span>{result}</span>;
27 +}
28 +
29 +export const FIXTURE_ENTRYPOINT = {
30 + fn: Foo,
31 + params: [{text: 'hello'}],
32 + sequentialRenders: [
33 + {text: 'hello'},
34 + {text: 'hello'},
35 + {text: 'world'},
36 + {text: ''},
37 + ],
38 +};
compiler/packages/snap/src/minimize.ts
+1 -59
@@ -11,15 +11,9 @@ import generate from '@babel/generator';
11 import traverse from '@babel/traverse';
12 import * as t from '@babel/types';
13 import type {parseConfigPragmaForTests as ParseConfigPragma} from 'babel-plugin-react-compiler/src/Utils/TestUtils';
14 -import fs from 'fs';
15 -import path from 'path';
16 -import {parseInput, parseLanguage, parseSourceType} from './compiler.js';
14 +import {parseInput} from './compiler.js';
15 import {PARSE_CONFIG_PRAGMA_IMPORT, PROJECT_SRC} from './constants.js';
16
19 -type MinimizeOptions = {
20 - path: string;
21 -};
22 -
17 type CompileSuccess = {kind: 'success'};
18 type CompileParseError = {kind: 'parse_error'; message: string};
19 type CompileErrors = {
@@ -2016,55 +2010,3 @@ export function minimize(
2010
2011 return {kind: 'minimized', source: currentCode};
2012 }
2019 -
2020 -/**
2021 - * Main minimize function that reads the input file, runs minimization,
2022 - * and reports results.
2023 - */
2024 -export async function runMinimize(options: MinimizeOptions): Promise<void> {
2025 - // Resolve the input path
2026 - const inputPath = path.isAbsolute(options.path)
2027 - ? options.path
2028 - : path.resolve(process.cwd(), options.path);
2029 -
2030 - // Check if file exists
2031 - if (!fs.existsSync(inputPath)) {
2032 - console.error(`Error: File not found: ${inputPath}`);
2033 - process.exit(1);
2034 - }
2035 -
2036 - // Read the input file
2037 - const input = fs.readFileSync(inputPath, 'utf-8');
2038 - const filename = path.basename(inputPath);
2039 - const firstLine = input.substring(0, input.indexOf('\n'));
2040 - const language = parseLanguage(firstLine);
2041 - const sourceType = parseSourceType(firstLine);
2042 -
2043 - console.log(`Minimizing: ${inputPath}`);
2044 -
2045 - const originalLines = input.split('\n').length;
2046 -
2047 - // Run the minimization
2048 - const result = minimize(input, filename, language, sourceType);
2049 -
2050 - if (result.kind === 'success') {
2051 - console.log('Could not minimize: the input compiles successfully.');
2052 - process.exit(0);
2053 - }
2054 -
2055 - if (result.kind === 'minimal') {
2056 - console.log(
2057 - 'Could not minimize: the input fails but is already minimal and cannot be reduced further.',
2058 - );
2059 - process.exit(0);
2060 - }
2061 -
2062 - // Output the minimized code
2063 - console.log('--- Minimized Code ---');
2064 - console.log(result.source);
2065 -
2066 - const minimizedLines = result.source.split('\n').length;
2067 - console.log(
2068 - `\nReduced from ${originalLines} lines to ${minimizedLines} lines`,
2069 - );
2070 -}
compiler/packages/snap/src/reporter.ts
+14 -5
@@ -139,15 +139,24 @@ export async function update(results: TestResults): Promise<void> {
139 * Report test results to the user
140 * @returns boolean indicatig whether all tests passed
141 */
142 -export function report(results: TestResults): boolean {
142 +export function report(
143 + results: TestResults,
144 + verbose: boolean = false,
145 +): boolean {
146 const failures: Array<[string, TestResult]> = [];
147 for (const [basename, result] of results) {
148 if (result.actual === result.expected && result.unexpectedError == null) {
146 - console.log(
147 - chalk.green.inverse.bold(' PASS ') + ' ' + chalk.dim(basename),
148 - );
149 + if (verbose) {
150 + console.log(
151 + chalk.green.inverse.bold(' PASS ') + ' ' + chalk.dim(basename),
152 + );
153 + }
154 } else {
150 - console.log(chalk.red.inverse.bold(' FAIL ') + ' ' + chalk.dim(basename));
155 + if (verbose) {
156 + console.log(
157 + chalk.red.inverse.bold(' FAIL ') + ' ' + chalk.dim(basename),
158 + );
159 + }
160 failures.push([basename, result]);
161 }
162 }
compiler/packages/snap/src/runner.ts
+172 -109
@@ -23,28 +23,177 @@ import {
23 } from './runner-watch';
24 import * as runnerWorker from './runner-worker';
25 import {execSync} from 'child_process';
26 -import {runMinimize} from './minimize';
26 +import fs from 'fs';
27 +import path from 'path';
28 +import {minimize} from './minimize';
29 +import {parseLanguage, parseSourceType} from './compiler';
30
31 const WORKER_PATH = require.resolve('./runner-worker.js');
32 const NUM_WORKERS = cpus().length - 1;
33
34 readline.emitKeypressEvents(process.stdin);
35
33 -type RunnerOptions = {
36 +type TestOptions = {
37 sync: boolean;
38 workerThreads: boolean;
39 watch: boolean;
40 update: boolean;
41 pattern?: string;
42 debug: boolean;
43 + verbose: boolean;
44 };
45
42 -async function runTestCommand(opts: RunnerOptions): Promise<void> {
43 - await main(opts);
46 +type MinimizeOptions = {
47 + path: string;
48 + update: boolean;
49 +};
50 +
51 +async function runTestCommand(opts: TestOptions): Promise<void> {
52 + const worker: Worker & typeof runnerWorker = new Worker(WORKER_PATH, {
53 + enableWorkerThreads: opts.workerThreads,
54 + numWorkers: NUM_WORKERS,
55 + }) as any;
56 + worker.getStderr().pipe(process.stderr);
57 + worker.getStdout().pipe(process.stdout);
58 +
59 + // Check if watch mode should be enabled
60 + const shouldWatch = opts.watch;
61 +
62 + if (shouldWatch) {
63 + makeWatchRunner(
64 + state => onChange(worker, state, opts.sync, opts.verbose),
65 + opts.debug,
66 + opts.pattern,
67 + );
68 + if (opts.pattern) {
69 + /**
70 + * Warm up wormers when in watch mode. Loading the Forget babel plugin
71 + * and all of its transitive dependencies takes 1-3s (per worker) on a M1.
72 + * As jest-worker dispatches tasks using a round-robin strategy, we can
73 + * avoid an additional 1-3s wait on the first num_workers runs by warming
74 + * up workers eagerly.
75 + */
76 + for (let i = 0; i < NUM_WORKERS - 1; i++) {
77 + worker.transformFixture(
78 + {
79 + fixturePath: 'tmp',
80 + snapshotPath: './tmp.expect.md',
81 + inputPath: './tmp.js',
82 + input: `
83 + function Foo(props) {
84 + return identity(props);
85 + }
86 + `,
87 + snapshot: null,
88 + },
89 + 0,
90 + false,
91 + false,
92 + );
93 + }
94 + }
95 + } else {
96 + // Non-watch mode. For simplicity we re-use the same watchSrc() function.
97 + // After the first build completes run tests and exit
98 + const tsWatch: ts.WatchOfConfigFile<ts.SemanticDiagnosticsBuilderProgram> =
99 + watchSrc(
100 + () => {},
101 + async (isTypecheckSuccess: boolean) => {
102 + let isSuccess = false;
103 + if (!isTypecheckSuccess) {
104 + console.error(
105 + 'Found typescript errors in Forget source code, skipping test fixtures.',
106 + );
107 + } else {
108 + try {
109 + execSync('yarn build', {cwd: PROJECT_ROOT});
110 + console.log('Built compiler successfully with tsup');
111 +
112 + // Determine which filter to use
113 + let testFilter: TestFilter | null = null;
114 + if (opts.pattern) {
115 + testFilter = {
116 + paths: [opts.pattern],
117 + };
118 + }
119 +
120 + const results = await runFixtures(
121 + worker,
122 + testFilter,
123 + 0,
124 + opts.debug,
125 + false, // no requireSingleFixture in non-watch mode
126 + opts.sync,
127 + );
128 + if (opts.update) {
129 + update(results);
130 + isSuccess = true;
131 + } else {
132 + isSuccess = report(results, opts.verbose);
133 + }
134 + } catch (e) {
135 + console.warn('Failed to build compiler with tsup:', e);
136 + }
137 + }
138 + tsWatch?.close();
139 + await worker.end();
140 + process.exit(isSuccess ? 0 : 1);
141 + },
142 + );
143 + }
144 }
145
46 -async function runMinimizeCommand(path: string): Promise<void> {
47 - await runMinimize({path});
146 +async function runMinimizeCommand(opts: MinimizeOptions): Promise<void> {
147 + // Resolve the input path
148 + const inputPath = path.isAbsolute(opts.path)
149 + ? opts.path
150 + : path.resolve(process.cwd(), opts.path);
151 +
152 + // Check if file exists
153 + if (!fs.existsSync(inputPath)) {
154 + console.error(`Error: File not found: ${inputPath}`);
155 + process.exit(1);
156 + }
157 +
158 + // Read the input file
159 + const input = fs.readFileSync(inputPath, 'utf-8');
160 + const filename = path.basename(inputPath);
161 + const firstLine = input.substring(0, input.indexOf('\n'));
162 + const language = parseLanguage(firstLine);
163 + const sourceType = parseSourceType(firstLine);
164 +
165 + console.log(`Minimizing: ${inputPath}`);
166 +
167 + const originalLines = input.split('\n').length;
168 +
169 + // Run the minimization
170 + const result = minimize(input, filename, language, sourceType);
171 +
172 + if (result.kind === 'success') {
173 + console.log('Could not minimize: the input compiles successfully.');
174 + process.exit(0);
175 + }
176 +
177 + if (result.kind === 'minimal') {
178 + console.log(
179 + 'Could not minimize: the input fails but is already minimal and cannot be reduced further.',
180 + );
181 + process.exit(0);
182 + }
183 +
184 + // Output the minimized code
185 + console.log('--- Minimized Code ---');
186 + console.log(result.source);
187 +
188 + const minimizedLines = result.source.split('\n').length;
189 + console.log(
190 + `\nReduced from ${originalLines} lines to ${minimizedLines} lines`,
191 + );
192 +
193 + if (opts.update) {
194 + fs.writeFileSync(inputPath, result.source, 'utf-8');
195 + console.log(`\nUpdated ${inputPath} with minimized code.`);
196 + }
197 }
198
199 yargs(hideBin(process.argv))
@@ -85,10 +234,14 @@ yargs(hideBin(process.argv))
234 .boolean('debug')
235 .alias('d', 'debug')
236 .describe('debug', 'Enable debug logging to print HIR for each pass')
88 - .default('debug', false);
237 + .default('debug', false)
238 + .boolean('verbose')
239 + .alias('v', 'verbose')
240 + .describe('verbose', 'Print individual test results')
241 + .default('verbose', false);
242 },
243 async argv => {
91 - await runTestCommand(argv as RunnerOptions);
244 + await runTestCommand(argv as TestOptions);
245 },
246 )
247 .command(
@@ -99,10 +252,17 @@ yargs(hideBin(process.argv))
252 .string('path')
253 .alias('p', 'path')
254 .describe('path', 'Path to the file to minimize')
102 - .demandOption('path');
255 + .demandOption('path')
256 + .boolean('update')
257 + .alias('u', 'update')
258 + .describe(
259 + 'update',
260 + 'Update the input file in-place with the minimized version',
261 + )
262 + .default('update', false);
263 },
264 async argv => {
105 - await runMinimizeCommand(argv.path as string);
265 + await runMinimizeCommand(argv as unknown as MinimizeOptions);
266 },
267 )
268 .help('help')
@@ -162,6 +322,7 @@ async function onChange(
322 worker: Worker & typeof runnerWorker,
323 state: RunnerState,
324 sync: boolean,
325 + verbose: boolean,
326 ) {
327 const {compilerVersion, isCompilerBuildValid, mode, filter, debug} = state;
328 if (isCompilerBuildValid) {
@@ -194,7 +355,7 @@ async function onChange(
355 update(results);
356 state.lastUpdate = end;
357 } else {
197 - report(results);
358 + report(results, verbose);
359 }
360 console.log(`Completed in ${Math.floor(end - start)} ms`);
361 } else {
@@ -216,101 +377,3 @@ async function onChange(
377 '[any] - rerun tests\n',
378 );
379 }
219 -
220 -/**
221 - * Runs the compiler in watch or single-execution mode
222 - */
223 -export async function main(opts: RunnerOptions): Promise<void> {
224 - const worker: Worker & typeof runnerWorker = new Worker(WORKER_PATH, {
225 - enableWorkerThreads: opts.workerThreads,
226 - numWorkers: NUM_WORKERS,
227 - }) as any;
228 - worker.getStderr().pipe(process.stderr);
229 - worker.getStdout().pipe(process.stdout);
230 -
231 - // Check if watch mode should be enabled
232 - const shouldWatch = opts.watch;
233 -
234 - if (shouldWatch) {
235 - makeWatchRunner(
236 - state => onChange(worker, state, opts.sync),
237 - opts.debug,
238 - opts.pattern,
239 - );
240 - if (opts.pattern) {
241 - /**
242 - * Warm up wormers when in watch mode. Loading the Forget babel plugin
243 - * and all of its transitive dependencies takes 1-3s (per worker) on a M1.
244 - * As jest-worker dispatches tasks using a round-robin strategy, we can
245 - * avoid an additional 1-3s wait on the first num_workers runs by warming
246 - * up workers eagerly.
247 - */
248 - for (let i = 0; i < NUM_WORKERS - 1; i++) {
249 - worker.transformFixture(
250 - {
251 - fixturePath: 'tmp',
252 - snapshotPath: './tmp.expect.md',
253 - inputPath: './tmp.js',
254 - input: `
255 - function Foo(props) {
256 - return identity(props);
257 - }
258 - `,
259 - snapshot: null,
260 - },
261 - 0,
262 - false,
263 - false,
264 - );
265 - }
266 - }
267 - } else {
268 - // Non-watch mode. For simplicity we re-use the same watchSrc() function.
269 - // After the first build completes run tests and exit
270 - const tsWatch: ts.WatchOfConfigFile<ts.SemanticDiagnosticsBuilderProgram> =
271 - watchSrc(
272 - () => {},
273 - async (isTypecheckSuccess: boolean) => {
274 - let isSuccess = false;
275 - if (!isTypecheckSuccess) {
276 - console.error(
277 - 'Found typescript errors in Forget source code, skipping test fixtures.',
278 - );
279 - } else {
280 - try {
281 - execSync('yarn build', {cwd: PROJECT_ROOT});
282 - console.log('Built compiler successfully with tsup');
283 -
284 - // Determine which filter to use
285 - let testFilter: TestFilter | null = null;
286 - if (opts.pattern) {
287 - testFilter = {
288 - paths: [opts.pattern],
289 - };
290 - }
291 -
292 - const results = await runFixtures(
293 - worker,
294 - testFilter,
295 - 0,
296 - opts.debug,
297 - false, // no requireSingleFixture in non-watch mode
298 - opts.sync,
299 - );
300 - if (opts.update) {
301 - update(results);
302 - isSuccess = true;
303 - } else {
304 - isSuccess = report(results);
305 - }
306 - } catch (e) {
307 - console.warn('Failed to build compiler with tsup:', e);
308 - }
309 - }
310 - tsWatch?.close();
311 - await worker.end();
312 - process.exit(isSuccess ? 0 : 1);
313 - },
314 - );
315 - }
316 -}