Compare commits

..

1 Commits

Author SHA1 Message Date
Joe Savona
8219b34b60 [compiler] Fix false positive hook return mutation error
This was fun. We previously added the MaybeAlias effect in #33984 in order to describe the semantic that an unknown function call _may_ alias its return value in its result, but that we don't know this for sure. We record mutations through MaybeAlias edges when walking backward in the data flow graph, but downgrade them to conditional mutations. See the original PR for full context.

That change was sufficient for the original case like

```js
const frozen = useContext();
useEffect(() => {
  frozen.method().property = true;
}, [...]);
```

But it wasn't sufficient for cases where the aliasing occured between operands:

```js
const dispatch = useDispatch();
<div onClick={(e) => {
  dispatch(...e.target.value)
  e.target.value = ...;
}} />
```

Here we would record a `Capture dispatch <- e.target` effect. Then during processing of the `event.target.value = ...` assignment we'd eventually  _forward_ from `event` to `dispatch` (along a MaybeAlias edge). But in #33984 I missed that this forward walk also has to downgrade to conditional.

In addition to that change, we also have to be a bit more precise about which set of effects we create for alias/capture/maybe-alias. The new logic is a bit clearer, I think:

* If the value is frozen, it's an ImmutableCapture edge
* If the values are mutable, it's a Capture
* If it's a context->context, context->mutable, or mutable->context, count it as MaybeAlias.
2025-09-09 13:18:05 -07:00
61 changed files with 746 additions and 3745 deletions

View File

@@ -325,15 +325,6 @@ function runWithEnvironment(
outlineJSX(hir);
}
if (env.config.enableNameAnonymousFunctions) {
nameAnonymousFunctions(hir);
log({
kind: 'hir',
name: 'NameAnonymousFunctions',
value: hir,
});
}
if (env.config.enableFunctionOutlining) {
outlineFunctions(hir, fbtOperands);
log({kind: 'hir', name: 'OutlineFunctions', value: hir});
@@ -424,6 +415,15 @@ function runWithEnvironment(
});
}
if (env.config.enableNameAnonymousFunctions) {
nameAnonymousFunctions(hir);
log({
kind: 'hir',
name: 'NameAnonymougFunctions',
value: hir,
});
}
const reactiveFunction = buildReactiveFunction(hir);
log({
kind: 'reactive',

View File

@@ -47,7 +47,6 @@ import {
makePropertyLiteral,
makeType,
promoteTemporary,
validateIdentifierName,
} from './HIR';
import HIRBuilder, {Bindings, createTemporaryPlace} from './HIRBuilder';
import {BuiltInArrayId} from './ObjectShape';
@@ -214,16 +213,6 @@ export function lower(
);
}
let validatedId: HIRFunction['id'] = null;
if (id != null) {
const idResult = validateIdentifierName(id);
if (idResult.isErr()) {
builder.errors.merge(idResult.unwrapErr());
} else {
validatedId = idResult.unwrap().value;
}
}
if (builder.errors.hasAnyErrors()) {
return Err(builder.errors);
}
@@ -245,8 +234,7 @@ export function lower(
);
return Ok({
id: validatedId,
nameHint: null,
id,
params,
fnType: bindings == null ? env.fnType : 'Other',
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
@@ -3575,14 +3563,19 @@ function lowerFunctionToValue(
): InstructionValue {
const exprNode = expr.node;
const exprLoc = exprNode.loc ?? GeneratedSource;
let name: string | null = null;
if (expr.isFunctionExpression()) {
name = expr.get('id')?.node?.name ?? null;
} else if (expr.isFunctionDeclaration()) {
name = expr.get('id')?.node?.name ?? null;
}
const loweredFunc = lowerFunction(builder, expr);
if (!loweredFunc) {
return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
}
return {
kind: 'FunctionExpression',
name: loweredFunc.func.id,
nameHint: null,
name,
type: expr.node.type,
loc: exprLoc,
loweredFunc,

View File

@@ -7,11 +7,7 @@
import {BindingKind} from '@babel/traverse';
import * as t from '@babel/types';
import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
} from '../CompilerError';
import {CompilerError} from '../CompilerError';
import {assertExhaustive} from '../Utils/utils';
import {Environment, ReactFunctionType} from './Environment';
import type {HookKind} from './ObjectShape';
@@ -58,8 +54,7 @@ export type SourceLocation = t.SourceLocation | typeof GeneratedSource;
*/
export type ReactiveFunction = {
loc: SourceLocation;
id: ValidIdentifierName | null;
nameHint: string | null;
id: string | null;
params: Array<Place | SpreadPattern>;
generator: boolean;
async: boolean;
@@ -281,8 +276,7 @@ export type ReactiveTryTerminal = {
// A function lowered to HIR form, ie where its body is lowered to an HIR control-flow graph
export type HIRFunction = {
loc: SourceLocation;
id: ValidIdentifierName | null;
nameHint: string | null;
id: string | null;
fnType: ReactFunctionType;
env: Environment;
params: Array<Place | SpreadPattern>;
@@ -1130,8 +1124,7 @@ export type JsxAttribute =
export type FunctionExpression = {
kind: 'FunctionExpression';
name: ValidIdentifierName | null;
nameHint: string | null;
name: string | null;
loweredFunc: LoweredFunction;
type:
| 'ArrowFunctionExpression'
@@ -1308,41 +1301,11 @@ export function forkTemporaryIdentifier(
export function validateIdentifierName(
name: string,
): Result<ValidatedIdentifier, CompilerError> {
if (isReservedWord(name)) {
const error = new CompilerError();
error.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Syntax,
reason: 'Expected a non-reserved identifier name',
description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
suggestions: null,
}).withDetails({
kind: 'error',
loc: GeneratedSource,
message: 'reserved word',
}),
);
return Err(error);
} else if (!t.isValidIdentifier(name)) {
const error = new CompilerError();
error.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Syntax,
reason: `Expected a valid identifier name`,
description: `\`${name}\` is not a valid JavaScript identifier`,
suggestions: null,
}).withDetails({
kind: 'error',
loc: GeneratedSource,
message: 'reserved word',
}),
);
): Result<ValidIdentifierName, null> {
if (isReservedWord(name) || !t.isValidIdentifier(name)) {
return Err(null);
}
return Ok({
kind: 'named',
value: name as ValidIdentifierName,
});
return Ok(makeIdentifierName(name).value);
}
/**
@@ -1351,7 +1314,31 @@ export function validateIdentifierName(
* original source code.
*/
export function makeIdentifierName(name: string): ValidatedIdentifier {
return validateIdentifierName(name).unwrap();
if (isReservedWord(name)) {
CompilerError.throwInvalidJS({
reason: 'Expected a non-reserved identifier name',
loc: GeneratedSource,
description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
suggestions: null,
});
} else {
CompilerError.invariant(t.isValidIdentifier(name), {
reason: `Expected a valid identifier name`,
description: `\`${name}\` is not a valid JavaScript identifier`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
suggestions: null,
});
}
return {
kind: 'named',
value: name as ValidIdentifierName,
};
}
/**

View File

@@ -56,9 +56,6 @@ export function printFunction(fn: HIRFunction): string {
} else {
definition += '<<anonymous>>';
}
if (fn.nameHint != null) {
definition += ` ${fn.nameHint}`;
}
if (fn.params.length !== 0) {
definition +=
'(' +

View File

@@ -748,10 +748,14 @@ function applyEffect(
case 'Alias':
case 'Capture': {
CompilerError.invariant(
effect.kind === 'Capture' || initialized.has(effect.into.identifier.id),
effect.kind === 'Capture' ||
effect.kind === 'MaybeAlias' ||
initialized.has(effect.into.identifier.id),
{
reason: `Expected destination value to already be initialized within this instruction for Alias effect`,
description: `Destination ${printPlace(effect.into)} is not initialized in this instruction`,
reason: `Expected destination to already be initialized within this instruction`,
description:
`Destination ${printPlace(effect.into)} is not initialized in this ` +
`instruction for effect ${printAliasingEffect(effect)}`,
details: [
{
kind: 'error',
@@ -767,49 +771,67 @@ function applyEffect(
* copy-on-write semantics, then we can prune the effect
*/
const intoKind = state.kind(effect.into).kind;
let isMutableDesination: boolean;
let destinationType: 'context' | 'mutable' | null = null;
switch (intoKind) {
case ValueKind.Context:
case ValueKind.Mutable:
case ValueKind.MaybeFrozen: {
isMutableDesination = true;
case ValueKind.Context: {
destinationType = 'context';
break;
}
default: {
isMutableDesination = false;
case ValueKind.Mutable:
case ValueKind.MaybeFrozen: {
destinationType = 'mutable';
break;
}
}
const fromKind = state.kind(effect.from).kind;
let isMutableReferenceType: boolean;
let sourceType: 'context' | 'mutable' | 'frozen' | null = null;
switch (fromKind) {
case ValueKind.Context: {
sourceType = 'context';
break;
}
case ValueKind.Global:
case ValueKind.Primitive: {
isMutableReferenceType = false;
break;
}
case ValueKind.Frozen: {
isMutableReferenceType = false;
applyEffect(
context,
state,
{
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
},
initialized,
effects,
);
sourceType = 'frozen';
break;
}
default: {
isMutableReferenceType = true;
sourceType = 'mutable';
break;
}
}
if (isMutableDesination && isMutableReferenceType) {
if (sourceType === 'frozen') {
applyEffect(
context,
state,
{
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
},
initialized,
effects,
);
} else if (
(sourceType === 'mutable' && destinationType === 'mutable') ||
effect.kind === 'MaybeAlias'
) {
effects.push(effect);
} else if (
(sourceType === 'context' && destinationType != null) ||
(sourceType === 'mutable' && destinationType === 'context')
) {
applyEffect(
context,
state,
{kind: 'MaybeAlias', from: effect.from, into: effect.into},
initialized,
effects,
);
}
break;
}

View File

@@ -779,7 +779,13 @@ class AliasingState {
if (edge.index >= index) {
break;
}
queue.push({place: edge.node, transitive, direction: 'forwards', kind});
queue.push({
place: edge.node,
transitive,
direction: 'forwards',
// Traversing a maybeAlias edge always downgrades to conditional mutation
kind: edge.kind === 'maybeAlias' ? MutationKind.Conditional : kind,
});
}
for (const [alias, when] of node.createdFrom) {
if (when >= index) {
@@ -807,7 +813,12 @@ class AliasingState {
if (when >= index) {
continue;
}
queue.push({place: alias, transitive, direction: 'backwards', kind});
queue.push({
place: alias,
transitive,
direction: 'backwards',
kind,
});
}
/**
* MaybeAlias indicates potential data flow from unknown function calls,

View File

@@ -249,7 +249,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
const fn: HIRFunction = {
loc: GeneratedSource,
id: null,
nameHint: null,
fnType: 'Other',
env,
params: [obj],
@@ -276,7 +275,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
value: {
kind: 'FunctionExpression',
name: null,
nameHint: null,
loweredFunc: {
func: fn,
},

View File

@@ -31,9 +31,7 @@ export function outlineFunctions(
) {
const loweredFunc = value.loweredFunc.func;
const id = fn.env.generateGloballyUniqueIdentifierName(
loweredFunc.id ?? loweredFunc.nameHint,
);
const id = fn.env.generateGloballyUniqueIdentifierName(loweredFunc.id);
loweredFunc.id = id.value;
fn.env.outlineFunction(loweredFunc, null);

View File

@@ -364,7 +364,6 @@ function emitOutlinedFn(
const fn: HIRFunction = {
loc: GeneratedSource,
id: null,
nameHint: null,
fnType: 'Other',
env,
params: [propsObj],

View File

@@ -44,7 +44,6 @@ export function buildReactiveFunction(fn: HIRFunction): ReactiveFunction {
return {
loc: fn.loc,
id: fn.id,
nameHint: fn.nameHint,
params: fn.params,
generator: fn.generator,
async: fn.async,

View File

@@ -43,6 +43,7 @@ import {
ValidIdentifierName,
getHookKind,
makeIdentifierName,
validateIdentifierName,
} from '../HIR/HIR';
import {printIdentifier, printInstruction, printPlace} from '../HIR/PrintHIR';
import {eachPatternOperand} from '../HIR/visitors';
@@ -61,7 +62,6 @@ export const EARLY_RETURN_SENTINEL = 'react.early_return_sentinel';
export type CodegenFunction = {
type: 'CodegenFunction';
id: t.Identifier | null;
nameHint: string | null;
params: t.FunctionDeclaration['params'];
body: t.BlockStatement;
generator: boolean;
@@ -384,7 +384,6 @@ function codegenReactiveFunction(
type: 'CodegenFunction',
loc: fn.loc,
id: fn.id !== null ? t.identifier(fn.id) : null,
nameHint: fn.nameHint,
params,
body,
generator: fn.generator,
@@ -2329,6 +2328,10 @@ function codegenInstructionValue(
reactiveFunction,
).unwrap();
const validatedName =
instrValue.name != null
? validateIdentifierName(instrValue.name)
: Err(null);
if (instrValue.type === 'ArrowFunctionExpression') {
let body: t.BlockStatement | t.Expression = fn.body;
if (body.body.length === 1 && loweredFunc.directives.length == 0) {
@@ -2340,7 +2343,9 @@ function codegenInstructionValue(
value = t.arrowFunctionExpression(fn.params, body, fn.async);
} else {
value = t.functionExpression(
instrValue.name != null ? t.identifier(instrValue.name) : null,
validatedName
.map<t.Identifier | null>(name => t.identifier(name))
.unwrapOr(null),
fn.params,
fn.body,
fn.generator,
@@ -2349,10 +2354,10 @@ function codegenInstructionValue(
}
if (
cx.env.config.enableNameAnonymousFunctions &&
instrValue.name == null &&
instrValue.nameHint != null
validatedName.isErr() &&
instrValue.name != null
) {
const name = instrValue.nameHint;
const name = instrValue.name;
value = t.memberExpression(
t.objectExpression([t.objectProperty(t.stringLiteral(name), value)]),
t.stringLiteral(name),

View File

@@ -26,8 +26,7 @@ export function nameAnonymousFunctions(fn: HIRFunction): void {
* nesting depth.
*/
const name = `${prefix}${node.generatedName}]`;
node.fn.nameHint = name;
node.fn.loweredFunc.func.nameHint = name;
node.fn.name = name;
}
/**
* Whether or not we generated a name for the function at this node,

View File

@@ -5,217 +5,21 @@
* LICENSE file in the root directory of this source tree.
*/
import {effect} from 'zod';
import {CompilerError, Effect, ErrorSeverity, SourceLocation} from '..';
import {CompilerError, SourceLocation} from '..';
import {ErrorCategory} from '../CompilerError';
import {
ArrayExpression,
BasicBlock,
BlockId,
Identifier,
FunctionExpression,
HIRFunction,
IdentifierId,
Instruction,
Place,
isSetStateType,
isUseEffectHookType,
isUseStateType,
IdentifierName,
GeneratedSource,
} from '../HIR';
import {printInstruction} from '../HIR/PrintHIR';
import {
eachInstructionOperand,
eachInstructionValueOperand,
eachTerminalOperand,
eachInstructionLValue,
eachPatternOperand,
} from '../HIR/visitors';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
import {assertExhaustive} from '../Utils/utils';
type SetStateCall = {
loc: SourceLocation;
invalidDeps: Map<Identifier, Place[]> | undefined;
setStateId: IdentifierId;
};
type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsOrState';
type DerivationMetadata = {
typeOfValue: TypeOfValue;
// TODO: Rename to place
identifierPlace: Place;
sources: Place[];
};
// TODO: This needs refining
type ErrorMetadata = {
errorType: 'HoistState' | 'CalculateInRender';
propInfo: string | undefined;
loc: SourceLocation;
setStateId: IdentifierId;
};
function joinValue(
lvalueType: TypeOfValue,
valueType: TypeOfValue,
): TypeOfValue {
if (lvalueType === 'ignored') return valueType;
if (valueType === 'ignored') return lvalueType;
if (lvalueType === valueType) return lvalueType;
return 'fromPropsOrState';
}
function updateDerivationMetadata(
target: Place,
sources: DerivationMetadata[],
typeOfValue: TypeOfValue,
derivedTuple: Map<IdentifierId, DerivationMetadata>,
): void {
let newValue: DerivationMetadata = {
identifierPlace: target,
sources: [],
typeOfValue: typeOfValue,
};
for (const source of sources) {
// If the identifier of the source is a promoted identifier, then
// we should set the target as the source.
if (source.identifierPlace.identifier.name?.kind === 'promoted') {
newValue.sources.push(target);
} else {
newValue.sources.push(...source.sources);
}
}
derivedTuple.set(target.identifier.id, newValue);
}
function parseInstr(
instr: Instruction,
derivedTuple: Map<IdentifierId, DerivationMetadata>,
setStateCalls: Map<string, Place>,
) {
// console.log(printInstruction(instr));
// console.log(instr);
let typeOfValue: TypeOfValue = 'ignored';
// If the instruction is destructuring a useState hook call
if (
instr.value.kind === 'Destructure' &&
instr.value.lvalue.pattern.kind === 'ArrayPattern' &&
isUseStateType(instr.value.value.identifier)
) {
const value = instr.value.lvalue.pattern.items[0];
if (value.kind === 'Identifier') {
derivedTuple.set(value.identifier.id, {
identifierPlace: value,
sources: [value],
typeOfValue: 'fromState',
});
}
}
// If the instruction is calling a setState
if (
instr.value.kind === 'CallExpression' &&
isSetStateType(instr.value.callee.identifier) &&
instr.value.args.length === 1 &&
instr.value.args[0].kind === 'Identifier' &&
instr.value.callee.loc !== GeneratedSource &&
instr.value.callee.loc.identifierName !== undefined &&
instr.value.callee.loc.identifierName !== null
) {
setStateCalls.set(
instr.value.callee.loc.identifierName,
instr.value.callee,
);
}
let sources: DerivationMetadata[] = [];
for (const operand of eachInstructionOperand(instr)) {
const opSource = derivedTuple.get(operand.identifier.id);
if (opSource === undefined) {
continue;
}
typeOfValue = joinValue(typeOfValue, opSource.typeOfValue);
sources.push(opSource);
}
if (typeOfValue !== 'ignored') {
for (const lvalue of eachInstructionLValue(instr)) {
updateDerivationMetadata(lvalue, sources, typeOfValue, derivedTuple);
}
for (const operand of eachInstructionOperand(instr)) {
switch (operand.effect) {
case Effect.Capture:
case Effect.Store:
case Effect.ConditionallyMutate:
case Effect.ConditionallyMutateIterator:
case Effect.Mutate: {
if (isMutable(instr, operand)) {
updateDerivationMetadata(
operand,
sources,
typeOfValue,
derivedTuple,
);
}
break;
}
case Effect.Freeze:
case Effect.Read: {
// no-op
break;
}
case Effect.Unknown: {
CompilerError.invariant(false, {
reason: 'Unexpected unknown effect',
description: null,
loc: operand.loc,
suggestions: null,
});
}
default: {
assertExhaustive(
operand.effect,
`Unexpected effect kind \`${operand.effect}\``,
);
}
}
}
}
}
function parseBlockPhi(
block: BasicBlock,
derivedTuple: Map<IdentifierId, DerivationMetadata>,
) {
for (const phi of block.phis) {
for (const operand of phi.operands.values()) {
const source = derivedTuple.get(operand.identifier.id);
if (source !== undefined && source.typeOfValue === 'fromProps') {
if (
source.identifierPlace.identifier.name === null ||
source.identifierPlace.identifier.name?.kind === 'promoted'
) {
derivedTuple.set(phi.place.identifier.id, {
identifierPlace: phi.place,
sources: [phi.place],
typeOfValue: 'fromProps',
});
} else {
derivedTuple.set(phi.place.identifier.id, {
identifierPlace: phi.place,
sources: source.sources,
typeOfValue: 'fromProps',
});
}
}
}
}
}
/**
* Validates that useEffect is not used for derived computations which could/should
@@ -244,58 +48,12 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
const functions: Map<IdentifierId, FunctionExpression> = new Map();
const locals: Map<IdentifierId, IdentifierId> = new Map();
const derivedTuple: Map<IdentifierId, DerivationMetadata> = new Map();
// Investigating
const effectSetStates: Map<string, Place> = new Map();
const setStateCalls: Map<string, Place> = new Map();
// let shouldCalculateInRender: boolean = true;
const errors: ErrorMetadata[] = [];
if (fn.fnType === 'Hook') {
for (const param of fn.params) {
if (param.kind === 'Identifier') {
derivedTuple.set(param.identifier.id, {
identifierPlace: param,
sources: [param],
typeOfValue: 'fromProps',
});
}
}
} else if (fn.fnType === 'Component') {
const props = fn.params[0];
if (props != null && props.kind === 'Identifier') {
derivedTuple.set(props.identifier.id, {
identifierPlace: props,
sources: [props],
typeOfValue: 'fromProps',
});
}
}
const errors = new CompilerError();
for (const block of fn.body.blocks.values()) {
parseBlockPhi(block, derivedTuple);
for (const instr of block.instructions) {
const {lvalue, value} = instr;
parseInstr(instr, derivedTuple, setStateCalls);
/*
* Special case for function expressions, we need to parse nested instructions
* TODO: Can there be more recursive levels?
*/
if (value.kind === 'FunctionExpression') {
for (const [, block] of value.loweredFunc.func.body.blocks) {
for (const instr of block.instructions) {
parseInstr(instr, derivedTuple, setStateCalls);
}
}
}
// Maybe this should run for every instruction being parsed
if (value.kind === 'LoadLocal') {
locals.set(lvalue.identifier.id, value.place.identifier.id);
} else if (value.kind === 'ArrayExpression') {
@@ -308,7 +66,6 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
) {
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
if (
isUseEffectHookType(callee.identifier) &&
value.args.length === 2 &&
@@ -340,8 +97,6 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
validateEffect(
effectFunction.loweredFunc.func,
dependencies,
derivedTuple,
effectSetStates,
errors,
);
}
@@ -349,82 +104,43 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
}
}
}
console.log('setStateCalls: ', setStateCalls);
console.log('effectSetStates: ', effectSetStates);
const throwableErrors = new CompilerError();
for (const error of errors) {
throwableErrors.push({
reason: `You may not need an effect. Values derived from state should be calculated in render, not in an effect. `,
description: `You are using a value derived from props${error.propInfo} to update local state in an effect.`,
severity: ErrorSeverity.InvalidReact,
loc: error.loc,
});
}
if (throwableErrors.hasAnyErrors()) {
throw throwableErrors;
if (errors.hasAnyErrors()) {
throw errors;
}
}
function validateEffect(
effectFunction: HIRFunction,
effectDeps: Array<IdentifierId>,
derivedTuple: Map<IdentifierId, DerivationMetadata>,
effectSetStates: Map<string, Place>,
errors: ErrorMetadata[],
errors: CompilerError,
): void {
/*
* TODO: This makes it so we only capture single line useEffects.
* We should be able to capture multiline as well
*/
for (const operand of effectFunction.context) {
if (isSetStateType(operand.identifier)) {
continue;
} else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
continue;
} else if (derivedTuple.has(operand.identifier.id)) {
continue;
} else {
// Captured something other than the effect dep or setState
return;
}
}
// This might be wrong gotta double check
let hasInvalidDep = false;
for (const dep of effectDeps) {
const depMetadata = derivedTuple.get(dep);
if (
effectFunction.context.find(operand => operand.identifier.id === dep) !=
null ||
(depMetadata !== undefined && depMetadata.typeOfValue !== 'ignored')
effectFunction.context.find(operand => operand.identifier.id === dep) ==
null
) {
hasInvalidDep = true;
// effect dep wasn't actually used in the function
return;
}
}
if (!hasInvalidDep) {
console.log('early return 2');
// effect dep wasn't actually used in the function
return;
}
const seenBlocks: Set<BlockId> = new Set();
// This variable is suspicious maybe we don't need it?
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
const effectInvalidlyDerived: Map<IdentifierId, DerivationMetadata> =
new Map();
for (const dep of effectDeps) {
values.set(dep, [dep]);
const depMetadata = derivedTuple.get(dep);
if (depMetadata !== undefined) {
effectInvalidlyDerived.set(dep, depMetadata);
}
}
const setStateCallsInEffect: Array<SetStateCall> = [];
const setStateLocations: Array<SourceLocation> = [];
for (const block of effectFunction.body.blocks.values()) {
for (const pred of block.preds) {
if (!seenBlocks.has(pred)) {
@@ -432,24 +148,21 @@ function validateEffect(
return;
}
}
parseBlockPhi(block, effectInvalidlyDerived);
for (const instr of block.instructions) {
if (
instr.value.kind === 'CallExpression' &&
isSetStateType(instr.value.callee.identifier) &&
instr.value.args.length === 1 &&
instr.value.args[0].kind === 'Identifier' &&
instr.value.callee.loc !== GeneratedSource &&
instr.value.callee.loc.identifierName !== undefined &&
instr.value.callee.loc.identifierName !== null
) {
effectSetStates.set(
instr.value.callee.loc.identifierName,
instr.value.callee,
);
for (const phi of block.phis) {
const aggregateDeps: Set<IdentifierId> = new Set();
for (const operand of phi.operands.values()) {
const deps = values.get(operand.identifier.id);
if (deps != null) {
for (const dep of deps) {
aggregateDeps.add(dep);
}
}
}
if (aggregateDeps.size !== 0) {
values.set(phi.place.identifier.id, Array.from(aggregateDeps));
}
}
for (const instr of block.instructions) {
switch (instr.value.kind) {
case 'Primitive':
case 'JSXText':
@@ -470,7 +183,7 @@ function validateEffect(
case 'CallExpression':
case 'MethodCall': {
const aggregateDeps: Set<IdentifierId> = new Set();
for (const operand of eachInstructionOperand(instr)) {
for (const operand of eachInstructionValueOperand(instr.value)) {
const deps = values.get(operand.identifier.id);
if (deps != null) {
for (const dep of deps) {
@@ -488,74 +201,38 @@ function validateEffect(
instr.value.args.length === 1 &&
instr.value.args[0].kind === 'Identifier'
) {
const propSources = derivedTuple.get(
instr.value.args[0].identifier.id,
);
if (propSources !== undefined) {
setStateCallsInEffect.push({
loc: instr.value.callee.loc,
setStateId: instr.value.callee.identifier.id,
invalidDeps: new Map([
[instr.value.args[0].identifier, propSources.sources],
]),
});
const deps = values.get(instr.value.args[0].identifier.id);
if (deps != null && new Set(deps).size === effectDeps.length) {
setStateLocations.push(instr.value.callee.loc);
} else {
setStateCallsInEffect.push({
loc: instr.value.callee.loc,
setStateId: instr.value.callee.identifier.id,
invalidDeps: undefined,
});
// doesn't depend on any deps
return;
}
}
break;
}
default: {
console.log('early return 4');
return;
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
if (values.has(operand.identifier.id)) {
//
return;
}
}
seenBlocks.add(block.id);
}
// need to track if the setState call has been used elsewhere
// if it is then the solution should be to lift the state up to the parent component
// if not the solution should be to calculate the value in rende
//
// If the same setState is used both inside and outside the effect
for (const call of setStateCallsInEffect) {
if (call.invalidDeps != null) {
let propNames = '';
for (const [, places] of call.invalidDeps.entries()) {
const placeNames = places
.map(place => place.identifier.name?.value)
.join(', ');
propNames += `[${placeNames}], `;
}
propNames = propNames.slice(0, -2);
const propInfo = propNames ? ` (from props '${propNames}')` : '';
errors.push({
errorType: 'HoistState',
propInfo: propInfo,
loc: call.loc,
setStateId: call.setStateId,
});
} else {
errors.push({
errorType: 'CalculateInRender',
propInfo: undefined,
loc: call.loc,
setStateId: call.setStateId,
});
}
for (const loc of setStateLocations) {
errors.push({
category: ErrorCategory.EffectDerivationsOfState,
reason:
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
description: null,
loc,
suggestions: null,
});
}
}

View File

@@ -18,6 +18,7 @@ import {
IdentifierId,
InstructionValue,
ManualMemoDependency,
Place,
PrunedReactiveScopeBlock,
ReactiveFunction,
ReactiveInstruction,
@@ -28,10 +29,7 @@ import {
SourceLocation,
} from '../HIR';
import {printIdentifier, printManualMemoDependency} from '../HIR/PrintHIR';
import {
eachInstructionValueLValue,
eachInstructionValueOperand,
} from '../HIR/visitors';
import {eachInstructionValueOperand} from '../HIR/visitors';
import {collectMaybeMemoDependencies} from '../Inference/DropManualMemoization';
import {
ReactiveFunctionVisitor,
@@ -339,53 +337,56 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
* @returns a @{ManualMemoDependency} representing the variable +
* property reads represented by @value
*/
recordDepsInValue(value: ReactiveValue, state: VisitorState): void {
recordDepsInValue(
value: ReactiveValue,
state: VisitorState,
): ManualMemoDependency | null {
switch (value.kind) {
case 'SequenceExpression': {
for (const instr of value.instructions) {
this.visitInstruction(instr, state);
}
this.recordDepsInValue(value.value, state);
break;
const result = this.recordDepsInValue(value.value, state);
return result;
}
case 'OptionalExpression': {
this.recordDepsInValue(value.value, state);
break;
return this.recordDepsInValue(value.value, state);
}
case 'ConditionalExpression': {
this.recordDepsInValue(value.test, state);
this.recordDepsInValue(value.consequent, state);
this.recordDepsInValue(value.alternate, state);
break;
return null;
}
case 'LogicalExpression': {
this.recordDepsInValue(value.left, state);
this.recordDepsInValue(value.right, state);
break;
return null;
}
default: {
collectMaybeMemoDependencies(value, this.temporaries, false);
if (
value.kind === 'StoreLocal' ||
value.kind === 'StoreContext' ||
value.kind === 'Destructure'
) {
for (const storeTarget of eachInstructionValueLValue(value)) {
state.manualMemoState?.decls.add(
storeTarget.identifier.declarationId,
);
if (storeTarget.identifier.name?.kind === 'named') {
this.temporaries.set(storeTarget.identifier.id, {
root: {
kind: 'NamedLocal',
value: storeTarget,
},
path: [],
});
}
const dep = collectMaybeMemoDependencies(
value,
this.temporaries,
false,
);
if (value.kind === 'StoreLocal' || value.kind === 'StoreContext') {
const storeTarget = value.lvalue.place;
state.manualMemoState?.decls.add(
storeTarget.identifier.declarationId,
);
if (storeTarget.identifier.name?.kind === 'named' && dep == null) {
const dep: ManualMemoDependency = {
root: {
kind: 'NamedLocal',
value: storeTarget,
},
path: [],
};
this.temporaries.set(storeTarget.identifier.id, dep);
return dep;
}
}
break;
return dep;
}
}
}
@@ -402,15 +403,19 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
state.manualMemoState.decls.add(lvalue.identifier.declarationId);
}
this.recordDepsInValue(value, state);
if (lvalue != null) {
temporaries.set(lvalue.identifier.id, {
root: {
kind: 'NamedLocal',
value: {...lvalue},
},
path: [],
});
const maybeDep = this.recordDepsInValue(value, state);
if (lvalId != null) {
if (maybeDep != null) {
temporaries.set(lvalId, maybeDep);
} else if (isNamedLocal) {
temporaries.set(lvalId, {
root: {
kind: 'NamedLocal',
value: {...(instr.lvalue as Place)},
},
path: [],
});
}
}
}

View File

@@ -24,15 +24,13 @@ function BadExample() {
```
Found 1 error:
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
This effect updates state based on other state values. Consider calculating this value directly during render.
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.invalid-derived-computation-in-effect.ts:9:4
7 | const [fullName, setFullName] = useState('');
8 | useEffect(() => {
> 9 | setFullName(capitalize(firstName + ' ' + lastName));
| ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
10 | }, [firstName, lastName]);
11 |
12 | return <div>{fullName}</div>;

View File

@@ -1,59 +0,0 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
/**
* Repro from https://github.com/facebook/react/issues/34262
*
* The compiler memoizes more precisely than the original code, with two reactive scopes:
* - One for `transform(input)` with `input` as dep
* - One for `{value}` with `value` as dep
*
* When we validate preserving manual memoization we incorrectly reject this, because
* the original memoization had `object` depending on `input` but our scope depends on
* `value`.
*
* This fixture adds a later potential mutation, which extends the scope and should
* fail validation. This confirms that even though we allow the dependency to diverge,
* we still check that the output value is memoized.
*/
function useInputValue(input) {
const object = React.useMemo(() => {
const {value} = transform(input);
return {value};
}, [input]);
mutate(object);
return object;
}
```
## Error
```
Found 1 error:
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.
error.repro-preserve-memoization-inner-destructured-value-mistaken-as-dependency-later-mutation.ts:19:17
17 | */
18 | function useInputValue(input) {
> 19 | const object = React.useMemo(() => {
| ^^^^^^^^^^^^^^^^^^^^^
> 20 | const {value} = transform(input);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 21 | return {value};
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 22 | }, [input]);
| ^^^^^^^^^^^^^^ Could not preserve existing memoization
23 | mutate(object);
24 | return object;
25 | }
```

View File

@@ -1,25 +0,0 @@
// @validatePreserveExistingMemoizationGuarantees
/**
* Repro from https://github.com/facebook/react/issues/34262
*
* The compiler memoizes more precisely than the original code, with two reactive scopes:
* - One for `transform(input)` with `input` as dep
* - One for `{value}` with `value` as dep
*
* When we validate preserving manual memoization we incorrectly reject this, because
* the original memoization had `object` depending on `input` but our scope depends on
* `value`.
*
* This fixture adds a later potential mutation, which extends the scope and should
* fail validation. This confirms that even though we allow the dependency to diverge,
* we still check that the output value is memoized.
*/
function useInputValue(input) {
const object = React.useMemo(() => {
const {value} = transform(input);
return {value};
}, [input]);
mutate(object);
return object;
}

View File

@@ -1,64 +0,0 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
import {identity, Stringify, useHook} from 'shared-runtime';
/**
* Repro from https://github.com/facebook/react/issues/34262
*
* The compiler memoizes more precisely than the original code, with two reactive scopes:
* - One for `transform(input)` with `input` as dep
* - One for `{value}` with `value` as dep
*
* When we validate preserving manual memoization we incorrectly reject this, because
* the original memoization had `object` depending on `input` but our scope depends on
* `value`.
*/
function useInputValue(input) {
// Conflate the `identity(input, x)` call with something outside the useMemo,
// to try and break memoization of `value`. This gets correctly flagged since
// the dependency is being mutated
let x = {};
useHook();
const object = React.useMemo(() => {
const {value} = identity(input, x);
return {value};
}, [input, x]);
return object;
}
function Component() {
return <Stringify value={useInputValue({value: 42}).value} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
## Error
```
Found 1 error:
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly.
error.repro-preserve-memoization-inner-destructured-value-mistaken-as-dependency-mutated-dep.ts:25:13
23 | const {value} = identity(input, x);
24 | return {value};
> 25 | }, [input, x]);
| ^ This dependency may be modified later
26 | return object;
27 | }
28 |
```

View File

@@ -1,36 +0,0 @@
// @validatePreserveExistingMemoizationGuarantees
import {identity, Stringify, useHook} from 'shared-runtime';
/**
* Repro from https://github.com/facebook/react/issues/34262
*
* The compiler memoizes more precisely than the original code, with two reactive scopes:
* - One for `transform(input)` with `input` as dep
* - One for `{value}` with `value` as dep
*
* When we validate preserving manual memoization we incorrectly reject this, because
* the original memoization had `object` depending on `input` but our scope depends on
* `value`.
*/
function useInputValue(input) {
// Conflate the `identity(input, x)` call with something outside the useMemo,
// to try and break memoization of `value`. This gets correctly flagged since
// the dependency is being mutated
let x = {};
useHook();
const object = React.useMemo(() => {
const {value} = identity(input, x);
return {value};
}, [input, x]);
return object;
}
function Component() {
return <Stringify value={useInputValue({value: 42}).value} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};

View File

@@ -1,52 +0,0 @@
## Input
```javascript
// @enableNameAnonymousFunctions
import {Stringify} from 'shared-runtime';
function Component(props) {
const onClick = () => {
console.log('hello!');
};
return <div onClick={onClick} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNameAnonymousFunctions
import { Stringify } from "shared-runtime";
function Component(props) {
const $ = _c(1);
const onClick = _ComponentOnClick;
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <div onClick={onClick} />;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
function _ComponentOnClick() {
console.log("hello!");
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 42 }],
};
```
### Eval output
(kind: ok) <div></div>

View File

@@ -1,14 +0,0 @@
// @enableNameAnonymousFunctions
import {Stringify} from 'shared-runtime';
function Component(props) {
const onClick = () => {
console.log('hello!');
};
return <div onClick={onClick} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};

View File

@@ -0,0 +1,82 @@
## Input
```javascript
// @compilationMode:"infer"
function Component() {
const dispatch = useDispatch();
// const [state, setState] = useState(0);
return (
<div>
<input
type="file"
onChange={event => {
dispatch(...event.target);
event.target.value = '';
}}
/>
</div>
);
}
function useDispatch() {
'use no memo';
// skip compilation to make it easier to debug the above function
return (...values) => {
console.log(...values);
};
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer"
function Component() {
const $ = _c(2);
const dispatch = useDispatch();
let t0;
if ($[0] !== dispatch) {
t0 = (
<div>
<input
type="file"
onChange={(event) => {
dispatch(...event.target);
event.target.value = "";
}}
/>
</div>
);
$[0] = dispatch;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
function useDispatch() {
"use no memo";
// skip compilation to make it easier to debug the above function
return (...values) => {
console.log(...values);
};
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
### Eval output
(kind: ok) <div><input type="file"></div>

View File

@@ -0,0 +1,30 @@
// @compilationMode:"infer"
function Component() {
const dispatch = useDispatch();
// const [state, setState] = useState(0);
return (
<div>
<input
type="file"
onChange={event => {
dispatch(...event.target);
event.target.value = '';
}}
/>
</div>
);
}
function useDispatch() {
'use no memo';
// skip compilation to make it easier to debug the above function
return (...values) => {
console.log(...values);
};
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};

View File

@@ -1,109 +0,0 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
import {identity, Stringify} from 'shared-runtime';
/**
* Repro from https://github.com/facebook/react/issues/34262
*
* The compiler memoizes more precisely than the original code, with two reactive scopes:
* - One for `transform(input)` with `input` as dep
* - One for `{value}` with `value` as dep
*
* Previously ValidatePreservedManualMemoization rejected this input, because
* the original memoization had `object` depending on `input` but we split the scope per above,
* and the scope for the FinishMemoize instruction is the second scope which depends on `value`
*/
function useInputValue(input) {
const object = React.useMemo(() => {
const {value} = identity(input);
return {value};
}, [input]);
return object;
}
function Component() {
return <Stringify value={useInputValue({value: 42}).value} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
import { identity, Stringify } from "shared-runtime";
/**
* Repro from https://github.com/facebook/react/issues/34262
*
* The compiler memoizes more precisely than the original code, with two reactive scopes:
* - One for `transform(input)` with `input` as dep
* - One for `{value}` with `value` as dep
*
* Previously ValidatePreservedManualMemoization rejected this input, because
* the original memoization had `object` depending on `input` but we split the scope per above,
* and the scope for the FinishMemoize instruction is the second scope which depends on `value`
*/
function useInputValue(input) {
const $ = _c(4);
let t0;
if ($[0] !== input) {
t0 = identity(input);
$[0] = input;
$[1] = t0;
} else {
t0 = $[1];
}
const { value } = t0;
let t1;
if ($[2] !== value) {
t1 = { value };
$[2] = value;
$[3] = t1;
} else {
t1 = $[3];
}
const object = t1;
return object;
}
function Component() {
const $ = _c(3);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = { value: 42 };
$[0] = t0;
} else {
t0 = $[0];
}
const t1 = useInputValue(t0);
let t2;
if ($[1] !== t1.value) {
t2 = <Stringify value={t1.value} />;
$[1] = t1.value;
$[2] = t2;
} else {
t2 = $[2];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
### Eval output
(kind: ok) <div>{"value":42}</div>

View File

@@ -1,31 +0,0 @@
// @validatePreserveExistingMemoizationGuarantees
import {identity, Stringify} from 'shared-runtime';
/**
* Repro from https://github.com/facebook/react/issues/34262
*
* The compiler memoizes more precisely than the original code, with two reactive scopes:
* - One for `transform(input)` with `input` as dep
* - One for `{value}` with `value` as dep
*
* Previously ValidatePreservedManualMemoization rejected this input, because
* the original memoization had `object` depending on `input` but we split the scope per above,
* and the scope for the FinishMemoize instruction is the second scope which depends on `value`
*/
function useInputValue(input) {
const object = React.useMemo(() => {
const {value} = identity(input);
return {value};
}, [input]);
return object;
}
function Component() {
return <Stringify value={useInputValue({value: 42}).value} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};

View File

@@ -1,87 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({initialName}) {
const [name, setName] = useState('');
useEffect(() => {
setName(initialName);
}, []);
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{initialName: 'John'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(6);
const { initialName } = t0;
const [name, setName] = useState("");
let t1;
if ($[0] !== initialName) {
t1 = () => {
setName(initialName);
};
$[0] = initialName;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t2 = [];
$[2] = t2;
} else {
t2 = $[2];
}
useEffect(t1, t2);
let t3;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t3 = (e) => setName(e.target.value);
$[3] = t3;
} else {
t3 = $[3];
}
let t4;
if ($[4] !== name) {
t4 = (
<div>
<input value={name} onChange={t3} />
</div>
);
$[4] = name;
$[5] = t4;
} else {
t4 = $[5];
}
return t4;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ initialName: "John" }],
};
```
### Eval output
(kind: ok) <div><input value="John"></div>

View File

@@ -1,21 +0,0 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({initialName}) {
const [name, setName] = useState('');
useEffect(() => {
setName(initialName);
}, []);
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{initialName: 'John'}],
};

View File

@@ -1,79 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({value, enabled}) {
const [localValue, setLocalValue] = useState('');
useEffect(() => {
if (enabled) {
setLocalValue(value);
} else {
setLocalValue('disabled');
}
}, [value, enabled]);
return <div>{localValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 'test', enabled: true}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(6);
const { value, enabled } = t0;
const [localValue, setLocalValue] = useState("");
let t1;
let t2;
if ($[0] !== enabled || $[1] !== value) {
t1 = () => {
if (enabled) {
setLocalValue(value);
} else {
setLocalValue("disabled");
}
};
t2 = [value, enabled];
$[0] = enabled;
$[1] = value;
$[2] = t1;
$[3] = t2;
} else {
t1 = $[2];
t2 = $[3];
}
useEffect(t1, t2);
let t3;
if ($[4] !== localValue) {
t3 = <div>{localValue}</div>;
$[4] = localValue;
$[5] = t3;
} else {
t3 = $[5];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: "test", enabled: true }],
};
```
### Eval output
(kind: ok) <div>test</div>

View File

@@ -1,21 +0,0 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({value, enabled}) {
const [localValue, setLocalValue] = useState('');
useEffect(() => {
if (enabled) {
setLocalValue(value);
} else {
setLocalValue('disabled');
}
}, [value, enabled]);
return <div>{localValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 'test', enabled: true}],
};

View File

@@ -1,74 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({value}) {
const [localValue, setLocalValue] = useState('');
useEffect(() => {
console.log('Value changed:', value);
setLocalValue(value);
document.title = `Value: ${value}`;
}, [value]);
return <div>{localValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 'test'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(5);
const { value } = t0;
const [localValue, setLocalValue] = useState("");
let t1;
let t2;
if ($[0] !== value) {
t1 = () => {
console.log("Value changed:", value);
setLocalValue(value);
document.title = `Value: ${value}`;
};
t2 = [value];
$[0] = value;
$[1] = t1;
$[2] = t2;
} else {
t1 = $[1];
t2 = $[2];
}
useEffect(t1, t2);
let t3;
if ($[3] !== localValue) {
t3 = <div>{localValue}</div>;
$[3] = localValue;
$[4] = t3;
} else {
t3 = $[4];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: "test" }],
};
```
### Eval output
(kind: ok) <div>test</div>
logs: ['Value changed:','test']

View File

@@ -1,19 +0,0 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({value}) {
const [localValue, setLocalValue] = useState('');
useEffect(() => {
console.log('Value changed:', value);
setLocalValue(value);
document.title = `Value: ${value}`;
}, [value]);
return <div>{localValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 'test'}],
};

View File

@@ -1,51 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({prefix}) {
const [name, setName] = useState('');
const [displayName, setDisplayName] = useState('');
useEffect(() => {
setDisplayName(prefix + name);
}, [prefix, name]);
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<div>{displayName}</div>
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prefix: 'Hello, '}],
};
```
## Error
```
Found 1 error:
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
This effect updates state based on other state values. Consider calculating this value directly during render.
error.bug-derived-state-from-mixed-deps.ts:9:4
7 |
8 | useEffect(() => {
> 9 | setDisplayName(prefix + name);
| ^^^^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
10 | }, [prefix, name]);
11 |
12 | return (
```

View File

@@ -1,23 +0,0 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({prefix}) {
const [name, setName] = useState('');
const [displayName, setDisplayName] = useState('');
useEffect(() => {
setDisplayName(prefix + name);
}, [prefix, name]);
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<div>{displayName}</div>
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prefix: 'Hello, '}],
};

View File

@@ -1,45 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({user: {firstName, lastName}}) {
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return <div>{fullName}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{user: {firstName: 'John', lastName: 'Doe'}}],
};
```
## Error
```
Found 1 error:
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
This effect updates state based on other state values. Consider calculating this value directly during render.
error.invalid-derived-state-from-props-destructured.ts:8:4
6 |
7 | useEffect(() => {
> 8 | setFullName(firstName + ' ' + lastName);
| ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
9 | }, [firstName, lastName]);
10 |
11 | return <div>{fullName}</div>;
```

View File

@@ -1,17 +0,0 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({firstName, lastName}) {
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return <div>{fullName}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{firstName: 'John', lastName: 'Doe'}],
};

View File

@@ -1,45 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({firstName, lastName}) {
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return <div>{fullName}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{firstName: 'John', lastName: 'Doe'}],
};
```
## Error
```
Found 1 error:
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
This effect updates state based on other state values. Consider calculating this value directly during render.
error.invalid-derived-state-from-props-in-effect.ts:8:4
6 |
7 | useEffect(() => {
> 8 | setFullName(firstName + ' ' + lastName);
| ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
9 | }, [firstName, lastName]);
10 |
11 | return <div>{fullName}</div>;
```

View File

@@ -1,17 +0,0 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({firstName, lastName}) {
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return <div>{fullName}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{firstName: 'John', lastName: 'Doe'}],
};

View File

@@ -1,53 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component() {
const [firstName, setFirstName] = useState('John');
const [lastName, setLastName] = useState('Doe');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return (
<div>
<input value={firstName} onChange={e => setFirstName(e.target.value)} />
<input value={lastName} onChange={e => setLastName(e.target.value)} />
<div>{fullName}</div>
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};
```
## Error
```
Found 1 error:
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
This effect updates state based on other state values. Consider calculating this value directly during render.
error.invalid-derived-state-from-state-in-effect.ts:10:4
8 |
9 | useEffect(() => {
> 10 | setFullName(firstName + ' ' + lastName);
| ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
11 | }, [firstName, lastName]);
12 |
13 | return (
```

View File

@@ -1,25 +0,0 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component() {
const [firstName, setFirstName] = useState('John');
const [lastName, setLastName] = useState('Doe');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return (
<div>
<input value={firstName} onChange={e => setFirstName(e.target.value)} />
<input value={lastName} onChange={e => setLastName(e.target.value)} />
<div>{fullName}</div>
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};

View File

@@ -1,72 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component(props) {
const [displayValue, setDisplayValue] = useState('');
useEffect(() => {
const computed = props.prefix + props.value + props.suffix;
setDisplayValue(computed);
}, [props.prefix, props.value, props.suffix]);
return <div>{displayValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prefix: '[', value: 'test', suffix: ']'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
import { useEffect, useState } from "react";
function Component(props) {
const $ = _c(7);
const [displayValue, setDisplayValue] = useState("");
let t0;
let t1;
if ($[0] !== props.prefix || $[1] !== props.suffix || $[2] !== props.value) {
t0 = () => {
const computed = props.prefix + props.value + props.suffix;
setDisplayValue(computed);
};
t1 = [props.prefix, props.value, props.suffix];
$[0] = props.prefix;
$[1] = props.suffix;
$[2] = props.value;
$[3] = t0;
$[4] = t1;
} else {
t0 = $[3];
t1 = $[4];
}
useEffect(t0, t1);
let t2;
if ($[5] !== displayValue) {
t2 = <div>{displayValue}</div>;
$[5] = displayValue;
$[6] = t2;
} else {
t2 = $[6];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ prefix: "[", value: "test", suffix: "]" }],
};
```
### Eval output
(kind: ok) <div>[test]</div>

View File

@@ -1,18 +0,0 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component(props) {
const [displayValue, setDisplayValue] = useState('');
useEffect(() => {
const computed = props.prefix + props.value + props.suffix;
setDisplayValue(computed);
}, [props.prefix, props.value, props.suffix]);
return <div>{displayValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prefix: '[', value: 'test', suffix: ']'}],
};

View File

@@ -34,8 +34,4 @@ const configs = {
},
};
const rules = Object.fromEntries(
Object.entries(allRules).map(([name, {rule}]) => [name, rule]),
);
export {configs, rules, meta};
export {configs, allRules as rules, meta};

View File

@@ -21,8 +21,6 @@ import {Note} from './cjs/Note.js';
import {GenerateImage} from './GenerateImage.js';
import LargeContent from './LargeContent.js';
import {like, greet, increment} from './actions.js';
import {getServerState} from './ServerState.js';
@@ -235,11 +233,6 @@ export default async function App({prerender, noCache}) {
<Foo>{dedupedChild}</Foo>
<Bar>{Promise.resolve([dedupedChild])}</Bar>
<Navigate />
{prerender ? null : ( // TODO: prerender is broken for large content for some reason.
<React.Suspense fallback={null}>
<LargeContent />
</React.Suspense>
)}
</Container>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -366,7 +366,6 @@ type Response = {
_debugRootOwner?: null | ReactComponentInfo, // DEV-only
_debugRootStack?: null | Error, // DEV-only
_debugRootTask?: null | ConsoleTask, // DEV-only
_debugStartTime: number, // DEV-only
_debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
_debugChannel?: void | DebugChannel, // DEV-only
_blockedConsole?: null | SomeChunk<ConsoleEntry>, // DEV-only
@@ -823,7 +822,6 @@ type InitializationReference = {
key: string,
) => any,
path: Array<string>,
isDebug?: boolean, // DEV-only
};
type InitializationHandler = {
parent: null | InitializationHandler,
@@ -874,7 +872,6 @@ function initializeDebugChunk(
response,
initializeDebugInfo,
[''], // path
true,
);
break;
}
@@ -897,7 +894,6 @@ function initializeDebugChunk(
response,
initializeDebugInfo,
[''], // path
true,
);
break;
}
@@ -1411,6 +1407,8 @@ function fulfillReference(
const mappedValue = map(response, value, parentObject, key);
parentObject[key] = mappedValue;
transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
// If this is the root object for a model reference, where `handler.value`
// is a stale `null`, the resolved value can be used directly.
if (key === '' && handler.value === null) {
@@ -1429,27 +1427,19 @@ function fulfillReference(
const element: any = handler.value;
switch (key) {
case '3':
transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
element.props = mappedValue;
break;
case '4':
// This path doesn't call transferReferencedDebugInfo because this reference is to a debug chunk.
if (__DEV__) {
element._owner = mappedValue;
}
break;
case '5':
// This path doesn't call transferReferencedDebugInfo because this reference is to a debug chunk.
if (__DEV__) {
element._debugStack = mappedValue;
}
break;
default:
transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
break;
}
} else if (__DEV__ && !reference.isDebug) {
transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
}
handler.deps--;
@@ -1528,7 +1518,6 @@ function waitForReference<T>(
response: Response,
map: (response: Response, model: any, parentObject: Object, key: string) => T,
path: Array<string>,
isAwaitingDebugInfo: boolean, // DEV-only
): T {
if (
__DEV__ &&
@@ -1573,9 +1562,6 @@ function waitForReference<T>(
map,
path,
};
if (__DEV__) {
reference.isDebug = isAwaitingDebugInfo;
}
// Add "listener".
if (referencedChunk.value === null) {
@@ -1808,21 +1794,13 @@ function transferReferencedDebugInfo(
existingDebugInfo.push.apply(existingDebugInfo, referencedDebugInfo);
}
}
// We also add the debug info to the initializing chunk since the resolution of that promise is
// also blocked by the referenced debug info. By adding it to both we can track it even if the array/element
// We also add it to the initializing chunk since the resolution of that promise is
// also blocked by these. By adding it to both we can track it even if the array/element
// is extracted, or if the root is rendered as is.
if (parentChunk !== null) {
const parentDebugInfo = parentChunk._debugInfo;
for (let i = 0; i < referencedDebugInfo.length; ++i) {
const debugInfoEntry = referencedDebugInfo[i];
if (debugInfoEntry.name != null) {
(debugInfoEntry: ReactComponentInfo);
// We're not transferring Component info since we use Component info
// in Debug info to fill in gaps between Fibers for the parent stack.
} else {
parentDebugInfo.push(debugInfoEntry);
}
}
// $FlowFixMe[method-unbinding]
parentDebugInfo.push.apply(parentDebugInfo, referencedDebugInfo);
}
}
}
@@ -1879,7 +1857,6 @@ function getOutlinedModel<T>(
response,
map,
path.slice(i - 1),
false,
);
}
case HALTED: {
@@ -1925,27 +1902,11 @@ function getOutlinedModel<T>(
value = value[path[i]];
}
const chunkValue = map(response, value, parentObject, key);
if (
parentObject[0] === REACT_ELEMENT_TYPE &&
(key === '4' || key === '5')
) {
// If we're resolving the "owner" or "stack" slot of an Element array, we don't call
// transferReferencedDebugInfo because this reference is to a debug chunk.
} else {
transferReferencedDebugInfo(initializingChunk, chunk, chunkValue);
}
transferReferencedDebugInfo(initializingChunk, chunk, chunkValue);
return chunkValue;
case PENDING:
case BLOCKED:
return waitForReference(
chunk,
parentObject,
key,
response,
map,
path,
false,
);
return waitForReference(chunk, parentObject, key, response, map, path);
case HALTED: {
// Add a dependency that will never resolve.
// TODO: Mark downstreams as halted too.
@@ -2483,13 +2444,6 @@ function ResponseInstance(
'"use ' + rootEnv.toLowerCase() + '"',
);
}
if (enableAsyncDebugInfo) {
// Track the start of the fetch to the best of our knowledge.
// Note: createFromFetch allows this to be marked at the start of the fetch
// where as if you use createFromReadableStream from the body of the fetch
// then the start time is when the headers resolved.
this._debugStartTime = performance.now();
}
this._debugFindSourceMapURL = findSourceMapURL;
this._debugChannel = debugChannel;
this._blockedConsole = null;
@@ -2558,99 +2512,16 @@ export type StreamState = {
_rowTag: number, // 0 indicates that we're currently parsing the row ID
_rowLength: number, // remaining bytes in the row. 0 indicates that we're looking for a newline.
_buffer: Array<Uint8Array>, // chunks received so far as part of this row
_debugInfo: ReactIOInfo, // DEV-only
_debugTargetChunkSize: number, // DEV-only
};
export function createStreamState(
weakResponse: WeakResponse, // DEV-only
streamDebugValue: mixed, // DEV-only
): StreamState {
const streamState: StreamState = (({
export function createStreamState(): StreamState {
return {
_rowState: 0,
_rowID: 0,
_rowTag: 0,
_rowLength: 0,
_buffer: [],
}: Omit<StreamState, '_debugInfo' | '_debugTargetChunkSize'>): any);
if (__DEV__ && enableAsyncDebugInfo) {
const response = unwrapWeakResponse(weakResponse);
// Create an entry for the I/O to load the stream itself.
const debugValuePromise = Promise.resolve(streamDebugValue);
(debugValuePromise: any).status = 'fulfilled';
(debugValuePromise: any).value = streamDebugValue;
streamState._debugInfo = {
name: 'RSC stream',
start: response._debugStartTime,
end: response._debugStartTime, // will be updated once we finish a chunk
byteSize: 0, // will be updated as we resolve a data chunk
value: debugValuePromise,
owner: response._debugRootOwner,
debugStack: response._debugRootStack,
debugTask: response._debugRootTask,
};
streamState._debugTargetChunkSize = MIN_CHUNK_SIZE;
}
return streamState;
}
// Depending on set up the chunks of a TLS connection can vary in size. However in practice it's often
// at 64kb or even multiples of 64kb. It can also be smaller but in practice it also happens that 64kb
// is around what you can download on fast 4G connection in 300ms which is what we throttle reveals at
// anyway. The net effect is that in practice, you won't really reveal anything in smaller units than
// 64kb if they're revealing at maximum speed in production. Therefore we group smaller chunks into
// these larger chunks since in production that's more realistic.
// TODO: If the stream is compressed, then you could fit much more in a single 300ms so maybe it should
// actually be larger.
const MIN_CHUNK_SIZE = 65536;
function incrementChunkDebugInfo(
streamState: StreamState,
chunkLength: number,
): void {
if (__DEV__ && enableAsyncDebugInfo) {
const debugInfo: ReactIOInfo = streamState._debugInfo;
const endTime = performance.now();
const previousEndTime = debugInfo.end;
const newByteLength = ((debugInfo.byteSize: any): number) + chunkLength;
if (
newByteLength > streamState._debugTargetChunkSize ||
endTime > previousEndTime + 10
) {
// This new chunk would overshoot the chunk size so therefore we treat it as its own new chunk
// by cloning the old one. Similarly, if some time has passed we assume that it was actually
// due to the server being unable to flush chunks faster e.g. due to I/O so it would be a
// new chunk in production even if the buffer hasn't been reached.
streamState._debugInfo = {
name: debugInfo.name,
start: debugInfo.start,
end: endTime,
byteSize: newByteLength,
value: debugInfo.value,
owner: debugInfo.owner,
debugStack: debugInfo.debugStack,
debugTask: debugInfo.debugTask,
};
streamState._debugTargetChunkSize = newByteLength + MIN_CHUNK_SIZE;
} else {
// Otherwise we reuse the old chunk but update the end time and byteSize to the latest.
// $FlowFixMe[cannot-write]
debugInfo.end = endTime;
// $FlowFixMe[cannot-write]
debugInfo.byteSize = newByteLength;
}
}
}
function resolveChunkDebugInfo(
streamState: StreamState,
chunk: SomeChunk<any>,
): void {
if (__DEV__ && enableAsyncDebugInfo) {
// Push the currently resolving chunk's debug info representing the stream on the Promise
// that was waiting on the stream.
chunk._debugInfo.push({awaited: streamState._debugInfo});
}
};
}
function resolveDebugHalt(response: Response, id: number): void {
@@ -2674,33 +2545,17 @@ function resolveModel(
response: Response,
id: number,
model: UninitializedModel,
streamState: StreamState,
): void {
const chunks = response._chunks;
const chunk = chunks.get(id);
if (!chunk) {
const newChunk: ResolvedModelChunk<any> = createResolvedModelChunk(
response,
model,
);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
}
chunks.set(id, newChunk);
chunks.set(id, createResolvedModelChunk(response, model));
} else {
if (__DEV__) {
resolveChunkDebugInfo(streamState, chunk);
}
resolveModelChunk(response, chunk, model);
}
}
function resolveText(
response: Response,
id: number,
text: string,
streamState: StreamState,
): void {
function resolveText(response: Response, id: number, text: string): void {
const chunks = response._chunks;
const chunk = chunks.get(id);
if (chunk && chunk.status !== PENDING) {
@@ -2714,18 +2569,13 @@ function resolveText(
if (chunk) {
releasePendingChunk(response, chunk);
}
const newChunk = createInitializedTextChunk(response, text);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
}
chunks.set(id, newChunk);
chunks.set(id, createInitializedTextChunk(response, text));
}
function resolveBuffer(
response: Response,
id: number,
buffer: $ArrayBufferView | ArrayBuffer,
streamState: StreamState,
): void {
const chunks = response._chunks;
const chunk = chunks.get(id);
@@ -2740,18 +2590,13 @@ function resolveBuffer(
if (chunk) {
releasePendingChunk(response, chunk);
}
const newChunk = createInitializedBufferChunk(response, buffer);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
}
chunks.set(id, newChunk);
chunks.set(id, createInitializedBufferChunk(response, buffer));
}
function resolveModule(
response: Response,
id: number,
model: UninitializedModel,
streamState: StreamState,
): void {
const chunks = response._chunks;
const chunk = chunks.get(id);
@@ -2788,24 +2633,14 @@ function resolveModule(
blockedChunk = (chunk: any);
blockedChunk.status = BLOCKED;
}
if (__DEV__) {
resolveChunkDebugInfo(streamState, blockedChunk);
}
promise.then(
() => resolveModuleChunk(response, blockedChunk, clientReference),
error => triggerErrorOnChunk(response, blockedChunk, error),
);
} else {
if (!chunk) {
const newChunk = createResolvedModuleChunk(response, clientReference);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
}
chunks.set(id, newChunk);
chunks.set(id, createResolvedModuleChunk(response, clientReference));
} else {
if (__DEV__) {
resolveChunkDebugInfo(streamState, chunk);
}
// This can't actually happen because we don't have any forward
// references to modules.
resolveModuleChunk(response, chunk, clientReference);
@@ -2818,21 +2653,13 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
id: number,
stream: T,
controller: FlightStreamController,
streamState: StreamState,
): void {
const chunks = response._chunks;
const chunk = chunks.get(id);
if (!chunk) {
const newChunk = createInitializedStreamChunk(response, stream, controller);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
}
chunks.set(id, newChunk);
chunks.set(id, createInitializedStreamChunk(response, stream, controller));
return;
}
if (__DEV__) {
resolveChunkDebugInfo(streamState, chunk);
}
if (chunk.status !== PENDING) {
// We already resolved. We didn't expect to see this.
return;
@@ -2888,7 +2715,6 @@ function startReadableStream<T>(
response: Response,
id: number,
type: void | 'bytes',
streamState: StreamState,
): void {
let controller: ReadableStreamController = (null: any);
const stream = new ReadableStream({
@@ -2969,7 +2795,7 @@ function startReadableStream<T>(
}
},
};
resolveStream(response, id, stream, flightController, streamState);
resolveStream(response, id, stream, flightController);
}
function asyncIterator(this: $AsyncIterator<any, any, void>) {
@@ -2995,7 +2821,6 @@ function startAsyncIterable<T>(
response: Response,
id: number,
iterator: boolean,
streamState: StreamState,
): void {
const buffer: Array<SomeChunk<IteratorResult<T, T>>> = [];
let closed = false;
@@ -3113,7 +2938,6 @@ function startAsyncIterable<T>(
id,
iterator ? iterable[ASYNC_ITERATOR]() : iterable,
flightController,
streamState,
);
}
@@ -3193,11 +3017,7 @@ function resolveErrorDev(
return error;
}
function resolvePostponeProd(
response: Response,
id: number,
streamState: StreamState,
): void {
function resolvePostponeProd(response: Response, id: number): void {
if (__DEV__) {
// These errors should never make it into a build so we don't need to encode them in codes.json
// eslint-disable-next-line react-internal/prod-error-codes
@@ -3215,11 +3035,7 @@ function resolvePostponeProd(
const chunks = response._chunks;
const chunk = chunks.get(id);
if (!chunk) {
const newChunk: ErroredChunk<any> = createErrorChunk(
response,
postponeInstance,
);
chunks.set(id, newChunk);
chunks.set(id, createErrorChunk(response, postponeInstance));
} else {
triggerErrorOnChunk(response, chunk, postponeInstance);
}
@@ -3231,7 +3047,6 @@ function resolvePostponeDev(
reason: string,
stack: ReactStackTrace,
env: string,
streamState: StreamState,
): void {
if (!__DEV__) {
// These errors should never make it into a build so we don't need to encode them in codes.json
@@ -3259,18 +3074,8 @@ function resolvePostponeDev(
const chunks = response._chunks;
const chunk = chunks.get(id);
if (!chunk) {
const newChunk: ErroredChunk<any> = createErrorChunk(
response,
postponeInstance,
);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
}
chunks.set(id, newChunk);
chunks.set(id, createErrorChunk(response, postponeInstance));
} else {
if (__DEV__) {
resolveChunkDebugInfo(streamState, chunk);
}
triggerErrorOnChunk(response, chunk, postponeInstance);
}
}
@@ -3279,7 +3084,6 @@ function resolveErrorModel(
response: Response,
id: number,
row: UninitializedModel,
streamState: StreamState,
): void {
const chunks = response._chunks;
const chunk = chunks.get(id);
@@ -3293,18 +3097,8 @@ function resolveErrorModel(
(error: any).digest = errorInfo.digest;
const errorWithDigest: ErrorWithDigest = (error: any);
if (!chunk) {
const newChunk: ErroredChunk<any> = createErrorChunk(
response,
errorWithDigest,
);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
}
chunks.set(id, newChunk);
chunks.set(id, createErrorChunk(response, errorWithDigest));
} else {
if (__DEV__) {
resolveChunkDebugInfo(streamState, chunk);
}
triggerErrorOnChunk(response, chunk, errorWithDigest);
}
}
@@ -4039,7 +3833,6 @@ function resolveTypedArray(
lastChunk: Uint8Array,
constructor: any,
bytesPerElement: number,
streamState: StreamState,
): void {
// If the view fits into one original buffer, we just reuse that buffer instead of
// copying it out to a separate copy. This means that it's not always possible to
@@ -4059,7 +3852,7 @@ function resolveTypedArray(
chunk.byteOffset,
chunk.byteLength / bytesPerElement,
);
resolveBuffer(response, id, view, streamState);
resolveBuffer(response, id, view);
}
function logComponentInfo(
@@ -4376,7 +4169,6 @@ function flushInitialRenderPerformance(response: Response): void {
function processFullBinaryRow(
response: Response,
streamState: StreamState,
id: number,
tag: number,
buffer: Array<Uint8Array>,
@@ -4385,125 +4177,47 @@ function processFullBinaryRow(
switch (tag) {
case 65 /* "A" */:
// We must always clone to extract it into a separate buffer instead of just a view.
resolveBuffer(
response,
id,
mergeBuffer(buffer, chunk).buffer,
streamState,
);
resolveBuffer(response, id, mergeBuffer(buffer, chunk).buffer);
return;
case 79 /* "O" */:
resolveTypedArray(response, id, buffer, chunk, Int8Array, 1, streamState);
resolveTypedArray(response, id, buffer, chunk, Int8Array, 1);
return;
case 111 /* "o" */:
resolveBuffer(
response,
id,
buffer.length === 0 ? chunk : mergeBuffer(buffer, chunk),
streamState,
);
return;
case 85 /* "U" */:
resolveTypedArray(
response,
id,
buffer,
chunk,
Uint8ClampedArray,
1,
streamState,
);
resolveTypedArray(response, id, buffer, chunk, Uint8ClampedArray, 1);
return;
case 83 /* "S" */:
resolveTypedArray(
response,
id,
buffer,
chunk,
Int16Array,
2,
streamState,
);
resolveTypedArray(response, id, buffer, chunk, Int16Array, 2);
return;
case 115 /* "s" */:
resolveTypedArray(
response,
id,
buffer,
chunk,
Uint16Array,
2,
streamState,
);
resolveTypedArray(response, id, buffer, chunk, Uint16Array, 2);
return;
case 76 /* "L" */:
resolveTypedArray(
response,
id,
buffer,
chunk,
Int32Array,
4,
streamState,
);
resolveTypedArray(response, id, buffer, chunk, Int32Array, 4);
return;
case 108 /* "l" */:
resolveTypedArray(
response,
id,
buffer,
chunk,
Uint32Array,
4,
streamState,
);
resolveTypedArray(response, id, buffer, chunk, Uint32Array, 4);
return;
case 71 /* "G" */:
resolveTypedArray(
response,
id,
buffer,
chunk,
Float32Array,
4,
streamState,
);
resolveTypedArray(response, id, buffer, chunk, Float32Array, 4);
return;
case 103 /* "g" */:
resolveTypedArray(
response,
id,
buffer,
chunk,
Float64Array,
8,
streamState,
);
resolveTypedArray(response, id, buffer, chunk, Float64Array, 8);
return;
case 77 /* "M" */:
resolveTypedArray(
response,
id,
buffer,
chunk,
BigInt64Array,
8,
streamState,
);
resolveTypedArray(response, id, buffer, chunk, BigInt64Array, 8);
return;
case 109 /* "m" */:
resolveTypedArray(
response,
id,
buffer,
chunk,
BigUint64Array,
8,
streamState,
);
resolveTypedArray(response, id, buffer, chunk, BigUint64Array, 8);
return;
case 86 /* "V" */:
resolveTypedArray(response, id, buffer, chunk, DataView, 1, streamState);
resolveTypedArray(response, id, buffer, chunk, DataView, 1);
return;
}
@@ -4513,19 +4227,18 @@ function processFullBinaryRow(
row += readPartialStringChunk(stringDecoder, buffer[i]);
}
row += readFinalStringChunk(stringDecoder, chunk);
processFullStringRow(response, streamState, id, tag, row);
processFullStringRow(response, id, tag, row);
}
function processFullStringRow(
response: Response,
streamState: StreamState,
id: number,
tag: number,
row: string,
): void {
switch (tag) {
case 73 /* "I" */: {
resolveModule(response, id, row, streamState);
resolveModule(response, id, row);
return;
}
case 72 /* "H" */: {
@@ -4534,11 +4247,11 @@ function processFullStringRow(
return;
}
case 69 /* "E" */: {
resolveErrorModel(response, id, row, streamState);
resolveErrorModel(response, id, row);
return;
}
case 84 /* "T" */: {
resolveText(response, id, row, streamState);
resolveText(response, id, row);
return;
}
case 78 /* "N" */: {
@@ -4583,22 +4296,22 @@ function processFullStringRow(
);
}
case 82 /* "R" */: {
startReadableStream(response, id, undefined, streamState);
startReadableStream(response, id, undefined);
return;
}
// Fallthrough
case 114 /* "r" */: {
startReadableStream(response, id, 'bytes', streamState);
startReadableStream(response, id, 'bytes');
return;
}
// Fallthrough
case 88 /* "X" */: {
startAsyncIterable(response, id, false, streamState);
startAsyncIterable(response, id, false);
return;
}
// Fallthrough
case 120 /* "x" */: {
startAsyncIterable(response, id, true, streamState);
startAsyncIterable(response, id, true);
return;
}
// Fallthrough
@@ -4617,10 +4330,9 @@ function processFullStringRow(
postponeInfo.reason,
postponeInfo.stack,
postponeInfo.env,
streamState,
);
} else {
resolvePostponeProd(response, id, streamState);
resolvePostponeProd(response, id);
}
return;
}
@@ -4632,7 +4344,7 @@ function processFullStringRow(
return;
}
// We assume anything else is JSON.
resolveModel(response, id, row, streamState);
resolveModel(response, id, row);
return;
}
}
@@ -4655,7 +4367,6 @@ export function processBinaryChunk(
let rowLength = streamState._rowLength;
const buffer = streamState._buffer;
const chunkLength = chunk.length;
incrementChunkDebugInfo(streamState, chunkLength);
while (i < chunkLength) {
let lastIdx = -1;
switch (rowState) {
@@ -4735,14 +4446,7 @@ export function processBinaryChunk(
// We found the last chunk of the row
const length = lastIdx - i;
const lastChunk = new Uint8Array(chunk.buffer, offset, length);
processFullBinaryRow(
response,
streamState,
rowID,
rowTag,
buffer,
lastChunk,
);
processFullBinaryRow(response, rowID, rowTag, buffer, lastChunk);
// Reset state machine for a new row
i = lastIdx;
if (rowState === ROW_CHUNK_BY_NEWLINE) {
@@ -4797,7 +4501,6 @@ export function processStringChunk(
let rowLength = streamState._rowLength;
const buffer = streamState._buffer;
const chunkLength = chunk.length;
incrementChunkDebugInfo(streamState, chunkLength);
while (i < chunkLength) {
let lastIdx = -1;
switch (rowState) {
@@ -4896,7 +4599,7 @@ export function processStringChunk(
);
}
const lastChunk = chunk.slice(i, lastIdx);
processFullStringRow(response, streamState, rowID, rowTag, lastChunk);
processFullStringRow(response, rowID, rowTag, lastChunk);
// Reset state machine for a new row
i = lastIdx;
if (rowState === ROW_CHUNK_BY_NEWLINE) {

View File

@@ -85,11 +85,7 @@ function getDebugInfo(obj) {
if (debugInfo) {
const copy = [];
for (let i = 0; i < debugInfo.length; i++) {
if (debugInfo[i].awaited && debugInfo[i].awaited.name === 'RSC stream') {
// Ignore RSC stream I/O info.
} else {
copy.push(normalizeComponentInfo(debugInfo[i]));
}
copy.push(normalizeComponentInfo(debugInfo[i]));
}
return copy;
}
@@ -2836,7 +2832,7 @@ describe('ReactFlight', () => {
transport: expect.arrayContaining([]),
},
},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 53 : 21},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 23 : 21},
]
: undefined,
);
@@ -2847,7 +2843,7 @@ describe('ReactFlight', () => {
expect(getDebugInfo(thirdPartyChildren[0])).toEqual(
__DEV__
? [
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22}, // Clamped to the start
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, // Clamped to the start
{
name: 'ThirdPartyComponent',
env: 'third-party',
@@ -2855,15 +2851,15 @@ describe('ReactFlight', () => {
stack: ' in Object.<anonymous> (at **)',
props: {},
},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 55 : 23}, // This last one is when the promise resolved into the first party.
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 25 : 23}, // This last one is when the promise resolved into the first party.
]
: undefined,
);
expect(getDebugInfo(thirdPartyChildren[1])).toEqual(
__DEV__
? [
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22}, // Clamped to the start
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, // Clamped to the start
{
name: 'ThirdPartyLazyComponent',
env: 'third-party',
@@ -2871,14 +2867,14 @@ describe('ReactFlight', () => {
stack: ' in myLazy (at **)\n in lazyInitializer (at **)',
props: {},
},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
]
: undefined,
);
expect(getDebugInfo(thirdPartyChildren[2])).toEqual(
__DEV__
? [
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
{
name: 'ThirdPartyFragmentComponent',
env: 'third-party',
@@ -2886,7 +2882,7 @@ describe('ReactFlight', () => {
stack: ' in Object.<anonymous> (at **)',
props: {},
},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
]
: undefined,
);
@@ -2964,10 +2960,17 @@ describe('ReactFlight', () => {
{
time: 16,
},
{
env: 'third-party',
key: null,
name: 'ThirdPartyAsyncIterableComponent',
props: {},
stack: ' in Object.<anonymous> (at **)',
},
{
time: 16,
},
{time: 31},
{time: 17},
]
: undefined,
);
@@ -2976,7 +2979,7 @@ describe('ReactFlight', () => {
expect(getDebugInfo(thirdPartyFragment)).toEqual(
__DEV__
? [
{time: 32},
{time: 18},
{
name: 'Keyed',
env: 'Server',
@@ -2987,14 +2990,19 @@ describe('ReactFlight', () => {
},
},
{
time: 33,
time: 19,
},
{
time: 33,
time: 19,
},
{
time: 33,
env: 'third-party',
key: null,
name: 'ThirdPartyAsyncIterableComponent',
props: {},
stack: ' in Object.<anonymous> (at **)',
},
{time: 19},
]
: undefined,
);
@@ -3002,7 +3010,7 @@ describe('ReactFlight', () => {
expect(getDebugInfo(thirdPartyFragment.props.children)).toEqual(
__DEV__
? [
{time: 33}, // Clamp to the start
{time: 19}, // Clamp to the start
{
name: 'ThirdPartyAsyncIterableComponent',
env: 'third-party',
@@ -3010,7 +3018,7 @@ describe('ReactFlight', () => {
stack: ' in Object.<anonymous> (at **)',
props: {},
},
{time: 33},
{time: 19},
]
: undefined,
);
@@ -3073,7 +3081,7 @@ describe('ReactFlight', () => {
props: {},
},
{time: 16},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 17},
{time: 17},
]
: undefined,
);
@@ -3839,115 +3847,4 @@ describe('ReactFlight', () => {
expect(ReactNoop).toMatchRenderedOutput(<div>not using props</div>);
});
// @gate !__DEV__ || enableComponentPerformanceTrack
it('produces correct parent stacks', async () => {
function Container() {
return ReactServer.createElement('div', null);
}
function ContainerParent() {
return ReactServer.createElement(Container, null);
}
function App() {
return ReactServer.createElement(
'main',
null,
ReactServer.createElement(ContainerParent, null),
);
}
const transport = ReactNoopFlightServer.render({
root: ReactServer.createElement(App, null),
});
await act(async () => {
const {root} = await ReactNoopFlightClient.read(transport);
ReactNoop.render(root);
expect(root.type).toBe('main');
if (__DEV__) {
const div = root.props.children;
expect(getDebugInfo(div)).toEqual([
{
time: 14,
},
{
env: 'Server',
key: null,
name: 'ContainerParent',
owner: {
env: 'Server',
key: null,
name: 'App',
props: {},
stack: ' in Object.<anonymous> (at **)',
},
props: {},
stack: ' in App (at **)',
},
{
time: 15,
},
{
env: 'Server',
key: null,
name: 'Container',
owner: {
env: 'Server',
key: null,
name: 'ContainerParent',
owner: {
env: 'Server',
key: null,
name: 'App',
props: {},
stack: ' in Object.<anonymous> (at **)',
},
props: {},
stack: ' in App (at **)',
},
props: {},
stack: ' in ContainerParent (at **)',
},
{
time: 16,
},
]);
expect(getDebugInfo(root)).toEqual([
{
time: 12,
},
{
env: 'Server',
key: null,
name: 'App',
props: {},
stack: ' in Object.<anonymous> (at **)',
},
{
time: 13,
},
{
time: 14,
},
{
time: 15,
},
{
time: 16,
},
]);
} else {
expect(root._debugInfo).toBe(undefined);
expect(root._owner).toBe(undefined);
}
});
expect(ReactNoop).toMatchRenderedOutput(
<main>
<div />
</main>,
);
});
});

View File

@@ -81,18 +81,7 @@ export function experimental_renderToHTML(
options?: MarkupOptions,
): Promise<string> {
return new Promise((resolve, reject) => {
const flightResponse = createFlightResponse(
null,
null,
null,
noServerCallOrFormAction,
noServerCallOrFormAction,
undefined,
undefined,
undefined,
false,
);
const streamState = createFlightStreamState(flightResponse, null);
const streamState = createFlightStreamState();
const flightDestination = {
push(chunk: string | null): boolean {
if (chunk !== null) {
@@ -186,6 +175,17 @@ export function experimental_renderToHTML(
undefined,
false,
);
const flightResponse = createFlightResponse(
null,
null,
null,
noServerCallOrFormAction,
noServerCallOrFormAction,
undefined,
undefined,
undefined,
false,
);
const resumableState = createResumableState(
options ? options.identifierPrefix : undefined,
undefined,

View File

@@ -77,7 +77,7 @@ function read<T>(source: Source, options: ReadOptions): Thenable<T> {
? options.debugChannel.onMessage
: undefined,
);
const streamState = createStreamState(response, source);
const streamState = createStreamState();
for (let i = 0; i < source.length; i++) {
processBinaryChunk(response, streamState, source[i], 0);
}

View File

@@ -115,7 +115,7 @@ function startReadingFromUniversalStream(
// This is the same as startReadingFromStream except this allows WebSocketStreams which
// return ArrayBuffer and string chunks instead of Uint8Array chunks. We could potentially
// always allow streams with variable chunk types.
const streamState = createStreamState(response, stream);
const streamState = createStreamState();
const reader = stream.getReader();
function progress({
done,
@@ -149,9 +149,8 @@ function startReadingFromStream(
response: FlightResponse,
stream: ReadableStream,
onDone: () => void,
debugValue: mixed,
): void {
const streamState = createStreamState(response, debugValue);
const streamState = createStreamState();
const reader = stream.getReader();
function progress({
done,
@@ -195,14 +194,9 @@ function createFromReadableStream<T>(
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, stream, handleDone, stream);
startReadingFromStream(response, stream, handleDone);
} else {
startReadingFromStream(
response,
stream,
close.bind(null, response),
stream,
);
startReadingFromStream(response, stream, close.bind(null, response));
}
return getRoot(response);
}
@@ -231,13 +225,12 @@ function createFromFetch<T>(
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, (r.body: any), handleDone, r);
startReadingFromStream(response, (r.body: any), handleDone);
} else {
startReadingFromStream(
response,
(r.body: any),
close.bind(null, response),
r,
);
}
},

View File

@@ -66,7 +66,7 @@ function startReadingFromStream(
stream: Readable,
onEnd: () => void,
): void {
const streamState = createStreamState(response, stream);
const streamState = createStreamState();
stream.on('data', chunk => {
if (typeof chunk === 'string') {

View File

@@ -141,7 +141,7 @@ function startReadingFromUniversalStream(
// This is the same as startReadingFromStream except this allows WebSocketStreams which
// return ArrayBuffer and string chunks instead of Uint8Array chunks. We could potentially
// always allow streams with variable chunk types.
const streamState = createStreamState(response, stream);
const streamState = createStreamState();
const reader = stream.getReader();
function progress({
done,
@@ -175,9 +175,8 @@ function startReadingFromStream(
response: FlightResponse,
stream: ReadableStream,
onDone: () => void,
debugValue: mixed,
): void {
const streamState = createStreamState(response, debugValue);
const streamState = createStreamState();
const reader = stream.getReader();
function progress({
done,
@@ -229,14 +228,9 @@ export function createFromReadableStream<T>(
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, stream, handleDone, stream);
startReadingFromStream(response, stream, handleDone);
} else {
startReadingFromStream(
response,
stream,
close.bind(null, response),
stream,
);
startReadingFromStream(response, stream, close.bind(null, response));
}
return getRoot(response);
}
@@ -265,13 +259,12 @@ export function createFromFetch<T>(
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, (r.body: any), handleDone, r);
startReadingFromStream(response, (r.body: any), handleDone);
} else {
startReadingFromStream(
response,
(r.body: any),
close.bind(null, response),
r,
);
}
},

View File

@@ -115,9 +115,8 @@ function startReadingFromStream(
response: FlightResponse,
stream: ReadableStream,
onDone: () => void,
debugValue: mixed,
): void {
const streamState = createStreamState(response, debugValue);
const streamState = createStreamState();
const reader = stream.getReader();
function progress({
done,
@@ -159,14 +158,9 @@ export function createFromReadableStream<T>(
}
};
startReadingFromStream(response, options.debugChannel.readable, handleDone);
startReadingFromStream(response, stream, handleDone, stream);
startReadingFromStream(response, stream, handleDone);
} else {
startReadingFromStream(
response,
stream,
close.bind(null, response),
stream,
);
startReadingFromStream(response, stream, close.bind(null, response));
}
return getRoot(response);
@@ -196,13 +190,12 @@ export function createFromFetch<T>(
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, (r.body: any), handleDone, r);
startReadingFromStream(response, (r.body: any), handleDone);
} else {
startReadingFromStream(
response,
(r.body: any),
close.bind(null, response),
r,
);
}
},

View File

@@ -61,7 +61,7 @@ function startReadingFromStream(
stream: Readable,
onEnd: () => void,
): void {
const streamState = createStreamState(response, stream);
const streamState = createStreamState();
stream.on('data', chunk => {
if (typeof chunk === 'string') {

View File

@@ -114,7 +114,7 @@ function startReadingFromUniversalStream(
// This is the same as startReadingFromStream except this allows WebSocketStreams which
// return ArrayBuffer and string chunks instead of Uint8Array chunks. We could potentially
// always allow streams with variable chunk types.
const streamState = createStreamState(response, stream);
const streamState = createStreamState();
const reader = stream.getReader();
function progress({
done,
@@ -148,9 +148,8 @@ function startReadingFromStream(
response: FlightResponse,
stream: ReadableStream,
onDone: () => void,
debugValue: mixed,
): void {
const streamState = createStreamState(response, debugValue);
const streamState = createStreamState();
const reader = stream.getReader();
function progress({
done,
@@ -195,14 +194,9 @@ function createFromReadableStream<T>(
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, stream, handleDone, stream);
startReadingFromStream(response, stream, handleDone);
} else {
startReadingFromStream(
response,
stream,
close.bind(null, response),
stream,
);
startReadingFromStream(response, stream, close.bind(null, response));
}
return getRoot(response);
}
@@ -231,13 +225,12 @@ function createFromFetch<T>(
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, (r.body: any), handleDone, r);
startReadingFromStream(response, (r.body: any), handleDone);
} else {
startReadingFromStream(
response,
(r.body: any),
close.bind(null, response),
r,
);
}
},

View File

@@ -117,9 +117,8 @@ function startReadingFromStream(
response: FlightResponse,
stream: ReadableStream,
onDone: () => void,
debugValue: mixed,
): void {
const streamState = createStreamState(response, debugValue);
const streamState = createStreamState();
const reader = stream.getReader();
function progress({
done,
@@ -161,14 +160,9 @@ function createFromReadableStream<T>(
}
};
startReadingFromStream(response, options.debugChannel.readable, handleDone);
startReadingFromStream(response, stream, handleDone, stream);
startReadingFromStream(response, stream, handleDone);
} else {
startReadingFromStream(
response,
stream,
close.bind(null, response),
stream,
);
startReadingFromStream(response, stream, close.bind(null, response));
}
return getRoot(response);
@@ -198,13 +192,12 @@ function createFromFetch<T>(
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, (r.body: any), handleDone, r);
startReadingFromStream(response, (r.body: any), handleDone);
} else {
startReadingFromStream(
response,
(r.body: any),
close.bind(null, response),
r,
);
}
},

View File

@@ -69,7 +69,7 @@ function startReadingFromStream(
stream: Readable,
onEnd: () => void,
): void {
const streamState = createStreamState(response, stream);
const streamState = createStreamState();
stream.on('data', chunk => {
if (typeof chunk === 'string') {

View File

@@ -1239,24 +1239,17 @@ describe('ReactFlightDOMEdge', () => {
name: 'Greeting',
env: 'Server',
});
if (gate(flags => flags.enableAsyncDebugInfo)) {
expect(lazyWrapper._debugInfo).toEqual([
{time: 12},
greetInfo,
{time: 13},
expect.objectContaining({
name: 'Container',
env: 'Server',
owner: greetInfo,
}),
{time: 14},
expect.objectContaining({
awaited: expect.objectContaining({
name: 'RSC stream',
}),
}),
]);
}
expect(lazyWrapper._debugInfo).toEqual([
{time: 12},
greetInfo,
{time: 13},
expect.objectContaining({
name: 'Container',
env: 'Server',
owner: greetInfo,
}),
{time: 14},
]);
// The owner that created the span was the outer server component.
// We expect the debug info to be referentially equal to the owner.
expect(greeting._owner).toBe(lazyWrapper._debugInfo[1]);

View File

@@ -114,7 +114,7 @@ function startReadingFromUniversalStream(
// This is the same as startReadingFromStream except this allows WebSocketStreams which
// return ArrayBuffer and string chunks instead of Uint8Array chunks. We could potentially
// always allow streams with variable chunk types.
const streamState = createStreamState(response, stream);
const streamState = createStreamState();
const reader = stream.getReader();
function progress({
done,
@@ -148,9 +148,8 @@ function startReadingFromStream(
response: FlightResponse,
stream: ReadableStream,
onDone: () => void,
debugValue: mixed,
): void {
const streamState = createStreamState(response, debugValue);
const streamState = createStreamState();
const reader = stream.getReader();
function progress({
done,
@@ -195,14 +194,9 @@ function createFromReadableStream<T>(
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, stream, handleDone, stream);
startReadingFromStream(response, stream, handleDone);
} else {
startReadingFromStream(
response,
stream,
close.bind(null, response),
stream,
);
startReadingFromStream(response, stream, close.bind(null, response));
}
return getRoot(response);
}
@@ -231,13 +225,12 @@ function createFromFetch<T>(
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, (r.body: any), handleDone, r);
startReadingFromStream(response, (r.body: any), handleDone);
} else {
startReadingFromStream(
response,
(r.body: any),
close.bind(null, response),
r,
);
}
},

View File

@@ -117,9 +117,8 @@ function startReadingFromStream(
response: FlightResponse,
stream: ReadableStream,
onDone: () => void,
debugValue: mixed,
): void {
const streamState = createStreamState(response, debugValue);
const streamState = createStreamState();
const reader = stream.getReader();
function progress({
done,
@@ -161,14 +160,9 @@ function createFromReadableStream<T>(
}
};
startReadingFromStream(response, options.debugChannel.readable, handleDone);
startReadingFromStream(response, stream, handleDone, stream);
startReadingFromStream(response, stream, handleDone);
} else {
startReadingFromStream(
response,
stream,
close.bind(null, response),
stream,
);
startReadingFromStream(response, stream, close.bind(null, response));
}
return getRoot(response);
@@ -198,13 +192,12 @@ function createFromFetch<T>(
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, (r.body: any), handleDone, r);
startReadingFromStream(response, (r.body: any), handleDone);
} else {
startReadingFromStream(
response,
(r.body: any),
close.bind(null, response),
r,
);
}
},

View File

@@ -69,7 +69,7 @@ function startReadingFromStream(
stream: Readable,
onEnd: () => void,
): void {
const streamState = createStreamState(response, stream);
const streamState = createStreamState();
stream.on('data', chunk => {
if (typeof chunk === 'string') {

File diff suppressed because it is too large Load Diff