main
ts 251 lines 7.67 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 import type {PluginObj} from '@babel/core';
9 import type {parseConfigPragmaForTests as ParseConfigPragma} from 'babel-plugin-react-compiler/src/Utils/TestUtils';
10 import type {printFunctionWithOutlined as PrintFunctionWithOutlined} from 'babel-plugin-react-compiler/src/HIR/PrintHIR';
11 import type {printReactiveFunctionWithOutlined as PrintReactiveFunctionWithOutlined} from 'babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction';
12 import {TransformResult, transformFixtureInput} from './compiler';
13 import {
14 PARSE_CONFIG_PRAGMA_IMPORT,
15 PRINT_HIR_IMPORT,
16 PRINT_REACTIVE_IR_IMPORT,
17 BABEL_PLUGIN_SRC,
18 BABEL_PLUGIN_RUST_SRC,
19 } from './constants';
20 import {TestFixture, getBasename, isExpectError} from './fixture-utils';
21 import {TestResult, writeOutputToString} from './reporter';
22 import {runSprout} from './sprout';
23 import type {
24 CompilerPipelineValue,
25 Effect,
26 ValueKind,
27 ValueReason,
28 } from 'babel-plugin-react-compiler/src';
29 import chalk from 'chalk';
30
31 const originalConsoleError = console.error;
32
33 // Try to avoid clearing the entire require cache, which (as of this PR)
34 // contains ~1250 files. This assumes that no dependencies have global caches
35 // that may need to be invalidated across Forget reloads.
36 const invalidationSubpath = 'packages/babel-plugin-react-compiler/dist';
37 const rustInvalidationSubpath =
38 'packages/babel-plugin-react-compiler-rust/dist';
39 let version: number | null = null;
40 export function clearRequireCache() {
41 Object.keys(require.cache).forEach(function (path) {
42 if (
43 path.includes(invalidationSubpath) ||
44 path.includes(rustInvalidationSubpath)
45 ) {
46 delete require.cache[path];
47 }
48 });
49 }
50
51 async function compile(
52 input: string,
53 fixturePath: string,
54 compilerVersion: number,
55 shouldLog: boolean,
56 includeEvaluator: boolean,
57 enableRust: boolean = false,
58 ): Promise<{
59 error: string | null;
60 compileResult: TransformResult | null;
61 }> {
62 const seenConsoleErrors: Array<string> = [];
63 console.error = (...messages: Array<string>) => {
64 seenConsoleErrors.push(...messages);
65 };
66 if (version !== null && compilerVersion !== version) {
67 clearRequireCache();
68 }
69 version = compilerVersion;
70
71 let compileResult: TransformResult | null = null;
72 let error: string | null = null;
73 try {
74 // Always load TS compiler for utilities (parseConfigPragmaForTests, print functions)
75 const importedCompilerPlugin = require(BABEL_PLUGIN_SRC) as Record<
76 string,
77 unknown
78 >;
79
80 // Load the appropriate babel plugin
81 const pluginSrc = enableRust ? BABEL_PLUGIN_RUST_SRC : BABEL_PLUGIN_SRC;
82 const importedPlugin = enableRust
83 ? (require(pluginSrc) as Record<string, unknown>)
84 : importedCompilerPlugin;
85
86 // NOTE: we intentionally require lazily here so that we can clear the require cache
87 // and load fresh versions of the compiler when `compilerVersion` changes.
88 const BabelPluginReactCompiler = importedPlugin['default'] as PluginObj;
89 const EffectEnum = importedCompilerPlugin['Effect'] as typeof Effect;
90 const ValueKindEnum = importedCompilerPlugin[
91 'ValueKind'
92 ] as typeof ValueKind;
93 const ValueReasonEnum = importedCompilerPlugin[
94 'ValueReason'
95 ] as typeof ValueReason;
96 const printFunctionWithOutlined = importedCompilerPlugin[
97 PRINT_HIR_IMPORT
98 ] as typeof PrintFunctionWithOutlined;
99 const printReactiveFunctionWithOutlined = importedCompilerPlugin[
100 PRINT_REACTIVE_IR_IMPORT
101 ] as typeof PrintReactiveFunctionWithOutlined;
102 const parseConfigPragmaForTests = importedCompilerPlugin[
103 PARSE_CONFIG_PRAGMA_IMPORT
104 ] as typeof ParseConfigPragma;
105
106 let lastLogged: string | null = null;
107 const debugIRLogger = shouldLog
108 ? (value: CompilerPipelineValue) => {
109 let printed: string;
110 switch (value.kind) {
111 case 'hir':
112 printed = printFunctionWithOutlined(value.value);
113 break;
114 case 'reactive':
115 printed = printReactiveFunctionWithOutlined(value.value);
116 break;
117 case 'debug':
118 printed = value.value;
119 break;
120 case 'ast':
121 // skip printing ast as we already write fixture output JS
122 printed = '(ast)';
123 break;
124 }
125
126 if (printed !== lastLogged) {
127 lastLogged = printed;
128 console.log(`${chalk.green(value.name)}:\n ${printed}\n`);
129 } else {
130 console.log(`${chalk.blue(value.name)}: (no change)\n`);
131 }
132 }
133 : () => {};
134
135 // only try logging if we filtered out all but one fixture,
136 // since console log order is non-deterministic
137 const result = await transformFixtureInput(
138 input,
139 fixturePath,
140 parseConfigPragmaForTests,
141 BabelPluginReactCompiler,
142 includeEvaluator,
143 debugIRLogger,
144 EffectEnum,
145 ValueKindEnum,
146 ValueReasonEnum,
147 );
148
149 if (result.kind === 'err') {
150 error = result.msg;
151 } else {
152 compileResult = result.value;
153 }
154 } catch (e) {
155 if (shouldLog) {
156 console.error(e.stack);
157 }
158 error = e.message.replace(/\u001b[^m]*m/g, '');
159 }
160
161 // Promote console errors so they can be recorded in fixture output
162 for (const consoleError of seenConsoleErrors) {
163 if (error != null) {
164 error = `${error}\n\n${consoleError}`;
165 } else {
166 error = `ConsoleError: ${consoleError}`;
167 }
168 }
169 console.error = originalConsoleError;
170
171 return {
172 error,
173 compileResult,
174 };
175 }
176
177 export async function transformFixture(
178 fixture: TestFixture,
179 compilerVersion: number,
180 shouldLog: boolean,
181 includeEvaluator: boolean,
182 enableRust: boolean = false,
183 ): Promise<TestResult> {
184 const {input, snapshot: expected, snapshotPath: outputPath} = fixture;
185 const basename = getBasename(fixture);
186 const expectError = isExpectError(fixture);
187
188 // Input will be null if the input file did not exist, in which case the output file
189 // is stale
190 if (input === null) {
191 return {
192 outputPath,
193 actual: null,
194 expected,
195 unexpectedError: null,
196 };
197 }
198 const {compileResult, error} = await compile(
199 input,
200 fixture.fixturePath,
201 compilerVersion,
202 shouldLog,
203 includeEvaluator,
204 enableRust,
205 );
206
207 let unexpectedError: string | null = null;
208 if (expectError) {
209 if (error === null) {
210 unexpectedError = `Expected an error to be thrown for fixture: \`${basename}\`, remove the 'error.' prefix if an error is not expected.`;
211 }
212 } else {
213 if (error !== null) {
214 unexpectedError = `Expected fixture \`${basename}\` to succeed but it failed with error:\n\n${error}`;
215 } else if (compileResult == null) {
216 unexpectedError = `Expected output for fixture \`${basename}\`.`;
217 }
218 }
219
220 const snapOutput: string | null = compileResult?.forgetOutput ?? null;
221 let sproutOutput: string | null = null;
222 if (compileResult?.evaluatorCode != null) {
223 const sproutResult = runSprout(
224 compileResult.evaluatorCode.original,
225 compileResult.evaluatorCode.forget,
226 );
227 if (sproutResult.kind === 'invalid') {
228 unexpectedError ??= '';
229 unexpectedError += `\n\n${sproutResult.value}`;
230 } else {
231 sproutOutput = sproutResult.value;
232 }
233 } else if (!includeEvaluator && expected != null) {
234 sproutOutput = expected.split('\n### Eval output\n')[1];
235 }
236
237 const actualOutput = writeOutputToString(
238 input,
239 snapOutput,
240 sproutOutput,
241 compileResult?.logs ?? null,
242 error,
243 );
244
245 return {
246 outputPath,
247 actual: actualOutput,
248 expected,
249 unexpectedError,
250 };
251 }