compiler/packages/snap/src/runner.ts TYPESCRIPT 674 lines View on github.com → Search inside
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.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.