[compiler] Deprecate noEmit, add outputMode (#35112)

This deprecates the `noEmit: boolean` flag and adds `outputMode:
'client' | 'client-no-memo' | 'ssr' | 'lint'` as the replacement.
OutputMode defaults to null and takes precedence if specified, otherwise
we use 'client' mode for noEmit=false and 'lint' mode for noEmit=true.

Key points:
* Retrying failed compilation switches from 'client' mode to
'client-no-memo'
* Validations are enabled behind
Environment.proto.shouldEnableValidations, enabled for all modes except
'client-no-memo'. Similar for dropping manual memoization.
* OptimizeSSR is now gated by the outputMode==='ssr', not a feature flag
* Creation of reactive scopes, and related codegen logic, is now gated
by outputMode==='client'
This commit is contained in:
Joseph Savona
2025-11-20 15:12:40 -08:00
committed by GitHub
parent 4cf770d7e1
commit 50e7ec8a69
30 changed files with 514 additions and 216 deletions

View File

@@ -102,14 +102,25 @@ export type PluginOptions = Partial<{
panicThreshold: PanicThresholdOptions;
/*
/**
* @deprecated
*
* When enabled, Forget will continue statically analyzing and linting code, but skip over codegen
* passes.
*
* NOTE: ignored if `outputMode` is specified
*
* Defaults to false
*/
noEmit: boolean;
/**
* If specified, overrides `noEmit` and controls the output mode of the compiler.
*
* Defaults to null
*/
outputMode: CompilerOutputMode | null;
/*
* Determines the strategy for determining which functions to compile. Note that regardless of
* which mode is enabled, a component can be opted out by adding the string literal
@@ -212,6 +223,19 @@ const CompilationModeSchema = z.enum([
export type CompilationMode = z.infer<typeof CompilationModeSchema>;
const CompilerOutputModeSchema = z.enum([
// Build optimized for SSR, with client features removed
'ssr',
// Build optimized for the client, with auto memoization
'client',
// Build optimized for the client without auto memo
'client-no-memo',
// Lint mode, the output is unused but validations should run
'lint',
]);
export type CompilerOutputMode = z.infer<typeof CompilerOutputModeSchema>;
/**
* Represents 'events' that may occur during compilation. Events are only
* recorded when a logger is set (through the config).
@@ -293,6 +317,7 @@ export const defaultOptions: ParsedPluginOptions = {
logger: null,
gating: null,
noEmit: false,
outputMode: null,
dynamicGating: null,
eslintSuppressionRules: null,
flowSuppressions: true,

View File

@@ -8,7 +8,7 @@
import {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import prettyFormat from 'pretty-format';
import {Logger, ProgramContext} from '.';
import {CompilerOutputMode, Logger, ProgramContext} from '.';
import {
HIRFunction,
ReactiveFunction,
@@ -24,7 +24,6 @@ import {
pruneUnusedLabelsHIR,
} from '../HIR';
import {
CompilerMode,
Environment,
EnvironmentConfig,
ReactFunctionType,
@@ -120,7 +119,7 @@ function run(
>,
config: EnvironmentConfig,
fnType: ReactFunctionType,
mode: CompilerMode,
mode: CompilerOutputMode,
programContext: ProgramContext,
logger: Logger | null,
filename: string | null,
@@ -170,7 +169,7 @@ function runWithEnvironment(
validateUseMemo(hir).unwrap();
if (
env.isInferredMemoEnabled &&
env.enableDropManualMemoization &&
!env.config.enablePreserveExistingManualUseMemo &&
!env.config.disableMemoizationForDebugging &&
!env.config.enableChangeDetectionForDebugging
@@ -206,7 +205,7 @@ function runWithEnvironment(
inferTypes(hir);
log({kind: 'hir', name: 'InferTypes', value: hir});
if (env.isInferredMemoEnabled) {
if (env.enableValidations) {
if (env.config.validateHooksUsage) {
validateHooksUsage(hir).unwrap();
}
@@ -232,13 +231,13 @@ function runWithEnvironment(
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
if (env.isInferredMemoEnabled) {
if (env.enableValidations) {
if (mutabilityAliasingErrors.isErr()) {
throw mutabilityAliasingErrors.unwrapErr();
}
}
if (env.config.enableOptimizeForSSR) {
if (env.outputMode === 'ssr') {
optimizeForSSR(hir);
log({kind: 'hir', name: 'OptimizeForSSR', value: hir});
}
@@ -259,14 +258,14 @@ function runWithEnvironment(
isFunctionExpression: false,
});
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
if (env.isInferredMemoEnabled) {
if (env.enableValidations) {
if (mutabilityAliasingRangeErrors.isErr()) {
throw mutabilityAliasingRangeErrors.unwrapErr();
}
validateLocalsNotReassignedAfterRender(hir);
}
if (env.isInferredMemoEnabled) {
if (env.enableValidations) {
if (env.config.assertValidMutableRanges) {
assertValidMutableRanges(hir);
}
@@ -310,20 +309,18 @@ function runWithEnvironment(
value: hir,
});
if (env.isInferredMemoEnabled) {
if (env.config.validateStaticComponents) {
env.logErrors(validateStaticComponents(hir));
}
if (env.enableValidations && env.config.validateStaticComponents) {
env.logErrors(validateStaticComponents(hir));
}
if (env.enableMemoization) {
/**
* Only create reactive scopes (which directly map to generated memo blocks)
* if inferred memoization is enabled. This makes all later passes which
* transform reactive-scope labeled instructions no-ops.
*/
if (!env.config.enableOptimizeForSSR) {
inferReactiveScopeVariables(hir);
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
}
inferReactiveScopeVariables(hir);
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
}
const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
@@ -588,7 +585,7 @@ export function compileFn(
>,
config: EnvironmentConfig,
fnType: ReactFunctionType,
mode: CompilerMode,
mode: CompilerOutputMode,
programContext: ProgramContext,
logger: Logger | null,
filename: string | null,

View File

@@ -24,6 +24,7 @@ import {
validateRestrictedImports,
} from './Imports';
import {
CompilerOutputMode,
CompilerReactTarget,
ParsedPluginOptions,
PluginOptions,
@@ -421,9 +422,17 @@ export function compileProgram(
);
const compiledFns: Array<CompileResult> = [];
// outputMode takes precedence if specified
const outputMode: CompilerOutputMode =
pass.opts.outputMode ?? (pass.opts.noEmit ? 'lint' : 'client');
while (queue.length !== 0) {
const current = queue.shift()!;
const compiled = processFn(current.fn, current.fnType, programContext);
const compiled = processFn(
current.fn,
current.fnType,
programContext,
outputMode,
);
if (compiled != null) {
for (const outlined of compiled.outlined) {
@@ -581,6 +590,7 @@ function processFn(
fn: BabelFn,
fnType: ReactFunctionType,
programContext: ProgramContext,
outputMode: CompilerOutputMode,
): null | CodegenFunction {
let directives: {
optIn: t.Directive | null;
@@ -616,18 +626,27 @@ function processFn(
}
let compiledFn: CodegenFunction;
const compileResult = tryCompileFunction(fn, fnType, programContext);
const compileResult = tryCompileFunction(
fn,
fnType,
programContext,
outputMode,
);
if (compileResult.kind === 'error') {
if (directives.optOut != null) {
logError(compileResult.error, programContext, fn.node.loc ?? null);
} else {
handleError(compileResult.error, programContext, fn.node.loc ?? null);
}
const retryResult = retryCompileFunction(fn, fnType, programContext);
if (retryResult == null) {
if (outputMode === 'client') {
const retryResult = retryCompileFunction(fn, fnType, programContext);
if (retryResult == null) {
return null;
}
compiledFn = retryResult;
} else {
return null;
}
compiledFn = retryResult;
} else {
compiledFn = compileResult.compiledFn;
}
@@ -663,7 +682,7 @@ function processFn(
if (programContext.hasModuleScopeOptOut) {
return null;
} else if (programContext.opts.noEmit) {
} else if (programContext.opts.outputMode === 'lint') {
/**
* inferEffectDependencies + noEmit is currently only used for linting. In
* this mode, add source locations for where the compiler *can* infer effect
@@ -693,6 +712,7 @@ function tryCompileFunction(
fn: BabelFn,
fnType: ReactFunctionType,
programContext: ProgramContext,
outputMode: CompilerOutputMode,
):
| {kind: 'compile'; compiledFn: CodegenFunction}
| {kind: 'error'; error: unknown} {
@@ -719,7 +739,7 @@ function tryCompileFunction(
fn,
programContext.opts.environment,
fnType,
'all_features',
outputMode,
programContext,
programContext.opts.logger,
programContext.filename,
@@ -757,7 +777,7 @@ function retryCompileFunction(
fn,
environment,
fnType,
'no_inferred_memo',
'client-no-memo',
programContext,
programContext.opts.logger,
programContext.filename,

View File

@@ -9,7 +9,7 @@ import * as t from '@babel/types';
import {ZodError, z} from 'zod/v4';
import {fromZodError} from 'zod-validation-error/v4';
import {CompilerError} from '../CompilerError';
import {Logger, ProgramContext} from '../Entrypoint';
import {CompilerOutputMode, Logger, ProgramContext} from '../Entrypoint';
import {Err, Ok, Result} from '../Utils/Result';
import {
DEFAULT_GLOBALS,
@@ -51,6 +51,7 @@ import {Scope as BabelScope, NodePath} from '@babel/traverse';
import {TypeSchema} from './TypeSchema';
import {FlowTypeEnv} from '../Flood/Types';
import {defaultModuleTypeProvider} from './DefaultModuleTypeProvider';
import {assertExhaustive} from '../Utils/utils';
export const ReactElementSymbolSchema = z.object({
elementSymbol: z.union([
@@ -691,8 +692,6 @@ export const EnvironmentConfigSchema = z.object({
* by React to only execute in response to events, not during render.
*/
enableInferEventHandlers: z.boolean().default(false),
enableOptimizeForSSR: z.boolean().default(false),
});
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
@@ -732,7 +731,7 @@ export class Environment {
code: string | null;
config: EnvironmentConfig;
fnType: ReactFunctionType;
compilerMode: CompilerMode;
outputMode: CompilerOutputMode;
programContext: ProgramContext;
hasFireRewrite: boolean;
hasInferredEffect: boolean;
@@ -747,7 +746,7 @@ export class Environment {
constructor(
scope: BabelScope,
fnType: ReactFunctionType,
compilerMode: CompilerMode,
outputMode: CompilerOutputMode,
config: EnvironmentConfig,
contextIdentifiers: Set<t.Identifier>,
parentFunction: NodePath<t.Function>, // the outermost function being compiled
@@ -758,7 +757,7 @@ export class Environment {
) {
this.#scope = scope;
this.fnType = fnType;
this.compilerMode = compilerMode;
this.outputMode = outputMode;
this.config = config;
this.filename = filename;
this.code = code;
@@ -854,8 +853,65 @@ export class Environment {
return this.#flowTypeEnvironment;
}
get isInferredMemoEnabled(): boolean {
return this.compilerMode !== 'no_inferred_memo';
get enableDropManualMemoization(): boolean {
switch (this.outputMode) {
case 'lint': {
// linting drops to be more compatible with compiler analysis
return true;
}
case 'client':
case 'ssr': {
return true;
}
case 'client-no-memo': {
return false;
}
default: {
assertExhaustive(
this.outputMode,
`Unexpected output mode '${this.outputMode}'`,
);
}
}
}
get enableMemoization(): boolean {
switch (this.outputMode) {
case 'client':
case 'lint': {
// linting also enables memoization so that we can check if manual memoization is preserved
return true;
}
case 'ssr':
case 'client-no-memo': {
return false;
}
default: {
assertExhaustive(
this.outputMode,
`Unexpected output mode '${this.outputMode}'`,
);
}
}
}
get enableValidations(): boolean {
switch (this.outputMode) {
case 'client':
case 'lint':
case 'ssr': {
return true;
}
case 'client-no-memo': {
return false;
}
default: {
assertExhaustive(
this.outputMode,
`Unexpected output mode '${this.outputMode}'`,
);
}
}
}
get nextIdentifierId(): IdentifierId {

View File

@@ -2452,7 +2452,7 @@ function computeEffectsForLegacySignature(
}),
});
}
if (signature.knownIncompatible != null && state.env.isInferredMemoEnabled) {
if (signature.knownIncompatible != null && state.env.enableValidations) {
const errors = new CompilerError();
errors.pushDiagnostic(
CompilerDiagnostic.create({

View File

@@ -319,7 +319,7 @@ function pruneableValue(value: InstructionValue, state: State): boolean {
}
case 'CallExpression':
case 'MethodCall': {
if (state.env.config.enableOptimizeForSSR) {
if (state.env.outputMode === 'ssr') {
const calleee =
value.kind === 'CallExpression' ? value.callee : value.property;
const hookKind = getHookKind(state.env, calleee.identifier);

View File

@@ -159,7 +159,7 @@ export function codegenFunction(
const compiled = compileResult.unwrap();
const hookGuard = fn.env.config.enableEmitHookGuards;
if (hookGuard != null && fn.env.isInferredMemoEnabled) {
if (hookGuard != null && fn.env.outputMode === 'client') {
compiled.body = t.blockStatement([
createHookGuard(
hookGuard,
@@ -259,7 +259,7 @@ export function codegenFunction(
if (
emitInstrumentForget != null &&
fn.id != null &&
fn.env.isInferredMemoEnabled
fn.env.outputMode === 'client'
) {
/*
* Technically, this is a conditional hook call. However, we expect
@@ -591,7 +591,10 @@ function codegenBlockNoReset(
}
function wrapCacheDep(cx: Context, value: t.Expression): t.Expression {
if (cx.env.config.enableEmitFreeze != null && cx.env.isInferredMemoEnabled) {
if (
cx.env.config.enableEmitFreeze != null &&
cx.env.outputMode === 'client'
) {
const emitFreezeIdentifier = cx.env.programContext.addImportSpecifier(
cx.env.config.enableEmitFreeze,
).name;
@@ -1772,7 +1775,7 @@ function createCallExpression(
}
const hookGuard = env.config.enableEmitHookGuards;
if (hookGuard != null && isHook && env.isInferredMemoEnabled) {
if (hookGuard != null && isHook && env.outputMode === 'client') {
const iife = t.functionExpression(
null,
[],

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @dynamicGating:{"source":"shared-runtime"} @noEmit
// @dynamicGating:{"source":"shared-runtime"} @outputMode:"lint"
function Foo() {
'use memo if(getTrue)';
@@ -19,7 +19,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @dynamicGating:{"source":"shared-runtime"} @noEmit
// @dynamicGating:{"source":"shared-runtime"} @outputMode:"lint"
function Foo() {
"use memo if(getTrue)";

View File

@@ -1,4 +1,4 @@
// @dynamicGating:{"source":"shared-runtime"} @noEmit
// @dynamicGating:{"source":"shared-runtime"} @outputMode:"lint"
function Foo() {
'use memo if(getTrue)';

View File

@@ -1,66 +0,0 @@
## Input
```javascript
// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';
function Foo({propVal}) {
const arr = [propVal];
useEffectWrapper(() => print(arr), AUTODEPS);
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
arr2.push(2);
return {arr, arr2};
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{propVal: 1}],
sequentialRenders: [{propVal: 1}, {propVal: 2}],
};
```
## Code
```javascript
// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly
import { print } from "shared-runtime";
import useEffectWrapper from "useEffectWrapper";
import { AUTODEPS } from "react";
function Foo({ propVal }) {
const arr = [propVal];
useEffectWrapper(() => print(arr), AUTODEPS);
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
arr2.push(2);
return { arr, arr2 };
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ propVal: 1 }],
sequentialRenders: [{ propVal: 1 }, { propVal: 2 }],
};
```
## Logs
```
{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":195},"end":{"line":14,"column":1,"index":409},"filename":"retry-no-emit.ts"},"detail":{"options":{"category":"Immutability","reason":"This value cannot be modified","description":"Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook","details":[{"kind":"error","loc":{"start":{"line":12,"column":2,"index":372},"end":{"line":12,"column":6,"index":376},"filename":"retry-no-emit.ts","identifierName":"arr2"},"message":"value cannot be modified"}]}}}
{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":8,"column":2,"index":248},"end":{"line":8,"column":46,"index":292},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":8,"column":31,"index":277},"end":{"line":8,"column":34,"index":280},"filename":"retry-no-emit.ts","identifierName":"arr"}]}
{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":11,"column":2,"index":316},"end":{"line":11,"column":54,"index":368},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":11,"column":25,"index":339},"end":{"line":11,"column":29,"index":343},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":11,"column":25,"index":339},"end":{"line":11,"column":29,"index":343},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":11,"column":35,"index":349},"end":{"line":11,"column":42,"index":356},"filename":"retry-no-emit.ts","identifierName":"propVal"}]}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":6,"column":0,"index":195},"end":{"line":14,"column":1,"index":409},"filename":"retry-no-emit.ts"},"fnName":"Foo","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output
(kind: ok) {"arr":[1],"arr2":[2]}
{"arr":[2],"arr2":[2]}
logs: [[ 1 ],[ 2 ]]

View File

@@ -0,0 +1,48 @@
## Input
```javascript
// @inferEffectDependencies @outputMode:"lint" @panicThreshold:"none"
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';
function Foo({propVal}) {
const arr = [propVal];
useEffectWrapper(() => print(arr), AUTODEPS);
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
arr2.push(2);
return {arr, arr2};
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{propVal: 1}],
sequentialRenders: [{propVal: 1}, {propVal: 2}],
};
```
## Error
```
Found 1 error:
Error: Cannot infer dependencies of this effect. This will break your build!
To resolve, either pass a dependency array or fix reported compiler bailout diagnostics.
error.infer-effect-deps-with-rule-violation--lint.ts:8:2
6 | function Foo({propVal}) {
7 | const arr = [propVal];
> 8 | useEffectWrapper(() => print(arr), AUTODEPS);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cannot infer dependencies
9 |
10 | const arr2 = [];
11 | useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
```

View File

@@ -1,4 +1,4 @@
// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly
// @inferEffectDependencies @outputMode:"lint" @panicThreshold:"none"
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';

View File

@@ -0,0 +1,49 @@
## Input
```javascript
// @inferEffectDependencies @outputMode:"lint" @panicThreshold:"none"
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';
function Foo({propVal}) {
'use memo';
const arr = [propVal];
useEffectWrapper(() => print(arr), AUTODEPS);
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
arr2.push(2);
return {arr, arr2};
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{propVal: 1}],
sequentialRenders: [{propVal: 1}, {propVal: 2}],
};
```
## Error
```
Found 1 error:
Error: Cannot infer dependencies of this effect. This will break your build!
To resolve, either pass a dependency array or fix reported compiler bailout diagnostics.
error.infer-effect-deps-with-rule-violation-use-memo-opt-in--lint.ts:9:2
7 | 'use memo';
8 | const arr = [propVal];
> 9 | useEffectWrapper(() => print(arr), AUTODEPS);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cannot infer dependencies
10 |
11 | const arr2 = [];
12 | useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
```

View File

@@ -1,7 +1,7 @@
// @compilationMode:"all" @inferEffectDependencies @panicThreshold:"none" @noEmit
// @inferEffectDependencies @outputMode:"lint" @panicThreshold:"none"
import {print} from 'shared-runtime';
import {AUTODEPS} from 'react';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';
function Foo({propVal}) {
'use memo';
@@ -11,7 +11,6 @@ function Foo({propVal}) {
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
arr2.push(2);
return {arr, arr2};
}

View File

@@ -0,0 +1,58 @@
## Input
```javascript
// @inferEffectDependencies @panicThreshold:"none"
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';
function Foo({propVal}) {
const arr = [propVal];
useEffectWrapper(() => print(arr), AUTODEPS);
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
arr2.push(2);
return {arr, arr2};
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{propVal: 1}],
sequentialRenders: [{propVal: 1}, {propVal: 2}],
};
```
## Code
```javascript
// @inferEffectDependencies @panicThreshold:"none"
import { print } from "shared-runtime";
import useEffectWrapper from "useEffectWrapper";
import { AUTODEPS } from "react";
function Foo(t0) {
const { propVal } = t0;
const arr = [propVal];
useEffectWrapper(() => print(arr), [arr]);
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal), [arr2, propVal]);
arr2.push(2);
return { arr, arr2 };
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ propVal: 1 }],
sequentialRenders: [{ propVal: 1 }, { propVal: 2 }],
};
```
### Eval output
(kind: ok) {"arr":[1],"arr2":[2]}
{"arr":[2],"arr2":[2]}
logs: [[ 1 ],[ 2 ]]

View File

@@ -1,4 +1,4 @@
// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel
// @inferEffectDependencies @panicThreshold:"none"
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';

View File

@@ -2,10 +2,10 @@
## Input
```javascript
// @compilationMode:"all" @inferEffectDependencies @panicThreshold:"none" @noEmit
// @inferEffectDependencies @panicThreshold:"none"
import {print} from 'shared-runtime';
import {AUTODEPS} from 'react';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';
function Foo({propVal}) {
'use memo';
@@ -15,7 +15,6 @@ function Foo({propVal}) {
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
arr2.push(2);
return {arr, arr2};
}
@@ -30,20 +29,21 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @compilationMode:"all" @inferEffectDependencies @panicThreshold:"none" @noEmit
// @inferEffectDependencies @panicThreshold:"none"
import { print } from "shared-runtime";
import { AUTODEPS } from "react";
import useEffectWrapper from "useEffectWrapper";
import { AUTODEPS } from "react";
function Foo({ propVal }) {
function Foo(t0) {
"use memo";
const { propVal } = t0;
const arr = [propVal];
useEffectWrapper(() => print(arr), AUTODEPS);
useEffectWrapper(() => print(arr), [arr]);
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
useEffectWrapper(() => arr2.push(propVal), [arr2, propVal]);
arr2.push(2);
return { arr, arr2 };
}

View File

@@ -0,0 +1,21 @@
// @inferEffectDependencies @panicThreshold:"none"
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';
function Foo({propVal}) {
'use memo';
const arr = [propVal];
useEffectWrapper(() => print(arr), AUTODEPS);
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
arr2.push(2);
return {arr, arr2};
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{propVal: 1}],
sequentialRenders: [{propVal: 1}, {propVal: 2}],
};

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @inferEffectDependencies @noEmit
// @inferEffectDependencies @outputMode:"lint"
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';
@@ -17,7 +17,7 @@ function ReactiveVariable({propVal}) {
## Code
```javascript
// @inferEffectDependencies @noEmit
// @inferEffectDependencies @outputMode:"lint"
import { print } from "shared-runtime";
import useEffectWrapper from "useEffectWrapper";
import { AUTODEPS } from "react";

View File

@@ -1,4 +1,4 @@
// @inferEffectDependencies @noEmit
// @inferEffectDependencies @outputMode:"lint"
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';

View File

@@ -1,66 +0,0 @@
## Input
```javascript
// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';
function Foo({propVal}) {
const arr = [propVal];
useEffectWrapper(() => print(arr), AUTODEPS);
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
arr2.push(2);
return {arr, arr2};
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{propVal: 1}],
sequentialRenders: [{propVal: 1}, {propVal: 2}],
};
```
## Code
```javascript
// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel
import { print } from "shared-runtime";
import useEffectWrapper from "useEffectWrapper";
import { AUTODEPS } from "react";
function Foo({ propVal }) {
const arr = [propVal];
useEffectWrapper(() => print(arr), AUTODEPS);
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
arr2.push(2);
return { arr, arr2 };
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ propVal: 1 }],
sequentialRenders: [{ propVal: 1 }, { propVal: 2 }],
};
```
## Logs
```
{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":227},"end":{"line":14,"column":1,"index":441},"filename":"retry-no-emit.ts"},"detail":{"options":{"category":"Immutability","reason":"This value cannot be modified","description":"Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook","details":[{"kind":"error","loc":{"start":{"line":12,"column":2,"index":404},"end":{"line":12,"column":6,"index":408},"filename":"retry-no-emit.ts","identifierName":"arr2"},"message":"value cannot be modified"}]}}}
{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":8,"column":2,"index":280},"end":{"line":8,"column":46,"index":324},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":8,"column":31,"index":309},"end":{"line":8,"column":34,"index":312},"filename":"retry-no-emit.ts","identifierName":"arr"}]}
{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":11,"column":2,"index":348},"end":{"line":11,"column":54,"index":400},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":11,"column":25,"index":371},"end":{"line":11,"column":29,"index":375},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":11,"column":25,"index":371},"end":{"line":11,"column":29,"index":375},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":11,"column":35,"index":381},"end":{"line":11,"column":42,"index":388},"filename":"retry-no-emit.ts","identifierName":"propVal"}]}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":6,"column":0,"index":227},"end":{"line":14,"column":1,"index":441},"filename":"retry-no-emit.ts"},"fnName":"Foo","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output
(kind: ok) {"arr":[1],"arr2":[2]}
{"arr":[2],"arr2":[2]}
logs: [[ 1 ],[ 2 ]]

View File

@@ -20,10 +20,40 @@ function Component() {
## Code
```javascript
// @enableOptimizeForSSR
import { c as _c } from "react/compiler-runtime"; // @enableOptimizeForSSR
function Component() {
const state = 0;
return <input value={state} />;
const $ = _c(4);
const [state, setState] = useState(0);
const ref = useRef(null);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = (e) => {
setState(e.target.value);
};
$[0] = t0;
} else {
t0 = $[0];
}
const onChange = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = () => {
log(ref.current.value);
};
$[1] = t1;
} else {
t1 = $[1];
}
useEffect(t1);
let t2;
if ($[2] !== state) {
t2 = <input value={state} onChange={onChange} ref={ref} />;
$[2] = state;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
```

View File

@@ -22,12 +22,40 @@ function Component() {
## Code
```javascript
// @enableOptimizeForSSR
import { c as _c } from "react/compiler-runtime"; // @enableOptimizeForSSR
function Component() {
const state = 0;
const $ = _c(4);
const [state, setState] = useState(0);
const ref = useRef(null);
const onChange = undefined;
return <CustomInput value={state} onChange={onChange} ref={ref} />;
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = (e) => {
setState(e.target.value);
};
$[0] = t0;
} else {
t0 = $[0];
}
const onChange = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = () => {
log(ref.current.value);
};
$[1] = t1;
} else {
t1 = $[1];
}
useEffect(t1);
let t2;
if ($[2] !== state) {
t2 = <CustomInput value={state} onChange={onChange} ref={ref} />;
$[2] = state;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
```

View File

@@ -25,13 +25,43 @@ function Component() {
## Code
```javascript
// @enableOptimizeForSSR
import { c as _c } from "react/compiler-runtime"; // @enableOptimizeForSSR
function Component() {
useTransition();
const state = 0;
const $ = _c(4);
const [, startTransition] = useTransition();
const [state, setState] = useState(0);
const ref = useRef(null);
const onChange = undefined;
return <CustomInput value={state} onChange={onChange} ref={ref} />;
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = (e) => {
startTransition(() => {
setState.call(null, e.target.value);
});
};
$[0] = t0;
} else {
t0 = $[0];
}
const onChange = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = () => {
log(ref.current.value);
};
$[1] = t1;
} else {
t1 = $[1];
}
useEffect(t1);
let t2;
if ($[2] !== state) {
t2 = <CustomInput value={state} onChange={onChange} ref={ref} />;
$[2] = state;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
```

View File

@@ -25,7 +25,7 @@ function Component() {
## Code
```javascript
// @enableOptimizeForSSR
import { c as _c } from "react/compiler-runtime"; // @enableOptimizeForSSR
import { useReducer } from "react";
@@ -34,8 +34,41 @@ const initializer = (x) => {
};
function Component() {
const state = initializer(0);
return <input value={state} />;
const $ = _c(4);
const [state, dispatch] = useReducer(_temp, 0, initializer);
const ref = useRef(null);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = (e) => {
dispatch(e.target.value);
};
$[0] = t0;
} else {
t0 = $[0];
}
const onChange = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = () => {
log(ref.current.value);
};
$[1] = t1;
} else {
t1 = $[1];
}
useEffect(t1);
let t2;
if ($[2] !== state) {
t2 = <input value={state} onChange={onChange} ref={ref} />;
$[2] = state;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
function _temp(_, next) {
return next;
}
```

View File

@@ -23,13 +23,46 @@ function Component() {
## Code
```javascript
// @enableOptimizeForSSR
import { c as _c } from "react/compiler-runtime"; // @enableOptimizeForSSR
import { useReducer } from "react";
function Component() {
const state = 0;
return <input value={state} />;
const $ = _c(4);
const [state, dispatch] = useReducer(_temp, 0);
const ref = useRef(null);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = (e) => {
dispatch(e.target.value);
};
$[0] = t0;
} else {
t0 = $[0];
}
const onChange = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = () => {
log(ref.current.value);
};
$[1] = t1;
} else {
t1 = $[1];
}
useEffect(t1);
let t2;
if ($[2] !== state) {
t2 = <input value={state} onChange={onChange} ref={ref} />;
$[2] = state;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
function _temp(_, next) {
return next;
}
```

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @noEmit
// @outputMode:"lint"
function Foo() {
'use memo';
@@ -19,7 +19,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @noEmit
// @outputMode:"lint"
function Foo() {
"use memo";

View File

@@ -1,4 +1,4 @@
// @noEmit
// @outputMode:"lint"
function Foo() {
'use memo';

View File

@@ -21,7 +21,7 @@ import {isDeepStrictEqual} from 'util';
import type {ParseResult} from '@babel/parser';
const COMPILER_OPTIONS: PluginOptions = {
noEmit: true,
outputMode: 'lint',
panicThreshold: 'none',
// Don't emit errors on Flow suppressions--Flow already gave a signal
flowSuppressions: false,