diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Imports.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Imports.ts index d5cac921e98..24ce37cf72c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Imports.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Imports.ts @@ -59,6 +59,7 @@ type ProgramContextOptions = { opts: PluginOptions; filename: string | null; code: string | null; + hasModuleScopeOptOut: boolean; }; export class ProgramContext { /** @@ -70,6 +71,7 @@ export class ProgramContext { code: string | null; reactRuntimeModule: string; suppressions: Array; + hasModuleScopeOptOut: boolean; /* * This is a hack to work around what seems to be a Babel bug. Babel doesn't @@ -94,6 +96,7 @@ export class ProgramContext { opts, filename, code, + hasModuleScopeOptOut, }: ProgramContextOptions) { this.scope = program.scope; this.opts = opts; @@ -101,6 +104,7 @@ export class ProgramContext { this.code = code; this.reactRuntimeModule = getReactCompilerRuntimeModule(opts.target); this.suppressions = suppressions; + this.hasModuleScopeOptOut = hasModuleScopeOptOut; } isHookName(name: string): boolean { 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 c732e164101..96cce887d86 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 1a3445531df..cb57bd2c49d 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(); @@ -325,6 +393,8 @@ export function compileProgram( filename: pass.filename, code: pass.code, suppressions, + hasModuleScopeOptOut: + findDirectiveDisablingMemoization(program.node.directives) != null, }); const queue: Array = findFunctionsToCompile( @@ -368,7 +438,19 @@ export function compileProgram( } // Avoid modifying the program if we find a program level opt-out - if (findDirectiveDisablingMemoization(program.node.directives) != null) { + if (programContext.hasModuleScopeOptOut) { + if (compiledFns.length > 0) { + const error = new CompilerError(); + error.pushErrorDetail( + new CompilerErrorDetail({ + reason: + 'Unexpected compiled functions when module scope opt-out is present', + severity: ErrorSeverity.Invariant, + loc: null, + }), + ); + handleError(error, programContext, null); + } return null; } @@ -463,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}; + 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: findDirectiveEnablingMemoization(fn.node.body.directives), + optIn: optIn.unwrapOr(null), optOut: findDirectiveDisablingMemoization(fn.node.body.directives), }; } @@ -491,9 +593,10 @@ function processFn( } /** - * Otherwise if 'use no forget/memo' is present, we still run the code through the compiler - * for validation but we don't mutate the babel AST. This allows us to flag if there is an - * unused 'use no forget/memo' directive. + * If 'use no forget/memo' is present and we still ran the code through the + * compiler for validation, log a skip event and don't mutate the babel AST. + * This allows us to flag if there is an unused 'use no forget/memo' + * directive. */ if ( programContext.opts.ignoreUseNoForget === false && @@ -518,16 +621,7 @@ function processFn( prunedMemoValues: compiledFn.prunedMemoValues, }); - /** - * Always compile functions with opt in directives. - */ - if (directives.optIn != null) { - return compiledFn; - } else if (programContext.opts.compilationMode === 'annotation') { - /** - * If no opt-in directive is found and the compiler is configured in - * annotation mode, don't insert the compiled function. - */ + if (programContext.hasModuleScopeOptOut) { return null; } else if (programContext.opts.noEmit) { /** @@ -541,6 +635,15 @@ function processFn( } } return null; + } else if ( + programContext.opts.compilationMode === 'annotation' && + directives.optIn == null + ) { + /** + * If no opt-in directive is found and the compiler is configured in + * annotation mode, don't insert the compiled function. + */ + return null; } else { return compiledFn; } @@ -644,25 +747,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 { @@ -718,8 +827,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 00000000000..364239e4e3a --- /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 00000000000..c30b30fe6f5 --- /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 00000000000..dc3cc2b98de --- /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 00000000000..ceddbefdd1b --- /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 00000000000..7d95b54317d --- /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 00000000000..be29f105687 --- /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 00000000000..272c5a57143 --- /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 00000000000..9280e25d116 --- /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 00000000000..c8c91910b02 --- /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 00000000000..4d0d9c3bb86 --- /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 00000000000..327adbe792e --- /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 00000000000..867ac8ee34b --- /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 00000000000..81ebd6dd9fa --- /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 00000000000..97cf777a552 --- /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 00000000000..7f9f608383b --- /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 00000000000..7d5b74acc79 --- /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 00000000000..c824afd6806 --- /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 00000000000..c4005544972 --- /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/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-dynamic-gating.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-dynamic-gating.expect.md new file mode 100644 index 00000000000..ec5ef238b78 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-dynamic-gating.expect.md @@ -0,0 +1,42 @@ + +## Input + +```javascript +// @dynamicGating:{"source":"shared-runtime"} @inferEffectDependencies @panicThreshold:"none" + +import useEffectWrapper from 'useEffectWrapper'; + +/** + * TODO: run the non-forget enabled version through the effect inference + * pipeline. + */ +function Component({foo}) { + 'use memo if(getTrue)'; + const arr = []; + useEffectWrapper(() => arr.push(foo)); + arr.push(2); + return arr; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{foo: 1}], + sequentialRenders: [{foo: 1}, {foo: 2}], +}; + +``` + + +## Error + +``` + 10 | 'use memo if(getTrue)'; + 11 | const arr = []; +> 12 | useEffectWrapper(() => arr.push(foo)); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 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) + 13 | arr.push(2); + 14 | return arr; + 15 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-dynamic-gating.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-dynamic-gating.js new file mode 100644 index 00000000000..4d1ceb92b78 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-dynamic-gating.js @@ -0,0 +1,21 @@ +// @dynamicGating:{"source":"shared-runtime"} @inferEffectDependencies @panicThreshold:"none" + +import useEffectWrapper from 'useEffectWrapper'; + +/** + * TODO: run the non-forget enabled version through the effect inference + * pipeline. + */ +function Component({foo}) { + 'use memo if(getTrue)'; + const arr = []; + useEffectWrapper(() => arr.push(foo)); + arr.push(2); + return arr; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{foo: 1}], + sequentialRenders: [{foo: 1}, {foo: 2}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-gating.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-gating.expect.md new file mode 100644 index 00000000000..e071e37cb99 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-gating.expect.md @@ -0,0 +1,40 @@ + +## Input + +```javascript +// @gating @inferEffectDependencies @panicThreshold:"none" +import useEffectWrapper from 'useEffectWrapper'; + +/** + * TODO: run the non-forget enabled version through the effect inference + * pipeline. + */ +function Component({foo}) { + const arr = []; + useEffectWrapper(() => arr.push(foo)); + arr.push(2); + return arr; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{foo: 1}], + sequentialRenders: [{foo: 1}, {foo: 2}], +}; + +``` + + +## Error + +``` + 8 | function Component({foo}) { + 9 | const arr = []; +> 10 | useEffectWrapper(() => arr.push(foo)); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 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) + 11 | arr.push(2); + 12 | return arr; + 13 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-gating.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-gating.js new file mode 100644 index 00000000000..651b24074f2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-gating.js @@ -0,0 +1,19 @@ +// @gating @inferEffectDependencies @panicThreshold:"none" +import useEffectWrapper from 'useEffectWrapper'; + +/** + * TODO: run the non-forget enabled version through the effect inference + * pipeline. + */ +function Component({foo}) { + const arr = []; + useEffectWrapper(() => arr.push(foo)); + arr.push(2); + return arr; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{foo: 1}], + sequentialRenders: [{foo: 1}, {foo: 2}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/retry-opt-in--no-emit.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/retry-opt-in--no-emit.expect.md index 8d2e5c7c31f..6e4ddeeb2b5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/retry-opt-in--no-emit.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/retry-opt-in--no-emit.expect.md @@ -33,16 +33,15 @@ export const FIXTURE_ENTRYPOINT = { import { print } from "shared-runtime"; import useEffectWrapper from "useEffectWrapper"; -function Foo(t0) { +function Foo({ propVal }) { "use memo"; - const { propVal } = t0; - const arr = [propVal]; - useEffectWrapper(() => print(arr), [arr]); + useEffectWrapper(() => print(arr)); const arr2 = []; - useEffectWrapper(() => arr2.push(propVal), [arr2, propVal]); + useEffectWrapper(() => arr2.push(propVal)); arr2.push(2); + return { arr, arr2 }; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-bailout-nopanic-shouldnt-outline.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-bailout-nopanic-shouldnt-outline.expect.md index f24d9492058..cfbaa345682 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-bailout-nopanic-shouldnt-outline.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-bailout-nopanic-shouldnt-outline.expect.md @@ -20,9 +20,6 @@ function Foo() { function Foo() { return ; } -function _temp() { - return alert("hello!"); -} ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-memo-noemit.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-memo-noemit.expect.md index dfc831555f0..c47501945b3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-memo-noemit.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-memo-noemit.expect.md @@ -19,22 +19,11 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; // @noEmit +// @noEmit function Foo() { "use memo"; - const $ = _c(1); - let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = ; - $[0] = t0; - } else { - t0 = $[0]; - } - return t0; -} -function _temp() { - return alert("hello!"); + return ; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/index.ts b/compiler/packages/babel-plugin-react-compiler/src/index.ts index 086e010fea5..cbae672e506 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 1b8648f4ff0..569d31cbd4d 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; }