/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import * as t from '@babel/types';
import {z} from 'zod';
import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError';
import {
EnvironmentConfig,
ExternalFunction,
parseEnvironmentConfig,
tryParseExternalFunction,
} from '../HIR/Environment';
import {hasOwnProperty} from '../Utils/utils';
import {fromZodError} from 'zod-validation-error';
import {CompilerPipelineValue} from './Pipeline';
const PanicThresholdOptionsSchema = z.enum([
/*
* Any errors will panic the compiler by throwing an exception, which will
* bubble up to the nearest exception handler above the Forget transform.
* If Forget is invoked through `BabelPluginReactCompiler`, this will at the least
* skip Forget compilation for the rest of current file.
*/
'all_errors',
/*
* Panic by throwing an exception only on critical or unrecognized errors.
* For all other errors, skip the erroring function without inserting
* a Forget-compiled version (i.e. same behavior as noEmit).
*/
'critical_errors',
// Never panic by throwing an exception.
'none',
]);
export type PanicThresholdOptions = z.infer<typeof PanicThresholdOptionsSchema>;
const DynamicGatingOptionsSchema = z.object({
source: z.string(),
});
export type DynamicGatingOptions = z.infer<typeof DynamicGatingOptionsSchema>;
const CustomOptOutDirectiveSchema = z
.nullable(z.array(z.string()))
.default(null);
type CustomOptOutDirective = z.infer<typeof CustomOptOutDirectiveSchema>;
export type PluginOptions = {
environment: EnvironmentConfig;
logger: Logger | null;
/*
* Specifying a `gating` config, makes Forget compile and emit a separate
* version of the function gated by importing the `gating.importSpecifierName` from the
* specified `gating.source`.
*
* For example:
* gating: {
* source: 'ReactForgetFeatureFlag',
* importSpecifierName: 'isForgetEnabled_Pokes',
* }
*
* produces:
* import {isForgetEnabled_Pokes} from 'ReactForgetFeatureFlag';
*
* Foo_forget() {}
*
* Foo_uncompiled() {}
*
* var Foo = isForgetEnabled_Pokes() ? Foo_forget : Foo_uncompiled;
*/
gating: ExternalFunction | null;
/**
* If specified, this enables dynamic gating which matches `use memo if(...)`
* directives.
*
* Example usage:
* ```js
* // @dynamicGating:{"source":"myModule"}
* export function MyComponent() {
* 'use memo if(isEnabled)';
* return <div>...</div>;
* }
* ```
* This will emit:
* ```js
* import {isEnabled} from 'myModule';
* export const MyComponent = isEnabled()
* ? <optimized version>
* : <original version>;
* ```
*/
dynamicGating: DynamicGatingOptions | null;
panicThreshold: PanicThresholdOptions;
/*
* When enabled, Forget will continue statically analyzing and linting code, but skip over codegen
* passes.
*
* Defaults to false
*/
noEmit: boolean;
/*
* Determines the strategy for determining which functions to compile. Note that regardless of
* which mode is enabled, a component can be opted out by adding the string literal
* `"use no forget"` at the top of the function body, eg.:
*
* ```
* function ComponentYouWantToSkipCompilation(props) {
* "use no forget";
* ...
* }
* ```
*/
compilationMode: CompilationMode;
/**
* By default React Compiler will skip compilation of code that suppresses the default
* React ESLint rules, since this is a strong indication that the code may be breaking React rules
* in some way.
*
* Use eslintSuppressionRules to pass a custom set of rule names: any code which suppresses the
* provided rules will skip compilation. To disable this feature (never bailout of compilation
* even if the default ESLint is suppressed), pass an empty array.
*/
eslintSuppressionRules: Array<string> | null | undefined;
flowSuppressions: boolean;
/*
* Ignore 'use no forget' annotations. Helpful during testing but should not be used in production.
*/
ignoreUseNoForget: boolean;
/**
* Unstable / do not use
*/
customOptOutDirectives: CustomOptOutDirective;
sources: Array<string> | ((filename: string) => boolean) | null;
/**
* The compiler has customized support for react-native-reanimated, intended as a temporary workaround.
* Set this flag (on by default) to automatically check for this library and activate the support.
*/
enableReanimatedCheck: boolean;
/**
* The minimum major version of React that the compiler should emit code for. If the target is 19
* or higher, the compiler emits direct imports of React runtime APIs needed by the compiler. On
* versions prior to 19, an extra runtime package react-compiler-runtime is necessary to provide
* a userspace approximation of runtime APIs.
*/
target: CompilerReactTarget;
};
const CompilerReactTargetSchema = z.union([
z.literal('17'),
z.literal('18'),
z.literal('19'),
/**
* Used exclusively for Meta apps which are guaranteed to have compatible
* react runtime and compiler versions. Note that only the FB-internal bundles
* re-export useMemoCache (see
* https://github.com/facebook/react/blob/5b0ef217ef32333a8e56f39be04327c89efa346f/packages/react/index.fb.js#L68-L70),
* so this option is invalid / creates runtime errors for open-source users.
*/
z.object({
kind: z.literal('donotuse_meta_internal'),
runtimeModule: z.string().default('react'),
}),
]);
export type CompilerReactTarget = z.infer<typeof CompilerReactTargetSchema>;
const CompilationModeSchema = z.enum([
/*
* Compiles functions annotated with "use forget" or component/hook-like functions.
* This latter includes:
* * Components declared with component syntax.
* * Functions which can be inferred to be a component or hook:
* - Be named like a hook or component. This logic matches the ESLint rule.
* - *and* create JSX and/or call a hook. This is an additional check to help prevent
* false positives, since compilation has a greater impact than linting.
* This is the default mode
*/
'infer',
// Compile only components using Flow component syntax and hooks using hook syntax.
'syntax',
// Compile only functions which are explicitly annotated with "use forget"
'annotation',
// Compile all top-level functions
'all',
]);
export type CompilationMode = z.infer<typeof CompilationModeSchema>;
/**
* Represents 'events' that may occur during compilation. Events are only
* recorded when a logger is set (through the config).
* These are the different types of events:
* CompileError:
* Forget skipped compilation of a function / file due to a known todo,
* invalid input, or compiler invariant being broken.
* CompileSuccess:
* Forget successfully compiled a function.
* PipelineError:
* Unexpected errors that occurred during compilation (e.g. failures in
* babel or other unhandled exceptions).
*/
export type LoggerEvent =
| CompileSuccessEvent
| CompileErrorEvent
| CompileDiagnosticEvent
| CompileSkipEvent
| PipelineErrorEvent
| TimingEvent
| AutoDepsDecorationsEvent
| AutoDepsEligibleEvent;
export type CompileErrorEvent = {
kind: 'CompileError';
fnLoc: t.SourceLocation | null;
detail: CompilerErrorDetailOptions;
};
export type CompileDiagnosticEvent = {
kind: 'CompileDiagnostic';
fnLoc: t.SourceLocation | null;
detail: Omit<Omit<CompilerErrorDetailOptions, 'severity'>, 'suggestions'>;
};
export type CompileSuccessEvent = {
kind: 'CompileSuccess';
fnLoc: t.SourceLocation | null;
fnName: string | null;
memoSlots: number;
memoBlocks: number;
memoValues: number;
prunedMemoBlocks: number;
prunedMemoValues: number;
};
export type CompileSkipEvent = {
kind: 'CompileSkip';
fnLoc: t.SourceLocation | null;
reason: string;
loc: t.SourceLocation | null;
};
export type PipelineErrorEvent = {
kind: 'PipelineError';
fnLoc: t.SourceLocation | null;
data: string;
};
export type TimingEvent = {
kind: 'Timing';
measurement: PerformanceMeasure;
};
export type AutoDepsDecorationsEvent = {
kind: 'AutoDepsDecorations';
fnLoc: t.SourceLocation;
decorations: Array<t.SourceLocation>;
};
export type AutoDepsEligibleEvent = {
kind: 'AutoDepsEligible';
fnLoc: t.SourceLocation;
depArrayLoc: t.SourceLocation;
};
export type Logger = {
logEvent: (filename: string | null, event: LoggerEvent) => void;
debugLogIRs?: (value: CompilerPipelineValue) => void;
};
export const defaultOptions: PluginOptions = {
compilationMode: 'infer',
panicThreshold: 'none',
environment: parseEnvironmentConfig({}).unwrap(),
logger: null,
gating: null,
noEmit: false,
dynamicGating: null,
eslintSuppressionRules: null,
flowSuppressions: true,
ignoreUseNoForget: false,
sources: filename => {
return filename.indexOf('node_modules') === -1;
},
enableReanimatedCheck: true,
customOptOutDirectives: null,
target: '19',
} as const;
export function parsePluginOptions(obj: unknown): PluginOptions {
if (obj == null || typeof obj !== 'object') {
return defaultOptions;
}
const parsedOptions = Object.create(null);
for (let [key, value] of Object.entries(obj)) {
if (typeof value === 'string') {
// normalize string configs to be case insensitive
value = value.toLowerCase();
}
if (isCompilerFlag(key)) {
switch (key) {
case 'environment': {
const environmentResult = parseEnvironmentConfig(value);
if (environmentResult.isErr()) {
CompilerError.throwInvalidConfig({
reason:
'Error in validating environment config. This is an advanced setting and not meant to be used directly',
description: environmentResult.unwrapErr().toString(),
suggestions: null,
loc: null,
});
}
parsedOptions[key] = environmentResult.unwrap();
break;
}
case 'target': {
parsedOptions[key] = parseTargetConfig(value);
break;
}
case 'gating': {
if (value == null) {
parsedOptions[key] = null;
} else {
parsedOptions[key] = tryParseExternalFunction(value);
}
break;
}
case 'dynamicGating': {
if (value == null) {
parsedOptions[key] = null;
} else {
const result = DynamicGatingOptionsSchema.safeParse(value);
if (result.success) {
parsedOptions[key] = result.data;
} else {
CompilerError.throwInvalidConfig({
reason:
'Could not parse dynamic gating. Update React Compiler config to fix the error',
description: `${fromZodError(result.error)}`,
loc: null,
suggestions: null,
});
}
}
break;
}
case 'customOptOutDirectives': {
const result = CustomOptOutDirectiveSchema.safeParse(value);
if (result.success) {
parsedOptions[key] = result.data;
} else {
CompilerError.throwInvalidConfig({
reason:
'Could not parse custom opt out directives. Update React Compiler config to fix the error',
description: `${fromZodError(result.error)}`,
loc: null,
suggestions: null,
});
}
break;
}
default: {
parsedOptions[key] = value;
}
}
}
}
return {...defaultOptions, ...parsedOptions};
}
export function parseTargetConfig(value: unknown): CompilerReactTarget {
const parsed = CompilerReactTargetSchema.safeParse(value);
if (parsed.success) {
return parsed.data;
} else {
CompilerError.throwInvalidConfig({
reason: 'Not a valid target',
description: `${fromZodError(parsed.error)}`,
suggestions: null,
loc: null,
});
}
}
function isCompilerFlag(s: string): s is keyof PluginOptions {
return hasOwnProperty(defaultOptions, s);
}