compiler/packages/snap/src/compiler.ts TYPESCRIPT 402 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 type * as BabelCore from '@babel/core';9import {transformFromAstSync} from '@babel/core';1011import * as BabelParser from '@babel/parser';12import {NodePath} from '@babel/traverse';13import * as t from '@babel/types';14import type {15  Logger,16  LoggerEvent,17  PluginOptions,18  CompilerReactTarget,19  CompilerPipelineValue,20} from 'babel-plugin-react-compiler/src/Entrypoint';21import type {22  Effect,23  ValueKind,24  ValueReason,25} from 'babel-plugin-react-compiler/src/HIR';26import type {parseConfigPragmaForTests as ParseConfigPragma} from 'babel-plugin-react-compiler/src/Utils/TestUtils';27import * as HermesParser from 'hermes-parser';28import invariant from 'invariant';29import path from 'path';30import prettier from 'prettier';31import SproutTodoFilter from './SproutTodoFilter';32import {isExpectError} from './fixture-utils';33import {makeSharedRuntimeTypeProvider} from './sprout/shared-runtime-type-provider';3435export function parseLanguage(source: string): 'flow' | 'typescript' {36  return source.indexOf('@flow') !== -1 ? 'flow' : 'typescript';37}3839export function parseSourceType(source: string): 'script' | 'module' {40  return source.indexOf('@script') !== -1 ? 'script' : 'module';41}4243/**44 * Parse react compiler plugin + environment options from test fixture. Note45 * that although this primarily uses `Environment:parseConfigPragma`, it also46 * has test fixture specific (i.e. not applicable to playground) parsing logic.47 */48function makePluginOptions(49  firstLine: string,50  parseConfigPragmaFn: typeof ParseConfigPragma,51  debugIRLogger: (value: CompilerPipelineValue) => void,52  EffectEnum: typeof Effect,53  ValueKindEnum: typeof ValueKind,54  ValueReasonEnum: typeof ValueReason,55): {56  options: PluginOptions;57  loggerTestOnly: boolean;58  logs: Array<{filename: string | null; event: LoggerEvent}>;59} {60  // TODO(@mofeiZ) rewrite snap fixtures to @validatePreserveExistingMemo:false61  let validatePreserveExistingMemoizationGuarantees = false;62  let target: CompilerReactTarget = '19';6364  /**65   * Snap currently runs all fixtures without `validatePreserveExistingMemo` as66   * most fixtures are interested in compilation output, not whether the67   * compiler was able to preserve existing memo.68   *69   * TODO: flip the default. `useMemo` is rare in test fixtures -- fixtures that70   * use useMemo should be explicit about whether this flag is enabled71   */72  if (firstLine.includes('@validatePreserveExistingMemoizationGuarantees')) {73    validatePreserveExistingMemoizationGuarantees = true;74  }7576  const loggerTestOnly = firstLine.includes('@loggerTestOnly');77  const logs: Array<{filename: string | null; event: LoggerEvent}> = [];78  const logger: Logger = {79    logEvent: (filename, event) => {80      logs.push({filename, event});81    },82    debugLogIRs: debugIRLogger,83  };8485  const config = parseConfigPragmaFn(firstLine, {compilationMode: 'all'});86  const options = {87    ...config,88    environment: {89      ...config.environment,90      moduleTypeProvider: makeSharedRuntimeTypeProvider({91        EffectEnum,92        ValueKindEnum,93        ValueReasonEnum,94      }),95      assertValidMutableRanges: true,96      validatePreserveExistingMemoizationGuarantees,97    },98    logger,99    enableReanimatedCheck: false,100    target,101  };102  return {options, loggerTestOnly, logs};103}104105export function parseInput(106  input: string,107  filename: string,108  language: 'flow' | 'typescript',109  sourceType: 'module' | 'script',110): BabelCore.types.File {111  // Extract the first line to quickly check for custom test directives112  if (language === 'flow') {113    return HermesParser.parse(input, {114      babel: true,115      flow: 'all',116      sourceFilename: filename,117      sourceType,118      enableExperimentalComponentSyntax: true,119      enableExperimentalFlowMatchSyntax: true,120    });121  } else {122    return BabelParser.parse(input, {123      sourceFilename: filename,124      plugins: ['typescript', 'jsx'],125      sourceType,126    });127  }128}129130function getEvaluatorPresets(131  language: 'typescript' | 'flow',132): Array<BabelCore.PluginItem> {133  const presets: Array<BabelCore.PluginItem> = [134    {135      plugins: [136        'babel-plugin-fbt',137        'babel-plugin-fbt-runtime',138        'babel-plugin-idx',139      ],140    },141  ];142  presets.push(143    language === 'typescript'144      ? [145          '@babel/preset-typescript',146          {147            /**148             * onlyRemoveTypeImports needs to be set as fbt imports149             * would otherwise be removed by this pass.150             * https://github.com/facebook/fbt/issues/49151             * https://github.com/facebook/sfbt/issues/72152             * https://dev.to/retyui/how-to-add-support-typescript-for-fbt-an-internationalization-framework-3lo0153             */154            onlyRemoveTypeImports: true,155          },156        ]157      : '@babel/preset-flow',158  );159160  presets.push({161    plugins: ['@babel/plugin-syntax-jsx'],162  });163  presets.push(164    ['@babel/preset-react', {throwIfNamespace: false}],165    {166      plugins: ['@babel/plugin-transform-modules-commonjs'],167    },168    {169      plugins: [170        function BabelPluginRewriteRequirePath() {171          return {172            visitor: {173              CallExpression(path: NodePath<t.CallExpression>) {174                const {callee} = path.node;175                if (callee.type === 'Identifier' && callee.name === 'require') {176                  const arg = path.node.arguments[0];177                  if (arg.type === 'StringLiteral') {178                    // rewrite to use relative import as eval happens in179                    // sprout/evaluator.ts180                    if (arg.value === 'shared-runtime') {181                      arg.value = './shared-runtime';182                    } else if (arg.value === 'ReactForgetFeatureFlag') {183                      arg.value = './ReactForgetFeatureFlag';184                    } else if (arg.value === 'useEffectWrapper') {185                      arg.value = './useEffectWrapper';186                    }187                  }188                }189              },190            },191          };192        },193      ],194    },195  );196  return presets;197}198async function format(199  inputCode: string,200  language: 'typescript' | 'flow',201): Promise<string> {202  return await prettier.format(inputCode, {203    semi: true,204    parser: language === 'typescript' ? 'babel-ts' : 'flow',205  });206}207const TypescriptEvaluatorPresets = getEvaluatorPresets('typescript');208const FlowEvaluatorPresets = getEvaluatorPresets('flow');209210export type TransformResult = {211  forgetOutput: string;212  logs: string | null;213  evaluatorCode: {214    original: string;215    forget: string;216  } | null;217};218219export async function transformFixtureInput(220  input: string,221  fixturePath: string,222  parseConfigPragmaFn: typeof ParseConfigPragma,223  plugin: BabelCore.PluginObj,224  includeEvaluator: boolean,225  debugIRLogger: (value: CompilerPipelineValue) => void,226  EffectEnum: typeof Effect,227  ValueKindEnum: typeof ValueKind,228  ValueReasonEnum: typeof ValueReason,229): Promise<{kind: 'ok'; value: TransformResult} | {kind: 'err'; msg: string}> {230  // Extract the first line to quickly check for custom test directives231  const firstLine = input.substring(0, input.indexOf('\n'));232233  const language = parseLanguage(firstLine);234  const sourceType = parseSourceType(firstLine);235  // Preserve file extension as it determines typescript's babel transform236  // mode (e.g. stripping types, parsing rules for brackets)237  const filename =238    path.basename(fixturePath) + (language === 'typescript' ? '.ts' : '');239  const inputAst = parseInput(input, filename, language, sourceType);240  // Give babel transforms an absolute path as relative paths get prefixed241  // with `cwd`, which is different across machines242  const virtualFilepath = '/' + filename;243244  const presets =245    language === 'typescript'246      ? TypescriptEvaluatorPresets247      : FlowEvaluatorPresets;248249  /**250   * Get Forget compiled code251   */252  const {options, loggerTestOnly, logs} = makePluginOptions(253    firstLine,254    parseConfigPragmaFn,255    debugIRLogger,256    EffectEnum,257    ValueKindEnum,258    ValueReasonEnum,259  );260  const forgetResult = transformFromAstSync(inputAst, input, {261    filename: virtualFilepath,262    highlightCode: false,263    retainLines: true,264    compact: true,265    plugins: [266      [plugin, options],267      'babel-plugin-fbt',268      'babel-plugin-fbt-runtime',269      'babel-plugin-idx',270    ],271    sourceType: 'module',272    ast: includeEvaluator,273    cloneInputAst: includeEvaluator,274    configFile: false,275    babelrc: false,276  });277  invariant(278    forgetResult?.code != null,279    'Expected BabelPluginReactForget to codegen successfully.',280  );281  const forgetCode = forgetResult.code;282  let evaluatorCode = null;283284  if (285    includeEvaluator &&286    !SproutTodoFilter.has(fixturePath) &&287    !isExpectError(filename)288  ) {289    let forgetEval: string;290    try {291      invariant(292        forgetResult?.ast != null,293        'Expected BabelPluginReactForget ast.',294      );295      const result = transformFromAstSync(forgetResult.ast, forgetCode, {296        presets,297        filename: virtualFilepath,298        configFile: false,299        babelrc: false,300      });301      if (result?.code == null) {302        return {303          kind: 'err',304          msg: 'Unexpected error in forget transform pipeline - no code emitted',305        };306      } else {307        forgetEval = result.code;308      }309    } catch (e) {310      return {311        kind: 'err',312        msg: 'Unexpected error in Forget transform pipeline: ' + e.message,313      };314    }315316    /**317     * Get evaluator code for source (no Forget)318     */319    let originalEval: string;320    try {321      const result = transformFromAstSync(inputAst, input, {322        presets,323        filename: virtualFilepath,324        configFile: false,325        babelrc: false,326      });327328      if (result?.code == null) {329        return {330          kind: 'err',331          msg: 'Unexpected error in non-forget transform pipeline - no code emitted',332        };333      } else {334        originalEval = result.code;335      }336    } catch (e) {337      return {338        kind: 'err',339        msg: 'Unexpected error in non-forget transform pipeline: ' + e.message,340      };341    }342    evaluatorCode = {343      forget: forgetEval,344      original: originalEval,345    };346  }347  const forgetOutput = await format(forgetCode, language);348  let formattedLogs = null;349  if (loggerTestOnly && logs.length !== 0) {350    formattedLogs = logs351      .map(({event}) => {352        return JSON.stringify(event, (key, value) => {353          if (354            key === 'detail' &&355            value != null &&356            typeof value.serialize === 'function'357          ) {358            return value.serialize();359          }360          return value;361        });362      })363      .join('\n');364  }365  const expectNothingCompiled =366    firstLine.indexOf('@expectNothingCompiled') !== -1;367  const successFailures = logs.filter(368    log =>369      log.event.kind === 'CompileSuccess' || log.event.kind === 'CompileError',370  );371  if (successFailures.length === 0 && !expectNothingCompiled) {372    return {373      kind: 'err',374      msg: 'No success/failure events, add `// @expectNothingCompiled` to the first line if this is expected',375    };376  } else if (successFailures.length !== 0 && expectNothingCompiled) {377    return {378      kind: 'err',379      msg: 'Expected nothing to be compiled (from `// @expectNothingCompiled`), but some functions compiled or errored',380    };381  }382  const unexpectedThrows = logs.filter(383    log => log.event.kind === 'CompileUnexpectedThrow',384  );385  if (unexpectedThrows.length > 0) {386    return {387      kind: 'err',388      msg:389        `Compiler pass(es) threw instead of recording errors:\n` +390        unexpectedThrows.map(l => (l.event as any).data).join('\n'),391    };392  }393  return {394    kind: 'ok',395    value: {396      forgetOutput,397      logs: formattedLogs,398      evaluatorCode,399    },400  };401}

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.