[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()