From 699bcaa9e5b11766373bdf85d03bfbefceddc482 Mon Sep 17 00:00:00 2001 From: Mofei Zhang Date: Thu, 8 May 2025 14:52:44 -0400 Subject: [PATCH] [compiler][gating] Experimental directive based gating 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
...
; } ``` Compiler output ```js import {isEnabled} from 'myModule'; export const MyComponent = isEnabled() ? : ; ``` --- .../src/Entrypoint/Options.ts | 46 ++++++ .../src/Entrypoint/Program.ts | 143 +++++++++++++++--- .../dynamic-gating-annotation.expect.md | 50 ++++++ .../gating/dynamic-gating-annotation.js | 11 ++ .../dynamic-gating-bailout-nopanic.expect.md | 66 ++++++++ .../gating/dynamic-gating-bailout-nopanic.js | 22 +++ .../gating/dynamic-gating-disabled.expect.md | 50 ++++++ .../gating/dynamic-gating-disabled.js | 11 ++ .../gating/dynamic-gating-enabled.expect.md | 50 ++++++ .../compiler/gating/dynamic-gating-enabled.js | 11 ++ ...ating-invalid-identifier-nopanic.expect.md | 37 +++++ ...namic-gating-invalid-identifier-nopanic.js | 11 ++ .../dynamic-gating-invalid-multiple.expect.md | 45 ++++++ .../gating/dynamic-gating-invalid-multiple.js | 12 ++ .../gating/dynamic-gating-noemit.expect.md | 37 +++++ .../compiler/gating/dynamic-gating-noemit.js | 11 ++ ...ntifier-nopanic-required-feature.expect.md | 35 +++++ ...lid-identifier-nopanic-required-feature.js | 14 ++ ...ynamic-gating-invalid-identifier.expect.md | 32 ++++ ...error.dynamic-gating-invalid-identifier.js | 11 ++ .../babel-plugin-react-compiler/src/index.ts | 2 +- .../snap/src/sprout/shared-runtime.ts | 8 + 22 files changed, 692 insertions(+), 23 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-annotation.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-annotation.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-bailout-nopanic.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-bailout-nopanic.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-disabled.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-disabled.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-enabled.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-enabled.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-identifier-nopanic.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-identifier-nopanic.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-multiple.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-multiple.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-noemit.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-noemit.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier-nopanic-required-feature.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier-nopanic-required-feature.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts index c732e16410..96cce887d8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts @@ -37,6 +37,10 @@ const PanicThresholdOptionsSchema = z.enum([ ]); export type PanicThresholdOptions = z.infer; +const DynamicGatingOptionsSchema = z.object({ + source: z.string(), +}); +export type DynamicGatingOptions = z.infer; export type PluginOptions = { environment: EnvironmentConfig; @@ -65,6 +69,28 @@ export type PluginOptions = { */ 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
...
; + * } + * ``` + * This will emit: + * ```js + * import {isEnabled} from 'myModule'; + * export const MyComponent = isEnabled() + * ? + * : ; + * ``` + */ + dynamicGating: DynamicGatingOptions | null; + panicThreshold: PanicThresholdOptions; /* @@ -244,6 +270,7 @@ export const defaultOptions: PluginOptions = { logger: null, gating: null, noEmit: false, + dynamicGating: null, eslintSuppressionRules: null, flowSuppressions: true, ignoreUseNoForget: false, @@ -292,6 +319,25 @@ export function parsePluginOptions(obj: unknown): PluginOptions { } 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; + } default: { parsedOptions[key] = value; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts index 91c6c5ceea..8449b40ca1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts @@ -12,7 +12,7 @@ import { CompilerErrorDetail, ErrorSeverity, } from '../CompilerError'; -import {ReactFunctionType} from '../HIR/Environment'; +import {ExternalFunction, ReactFunctionType} from '../HIR/Environment'; import {CodegenFunction} from '../ReactiveScopes'; import {isComponentDeclaration} from '../Utils/ComponentDeclaration'; import {isHookDeclaration} from '../Utils/HookDeclaration'; @@ -31,6 +31,7 @@ import { suppressionsToCompilerError, } from './Suppression'; import {GeneratedSource} from '../HIR'; +import {Err, Ok, Result} from '../Utils/Result'; export type CompilerPass = { opts: PluginOptions; @@ -40,15 +41,24 @@ export type CompilerPass = { }; export const OPT_IN_DIRECTIVES = new Set(['use forget', 'use memo']); export const OPT_OUT_DIRECTIVES = new Set(['use no forget', 'use no memo']); +const DYNAMIC_GATING_DIRECTIVE = new RegExp('^use memo if\\(([^\\)]*)\\)$'); -export function findDirectiveEnablingMemoization( +export function tryFindDirectiveEnablingMemoization( directives: Array, -): t.Directive | null { - return ( - directives.find(directive => - OPT_IN_DIRECTIVES.has(directive.value.value), - ) ?? null + opts: PluginOptions, +): Result { + const optIn = directives.find(directive => + OPT_IN_DIRECTIVES.has(directive.value.value), ); + if (optIn != null) { + return Ok(optIn); + } + const dynamicGating = findDirectivesDynamicGating(directives, opts); + if (dynamicGating.isOk()) { + return Ok(dynamicGating.unwrap()?.directive ?? null); + } else { + return Err(dynamicGating.unwrapErr()); + } } export function findDirectiveDisablingMemoization( @@ -60,6 +70,64 @@ export function findDirectiveDisablingMemoization( ) ?? null ); } +function findDirectivesDynamicGating( + directives: Array, + opts: PluginOptions, +): Result< + { + gating: ExternalFunction; + directive: t.Directive; + } | null, + CompilerError +> { + if (opts.dynamicGating === null) { + return Ok(null); + } + const errors = new CompilerError(); + const result: Array<{directive: t.Directive; match: string}> = []; + + for (const directive of directives) { + const maybeMatch = DYNAMIC_GATING_DIRECTIVE.exec(directive.value.value); + if (maybeMatch != null && maybeMatch[1] != null) { + if (t.isValidIdentifier(maybeMatch[1])) { + result.push({directive, match: maybeMatch[1]}); + } else { + errors.push({ + reason: `Dynamic gating directive is not a valid JavaScript identifier`, + description: `Found '${directive.value.value}'`, + severity: ErrorSeverity.InvalidReact, + loc: directive.loc ?? null, + suggestions: null, + }); + } + } + } + if (errors.hasErrors()) { + return Err(errors); + } else if (result.length > 1) { + const error = new CompilerError(); + error.push({ + reason: `Multiple dynamic gating directives found`, + description: `Expected a single directive but found [${result + .map(r => r.directive.value.value) + .join(', ')}]`, + severity: ErrorSeverity.InvalidReact, + loc: result[0].directive.loc ?? null, + suggestions: null, + }); + return Err(error); + } else if (result.length === 1) { + return Ok({ + gating: { + source: opts.dynamicGating.source, + importSpecifierName: result[0].match, + }, + directive: result[0].directive, + }); + } else { + return Ok(null); + } +} function isCriticalError(err: unknown): boolean { return !(err instanceof CompilerError) || err.isCritical(); @@ -477,12 +545,32 @@ function processFn( fnType: ReactFunctionType, programContext: ProgramContext, ): null | CodegenFunction { - let directives; + let directives: { + optIn: t.Directive | null; + optOut: t.Directive | null; + }; if (fn.node.body.type !== 'BlockStatement') { - directives = {optIn: null, optOut: null}; - } else { directives = { - optIn: findDirectiveEnablingMemoization(fn.node.body.directives), + optIn: null, + optOut: null, + }; + } else { + const optIn = tryFindDirectiveEnablingMemoization( + fn.node.body.directives, + programContext.opts, + ); + if (optIn.isErr()) { + /** + * If parsing opt-in directive fails, it's most likely that React Compiler + * was not tested or rolled out on this function. In that case, we handle + * the error and fall back to the safest option which is to not optimize + * the function. + */ + handleError(optIn.unwrapErr(), programContext, fn.node.loc ?? null); + return null; + } + directives = { + optIn: optIn.unwrapOr(null), optOut: findDirectiveDisablingMemoization(fn.node.body.directives), }; } @@ -661,25 +749,31 @@ function applyCompiledFunctions( pass: CompilerPass, programContext: ProgramContext, ): void { - const referencedBeforeDeclared = - pass.opts.gating != null - ? getFunctionReferencedBeforeDeclarationAtTopLevel(program, compiledFns) - : null; + let referencedBeforeDeclared = null; for (const result of compiledFns) { const {kind, originalFn, compiledFn} = result; const transformedFn = createNewFunctionNode(originalFn, compiledFn); programContext.alreadyCompiled.add(transformedFn); - if (referencedBeforeDeclared != null && kind === 'original') { - CompilerError.invariant(pass.opts.gating != null, { - reason: "Expected 'gating' import to be present", - loc: null, - }); + let dynamicGating: ExternalFunction | null = null; + if (originalFn.node.body.type === 'BlockStatement') { + const result = findDirectivesDynamicGating( + originalFn.node.body.directives, + pass.opts, + ); + if (result.isOk()) { + dynamicGating = result.unwrap()?.gating ?? null; + } + } + const functionGating = dynamicGating ?? pass.opts.gating; + if (kind === 'original' && functionGating != null) { + referencedBeforeDeclared ??= + getFunctionReferencedBeforeDeclarationAtTopLevel(program, compiledFns); insertGatedFunctionDeclaration( originalFn, transformedFn, programContext, - pass.opts.gating, + functionGating, referencedBeforeDeclared.has(result), ); } else { @@ -735,8 +829,13 @@ function getReactFunctionType( ): ReactFunctionType | null { const hookPattern = pass.opts.environment.hookPattern; if (fn.node.body.type === 'BlockStatement') { - if (findDirectiveEnablingMemoization(fn.node.body.directives) != null) + const optInDirectives = tryFindDirectiveEnablingMemoization( + fn.node.body.directives, + pass.opts, + ); + if (optInDirectives.unwrapOr(null) != null) { return getComponentOrHookLike(fn, hookPattern) ?? 'Other'; + } } // Component and hook declarations are known components/hooks diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-annotation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-annotation.expect.md new file mode 100644 index 0000000000..364239e4e3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-annotation.expect.md @@ -0,0 +1,50 @@ + +## Input + +```javascript +// @dynamicGating:{"source":"shared-runtime"} @compilationMode:"annotation" + +function Foo() { + 'use memo if(getTrue)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { getTrue } from "shared-runtime"; // @dynamicGating:{"source":"shared-runtime"} @compilationMode:"annotation" +const Foo = getTrue() + ? function Foo() { + "use memo if(getTrue)"; + const $ = _c(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 =
hello world
; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; + } + : function Foo() { + "use memo if(getTrue)"; + return
hello world
; + }; + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +### Eval output +(kind: ok)
hello world
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-annotation.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-annotation.js new file mode 100644 index 0000000000..c30b30fe6f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-annotation.js @@ -0,0 +1,11 @@ +// @dynamicGating:{"source":"shared-runtime"} @compilationMode:"annotation" + +function Foo() { + 'use memo if(getTrue)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-bailout-nopanic.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-bailout-nopanic.expect.md new file mode 100644 index 0000000000..dc3cc2b98d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-bailout-nopanic.expect.md @@ -0,0 +1,66 @@ + +## Input + +```javascript +// @dynamicGating:{"source":"shared-runtime"} @validatePreserveExistingMemoizationGuarantees @panicThreshold:"none" @loggerTestOnly + +import {useMemo} from 'react'; +import {identity} from 'shared-runtime'; + +function Foo({value}) { + 'use memo if(getTrue)'; + + const initialValue = useMemo(() => identity(value), []); + return ( + <> +
initial value {initialValue}
+
current value {value}
+ + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{value: 1}], + sequentialRenders: [{value: 1}, {value: 2}], +}; + +``` + +## Code + +```javascript +// @dynamicGating:{"source":"shared-runtime"} @validatePreserveExistingMemoizationGuarantees @panicThreshold:"none" @loggerTestOnly + +import { useMemo } from "react"; +import { identity } from "shared-runtime"; + +function Foo({ value }) { + "use memo if(getTrue)"; + + const initialValue = useMemo(() => identity(value), []); + return ( + <> +
initial value {initialValue}
+
current value {value}
+ + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ value: 1 }], + sequentialRenders: [{ value: 1 }, { value: 2 }], +}; + +``` + +## Logs + +``` +{"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"}}} +``` + +### Eval output +(kind: ok)
initial value 1
current value 1
+
initial value 1
current value 2
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-bailout-nopanic.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-bailout-nopanic.js new file mode 100644 index 0000000000..ceddbefdd1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-bailout-nopanic.js @@ -0,0 +1,22 @@ +// @dynamicGating:{"source":"shared-runtime"} @validatePreserveExistingMemoizationGuarantees @panicThreshold:"none" @loggerTestOnly + +import {useMemo} from 'react'; +import {identity} from 'shared-runtime'; + +function Foo({value}) { + 'use memo if(getTrue)'; + + const initialValue = useMemo(() => identity(value), []); + return ( + <> +
initial value {initialValue}
+
current value {value}
+ + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{value: 1}], + sequentialRenders: [{value: 1}, {value: 2}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-disabled.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-disabled.expect.md new file mode 100644 index 0000000000..7d95b54317 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-disabled.expect.md @@ -0,0 +1,50 @@ + +## Input + +```javascript +// @dynamicGating:{"source":"shared-runtime"} + +function Foo() { + 'use memo if(getFalse)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { getFalse } from "shared-runtime"; // @dynamicGating:{"source":"shared-runtime"} +const Foo = getFalse() + ? function Foo() { + "use memo if(getFalse)"; + const $ = _c(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 =
hello world
; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; + } + : function Foo() { + "use memo if(getFalse)"; + return
hello world
; + }; + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +### Eval output +(kind: ok)
hello world
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-disabled.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-disabled.js new file mode 100644 index 0000000000..be29f10568 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-disabled.js @@ -0,0 +1,11 @@ +// @dynamicGating:{"source":"shared-runtime"} + +function Foo() { + 'use memo if(getFalse)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-enabled.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-enabled.expect.md new file mode 100644 index 0000000000..272c5a5714 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-enabled.expect.md @@ -0,0 +1,50 @@ + +## Input + +```javascript +// @dynamicGating:{"source":"shared-runtime"} + +function Foo() { + 'use memo if(getTrue)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { getTrue } from "shared-runtime"; // @dynamicGating:{"source":"shared-runtime"} +const Foo = getTrue() + ? function Foo() { + "use memo if(getTrue)"; + const $ = _c(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 =
hello world
; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; + } + : function Foo() { + "use memo if(getTrue)"; + return
hello world
; + }; + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +### Eval output +(kind: ok)
hello world
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-enabled.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-enabled.js new file mode 100644 index 0000000000..9280e25d11 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-enabled.js @@ -0,0 +1,11 @@ +// @dynamicGating:{"source":"shared-runtime"} + +function Foo() { + 'use memo if(getTrue)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-identifier-nopanic.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-identifier-nopanic.expect.md new file mode 100644 index 0000000000..c8c91910b0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-identifier-nopanic.expect.md @@ -0,0 +1,37 @@ + +## Input + +```javascript +// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" + +function Foo() { + 'use memo if(true)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +## Code + +```javascript +// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" + +function Foo() { + "use memo if(true)"; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +### Eval output +(kind: ok)
hello world
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-identifier-nopanic.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-identifier-nopanic.js new file mode 100644 index 0000000000..4d0d9c3bb8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-identifier-nopanic.js @@ -0,0 +1,11 @@ +// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" + +function Foo() { + 'use memo if(true)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-multiple.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-multiple.expect.md new file mode 100644 index 0000000000..327adbe792 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-multiple.expect.md @@ -0,0 +1,45 @@ + +## Input + +```javascript +// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" @loggerTestOnly + +function Foo() { + 'use memo if(getTrue)'; + 'use memo if(getFalse)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +## Code + +```javascript +// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" @loggerTestOnly + +function Foo() { + "use memo if(getTrue)"; + "use memo if(getFalse)"; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +## Logs + +``` +{"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"}}} +``` + +### Eval output +(kind: ok)
hello world
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-multiple.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-multiple.js new file mode 100644 index 0000000000..867ac8ee34 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-multiple.js @@ -0,0 +1,12 @@ +// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" @loggerTestOnly + +function Foo() { + 'use memo if(getTrue)'; + 'use memo if(getFalse)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-noemit.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-noemit.expect.md new file mode 100644 index 0000000000..81ebd6dd9f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-noemit.expect.md @@ -0,0 +1,37 @@ + +## Input + +```javascript +// @dynamicGating:{"source":"shared-runtime"} @noEmit + +function Foo() { + 'use memo if(getTrue)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +## Code + +```javascript +// @dynamicGating:{"source":"shared-runtime"} @noEmit + +function Foo() { + "use memo if(getTrue)"; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +### Eval output +(kind: ok)
hello world
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-noemit.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-noemit.js new file mode 100644 index 0000000000..97cf777a55 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-noemit.js @@ -0,0 +1,11 @@ +// @dynamicGating:{"source":"shared-runtime"} @noEmit + +function Foo() { + 'use memo if(getTrue)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier-nopanic-required-feature.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier-nopanic-required-feature.expect.md new file mode 100644 index 0000000000..7f9f608383 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier-nopanic-required-feature.expect.md @@ -0,0 +1,35 @@ + +## Input + +```javascript +// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" @inferEffectDependencies +import {useEffect} from 'react'; +import {print} from 'shared-runtime'; + +function ReactiveVariable({propVal}) { + 'use memo if(invalid identifier)'; + const arr = [propVal]; + useEffect(() => print(arr)); +} + +export const FIXTURE_ENTRYPOINT = { + fn: ReactiveVariable, + params: [{}], +}; + +``` + + +## Error + +``` + 6 | 'use memo if(invalid identifier)'; + 7 | const arr = [propVal]; +> 8 | useEffect(() => print(arr)); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 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) + 9 | } + 10 | + 11 | export const FIXTURE_ENTRYPOINT = { +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier-nopanic-required-feature.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier-nopanic-required-feature.js new file mode 100644 index 0000000000..7d5b74acc7 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier-nopanic-required-feature.js @@ -0,0 +1,14 @@ +// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" @inferEffectDependencies +import {useEffect} from 'react'; +import {print} from 'shared-runtime'; + +function ReactiveVariable({propVal}) { + 'use memo if(invalid identifier)'; + const arr = [propVal]; + useEffect(() => print(arr)); +} + +export const FIXTURE_ENTRYPOINT = { + fn: ReactiveVariable, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier.expect.md new file mode 100644 index 0000000000..c824afd680 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier.expect.md @@ -0,0 +1,32 @@ + +## Input + +```javascript +// @dynamicGating:{"source":"shared-runtime"} + +function Foo() { + 'use memo if(true)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + + +## Error + +``` + 2 | + 3 | function Foo() { +> 4 | 'use memo if(true)'; + | ^^^^^^^^^^^^^^^^^^^^ InvalidReact: Dynamic gating directive is not a valid JavaScript identifier. Found 'use memo if(true)' (4:4) + 5 | return
hello world
; + 6 | } + 7 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier.js new file mode 100644 index 0000000000..c400554497 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier.js @@ -0,0 +1,11 @@ +// @dynamicGating:{"source":"shared-runtime"} + +function Foo() { + 'use memo if(true)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/index.ts b/compiler/packages/babel-plugin-react-compiler/src/index.ts index 086e010fea..cbae672e50 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/index.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/index.ts @@ -20,7 +20,7 @@ export { OPT_OUT_DIRECTIVES, OPT_IN_DIRECTIVES, ProgramContext, - findDirectiveEnablingMemoization, + tryFindDirectiveEnablingMemoization as findDirectiveEnablingMemoization, findDirectiveDisablingMemoization, type CompilerPipelineValue, type Logger, diff --git a/compiler/packages/snap/src/sprout/shared-runtime.ts b/compiler/packages/snap/src/sprout/shared-runtime.ts index 1b8648f4ff..569d31cbd4 100644 --- a/compiler/packages/snap/src/sprout/shared-runtime.ts +++ b/compiler/packages/snap/src/sprout/shared-runtime.ts @@ -128,6 +128,14 @@ export function getNull(): null { return null; } +export function getTrue(): true { + return true; +} + +export function getFalse(): false { + return false; +} + export function calculateExpensiveNumber(x: number): number { return x; }