Compare commits

...

2 Commits

Author SHA1 Message Date
Mofei Zhang
ab752bf7a1 [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 <div>...</div>;
}
```
Compiler output
```js
import {isEnabled} from 'myModule';
export const MyComponent = isEnabled()
  ? <optimized version>
  : <original version>;
```
2025-05-09 13:30:42 -04:00
Mofei Zhang
1b658717e9 [compiler][entrypoint] Fix edgecases for noEmit and opt-outs
Title
2025-05-09 13:30:42 -04:00
30 changed files with 853 additions and 58 deletions

View File

@@ -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<SuppressionRange>;
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 {

View File

@@ -37,6 +37,10 @@ const PanicThresholdOptionsSchema = z.enum([
]);
export type PanicThresholdOptions = z.infer<typeof PanicThresholdOptionsSchema>;
const DynamicGatingOptionsSchema = z.object({
source: z.string(),
});
export type DynamicGatingOptions = z.infer<typeof DynamicGatingOptionsSchema>;
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 <div>...</div>;
* }
* ```
* This will emit:
* ```js
* import {isEnabled} from 'myModule';
* export const MyComponent = isEnabled()
* ? <optimized version>
* : <original version>;
* ```
*/
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;
}

View File

@@ -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>,
): t.Directive | null {
return (
directives.find(directive =>
OPT_IN_DIRECTIVES.has(directive.value.value),
) ?? null
opts: PluginOptions,
): Result<t.Directive | null, CompilerError> {
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<t.Directive>,
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<CompileSource> = 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};
} 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),
};
}
@@ -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

View File

@@ -0,0 +1,50 @@
## Input
```javascript
// @dynamicGating:{"source":"shared-runtime"} @compilationMode:"annotation"
function Foo() {
'use memo if(getTrue)';
return <div>hello world</div>;
}
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 = <div>hello world</div>;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
: function Foo() {
"use memo if(getTrue)";
return <div>hello world</div>;
};
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};
```
### Eval output
(kind: ok) <div>hello world</div>

View File

@@ -0,0 +1,11 @@
// @dynamicGating:{"source":"shared-runtime"} @compilationMode:"annotation"
function Foo() {
'use memo if(getTrue)';
return <div>hello world</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};

View File

@@ -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 (
<>
<div>initial value {initialValue}</div>
<div>current value {value}</div>
</>
);
}
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 (
<>
<div>initial value {initialValue}</div>
<div>current value {value}</div>
</>
);
}
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) <div>initial value 1</div><div>current value 1</div>
<div>initial value 1</div><div>current value 2</div>

View File

@@ -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 (
<>
<div>initial value {initialValue}</div>
<div>current value {value}</div>
</>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{value: 1}],
sequentialRenders: [{value: 1}, {value: 2}],
};

View File

@@ -0,0 +1,50 @@
## Input
```javascript
// @dynamicGating:{"source":"shared-runtime"}
function Foo() {
'use memo if(getFalse)';
return <div>hello world</div>;
}
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 = <div>hello world</div>;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
: function Foo() {
"use memo if(getFalse)";
return <div>hello world</div>;
};
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};
```
### Eval output
(kind: ok) <div>hello world</div>

View File

@@ -0,0 +1,11 @@
// @dynamicGating:{"source":"shared-runtime"}
function Foo() {
'use memo if(getFalse)';
return <div>hello world</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};

View File

@@ -0,0 +1,50 @@
## Input
```javascript
// @dynamicGating:{"source":"shared-runtime"}
function Foo() {
'use memo if(getTrue)';
return <div>hello world</div>;
}
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 = <div>hello world</div>;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
: function Foo() {
"use memo if(getTrue)";
return <div>hello world</div>;
};
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};
```
### Eval output
(kind: ok) <div>hello world</div>

View File

@@ -0,0 +1,11 @@
// @dynamicGating:{"source":"shared-runtime"}
function Foo() {
'use memo if(getTrue)';
return <div>hello world</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};

View File

@@ -0,0 +1,37 @@
## Input
```javascript
// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none"
function Foo() {
'use memo if(true)';
return <div>hello world</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};
```
## Code
```javascript
// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none"
function Foo() {
"use memo if(true)";
return <div>hello world</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};
```
### Eval output
(kind: ok) <div>hello world</div>

View File

@@ -0,0 +1,11 @@
// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none"
function Foo() {
'use memo if(true)';
return <div>hello world</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};

View File

@@ -0,0 +1,45 @@
## Input
```javascript
// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" @loggerTestOnly
function Foo() {
'use memo if(getTrue)';
'use memo if(getFalse)';
return <div>hello world</div>;
}
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 <div>hello world</div>;
}
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) <div>hello world</div>

View File

@@ -0,0 +1,12 @@
// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" @loggerTestOnly
function Foo() {
'use memo if(getTrue)';
'use memo if(getFalse)';
return <div>hello world</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};

View File

@@ -0,0 +1,37 @@
## Input
```javascript
// @dynamicGating:{"source":"shared-runtime"} @noEmit
function Foo() {
'use memo if(getTrue)';
return <div>hello world</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};
```
## Code
```javascript
// @dynamicGating:{"source":"shared-runtime"} @noEmit
function Foo() {
"use memo if(getTrue)";
return <div>hello world</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};
```
### Eval output
(kind: ok) <div>hello world</div>

View File

@@ -0,0 +1,11 @@
// @dynamicGating:{"source":"shared-runtime"} @noEmit
function Foo() {
'use memo if(getTrue)';
return <div>hello world</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};

View File

@@ -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 = {
```

View File

@@ -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: [{}],
};

View File

@@ -0,0 +1,32 @@
## Input
```javascript
// @dynamicGating:{"source":"shared-runtime"}
function Foo() {
'use memo if(true)';
return <div>hello world</div>;
}
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 <div>hello world</div>;
6 | }
7 |
```

View File

@@ -0,0 +1,11 @@
// @dynamicGating:{"source":"shared-runtime"}
function Foo() {
'use memo if(true)';
return <div>hello world</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};

View File

@@ -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 | }
```

View File

@@ -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}],
};

View File

@@ -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 | }
```

View File

@@ -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}],
};

View File

@@ -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 };
}

View File

@@ -20,9 +20,6 @@ function Foo() {
function Foo() {
return <button onClick={() => alert("hello!")}>Click me!</button>;
}
function _temp() {
return alert("hello!");
}
```

View File

@@ -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 = <button onClick={_temp}>Click me!</button>;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
function _temp() {
return alert("hello!");
return <button onClick={() => alert("hello!")}>Click me!</button>;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -20,7 +20,7 @@ export {
OPT_OUT_DIRECTIVES,
OPT_IN_DIRECTIVES,
ProgramContext,
findDirectiveEnablingMemoization,
tryFindDirectiveEnablingMemoization as findDirectiveEnablingMemoization,
findDirectiveDisablingMemoization,
type CompilerPipelineValue,
type Logger,

View File

@@ -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;
}