@samitouri / QOS-React-2 / commits / ea4899e13f

[compiler][snap] Support pattern of files to test as CLI argument (#35148)

I've been trying out LLM agents for compiler development, and one thing i found is that the agent naturally wants to run `yarn snap <pattern>` to test a specific fixture, and I want to be able to tell it (directly or in rules/skills) to do this in order to get the debug output from all the compiler passes. Agents can figure out our current testfilter.txt file system but that's just tedious. So here we add support for `yarn snap -p <pattern>`. If you pass in a pattern with an extension, we target that extension specifically. If you pass in a .expect.md file, we look at that specific fixture. And if the pattern doesn't have extensions, we search for `<pattern>{.js,.jsx,.ts,.tsx}`. When patterns are enabled we automatically log as in debug mode (if there is a single match), and disable watch mode. Open to feedback!

Joseph Savona committed Nov 17, 2025 at 12:09 UTC ea4899e13f9e29815321e3cac70fa08bb8ed790a
2 files changed +66 -11
compiler/packages/snap/src/fixture-utils.ts
+39 -8
@@ -44,6 +44,21 @@ function stripExtension(filename: string, extensions: Array<string>): string {
44 return filename;
45 }
46
47 +/**
48 + * Strip all extensions from a filename
49 + * e.g., "foo.expect.md" -> "foo"
50 + */
51 +function stripAllExtensions(filename: string): string {
52 + let result = filename;
53 + while (true) {
54 + const extension = path.extname(result);
55 + if (extension === '') {
56 + return result;
57 + }
58 + result = path.basename(result, extension);
59 + }
60 +}
61 +
62 export async function readTestFilter(): Promise<TestFilter | null> {
63 if (!(await exists(FILTER_PATH))) {
64 throw new Error(`testfilter file not found at \`${FILTER_PATH}\``);
@@ -111,11 +126,25 @@ async function readInputFixtures(
126 } else {
127 inputFiles = (
128 await Promise.all(
114 - filter.paths.map(pattern =>
115 - glob.glob(`${pattern}{${INPUT_EXTENSIONS.join(',')}}`, {
129 + filter.paths.map(pattern => {
130 + // If the pattern already has an extension other than .expect.md,
131 + // search for the pattern directly. Otherwise, search for the
132 + // pattern with the expected input extensions added.
133 + // Eg
134 + // `alias-while` => search for `alias-while{.js,.jsx,.ts,.tsx}`
135 + // `alias-while.js` => search as-is
136 + // `alias-while.expect.md` => search for `alias-while{.js,.jsx,.ts,.tsx}`
137 + const basename = path.basename(pattern);
138 + const basenameWithoutExt = stripAllExtensions(basename);
139 + const hasExtension = basename !== basenameWithoutExt;
140 + const globPattern =
141 + hasExtension && !pattern.endsWith(SNAPSHOT_EXTENSION)
142 + ? pattern
143 + : `${basenameWithoutExt}{${INPUT_EXTENSIONS.join(',')}}`;
144 + return glob.glob(globPattern, {
145 cwd: rootDir,
117 - }),
118 - ),
146 + });
147 + }),
148 )
149 ).flat();
150 }
@@ -150,11 +179,13 @@ async function readOutputFixtures(
179 } else {
180 outputFiles = (
181 await Promise.all(
153 - filter.paths.map(pattern =>
154 - glob.glob(`${pattern}${SNAPSHOT_EXTENSION}`, {
182 + filter.paths.map(pattern => {
183 + // Strip all extensions and find matching .expect.md files
184 + const basenameWithoutExt = stripAllExtensions(pattern);
185 + return glob.glob(`${basenameWithoutExt}${SNAPSHOT_EXTENSION}`, {
186 cwd: rootDir,
156 - }),
157 - ),
187 + });
188 + }),
189 )
190 ).flat();
191 }
compiler/packages/snap/src/runner.ts
+27 -3
@@ -35,6 +35,7 @@ type RunnerOptions = {
35 watch: boolean;
36 filter: boolean;
37 update: boolean;
38 + pattern?: string;
39 };
40
41 const opts: RunnerOptions = yargs
@@ -62,9 +63,15 @@ const opts: RunnerOptions = yargs
63 'Only run fixtures which match the contents of testfilter.txt',
64 )
65 .default('filter', false)
66 + .string('pattern')
67 + .alias('p', 'pattern')
68 + .describe(
69 + 'pattern',
70 + 'Optional glob pattern to filter fixtures (e.g., "error.*", "use-memo")',
71 + )
72 .help('help')
73 .strict()
67 - .parseSync(hideBin(process.argv));
74 + .parseSync(hideBin(process.argv)) as RunnerOptions;
75
76 /**
77 * Do a test run and return the test results
@@ -171,7 +178,13 @@ export async function main(opts: RunnerOptions): Promise<void> {
178 worker.getStderr().pipe(process.stderr);
179 worker.getStdout().pipe(process.stdout);
180
174 - if (opts.watch) {
181 + // If pattern is provided, force watch mode off and use pattern filter
182 + const shouldWatch = opts.watch && opts.pattern == null;
183 + if (opts.watch && opts.pattern != null) {
184 + console.warn('NOTE: --watch is ignored when a --pattern is supplied');
185 + }
186 +
187 + if (shouldWatch) {
188 makeWatchRunner(state => onChange(worker, state), opts.filter);
189 if (opts.filter) {
190 /**
@@ -216,7 +229,18 @@ export async function main(opts: RunnerOptions): Promise<void> {
229 try {
230 execSync('yarn build', {cwd: PROJECT_ROOT});
231 console.log('Built compiler successfully with tsup');
219 - const testFilter = opts.filter ? await readTestFilter() : null;
232 +
233 + // Determine which filter to use
234 + let testFilter: TestFilter | null = null;
235 + if (opts.pattern) {
236 + testFilter = {
237 + debug: true,
238 + paths: [opts.pattern],
239 + };
240 + } else if (opts.filter) {
241 + testFilter = await readTestFilter();
242 + }
243 +
244 const results = await runFixtures(worker, testFilter, 0);
245 if (opts.update) {
246 update(results);