main
ts 350 lines 9.4 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 {parse as babelParse, ParseResult} from '@babel/parser';
9 import * as HermesParser from 'hermes-parser';
10 import * as t from '@babel/types';
11 import BabelPluginReactCompiler, {
12 CompilerError,
13 CompilerErrorDetail,
14 CompilerDiagnostic,
15 Effect,
16 ErrorCategory,
17 parseConfigPragmaForTests,
18 ValueKind,
19 type CompilerDiagnosticDetail,
20 type CompilerErrorDetailOptions,
21 type Hook,
22 PluginOptions,
23 CompilerPipelineValue,
24 parsePluginOptions,
25 printReactiveFunctionWithOutlined,
26 printFunctionWithOutlined,
27 type LoggerEvent,
28 } from 'babel-plugin-react-compiler';
29 import {transformFromAstSync} from '@babel/core';
30 import JSON5 from 'json5';
31 import type {
32 CompilerOutput,
33 CompilerTransformOutput,
34 PrintedCompilerPipelineValue,
35 } from '../components/Editor/Output';
36
37 type LoggedCompileErrorDetail = Extract<
38 LoggerEvent,
39 {kind: 'CompileError'}
40 >['detail'];
41
42 /**
43 * logEvent() emits error details as plain objects (normalized for parity with
44 * the Rust compiler's logger output), not class instances. Rehydrate them into
45 * CompilerDiagnostic / CompilerErrorDetail so downstream consumers (error
46 * printing, Monaco diagnostics) can call methods like printErrorMessage().
47 */
48 function rehydrateLoggedDetail(
49 detail: LoggedCompileErrorDetail,
50 ): CompilerErrorDetail | CompilerDiagnostic {
51 const category = detail.category as ErrorCategory;
52 const suggestions =
53 (detail.suggestions as CompilerErrorDetailOptions['suggestions']) ?? null;
54 if (detail.details != null) {
55 return new CompilerDiagnostic({
56 category,
57 reason: detail.reason,
58 description: detail.description,
59 suggestions,
60 details: detail.details.map((d): CompilerDiagnosticDetail => {
61 if (d.kind === 'hint') {
62 return {kind: 'hint', message: d.message ?? ''};
63 }
64 return {kind: 'error', loc: d.loc, message: d.message};
65 }),
66 });
67 }
68 return new CompilerErrorDetail({
69 category,
70 reason: detail.reason,
71 description: detail.description,
72 loc: detail.loc ?? null,
73 suggestions,
74 });
75 }
76
77 function parseInput(
78 input: string,
79 language: 'flow' | 'typescript',
80 ): ParseResult<t.File> {
81 // Extract the first line to quickly check for custom test directives
82 if (language === 'flow') {
83 return HermesParser.parse(input, {
84 babel: true,
85 flow: 'all',
86 sourceType: 'module',
87 enableExperimentalComponentSyntax: true,
88 });
89 } else {
90 return babelParse(input, {
91 plugins: ['typescript', 'jsx'],
92 sourceType: 'module',
93 }) as ParseResult<t.File>;
94 }
95 }
96
97 function invokeCompiler(
98 source: string,
99 language: 'flow' | 'typescript',
100 options: PluginOptions,
101 ): CompilerTransformOutput {
102 const ast = parseInput(source, language);
103 let result = transformFromAstSync(ast, source, {
104 filename: '_playgroundFile.js',
105 highlightCode: false,
106 retainLines: true,
107 plugins: [[BabelPluginReactCompiler, options]],
108 ast: true,
109 sourceType: 'module',
110 configFile: false,
111 sourceMaps: true,
112 babelrc: false,
113 });
114 if (result?.ast == null || result?.code == null || result?.map == null) {
115 throw new Error('Expected successful compilation');
116 }
117 return {
118 code: result.code,
119 sourceMaps: result.map,
120 language,
121 };
122 }
123
124 const COMMON_HOOKS: Array<[string, Hook]> = [
125 [
126 'useFragment',
127 {
128 valueKind: ValueKind.Frozen,
129 effectKind: Effect.Freeze,
130 noAlias: true,
131 transitiveMixedData: true,
132 },
133 ],
134 [
135 'usePaginationFragment',
136 {
137 valueKind: ValueKind.Frozen,
138 effectKind: Effect.Freeze,
139 noAlias: true,
140 transitiveMixedData: true,
141 },
142 ],
143 [
144 'useRefetchableFragment',
145 {
146 valueKind: ValueKind.Frozen,
147 effectKind: Effect.Freeze,
148 noAlias: true,
149 transitiveMixedData: true,
150 },
151 ],
152 [
153 'useLazyLoadQuery',
154 {
155 valueKind: ValueKind.Frozen,
156 effectKind: Effect.Freeze,
157 noAlias: true,
158 transitiveMixedData: true,
159 },
160 ],
161 [
162 'usePreloadedQuery',
163 {
164 valueKind: ValueKind.Frozen,
165 effectKind: Effect.Freeze,
166 noAlias: true,
167 transitiveMixedData: true,
168 },
169 ],
170 ];
171
172 export function parseConfigOverrides(configOverrides: string): any {
173 const trimmed = configOverrides.trim();
174 if (!trimmed) {
175 return {};
176 }
177 return JSON5.parse(trimmed);
178 }
179
180 function parseOptions(
181 source: string,
182 mode: 'compiler' | 'linter',
183 configOverrides: string,
184 ): PluginOptions {
185 // Extract the first line to quickly check for custom test directives
186 const pragma = source.substring(0, source.indexOf('\n'));
187
188 const parsedPragmaOptions = parseConfigPragmaForTests(pragma, {
189 compilationMode: 'infer',
190 environment:
191 mode === 'linter'
192 ? {
193 // enabled in compiler
194 validateRefAccessDuringRender: false,
195 // enabled in linter
196 validateNoSetStateInRender: true,
197 validateNoSetStateInEffects: true,
198 validateNoJSXInTryStatements: true,
199 validateNoImpureFunctionsInRender: true,
200 validateStaticComponents: true,
201 validateNoFreezingKnownMutableFunctions: true,
202 validateNoVoidUseMemo: true,
203 }
204 : {
205 /* use defaults for compiler mode */
206 },
207 });
208
209 // Parse config overrides from config editor
210 const configOverrideOptions = parseConfigOverrides(configOverrides);
211
212 const opts: PluginOptions = parsePluginOptions({
213 ...parsedPragmaOptions,
214 ...configOverrideOptions,
215 environment: {
216 ...parsedPragmaOptions.environment,
217 ...configOverrideOptions.environment,
218 customHooks: new Map([...COMMON_HOOKS]),
219 },
220 });
221
222 return opts;
223 }
224
225 export function compile(
226 source: string,
227 mode: 'compiler' | 'linter',
228 configOverrides: string,
229 ): [CompilerOutput, 'flow' | 'typescript', PluginOptions | null] {
230 const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
231 const error = new CompilerError();
232 const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
233 const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
234 const entry = results.get(result.name);
235 if (Array.isArray(entry)) {
236 entry.push(result);
237 } else {
238 results.set(result.name, [result]);
239 }
240 };
241 let language: 'flow' | 'typescript';
242 if (source.match(/\@flow/)) {
243 language = 'flow';
244 } else {
245 language = 'typescript';
246 }
247 let transformOutput;
248
249 let baseOpts: PluginOptions | null = null;
250 try {
251 baseOpts = parseOptions(source, mode, configOverrides);
252 } catch (err) {
253 error.details.push(
254 new CompilerErrorDetail({
255 category: ErrorCategory.Config,
256 reason: `Unexpected failure when transforming configs! \n${err}`,
257 loc: null,
258 suggestions: null,
259 }),
260 );
261 }
262 if (baseOpts) {
263 try {
264 const logIR = (result: CompilerPipelineValue): void => {
265 switch (result.kind) {
266 case 'ast': {
267 break;
268 }
269 case 'hir': {
270 upsert({
271 kind: 'hir',
272 fnName: result.value.id,
273 name: result.name,
274 value: printFunctionWithOutlined(result.value),
275 });
276 break;
277 }
278 case 'reactive': {
279 upsert({
280 kind: 'reactive',
281 fnName: result.value.id,
282 name: result.name,
283 value: printReactiveFunctionWithOutlined(result.value),
284 });
285 break;
286 }
287 case 'debug': {
288 upsert({
289 kind: 'debug',
290 fnName: null,
291 name: result.name,
292 value: result.value,
293 });
294 break;
295 }
296 default: {
297 const _: never = result;
298 throw new Error(`Unhandled result ${result}`);
299 }
300 }
301 };
302 // Add logger options to the parsed options
303 const opts = {
304 ...baseOpts,
305 logger: {
306 debugLogIRs: logIR,
307 logEvent: (_filename: string | null, event: LoggerEvent): void => {
308 if (event.kind === 'CompileError') {
309 otherErrors.push(rehydrateLoggedDetail(event.detail));
310 }
311 },
312 },
313 };
314 transformOutput = invokeCompiler(source, language, opts);
315 } catch (err) {
316 /**
317 * error might be an invariant violation or other runtime error
318 * (i.e. object shape that is not CompilerError)
319 */
320 if (err instanceof CompilerError && err.details.length > 0) {
321 error.merge(err);
322 } else {
323 /**
324 * Handle unexpected failures by logging (to get a stack trace)
325 * and reporting
326 */
327 error.details.push(
328 new CompilerErrorDetail({
329 category: ErrorCategory.Invariant,
330 reason: `Unexpected failure when transforming input! \n${err}`,
331 loc: null,
332 suggestions: null,
333 }),
334 );
335 }
336 }
337 }
338 // Only include logger errors if there weren't other errors
339 if (!error.hasErrors() && otherErrors.length !== 0) {
340 otherErrors.forEach(e => error.details.push(e));
341 }
342 if (error.hasErrors() || !transformOutput) {
343 return [{kind: 'err', results, error}, language, baseOpts];
344 }
345 return [
346 {kind: 'ok', results, transformOutput, errors: error.details},
347 language,
348 baseOpts,
349 ];
350 }