@samitouri / QOS-React / commits / 03613cd68c

[compiler] Improve snap usability (#35537)

A whole bunch of changes to snap aimed at making it more usable for humans and agents. Here's the new CLI interface: ``` node dist/main.js --help Options: --version Show version number [boolean] --sync Run compiler in main thread (instead of using worker threads or subprocesses). Defaults to false. [boolean] [default: false] --worker-threads Run compiler in worker threads (instead of subprocesses). Defaults to true. [boolean] [default: true] --help Show help [boolean] -w, --watch Run compiler in watch mode, re-running after changes [boolean] -u, --update Update fixtures [boolean] -p, --pattern Optional glob pattern to filter fixtures (e.g., "error.*", "use-memo") [string] -d, --debug Enable debug logging to print HIR for each pass[boolean] ``` Key changes: * Added abbreviations for common arguments * No more testfilter.txt! Filtering/debugging works more like Jest, see below. * The `--debug` flag (`-d`) controls whether to emit debug information. In watch mode, this flag sets the initial debug value, and it can be toggled by pressing the 'd' key while watching. * The `--pattern` flag (`-p`) sets a filter pattern. In watch mode, this flag sets the initial filter. It can be changed by pressing 'p' and typing a new pattern, or pressing 'a' to switch to running all tests. * As before, we only actually enable debugging if debug mode is enabled _and_ there is only one test selected. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35537). * #35607 * #35298 * #35596 * #35573 * #35595 * #35539 * __->__ #35537 * #35523

Joseph Savona committed Jan 23, 2026 at 10:36 UTC 03613cd68c9e1d7ec5cb1eceb91f27f1935a7b8b
4 files changed +109 -104
compiler/packages/snap/src/constants.ts
-2
@@ -26,5 +26,3 @@ export const FIXTURES_PATH = path.join(
26 'compiler',
27 );
28 export const SNAPSHOT_EXTENSION = '.expect.md';
29 -export const FILTER_FILENAME = 'testfilter.txt';
30 -export const FILTER_PATH = path.join(PROJECT_ROOT, FILTER_FILENAME);
compiler/packages/snap/src/fixture-utils.ts
+1 -42
@@ -8,7 +8,7 @@
8 import fs from 'fs/promises';
9 import * as glob from 'glob';
10 import path from 'path';
11 -import {FILTER_PATH, FIXTURES_PATH, SNAPSHOT_EXTENSION} from './constants';
11 +import {FIXTURES_PATH, SNAPSHOT_EXTENSION} from './constants';
12
13 const INPUT_EXTENSIONS = [
14 '.js',
@@ -22,19 +22,9 @@ const INPUT_EXTENSIONS = [
22 ];
23
24 export type TestFilter = {
25 - debug: boolean;
25 paths: Array<string>;
26 };
27
29 -async function exists(file: string): Promise<boolean> {
30 - try {
31 - await fs.access(file);
32 - return true;
33 - } catch {
34 - return false;
35 - }
36 -}
37 -
28 function stripExtension(filename: string, extensions: Array<string>): string {
29 for (const ext of extensions) {
30 if (filename.endsWith(ext)) {
@@ -44,37 +34,6 @@ function stripExtension(filename: string, extensions: Array<string>): string {
34 return filename;
35 }
36
47 -export async function readTestFilter(): Promise<TestFilter | null> {
48 - if (!(await exists(FILTER_PATH))) {
49 - throw new Error(`testfilter file not found at \`${FILTER_PATH}\``);
50 - }
51 -
52 - const input = await fs.readFile(FILTER_PATH, 'utf8');
53 - const lines = input.trim().split('\n');
54 -
55 - let debug: boolean = false;
56 - const line0 = lines[0];
57 - if (line0 != null) {
58 - // Try to parse pragmas
59 - let consumedLine0 = false;
60 - if (line0.indexOf('@only') !== -1) {
61 - consumedLine0 = true;
62 - }
63 - if (line0.indexOf('@debug') !== -1) {
64 - debug = true;
65 - consumedLine0 = true;
66 - }
67 -
68 - if (consumedLine0) {
69 - lines.shift();
70 - }
71 - }
72 - return {
73 - debug,
74 - paths: lines.filter(line => !line.trimStart().startsWith('//')),
75 - };
76 -}
77 -
37 export function getBasename(fixture: TestFixture): string {
38 return stripExtension(path.basename(fixture.inputPath), INPUT_EXTENSIONS);
39 }
compiler/packages/snap/src/runner-watch.ts
+78 -30
@@ -8,8 +8,8 @@
8 import watcher from '@parcel/watcher';
9 import path from 'path';
10 import ts from 'typescript';
11 -import {FILTER_FILENAME, FIXTURES_PATH, PROJECT_ROOT} from './constants';
12 -import {TestFilter, readTestFilter} from './fixture-utils';
11 +import {FIXTURES_PATH, PROJECT_ROOT} from './constants';
12 +import {TestFilter} from './fixture-utils';
13 import {execSync} from 'child_process';
14
15 export function watchSrc(
@@ -117,6 +117,10 @@ export type RunnerState = {
117 lastUpdate: number;
118 mode: RunnerMode;
119 filter: TestFilter | null;
120 + debug: boolean;
121 + // Input mode for interactive pattern entry
122 + inputMode: 'none' | 'pattern';
123 + inputBuffer: string;
124 };
125
126 function subscribeFixtures(
@@ -142,26 +146,6 @@ function subscribeFixtures(
146 });
147 }
148
145 -function subscribeFilterFile(
146 - state: RunnerState,
147 - onChange: (state: RunnerState) => void,
148 -) {
149 - watcher.subscribe(PROJECT_ROOT, async (err, events) => {
150 - if (err) {
151 - console.error(err);
152 - process.exit(1);
153 - } else if (
154 - events.findIndex(event => event.path.includes(FILTER_FILENAME)) !== -1
155 - ) {
156 - if (state.mode.filter) {
157 - state.filter = await readTestFilter();
158 - state.mode.action = RunnerAction.Test;
159 - onChange(state);
160 - }
161 - }
162 - });
163 -}
164 -
149 function subscribeTsc(
150 state: RunnerState,
151 onChange: (state: RunnerState) => void,
@@ -200,15 +184,67 @@ function subscribeKeyEvents(
184 onChange: (state: RunnerState) => void,
185 ) {
186 process.stdin.on('keypress', async (str, key) => {
187 + // Handle input mode (pattern entry)
188 + if (state.inputMode !== 'none') {
189 + if (key.name === 'return') {
190 + // Enter pressed - process input
191 + const pattern = state.inputBuffer.trim();
192 + state.inputMode = 'none';
193 + state.inputBuffer = '';
194 + process.stdout.write('\n');
195 +
196 + if (pattern !== '') {
197 + // Set the pattern as filter
198 + state.filter = {paths: [pattern]};
199 + state.mode.filter = true;
200 + state.mode.action = RunnerAction.Test;
201 + onChange(state);
202 + }
203 + // If empty, just exit input mode without changes
204 + return;
205 + } else if (key.name === 'escape') {
206 + // Cancel input mode
207 + state.inputMode = 'none';
208 + state.inputBuffer = '';
209 + process.stdout.write(' (cancelled)\n');
210 + return;
211 + } else if (key.name === 'backspace') {
212 + if (state.inputBuffer.length > 0) {
213 + state.inputBuffer = state.inputBuffer.slice(0, -1);
214 + // Erase character: backspace, space, backspace
215 + process.stdout.write('\b \b');
216 + }
217 + return;
218 + } else if (str && !key.ctrl && !key.meta) {
219 + // Regular character - accumulate and echo
220 + state.inputBuffer += str;
221 + process.stdout.write(str);
222 + return;
223 + }
224 + return; // Ignore other keys in input mode
225 + }
226 +
227 + // Normal mode keypress handling
228 if (key.name === 'u') {
229 // u => update fixtures
230 state.mode.action = RunnerAction.Update;
231 } else if (key.name === 'q') {
232 process.exit(0);
208 - } else if (key.name === 'f') {
209 - state.mode.filter = !state.mode.filter;
210 - state.filter = state.mode.filter ? await readTestFilter() : null;
233 + } else if (key.name === 'a') {
234 + // a => exit filter mode and run all tests
235 + state.mode.filter = false;
236 + state.filter = null;
237 + state.mode.action = RunnerAction.Test;
238 + } else if (key.name === 'd') {
239 + // d => toggle debug logging
240 + state.debug = !state.debug;
241 state.mode.action = RunnerAction.Test;
242 + } else if (key.name === 'p') {
243 + // p => enter pattern input mode
244 + state.inputMode = 'pattern';
245 + state.inputBuffer = '';
246 + process.stdout.write('Pattern: ');
247 + return; // Don't trigger onChange yet
248 } else {
249 // any other key re-runs tests
250 state.mode.action = RunnerAction.Test;
@@ -219,21 +255,33 @@ function subscribeKeyEvents(
255
256 export async function makeWatchRunner(
257 onChange: (state: RunnerState) => void,
222 - filterMode: boolean,
258 + debugMode: boolean,
259 + initialPattern?: string,
260 ): Promise<void> {
224 - const state = {
261 + // Determine initial filter state
262 + let filter: TestFilter | null = null;
263 + let filterEnabled = false;
264 +
265 + if (initialPattern) {
266 + filter = {paths: [initialPattern]};
267 + filterEnabled = true;
268 + }
269 +
270 + const state: RunnerState = {
271 compilerVersion: 0,
272 isCompilerBuildValid: false,
273 lastUpdate: -1,
274 mode: {
275 action: RunnerAction.Test,
230 - filter: filterMode,
276 + filter: filterEnabled,
277 },
232 - filter: filterMode ? await readTestFilter() : null,
278 + filter,
279 + debug: debugMode,
280 + inputMode: 'none',
281 + inputBuffer: '',
282 };
283
284 subscribeTsc(state, onChange);
285 subscribeFixtures(state, onChange);
286 subscribeKeyEvents(state, onChange);
238 - subscribeFilterFile(state, onChange);
287 }
compiler/packages/snap/src/runner.ts
+30 -30
@@ -12,8 +12,8 @@ import * as readline from 'readline';
12 import ts from 'typescript';
13 import yargs from 'yargs';
14 import {hideBin} from 'yargs/helpers';
15 -import {FILTER_PATH, PROJECT_ROOT} from './constants';
16 -import {TestFilter, getFixtures, readTestFilter} from './fixture-utils';
15 +import {PROJECT_ROOT} from './constants';
16 +import {TestFilter, getFixtures} from './fixture-utils';
17 import {TestResult, TestResults, report, update} from './reporter';
18 import {
19 RunnerAction,
@@ -33,9 +33,9 @@ type RunnerOptions = {
33 sync: boolean;
34 workerThreads: boolean;
35 watch: boolean;
36 - filter: boolean;
36 update: boolean;
37 pattern?: string;
38 + debug: boolean;
39 };
40
41 const opts: RunnerOptions = yargs
@@ -59,18 +59,16 @@ const opts: RunnerOptions = yargs
59 .alias('u', 'update')
60 .describe('update', 'Update fixtures')
61 .default('update', false)
62 - .boolean('filter')
63 - .describe(
64 - 'filter',
65 - 'Only run fixtures which match the contents of testfilter.txt',
66 - )
67 - .default('filter', false)
62 .string('pattern')
63 .alias('p', 'pattern')
64 .describe(
65 'pattern',
66 'Optional glob pattern to filter fixtures (e.g., "error.*", "use-memo")',
67 )
68 + .boolean('debug')
69 + .alias('d', 'debug')
70 + .describe('debug', 'Enable debug logging to print HIR for each pass')
71 + .default('debug', false)
72 .help('help')
73 .strict()
74 .parseSync(hideBin(process.argv)) as RunnerOptions;
@@ -82,12 +80,15 @@ async function runFixtures(
80 worker: Worker & typeof runnerWorker,
81 filter: TestFilter | null,
82 compilerVersion: number,
83 + debug: boolean,
84 + requireSingleFixture: boolean,
85 ): Promise<TestResults> {
86 // We could in theory be fancy about tracking the contents of the fixtures
87 // directory via our file subscription, but it's simpler to just re-read
88 // the directory each time.
89 const fixtures = await getFixtures(filter);
90 const isOnlyFixture = filter !== null && fixtures.size === 1;
91 + const shouldLog = debug && (!requireSingleFixture || isOnlyFixture);
92
93 let entries: Array<[string, TestResult]>;
94 if (!opts.sync) {
@@ -96,12 +97,7 @@ async function runFixtures(
97 for (const [fixtureName, fixture] of fixtures) {
98 work.push(
99 worker
99 - .transformFixture(
100 - fixture,
101 - compilerVersion,
102 - (filter?.debug ?? false) && isOnlyFixture,
103 - true,
104 - )
100 + .transformFixture(fixture, compilerVersion, shouldLog, true)
101 .then(result => [fixtureName, result]),
102 );
103 }
@@ -113,7 +109,7 @@ async function runFixtures(
109 let output = await runnerWorker.transformFixture(
110 fixture,
111 compilerVersion,
116 - (filter?.debug ?? false) && isOnlyFixture,
112 + shouldLog,
113 true,
114 );
115 entries.push([fixtureName, output]);
@@ -128,7 +124,7 @@ async function onChange(
124 worker: Worker & typeof runnerWorker,
125 state: RunnerState,
126 ) {
131 - const {compilerVersion, isCompilerBuildValid, mode, filter} = state;
127 + const {compilerVersion, isCompilerBuildValid, mode, filter, debug} = state;
128 if (isCompilerBuildValid) {
129 const start = performance.now();
130
@@ -142,6 +138,8 @@ async function onChange(
138 worker,
139 mode.filter ? filter : null,
140 compilerVersion,
141 + debug,
142 + true, // requireSingleFixture in watch mode
143 );
144 const end = performance.now();
145 if (mode.action === RunnerAction.Update) {
@@ -159,11 +157,13 @@ async function onChange(
157 console.log(
158 '\n' +
159 (mode.filter
162 - ? `Current mode = FILTER, filter test fixtures by "${FILTER_PATH}".`
160 + ? `Current mode = FILTER, pattern = "${filter?.paths[0] ?? ''}".`
161 : 'Current mode = NORMAL, run all test fixtures.') +
162 '\nWaiting for input or file changes...\n' +
163 'u - update all fixtures\n' +
166 - `f - toggle (turn ${mode.filter ? 'off' : 'on'}) filter mode\n` +
164 + `d - toggle (turn ${debug ? 'off' : 'on'}) debug logging\n` +
165 + 'p - enter pattern to filter fixtures\n' +
166 + (mode.filter ? 'a - run all tests (exit filter mode)\n' : '') +
167 'q - quit\n' +
168 '[any] - rerun tests\n',
169 );
@@ -180,15 +180,12 @@ export async function main(opts: RunnerOptions): Promise<void> {
180 worker.getStderr().pipe(process.stderr);
181 worker.getStdout().pipe(process.stdout);
182
183 - // If pattern is provided, force watch mode off and use pattern filter
184 - const shouldWatch = opts.watch && opts.pattern == null;
185 - if (opts.watch && opts.pattern != null) {
186 - console.warn('NOTE: --watch is ignored when a --pattern is supplied');
187 - }
183 + // Check if watch mode should be enabled
184 + const shouldWatch = opts.watch;
185
186 if (shouldWatch) {
190 - makeWatchRunner(state => onChange(worker, state), opts.filter);
191 - if (opts.filter) {
187 + makeWatchRunner(state => onChange(worker, state), opts.debug, opts.pattern);
188 + if (opts.pattern) {
189 /**
190 * Warm up wormers when in watch mode. Loading the Forget babel plugin
191 * and all of its transitive dependencies takes 1-3s (per worker) on a M1.
@@ -236,14 +233,17 @@ export async function main(opts: RunnerOptions): Promise<void> {
233 let testFilter: TestFilter | null = null;
234 if (opts.pattern) {
235 testFilter = {
239 - debug: true,
236 paths: [opts.pattern],
237 };
242 - } else if (opts.filter) {
243 - testFilter = await readTestFilter();
238 }
239
246 - const results = await runFixtures(worker, testFilter, 0);
240 + const results = await runFixtures(
241 + worker,
242 + testFilter,
243 + 0,
244 + opts.debug,
245 + false, // no requireSingleFixture in non-watch mode
246 + );
247 if (opts.update) {
248 update(results);
249 isSuccess = true;