main
ts 429 lines 13.1 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 * as t from '@babel/types';
9 import {z} from 'zod/v4';
10 import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError';
11 import {
12 EnvironmentConfig,
13 ExternalFunction,
14 parseEnvironmentConfig,
15 tryParseExternalFunction,
16 } from '../HIR/Environment';
17 import {hasOwnProperty} from '../Utils/utils';
18 import {fromZodError} from 'zod-validation-error/v4';
19 import {CompilerPipelineValue} from './Pipeline';
20
21 const PanicThresholdOptionsSchema = z.enum([
22 /*
23 * Any errors will panic the compiler by throwing an exception, which will
24 * bubble up to the nearest exception handler above the Forget transform.
25 * If Forget is invoked through `BabelPluginReactCompiler`, this will at the least
26 * skip Forget compilation for the rest of current file.
27 */
28 'all_errors',
29 /*
30 * Panic by throwing an exception only on critical or unrecognized errors.
31 * For all other errors, skip the erroring function without inserting
32 * a Forget-compiled version (i.e. same behavior as noEmit).
33 */
34 'critical_errors',
35 // Never panic by throwing an exception.
36 'none',
37 ]);
38
39 export type PanicThresholdOptions = z.infer<typeof PanicThresholdOptionsSchema>;
40 const DynamicGatingOptionsSchema = z.object({
41 source: z.string(),
42 });
43 export type DynamicGatingOptions = z.infer<typeof DynamicGatingOptionsSchema>;
44 const CustomOptOutDirectiveSchema = z
45 .nullable(z.array(z.string()))
46 .default(null);
47 type CustomOptOutDirective = z.infer<typeof CustomOptOutDirectiveSchema>;
48
49 export type PluginOptions = Partial<{
50 environment: Partial<EnvironmentConfig>;
51
52 logger: Logger | null;
53
54 /*
55 * Specifying a `gating` config, makes Forget compile and emit a separate
56 * version of the function gated by importing the `gating.importSpecifierName` from the
57 * specified `gating.source`.
58 *
59 * For example:
60 * gating: {
61 * source: 'ReactForgetFeatureFlag',
62 * importSpecifierName: 'isForgetEnabled_Pokes',
63 * }
64 *
65 * produces:
66 * import {isForgetEnabled_Pokes} from 'ReactForgetFeatureFlag';
67 *
68 * Foo_forget() {}
69 *
70 * Foo_uncompiled() {}
71 *
72 * var Foo = isForgetEnabled_Pokes() ? Foo_forget : Foo_uncompiled;
73 */
74 gating: ExternalFunction | null;
75
76 /**
77 * If specified, this enables dynamic gating which matches `use memo if(...)`
78 * directives.
79 *
80 * Example usage:
81 * ```js
82 * // @dynamicGating:{"source":"myModule"}
83 * export function MyComponent() {
84 * 'use memo if(isEnabled)';
85 * return <div>...</div>;
86 * }
87 * ```
88 * This will emit:
89 * ```js
90 * import {isEnabled} from 'myModule';
91 * export const MyComponent = isEnabled()
92 * ? <optimized version>
93 * : <original version>;
94 * ```
95 */
96 dynamicGating: DynamicGatingOptions | null;
97
98 panicThreshold: PanicThresholdOptions;
99
100 /**
101 * @deprecated
102 *
103 * When enabled, Forget will continue statically analyzing and linting code, but skip over codegen
104 * passes.
105 *
106 * NOTE: ignored if `outputMode` is specified
107 *
108 * Defaults to false
109 */
110 noEmit: boolean;
111
112 /**
113 * If specified, overrides `noEmit` and controls the output mode of the compiler.
114 *
115 * Defaults to null
116 */
117 outputMode: CompilerOutputMode | null;
118
119 /*
120 * Determines the strategy for determining which functions to compile. Note that regardless of
121 * which mode is enabled, a component can be opted out by adding the string literal
122 * `"use no forget"` at the top of the function body, eg.:
123 *
124 * ```
125 * function ComponentYouWantToSkipCompilation(props) {
126 * "use no forget";
127 * ...
128 * }
129 * ```
130 */
131 compilationMode: CompilationMode;
132
133 /**
134 * By default React Compiler will skip compilation of code that suppresses the default
135 * React ESLint rules, since this is a strong indication that the code may be breaking React rules
136 * in some way.
137 *
138 * Use eslintSuppressionRules to pass a custom set of rule names: any code which suppresses the
139 * provided rules will skip compilation. To disable this feature (never bailout of compilation
140 * even if the default ESLint is suppressed), pass an empty array.
141 */
142 eslintSuppressionRules: Array<string> | null | undefined;
143
144 /**
145 * Whether to report "suppression" errors for Flow suppressions. If false, suppression errors
146 * are only emitted for ESLint suppressions
147 */
148 flowSuppressions: boolean;
149
150 /*
151 * Ignore 'use no forget' annotations. Helpful during testing but should not be used in production.
152 */
153 ignoreUseNoForget: boolean;
154
155 /**
156 * Unstable / do not use
157 */
158 customOptOutDirectives: CustomOptOutDirective;
159
160 sources: Array<string> | ((filename: string) => boolean) | null;
161
162 /**
163 * The compiler has customized support for react-native-reanimated, intended as a temporary workaround.
164 * Set this flag (on by default) to automatically check for this library and activate the support.
165 */
166 enableReanimatedCheck: boolean;
167
168 /**
169 * The minimum major version of React that the compiler should emit code for. If the target is 19
170 * or higher, the compiler emits direct imports of React runtime APIs needed by the compiler. On
171 * versions prior to 19, an extra runtime package react-compiler-runtime is necessary to provide
172 * a userspace approximation of runtime APIs.
173 */
174 target: CompilerReactTarget;
175 }>;
176
177 export type ParsedPluginOptions = Required<
178 Omit<PluginOptions, 'environment'>
179 > & {environment: EnvironmentConfig};
180
181 const CompilerReactTargetSchema = z.union([
182 z.literal('17'),
183 z.literal('18'),
184 z.literal('19'),
185 /**
186 * Used exclusively for Meta apps which are guaranteed to have compatible
187 * react runtime and compiler versions. Note that only the FB-internal bundles
188 * re-export useMemoCache (see
189 * https://github.com/facebook/react/blob/5b0ef217ef32333a8e56f39be04327c89efa346f/packages/react/index.fb.js#L68-L70),
190 * so this option is invalid / creates runtime errors for open-source users.
191 */
192 z.object({
193 kind: z.literal('donotuse_meta_internal'),
194 runtimeModule: z.string().default('react'),
195 }),
196 ]);
197 export type CompilerReactTarget = z.infer<typeof CompilerReactTargetSchema>;
198
199 const CompilationModeSchema = z.enum([
200 /*
201 * Compiles functions annotated with "use forget" or component/hook-like functions.
202 * This latter includes:
203 * * Components declared with component syntax.
204 * * Functions which can be inferred to be a component or hook:
205 * - Be named like a hook or component. This logic matches the ESLint rule.
206 * - *and* create JSX and/or call a hook. This is an additional check to help prevent
207 * false positives, since compilation has a greater impact than linting.
208 * This is the default mode
209 */
210 'infer',
211 // Compile only components using Flow component syntax and hooks using hook syntax.
212 'syntax',
213 // Compile only functions which are explicitly annotated with "use forget"
214 'annotation',
215 // Compile all top-level functions
216 'all',
217 ]);
218
219 export type CompilationMode = z.infer<typeof CompilationModeSchema>;
220
221 const CompilerOutputModeSchema = z.enum([
222 // Build optimized for SSR, with client features removed
223 'ssr',
224 // Build optimized for the client, with auto memoization
225 'client',
226 // Lint mode, the output is unused but validations should run
227 'lint',
228 ]);
229
230 export type CompilerOutputMode = z.infer<typeof CompilerOutputModeSchema>;
231
232 /**
233 * Represents 'events' that may occur during compilation. Events are only
234 * recorded when a logger is set (through the config).
235 * These are the different types of events:
236 * CompileError:
237 * Forget skipped compilation of a function / file due to a known todo,
238 * invalid input, or compiler invariant being broken.
239 * CompileSuccess:
240 * Forget successfully compiled a function.
241 * PipelineError:
242 * Unexpected errors that occurred during compilation (e.g. failures in
243 * babel or other unhandled exceptions).
244 */
245 export type LoggerEvent =
246 | CompileSuccessEvent
247 | CompileErrorEvent
248 | CompileDiagnosticEvent
249 | CompileSkipEvent
250 | CompileUnexpectedThrowEvent
251 | PipelineErrorEvent
252 | TimingEvent;
253
254 export type CompileErrorDetail = {
255 category: string;
256 reason: string;
257 description: string | null;
258 severity: string;
259 suggestions: Array<unknown> | null;
260 details?: Array<{
261 kind: string;
262 loc: t.SourceLocation | null;
263 message: string | null;
264 }>;
265 loc?: t.SourceLocation | null;
266 };
267 export type CompileErrorEvent = {
268 kind: 'CompileError';
269 fnLoc: t.SourceLocation | null;
270 detail: CompileErrorDetail;
271 };
272 export type CompileDiagnosticEvent = {
273 kind: 'CompileDiagnostic';
274 fnLoc: t.SourceLocation | null;
275 detail: Omit<Omit<CompilerErrorDetailOptions, 'severity'>, 'suggestions'>;
276 };
277 export type CompileSuccessEvent = {
278 kind: 'CompileSuccess';
279 fnLoc: t.SourceLocation | null;
280 fnName: string | null;
281 memoSlots: number;
282 memoBlocks: number;
283 memoValues: number;
284 prunedMemoBlocks: number;
285 prunedMemoValues: number;
286 };
287 export type CompileSkipEvent = {
288 kind: 'CompileSkip';
289 fnLoc: t.SourceLocation | null;
290 reason: string;
291 loc: t.SourceLocation | null;
292 };
293 export type PipelineErrorEvent = {
294 kind: 'PipelineError';
295 fnLoc: t.SourceLocation | null;
296 data: string;
297 };
298 export type CompileUnexpectedThrowEvent = {
299 kind: 'CompileUnexpectedThrow';
300 fnLoc: t.SourceLocation | null;
301 data: string;
302 };
303 export type TimingEvent = {
304 kind: 'Timing';
305 measurement: PerformanceMeasure;
306 };
307 export type Logger = {
308 logEvent: (filename: string | null, event: LoggerEvent) => void;
309 debugLogIRs?: (value: CompilerPipelineValue) => void;
310 };
311
312 export const defaultOptions: ParsedPluginOptions = {
313 compilationMode: 'infer',
314 panicThreshold: 'none',
315 environment: parseEnvironmentConfig({}).unwrap(),
316 logger: null,
317 gating: null,
318 noEmit: false,
319 outputMode: null,
320 dynamicGating: null,
321 eslintSuppressionRules: null,
322 flowSuppressions: true,
323 ignoreUseNoForget: false,
324 sources: filename => {
325 return filename.indexOf('node_modules') === -1;
326 },
327 enableReanimatedCheck: true,
328 customOptOutDirectives: null,
329 target: '19',
330 };
331
332 export function parsePluginOptions(obj: unknown): ParsedPluginOptions {
333 if (obj == null || typeof obj !== 'object') {
334 return defaultOptions;
335 }
336 const parsedOptions = Object.create(null);
337 for (let [key, value] of Object.entries(obj)) {
338 if (typeof value === 'string') {
339 // normalize string configs to be case insensitive
340 value = value.toLowerCase();
341 }
342 if (isCompilerFlag(key)) {
343 switch (key) {
344 case 'environment': {
345 const environmentResult = parseEnvironmentConfig(value);
346 if (environmentResult.isErr()) {
347 CompilerError.throwInvalidConfig({
348 reason:
349 'Error in validating environment config. This is an advanced setting and not meant to be used directly',
350 description: environmentResult.unwrapErr().toString(),
351 suggestions: null,
352 loc: null,
353 });
354 }
355 parsedOptions[key] = environmentResult.unwrap();
356 break;
357 }
358 case 'target': {
359 parsedOptions[key] = parseTargetConfig(value);
360 break;
361 }
362 case 'gating': {
363 if (value == null) {
364 parsedOptions[key] = null;
365 } else {
366 parsedOptions[key] = tryParseExternalFunction(value);
367 }
368 break;
369 }
370 case 'dynamicGating': {
371 if (value == null) {
372 parsedOptions[key] = null;
373 } else {
374 const result = DynamicGatingOptionsSchema.safeParse(value);
375 if (result.success) {
376 parsedOptions[key] = result.data;
377 } else {
378 CompilerError.throwInvalidConfig({
379 reason:
380 'Could not parse dynamic gating. Update React Compiler config to fix the error',
381 description: `${fromZodError(result.error)}`,
382 loc: null,
383 suggestions: null,
384 });
385 }
386 }
387 break;
388 }
389 case 'customOptOutDirectives': {
390 const result = CustomOptOutDirectiveSchema.safeParse(value);
391 if (result.success) {
392 parsedOptions[key] = result.data;
393 } else {
394 CompilerError.throwInvalidConfig({
395 reason:
396 'Could not parse custom opt out directives. Update React Compiler config to fix the error',
397 description: `${fromZodError(result.error)}`,
398 loc: null,
399 suggestions: null,
400 });
401 }
402 break;
403 }
404 default: {
405 parsedOptions[key] = value;
406 }
407 }
408 }
409 }
410 return {...defaultOptions, ...parsedOptions};
411 }
412
413 export function parseTargetConfig(value: unknown): CompilerReactTarget {
414 const parsed = CompilerReactTargetSchema.safeParse(value);
415 if (parsed.success) {
416 return parsed.data;
417 } else {
418 CompilerError.throwInvalidConfig({
419 reason: 'Not a valid target',
420 description: `${fromZodError(parsed.error)}`,
421 suggestions: null,
422 loc: null,
423 });
424 }
425 }
426
427 function isCompilerFlag(s: string): s is keyof PluginOptions {
428 return hasOwnProperty(defaultOptions, s);
429 }