@samitouri / QOS-React-2 / commits / 459a2c4298

[compiler][gating] Experimental directive based gating (#33149)

Adds `dynamicGating` as an experimental option for testing rollout DX at Meta. If specified, this enables dynamic gating which matches `use memo if(...)` directives. #### Example usage Input file ```js // @dynamicGating:{"source":"myModule"} export function MyComponent() { 'use memo if(isEnabled)'; return <div>...</div>; } ``` Compiler output ```js import {isEnabled} from 'myModule'; export const MyComponent = isEnabled() ? <optimized version> : <original version>; ``` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33149). * __->__ #33149 * #33148

mofeiZ committed May 21, 2025 at 17:23 UTC 459a2c4298187cb0ee45605e2575ff35f4a81183
26 files changed +813 -22
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts
+46
@@ -37,6 +37,10 @@ const PanicThresholdOptionsSchema = z.enum([
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
45 export type PluginOptions = {
46 environment: EnvironmentConfig;
@@ -65,6 +69,28 @@ export type PluginOptions = {
69 */
70 gating: ExternalFunction | null;
71
72 + /**
73 + * If specified, this enables dynamic gating which matches `use memo if(...)`
74 + * directives.
75 + *
76 + * Example usage:
77 + * ```js
78 + * // @dynamicGating:{"source":"myModule"}
79 + * export function MyComponent() {
80 + * 'use memo if(isEnabled)';
81 + * return <div>...</div>;
82 + * }
83 + * ```
84 + * This will emit:
85 + * ```js
86 + * import {isEnabled} from 'myModule';
87 + * export const MyComponent = isEnabled()
88 + * ? <optimized version>
89 + * : <original version>;
90 + * ```
91 + */
92 + dynamicGating: DynamicGatingOptions | null;
93 +
94 panicThreshold: PanicThresholdOptions;
95
96 /*
@@ -244,6 +270,7 @@ export const defaultOptions: PluginOptions = {
270 logger: null,
271 gating: null,
272 noEmit: false,
273 + dynamicGating: null,
274 eslintSuppressionRules: null,
275 flowSuppressions: true,
276 ignoreUseNoForget: false,
@@ -292,6 +319,25 @@ export function parsePluginOptions(obj: unknown): PluginOptions {
319 }
320 break;
321 }
322 + case 'dynamicGating': {
323 + if (value == null) {
324 + parsedOptions[key] = null;
325 + } else {
326 + const result = DynamicGatingOptionsSchema.safeParse(value);
327 + if (result.success) {
328 + parsedOptions[key] = result.data;
329 + } else {
330 + CompilerError.throwInvalidConfig({
331 + reason:
332 + 'Could not parse dynamic gating. Update React Compiler config to fix the error',
333 + description: `${fromZodError(result.error)}`,
334 + loc: null,
335 + suggestions: null,
336 + });
337 + }
338 + }
339 + break;
340 + }
341 default: {
342 parsedOptions[key] = value;
343 }
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts
+120 -21
@@ -12,7 +12,7 @@ import {
12 CompilerErrorDetail,
13 ErrorSeverity,
14 } from '../CompilerError';
15 -import {ReactFunctionType} from '../HIR/Environment';
15 +import {ExternalFunction, ReactFunctionType} from '../HIR/Environment';
16 import {CodegenFunction} from '../ReactiveScopes';
17 import {isComponentDeclaration} from '../Utils/ComponentDeclaration';
18 import {isHookDeclaration} from '../Utils/HookDeclaration';
@@ -31,6 +31,7 @@ import {
31 suppressionsToCompilerError,
32 } from './Suppression';
33 import {GeneratedSource} from '../HIR';
34 +import {Err, Ok, Result} from '../Utils/Result';
35
36 export type CompilerPass = {
37 opts: PluginOptions;
@@ -40,15 +41,24 @@ export type CompilerPass = {
41 };
42 export const OPT_IN_DIRECTIVES = new Set(['use forget', 'use memo']);
43 export const OPT_OUT_DIRECTIVES = new Set(['use no forget', 'use no memo']);
44 +const DYNAMIC_GATING_DIRECTIVE = new RegExp('^use memo if\\(([^\\)]*)\\)$');
45
44 -export function findDirectiveEnablingMemoization(
46 +export function tryFindDirectiveEnablingMemoization(
47 directives: Array<t.Directive>,
46 -): t.Directive | null {
47 - return (
48 - directives.find(directive =>
49 - OPT_IN_DIRECTIVES.has(directive.value.value),
50 - ) ?? null
48 + opts: PluginOptions,
49 +): Result<t.Directive | null, CompilerError> {
50 + const optIn = directives.find(directive =>
51 + OPT_IN_DIRECTIVES.has(directive.value.value),
52 );
53 + if (optIn != null) {
54 + return Ok(optIn);
55 + }
56 + const dynamicGating = findDirectivesDynamicGating(directives, opts);
57 + if (dynamicGating.isOk()) {
58 + return Ok(dynamicGating.unwrap()?.directive ?? null);
59 + } else {
60 + return Err(dynamicGating.unwrapErr());
61 + }
62 }
63
64 export function findDirectiveDisablingMemoization(
@@ -60,6 +70,64 @@ export function findDirectiveDisablingMemoization(
70 ) ?? null
71 );
72 }
73 +function findDirectivesDynamicGating(
74 + directives: Array<t.Directive>,
75 + opts: PluginOptions,
76 +): Result<
77 + {
78 + gating: ExternalFunction;
79 + directive: t.Directive;
80 + } | null,
81 + CompilerError
82 +> {
83 + if (opts.dynamicGating === null) {
84 + return Ok(null);
85 + }
86 + const errors = new CompilerError();
87 + const result: Array<{directive: t.Directive; match: string}> = [];
88 +
89 + for (const directive of directives) {
90 + const maybeMatch = DYNAMIC_GATING_DIRECTIVE.exec(directive.value.value);
91 + if (maybeMatch != null && maybeMatch[1] != null) {
92 + if (t.isValidIdentifier(maybeMatch[1])) {
93 + result.push({directive, match: maybeMatch[1]});
94 + } else {
95 + errors.push({
96 + reason: `Dynamic gating directive is not a valid JavaScript identifier`,
97 + description: `Found '${directive.value.value}'`,
98 + severity: ErrorSeverity.InvalidReact,
99 + loc: directive.loc ?? null,
100 + suggestions: null,
101 + });
102 + }
103 + }
104 + }
105 + if (errors.hasErrors()) {
106 + return Err(errors);
107 + } else if (result.length > 1) {
108 + const error = new CompilerError();
109 + error.push({
110 + reason: `Multiple dynamic gating directives found`,
111 + description: `Expected a single directive but found [${result
112 + .map(r => r.directive.value.value)
113 + .join(', ')}]`,
114 + severity: ErrorSeverity.InvalidReact,
115 + loc: result[0].directive.loc ?? null,
116 + suggestions: null,
117 + });
118 + return Err(error);
119 + } else if (result.length === 1) {
120 + return Ok({
121 + gating: {
122 + source: opts.dynamicGating.source,
123 + importSpecifierName: result[0].match,
124 + },
125 + directive: result[0].directive,
126 + });
127 + } else {
128 + return Ok(null);
129 + }
130 +}
131
132 function isCriticalError(err: unknown): boolean {
133 return !(err instanceof CompilerError) || err.isCritical();
@@ -477,12 +545,32 @@ function processFn(
545 fnType: ReactFunctionType,
546 programContext: ProgramContext,
547 ): null | CodegenFunction {
480 - let directives;
548 + let directives: {
549 + optIn: t.Directive | null;
550 + optOut: t.Directive | null;
551 + };
552 if (fn.node.body.type !== 'BlockStatement') {
482 - directives = {optIn: null, optOut: null};
553 + directives = {
554 + optIn: null,
555 + optOut: null,
556 + };
557 } else {
558 + const optIn = tryFindDirectiveEnablingMemoization(
559 + fn.node.body.directives,
560 + programContext.opts,
561 + );
562 + if (optIn.isErr()) {
563 + /**
564 + * If parsing opt-in directive fails, it's most likely that React Compiler
565 + * was not tested or rolled out on this function. In that case, we handle
566 + * the error and fall back to the safest option which is to not optimize
567 + * the function.
568 + */
569 + handleError(optIn.unwrapErr(), programContext, fn.node.loc ?? null);
570 + return null;
571 + }
572 directives = {
485 - optIn: findDirectiveEnablingMemoization(fn.node.body.directives),
573 + optIn: optIn.unwrapOr(null),
574 optOut: findDirectiveDisablingMemoization(fn.node.body.directives),
575 };
576 }
@@ -659,25 +747,31 @@ function applyCompiledFunctions(
747 pass: CompilerPass,
748 programContext: ProgramContext,
749 ): void {
662 - const referencedBeforeDeclared =
663 - pass.opts.gating != null
664 - ? getFunctionReferencedBeforeDeclarationAtTopLevel(program, compiledFns)
665 - : null;
750 + let referencedBeforeDeclared = null;
751 for (const result of compiledFns) {
752 const {kind, originalFn, compiledFn} = result;
753 const transformedFn = createNewFunctionNode(originalFn, compiledFn);
754 programContext.alreadyCompiled.add(transformedFn);
755
671 - if (referencedBeforeDeclared != null && kind === 'original') {
672 - CompilerError.invariant(pass.opts.gating != null, {
673 - reason: "Expected 'gating' import to be present",
674 - loc: null,
675 - });
756 + let dynamicGating: ExternalFunction | null = null;
757 + if (originalFn.node.body.type === 'BlockStatement') {
758 + const result = findDirectivesDynamicGating(
759 + originalFn.node.body.directives,
760 + pass.opts,
761 + );
762 + if (result.isOk()) {
763 + dynamicGating = result.unwrap()?.gating ?? null;
764 + }
765 + }
766 + const functionGating = dynamicGating ?? pass.opts.gating;
767 + if (kind === 'original' && functionGating != null) {
768 + referencedBeforeDeclared ??=
769 + getFunctionReferencedBeforeDeclarationAtTopLevel(program, compiledFns);
770 insertGatedFunctionDeclaration(
771 originalFn,
772 transformedFn,
773 programContext,
680 - pass.opts.gating,
774 + functionGating,
775 referencedBeforeDeclared.has(result),
776 );
777 } else {
@@ -733,8 +827,13 @@ function getReactFunctionType(
827 ): ReactFunctionType | null {
828 const hookPattern = pass.opts.environment.hookPattern;
829 if (fn.node.body.type === 'BlockStatement') {
736 - if (findDirectiveEnablingMemoization(fn.node.body.directives) != null)
830 + const optInDirectives = tryFindDirectiveEnablingMemoization(
831 + fn.node.body.directives,
832 + pass.opts,
833 + );
834 + if (optInDirectives.unwrapOr(null) != null) {
835 return getComponentOrHookLike(fn, hookPattern) ?? 'Other';
836 + }
837 }
838
839 // Component and hook declarations are known components/hooks
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-annotation.expect.md new
+50
@@ -0,0 +1,50 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @dynamicGating:{"source":"shared-runtime"} @compilationMode:"annotation"
6 +
7 +function Foo() {
8 + 'use memo if(getTrue)';
9 + return <div>hello world</div>;
10 +}
11 +
12 +export const FIXTURE_ENTRYPOINT = {
13 + fn: Foo,
14 + params: [{}],
15 +};
16 +
17 +```
18 +
19 +## Code
20 +
21 +```javascript
22 +import { c as _c } from "react/compiler-runtime";
23 +import { getTrue } from "shared-runtime"; // @dynamicGating:{"source":"shared-runtime"} @compilationMode:"annotation"
24 +const Foo = getTrue()
25 + ? function Foo() {
26 + "use memo if(getTrue)";
27 + const $ = _c(1);
28 + let t0;
29 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
30 + t0 = <div>hello world</div>;
31 + $[0] = t0;
32 + } else {
33 + t0 = $[0];
34 + }
35 + return t0;
36 + }
37 + : function Foo() {
38 + "use memo if(getTrue)";
39 + return <div>hello world</div>;
40 + };
41 +
42 +export const FIXTURE_ENTRYPOINT = {
43 + fn: Foo,
44 + params: [{}],
45 +};
46 +
47 +```
48 +
49 +### Eval output
50 +(kind: ok) <div>hello world</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-annotation.js new
+11
@@ -0,0 +1,11 @@
1 +// @dynamicGating:{"source":"shared-runtime"} @compilationMode:"annotation"
2 +
3 +function Foo() {
4 + 'use memo if(getTrue)';
5 + return <div>hello world</div>;
6 +}
7 +
8 +export const FIXTURE_ENTRYPOINT = {
9 + fn: Foo,
10 + params: [{}],
11 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-bailout-nopanic.expect.md new
+66
@@ -0,0 +1,66 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @dynamicGating:{"source":"shared-runtime"} @validatePreserveExistingMemoizationGuarantees @panicThreshold:"none" @loggerTestOnly
6 +
7 +import {useMemo} from 'react';
8 +import {identity} from 'shared-runtime';
9 +
10 +function Foo({value}) {
11 + 'use memo if(getTrue)';
12 +
13 + const initialValue = useMemo(() => identity(value), []);
14 + return (
15 + <>
16 + <div>initial value {initialValue}</div>
17 + <div>current value {value}</div>
18 + </>
19 + );
20 +}
21 +
22 +export const FIXTURE_ENTRYPOINT = {
23 + fn: Foo,
24 + params: [{value: 1}],
25 + sequentialRenders: [{value: 1}, {value: 2}],
26 +};
27 +
28 +```
29 +
30 +## Code
31 +
32 +```javascript
33 +// @dynamicGating:{"source":"shared-runtime"} @validatePreserveExistingMemoizationGuarantees @panicThreshold:"none" @loggerTestOnly
34 +
35 +import { useMemo } from "react";
36 +import { identity } from "shared-runtime";
37 +
38 +function Foo({ value }) {
39 + "use memo if(getTrue)";
40 +
41 + const initialValue = useMemo(() => identity(value), []);
42 + return (
43 + <>
44 + <div>initial value {initialValue}</div>
45 + <div>current value {value}</div>
46 + </>
47 + );
48 +}
49 +
50 +export const FIXTURE_ENTRYPOINT = {
51 + fn: Foo,
52 + params: [{ value: 1 }],
53 + sequentialRenders: [{ value: 1 }, { value: 2 }],
54 +};
55 +
56 +```
57 +
58 +## Logs
59 +
60 +```
61 +{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":206},"end":{"line":16,"column":1,"index":433},"filename":"dynamic-gating-bailout-nopanic.ts"},"detail":{"reason":"React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected","description":"The inferred dependency was `value`, but the source dependencies were []. Inferred dependency not present in source","severity":"CannotPreserveMemoization","suggestions":null,"loc":{"start":{"line":9,"column":31,"index":288},"end":{"line":9,"column":52,"index":309},"filename":"dynamic-gating-bailout-nopanic.ts"}}}
62 +```
63 +
64 +### Eval output
65 +(kind: ok) <div>initial value 1</div><div>current value 1</div>
66 +<div>initial value 1</div><div>current value 2</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-bailout-nopanic.js new
+22
@@ -0,0 +1,22 @@
1 +// @dynamicGating:{"source":"shared-runtime"} @validatePreserveExistingMemoizationGuarantees @panicThreshold:"none" @loggerTestOnly
2 +
3 +import {useMemo} from 'react';
4 +import {identity} from 'shared-runtime';
5 +
6 +function Foo({value}) {
7 + 'use memo if(getTrue)';
8 +
9 + const initialValue = useMemo(() => identity(value), []);
10 + return (
11 + <>
12 + <div>initial value {initialValue}</div>
13 + <div>current value {value}</div>
14 + </>
15 + );
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: Foo,
20 + params: [{value: 1}],
21 + sequentialRenders: [{value: 1}, {value: 2}],
22 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-disabled.expect.md new
+50
@@ -0,0 +1,50 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @dynamicGating:{"source":"shared-runtime"}
6 +
7 +function Foo() {
8 + 'use memo if(getFalse)';
9 + return <div>hello world</div>;
10 +}
11 +
12 +export const FIXTURE_ENTRYPOINT = {
13 + fn: Foo,
14 + params: [{}],
15 +};
16 +
17 +```
18 +
19 +## Code
20 +
21 +```javascript
22 +import { c as _c } from "react/compiler-runtime";
23 +import { getFalse } from "shared-runtime"; // @dynamicGating:{"source":"shared-runtime"}
24 +const Foo = getFalse()
25 + ? function Foo() {
26 + "use memo if(getFalse)";
27 + const $ = _c(1);
28 + let t0;
29 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
30 + t0 = <div>hello world</div>;
31 + $[0] = t0;
32 + } else {
33 + t0 = $[0];
34 + }
35 + return t0;
36 + }
37 + : function Foo() {
38 + "use memo if(getFalse)";
39 + return <div>hello world</div>;
40 + };
41 +
42 +export const FIXTURE_ENTRYPOINT = {
43 + fn: Foo,
44 + params: [{}],
45 +};
46 +
47 +```
48 +
49 +### Eval output
50 +(kind: ok) <div>hello world</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-disabled.js new
+11
@@ -0,0 +1,11 @@
1 +// @dynamicGating:{"source":"shared-runtime"}
2 +
3 +function Foo() {
4 + 'use memo if(getFalse)';
5 + return <div>hello world</div>;
6 +}
7 +
8 +export const FIXTURE_ENTRYPOINT = {
9 + fn: Foo,
10 + params: [{}],
11 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-enabled.expect.md new
+50
@@ -0,0 +1,50 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @dynamicGating:{"source":"shared-runtime"}
6 +
7 +function Foo() {
8 + 'use memo if(getTrue)';
9 + return <div>hello world</div>;
10 +}
11 +
12 +export const FIXTURE_ENTRYPOINT = {
13 + fn: Foo,
14 + params: [{}],
15 +};
16 +
17 +```
18 +
19 +## Code
20 +
21 +```javascript
22 +import { c as _c } from "react/compiler-runtime";
23 +import { getTrue } from "shared-runtime"; // @dynamicGating:{"source":"shared-runtime"}
24 +const Foo = getTrue()
25 + ? function Foo() {
26 + "use memo if(getTrue)";
27 + const $ = _c(1);
28 + let t0;
29 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
30 + t0 = <div>hello world</div>;
31 + $[0] = t0;
32 + } else {
33 + t0 = $[0];
34 + }
35 + return t0;
36 + }
37 + : function Foo() {
38 + "use memo if(getTrue)";
39 + return <div>hello world</div>;
40 + };
41 +
42 +export const FIXTURE_ENTRYPOINT = {
43 + fn: Foo,
44 + params: [{}],
45 +};
46 +
47 +```
48 +
49 +### Eval output
50 +(kind: ok) <div>hello world</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-enabled.js new
+11
@@ -0,0 +1,11 @@
1 +// @dynamicGating:{"source":"shared-runtime"}
2 +
3 +function Foo() {
4 + 'use memo if(getTrue)';
5 + return <div>hello world</div>;
6 +}
7 +
8 +export const FIXTURE_ENTRYPOINT = {
9 + fn: Foo,
10 + params: [{}],
11 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-identifier-nopanic.expect.md new
+37
@@ -0,0 +1,37 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none"
6 +
7 +function Foo() {
8 + 'use memo if(true)';
9 + return <div>hello world</div>;
10 +}
11 +
12 +export const FIXTURE_ENTRYPOINT = {
13 + fn: Foo,
14 + params: [{}],
15 +};
16 +
17 +```
18 +
19 +## Code
20 +
21 +```javascript
22 +// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none"
23 +
24 +function Foo() {
25 + "use memo if(true)";
26 + return <div>hello world</div>;
27 +}
28 +
29 +export const FIXTURE_ENTRYPOINT = {
30 + fn: Foo,
31 + params: [{}],
32 +};
33 +
34 +```
35 +
36 +### Eval output
37 +(kind: ok) <div>hello world</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-identifier-nopanic.js new
+11
@@ -0,0 +1,11 @@
1 +// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none"
2 +
3 +function Foo() {
4 + 'use memo if(true)';
5 + return <div>hello world</div>;
6 +}
7 +
8 +export const FIXTURE_ENTRYPOINT = {
9 + fn: Foo,
10 + params: [{}],
11 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-multiple.expect.md new
+45
@@ -0,0 +1,45 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" @loggerTestOnly
6 +
7 +function Foo() {
8 + 'use memo if(getTrue)';
9 + 'use memo if(getFalse)';
10 + return <div>hello world</div>;
11 +}
12 +
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: Foo,
15 + params: [{}],
16 +};
17 +
18 +```
19 +
20 +## Code
21 +
22 +```javascript
23 +// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" @loggerTestOnly
24 +
25 +function Foo() {
26 + "use memo if(getTrue)";
27 + "use memo if(getFalse)";
28 + return <div>hello world</div>;
29 +}
30 +
31 +export const FIXTURE_ENTRYPOINT = {
32 + fn: Foo,
33 + params: [{}],
34 +};
35 +
36 +```
37 +
38 +## Logs
39 +
40 +```
41 +{"kind":"CompileError","fnLoc":{"start":{"line":3,"column":0,"index":86},"end":{"line":7,"column":1,"index":190},"filename":"dynamic-gating-invalid-multiple.ts"},"detail":{"reason":"Multiple dynamic gating directives found","description":"Expected a single directive but found [use memo if(getTrue), use memo if(getFalse)]","severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":4,"column":2,"index":105},"end":{"line":4,"column":25,"index":128},"filename":"dynamic-gating-invalid-multiple.ts"}}}
42 +```
43 +
44 +### Eval output
45 +(kind: ok) <div>hello world</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-multiple.js new
+12
@@ -0,0 +1,12 @@
1 +// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" @loggerTestOnly
2 +
3 +function Foo() {
4 + 'use memo if(getTrue)';
5 + 'use memo if(getFalse)';
6 + return <div>hello world</div>;
7 +}
8 +
9 +export const FIXTURE_ENTRYPOINT = {
10 + fn: Foo,
11 + params: [{}],
12 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-noemit.expect.md new
+37
@@ -0,0 +1,37 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @dynamicGating:{"source":"shared-runtime"} @noEmit
6 +
7 +function Foo() {
8 + 'use memo if(getTrue)';
9 + return <div>hello world</div>;
10 +}
11 +
12 +export const FIXTURE_ENTRYPOINT = {
13 + fn: Foo,
14 + params: [{}],
15 +};
16 +
17 +```
18 +
19 +## Code
20 +
21 +```javascript
22 +// @dynamicGating:{"source":"shared-runtime"} @noEmit
23 +
24 +function Foo() {
25 + "use memo if(getTrue)";
26 + return <div>hello world</div>;
27 +}
28 +
29 +export const FIXTURE_ENTRYPOINT = {
30 + fn: Foo,
31 + params: [{}],
32 +};
33 +
34 +```
35 +
36 +### Eval output
37 +(kind: ok) <div>hello world</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-noemit.js new
+11
@@ -0,0 +1,11 @@
1 +// @dynamicGating:{"source":"shared-runtime"} @noEmit
2 +
3 +function Foo() {
4 + 'use memo if(getTrue)';
5 + return <div>hello world</div>;
6 +}
7 +
8 +export const FIXTURE_ENTRYPOINT = {
9 + fn: Foo,
10 + params: [{}],
11 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier-nopanic-required-feature.expect.md new
+35
@@ -0,0 +1,35 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" @inferEffectDependencies
6 +import {useEffect} from 'react';
7 +import {print} from 'shared-runtime';
8 +
9 +function ReactiveVariable({propVal}) {
10 + 'use memo if(invalid identifier)';
11 + const arr = [propVal];
12 + useEffect(() => print(arr));
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: ReactiveVariable,
17 + params: [{}],
18 +};
19 +
20 +```
21 +
22 +
23 +## Error
24 +
25 +```
26 + 6 | 'use memo if(invalid identifier)';
27 + 7 | const arr = [propVal];
28 +> 8 | useEffect(() => print(arr));
29 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: [InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. This will break your build! To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics. (8:8)
30 + 9 | }
31 + 10 |
32 + 11 | export const FIXTURE_ENTRYPOINT = {
33 +```
34 +
35 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier-nopanic-required-feature.js new
+14
@@ -0,0 +1,14 @@
1 +// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" @inferEffectDependencies
2 +import {useEffect} from 'react';
3 +import {print} from 'shared-runtime';
4 +
5 +function ReactiveVariable({propVal}) {
6 + 'use memo if(invalid identifier)';
7 + const arr = [propVal];
8 + useEffect(() => print(arr));
9 +}
10 +
11 +export const FIXTURE_ENTRYPOINT = {
12 + fn: ReactiveVariable,
13 + params: [{}],
14 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier.expect.md new
+32
@@ -0,0 +1,32 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @dynamicGating:{"source":"shared-runtime"}
6 +
7 +function Foo() {
8 + 'use memo if(true)';
9 + return <div>hello world</div>;
10 +}
11 +
12 +export const FIXTURE_ENTRYPOINT = {
13 + fn: Foo,
14 + params: [{}],
15 +};
16 +
17 +```
18 +
19 +
20 +## Error
21 +
22 +```
23 + 2 |
24 + 3 | function Foo() {
25 +> 4 | 'use memo if(true)';
26 + | ^^^^^^^^^^^^^^^^^^^^ InvalidReact: Dynamic gating directive is not a valid JavaScript identifier. Found 'use memo if(true)' (4:4)
27 + 5 | return <div>hello world</div>;
28 + 6 | }
29 + 7 |
30 +```
31 +
32 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier.js new
+11
@@ -0,0 +1,11 @@
1 +// @dynamicGating:{"source":"shared-runtime"}
2 +
3 +function Foo() {
4 + 'use memo if(true)';
5 + return <div>hello world</div>;
6 +}
7 +
8 +export const FIXTURE_ENTRYPOINT = {
9 + fn: Foo,
10 + params: [{}],
11 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-dynamic-gating.expect.md new
+42
@@ -0,0 +1,42 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @dynamicGating:{"source":"shared-runtime"} @inferEffectDependencies @panicThreshold:"none"
6 +
7 +import useEffectWrapper from 'useEffectWrapper';
8 +
9 +/**
10 + * TODO: run the non-forget enabled version through the effect inference
11 + * pipeline.
12 + */
13 +function Component({foo}) {
14 + 'use memo if(getTrue)';
15 + const arr = [];
16 + useEffectWrapper(() => arr.push(foo));
17 + arr.push(2);
18 + return arr;
19 +}
20 +
21 +export const FIXTURE_ENTRYPOINT = {
22 + fn: Component,
23 + params: [{foo: 1}],
24 + sequentialRenders: [{foo: 1}, {foo: 2}],
25 +};
26 +
27 +```
28 +
29 +
30 +## Error
31 +
32 +```
33 + 10 | 'use memo if(getTrue)';
34 + 11 | const arr = [];
35 +> 12 | useEffectWrapper(() => arr.push(foo));
36 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: [InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. This will break your build! To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics. (12:12)
37 + 13 | arr.push(2);
38 + 14 | return arr;
39 + 15 | }
40 +```
41 +
42 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-dynamic-gating.js new
+21
@@ -0,0 +1,21 @@
1 +// @dynamicGating:{"source":"shared-runtime"} @inferEffectDependencies @panicThreshold:"none"
2 +
3 +import useEffectWrapper from 'useEffectWrapper';
4 +
5 +/**
6 + * TODO: run the non-forget enabled version through the effect inference
7 + * pipeline.
8 + */
9 +function Component({foo}) {
10 + 'use memo if(getTrue)';
11 + const arr = [];
12 + useEffectWrapper(() => arr.push(foo));
13 + arr.push(2);
14 + return arr;
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Component,
19 + params: [{foo: 1}],
20 + sequentialRenders: [{foo: 1}, {foo: 2}],
21 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-gating.expect.md new
+40
@@ -0,0 +1,40 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @gating @inferEffectDependencies @panicThreshold:"none"
6 +import useEffectWrapper from 'useEffectWrapper';
7 +
8 +/**
9 + * TODO: run the non-forget enabled version through the effect inference
10 + * pipeline.
11 + */
12 +function Component({foo}) {
13 + const arr = [];
14 + useEffectWrapper(() => arr.push(foo));
15 + arr.push(2);
16 + return arr;
17 +}
18 +
19 +export const FIXTURE_ENTRYPOINT = {
20 + fn: Component,
21 + params: [{foo: 1}],
22 + sequentialRenders: [{foo: 1}, {foo: 2}],
23 +};
24 +
25 +```
26 +
27 +
28 +## Error
29 +
30 +```
31 + 8 | function Component({foo}) {
32 + 9 | const arr = [];
33 +> 10 | useEffectWrapper(() => arr.push(foo));
34 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: [InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. This will break your build! To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics. (10:10)
35 + 11 | arr.push(2);
36 + 12 | return arr;
37 + 13 | }
38 +```
39 +
40 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-gating.js new
+19
@@ -0,0 +1,19 @@
1 +// @gating @inferEffectDependencies @panicThreshold:"none"
2 +import useEffectWrapper from 'useEffectWrapper';
3 +
4 +/**
5 + * TODO: run the non-forget enabled version through the effect inference
6 + * pipeline.
7 + */
8 +function Component({foo}) {
9 + const arr = [];
10 + useEffectWrapper(() => arr.push(foo));
11 + arr.push(2);
12 + return arr;
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: Component,
17 + params: [{foo: 1}],
18 + sequentialRenders: [{foo: 1}, {foo: 2}],
19 +};
compiler/packages/babel-plugin-react-compiler/src/index.ts
+1 -1
@@ -20,7 +20,7 @@ export {
20 OPT_OUT_DIRECTIVES,
21 OPT_IN_DIRECTIVES,
22 ProgramContext,
23 - findDirectiveEnablingMemoization,
23 + tryFindDirectiveEnablingMemoization as findDirectiveEnablingMemoization,
24 findDirectiveDisablingMemoization,
25 type CompilerPipelineValue,
26 type Logger,
compiler/packages/snap/src/sprout/shared-runtime.ts
+8
@@ -128,6 +128,14 @@ export function getNull(): null {
128 return null;
129 }
130
131 +export function getTrue(): true {
132 + return true;
133 +}
134 +
135 +export function getFalse(): false {
136 + return false;
137 +}
138 +
139 export function calculateExpensiveNumber(x: number): number {
140 return x;
141 }