1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 */78import {Worker} from 'jest-worker';9import {cpus} from 'os';10import process from 'process';11import * as readline from 'readline';12import ts from 'typescript';13import yargs from 'yargs';14import {hideBin} from 'yargs/helpers';15import {BABEL_PLUGIN_ROOT, PROJECT_ROOT} from './constants';16import {TestFilter, getFixtures} from './fixture-utils';17import {18 TestResult,19 TestResults,20 normalizeCodeBlankLines,21 report,22 update,23} from './reporter';24import {25 RunnerAction,26 RunnerState,27 buildRust,28 makeWatchRunner,29 watchSrc,30} from './runner-watch';31import * as runnerWorker from './runner-worker';32import {execSync} from 'child_process';33import fs from 'fs';34import path from 'path';35import {minimize, minimizeRustDelta} from './minimize';36import {parseInput, parseLanguage, parseSourceType} from './compiler';37import {38 PARSE_CONFIG_PRAGMA_IMPORT,39 PRINT_HIR_IMPORT,40 PRINT_REACTIVE_IR_IMPORT,41 BABEL_PLUGIN_SRC,42} from './constants';43import chalk from 'chalk';4445const WORKER_PATH = require.resolve('./runner-worker.js');46const NUM_WORKERS = cpus().length - 1;4748readline.emitKeypressEvents(process.stdin);4950type TestOptions = {51 sync: boolean;52 workerThreads: boolean;53 watch: boolean;54 update: boolean;55 pattern?: string;56 debug: boolean;57 verbose: boolean;58 rust: boolean;59};6061type MinimizeOptions = {62 path: string;63 update: boolean;64 rust: boolean;65};6667type MinimizeRustDeltaOptions = {68 path: string;69 update: boolean;70};7172type CompileOptions = {73 path: string;74 debug: boolean;75};7677async function runTestCommand(opts: TestOptions): Promise<void> {78 // Rust native module doesn't load in jest-worker child processes,79 // so force sync mode when using the Rust backend.80 if (opts.rust) {81 opts.sync = true;82 }8384 const worker: Worker & typeof runnerWorker = new Worker(WORKER_PATH, {85 enableWorkerThreads: opts.workerThreads,86 numWorkers: NUM_WORKERS,87 }) as any;88 worker.getStderr().pipe(process.stderr);89 worker.getStdout().pipe(process.stdout);9091 // Check if watch mode should be enabled92 const shouldWatch = opts.watch;9394 if (shouldWatch) {95 makeWatchRunner(96 state => onChange(worker, state, opts.sync, opts.verbose, opts.rust),97 opts.debug,98 opts.pattern,99 opts.rust,100 );101 if (opts.pattern) {102 /**103 * Warm up wormers when in watch mode. Loading the Forget babel plugin104 * and all of its transitive dependencies takes 1-3s (per worker) on a M1.105 * As jest-worker dispatches tasks using a round-robin strategy, we can106 * avoid an additional 1-3s wait on the first num_workers runs by warming107 * up workers eagerly.108 */109 for (let i = 0; i < NUM_WORKERS - 1; i++) {110 worker.transformFixture(111 {112 fixturePath: 'tmp',113 snapshotPath: './tmp.expect.md',114 inputPath: './tmp.js',115 input: `116 function Foo(props) {117 return identity(props);118 }119 `,120 snapshot: null,121 },122 0,123 false,124 false,125 opts.rust,126 );127 }128 }129 } else {130 // Non-watch mode. For simplicity we re-use the same watchSrc() function.131 // After the first build completes run tests and exit132 const tsWatch: ts.WatchOfConfigFile<ts.SemanticDiagnosticsBuilderProgram> =133 watchSrc(134 () => {},135 async (isTypecheckSuccess: boolean) => {136 let isSuccess = false;137 if (!isTypecheckSuccess) {138 console.error(139 'Found typescript errors in Forget source code, skipping test fixtures.',140 );141 } else {142 try {143 execSync('yarn build', {cwd: BABEL_PLUGIN_ROOT});144 console.log('Built compiler successfully with tsup');145146 if (opts.rust && !buildRust()) {147 throw new Error('Failed to build Rust compiler');148 }149150 // Determine which filter to use151 let testFilter: TestFilter | null = null;152 if (opts.pattern) {153 testFilter = {154 paths: [opts.pattern],155 };156 }157158 const results = await runFixtures(159 worker,160 testFilter,161 0,162 opts.debug,163 false, // no requireSingleFixture in non-watch mode164 opts.sync,165 opts.rust,166 );167 if (opts.update) {168 update(results);169 isSuccess = true;170 } else {171 isSuccess = report(results, opts.verbose, opts.rust);172 }173 } catch (e) {174 console.warn('Failed to build compiler with tsup:', e);175 }176 }177 tsWatch?.close();178 await worker.end();179 process.exit(isSuccess ? 0 : 1);180 },181 );182 }183}184185async function runMinimizeCommand(opts: MinimizeOptions): Promise<void> {186 // Resolve the input path187 const inputPath = path.isAbsolute(opts.path)188 ? opts.path189 : path.resolve(PROJECT_ROOT, opts.path);190191 // Check if file exists192 if (!fs.existsSync(inputPath)) {193 console.error(`Error: File not found: ${inputPath}`);194 process.exit(1);195 }196197 // Read the input file198 const input = fs.readFileSync(inputPath, 'utf-8');199 const filename = path.basename(inputPath);200 const firstLine = input.substring(0, input.indexOf('\n'));201 const language = parseLanguage(firstLine);202 const sourceType = parseSourceType(firstLine);203204 if (opts.rust && !buildRust()) {205 console.error('Error: Failed to build Rust compiler');206 process.exit(1);207 }208209 console.log(210 `Minimizing: ${inputPath}${opts.rust ? ' (using Rust compiler)' : ''}`,211 );212213 const originalLines = input.split('\n').length;214215 // Run the minimization216 const result = minimize(input, filename, language, sourceType, opts.rust);217218 if (result.kind === 'success') {219 console.log('Could not minimize: the input compiles successfully.');220 process.exit(0);221 }222223 if (result.kind === 'minimal') {224 console.log(225 'Could not minimize: the input fails but is already minimal and cannot be reduced further.',226 );227 process.exit(0);228 }229230 // Output the minimized code231 console.log('--- Minimized Code ---');232 console.log(result.source);233234 const minimizedLines = result.source.split('\n').length;235 console.log(236 `\nReduced from ${originalLines} lines to ${minimizedLines} lines`,237 );238239 if (opts.update) {240 fs.writeFileSync(inputPath, result.source, 'utf-8');241 console.log(`\nUpdated ${inputPath} with minimized code.`);242 }243}244245async function runMinimizeRustDeltaCommand(246 opts: MinimizeRustDeltaOptions,247): Promise<void> {248 const inputPath = path.isAbsolute(opts.path)249 ? opts.path250 : path.resolve(PROJECT_ROOT, opts.path);251252 if (!fs.existsSync(inputPath)) {253 console.error(`Error: File not found: ${inputPath}`);254 process.exit(1);255 }256257 // Build both compilers258 execSync('yarn build', {cwd: BABEL_PLUGIN_ROOT});259 if (!buildRust()) {260 console.error('Error: Failed to build Rust compiler');261 process.exit(1);262 }263264 const input = fs.readFileSync(inputPath, 'utf-8');265 const filename = path.basename(inputPath);266 const firstLine = input.substring(0, input.indexOf('\n'));267 const language = parseLanguage(firstLine);268 const sourceType = parseSourceType(firstLine);269270 console.log(`Minimizing TS/Rust delta: ${inputPath}`);271272 const originalLines = input.split('\n').length;273274 const result = minimizeRustDelta(input, filename, language, sourceType);275276 if (result.kind === 'no_delta') {277 console.log(278 'Could not minimize: TS and Rust compilers produce the same output.',279 );280 process.exit(0);281 }282283 if (result.kind === 'minimal') {284 console.log(285 'Could not minimize: the delta exists but the input is already minimal.',286 );287 process.exit(0);288 }289290 console.log('--- Minimized Code ---');291 console.log(result.source);292293 const minimizedLines = result.source.split('\n').length;294 console.log(295 `\nReduced from ${originalLines} lines to ${minimizedLines} lines`,296 );297298 if (opts.update) {299 fs.writeFileSync(inputPath, result.source, 'utf-8');300 console.log(`\nUpdated ${inputPath} with minimized code.`);301 }302}303304async function runCompileCommand(opts: CompileOptions): Promise<void> {305 // Resolve the input path306 const inputPath = path.isAbsolute(opts.path)307 ? opts.path308 : path.resolve(PROJECT_ROOT, opts.path);309310 // Check if file exists311 if (!fs.existsSync(inputPath)) {312 console.error(`Error: File not found: ${inputPath}`);313 process.exit(1);314 }315316 // Read the input file317 const input = fs.readFileSync(inputPath, 'utf-8');318 const filename = path.basename(inputPath);319 const firstLine = input.substring(0, input.indexOf('\n'));320 const language = parseLanguage(firstLine);321 const sourceType = parseSourceType(firstLine);322323 // Import the compiler324 const importedCompilerPlugin = require(BABEL_PLUGIN_SRC) as Record<325 string,326 any327 >;328 const BabelPluginReactCompiler = importedCompilerPlugin['default'];329 const parseConfigPragmaForTests =330 importedCompilerPlugin[PARSE_CONFIG_PRAGMA_IMPORT];331 const printFunctionWithOutlined = importedCompilerPlugin[PRINT_HIR_IMPORT];332 const printReactiveFunctionWithOutlined =333 importedCompilerPlugin[PRINT_REACTIVE_IR_IMPORT];334 const EffectEnum = importedCompilerPlugin['Effect'];335 const ValueKindEnum = importedCompilerPlugin['ValueKind'];336 const ValueReasonEnum = importedCompilerPlugin['ValueReason'];337338 // Setup debug logger339 let lastLogged: string | null = null;340 const debugIRLogger = opts.debug341 ? (value: any) => {342 let printed: string;343 switch (value.kind) {344 case 'hir':345 printed = printFunctionWithOutlined(value.value);346 break;347 case 'reactive':348 printed = printReactiveFunctionWithOutlined(value.value);349 break;350 case 'debug':351 printed = value.value;352 break;353 case 'ast':354 printed = '(ast)';355 break;356 default:357 printed = String(value);358 }359360 if (printed !== lastLogged) {361 lastLogged = printed;362 console.log(`${chalk.green(value.name)}:\n${printed}\n`);363 } else {364 console.log(`${chalk.blue(value.name)}: (no change)\n`);365 }366 }367 : () => {};368369 // Parse the input370 let ast;371 try {372 ast = parseInput(input, filename, language, sourceType);373 } catch (e: any) {374 console.error(`Parse error: ${e.message}`);375 process.exit(1);376 }377378 // Build plugin options379 const config = parseConfigPragmaForTests(firstLine, {compilationMode: 'all'});380 const options = {381 ...config,382 environment: {383 ...config.environment,384 },385 logger: {386 logEvent: () => {},387 debugLogIRs: debugIRLogger,388 },389 enableReanimatedCheck: false,390 };391392 // Compile393 const {transformFromAstSync} = require('@babel/core');394 try {395 const result = transformFromAstSync(ast, input, {396 filename: '/' + filename,397 highlightCode: false,398 retainLines: true,399 compact: true,400 plugins: [[BabelPluginReactCompiler, options]],401 sourceType: 'module',402 ast: false,403 cloneInputAst: true,404 configFile: false,405 babelrc: false,406 });407408 if (result?.code != null) {409 // Format the output410 const prettier = require('prettier');411 const formatted = await prettier.format(result.code, {412 semi: true,413 parser: language === 'typescript' ? 'babel-ts' : 'flow',414 });415 console.log(formatted);416 } else {417 console.error('Error: No code emitted from compiler');418 process.exit(1);419 }420 } catch (e: any) {421 console.error(e.message);422 process.exit(1);423 }424}425426yargs(hideBin(process.argv))427 .command(428 ['test', '$0'],429 'Run compiler tests',430 yargs => {431 return yargs432 .boolean('sync')433 .describe(434 'sync',435 'Run compiler in main thread (instead of using worker threads or subprocesses). Defaults to false.',436 )437 .default('sync', false)438 .boolean('worker-threads')439 .describe(440 'worker-threads',441 'Run compiler in worker threads (instead of subprocesses). Defaults to true.',442 )443 .default('worker-threads', true)444 .boolean('watch')445 .describe(446 'watch',447 'Run compiler in watch mode, re-running after changes',448 )449 .alias('w', 'watch')450 .default('watch', false)451 .boolean('update')452 .alias('u', 'update')453 .describe('update', 'Update fixtures')454 .default('update', false)455 .string('pattern')456 .alias('p', 'pattern')457 .describe(458 'pattern',459 'Optional glob pattern to filter fixtures (e.g., "error.*", "use-memo")',460 )461 .boolean('debug')462 .alias('d', 'debug')463 .describe('debug', 'Enable debug logging to print HIR for each pass')464 .default('debug', false)465 .boolean('verbose')466 .alias('v', 'verbose')467 .describe('verbose', 'Print individual test results')468 .default('verbose', false)469 .boolean('rust')470 .describe('rust', 'Use the Rust compiler backend instead of TypeScript')471 .default('rust', false);472 },473 async argv => {474 await runTestCommand(argv as TestOptions);475 },476 )477 .command(478 'minimize <path>',479 'Minimize a test case to reproduce a compiler error',480 yargs => {481 return yargs482 .positional('path', {483 describe: 'Path to the file to minimize',484 type: 'string',485 demandOption: true,486 })487 .boolean('update')488 .alias('u', 'update')489 .describe(490 'update',491 'Update the input file in-place with the minimized version',492 )493 .default('update', false)494 .boolean('rust')495 .describe('rust', 'Use the Rust compiler backend instead of TypeScript')496 .default('rust', false);497 },498 async argv => {499 await runMinimizeCommand(argv as unknown as MinimizeOptions);500 },501 )502 .command(503 'minimize-rust-delta <path>',504 'Minimize a test case to the smallest code that still produces different output between TS and Rust compilers',505 yargs => {506 return yargs507 .positional('path', {508 describe: 'Path to the file to minimize',509 type: 'string',510 demandOption: true,511 })512 .boolean('update')513 .alias('u', 'update')514 .describe(515 'update',516 'Update the input file in-place with the minimized version',517 )518 .default('update', false);519 },520 async argv => {521 await runMinimizeRustDeltaCommand(522 argv as unknown as MinimizeRustDeltaOptions,523 );524 },525 )526 .command(527 'compile <path>',528 'Compile a file with the React Compiler',529 yargs => {530 return yargs531 .positional('path', {532 describe: 'Path to the file to compile',533 type: 'string',534 demandOption: true,535 })536 .boolean('debug')537 .alias('d', 'debug')538 .describe('debug', 'Enable debug logging to print HIR for each pass')539 .default('debug', false);540 },541 async argv => {542 await runCompileCommand(argv as unknown as CompileOptions);543 },544 )545 .help('help')546 .strict()547 .demandCommand()548 .parse();549550/**551 * Do a test run and return the test results552 */553async function runFixtures(554 worker: Worker & typeof runnerWorker,555 filter: TestFilter | null,556 compilerVersion: number,557 debug: boolean,558 requireSingleFixture: boolean,559 sync: boolean,560 enableRust: boolean = false,561): Promise<TestResults> {562 // We could in theory be fancy about tracking the contents of the fixtures563 // directory via our file subscription, but it's simpler to just re-read564 // the directory each time.565 const fixtures = await getFixtures(filter);566 const isOnlyFixture = filter !== null && fixtures.size === 1;567 const shouldLog = debug && (!requireSingleFixture || isOnlyFixture);568569 let entries: Array<[string, TestResult]>;570 if (!sync) {571 // Note: promise.all to ensure parallelism when enabled572 const work: Array<Promise<[string, TestResult]>> = [];573 for (const [fixtureName, fixture] of fixtures) {574 work.push(575 worker576 .transformFixture(577 fixture,578 compilerVersion,579 shouldLog,580 true,581 enableRust,582 )583 .then(result => [fixtureName, result]),584 );585 }586587 entries = await Promise.all(work);588 } else {589 entries = [];590 for (const [fixtureName, fixture] of fixtures) {591 let output = await runnerWorker.transformFixture(592 fixture,593 compilerVersion,594 shouldLog,595 true,596 enableRust,597 );598 entries.push([fixtureName, output]);599 }600 }601602 return new Map(entries);603}604605// Callback to re-run tests after some change606async function onChange(607 worker: Worker & typeof runnerWorker,608 state: RunnerState,609 sync: boolean,610 verbose: boolean,611 enableRust: boolean = false,612) {613 const {compilerVersion, isCompilerBuildValid, mode, filter, debug} = state;614 if (isCompilerBuildValid) {615 const start = performance.now();616617 // console.clear() only works when stdout is connected to a TTY device.618 // we're currently piping stdout (see main.ts), so let's do a 'hack'619 console.log('\u001Bc');620621 // we don't clear console after this point, since622 // it may contain debug console logging623 const results = await runFixtures(624 worker,625 mode.filter ? filter : null,626 compilerVersion,627 debug,628 true, // requireSingleFixture in watch mode629 sync,630 enableRust,631 );632 const end = performance.now();633634 // Track fixture status for autocomplete suggestions635 for (const [basename, result] of results) {636 const actual =637 enableRust && result.actual638 ? normalizeCodeBlankLines(result.actual)639 : result.actual;640 const expected =641 enableRust && result.expected642 ? normalizeCodeBlankLines(result.expected)643 : result.expected;644 const failed = actual !== expected || result.unexpectedError != null;645 state.fixtureLastRunStatus.set(basename, failed ? 'fail' : 'pass');646 }647648 if (mode.action === RunnerAction.Update) {649 update(results);650 state.lastUpdate = end;651 } else {652 report(results, verbose, enableRust);653 }654 console.log(`Completed in ${Math.floor(end - start)} ms`);655 } else {656 console.error(657 `${mode}: Found errors in Forget source code, skipping test fixtures.`,658 );659 }660 console.log(661 '\n' +662 (mode.filter663 ? `Current mode = FILTER, pattern = "${filter?.paths[0] ?? ''}".`664 : 'Current mode = NORMAL, run all test fixtures.') +665 '\nWaiting for input or file changes...\n' +666 'u - update all fixtures\n' +667 `d - toggle (turn ${debug ? 'off' : 'on'}) debug logging\n` +668 'p - enter pattern to filter fixtures\n' +669 (mode.filter ? 'a - run all tests (exit filter mode)\n' : '') +670 'q - quit\n' +671 '[any] - rerun tests\n',672 );673}
Findings
✓ No findings reported for this file.