@samitouri / QOS-React-2 / commits / 3ce1316b05

[compiler][snap] Fixes to relative path resolution; compile subcommand (#35688)

More snap improvements for use with agents: * `yarn snap compile [--debug] <path>` for compiling any file, optionally with debug logs * `yarn snap minimize <path>` now accepts path as a positional param for consistency w 'compile' command * Both compile/minimize commands properly handle paths relative to the compiler/ directory. When using `yarn snap` the current working directory is compiler/packages/snap, but you're generally running it from the compiler directory so this matches expectations of callers better.

Joseph Savona committed Feb 3, 2026 at 22:12 UTC 3ce1316b05968d2a8cffe42a110f2726f2c44c3e
9 files changed +238 -24
compiler/.claude/agents/investigate-error.md
+1 -1
@@ -13,7 +13,7 @@ You are an expert React Compiler debugging specialist with deep knowledge of com
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.
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. You can also use `yarn snap compile -d <path-to-fixture>`.
17
18 ### Step 3: Analyze Compilation Results
19
compiler/CLAUDE.md
+25
@@ -35,6 +35,31 @@ yarn snap -p <file-basename> -d
35 yarn snap -u
36 ```
37
38 +## Compiling Arbitrary Files
39 +
40 +Use `yarn snap compile` to compile any file (not just fixtures) with the React Compiler:
41 +
42 +```bash
43 +# Compile a file and see the output
44 +yarn snap compile <path>
45 +
46 +# Compile with debug logging to see the state after each compiler pass
47 +# This is an alternative to `yarn snap -d -p <pattern>` when you don't have a fixture file yet
48 +yarn snap compile --debug <path>
49 +```
50 +
51 +## Minimizing Test Cases
52 +
53 +Use `yarn snap minimize` to automatically reduce a failing test case to its minimal reproduction:
54 +
55 +```bash
56 +# Minimize a file that causes a compiler error
57 +yarn snap minimize <path>
58 +
59 +# Minimize and update the file in-place with the minimized version
60 +yarn snap minimize --update <path>
61 +```
62 +
63 ## Version Control
64
65 This repository uses Sapling (`sl`) for version control. Sapling is similar to Mercurial: there is not staging area, but new/deleted files must be explicitlyu added/removed.
compiler/docs/DEVELOPMENT_GUIDE.md
+26 -1
@@ -17,7 +17,32 @@ yarn snap:build
17 yarn snap --watch
18 ```
19
20 -`snap` is our custom test runner, which creates "golden" test files that have the expected output for each input fixture, as well as the results of executing a specific input (or sequence of inputs) in both the uncompiled and compiler versions of the input.
20 +`snap` is our custom test runner, which creates "golden" test files that have the expected output for each input fixture, as well as the results of executing a specific input (or sequence of inputs) in both the uncompiled and compiler versions of the input.
21 +
22 +### Compiling Arbitrary Files
23 +
24 +You can compile any file (not just fixtures) using:
25 +
26 +```sh
27 +# Compile a file and see the output
28 +yarn snap compile <path>
29 +
30 +# Compile with debug output to see the state after each compiler pass
31 +# This is an alternative to `yarn snap -d -p <pattern>` when you don't have a fixture file yet
32 +yarn snap compile --debug <path>
33 +```
34 +
35 +### Minimizing Test Cases
36 +
37 +To reduce a failing test case to its minimal reproduction:
38 +
39 +```sh
40 +# Minimize a file that causes a compiler error
41 +yarn snap minimize <path>
42 +
43 +# Minimize and update the file in-place
44 +yarn snap minimize --update <path>
45 +```
46
47 When contributing changes, we prefer to:
48 * Add one or more fixtures that demonstrate the current compiled output for a particular combination of input and configuration. Send this as a first PR.
compiler/packages/babel-plugin-react-compiler/docs/passes/README.md
+9
@@ -294,6 +294,15 @@ yarn snap -p <fixture-name>
294 # Run with debug output (shows all passes)
295 yarn snap -p <fixture-name> -d
296
297 +# Compile any file (not just fixtures) and see output
298 +yarn snap compile <path>
299 +
300 +# Compile any file with debug output (alternative to yarn snap -d -p when you don't have a fixture)
301 +yarn snap compile --debug <path>
302 +
303 +# Minimize a failing test case to its minimal reproduction
304 +yarn snap minimize <path>
305 +
306 # Update expected outputs
307 yarn snap -u
308 ```
compiler/packages/snap/src/constants.ts
+7 -5
@@ -7,19 +7,21 @@
7
8 import path from 'path';
9
10 +export const PROJECT_ROOT = path.join(process.cwd(), '..', '..');
11 +
12 // We assume this is run from `babel-plugin-react-compiler`
11 -export const PROJECT_ROOT = path.normalize(
12 - path.join(process.cwd(), '..', 'babel-plugin-react-compiler'),
13 +export const BABEL_PLUGIN_ROOT = path.normalize(
14 + path.join(PROJECT_ROOT, 'packages', 'babel-plugin-react-compiler'),
15 );
16
15 -export const PROJECT_SRC = path.normalize(
16 - path.join(PROJECT_ROOT, 'dist', 'index.js'),
17 +export const BABEL_PLUGIN_SRC = path.normalize(
18 + path.join(BABEL_PLUGIN_ROOT, 'dist', 'index.js'),
19 );
20 export const PRINT_HIR_IMPORT = 'printFunctionWithOutlined';
21 export const PRINT_REACTIVE_IR_IMPORT = 'printReactiveFunction';
22 export const PARSE_CONFIG_PRAGMA_IMPORT = 'parseConfigPragmaForTests';
23 export const FIXTURES_PATH = path.join(
22 - PROJECT_ROOT,
24 + BABEL_PLUGIN_ROOT,
25 'src',
26 '__tests__',
27 'fixtures',
compiler/packages/snap/src/minimize.ts
+2 -2
@@ -12,7 +12,7 @@ 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 {parseInput} from './compiler.js';
15 -import {PARSE_CONFIG_PRAGMA_IMPORT, PROJECT_SRC} from './constants.js';
15 +import {PARSE_CONFIG_PRAGMA_IMPORT, BABEL_PLUGIN_SRC} from './constants.js';
16
17 type CompileSuccess = {kind: 'success'};
18 type CompileParseError = {kind: 'parse_error'; message: string};
@@ -1919,7 +1919,7 @@ export function minimize(
1919 sourceType: 'module' | 'script',
1920 ): MinimizeResult {
1921 // Load the compiler plugin
1922 - const importedCompilerPlugin = require(PROJECT_SRC) as Record<
1922 + const importedCompilerPlugin = require(BABEL_PLUGIN_SRC) as Record<
1923 string,
1924 unknown
1925 >;
compiler/packages/snap/src/runner-watch.ts
+3 -3
@@ -8,7 +8,7 @@
8 import watcher from '@parcel/watcher';
9 import path from 'path';
10 import ts from 'typescript';
11 -import {FIXTURES_PATH, PROJECT_ROOT} from './constants';
11 +import {FIXTURES_PATH, BABEL_PLUGIN_ROOT} from './constants';
12 import {TestFilter, getFixtures} from './fixture-utils';
13 import {execSync} from 'child_process';
14
@@ -17,7 +17,7 @@ export function watchSrc(
17 onComplete: (isSuccess: boolean) => void,
18 ): ts.WatchOfConfigFile<ts.SemanticDiagnosticsBuilderProgram> {
19 const configPath = ts.findConfigFile(
20 - /*searchPath*/ PROJECT_ROOT,
20 + /*searchPath*/ BABEL_PLUGIN_ROOT,
21 ts.sys.fileExists,
22 'tsconfig.json',
23 );
@@ -166,7 +166,7 @@ function subscribeTsc(
166 let isCompilerBuildValid = false;
167 if (isTypecheckSuccess) {
168 try {
169 - execSync('yarn build', {cwd: PROJECT_ROOT});
169 + execSync('yarn build', {cwd: BABEL_PLUGIN_ROOT});
170 console.log('Built compiler successfully with tsup');
171 isCompilerBuildValid = true;
172 } catch (e) {
compiler/packages/snap/src/runner-worker.ts
+2 -3
@@ -5,7 +5,6 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import {codeFrameColumns} from '@babel/code-frame';
8 import type {PluginObj} from '@babel/core';
9 import type {parseConfigPragmaForTests as ParseConfigPragma} from 'babel-plugin-react-compiler/src/Utils/TestUtils';
10 import type {printFunctionWithOutlined as PrintFunctionWithOutlined} from 'babel-plugin-react-compiler/src/HIR/PrintHIR';
@@ -15,7 +14,7 @@ import {
14 PARSE_CONFIG_PRAGMA_IMPORT,
15 PRINT_HIR_IMPORT,
16 PRINT_REACTIVE_IR_IMPORT,
18 - PROJECT_SRC,
17 + BABEL_PLUGIN_SRC,
18 } from './constants';
19 import {TestFixture, getBasename, isExpectError} from './fixture-utils';
20 import {TestResult, writeOutputToString} from './reporter';
@@ -65,7 +64,7 @@ async function compile(
64 let compileResult: TransformResult | null = null;
65 let error: string | null = null;
66 try {
68 - const importedCompilerPlugin = require(PROJECT_SRC) as Record<
67 + const importedCompilerPlugin = require(BABEL_PLUGIN_SRC) as Record<
68 string,
69 unknown
70 >;
compiler/packages/snap/src/runner.ts
+163 -9
@@ -12,7 +12,7 @@ import * as readline from 'readline';
12 import ts from 'typescript';
13 import yargs from 'yargs';
14 import {hideBin} from 'yargs/helpers';
15 -import {PROJECT_ROOT} from './constants';
15 +import {BABEL_PLUGIN_ROOT, PROJECT_ROOT} from './constants';
16 import {TestFilter, getFixtures} from './fixture-utils';
17 import {TestResult, TestResults, report, update} from './reporter';
18 import {
@@ -26,7 +26,14 @@ import {execSync} from 'child_process';
26 import fs from 'fs';
27 import path from 'path';
28 import {minimize} from './minimize';
29 -import {parseLanguage, parseSourceType} from './compiler';
29 +import {parseInput, parseLanguage, parseSourceType} from './compiler';
30 +import {
31 + PARSE_CONFIG_PRAGMA_IMPORT,
32 + PRINT_HIR_IMPORT,
33 + PRINT_REACTIVE_IR_IMPORT,
34 + BABEL_PLUGIN_SRC,
35 +} from './constants';
36 +import chalk from 'chalk';
37
38 const WORKER_PATH = require.resolve('./runner-worker.js');
39 const NUM_WORKERS = cpus().length - 1;
@@ -48,6 +55,11 @@ type MinimizeOptions = {
55 update: boolean;
56 };
57
58 +type CompileOptions = {
59 + path: string;
60 + debug: boolean;
61 +};
62 +
63 async function runTestCommand(opts: TestOptions): Promise<void> {
64 const worker: Worker & typeof runnerWorker = new Worker(WORKER_PATH, {
65 enableWorkerThreads: opts.workerThreads,
@@ -106,7 +118,7 @@ async function runTestCommand(opts: TestOptions): Promise<void> {
118 );
119 } else {
120 try {
109 - execSync('yarn build', {cwd: PROJECT_ROOT});
121 + execSync('yarn build', {cwd: BABEL_PLUGIN_ROOT});
122 console.log('Built compiler successfully with tsup');
123
124 // Determine which filter to use
@@ -147,7 +159,7 @@ async function runMinimizeCommand(opts: MinimizeOptions): Promise<void> {
159 // Resolve the input path
160 const inputPath = path.isAbsolute(opts.path)
161 ? opts.path
150 - : path.resolve(process.cwd(), opts.path);
162 + : path.resolve(PROJECT_ROOT, opts.path);
163
164 // Check if file exists
165 if (!fs.existsSync(inputPath)) {
@@ -196,6 +208,128 @@ async function runMinimizeCommand(opts: MinimizeOptions): Promise<void> {
208 }
209 }
210
211 +async function runCompileCommand(opts: CompileOptions): Promise<void> {
212 + // Resolve the input path
213 + const inputPath = path.isAbsolute(opts.path)
214 + ? opts.path
215 + : path.resolve(PROJECT_ROOT, opts.path);
216 +
217 + // Check if file exists
218 + if (!fs.existsSync(inputPath)) {
219 + console.error(`Error: File not found: ${inputPath}`);
220 + process.exit(1);
221 + }
222 +
223 + // Read the input file
224 + const input = fs.readFileSync(inputPath, 'utf-8');
225 + const filename = path.basename(inputPath);
226 + const firstLine = input.substring(0, input.indexOf('\n'));
227 + const language = parseLanguage(firstLine);
228 + const sourceType = parseSourceType(firstLine);
229 +
230 + // Import the compiler
231 + const importedCompilerPlugin = require(BABEL_PLUGIN_SRC) as Record<
232 + string,
233 + any
234 + >;
235 + const BabelPluginReactCompiler = importedCompilerPlugin['default'];
236 + const parseConfigPragmaForTests =
237 + importedCompilerPlugin[PARSE_CONFIG_PRAGMA_IMPORT];
238 + const printFunctionWithOutlined = importedCompilerPlugin[PRINT_HIR_IMPORT];
239 + const printReactiveFunctionWithOutlined =
240 + importedCompilerPlugin[PRINT_REACTIVE_IR_IMPORT];
241 + const EffectEnum = importedCompilerPlugin['Effect'];
242 + const ValueKindEnum = importedCompilerPlugin['ValueKind'];
243 + const ValueReasonEnum = importedCompilerPlugin['ValueReason'];
244 +
245 + // Setup debug logger
246 + let lastLogged: string | null = null;
247 + const debugIRLogger = opts.debug
248 + ? (value: any) => {
249 + let printed: string;
250 + switch (value.kind) {
251 + case 'hir':
252 + printed = printFunctionWithOutlined(value.value);
253 + break;
254 + case 'reactive':
255 + printed = printReactiveFunctionWithOutlined(value.value);
256 + break;
257 + case 'debug':
258 + printed = value.value;
259 + break;
260 + case 'ast':
261 + printed = '(ast)';
262 + break;
263 + default:
264 + printed = String(value);
265 + }
266 +
267 + if (printed !== lastLogged) {
268 + lastLogged = printed;
269 + console.log(`${chalk.green(value.name)}:\n${printed}\n`);
270 + } else {
271 + console.log(`${chalk.blue(value.name)}: (no change)\n`);
272 + }
273 + }
274 + : () => {};
275 +
276 + // Parse the input
277 + let ast;
278 + try {
279 + ast = parseInput(input, filename, language, sourceType);
280 + } catch (e: any) {
281 + console.error(`Parse error: ${e.message}`);
282 + process.exit(1);
283 + }
284 +
285 + // Build plugin options
286 + const config = parseConfigPragmaForTests(firstLine, {compilationMode: 'all'});
287 + const options = {
288 + ...config,
289 + environment: {
290 + ...config.environment,
291 + },
292 + logger: {
293 + logEvent: () => {},
294 + debugLogIRs: debugIRLogger,
295 + },
296 + enableReanimatedCheck: false,
297 + };
298 +
299 + // Compile
300 + const {transformFromAstSync} = require('@babel/core');
301 + try {
302 + const result = transformFromAstSync(ast, input, {
303 + filename: '/' + filename,
304 + highlightCode: false,
305 + retainLines: true,
306 + compact: true,
307 + plugins: [[BabelPluginReactCompiler, options]],
308 + sourceType: 'module',
309 + ast: false,
310 + cloneInputAst: true,
311 + configFile: false,
312 + babelrc: false,
313 + });
314 +
315 + if (result?.code != null) {
316 + // Format the output
317 + const prettier = require('prettier');
318 + const formatted = await prettier.format(result.code, {
319 + semi: true,
320 + parser: language === 'typescript' ? 'babel-ts' : 'flow',
321 + });
322 + console.log(formatted);
323 + } else {
324 + console.error('Error: No code emitted from compiler');
325 + process.exit(1);
326 + }
327 + } catch (e: any) {
328 + console.error(e.message);
329 + process.exit(1);
330 + }
331 +}
332 +
333 yargs(hideBin(process.argv))
334 .command(
335 ['test', '$0'],
@@ -245,14 +379,15 @@ yargs(hideBin(process.argv))
379 },
380 )
381 .command(
248 - 'minimize',
382 + 'minimize <path>',
383 'Minimize a test case to reproduce a compiler error',
384 yargs => {
385 return yargs
252 - .string('path')
253 - .alias('p', 'path')
254 - .describe('path', 'Path to the file to minimize')
255 - .demandOption('path')
386 + .positional('path', {
387 + describe: 'Path to the file to minimize',
388 + type: 'string',
389 + demandOption: true,
390 + })
391 .boolean('update')
392 .alias('u', 'update')
393 .describe(
@@ -265,6 +400,25 @@ yargs(hideBin(process.argv))
400 await runMinimizeCommand(argv as unknown as MinimizeOptions);
401 },
402 )
403 + .command(
404 + 'compile <path>',
405 + 'Compile a file with the React Compiler',
406 + yargs => {
407 + return yargs
408 + .positional('path', {
409 + describe: 'Path to the file to compile',
410 + type: 'string',
411 + demandOption: true,
412 + })
413 + .boolean('debug')
414 + .alias('d', 'debug')
415 + .describe('debug', 'Enable debug logging to print HIR for each pass')
416 + .default('debug', false);
417 + },
418 + async argv => {
419 + await runCompileCommand(argv as unknown as CompileOptions);
420 + },
421 + )
422 .help('help')
423 .strict()
424 .demandCommand()