main
ts 724 lines 21.8 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 /**
9 * Compare TS and Rust React Compiler output on external production files.
10 *
11 * Given a configuration module and a source root directory, compiles files
12 * using the exact plugin options from the configuration, then compares code
13 * output and error events between the TS and Rust compilers. Files with
14 * differences can be copied as test fixtures for further investigation.
15 *
16 * Usage:
17 * npx tsx compiler/scripts/test-internal-files.ts <config-path> <source-root> [flags]
18 *
19 * Arguments:
20 * <config-path> Path to the compiler configuration module (JS file that
21 * exports getForgetConfiguration and config)
22 * <source-root> Root directory from which the config's source prefixes
23 * are resolved
24 *
25 * Flags:
26 * --limit N Max files to process (default: 0 = all)
27 * --pattern PAT Filter file paths (substring match)
28 * --project NAME Filter by project name from config
29 * --dry-run Compare only, skip fixture creation
30 * --no-color Disable ANSI color codes
31 */
32
33 import * as babel from '@babel/core';
34 import hermesParserPlugin from 'babel-plugin-syntax-hermes-parser';
35 import {execSync} from 'child_process';
36 import fs from 'fs';
37 import path from 'path';
38 import prettier from 'prettier';
39
40 const REPO_ROOT = path.resolve(__dirname, '../..');
41 const FIXTURE_DIR = path.join(
42 REPO_ROOT,
43 'compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/internal',
44 );
45
46 // --- Parse flags and positional args ---
47 const rawArgs = process.argv.slice(2);
48 const noColor = rawArgs.includes('--no-color') || !!process.env.NO_COLOR;
49 const dryRun = rawArgs.includes('--dry-run');
50 const limitIdx = rawArgs.indexOf('--limit');
51 const limitArg = limitIdx >= 0 ? parseInt(rawArgs[limitIdx + 1], 10) : 0;
52 const patternIdx = rawArgs.indexOf('--pattern');
53 const patternArg = patternIdx >= 0 ? rawArgs[patternIdx + 1] : null;
54 const projectIdx = rawArgs.indexOf('--project');
55 const projectArg = projectIdx >= 0 ? rawArgs[projectIdx + 1] : null;
56
57 // Collect flag indices to exclude from positional args
58 const flagValueIndices = new Set<number>();
59 if (limitIdx >= 0) flagValueIndices.add(limitIdx + 1);
60 if (patternIdx >= 0) flagValueIndices.add(patternIdx + 1);
61 if (projectIdx >= 0) flagValueIndices.add(projectIdx + 1);
62 const positional = rawArgs.filter(
63 (a, i) => !a.startsWith('--') && !flagValueIndices.has(i),
64 );
65
66 if (positional.length < 2) {
67 console.error(
68 'Usage: npx tsx compiler/scripts/test-internal-files.ts <config-path> <source-root> [flags]',
69 );
70 console.error('');
71 console.error('Arguments:');
72 console.error(' <config-path> Path to the compiler configuration module');
73 console.error(
74 ' <source-root> Root directory for resolving config source prefixes',
75 );
76 console.error('');
77 console.error('Flags:');
78 console.error(' --limit N Max files to process');
79 console.error(' --pattern PAT Filter file paths (substring match)');
80 console.error(' --project NAME Filter by project name from config');
81 console.error(' --dry-run Compare only, skip fixture creation');
82 console.error(' --no-color Disable ANSI color codes');
83 process.exit(1);
84 }
85
86 const configPath = path.resolve(positional[0]);
87 const sourceRoot = path.resolve(positional[1]);
88
89 // --- ANSI colors ---
90 const RED = noColor ? '' : '\x1b[0;31m';
91 const GREEN = noColor ? '' : '\x1b[0;32m';
92 const YELLOW = noColor ? '' : '\x1b[0;33m';
93 const BOLD = noColor ? '' : '\x1b[1m';
94 const DIM = noColor ? '' : '\x1b[2m';
95 const RESET = noColor ? '' : '\x1b[0m';
96
97 // --- Load config ---
98 if (!fs.existsSync(configPath)) {
99 console.error(`${RED}ERROR: Could not find config at ${configPath}${RESET}`);
100 process.exit(1);
101 }
102 if (!fs.existsSync(sourceRoot) || !fs.statSync(sourceRoot).isDirectory()) {
103 console.error(
104 `${RED}ERROR: Source root is not a valid directory: ${sourceRoot}${RESET}`,
105 );
106 process.exit(1);
107 }
108 const forgetConfig = require(configPath);
109 const {getForgetConfiguration, config: rawConfig} = forgetConfig;
110
111 // --- Build native module ---
112 const NATIVE_DIR = path.join(
113 REPO_ROOT,
114 'compiler/packages/babel-plugin-react-compiler-rust/native',
115 );
116 const NATIVE_NODE_PATH = path.join(NATIVE_DIR, 'index.node');
117
118 console.log('Building Rust native module...');
119 try {
120 execSync('~/.cargo/bin/cargo build -p react_compiler_napi', {
121 cwd: path.join(REPO_ROOT, 'compiler/crates'),
122 stdio: 'inherit',
123 shell: true,
124 });
125 } catch {
126 console.error(`${RED}ERROR: Failed to build Rust native module.${RESET}`);
127 process.exit(1);
128 }
129
130 const TARGET_DIR = path.join(REPO_ROOT, 'compiler/target/debug');
131 const dylib = fs.existsSync(
132 path.join(TARGET_DIR, 'libreact_compiler_napi.dylib'),
133 )
134 ? path.join(TARGET_DIR, 'libreact_compiler_napi.dylib')
135 : path.join(TARGET_DIR, 'libreact_compiler_napi.so');
136
137 if (!fs.existsSync(dylib)) {
138 console.error(
139 `${RED}ERROR: Could not find built native module in ${TARGET_DIR}${RESET}`,
140 );
141 process.exit(1);
142 }
143 fs.copyFileSync(dylib, NATIVE_NODE_PATH);
144
145 // --- Load plugins ---
146 const tsPlugin = require('../packages/babel-plugin-react-compiler/src').default;
147 const rustPlugin =
148 require('../packages/babel-plugin-react-compiler-rust/src').default;
149
150 // --- Types ---
151 interface LoggerEvent {
152 kind: string;
153 fnName?: string | null;
154 fnLoc?: unknown;
155 reason?: string;
156 detail?: {
157 reason?: string;
158 severity?: string;
159 category?: string;
160 description?: string;
161 };
162 data?: string;
163 }
164
165 interface CompileResult {
166 code: string | null;
167 events: LoggerEvent[];
168 error: string | null;
169 }
170
171 interface FileEntry {
172 filePath: string;
173 projectName: string;
174 projectConfig: Record<string, unknown>;
175 }
176
177 // --- File discovery ---
178 // Additional directory exclusions beyond what the config handles
179 const EXTRA_EXCLUDES = [
180 '__server_snapshot_tests__',
181 '__e2e_tests__',
182 '__perf_tests__',
183 '__integration_tests__',
184 ];
185
186 function walkDir(dir: string, callback: (filePath: string) => void): void {
187 let entries: fs.Dirent[];
188 try {
189 entries = fs.readdirSync(dir, {withFileTypes: true});
190 } catch {
191 return;
192 }
193 for (const entry of entries) {
194 if (EXTRA_EXCLUDES.includes(entry.name)) continue;
195 const fullPath = path.join(dir, entry.name);
196 if (entry.isDirectory()) {
197 walkDir(fullPath, callback);
198 } else if (/\.(js|jsx)$/.test(entry.name)) {
199 callback(fullPath);
200 }
201 }
202 }
203
204 function discoverFiles(): FileEntry[] {
205 console.log('Discovering files...');
206 const results: FileEntry[] = [];
207 const seen = new Set<string>();
208
209 for (const [prefix, projectName] of Object.entries(
210 rawConfig.sources as Record<string, string>,
211 )) {
212 if (projectArg && projectName !== projectArg) continue;
213
214 // Resolve prefix to absolute path relative to source root
215 const absPrefix = path.join(sourceRoot, prefix);
216 if (!fs.existsSync(absPrefix)) continue;
217
218 const stat = fs.statSync(absPrefix);
219 if (!stat.isDirectory()) continue;
220
221 walkDir(absPrefix, filePath => {
222 if (seen.has(filePath)) return;
223 seen.add(filePath);
224
225 if (patternArg && !filePath.includes(patternArg)) return;
226
227 const config = getForgetConfiguration(filePath);
228 if (config == null || !config.enable) return;
229
230 results.push({filePath, projectName, projectConfig: config});
231 });
232
233 if (limitArg > 0 && results.length >= limitArg) break;
234 }
235
236 if (limitArg > 0) {
237 return results.slice(0, limitArg);
238 }
239 return results;
240 }
241
242 // --- Build plugin options from production config ---
243 function makePluginOptions(
244 projectConfig: Record<string, unknown>,
245 logger: {logEvent: (filename: string | null, event: LoggerEvent) => void},
246 ): Record<string, unknown> {
247 return {
248 compilationMode: projectConfig.compilationMode,
249 panicThreshold: 'none',
250 environment: projectConfig.environment,
251 target:
252 projectConfig.compilerVersion === 'experimental'
253 ? projectConfig.target
254 : projectConfig.target,
255 gating: null,
256 flowSuppressions: projectConfig.flowSuppressions,
257 enableReanimatedCheck: false,
258 logger,
259 sources: null,
260 };
261 }
262
263 // --- Compile a file ---
264 function compileFile(
265 mode: 'ts' | 'rust',
266 filePath: string,
267 pluginOptions: Record<string, unknown>,
268 ): CompileResult {
269 const source = fs.readFileSync(filePath, 'utf8');
270 const events: LoggerEvent[] = [];
271
272 const logger = {
273 logEvent(_filename: string | null, event: LoggerEvent): void {
274 events.push(event);
275 },
276 };
277
278 const opts = {...pluginOptions, logger};
279 const plugin = mode === 'ts' ? tsPlugin : rustPlugin;
280
281 try {
282 const result = babel.transformSync(source, {
283 filename: filePath,
284 sourceType: 'module',
285 plugins: [hermesParserPlugin, [plugin, opts]],
286 configFile: false,
287 babelrc: false,
288 });
289 return {code: result?.code ?? null, events, error: null};
290 } catch (e) {
291 const msg = e instanceof Error ? e.message : String(e);
292 return {code: null, events, error: msg};
293 }
294 }
295
296 // --- Format events for comparison ---
297 function formatEvents(events: LoggerEvent[]): string {
298 return events
299 .filter(e =>
300 [
301 'CompileError',
302 'CompileSkip',
303 'CompileSuccess',
304 'PipelineError',
305 ].includes(e.kind),
306 )
307 .map(e => {
308 if (e.kind === 'CompileSuccess') {
309 return `[CompileSuccess] ${e.fnName ?? '(anonymous)'}`;
310 }
311 if (e.kind === 'CompileError') {
312 const d = e.detail;
313 return `[CompileError] ${d?.reason ?? '(no reason)'} (${d?.severity ?? ''}, ${d?.category ?? ''})`;
314 }
315 if (e.kind === 'CompileSkip') {
316 return `[CompileSkip] ${e.reason ?? '(no reason)'}`;
317 }
318 return `[${e.kind}] ${e.data ?? ''}`;
319 })
320 .join('\n');
321 }
322
323 // --- Simple diff ---
324 function unifiedDiff(expected: string, actual: string): string {
325 const expectedLines = expected.split('\n');
326 const actualLines = actual.split('\n');
327 const lines: string[] = [];
328 lines.push(`${RED}--- TS${RESET}`);
329 lines.push(`${GREEN}+++ Rust${RESET}`);
330
331 const maxLen = Math.max(expectedLines.length, actualLines.length);
332 let contextStart = -1;
333 for (let i = 0; i < maxLen; i++) {
334 const eLine = i < expectedLines.length ? expectedLines[i] : undefined;
335 const aLine = i < actualLines.length ? actualLines[i] : undefined;
336 if (eLine === aLine) continue;
337 if (contextStart !== i) {
338 lines.push(`${YELLOW}@@ line ${i + 1} @@${RESET}`);
339 }
340 contextStart = i + 1;
341 if (eLine !== undefined) lines.push(`${RED}-${eLine}${RESET}`);
342 if (aLine !== undefined) lines.push(`${GREEN}+${aLine}${RESET}`);
343 }
344 return lines.join('\n');
345 }
346
347 // --- Format code with prettier ---
348 async function formatCode(code: string): Promise<string> {
349 return prettier.format(code, {semi: true, parser: 'flow'});
350 }
351
352 // --- Generate pragma line for fixture ---
353 function generatePragma(
354 projectConfig: Record<string, unknown>,
355 isFlow: boolean,
356 ): string {
357 const parts: string[] = [];
358
359 // The snap tool checks the first line for @flow to determine parser
360 if (isFlow) {
361 parts.push('@flow');
362 }
363
364 // compilationMode
365 const mode = projectConfig.compilationMode as string;
366 if (mode && mode !== 'all') {
367 parts.push(`@compilationMode:"${mode}"`);
368 }
369
370 // Environment options
371 const env = (projectConfig.environment ?? {}) as Record<string, unknown>;
372
373 if (env.enableAssumeHooksFollowRulesOfReact === true) {
374 parts.push('@enableAssumeHooksFollowRulesOfReact');
375 }
376 if (env.enableTransitivelyFreezeFunctionExpressions === true) {
377 parts.push('@enableTransitivelyFreezeFunctionExpressions');
378 }
379 if (env.validatePreserveExistingMemoizationGuarantees === true) {
380 parts.push('@validatePreserveExistingMemoizationGuarantees');
381 }
382 if (env.enableFunctionOutlining === false) {
383 parts.push('@enableFunctionOutlining:false');
384 }
385 if (
386 Array.isArray(env.validateNoCapitalizedCalls) &&
387 env.validateNoCapitalizedCalls.length > 0
388 ) {
389 parts.push(
390 `@validateNoCapitalizedCalls:${JSON.stringify(env.validateNoCapitalizedCalls)}`,
391 );
392 }
393
394 // target for experimental projects
395 if (projectConfig.compilerVersion === 'experimental') {
396 parts.push('@target:"donotuse_meta_internal"');
397 }
398
399 return parts.length > 0 ? '// ' + parts.join(' ') : '';
400 }
401
402 // --- Create fixture file and baseline .expect.md ---
403 async function createFixture(
404 filePath: string,
405 projectName: string,
406 projectConfig: Record<string, unknown>,
407 tsCode: string | null,
408 tsError: string | null,
409 ): Promise<string> {
410 fs.mkdirSync(FIXTURE_DIR, {recursive: true});
411
412 const basename = path.basename(filePath);
413 const ext = path.extname(basename);
414 const stem = basename.slice(0, -ext.length);
415 let fixtureName = `${projectName}--${basename}`;
416
417 // Deduplicate
418 let counter = 0;
419 while (fs.existsSync(path.join(FIXTURE_DIR, fixtureName))) {
420 counter++;
421 fixtureName = `${projectName}--${stem}-${counter}${ext}`;
422 }
423
424 const source = fs.readFileSync(filePath, 'utf8');
425 const headerBlock = source.substring(0, source.indexOf('*/') + 2 || 200);
426 const isFlow = headerBlock.includes('@flow');
427 const pragma = generatePragma(projectConfig, isFlow);
428
429 const content = pragma ? `${pragma}\n${source}` : source;
430
431 const fixturePath = path.join(FIXTURE_DIR, fixtureName);
432 fs.writeFileSync(fixturePath, content, 'utf8');
433
434 // Generate .expect.md baseline from TS compiler output.
435 // This bypasses yarn snap -u (which fails due to sprout evaluator
436 // not being able to resolve internal module imports).
437 // Format matches snap's writeOutputToString in reporter.ts.
438 let formattedCode: string | null = null;
439 if (tsCode != null) {
440 try {
441 formattedCode = await formatCode(tsCode);
442 } catch {
443 formattedCode = tsCode;
444 }
445 }
446
447 let expectMd = `\n## Input\n\n\`\`\`javascript\n${content}\n\`\`\`\n`;
448 if (formattedCode != null) {
449 expectMd += `\n## Code\n\n\`\`\`javascript\n${formattedCode}\`\`\`\n`;
450 } else {
451 expectMd += '\n';
452 }
453 if (tsError != null) {
454 const cleanError = tsError.replace(/^\/.*?:\s/, '');
455 expectMd += `\n## Error\n\n\`\`\`\n${cleanError}\n\`\`\`\n \n`;
456 }
457 expectMd += ` `;
458
459 const expectPath = fixturePath.replace(/\.[^.]+$/, '.expect.md');
460 fs.writeFileSync(expectPath, expectMd, 'utf8');
461
462 return fixturePath;
463 }
464
465 // --- Main ---
466 (async () => {
467 const files = discoverFiles();
468 if (files.length === 0) {
469 console.error('No files found matching filters.');
470 process.exit(1);
471 }
472
473 console.log(
474 `\nComparing ${BOLD}${files.length}${RESET} files (TS vs Rust)...\n`,
475 );
476
477 // Crash recovery: maintain a skip list of files that segfault the native module.
478 // On crash, the current file is appended to the skip list.
479 // On subsequent runs, those files are skipped automatically.
480 const crashLogPath = path.join(
481 REPO_ROOT,
482 'compiler/.test-internal-files-current',
483 );
484 const skipListPath = path.join(
485 REPO_ROOT,
486 'compiler/.test-internal-skip-list',
487 );
488 const skipSet = new Set<string>();
489 try {
490 const skipData = fs.readFileSync(skipListPath, 'utf8');
491 for (const line of skipData.split('\n')) {
492 const trimmed = line.trim();
493 if (trimmed) skipSet.add(trimmed);
494 }
495 if (skipSet.size > 0) {
496 console.log(
497 `${DIM}Skipping ${skipSet.size} previously-crashing files${RESET}`,
498 );
499 }
500 } catch {}
501
502 process.on('exit', code => {
503 if (code === 139 || code === 134 || code === 11) {
504 // SIGSEGV or SIGABRT — append the culprit to skip list
505 try {
506 const crashFile = fs.readFileSync(crashLogPath, 'utf8').trim();
507 fs.appendFileSync(skipListPath, crashFile + '\n', 'utf8');
508 console.error(
509 `\n${RED}CRASH (signal ${code}) on file: ${crashFile}${RESET}`,
510 );
511 console.error(
512 `${YELLOW}File added to skip list. Re-run to continue.${RESET}`,
513 );
514 } catch {}
515 }
516 try {
517 fs.unlinkSync(crashLogPath);
518 } catch {}
519 });
520
521 let processed = 0;
522 let codeDiffs = 0;
523 let eventDiffs = 0;
524 let bothErrored = 0;
525 let crashes = 0;
526 const diffFiles: Array<{
527 filePath: string;
528 projectName: string;
529 projectConfig: Record<string, unknown>;
530 codeDiff: string | null;
531 eventDiff: string | null;
532 tsCode: string | null;
533 tsError: string | null;
534 }> = [];
535 const createdFixtures: string[] = [];
536
537 for (const {filePath, projectName, projectConfig} of files) {
538 if (skipSet.has(filePath)) continue;
539 processed++;
540
541 // Write current file for crash identification
542 fs.writeFileSync(crashLogPath, filePath, 'utf8');
543
544 // Progress
545 const relPath = filePath.replace(sourceRoot + '/', '');
546 process.stdout.write(
547 `\r${DIM}[${processed}/${files.length}] ${codeDiffs} code diffs, ${eventDiffs} event diffs, ${crashes} crashes${RESET} `,
548 );
549
550 const pluginOpts = makePluginOptions(projectConfig, {
551 logEvent() {},
552 });
553
554 let tsResult: CompileResult;
555 let rustResult: CompileResult;
556 try {
557 tsResult = compileFile('ts', filePath, pluginOpts);
558 } catch (e) {
559 // Unexpected crash in TS compiler
560 tsResult = {
561 code: null,
562 events: [],
563 error: `TS crash: ${e instanceof Error ? e.message : String(e)}`,
564 };
565 }
566 try {
567 rustResult = compileFile('rust', filePath, pluginOpts);
568 } catch (e) {
569 // Unexpected crash in Rust compiler
570 rustResult = {
571 code: null,
572 events: [],
573 error: `Rust crash: ${e instanceof Error ? e.message : String(e)}`,
574 };
575 }
576
577 // Compare code
578 let hasCodeDiff = false;
579 let codeDiffDetail: string | null = null;
580 try {
581 const tsCode = await formatCode(tsResult.code ?? '');
582 const rustCode = await formatCode(rustResult.code ?? '');
583 if (tsCode !== rustCode) {
584 hasCodeDiff = true;
585 codeDiffDetail = unifiedDiff(tsCode, rustCode);
586 }
587 } catch {
588 // Prettier failed — compare raw
589 if ((tsResult.code ?? '') !== (rustResult.code ?? '')) {
590 hasCodeDiff = true;
591 codeDiffDetail = unifiedDiff(
592 tsResult.code ?? '',
593 rustResult.code ?? '',
594 );
595 }
596 }
597
598 // Compare error events
599 const tsEvents = formatEvents(tsResult.events);
600 const rustEvents = formatEvents(rustResult.events);
601 let hasEventDiff = false;
602 let eventDiffDetail: string | null = null;
603 if (tsEvents !== rustEvents) {
604 hasEventDiff = true;
605 eventDiffDetail = unifiedDiff(tsEvents, rustEvents);
606 }
607
608 // Track errors
609 if (tsResult.error && rustResult.error) {
610 bothErrored++;
611 }
612
613 if (hasCodeDiff || hasEventDiff) {
614 if (hasCodeDiff) codeDiffs++;
615 if (hasEventDiff) eventDiffs++;
616 diffFiles.push({
617 filePath,
618 projectName,
619 projectConfig,
620 codeDiff: codeDiffDetail,
621 eventDiff: eventDiffDetail,
622 tsCode: tsResult.code,
623 tsError: tsResult.error,
624 });
625 }
626 }
627
628 // Clear progress line
629 process.stdout.write('\r' + ' '.repeat(80) + '\r');
630
631 // --- Report diffs ---
632 if (diffFiles.length > 0) {
633 console.log(`\n${BOLD}--- Differences ---${RESET}\n`);
634 const showLimit = 50;
635 const toShow = diffFiles.slice(0, showLimit);
636
637 for (const entry of toShow) {
638 const relPath = entry.filePath.replace(sourceRoot + '/', '');
639 console.log(
640 `${RED}DIFF${RESET} ${relPath} ${DIM}(${entry.projectName})${RESET}`,
641 );
642 if (entry.codeDiff) {
643 console.log(entry.codeDiff);
644 }
645 if (entry.eventDiff) {
646 console.log(`${YELLOW}Event diff:${RESET}`);
647 console.log(entry.eventDiff);
648 }
649 console.log('');
650 }
651 if (diffFiles.length > showLimit) {
652 console.log(
653 `${DIM}(showing first ${showLimit} of ${diffFiles.length} diffs)${RESET}`,
654 );
655 }
656 }
657
658 // --- Create fixtures ---
659 if (!dryRun && diffFiles.length > 0) {
660 console.log(`\n${BOLD}--- Creating fixtures ---${RESET}\n`);
661
662 // Only create fixtures for files with code diffs (not event-only diffs)
663 const codeDiffFiles = diffFiles.filter(f => f.codeDiff != null);
664
665 for (const entry of codeDiffFiles) {
666 const fixturePath = await createFixture(
667 entry.filePath,
668 entry.projectName,
669 entry.projectConfig,
670 entry.tsCode,
671 entry.tsError,
672 );
673 createdFixtures.push(fixturePath);
674 const fixtureName = path.basename(fixturePath);
675 console.log(` ${GREEN}+${RESET} ${fixtureName}`);
676 }
677
678 if (createdFixtures.length > 0) {
679 console.log(
680 `\nCreated ${BOLD}${createdFixtures.length}${RESET} fixtures in internal/`,
681 );
682
683 // --- Verification ---
684 console.log(`\n${BOLD}--- Verifying fixtures ---${RESET}\n`);
685
686 // Verify with Rust compiler (baselines already written as .expect.md)
687 console.log('\nVerifying with yarn snap --rust ...');
688 try {
689 execSync("yarn snap --rust -p 'internal/*'", {
690 cwd: path.join(REPO_ROOT, 'compiler'),
691 stdio: 'inherit',
692 });
693 // If snap --rust passes, all fixtures matched (unexpected)
694 console.log(
695 `${YELLOW}WARNING: yarn snap --rust passed — fixtures may not reproduce differences.${RESET}`,
696 );
697 } catch {
698 // Expected: snap --rust should fail for differing fixtures
699 console.log(
700 `${GREEN}yarn snap --rust failed as expected — fixtures reproduce differences.${RESET}`,
701 );
702 }
703
704 console.log(
705 `\n${RED}${BOLD}DO NOT COMMIT${RESET}${RED} the fixture files in internal/${RESET}`,
706 );
707 }
708 }
709
710 // --- Summary ---
711 console.log(`\n${BOLD}--- Summary ---${RESET}`);
712 console.log(`Processed: ${processed}`);
713 console.log(`Code diffs: ${codeDiffs}`);
714 console.log(
715 `Event-only diffs: ${eventDiffs - codeDiffs > 0 ? eventDiffs - codeDiffs : 0}`,
716 );
717 console.log(`Both errored: ${bothErrored}`);
718 if (crashes > 0) console.log(`Crashes (skipped): ${crashes}`);
719 if (createdFixtures.length > 0) {
720 console.log(`Fixtures created: ${createdFixtures.length}`);
721 }
722
723 process.exit(codeDiffs > 0 ? 1 : 0);
724 })();