Skip to content

Commit 03613cd

Browse files
authored
[compiler] Improve snap usability (facebook#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). * facebook#35607 * facebook#35298 * facebook#35596 * facebook#35573 * facebook#35595 * facebook#35539 * __->__ facebook#35537 * facebook#35523
1 parent 2af6822 commit 03613cd

File tree

4 files changed

+109
-104
lines changed

4 files changed

+109
-104
lines changed

compiler/packages/snap/src/constants.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,3 @@ export const FIXTURES_PATH = path.join(
2626
'compiler',
2727
);
2828
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

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import fs from 'fs/promises';
99
import * as glob from 'glob';
1010
import path from 'path';
11-
import {FILTER_PATH, FIXTURES_PATH, SNAPSHOT_EXTENSION} from './constants';
11+
import {FIXTURES_PATH, SNAPSHOT_EXTENSION} from './constants';
1212

1313
const INPUT_EXTENSIONS = [
1414
'.js',
@@ -22,19 +22,9 @@ const INPUT_EXTENSIONS = [
2222
];
2323

2424
export type TestFilter = {
25-
debug: boolean;
2625
paths: Array<string>;
2726
};
2827

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-
3828
function stripExtension(filename: string, extensions: Array<string>): string {
3929
for (const ext of extensions) {
4030
if (filename.endsWith(ext)) {
@@ -44,37 +34,6 @@ function stripExtension(filename: string, extensions: Array<string>): string {
4434
return filename;
4535
}
4636

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-
7837
export function getBasename(fixture: TestFixture): string {
7938
return stripExtension(path.basename(fixture.inputPath), INPUT_EXTENSIONS);
8039
}

compiler/packages/snap/src/runner-watch.ts

Lines changed: 78 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
import watcher from '@parcel/watcher';
99
import path from 'path';
1010
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';
1313
import {execSync} from 'child_process';
1414

1515
export function watchSrc(
@@ -117,6 +117,10 @@ export type RunnerState = {
117117
lastUpdate: number;
118118
mode: RunnerMode;
119119
filter: TestFilter | null;
120+
debug: boolean;
121+
// Input mode for interactive pattern entry
122+
inputMode: 'none' | 'pattern';
123+
inputBuffer: string;
120124
};
121125

122126
function subscribeFixtures(
@@ -142,26 +146,6 @@ function subscribeFixtures(
142146
});
143147
}
144148

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-
165149
function subscribeTsc(
166150
state: RunnerState,
167151
onChange: (state: RunnerState) => void,
@@ -200,15 +184,67 @@ function subscribeKeyEvents(
200184
onChange: (state: RunnerState) => void,
201185
) {
202186
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
203228
if (key.name === 'u') {
204229
// u => update fixtures
205230
state.mode.action = RunnerAction.Update;
206231
} else if (key.name === 'q') {
207232
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;
211241
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
212248
} else {
213249
// any other key re-runs tests
214250
state.mode.action = RunnerAction.Test;
@@ -219,21 +255,33 @@ function subscribeKeyEvents(
219255

220256
export async function makeWatchRunner(
221257
onChange: (state: RunnerState) => void,
222-
filterMode: boolean,
258+
debugMode: boolean,
259+
initialPattern?: string,
223260
): 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 = {
225271
compilerVersion: 0,
226272
isCompilerBuildValid: false,
227273
lastUpdate: -1,
228274
mode: {
229275
action: RunnerAction.Test,
230-
filter: filterMode,
276+
filter: filterEnabled,
231277
},
232-
filter: filterMode ? await readTestFilter() : null,
278+
filter,
279+
debug: debugMode,
280+
inputMode: 'none',
281+
inputBuffer: '',
233282
};
234283

235284
subscribeTsc(state, onChange);
236285
subscribeFixtures(state, onChange);
237286
subscribeKeyEvents(state, onChange);
238-
subscribeFilterFile(state, onChange);
239287
}

compiler/packages/snap/src/runner.ts

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import * as readline from 'readline';
1212
import ts from 'typescript';
1313
import yargs from 'yargs';
1414
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';
1717
import {TestResult, TestResults, report, update} from './reporter';
1818
import {
1919
RunnerAction,
@@ -33,9 +33,9 @@ type RunnerOptions = {
3333
sync: boolean;
3434
workerThreads: boolean;
3535
watch: boolean;
36-
filter: boolean;
3736
update: boolean;
3837
pattern?: string;
38+
debug: boolean;
3939
};
4040

4141
const opts: RunnerOptions = yargs
@@ -59,18 +59,16 @@ const opts: RunnerOptions = yargs
5959
.alias('u', 'update')
6060
.describe('update', 'Update fixtures')
6161
.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)
6862
.string('pattern')
6963
.alias('p', 'pattern')
7064
.describe(
7165
'pattern',
7266
'Optional glob pattern to filter fixtures (e.g., "error.*", "use-memo")',
7367
)
68+
.boolean('debug')
69+
.alias('d', 'debug')
70+
.describe('debug', 'Enable debug logging to print HIR for each pass')
71+
.default('debug', false)
7472
.help('help')
7573
.strict()
7674
.parseSync(hideBin(process.argv)) as RunnerOptions;
@@ -82,12 +80,15 @@ async function runFixtures(
8280
worker: Worker & typeof runnerWorker,
8381
filter: TestFilter | null,
8482
compilerVersion: number,
83+
debug: boolean,
84+
requireSingleFixture: boolean,
8585
): Promise<TestResults> {
8686
// We could in theory be fancy about tracking the contents of the fixtures
8787
// directory via our file subscription, but it's simpler to just re-read
8888
// the directory each time.
8989
const fixtures = await getFixtures(filter);
9090
const isOnlyFixture = filter !== null && fixtures.size === 1;
91+
const shouldLog = debug && (!requireSingleFixture || isOnlyFixture);
9192

9293
let entries: Array<[string, TestResult]>;
9394
if (!opts.sync) {
@@ -96,12 +97,7 @@ async function runFixtures(
9697
for (const [fixtureName, fixture] of fixtures) {
9798
work.push(
9899
worker
99-
.transformFixture(
100-
fixture,
101-
compilerVersion,
102-
(filter?.debug ?? false) && isOnlyFixture,
103-
true,
104-
)
100+
.transformFixture(fixture, compilerVersion, shouldLog, true)
105101
.then(result => [fixtureName, result]),
106102
);
107103
}
@@ -113,7 +109,7 @@ async function runFixtures(
113109
let output = await runnerWorker.transformFixture(
114110
fixture,
115111
compilerVersion,
116-
(filter?.debug ?? false) && isOnlyFixture,
112+
shouldLog,
117113
true,
118114
);
119115
entries.push([fixtureName, output]);
@@ -128,7 +124,7 @@ async function onChange(
128124
worker: Worker & typeof runnerWorker,
129125
state: RunnerState,
130126
) {
131-
const {compilerVersion, isCompilerBuildValid, mode, filter} = state;
127+
const {compilerVersion, isCompilerBuildValid, mode, filter, debug} = state;
132128
if (isCompilerBuildValid) {
133129
const start = performance.now();
134130

@@ -142,6 +138,8 @@ async function onChange(
142138
worker,
143139
mode.filter ? filter : null,
144140
compilerVersion,
141+
debug,
142+
true, // requireSingleFixture in watch mode
145143
);
146144
const end = performance.now();
147145
if (mode.action === RunnerAction.Update) {
@@ -159,11 +157,13 @@ async function onChange(
159157
console.log(
160158
'\n' +
161159
(mode.filter
162-
? `Current mode = FILTER, filter test fixtures by "${FILTER_PATH}".`
160+
? `Current mode = FILTER, pattern = "${filter?.paths[0] ?? ''}".`
163161
: 'Current mode = NORMAL, run all test fixtures.') +
164162
'\nWaiting for input or file changes...\n' +
165163
'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' : '') +
167167
'q - quit\n' +
168168
'[any] - rerun tests\n',
169169
);
@@ -180,15 +180,12 @@ export async function main(opts: RunnerOptions): Promise<void> {
180180
worker.getStderr().pipe(process.stderr);
181181
worker.getStdout().pipe(process.stdout);
182182

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;
188185

189186
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) {
192189
/**
193190
* Warm up wormers when in watch mode. Loading the Forget babel plugin
194191
* 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> {
236233
let testFilter: TestFilter | null = null;
237234
if (opts.pattern) {
238235
testFilter = {
239-
debug: true,
240236
paths: [opts.pattern],
241237
};
242-
} else if (opts.filter) {
243-
testFilter = await readTestFilter();
244238
}
245239

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+
);
247247
if (opts.update) {
248248
update(results);
249249
isSuccess = true;

0 commit comments

Comments
 (0)