Compare commits

..

1 Commits

Author SHA1 Message Date
Joe Savona
5aa10ca5ca [compiler] Fix false positive memo validation (alternative)
Alternative to #34276

---
(Summary taken from @josephsavona 's #34276)
Partial fix for #34262. Consider this example:

```js
function useInputValue(input) {
  const object = React.useMemo(() => {
    const {value} = transform(input);
    return {value};
  }, [input]);
  return object;
}
```

React Compiler breaks this code into two reactive scopes:
* One for `transform(input)`
* One for `{value}`

When we run ValidatePreserveExistingMemo, we see that the scope for `{value}` has the dependency `value`, whereas the original memoization had the dependency `input`, and throw an error that the dependencies didn't match.

In other words, we're flagging the fact that memoized _better than the user_ as a problem. The more complete solution would be to validate that there is a subgraph of reactive scopes with a single input and output node, where the input node has the same dependencies as the original useMemo, and the output has the same outputs. That is true in this case, with the subgraph being the two consecutive scopes mentioned above.

But that's complicated. As a shortcut, this PR checks for any dependencies that are defined after the start of the original useMemo. If we find one, we know that it's a case where we were able to memoize more precisely than the original, and we don't report an error on the dependency. We still check that the original _output_ value is able to be memoized, though. So if the scope of `object` were extended, eg with a call to `mutate(object)`, then we'd still correctly report an error that we couldn't preserve memoization.
2025-09-09 13:01:11 -04:00
62 changed files with 634 additions and 4026 deletions

View File

@@ -103,7 +103,6 @@ import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoF
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -325,15 +324,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});

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

@@ -261,8 +261,6 @@ export const EnvironmentConfigSchema = z.object({
enableFire: z.boolean().default(false),
enableNameAnonymousFunctions: z.boolean().default(false),
/**
* Enables inference and auto-insertion of effect dependencies. Takes in an array of
* configurable module and import pairs to allow for user-land experimentation. For example,

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';
@@ -19,7 +15,6 @@ import {Type, makeType} from './Types';
import {z} from 'zod';
import type {AliasingEffect} from '../Inference/AliasingEffects';
import {isReservedWord} from '../Utils/Keyword';
import {Err, Ok, Result} from '../Utils/Result';
/*
* *******************************************************************************************
@@ -58,8 +53,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 +275,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 +1123,7 @@ export type JsxAttribute =
export type FunctionExpression = {
kind: 'FunctionExpression';
name: ValidIdentifierName | null;
nameHint: string | null;
name: string | null;
loweredFunc: LoweredFunction;
type:
| 'ArrowFunctionExpression'
@@ -1306,52 +1298,37 @@ 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',
}),
);
}
return Ok({
kind: 'named',
value: name as ValidIdentifierName,
});
}
/**
* Creates a valid identifier name. This should *not* be used for synthesizing
* identifier names: only call this method for identifier names that appear in the
* 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

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

@@ -61,7 +61,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 +383,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,
@@ -2328,7 +2326,6 @@ function codegenInstructionValue(
),
reactiveFunction,
).unwrap();
if (instrValue.type === 'ArrowFunctionExpression') {
let body: t.BlockStatement | t.Expression = fn.body;
if (body.body.length === 1 && loweredFunc.directives.length == 0) {
@@ -2340,26 +2337,14 @@ function codegenInstructionValue(
value = t.arrowFunctionExpression(fn.params, body, fn.async);
} else {
value = t.functionExpression(
instrValue.name != null ? t.identifier(instrValue.name) : null,
fn.id ??
(instrValue.name != null ? t.identifier(instrValue.name) : null),
fn.params,
fn.body,
fn.generator,
fn.async,
);
}
if (
cx.env.config.enableNameAnonymousFunctions &&
instrValue.name == null &&
instrValue.nameHint != null
) {
const name = instrValue.nameHint;
value = t.memberExpression(
t.objectExpression([t.objectProperty(t.stringLiteral(name), value)]),
t.stringLiteral(name),
true,
false,
);
}
break;
}
case 'TaggedTemplateExpression': {

View File

@@ -1,174 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
FunctionExpression,
getHookKind,
HIRFunction,
IdentifierId,
} from '../HIR';
export function nameAnonymousFunctions(fn: HIRFunction): void {
if (fn.id == null) {
return;
}
const parentName = fn.id;
const functions = nameAnonymousFunctionsImpl(fn);
function visit(node: Node, prefix: string): void {
if (node.generatedName != null) {
/**
* Note that we don't generate a name for functions that already had one,
* so we'll only add the prefix to anonymous functions regardless of
* nesting depth.
*/
const name = `${prefix}${node.generatedName}]`;
node.fn.nameHint = name;
node.fn.loweredFunc.func.nameHint = name;
}
/**
* Whether or not we generated a name for the function at this node,
* traverse into its nested functions to assign them names
*/
const nextPrefix = `${prefix}${node.generatedName ?? node.fn.name ?? '<anonymous>'} > `;
for (const inner of node.inner) {
visit(inner, nextPrefix);
}
}
for (const node of functions) {
visit(node, `${parentName}[`);
}
}
type Node = {
fn: FunctionExpression;
generatedName: string | null;
inner: Array<Node>;
};
function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
// Functions that we track to generate names for
const functions: Map<IdentifierId, Node> = new Map();
// Tracks temporaries that read from variables/globals/properties
const names: Map<IdentifierId, string> = new Map();
// Tracks all function nodes to bubble up for later renaming
const nodes: Array<Node> = [];
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {lvalue, value} = instr;
switch (value.kind) {
case 'LoadGlobal': {
names.set(lvalue.identifier.id, value.binding.name);
break;
}
case 'LoadContext':
case 'LoadLocal': {
const name = value.place.identifier.name;
if (name != null && name.kind === 'named') {
names.set(lvalue.identifier.id, name.value);
}
break;
}
case 'PropertyLoad': {
const objectName = names.get(value.object.identifier.id);
if (objectName != null) {
names.set(
lvalue.identifier.id,
`${objectName}.${String(value.property)}`,
);
}
break;
}
case 'FunctionExpression': {
const inner = nameAnonymousFunctionsImpl(value.loweredFunc.func);
const node: Node = {
fn: value,
generatedName: null,
inner,
};
/**
* Bubble-up all functions, even if they're named, so that we can
* later generate names for any inner anonymous functions
*/
nodes.push(node);
if (value.name == null) {
// but only generate names for anonymous functions
functions.set(lvalue.identifier.id, node);
}
break;
}
case 'StoreContext':
case 'StoreLocal': {
const node = functions.get(value.value.identifier.id);
const variableName = value.lvalue.place.identifier.name;
if (
node != null &&
variableName != null &&
variableName.kind === 'named'
) {
node.generatedName = variableName.value;
functions.delete(value.value.identifier.id);
}
break;
}
case 'CallExpression':
case 'MethodCall': {
const callee =
value.kind === 'MethodCall' ? value.property : value.callee;
const hookKind = getHookKind(fn.env, callee.identifier);
let calleeName: string | null = null;
if (hookKind != null && hookKind !== 'Custom') {
calleeName = hookKind;
} else {
calleeName = names.get(callee.identifier.id) ?? '(anonymous)';
}
let fnArgCount = 0;
for (const arg of value.args) {
if (arg.kind === 'Identifier' && functions.has(arg.identifier.id)) {
fnArgCount++;
}
}
for (let i = 0; i < value.args.length; i++) {
const arg = value.args[i]!;
if (arg.kind === 'Spread') {
continue;
}
const node = functions.get(arg.identifier.id);
if (node != null) {
const generatedName =
fnArgCount > 1 ? `${calleeName}(arg${i})` : `${calleeName}()`;
node.generatedName = generatedName;
functions.delete(arg.identifier.id);
}
}
break;
}
case 'JsxExpression': {
for (const attr of value.props) {
if (attr.kind === 'JsxSpreadAttribute') {
continue;
}
const node = functions.get(attr.place.identifier.id);
if (node != null) {
const elementName =
value.tag.kind === 'BuiltinTag'
? value.tag.name
: (names.get(value.tag.identifier.id) ?? null);
const propName =
elementName == null
? attr.name
: `<${elementName}>.${attr.name}`;
node.generatedName = `${propName}`;
functions.delete(attr.place.identifier.id);
}
}
break;
}
}
}
}
return nodes;
}

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

@@ -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,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

@@ -1,272 +0,0 @@
## Input
```javascript
// @enableNameAnonymousFunctions
import {useEffect} from 'react';
import {identity, Stringify, useIdentity} from 'shared-runtime';
import * as SharedRuntime from 'shared-runtime';
function Component(props) {
function named() {
const inner = () => props.named;
return inner();
}
const namedVariable = function () {
return props.namedVariable;
};
const methodCall = SharedRuntime.identity(() => props.methodCall);
const call = identity(() => props.call);
const builtinElementAttr = <div onClick={() => props.builtinElementAttr} />;
const namedElementAttr = <Stringify onClick={() => props.namedElementAttr} />;
const hookArgument = useIdentity(() => props.hookArgument);
useEffect(() => {
console.log(props.useEffect);
JSON.stringify(null, null, () => props.useEffect);
const g = () => props.useEffect;
console.log(g());
}, [props.useEffect]);
return (
<>
{named()}
{namedVariable()}
{methodCall()}
{call()}
{builtinElementAttr}
{namedElementAttr}
{hookArgument()}
</>
);
}
export const TODO_FIXTURE_ENTRYPOINT = {
fn: Component,
params: [
{
named: '<named>',
namedVariable: '<namedVariable>',
methodCall: '<methodCall>',
call: '<call>',
builtinElementAttr: '<builtinElementAttr>',
namedElementAttr: '<namedElementAttr>',
hookArgument: '<hookArgument>',
useEffect: '<useEffect>',
},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNameAnonymousFunctions
import { useEffect } from "react";
import { identity, Stringify, useIdentity } from "shared-runtime";
import * as SharedRuntime from "shared-runtime";
function Component(props) {
const $ = _c(31);
let t0;
if ($[0] !== props.named) {
t0 = function named() {
const inner = { "Component[named > inner]": () => props.named }[
"Component[named > inner]"
];
return inner();
};
$[0] = props.named;
$[1] = t0;
} else {
t0 = $[1];
}
const named = t0;
let t1;
if ($[2] !== props.namedVariable) {
t1 = {
"Component[namedVariable]": function () {
return props.namedVariable;
},
}["Component[namedVariable]"];
$[2] = props.namedVariable;
$[3] = t1;
} else {
t1 = $[3];
}
const namedVariable = t1;
let t2;
if ($[4] !== props.methodCall) {
t2 = { "Component[SharedRuntime.identity()]": () => props.methodCall }[
"Component[SharedRuntime.identity()]"
];
$[4] = props.methodCall;
$[5] = t2;
} else {
t2 = $[5];
}
const methodCall = SharedRuntime.identity(t2);
let t3;
if ($[6] !== props.call) {
t3 = { "Component[identity()]": () => props.call }["Component[identity()]"];
$[6] = props.call;
$[7] = t3;
} else {
t3 = $[7];
}
const call = identity(t3);
let t4;
if ($[8] !== props.builtinElementAttr) {
t4 = (
<div
onClick={
{ "Component[<div>.onClick]": () => props.builtinElementAttr }[
"Component[<div>.onClick]"
]
}
/>
);
$[8] = props.builtinElementAttr;
$[9] = t4;
} else {
t4 = $[9];
}
const builtinElementAttr = t4;
let t5;
if ($[10] !== props.namedElementAttr) {
t5 = (
<Stringify
onClick={
{ "Component[<Stringify>.onClick]": () => props.namedElementAttr }[
"Component[<Stringify>.onClick]"
]
}
/>
);
$[10] = props.namedElementAttr;
$[11] = t5;
} else {
t5 = $[11];
}
const namedElementAttr = t5;
let t6;
if ($[12] !== props.hookArgument) {
t6 = { "Component[useIdentity()]": () => props.hookArgument }[
"Component[useIdentity()]"
];
$[12] = props.hookArgument;
$[13] = t6;
} else {
t6 = $[13];
}
const hookArgument = useIdentity(t6);
let t7;
let t8;
if ($[14] !== props.useEffect) {
t7 = {
"Component[useEffect()]": () => {
console.log(props.useEffect);
JSON.stringify(
null,
null,
{
"Component[useEffect() > JSON.stringify()]": () => props.useEffect,
}["Component[useEffect() > JSON.stringify()]"],
);
const g = { "Component[useEffect() > g]": () => props.useEffect }[
"Component[useEffect() > g]"
];
console.log(g());
},
}["Component[useEffect()]"];
t8 = [props.useEffect];
$[14] = props.useEffect;
$[15] = t7;
$[16] = t8;
} else {
t7 = $[15];
t8 = $[16];
}
useEffect(t7, t8);
let t9;
if ($[17] !== named) {
t9 = named();
$[17] = named;
$[18] = t9;
} else {
t9 = $[18];
}
let t10;
if ($[19] !== namedVariable) {
t10 = namedVariable();
$[19] = namedVariable;
$[20] = t10;
} else {
t10 = $[20];
}
const t11 = methodCall();
const t12 = call();
let t13;
if ($[21] !== hookArgument) {
t13 = hookArgument();
$[21] = hookArgument;
$[22] = t13;
} else {
t13 = $[22];
}
let t14;
if (
$[23] !== builtinElementAttr ||
$[24] !== namedElementAttr ||
$[25] !== t10 ||
$[26] !== t11 ||
$[27] !== t12 ||
$[28] !== t13 ||
$[29] !== t9
) {
t14 = (
<>
{t9}
{t10}
{t11}
{t12}
{builtinElementAttr}
{namedElementAttr}
{t13}
</>
);
$[23] = builtinElementAttr;
$[24] = namedElementAttr;
$[25] = t10;
$[26] = t11;
$[27] = t12;
$[28] = t13;
$[29] = t9;
$[30] = t14;
} else {
t14 = $[30];
}
return t14;
}
export const TODO_FIXTURE_ENTRYPOINT = {
fn: Component,
params: [
{
named: "<named>",
namedVariable: "<namedVariable>",
methodCall: "<methodCall>",
call: "<call>",
builtinElementAttr: "<builtinElementAttr>",
namedElementAttr: "<namedElementAttr>",
hookArgument: "<hookArgument>",
useEffect: "<useEffect>",
},
],
};
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,53 +0,0 @@
// @enableNameAnonymousFunctions
import {useEffect} from 'react';
import {identity, Stringify, useIdentity} from 'shared-runtime';
import * as SharedRuntime from 'shared-runtime';
function Component(props) {
function named() {
const inner = () => props.named;
return inner();
}
const namedVariable = function () {
return props.namedVariable;
};
const methodCall = SharedRuntime.identity(() => props.methodCall);
const call = identity(() => props.call);
const builtinElementAttr = <div onClick={() => props.builtinElementAttr} />;
const namedElementAttr = <Stringify onClick={() => props.namedElementAttr} />;
const hookArgument = useIdentity(() => props.hookArgument);
useEffect(() => {
console.log(props.useEffect);
JSON.stringify(null, null, () => props.useEffect);
const g = () => props.useEffect;
console.log(g());
}, [props.useEffect]);
return (
<>
{named()}
{namedVariable()}
{methodCall()}
{call()}
{builtinElementAttr}
{namedElementAttr}
{hookArgument()}
</>
);
}
export const TODO_FIXTURE_ENTRYPOINT = {
fn: Component,
params: [
{
named: '<named>',
namedVariable: '<namedVariable>',
methodCall: '<methodCall>',
call: '<call>',
builtinElementAttr: '<builtinElementAttr>',
namedElementAttr: '<namedElementAttr>',
hookArgument: '<hookArgument>',
useEffect: '<useEffect>',
},
],
};

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

@@ -5,19 +5,13 @@
* LICENSE file in the root directory of this source tree.
*/
export type MessagePayload =
| null
| string
| number
| boolean
| {[key: string]: MessagePayload}
| MessagePayload[];
export type Message = {event: string; payload?: MessagePayload};
export type MessagePayload = null | string | number | boolean | { [key: string]: MessagePayload } | MessagePayload[];
export type Message = { event: string, payload?: MessagePayload };
export type WallListener = (message: Message) => void;
export type Wall = {
listen: (fn: WallListener) => Function;
send: (event: string, payload?: MessagePayload) => void;
listen: (fn: WallListener) => Function,
send: (event: string, payload?: MessagePayload) => void,
};
export type Bridge = {
@@ -28,7 +22,7 @@ export type Bridge = {
export type Store = Object;
export type BrowserTheme = 'dark' | 'light';
export type Config = {
supportsReloadAndProfile?: boolean;
supportsReloadAndProfile?: boolean,
};
export function createBridge(wall: Wall): Bridge;
@@ -61,23 +55,15 @@ export type CanViewElementSource = (
source: ReactFunctionLocation | ReactCallSite,
symbolicatedSource: ReactFunctionLocation | ReactCallSite | null,
) => boolean;
export type FetchFileWithCaching = (url: string) => Promise<string>;
export type InitializationOptions = {
bridge: Bridge;
store: Store;
theme?: BrowserTheme;
viewAttributeSourceFunction?: ViewAttributeSource;
viewElementSourceFunction?: ViewElementSource;
canViewElementSourceFunction?: CanViewElementSource;
fetchFileWithCaching?: FetchFileWithCaching;
bridge: Bridge,
store: Store,
theme?: BrowserTheme,
viewAttributeSourceFunction?: ViewAttributeSource,
viewElementSourceFunction?: ViewElementSource,
canViewElementSourceFunction?: CanViewElementSource,
};
export function initializeComponents(
node: Element | Document,
options: InitializationOptions,
): void;
export function initializeProfiler(
node: Element | Document,
options: InitializationOptions,
): void;
export function initializeComponents(node: Element | Document, options: InitializationOptions): void;
export function initializeProfiler(node: Element | Document, options: InitializationOptions): void;

View File

@@ -24,7 +24,6 @@ import type {
ViewAttributeSource,
ViewElementSource,
} from 'react-devtools-shared/src/devtools/views/DevTools';
import type {FetchFileWithCaching} from 'react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext';
import type {Config} from 'react-devtools-shared/src/devtools/store';
export function createBridge(wall?: Wall): FrontendBridge {
@@ -51,7 +50,6 @@ type InitializationOptions = {
viewAttributeSourceFunction?: ViewAttributeSource,
viewElementSourceFunction?: ViewElementSource,
canViewElementSourceFunction?: CanViewElementSource,
fetchFileWithCaching?: FetchFileWithCaching,
};
function initializeTab(
@@ -66,7 +64,6 @@ function initializeTab(
viewAttributeSourceFunction,
viewElementSourceFunction,
canViewElementSourceFunction,
fetchFileWithCaching,
} = options;
const root = createRoot(contentWindow);
@@ -82,7 +79,6 @@ function initializeTab(
viewAttributeSourceFunction={viewAttributeSourceFunction}
viewElementSourceFunction={viewElementSourceFunction}
canViewElementSourceFunction={canViewElementSourceFunction}
fetchFileWithCaching={fetchFileWithCaching}
/>,
);
}

View File

@@ -228,8 +228,8 @@ describe('commit tree', () => {
[root]
▾ <App>
<Suspense>
[suspense-root] rects={null}
<Suspense name="App" rects={null}>
[shell]
<Suspense name="App>?" rects={null}>
`);
utils.act(() => modernRender(<App renderChildren={true} />));
expect(store).toMatchInlineSnapshot(`
@@ -237,8 +237,8 @@ describe('commit tree', () => {
▾ <App>
▾ <Suspense>
<LazyInnerComponent>
[suspense-root] rects={null}
<Suspense name="App" rects={null}>
[shell]
<Suspense name="App>?" rects={null}>
`);
utils.act(() => modernRender(<App renderChildren={false} />));
expect(store).toMatchInlineSnapshot(`
@@ -303,8 +303,8 @@ describe('commit tree', () => {
[root]
▾ <App>
<Suspense>
[suspense-root] rects={null}
<Suspense name="App" rects={null}>
[shell]
<Suspense name="App>?" rects={null}>
`);
utils.act(() => modernRender(<App renderChildren={false} />));
expect(store).toMatchInlineSnapshot(`

View File

@@ -133,7 +133,7 @@ describe('Store', () => {
<Suspense>
▾ <Parent>
<Child>
[suspense-root] rects={null}
[shell]
<Suspense name="Unknown" rects={null}>
`);
});
@@ -492,8 +492,8 @@ describe('Store', () => {
<Component key="Outside">
▾ <Suspense>
<Loading>
[suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:10,height:1}]}
<Suspense name="Wrapper" rects={null}>
[shell]
<Suspense name="Wrapper>?" rects={null}>
`);
await act(() => {
@@ -505,8 +505,8 @@ describe('Store', () => {
<Component key="Outside">
▾ <Suspense>
<Component key="Inside">
[suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}
<Suspense name="Wrapper" rects={[{x:1,y:2,width:5,height:1}]}>
[shell]
<Suspense name="Wrapper>?" rects={[{x:1,y:2,width:5,height:1}]}>
`);
});
@@ -584,7 +584,7 @@ describe('Store', () => {
▾ <Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[suspense-root] rects={[{x:1,y:2,width:10,height:1}]}
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
@@ -612,7 +612,7 @@ describe('Store', () => {
▾ <Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
@@ -640,7 +640,7 @@ describe('Store', () => {
▾ <Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
@@ -668,7 +668,7 @@ describe('Store', () => {
▾ <Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
@@ -689,7 +689,7 @@ describe('Store', () => {
<Component key="Outside">
▾ <Suspense name="parent">
<Loading key="Parent Fallback">
[suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
@@ -717,7 +717,7 @@ describe('Store', () => {
▾ <Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
@@ -745,7 +745,7 @@ describe('Store', () => {
▾ <Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[suspense-root] rects={[{x:1,y:2,width:10,height:1}]}
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
@@ -773,7 +773,7 @@ describe('Store', () => {
▾ <Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
@@ -792,7 +792,7 @@ describe('Store', () => {
<Component key="Outside">
▾ <Suspense name="parent">
<Loading key="Parent Fallback">
[suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
@@ -813,7 +813,7 @@ describe('Store', () => {
<Component key="Outside">
▾ <Suspense name="parent">
<Loading key="Parent Fallback">
[suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
@@ -839,7 +839,7 @@ describe('Store', () => {
▾ <Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
@@ -865,7 +865,7 @@ describe('Store', () => {
▾ <Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
@@ -893,7 +893,7 @@ describe('Store', () => {
▾ <Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[suspense-root] rects={[{x:1,y:2,width:10,height:1}]}
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
@@ -948,7 +948,7 @@ describe('Store', () => {
▾ <Suspense name="three">
<Component key="Suspense 3 Content">
<Component key="Unrelated at End">
[suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}>
<Suspense name="one" rects={[{x:1,y:2,width:5,height:1}]}>
<Suspense name="two" rects={[{x:1,y:2,width:5,height:1}]}>
@@ -981,7 +981,7 @@ describe('Store', () => {
▾ <Suspense name="three">
<Component key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}>
<Suspense name="one" rects={[{x:1,y:2,width:5,height:1}]}>
<Suspense name="two" rects={[{x:1,y:2,width:5,height:1}]}>
@@ -1009,7 +1009,7 @@ describe('Store', () => {
▾ <Suspense name="three">
<Component key="Suspense 3 Content">
<Component key="Unrelated at End">
[suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}>
<Suspense name="one" rects={[{x:1,y:2,width:5,height:1}]}>
<Suspense name="two" rects={[{x:1,y:2,width:5,height:1}]}>
@@ -1053,8 +1053,8 @@ describe('Store', () => {
<Component key="A">
▾ <Suspense>
<Loading>
[suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:10,height:1}]}
<Suspense name="Wrapper" rects={null}>
[shell]
<Suspense name="Wrapper>?" rects={null}>
`);
await act(() => {
@@ -1068,8 +1068,8 @@ describe('Store', () => {
▾ <Suspense>
<Component key="B">
<Component key="C">
[suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}
<Suspense name="Wrapper" rects={[{x:1,y:2,width:5,height:1}]}>
[shell]
<Suspense name="Wrapper>?" rects={[{x:1,y:2,width:5,height:1}]}>
`);
});
@@ -1406,8 +1406,8 @@ describe('Store', () => {
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Wrapper>
[suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:10,height:1}]}
<Suspense name="Wrapper" rects={null}>
[shell]
<Suspense name="Wrapper>?" rects={null}>
`);
// This test isn't meaningful unless we expand the suspended tree
@@ -1423,8 +1423,8 @@ describe('Store', () => {
<Component key="Outside">
▾ <Suspense>
<Loading>
[suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:10,height:1}]}
<Suspense name="Wrapper" rects={null}>
[shell]
<Suspense name="Wrapper>?" rects={null}>
`);
await act(() => {
@@ -1436,8 +1436,8 @@ describe('Store', () => {
<Component key="Outside">
▾ <Suspense>
<Component key="Inside">
[suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}
<Suspense name="Wrapper" rects={[{x:1,y:2,width:5,height:1}]}>
[shell]
<Suspense name="Wrapper>?" rects={[{x:1,y:2,width:5,height:1}]}>
`);
});
@@ -1662,8 +1662,8 @@ describe('Store', () => {
expect(store).toMatchInlineSnapshot(`
[root]
▸ <SuspenseTree>
[suspense-root] rects={null}
<Suspense name="SuspenseTree" rects={null}>
[shell]
<Suspense name="SuspenseTree>?" rects={null}>
`);
await act(() =>
@@ -1677,8 +1677,8 @@ describe('Store', () => {
▾ <SuspenseTree>
▾ <Suspense>
▸ <Parent>
[suspense-root] rects={null}
<Suspense name="SuspenseTree" rects={null}>
[shell]
<Suspense name="SuspenseTree>?" rects={null}>
`);
const rendererID = getRendererID();
@@ -1696,8 +1696,8 @@ describe('Store', () => {
▾ <SuspenseTree>
▾ <Suspense>
<Fallback>
[suspense-root] rects={null}
<Suspense name="SuspenseTree" rects={null}>
[shell]
<Suspense name="SuspenseTree>?" rects={null}>
`);
await act(() =>
@@ -1712,8 +1712,8 @@ describe('Store', () => {
▾ <SuspenseTree>
▾ <Suspense>
▸ <Parent>
[suspense-root] rects={null}
<Suspense name="SuspenseTree" rects={null}>
[shell]
<Suspense name="SuspenseTree>?" rects={null}>
`);
});
});
@@ -2017,8 +2017,8 @@ describe('Store', () => {
[root]
▾ <App>
<Suspense>
[suspense-root] rects={null}
<Suspense name="App" rects={null}>
[shell]
<Suspense name="App>?" rects={null}>
`);
await Promise.resolve();
@@ -2031,8 +2031,8 @@ describe('Store', () => {
▾ <App>
▾ <Suspense>
<LazyInnerComponent>
[suspense-root] rects={null}
<Suspense name="App" rects={null}>
[shell]
<Suspense name="App>?" rects={null}>
`);
// Render again to unmount it
@@ -2522,8 +2522,8 @@ describe('Store', () => {
▾ <App>
▾ <Suspense>
<ChildA>
[suspense-root] rects={null}
<Suspense name="App" rects={null}>
[shell]
<Suspense name="App>?" rects={null}>
`);
await actAsync(() => render(<App renderA={false} />));
@@ -2533,8 +2533,8 @@ describe('Store', () => {
▾ <App>
▾ <Suspense>
<ChildB>
[suspense-root] rects={null}
<Suspense name="App" rects={null}>
[shell]
<Suspense name="App>?" rects={null}>
`);
});
});
@@ -2860,7 +2860,7 @@ describe('Store', () => {
▾ <Suspense name="content">
▾ <Suspense name="fallback">
<Component key="fallback-fallback">
[suspense-root] rects={[{x:1,y:2,width:19,height:1}]}
[shell]
<Suspense name="content" rects={null}>
<Suspense name="fallback" rects={null}>
`);
@@ -2874,7 +2874,7 @@ describe('Store', () => {
▾ <Suspense name="content">
▾ <Suspense name="fallback">
<Component key="fallback-content">
[suspense-root] rects={[{x:1,y:2,width:10,height:1}]}
[shell]
<Suspense name="content" rects={null}>
<Suspense name="fallback" rects={[{x:1,y:2,width:10,height:1}]}>
`);
@@ -2887,7 +2887,7 @@ describe('Store', () => {
[root]
▾ <Suspense name="content">
<Component key="content">
[suspense-root] rects={[{x:1,y:2,width:4,height:1}]}
[shell]
<Suspense name="content" rects={[{x:1,y:2,width:4,height:1}]}>
`);
});
@@ -2990,7 +2990,7 @@ describe('Store', () => {
▾ <Suspense name="main">
▾ <Suspense name="main-fallback">
<Component key="main-fallback-fallback">
[suspense-root] rects={[{x:1,y:2,width:19,height:1}, {x:1,y:2,width:19,height:1}]}
[shell]
<Suspense name="head" rects={null}>
<Suspense name="head-fallback" rects={null}>
<Suspense name="main" rects={null}>
@@ -3013,7 +3013,7 @@ describe('Store', () => {
▾ <WithSuspenseInFallback>
▾ <Suspense name="main">
<Component key="main-content">
[suspense-root] rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:4,height:1}]}
[shell]
<Suspense name="head" rects={[{x:1,y:2,width:4,height:1}]}>
<Suspense name="main" rects={[{x:1,y:2,width:4,height:1}]}>
`);
@@ -3043,7 +3043,7 @@ describe('Store', () => {
▾ <WithSuspenseInFallback>
▾ <Suspense name="main">
<Component key="main-content">
[suspense-root] rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:4,height:1}]}
[shell]
<Suspense name="head" rects={[{x:1,y:2,width:4,height:1}]}>
<Suspense name="head-fallback" rects={[{x:1,y:2,width:10,height:1}]}>
<Suspense name="main" rects={[{x:1,y:2,width:4,height:1}]}>
@@ -3074,15 +3074,10 @@ describe('Store', () => {
▾ <WithSuspenseInFallback>
▾ <Suspense name="main">
<Component key="main-content">
[suspense-root] rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:19,height:1}, {x:1,y:2,width:4,height:1}]}
[shell]
<Suspense name="head" rects={[{x:1,y:2,width:4,height:1}]}>
<Suspense name="head-fallback" rects={[{x:1,y:2,width:10,height:1}]}>
<Suspense name="main" rects={[{x:1,y:2,width:4,height:1}]}>
`);
});
it('should handle an empty root', async () => {
await actAsync(() => render(null));
expect(store).toMatchInlineSnapshot(`[root]`);
});
});

View File

@@ -156,7 +156,7 @@ describe('Store component filters', () => {
<div>
▾ <Suspense>
<div>
[suspense-root] rects={[]}
[shell]
<Suspense name="Unknown" rects={[]}>
<Suspense name="Unknown" rects={[]}>
`);
@@ -174,7 +174,7 @@ describe('Store component filters', () => {
<div>
▾ <Suspense>
<div>
[suspense-root] rects={[]}
[shell]
<Suspense name="Unknown" rects={[]}>
<Suspense name="Unknown" rects={[]}>
`);
@@ -192,7 +192,7 @@ describe('Store component filters', () => {
<div>
▾ <Suspense>
<div>
[suspense-root] rects={[]}
[shell]
<Suspense name="Unknown" rects={[]}>
<Suspense name="Unknown" rects={[]}>
`);

View File

@@ -1368,9 +1368,9 @@ describe('TreeListContext', () => {
▾ <Child>
▾ <Suspense>
<Grandchild>
[suspense-root] rects={null}
<Suspense name="Parent" rects={null}>
<Suspense name="Child" rects={null}>
[shell]
<Suspense name="Parent>?" rects={null}>
<Suspense name="Child>?" rects={null}>
`);
const outerSuspenseID = ((store.getElementIDAtIndex(1): any): number);
@@ -1410,9 +1410,9 @@ describe('TreeListContext', () => {
▾ <Child>
▾ <Suspense>
<Grandchild>
[suspense-root] rects={null}
<Suspense name="Parent" rects={null}>
<Suspense name="Child" rects={null}>
[shell]
<Suspense name="Parent>?" rects={null}>
<Suspense name="Child>?" rects={null}>
`);
});
});
@@ -2369,7 +2369,7 @@ describe('TreeListContext', () => {
expect(state).toMatchInlineSnapshot(`
[root]
<Suspense>
[suspense-root] rects={null}
[shell]
<Suspense name="Unknown" rects={null}>
`);
@@ -2378,7 +2378,7 @@ describe('TreeListContext', () => {
expect(state).toMatchInlineSnapshot(`
[root]
<Suspense>
[suspense-root] rects={null}
[shell]
<Suspense name="Unknown" rects={null}>
`);
});
@@ -2404,7 +2404,7 @@ describe('TreeListContext', () => {
expect(state).toMatchInlineSnapshot(`
[root]
<Suspense>
[suspense-root] rects={null}
[shell]
<Suspense name="Unknown" rects={null}>
`);
@@ -2426,7 +2426,7 @@ describe('TreeListContext', () => {
▾ <Suspense>
<Child> ⚠
<Child>
[suspense-root] rects={null}
[shell]
<Suspense name="Unknown" rects={null}>
`);
});
@@ -2456,7 +2456,7 @@ describe('TreeListContext', () => {
▾ <Suspense>
▾ <Fallback>
<Child> ✕
[suspense-root] rects={null}
[shell]
<Suspense name="Unknown" rects={null}>
`);
@@ -2475,7 +2475,7 @@ describe('TreeListContext', () => {
[root]
▾ <Suspense>
<Child>
[suspense-root] rects={null}
[shell]
<Suspense name="Unknown" rects={null}>
`);
});

View File

@@ -3700,32 +3700,28 @@ export function attach(
// just use the Fiber anyway.
// Fallbacks get attributed to the parent so we only measure if we're
// showing primary content.
if (fiber.tag === SuspenseComponent) {
if (OffscreenComponent === -1) {
const isTimedOut = fiber.memoizedState !== null;
if (!isTimedOut) {
newSuspenseNode.rects = measureInstance(newInstance);
}
} else {
const hydrated = isFiberHydrated(fiber);
if (hydrated) {
const contentFiber = fiber.child;
if (contentFiber === null) {
throw new Error(
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
);
}
} else {
// This Suspense Fiber is still dehydrated. It won't have any children
// until hydration.
}
const isTimedOut = fiber.memoizedState !== null;
if (!isTimedOut) {
newSuspenseNode.rects = measureInstance(newInstance);
}
if (OffscreenComponent === -1) {
const isTimedOut = fiber.memoizedState !== null;
if (!isTimedOut) {
newSuspenseNode.rects = measureInstance(newInstance);
}
} else {
newSuspenseNode.rects = measureInstance(newInstance);
const hydrated = isFiberHydrated(fiber);
if (hydrated) {
const contentFiber = fiber.child;
if (contentFiber === null) {
throw new Error(
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
);
}
} else {
// This Suspense Fiber is still dehydrated. It won't have any children
// until hydration.
}
const isTimedOut = fiber.memoizedState !== null;
if (!isTimedOut) {
newSuspenseNode.rects = measureInstance(newInstance);
}
}
recordSuspenseMount(newSuspenseNode, reconcilingParentSuspenseNode);
}

View File

@@ -1950,13 +1950,14 @@ export default class Store extends EventEmitter<{
throw error;
}
_guessSuspenseName(element: Element): string {
_guessSuspenseName(element: Element): string | null {
// TODO: Use key
const owner = this._idToElement.get(element.ownerID);
let name = 'Unknown';
if (owner !== undefined && owner.displayName !== null) {
name = owner.displayName;
if (owner !== undefined) {
// TODO: This is clowny
return `${owner.displayName || 'Unknown'}>?`;
}
return name;
return null;
}
}

View File

@@ -54,21 +54,22 @@ export function printElement(
}${key}${name}>${hocs}${suffix}`;
}
function printRects(rects: SuspenseNode['rects']): string {
if (rects === null) {
return ' rects={null}';
} else {
return ` rects={[${rects.map(rect => `{x:${rect.x},y:${rect.y},width:${rect.width},height:${rect.height}}`).join(', ')}]}`;
}
}
function printSuspense(suspense: SuspenseNode): string {
function printSuspense(
suspense: SuspenseNode,
includeWeight: boolean = false,
): string {
let name = '';
if (suspense.name !== null) {
name = ` name="${suspense.name}"`;
}
const printedRects = printRects(suspense.rects);
let printedRects = '';
const rects = suspense.rects;
if (rects === null) {
printedRects = ' rects={null}';
} else {
printedRects = ` rects={[${rects.map(rect => `{x:${rect.x},y:${rect.y},width:${rect.width},height:${rect.height}}`).join(', ')}]}`;
}
return `<Suspense${name}${printedRects}>`;
}
@@ -177,13 +178,13 @@ export function printStore(
rootWeight += weight;
if (includeSuspense) {
const root = store.getSuspenseByID(rootID);
const shell = store.getSuspenseByID(rootID);
// Roots from legacy renderers don't have a separate Suspense tree
if (root !== null) {
if (root.children.length > 0) {
snapshotLines.push('[suspense-root] ' + printRects(root.rects));
for (let i = 0; i < root.children.length; i++) {
const childID = root.children[i];
if (shell !== null) {
if (shell.children.length > 0) {
snapshotLines.push('[shell]');
for (let i = 0; i < shell.children.length; i++) {
const childID = shell.children[i];
const child = store.getSuspenseByID(childID);
if (child === null) {
throw new Error(

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