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.