| 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 | } |