@samitouri / QOS-React-2 / commits / f9ae0a4c2e

[compiler][gating] Custom opt out directives (experimental option) (#33328)

Adding an experimental / unstable compiler config to enable custom opt-out directives

mofeiZ committed May 27, 2025 at 12:02 UTC f9ae0a4c2edc9ad93507b550f2aeb60119955336
5 files changed +104 -21
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts
+25
@@ -41,6 +41,10 @@ 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 = {
50 environment: EnvironmentConfig;
@@ -132,6 +136,11 @@ export type PluginOptions = {
136 */
137 ignoreUseNoForget: boolean;
138
139 + /**
140 + * Unstable / do not use
141 + */
142 + customOptOutDirectives: CustomOptOutDirective;
143 +
144 sources: Array<string> | ((filename: string) => boolean) | null;
145
146 /**
@@ -278,6 +287,7 @@ export const defaultOptions: PluginOptions = {
287 return filename.indexOf('node_modules') === -1;
288 },
289 enableReanimatedCheck: true,
290 + customOptOutDirectives: null,
291 target: '19',
292 } as const;
293
@@ -338,6 +348,21 @@ export function parsePluginOptions(obj: unknown): PluginOptions {
348 }
349 break;
350 }
351 + case 'customOptOutDirectives': {
352 + const result = CustomOptOutDirectiveSchema.safeParse(value);
353 + if (result.success) {
354 + parsedOptions[key] = result.data;
355 + } else {
356 + CompilerError.throwInvalidConfig({
357 + reason:
358 + 'Could not parse custom opt out directives. Update React Compiler config to fix the error',
359 + description: `${fromZodError(result.error)}`,
360 + loc: null,
361 + suggestions: null,
362 + });
363 + }
364 + break;
365 + }
366 default: {
367 parsedOptions[key] = value;
368 }
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts
+15 -2
@@ -63,7 +63,16 @@ export function tryFindDirectiveEnablingMemoization(
63
64 export function findDirectiveDisablingMemoization(
65 directives: Array<t.Directive>,
66 + {customOptOutDirectives}: PluginOptions,
67 ): t.Directive | null {
68 + if (customOptOutDirectives != null) {
69 + return (
70 + directives.find(
71 + directive =>
72 + customOptOutDirectives.indexOf(directive.value.value) !== -1,
73 + ) ?? null
74 + );
75 + }
76 return (
77 directives.find(directive =>
78 OPT_OUT_DIRECTIVES.has(directive.value.value),
@@ -394,7 +403,8 @@ export function compileProgram(
403 code: pass.code,
404 suppressions,
405 hasModuleScopeOptOut:
397 - findDirectiveDisablingMemoization(program.node.directives) != null,
406 + findDirectiveDisablingMemoization(program.node.directives, pass.opts) !=
407 + null,
408 });
409
410 const queue: Array<CompileSource> = findFunctionsToCompile(
@@ -571,7 +581,10 @@ function processFn(
581 }
582 directives = {
583 optIn: optIn.unwrapOr(null),
574 - optOut: findDirectiveDisablingMemoization(fn.node.body.directives),
584 + optOut: findDirectiveDisablingMemoization(
585 + fn.node.body.directives,
586 + programContext.opts,
587 + ),
588 };
589 }
590
compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts
+19 -19
@@ -93,6 +93,21 @@ const testComplexConfigDefaults: PartialEnvironmentConfig = {
93 },
94 ],
95 };
96 +
97 +function* splitPragma(
98 + pragma: string,
99 +): Generator<{key: string; value: string | null}> {
100 + for (const entry of pragma.split('@')) {
101 + const keyVal = entry.trim();
102 + const valIdx = keyVal.indexOf(':');
103 + if (valIdx === -1) {
104 + yield {key: keyVal.split(' ', 1)[0], value: null};
105 + } else {
106 + yield {key: keyVal.slice(0, valIdx), value: keyVal.slice(valIdx + 1)};
107 + }
108 + }
109 +}
110 +
111 /**
112 * For snap test fixtures and playground only.
113 */
@@ -101,19 +116,11 @@ function parseConfigPragmaEnvironmentForTest(
116 ): EnvironmentConfig {
117 const maybeConfig: Partial<Record<keyof EnvironmentConfig, unknown>> = {};
118
104 - for (const token of pragma.split(' ')) {
105 - if (!token.startsWith('@')) {
106 - continue;
107 - }
108 - const keyVal = token.slice(1);
109 - const valIdx = keyVal.indexOf(':');
110 - const key = valIdx === -1 ? keyVal : keyVal.slice(0, valIdx);
111 - const val = valIdx === -1 ? undefined : keyVal.slice(valIdx + 1);
112 - const isSet = val === undefined || val === 'true';
119 + for (const {key, value: val} of splitPragma(pragma)) {
120 if (!hasOwnProperty(EnvironmentConfigSchema.shape, key)) {
121 continue;
122 }
116 -
123 + const isSet = val == null || val === 'true';
124 if (isSet && key in testComplexConfigDefaults) {
125 maybeConfig[key] = testComplexConfigDefaults[key];
126 } else if (isSet) {
@@ -176,18 +183,11 @@ export function parseConfigPragmaForTests(
183 compilationMode: defaults.compilationMode,
184 environment,
185 };
179 - for (const token of pragma.split(' ')) {
180 - if (!token.startsWith('@')) {
181 - continue;
182 - }
183 - const keyVal = token.slice(1);
184 - const idx = keyVal.indexOf(':');
185 - const key = idx === -1 ? keyVal : keyVal.slice(0, idx);
186 - const val = idx === -1 ? undefined : keyVal.slice(idx + 1);
186 + for (const {key, value: val} of splitPragma(pragma)) {
187 if (!hasOwnProperty(defaultOptions, key)) {
188 continue;
189 }
190 - const isSet = val === undefined || val === 'true';
190 + const isSet = val == null || val === 'true';
191 if (isSet && key in testComplexPluginOptionDefaults) {
192 options[key] = testComplexPluginOptionDefaults[key];
193 } else if (isSet) {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/custom-opt-out-directive.expect.md new
+35
@@ -0,0 +1,35 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @customOptOutDirectives:["use todo memo"]
6 +function Component() {
7 + 'use todo memo';
8 + return <div>hello world!</div>;
9 +}
10 +
11 +export const FIXTURE_ENTRYPOINT = {
12 + fn: Component,
13 + params: [],
14 +};
15 +
16 +```
17 +
18 +## Code
19 +
20 +```javascript
21 +// @customOptOutDirectives:["use todo memo"]
22 +function Component() {
23 + "use todo memo";
24 + return <div>hello world!</div>;
25 +}
26 +
27 +export const FIXTURE_ENTRYPOINT = {
28 + fn: Component,
29 + params: [],
30 +};
31 +
32 +```
33 +
34 +### Eval output
35 +(kind: ok) <div>hello world!</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/custom-opt-out-directive.tsx new
+10
@@ -0,0 +1,10 @@
1 +// @customOptOutDirectives:["use todo memo"]
2 +function Component() {
3 + 'use todo memo';
4 + return <div>hello world!</div>;
5 +}
6 +
7 +export const FIXTURE_ENTRYPOINT = {
8 + fn: Component,
9 + params: [],
10 +};