[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
+};