Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d651f69bc1 | ||
|
|
853550e7c8 | ||
|
|
5cf71b322d | ||
|
|
f807ce6492 | ||
|
|
7b38acca0b | ||
|
|
1d9c3927ea | ||
|
|
969a9790ad | ||
|
|
665de2ed28 | ||
|
|
eda778b8ae | ||
|
|
1836b46fff | ||
|
|
eec50b17b3 | ||
|
|
a9410fb487 | ||
|
|
6b70072c4f | ||
|
|
b2cff47472 | ||
|
|
8943025358 |
@@ -103,6 +103,7 @@ 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}
|
||||
@@ -324,6 +325,15 @@ 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});
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
makePropertyLiteral,
|
||||
makeType,
|
||||
promoteTemporary,
|
||||
validateIdentifierName,
|
||||
} from './HIR';
|
||||
import HIRBuilder, {Bindings, createTemporaryPlace} from './HIRBuilder';
|
||||
import {BuiltInArrayId} from './ObjectShape';
|
||||
@@ -213,6 +214,16 @@ 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);
|
||||
}
|
||||
@@ -234,7 +245,8 @@ export function lower(
|
||||
);
|
||||
|
||||
return Ok({
|
||||
id,
|
||||
id: validatedId,
|
||||
nameHint: null,
|
||||
params,
|
||||
fnType: bindings == null ? env.fnType : 'Other',
|
||||
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
|
||||
@@ -3563,17 +3575,14 @@ 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,
|
||||
name: loweredFunc.func.id,
|
||||
nameHint: null,
|
||||
type: expr.node.type,
|
||||
loc: exprLoc,
|
||||
loweredFunc,
|
||||
|
||||
@@ -261,6 +261,8 @@ 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,
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
|
||||
import {BindingKind} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {Environment, ReactFunctionType} from './Environment';
|
||||
import type {HookKind} from './ObjectShape';
|
||||
@@ -15,6 +19,7 @@ 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';
|
||||
|
||||
/*
|
||||
* *******************************************************************************************
|
||||
@@ -53,7 +58,8 @@ export type SourceLocation = t.SourceLocation | typeof GeneratedSource;
|
||||
*/
|
||||
export type ReactiveFunction = {
|
||||
loc: SourceLocation;
|
||||
id: string | null;
|
||||
id: ValidIdentifierName | null;
|
||||
nameHint: string | null;
|
||||
params: Array<Place | SpreadPattern>;
|
||||
generator: boolean;
|
||||
async: boolean;
|
||||
@@ -275,7 +281,8 @@ 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: string | null;
|
||||
id: ValidIdentifierName | null;
|
||||
nameHint: string | null;
|
||||
fnType: ReactFunctionType;
|
||||
env: Environment;
|
||||
params: Array<Place | SpreadPattern>;
|
||||
@@ -1123,7 +1130,8 @@ export type JsxAttribute =
|
||||
|
||||
export type FunctionExpression = {
|
||||
kind: 'FunctionExpression';
|
||||
name: string | null;
|
||||
name: ValidIdentifierName | null;
|
||||
nameHint: string | null;
|
||||
loweredFunc: LoweredFunction;
|
||||
type:
|
||||
| 'ArrowFunctionExpression'
|
||||
@@ -1298,37 +1306,52 @@ 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 {
|
||||
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,
|
||||
};
|
||||
return validateIdentifierName(name).unwrap();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -56,6 +56,9 @@ export function printFunction(fn: HIRFunction): string {
|
||||
} else {
|
||||
definition += '<<anonymous>>';
|
||||
}
|
||||
if (fn.nameHint != null) {
|
||||
definition += ` ${fn.nameHint}`;
|
||||
}
|
||||
if (fn.params.length !== 0) {
|
||||
definition +=
|
||||
'(' +
|
||||
|
||||
@@ -249,6 +249,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
const fn: HIRFunction = {
|
||||
loc: GeneratedSource,
|
||||
id: null,
|
||||
nameHint: null,
|
||||
fnType: 'Other',
|
||||
env,
|
||||
params: [obj],
|
||||
@@ -275,6 +276,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
value: {
|
||||
kind: 'FunctionExpression',
|
||||
name: null,
|
||||
nameHint: null,
|
||||
loweredFunc: {
|
||||
func: fn,
|
||||
},
|
||||
|
||||
@@ -31,7 +31,9 @@ export function outlineFunctions(
|
||||
) {
|
||||
const loweredFunc = value.loweredFunc.func;
|
||||
|
||||
const id = fn.env.generateGloballyUniqueIdentifierName(loweredFunc.id);
|
||||
const id = fn.env.generateGloballyUniqueIdentifierName(
|
||||
loweredFunc.id ?? loweredFunc.nameHint,
|
||||
);
|
||||
loweredFunc.id = id.value;
|
||||
|
||||
fn.env.outlineFunction(loweredFunc, null);
|
||||
|
||||
@@ -364,6 +364,7 @@ function emitOutlinedFn(
|
||||
const fn: HIRFunction = {
|
||||
loc: GeneratedSource,
|
||||
id: null,
|
||||
nameHint: null,
|
||||
fnType: 'Other',
|
||||
env,
|
||||
params: [propsObj],
|
||||
|
||||
@@ -44,6 +44,7 @@ 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,
|
||||
|
||||
@@ -61,6 +61,7 @@ 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;
|
||||
@@ -383,6 +384,7 @@ function codegenReactiveFunction(
|
||||
type: 'CodegenFunction',
|
||||
loc: fn.loc,
|
||||
id: fn.id !== null ? t.identifier(fn.id) : null,
|
||||
nameHint: fn.nameHint,
|
||||
params,
|
||||
body,
|
||||
generator: fn.generator,
|
||||
@@ -2326,6 +2328,7 @@ 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) {
|
||||
@@ -2337,14 +2340,26 @@ function codegenInstructionValue(
|
||||
value = t.arrowFunctionExpression(fn.params, body, fn.async);
|
||||
} else {
|
||||
value = t.functionExpression(
|
||||
fn.id ??
|
||||
(instrValue.name != null ? t.identifier(instrValue.name) : null),
|
||||
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': {
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -5,21 +5,223 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {effect} from 'zod';
|
||||
import {CompilerError, Effect, ErrorSeverity, 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 {
|
||||
eachInstructionValueOperand,
|
||||
eachInstructionOperand,
|
||||
eachTerminalOperand,
|
||||
eachInstructionLValue,
|
||||
eachPatternOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
// TODO: Maybe I can consolidate some types
|
||||
type SetStateCall = {
|
||||
loc: SourceLocation;
|
||||
invalidDeps: DerivationMetadata;
|
||||
setStateId: IdentifierId;
|
||||
};
|
||||
|
||||
type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsOrState';
|
||||
|
||||
type SetStateName = string | undefined | null;
|
||||
|
||||
type DerivationMetadata = {
|
||||
typeOfValue: TypeOfValue;
|
||||
// TODO: Rename to place
|
||||
identifierPlace: Place;
|
||||
sources: Place[];
|
||||
};
|
||||
|
||||
// TODO: This needs refining
|
||||
type ErrorMetadata = {
|
||||
errorType: TypeOfValue;
|
||||
invalidDepInfo: string | undefined;
|
||||
loc: SourceLocation;
|
||||
setStateName: SetStateName;
|
||||
};
|
||||
|
||||
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<SetStateName, Place[]>,
|
||||
) {
|
||||
// console.log(printInstruction(instr));
|
||||
// console.log(instr);
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
|
||||
// TODO: Not sure if this will catch every time we create a new useState
|
||||
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 (
|
||||
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
|
||||
) {
|
||||
if (setStateCalls.has(instr.value.callee.loc.identifierName)) {
|
||||
setStateCalls
|
||||
.get(instr.value.callee.loc.identifierName)!
|
||||
.push(instr.value.callee);
|
||||
} else {
|
||||
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
|
||||
@@ -48,12 +250,54 @@ 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();
|
||||
|
||||
const errors = new CompilerError();
|
||||
const effectSetStates: Map<SetStateName, Place[]> = new Map();
|
||||
const setStateCalls: Map<SetStateName, Place[]> = new Map();
|
||||
|
||||
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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value.kind === 'LoadLocal') {
|
||||
locals.set(lvalue.identifier.id, value.place.identifier.id);
|
||||
} else if (value.kind === 'ArrayExpression') {
|
||||
@@ -66,6 +310,7 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
) {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
|
||||
if (
|
||||
isUseEffectHookType(callee.identifier) &&
|
||||
value.args.length === 2 &&
|
||||
@@ -97,6 +342,8 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
validateEffect(
|
||||
effectFunction.loweredFunc.func,
|
||||
dependencies,
|
||||
derivedTuple,
|
||||
effectSetStates,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
@@ -104,43 +351,99 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.hasAnyErrors()) {
|
||||
throw errors;
|
||||
|
||||
const throwableErrors = new CompilerError();
|
||||
for (const error of errors) {
|
||||
let reason;
|
||||
let description = '';
|
||||
// TODO: Not sure if this is robust enough.
|
||||
/*
|
||||
* If we use a setState from an invalid useEffect elsewhere then we probably have to
|
||||
* hoist state up, else we should calculate in render
|
||||
*/
|
||||
if (
|
||||
setStateCalls.get(error.setStateName)?.length !=
|
||||
effectSetStates.get(error.setStateName)?.length &&
|
||||
error.errorType !== 'fromState'
|
||||
) {
|
||||
reason =
|
||||
'Consider lifting state up to the parent component to make this a controlled component. (https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes)';
|
||||
} else {
|
||||
reason =
|
||||
'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)';
|
||||
}
|
||||
|
||||
throwableErrors.push({
|
||||
reason: reason,
|
||||
description: `You are using invalid dependencies: \n\n${error.invalidDepInfo}`,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: error.loc,
|
||||
});
|
||||
}
|
||||
|
||||
if (throwableErrors.hasAnyErrors()) {
|
||||
throw throwableErrors;
|
||||
}
|
||||
}
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
effectDeps: Array<IdentifierId>,
|
||||
errors: CompilerError,
|
||||
derivedTuple: Map<IdentifierId, DerivationMetadata>,
|
||||
effectSetStates: Map<SetStateName, Place[]>,
|
||||
errors: ErrorMetadata[],
|
||||
): 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;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 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
|
||||
effectFunction.context.find(operand => operand.identifier.id === dep) !=
|
||||
null ||
|
||||
(depMetadata !== undefined && depMetadata.typeOfValue !== 'ignored')
|
||||
) {
|
||||
// effect dep wasn't actually used in the function
|
||||
return;
|
||||
hasInvalidDep = true;
|
||||
}
|
||||
}
|
||||
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
|
||||
for (const dep of effectDeps) {
|
||||
values.set(dep, [dep]);
|
||||
if (!hasInvalidDep) {
|
||||
console.log('early return 2');
|
||||
// effect dep wasn't actually used in the function
|
||||
return;
|
||||
}
|
||||
|
||||
const setStateLocations: Array<SourceLocation> = [];
|
||||
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> = [];
|
||||
for (const block of effectFunction.body.blocks.values()) {
|
||||
for (const pred of block.preds) {
|
||||
if (!seenBlocks.has(pred)) {
|
||||
@@ -148,21 +451,29 @@ function validateEffect(
|
||||
return;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
) {
|
||||
if (effectSetStates.has(instr.value.callee.loc.identifierName)) {
|
||||
effectSetStates
|
||||
.get(instr.value.callee.loc.identifierName)!
|
||||
.push(instr.value.callee);
|
||||
} else {
|
||||
effectSetStates.set(instr.value.callee.loc.identifierName, [
|
||||
instr.value.callee,
|
||||
]);
|
||||
}
|
||||
}
|
||||
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':
|
||||
@@ -183,7 +494,7 @@ function validateEffect(
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
const aggregateDeps: Set<IdentifierId> = new Set();
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
const deps = values.get(operand.identifier.id);
|
||||
if (deps != null) {
|
||||
for (const dep of deps) {
|
||||
@@ -201,38 +512,63 @@ function validateEffect(
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
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 {
|
||||
// doesn't depend on any deps
|
||||
return;
|
||||
const invalidDeps = derivedTuple.get(
|
||||
instr.value.args[0].identifier.id,
|
||||
);
|
||||
|
||||
if (invalidDeps !== undefined) {
|
||||
setStateCallsInEffect.push({
|
||||
loc: instr.value.callee.loc,
|
||||
setStateId: instr.value.callee.identifier.id,
|
||||
invalidDeps: invalidDeps,
|
||||
});
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
for (const loc of setStateLocations) {
|
||||
for (const call of setStateCallsInEffect) {
|
||||
const placeNames = call.invalidDeps.sources
|
||||
.map(place => place.identifier.name?.value)
|
||||
.join(', ');
|
||||
|
||||
let sourceNames = '';
|
||||
let invalidDepInfo = '';
|
||||
console.log(call.invalidDeps);
|
||||
if (call.invalidDeps.typeOfValue === 'fromProps') {
|
||||
sourceNames += `[${placeNames}], `;
|
||||
sourceNames = sourceNames.slice(0, -2);
|
||||
invalidDepInfo = sourceNames
|
||||
? `Invalid deps from props ${sourceNames}`
|
||||
: '';
|
||||
} else if (call.invalidDeps.typeOfValue === 'fromState') {
|
||||
sourceNames += `[${placeNames}], `;
|
||||
sourceNames = sourceNames.slice(0, -2);
|
||||
invalidDepInfo = sourceNames
|
||||
? `Invalid deps from local state: ${sourceNames}`
|
||||
: '';
|
||||
}
|
||||
|
||||
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,
|
||||
errorType: call.invalidDeps.typeOfValue,
|
||||
invalidDepInfo: invalidDepInfo,
|
||||
loc: call.loc,
|
||||
setStateName:
|
||||
call.loc !== GeneratedSource ? call.loc.identifierName : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,13 +24,15 @@ function BadExample() {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
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: 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-computation-in-effect.ts:9:4
|
||||
7 | const [fullName, setFullName] = useState('');
|
||||
8 | useEffect(() => {
|
||||
> 9 | setFullName(capitalize(firstName + ' ' + lastName));
|
||||
| ^^^^^^^^^^^ 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)
|
||||
| ^^^^^^^^^^^ 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 | }, [firstName, lastName]);
|
||||
11 |
|
||||
12 | return <div>{fullName}</div>;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
|
||||
## 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>
|
||||
@@ -0,0 +1,14 @@
|
||||
// @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}],
|
||||
};
|
||||
@@ -0,0 +1,272 @@
|
||||
|
||||
## 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
|
||||
@@ -0,0 +1,53 @@
|
||||
// @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>',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
|
||||
## 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>
|
||||
@@ -0,0 +1,21 @@
|
||||
// @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'}],
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
|
||||
## 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>
|
||||
@@ -0,0 +1,21 @@
|
||||
// @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}],
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
|
||||
## 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']
|
||||
@@ -0,0 +1,19 @@
|
||||
// @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'}],
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
## 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 (
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// @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, '}],
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
## 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>;
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// @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'}],
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
## 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>;
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// @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'}],
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
|
||||
## 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 (
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// @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: [],
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
|
||||
## 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>
|
||||
@@ -0,0 +1,18 @@
|
||||
// @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: ']'}],
|
||||
};
|
||||
@@ -34,4 +34,8 @@ const configs = {
|
||||
},
|
||||
};
|
||||
|
||||
export {configs, allRules as rules, meta};
|
||||
const rules = Object.fromEntries(
|
||||
Object.entries(allRules).map(([name, {rule}]) => [name, rule]),
|
||||
);
|
||||
|
||||
export {configs, rules, meta};
|
||||
|
||||
@@ -21,6 +21,8 @@ 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';
|
||||
@@ -233,6 +235,11 @@ 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>
|
||||
|
||||
1115
fixtures/flight/src/LargeContent.js
Normal file
1115
fixtures/flight/src/LargeContent.js
Normal file
File diff suppressed because it is too large
Load Diff
391
packages/react-client/src/ReactFlightClient.js
vendored
391
packages/react-client/src/ReactFlightClient.js
vendored
@@ -366,6 +366,7 @@ 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
|
||||
@@ -822,6 +823,7 @@ type InitializationReference = {
|
||||
key: string,
|
||||
) => any,
|
||||
path: Array<string>,
|
||||
isDebug?: boolean, // DEV-only
|
||||
};
|
||||
type InitializationHandler = {
|
||||
parent: null | InitializationHandler,
|
||||
@@ -872,6 +874,7 @@ function initializeDebugChunk(
|
||||
response,
|
||||
initializeDebugInfo,
|
||||
[''], // path
|
||||
true,
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -894,6 +897,7 @@ function initializeDebugChunk(
|
||||
response,
|
||||
initializeDebugInfo,
|
||||
[''], // path
|
||||
true,
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -1407,8 +1411,6 @@ 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) {
|
||||
@@ -1427,19 +1429,27 @@ 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--;
|
||||
@@ -1518,6 +1528,7 @@ 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__ &&
|
||||
@@ -1562,6 +1573,9 @@ function waitForReference<T>(
|
||||
map,
|
||||
path,
|
||||
};
|
||||
if (__DEV__) {
|
||||
reference.isDebug = isAwaitingDebugInfo;
|
||||
}
|
||||
|
||||
// Add "listener".
|
||||
if (referencedChunk.value === null) {
|
||||
@@ -1794,13 +1808,21 @@ function transferReferencedDebugInfo(
|
||||
existingDebugInfo.push.apply(existingDebugInfo, referencedDebugInfo);
|
||||
}
|
||||
}
|
||||
// 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
|
||||
// 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
|
||||
// is extracted, or if the root is rendered as is.
|
||||
if (parentChunk !== null) {
|
||||
const parentDebugInfo = parentChunk._debugInfo;
|
||||
// $FlowFixMe[method-unbinding]
|
||||
parentDebugInfo.push.apply(parentDebugInfo, referencedDebugInfo);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1857,6 +1879,7 @@ function getOutlinedModel<T>(
|
||||
response,
|
||||
map,
|
||||
path.slice(i - 1),
|
||||
false,
|
||||
);
|
||||
}
|
||||
case HALTED: {
|
||||
@@ -1902,11 +1925,27 @@ function getOutlinedModel<T>(
|
||||
value = value[path[i]];
|
||||
}
|
||||
const chunkValue = map(response, value, parentObject, key);
|
||||
transferReferencedDebugInfo(initializingChunk, chunk, chunkValue);
|
||||
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);
|
||||
}
|
||||
return chunkValue;
|
||||
case PENDING:
|
||||
case BLOCKED:
|
||||
return waitForReference(chunk, parentObject, key, response, map, path);
|
||||
return waitForReference(
|
||||
chunk,
|
||||
parentObject,
|
||||
key,
|
||||
response,
|
||||
map,
|
||||
path,
|
||||
false,
|
||||
);
|
||||
case HALTED: {
|
||||
// Add a dependency that will never resolve.
|
||||
// TODO: Mark downstreams as halted too.
|
||||
@@ -2444,6 +2483,13 @@ 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;
|
||||
@@ -2512,16 +2558,99 @@ 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(): StreamState {
|
||||
return {
|
||||
export function createStreamState(
|
||||
weakResponse: WeakResponse, // DEV-only
|
||||
streamDebugValue: mixed, // DEV-only
|
||||
): StreamState {
|
||||
const streamState: StreamState = (({
|
||||
_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 {
|
||||
@@ -2545,17 +2674,33 @@ function resolveModel(
|
||||
response: Response,
|
||||
id: number,
|
||||
model: UninitializedModel,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
if (!chunk) {
|
||||
chunks.set(id, createResolvedModelChunk(response, model));
|
||||
const newChunk: ResolvedModelChunk<any> = createResolvedModelChunk(
|
||||
response,
|
||||
model,
|
||||
);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, chunk);
|
||||
}
|
||||
resolveModelChunk(response, chunk, model);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveText(response: Response, id: number, text: string): void {
|
||||
function resolveText(
|
||||
response: Response,
|
||||
id: number,
|
||||
text: string,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
if (chunk && chunk.status !== PENDING) {
|
||||
@@ -2569,13 +2714,18 @@ function resolveText(response: Response, id: number, text: string): void {
|
||||
if (chunk) {
|
||||
releasePendingChunk(response, chunk);
|
||||
}
|
||||
chunks.set(id, createInitializedTextChunk(response, text));
|
||||
const newChunk = createInitializedTextChunk(response, text);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
}
|
||||
|
||||
function resolveBuffer(
|
||||
response: Response,
|
||||
id: number,
|
||||
buffer: $ArrayBufferView | ArrayBuffer,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
@@ -2590,13 +2740,18 @@ function resolveBuffer(
|
||||
if (chunk) {
|
||||
releasePendingChunk(response, chunk);
|
||||
}
|
||||
chunks.set(id, createInitializedBufferChunk(response, buffer));
|
||||
const newChunk = createInitializedBufferChunk(response, buffer);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
}
|
||||
|
||||
function resolveModule(
|
||||
response: Response,
|
||||
id: number,
|
||||
model: UninitializedModel,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
@@ -2633,14 +2788,24 @@ 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) {
|
||||
chunks.set(id, createResolvedModuleChunk(response, clientReference));
|
||||
const newChunk = createResolvedModuleChunk(response, clientReference);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
} 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);
|
||||
@@ -2653,13 +2818,21 @@ 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) {
|
||||
chunks.set(id, createInitializedStreamChunk(response, stream, controller));
|
||||
const newChunk = createInitializedStreamChunk(response, stream, controller);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
return;
|
||||
}
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, chunk);
|
||||
}
|
||||
if (chunk.status !== PENDING) {
|
||||
// We already resolved. We didn't expect to see this.
|
||||
return;
|
||||
@@ -2715,6 +2888,7 @@ function startReadableStream<T>(
|
||||
response: Response,
|
||||
id: number,
|
||||
type: void | 'bytes',
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
let controller: ReadableStreamController = (null: any);
|
||||
const stream = new ReadableStream({
|
||||
@@ -2795,7 +2969,7 @@ function startReadableStream<T>(
|
||||
}
|
||||
},
|
||||
};
|
||||
resolveStream(response, id, stream, flightController);
|
||||
resolveStream(response, id, stream, flightController, streamState);
|
||||
}
|
||||
|
||||
function asyncIterator(this: $AsyncIterator<any, any, void>) {
|
||||
@@ -2821,6 +2995,7 @@ function startAsyncIterable<T>(
|
||||
response: Response,
|
||||
id: number,
|
||||
iterator: boolean,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
const buffer: Array<SomeChunk<IteratorResult<T, T>>> = [];
|
||||
let closed = false;
|
||||
@@ -2938,6 +3113,7 @@ function startAsyncIterable<T>(
|
||||
id,
|
||||
iterator ? iterable[ASYNC_ITERATOR]() : iterable,
|
||||
flightController,
|
||||
streamState,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3017,7 +3193,11 @@ function resolveErrorDev(
|
||||
return error;
|
||||
}
|
||||
|
||||
function resolvePostponeProd(response: Response, id: number): void {
|
||||
function resolvePostponeProd(
|
||||
response: Response,
|
||||
id: number,
|
||||
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
|
||||
// eslint-disable-next-line react-internal/prod-error-codes
|
||||
@@ -3035,7 +3215,11 @@ function resolvePostponeProd(response: Response, id: number): void {
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
if (!chunk) {
|
||||
chunks.set(id, createErrorChunk(response, postponeInstance));
|
||||
const newChunk: ErroredChunk<any> = createErrorChunk(
|
||||
response,
|
||||
postponeInstance,
|
||||
);
|
||||
chunks.set(id, newChunk);
|
||||
} else {
|
||||
triggerErrorOnChunk(response, chunk, postponeInstance);
|
||||
}
|
||||
@@ -3047,6 +3231,7 @@ 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
|
||||
@@ -3074,8 +3259,18 @@ function resolvePostponeDev(
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
if (!chunk) {
|
||||
chunks.set(id, createErrorChunk(response, postponeInstance));
|
||||
const newChunk: ErroredChunk<any> = createErrorChunk(
|
||||
response,
|
||||
postponeInstance,
|
||||
);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, chunk);
|
||||
}
|
||||
triggerErrorOnChunk(response, chunk, postponeInstance);
|
||||
}
|
||||
}
|
||||
@@ -3084,6 +3279,7 @@ function resolveErrorModel(
|
||||
response: Response,
|
||||
id: number,
|
||||
row: UninitializedModel,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
@@ -3097,8 +3293,18 @@ function resolveErrorModel(
|
||||
(error: any).digest = errorInfo.digest;
|
||||
const errorWithDigest: ErrorWithDigest = (error: any);
|
||||
if (!chunk) {
|
||||
chunks.set(id, createErrorChunk(response, errorWithDigest));
|
||||
const newChunk: ErroredChunk<any> = createErrorChunk(
|
||||
response,
|
||||
errorWithDigest,
|
||||
);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, chunk);
|
||||
}
|
||||
triggerErrorOnChunk(response, chunk, errorWithDigest);
|
||||
}
|
||||
}
|
||||
@@ -3833,6 +4039,7 @@ 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
|
||||
@@ -3852,7 +4059,7 @@ function resolveTypedArray(
|
||||
chunk.byteOffset,
|
||||
chunk.byteLength / bytesPerElement,
|
||||
);
|
||||
resolveBuffer(response, id, view);
|
||||
resolveBuffer(response, id, view, streamState);
|
||||
}
|
||||
|
||||
function logComponentInfo(
|
||||
@@ -4169,6 +4376,7 @@ function flushInitialRenderPerformance(response: Response): void {
|
||||
|
||||
function processFullBinaryRow(
|
||||
response: Response,
|
||||
streamState: StreamState,
|
||||
id: number,
|
||||
tag: number,
|
||||
buffer: Array<Uint8Array>,
|
||||
@@ -4177,47 +4385,125 @@ 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);
|
||||
resolveBuffer(
|
||||
response,
|
||||
id,
|
||||
mergeBuffer(buffer, chunk).buffer,
|
||||
streamState,
|
||||
);
|
||||
return;
|
||||
case 79 /* "O" */:
|
||||
resolveTypedArray(response, id, buffer, chunk, Int8Array, 1);
|
||||
resolveTypedArray(response, id, buffer, chunk, Int8Array, 1, streamState);
|
||||
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);
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
Uint8ClampedArray,
|
||||
1,
|
||||
streamState,
|
||||
);
|
||||
return;
|
||||
case 83 /* "S" */:
|
||||
resolveTypedArray(response, id, buffer, chunk, Int16Array, 2);
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
Int16Array,
|
||||
2,
|
||||
streamState,
|
||||
);
|
||||
return;
|
||||
case 115 /* "s" */:
|
||||
resolveTypedArray(response, id, buffer, chunk, Uint16Array, 2);
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
Uint16Array,
|
||||
2,
|
||||
streamState,
|
||||
);
|
||||
return;
|
||||
case 76 /* "L" */:
|
||||
resolveTypedArray(response, id, buffer, chunk, Int32Array, 4);
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
Int32Array,
|
||||
4,
|
||||
streamState,
|
||||
);
|
||||
return;
|
||||
case 108 /* "l" */:
|
||||
resolveTypedArray(response, id, buffer, chunk, Uint32Array, 4);
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
Uint32Array,
|
||||
4,
|
||||
streamState,
|
||||
);
|
||||
return;
|
||||
case 71 /* "G" */:
|
||||
resolveTypedArray(response, id, buffer, chunk, Float32Array, 4);
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
Float32Array,
|
||||
4,
|
||||
streamState,
|
||||
);
|
||||
return;
|
||||
case 103 /* "g" */:
|
||||
resolveTypedArray(response, id, buffer, chunk, Float64Array, 8);
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
Float64Array,
|
||||
8,
|
||||
streamState,
|
||||
);
|
||||
return;
|
||||
case 77 /* "M" */:
|
||||
resolveTypedArray(response, id, buffer, chunk, BigInt64Array, 8);
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
BigInt64Array,
|
||||
8,
|
||||
streamState,
|
||||
);
|
||||
return;
|
||||
case 109 /* "m" */:
|
||||
resolveTypedArray(response, id, buffer, chunk, BigUint64Array, 8);
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
BigUint64Array,
|
||||
8,
|
||||
streamState,
|
||||
);
|
||||
return;
|
||||
case 86 /* "V" */:
|
||||
resolveTypedArray(response, id, buffer, chunk, DataView, 1);
|
||||
resolveTypedArray(response, id, buffer, chunk, DataView, 1, streamState);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4227,18 +4513,19 @@ function processFullBinaryRow(
|
||||
row += readPartialStringChunk(stringDecoder, buffer[i]);
|
||||
}
|
||||
row += readFinalStringChunk(stringDecoder, chunk);
|
||||
processFullStringRow(response, id, tag, row);
|
||||
processFullStringRow(response, streamState, 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);
|
||||
resolveModule(response, id, row, streamState);
|
||||
return;
|
||||
}
|
||||
case 72 /* "H" */: {
|
||||
@@ -4247,11 +4534,11 @@ function processFullStringRow(
|
||||
return;
|
||||
}
|
||||
case 69 /* "E" */: {
|
||||
resolveErrorModel(response, id, row);
|
||||
resolveErrorModel(response, id, row, streamState);
|
||||
return;
|
||||
}
|
||||
case 84 /* "T" */: {
|
||||
resolveText(response, id, row);
|
||||
resolveText(response, id, row, streamState);
|
||||
return;
|
||||
}
|
||||
case 78 /* "N" */: {
|
||||
@@ -4296,22 +4583,22 @@ function processFullStringRow(
|
||||
);
|
||||
}
|
||||
case 82 /* "R" */: {
|
||||
startReadableStream(response, id, undefined);
|
||||
startReadableStream(response, id, undefined, streamState);
|
||||
return;
|
||||
}
|
||||
// Fallthrough
|
||||
case 114 /* "r" */: {
|
||||
startReadableStream(response, id, 'bytes');
|
||||
startReadableStream(response, id, 'bytes', streamState);
|
||||
return;
|
||||
}
|
||||
// Fallthrough
|
||||
case 88 /* "X" */: {
|
||||
startAsyncIterable(response, id, false);
|
||||
startAsyncIterable(response, id, false, streamState);
|
||||
return;
|
||||
}
|
||||
// Fallthrough
|
||||
case 120 /* "x" */: {
|
||||
startAsyncIterable(response, id, true);
|
||||
startAsyncIterable(response, id, true, streamState);
|
||||
return;
|
||||
}
|
||||
// Fallthrough
|
||||
@@ -4330,9 +4617,10 @@ function processFullStringRow(
|
||||
postponeInfo.reason,
|
||||
postponeInfo.stack,
|
||||
postponeInfo.env,
|
||||
streamState,
|
||||
);
|
||||
} else {
|
||||
resolvePostponeProd(response, id);
|
||||
resolvePostponeProd(response, id, streamState);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -4344,7 +4632,7 @@ function processFullStringRow(
|
||||
return;
|
||||
}
|
||||
// We assume anything else is JSON.
|
||||
resolveModel(response, id, row);
|
||||
resolveModel(response, id, row, streamState);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -4367,6 +4655,7 @@ 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) {
|
||||
@@ -4446,7 +4735,14 @@ 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, rowID, rowTag, buffer, lastChunk);
|
||||
processFullBinaryRow(
|
||||
response,
|
||||
streamState,
|
||||
rowID,
|
||||
rowTag,
|
||||
buffer,
|
||||
lastChunk,
|
||||
);
|
||||
// Reset state machine for a new row
|
||||
i = lastIdx;
|
||||
if (rowState === ROW_CHUNK_BY_NEWLINE) {
|
||||
@@ -4501,6 +4797,7 @@ 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) {
|
||||
@@ -4599,7 +4896,7 @@ export function processStringChunk(
|
||||
);
|
||||
}
|
||||
const lastChunk = chunk.slice(i, lastIdx);
|
||||
processFullStringRow(response, rowID, rowTag, lastChunk);
|
||||
processFullStringRow(response, streamState, rowID, rowTag, lastChunk);
|
||||
// Reset state machine for a new row
|
||||
i = lastIdx;
|
||||
if (rowState === ROW_CHUNK_BY_NEWLINE) {
|
||||
|
||||
@@ -85,7 +85,11 @@ function getDebugInfo(obj) {
|
||||
if (debugInfo) {
|
||||
const copy = [];
|
||||
for (let i = 0; i < debugInfo.length; i++) {
|
||||
copy.push(normalizeComponentInfo(debugInfo[i]));
|
||||
if (debugInfo[i].awaited && debugInfo[i].awaited.name === 'RSC stream') {
|
||||
// Ignore RSC stream I/O info.
|
||||
} else {
|
||||
copy.push(normalizeComponentInfo(debugInfo[i]));
|
||||
}
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
@@ -2832,7 +2836,7 @@ describe('ReactFlight', () => {
|
||||
transport: expect.arrayContaining([]),
|
||||
},
|
||||
},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 23 : 21},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 53 : 21},
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
@@ -2843,7 +2847,7 @@ describe('ReactFlight', () => {
|
||||
expect(getDebugInfo(thirdPartyChildren[0])).toEqual(
|
||||
__DEV__
|
||||
? [
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, // Clamped to the start
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22}, // Clamped to the start
|
||||
{
|
||||
name: 'ThirdPartyComponent',
|
||||
env: 'third-party',
|
||||
@@ -2851,15 +2855,15 @@ describe('ReactFlight', () => {
|
||||
stack: ' in Object.<anonymous> (at **)',
|
||||
props: {},
|
||||
},
|
||||
{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.
|
||||
{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.
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
expect(getDebugInfo(thirdPartyChildren[1])).toEqual(
|
||||
__DEV__
|
||||
? [
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, // Clamped to the start
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22}, // Clamped to the start
|
||||
{
|
||||
name: 'ThirdPartyLazyComponent',
|
||||
env: 'third-party',
|
||||
@@ -2867,14 +2871,14 @@ describe('ReactFlight', () => {
|
||||
stack: ' in myLazy (at **)\n in lazyInitializer (at **)',
|
||||
props: {},
|
||||
},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22},
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
expect(getDebugInfo(thirdPartyChildren[2])).toEqual(
|
||||
__DEV__
|
||||
? [
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22},
|
||||
{
|
||||
name: 'ThirdPartyFragmentComponent',
|
||||
env: 'third-party',
|
||||
@@ -2882,7 +2886,7 @@ describe('ReactFlight', () => {
|
||||
stack: ' in Object.<anonymous> (at **)',
|
||||
props: {},
|
||||
},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22},
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
@@ -2960,17 +2964,10 @@ describe('ReactFlight', () => {
|
||||
{
|
||||
time: 16,
|
||||
},
|
||||
{
|
||||
env: 'third-party',
|
||||
key: null,
|
||||
name: 'ThirdPartyAsyncIterableComponent',
|
||||
props: {},
|
||||
stack: ' in Object.<anonymous> (at **)',
|
||||
},
|
||||
{
|
||||
time: 16,
|
||||
},
|
||||
{time: 17},
|
||||
{time: 31},
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
@@ -2979,7 +2976,7 @@ describe('ReactFlight', () => {
|
||||
expect(getDebugInfo(thirdPartyFragment)).toEqual(
|
||||
__DEV__
|
||||
? [
|
||||
{time: 18},
|
||||
{time: 32},
|
||||
{
|
||||
name: 'Keyed',
|
||||
env: 'Server',
|
||||
@@ -2990,19 +2987,14 @@ describe('ReactFlight', () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
time: 19,
|
||||
time: 33,
|
||||
},
|
||||
{
|
||||
time: 19,
|
||||
time: 33,
|
||||
},
|
||||
{
|
||||
env: 'third-party',
|
||||
key: null,
|
||||
name: 'ThirdPartyAsyncIterableComponent',
|
||||
props: {},
|
||||
stack: ' in Object.<anonymous> (at **)',
|
||||
time: 33,
|
||||
},
|
||||
{time: 19},
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
@@ -3010,7 +3002,7 @@ describe('ReactFlight', () => {
|
||||
expect(getDebugInfo(thirdPartyFragment.props.children)).toEqual(
|
||||
__DEV__
|
||||
? [
|
||||
{time: 19}, // Clamp to the start
|
||||
{time: 33}, // Clamp to the start
|
||||
{
|
||||
name: 'ThirdPartyAsyncIterableComponent',
|
||||
env: 'third-party',
|
||||
@@ -3018,7 +3010,7 @@ describe('ReactFlight', () => {
|
||||
stack: ' in Object.<anonymous> (at **)',
|
||||
props: {},
|
||||
},
|
||||
{time: 19},
|
||||
{time: 33},
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
@@ -3081,7 +3073,7 @@ describe('ReactFlight', () => {
|
||||
props: {},
|
||||
},
|
||||
{time: 16},
|
||||
{time: 17},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 17},
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
@@ -3847,4 +3839,115 @@ 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>,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,13 +5,19 @@
|
||||
* 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 = {
|
||||
@@ -22,7 +28,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;
|
||||
@@ -55,15 +61,23 @@ 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,
|
||||
bridge: Bridge;
|
||||
store: Store;
|
||||
theme?: BrowserTheme;
|
||||
viewAttributeSourceFunction?: ViewAttributeSource;
|
||||
viewElementSourceFunction?: ViewElementSource;
|
||||
canViewElementSourceFunction?: CanViewElementSource;
|
||||
fetchFileWithCaching?: FetchFileWithCaching;
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
@@ -24,6 +24,7 @@ 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 {
|
||||
@@ -50,6 +51,7 @@ type InitializationOptions = {
|
||||
viewAttributeSourceFunction?: ViewAttributeSource,
|
||||
viewElementSourceFunction?: ViewElementSource,
|
||||
canViewElementSourceFunction?: CanViewElementSource,
|
||||
fetchFileWithCaching?: FetchFileWithCaching,
|
||||
};
|
||||
|
||||
function initializeTab(
|
||||
@@ -64,6 +66,7 @@ function initializeTab(
|
||||
viewAttributeSourceFunction,
|
||||
viewElementSourceFunction,
|
||||
canViewElementSourceFunction,
|
||||
fetchFileWithCaching,
|
||||
} = options;
|
||||
const root = createRoot(contentWindow);
|
||||
|
||||
@@ -79,6 +82,7 @@ function initializeTab(
|
||||
viewAttributeSourceFunction={viewAttributeSourceFunction}
|
||||
viewElementSourceFunction={viewElementSourceFunction}
|
||||
canViewElementSourceFunction={canViewElementSourceFunction}
|
||||
fetchFileWithCaching={fetchFileWithCaching}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -228,8 +228,8 @@ describe('commit tree', () => {
|
||||
[root]
|
||||
▾ <App>
|
||||
<Suspense>
|
||||
[shell]
|
||||
<Suspense name="App>?" rects={null}>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
utils.act(() => modernRender(<App renderChildren={true} />));
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
@@ -237,8 +237,8 @@ describe('commit tree', () => {
|
||||
▾ <App>
|
||||
▾ <Suspense>
|
||||
<LazyInnerComponent>
|
||||
[shell]
|
||||
<Suspense name="App>?" rects={null}>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
utils.act(() => modernRender(<App renderChildren={false} />));
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
@@ -303,8 +303,8 @@ describe('commit tree', () => {
|
||||
[root]
|
||||
▾ <App>
|
||||
<Suspense>
|
||||
[shell]
|
||||
<Suspense name="App>?" rects={null}>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
utils.act(() => modernRender(<App renderChildren={false} />));
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
|
||||
@@ -133,7 +133,7 @@ describe('Store', () => {
|
||||
<Suspense>
|
||||
▾ <Parent>
|
||||
<Child>
|
||||
[shell]
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`);
|
||||
});
|
||||
@@ -492,8 +492,8 @@ describe('Store', () => {
|
||||
<Component key="Outside">
|
||||
▾ <Suspense>
|
||||
<Loading>
|
||||
[shell]
|
||||
<Suspense name="Wrapper>?" rects={null}>
|
||||
[suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:10,height:1}]}
|
||||
<Suspense name="Wrapper" rects={null}>
|
||||
`);
|
||||
|
||||
await act(() => {
|
||||
@@ -505,8 +505,8 @@ describe('Store', () => {
|
||||
<Component key="Outside">
|
||||
▾ <Suspense>
|
||||
<Component key="Inside">
|
||||
[shell]
|
||||
<Suspense name="Wrapper>?" rects={[{x:1,y:2,width:5,height:1}]}>
|
||||
[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}]}>
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -584,7 +584,7 @@ describe('Store', () => {
|
||||
▾ <Suspense name="three">
|
||||
<Loading key="Suspense 3 Fallback">
|
||||
<Component key="Unrelated at End">
|
||||
[shell]
|
||||
[suspense-root] rects={[{x:1,y:2,width:10,height:1}]}
|
||||
<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">
|
||||
[shell]
|
||||
[suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}
|
||||
<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">
|
||||
[shell]
|
||||
[suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}
|
||||
<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">
|
||||
[shell]
|
||||
[suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}
|
||||
<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">
|
||||
[shell]
|
||||
[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}]}
|
||||
<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">
|
||||
[shell]
|
||||
[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}]}
|
||||
<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">
|
||||
[shell]
|
||||
[suspense-root] rects={[{x:1,y:2,width:10,height:1}]}
|
||||
<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">
|
||||
[shell]
|
||||
[suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}
|
||||
<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">
|
||||
[shell]
|
||||
[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}]}
|
||||
<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">
|
||||
[shell]
|
||||
[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}]}
|
||||
<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">
|
||||
[shell]
|
||||
[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}]}
|
||||
<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">
|
||||
[shell]
|
||||
[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}]}
|
||||
<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">
|
||||
[shell]
|
||||
[suspense-root] rects={[{x:1,y:2,width:10,height:1}]}
|
||||
<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">
|
||||
[shell]
|
||||
[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}]}
|
||||
<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">
|
||||
[shell]
|
||||
[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}]}
|
||||
<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">
|
||||
[shell]
|
||||
[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}]}
|
||||
<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>
|
||||
[shell]
|
||||
<Suspense name="Wrapper>?" rects={null}>
|
||||
[suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:10,height:1}]}
|
||||
<Suspense name="Wrapper" rects={null}>
|
||||
`);
|
||||
|
||||
await act(() => {
|
||||
@@ -1068,8 +1068,8 @@ describe('Store', () => {
|
||||
▾ <Suspense>
|
||||
<Component key="B">
|
||||
<Component key="C">
|
||||
[shell]
|
||||
<Suspense name="Wrapper>?" rects={[{x:1,y:2,width:5,height:1}]}>
|
||||
[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}]}>
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -1406,8 +1406,8 @@ describe('Store', () => {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▸ <Wrapper>
|
||||
[shell]
|
||||
<Suspense name="Wrapper>?" rects={null}>
|
||||
[suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:10,height:1}]}
|
||||
<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>
|
||||
[shell]
|
||||
<Suspense name="Wrapper>?" rects={null}>
|
||||
[suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:10,height:1}]}
|
||||
<Suspense name="Wrapper" rects={null}>
|
||||
`);
|
||||
|
||||
await act(() => {
|
||||
@@ -1436,8 +1436,8 @@ describe('Store', () => {
|
||||
<Component key="Outside">
|
||||
▾ <Suspense>
|
||||
<Component key="Inside">
|
||||
[shell]
|
||||
<Suspense name="Wrapper>?" rects={[{x:1,y:2,width:5,height:1}]}>
|
||||
[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}]}>
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -1662,8 +1662,8 @@ describe('Store', () => {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▸ <SuspenseTree>
|
||||
[shell]
|
||||
<Suspense name="SuspenseTree>?" rects={null}>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="SuspenseTree" rects={null}>
|
||||
`);
|
||||
|
||||
await act(() =>
|
||||
@@ -1677,8 +1677,8 @@ describe('Store', () => {
|
||||
▾ <SuspenseTree>
|
||||
▾ <Suspense>
|
||||
▸ <Parent>
|
||||
[shell]
|
||||
<Suspense name="SuspenseTree>?" rects={null}>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="SuspenseTree" rects={null}>
|
||||
`);
|
||||
|
||||
const rendererID = getRendererID();
|
||||
@@ -1696,8 +1696,8 @@ describe('Store', () => {
|
||||
▾ <SuspenseTree>
|
||||
▾ <Suspense>
|
||||
<Fallback>
|
||||
[shell]
|
||||
<Suspense name="SuspenseTree>?" rects={null}>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="SuspenseTree" rects={null}>
|
||||
`);
|
||||
|
||||
await act(() =>
|
||||
@@ -1712,8 +1712,8 @@ describe('Store', () => {
|
||||
▾ <SuspenseTree>
|
||||
▾ <Suspense>
|
||||
▸ <Parent>
|
||||
[shell]
|
||||
<Suspense name="SuspenseTree>?" rects={null}>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="SuspenseTree" rects={null}>
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -2017,8 +2017,8 @@ describe('Store', () => {
|
||||
[root]
|
||||
▾ <App>
|
||||
<Suspense>
|
||||
[shell]
|
||||
<Suspense name="App>?" rects={null}>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
|
||||
await Promise.resolve();
|
||||
@@ -2031,8 +2031,8 @@ describe('Store', () => {
|
||||
▾ <App>
|
||||
▾ <Suspense>
|
||||
<LazyInnerComponent>
|
||||
[shell]
|
||||
<Suspense name="App>?" rects={null}>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
|
||||
// Render again to unmount it
|
||||
@@ -2522,8 +2522,8 @@ describe('Store', () => {
|
||||
▾ <App>
|
||||
▾ <Suspense>
|
||||
<ChildA>
|
||||
[shell]
|
||||
<Suspense name="App>?" rects={null}>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
|
||||
await actAsync(() => render(<App renderA={false} />));
|
||||
@@ -2533,8 +2533,8 @@ describe('Store', () => {
|
||||
▾ <App>
|
||||
▾ <Suspense>
|
||||
<ChildB>
|
||||
[shell]
|
||||
<Suspense name="App>?" rects={null}>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -2860,7 +2860,7 @@ describe('Store', () => {
|
||||
▾ <Suspense name="content">
|
||||
▾ <Suspense name="fallback">
|
||||
<Component key="fallback-fallback">
|
||||
[shell]
|
||||
[suspense-root] rects={[{x:1,y:2,width:19,height:1}]}
|
||||
<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">
|
||||
[shell]
|
||||
[suspense-root] rects={[{x:1,y:2,width:10,height:1}]}
|
||||
<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">
|
||||
[shell]
|
||||
[suspense-root] rects={[{x:1,y:2,width:4,height:1}]}
|
||||
<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">
|
||||
[shell]
|
||||
[suspense-root] rects={[{x:1,y:2,width:19,height:1}, {x:1,y:2,width:19,height:1}]}
|
||||
<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">
|
||||
[shell]
|
||||
[suspense-root] rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:4,height:1}]}
|
||||
<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">
|
||||
[shell]
|
||||
[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}]}
|
||||
<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,10 +3074,15 @@ describe('Store', () => {
|
||||
▾ <WithSuspenseInFallback>
|
||||
▾ <Suspense name="main">
|
||||
<Component key="main-content">
|
||||
[shell]
|
||||
[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}]}
|
||||
<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]`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -156,7 +156,7 @@ describe('Store component filters', () => {
|
||||
<div>
|
||||
▾ <Suspense>
|
||||
<div>
|
||||
[shell]
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`);
|
||||
@@ -174,7 +174,7 @@ describe('Store component filters', () => {
|
||||
<div>
|
||||
▾ <Suspense>
|
||||
<div>
|
||||
[shell]
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`);
|
||||
@@ -192,7 +192,7 @@ describe('Store component filters', () => {
|
||||
<div>
|
||||
▾ <Suspense>
|
||||
<div>
|
||||
[shell]
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
`);
|
||||
|
||||
@@ -1368,9 +1368,9 @@ describe('TreeListContext', () => {
|
||||
▾ <Child>
|
||||
▾ <Suspense>
|
||||
<Grandchild>
|
||||
[shell]
|
||||
<Suspense name="Parent>?" rects={null}>
|
||||
<Suspense name="Child>?" rects={null}>
|
||||
[suspense-root] rects={null}
|
||||
<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>
|
||||
[shell]
|
||||
<Suspense name="Parent>?" rects={null}>
|
||||
<Suspense name="Child>?" rects={null}>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="Parent" rects={null}>
|
||||
<Suspense name="Child" rects={null}>
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -2369,7 +2369,7 @@ describe('TreeListContext', () => {
|
||||
expect(state).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
<Suspense>
|
||||
[shell]
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`);
|
||||
|
||||
@@ -2378,7 +2378,7 @@ describe('TreeListContext', () => {
|
||||
expect(state).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
<Suspense>
|
||||
[shell]
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`);
|
||||
});
|
||||
@@ -2404,7 +2404,7 @@ describe('TreeListContext', () => {
|
||||
expect(state).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
<Suspense>
|
||||
[shell]
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`);
|
||||
|
||||
@@ -2426,7 +2426,7 @@ describe('TreeListContext', () => {
|
||||
▾ <Suspense>
|
||||
<Child> ⚠
|
||||
<Child>
|
||||
[shell]
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`);
|
||||
});
|
||||
@@ -2456,7 +2456,7 @@ describe('TreeListContext', () => {
|
||||
▾ <Suspense>
|
||||
▾ <Fallback>
|
||||
<Child> ✕
|
||||
[shell]
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`);
|
||||
|
||||
@@ -2475,7 +2475,7 @@ describe('TreeListContext', () => {
|
||||
[root]
|
||||
▾ <Suspense>
|
||||
<Child>
|
||||
[shell]
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="Unknown" rects={null}>
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -3700,28 +3700,32 @@ 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 (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.',
|
||||
);
|
||||
if (fiber.tag === SuspenseComponent) {
|
||||
if (OffscreenComponent === -1) {
|
||||
const isTimedOut = fiber.memoizedState !== null;
|
||||
if (!isTimedOut) {
|
||||
newSuspenseNode.rects = measureInstance(newInstance);
|
||||
}
|
||||
} 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newSuspenseNode.rects = measureInstance(newInstance);
|
||||
}
|
||||
recordSuspenseMount(newSuspenseNode, reconcilingParentSuspenseNode);
|
||||
}
|
||||
|
||||
@@ -1950,14 +1950,13 @@ export default class Store extends EventEmitter<{
|
||||
throw error;
|
||||
}
|
||||
|
||||
_guessSuspenseName(element: Element): string | null {
|
||||
// TODO: Use key
|
||||
_guessSuspenseName(element: Element): string {
|
||||
const owner = this._idToElement.get(element.ownerID);
|
||||
if (owner !== undefined) {
|
||||
// TODO: This is clowny
|
||||
return `${owner.displayName || 'Unknown'}>?`;
|
||||
let name = 'Unknown';
|
||||
if (owner !== undefined && owner.displayName !== null) {
|
||||
name = owner.displayName;
|
||||
}
|
||||
|
||||
return null;
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,22 +54,21 @@ export function printElement(
|
||||
}${key}${name}>${hocs}${suffix}`;
|
||||
}
|
||||
|
||||
function printSuspense(
|
||||
suspense: SuspenseNode,
|
||||
includeWeight: boolean = false,
|
||||
): string {
|
||||
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 {
|
||||
let name = '';
|
||||
if (suspense.name !== null) {
|
||||
name = ` name="${suspense.name}"`;
|
||||
}
|
||||
|
||||
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(', ')}]}`;
|
||||
}
|
||||
const printedRects = printRects(suspense.rects);
|
||||
|
||||
return `<Suspense${name}${printedRects}>`;
|
||||
}
|
||||
@@ -178,13 +177,13 @@ export function printStore(
|
||||
rootWeight += weight;
|
||||
|
||||
if (includeSuspense) {
|
||||
const shell = store.getSuspenseByID(rootID);
|
||||
const root = store.getSuspenseByID(rootID);
|
||||
// Roots from legacy renderers don't have a separate Suspense tree
|
||||
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];
|
||||
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];
|
||||
const child = store.getSuspenseByID(childID);
|
||||
if (child === null) {
|
||||
throw new Error(
|
||||
|
||||
24
packages/react-markup/src/ReactMarkupServer.js
vendored
24
packages/react-markup/src/ReactMarkupServer.js
vendored
@@ -81,7 +81,18 @@ export function experimental_renderToHTML(
|
||||
options?: MarkupOptions,
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const streamState = createFlightStreamState();
|
||||
const flightResponse = createFlightResponse(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
noServerCallOrFormAction,
|
||||
noServerCallOrFormAction,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
const streamState = createFlightStreamState(flightResponse, null);
|
||||
const flightDestination = {
|
||||
push(chunk: string | null): boolean {
|
||||
if (chunk !== null) {
|
||||
@@ -175,17 +186,6 @@ 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,
|
||||
|
||||
@@ -77,7 +77,7 @@ function read<T>(source: Source, options: ReadOptions): Thenable<T> {
|
||||
? options.debugChannel.onMessage
|
||||
: undefined,
|
||||
);
|
||||
const streamState = createStreamState();
|
||||
const streamState = createStreamState(response, source);
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
processBinaryChunk(response, streamState, source[i], 0);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
const streamState = createStreamState(response, stream);
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -149,8 +149,9 @@ function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
onDone: () => void,
|
||||
debugValue: mixed,
|
||||
): void {
|
||||
const streamState = createStreamState();
|
||||
const streamState = createStreamState(response, debugValue);
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -194,9 +195,14 @@ function createFromReadableStream<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, stream, handleDone);
|
||||
startReadingFromStream(response, stream, handleDone, stream);
|
||||
} else {
|
||||
startReadingFromStream(response, stream, close.bind(null, response));
|
||||
startReadingFromStream(
|
||||
response,
|
||||
stream,
|
||||
close.bind(null, response),
|
||||
stream,
|
||||
);
|
||||
}
|
||||
return getRoot(response);
|
||||
}
|
||||
@@ -225,12 +231,13 @@ function createFromFetch<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, (r.body: any), handleDone);
|
||||
startReadingFromStream(response, (r.body: any), handleDone, r);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
(r.body: any),
|
||||
close.bind(null, response),
|
||||
r,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -66,7 +66,7 @@ function startReadingFromStream(
|
||||
stream: Readable,
|
||||
onEnd: () => void,
|
||||
): void {
|
||||
const streamState = createStreamState();
|
||||
const streamState = createStreamState(response, stream);
|
||||
|
||||
stream.on('data', chunk => {
|
||||
if (typeof chunk === 'string') {
|
||||
|
||||
@@ -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();
|
||||
const streamState = createStreamState(response, stream);
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -175,8 +175,9 @@ function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
onDone: () => void,
|
||||
debugValue: mixed,
|
||||
): void {
|
||||
const streamState = createStreamState();
|
||||
const streamState = createStreamState(response, debugValue);
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -228,9 +229,14 @@ export function createFromReadableStream<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, stream, handleDone);
|
||||
startReadingFromStream(response, stream, handleDone, stream);
|
||||
} else {
|
||||
startReadingFromStream(response, stream, close.bind(null, response));
|
||||
startReadingFromStream(
|
||||
response,
|
||||
stream,
|
||||
close.bind(null, response),
|
||||
stream,
|
||||
);
|
||||
}
|
||||
return getRoot(response);
|
||||
}
|
||||
@@ -259,12 +265,13 @@ export function createFromFetch<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, (r.body: any), handleDone);
|
||||
startReadingFromStream(response, (r.body: any), handleDone, r);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
(r.body: any),
|
||||
close.bind(null, response),
|
||||
r,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -115,8 +115,9 @@ function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
onDone: () => void,
|
||||
debugValue: mixed,
|
||||
): void {
|
||||
const streamState = createStreamState();
|
||||
const streamState = createStreamState(response, debugValue);
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -158,9 +159,14 @@ export function createFromReadableStream<T>(
|
||||
}
|
||||
};
|
||||
startReadingFromStream(response, options.debugChannel.readable, handleDone);
|
||||
startReadingFromStream(response, stream, handleDone);
|
||||
startReadingFromStream(response, stream, handleDone, stream);
|
||||
} else {
|
||||
startReadingFromStream(response, stream, close.bind(null, response));
|
||||
startReadingFromStream(
|
||||
response,
|
||||
stream,
|
||||
close.bind(null, response),
|
||||
stream,
|
||||
);
|
||||
}
|
||||
|
||||
return getRoot(response);
|
||||
@@ -190,12 +196,13 @@ export function createFromFetch<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, (r.body: any), handleDone);
|
||||
startReadingFromStream(response, (r.body: any), handleDone, r);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
(r.body: any),
|
||||
close.bind(null, response),
|
||||
r,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -61,7 +61,7 @@ function startReadingFromStream(
|
||||
stream: Readable,
|
||||
onEnd: () => void,
|
||||
): void {
|
||||
const streamState = createStreamState();
|
||||
const streamState = createStreamState(response, stream);
|
||||
|
||||
stream.on('data', chunk => {
|
||||
if (typeof chunk === 'string') {
|
||||
|
||||
@@ -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();
|
||||
const streamState = createStreamState(response, stream);
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -148,8 +148,9 @@ function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
onDone: () => void,
|
||||
debugValue: mixed,
|
||||
): void {
|
||||
const streamState = createStreamState();
|
||||
const streamState = createStreamState(response, debugValue);
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -194,9 +195,14 @@ function createFromReadableStream<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, stream, handleDone);
|
||||
startReadingFromStream(response, stream, handleDone, stream);
|
||||
} else {
|
||||
startReadingFromStream(response, stream, close.bind(null, response));
|
||||
startReadingFromStream(
|
||||
response,
|
||||
stream,
|
||||
close.bind(null, response),
|
||||
stream,
|
||||
);
|
||||
}
|
||||
return getRoot(response);
|
||||
}
|
||||
@@ -225,12 +231,13 @@ function createFromFetch<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, (r.body: any), handleDone);
|
||||
startReadingFromStream(response, (r.body: any), handleDone, r);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
(r.body: any),
|
||||
close.bind(null, response),
|
||||
r,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -117,8 +117,9 @@ function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
onDone: () => void,
|
||||
debugValue: mixed,
|
||||
): void {
|
||||
const streamState = createStreamState();
|
||||
const streamState = createStreamState(response, debugValue);
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -160,9 +161,14 @@ function createFromReadableStream<T>(
|
||||
}
|
||||
};
|
||||
startReadingFromStream(response, options.debugChannel.readable, handleDone);
|
||||
startReadingFromStream(response, stream, handleDone);
|
||||
startReadingFromStream(response, stream, handleDone, stream);
|
||||
} else {
|
||||
startReadingFromStream(response, stream, close.bind(null, response));
|
||||
startReadingFromStream(
|
||||
response,
|
||||
stream,
|
||||
close.bind(null, response),
|
||||
stream,
|
||||
);
|
||||
}
|
||||
|
||||
return getRoot(response);
|
||||
@@ -192,12 +198,13 @@ function createFromFetch<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, (r.body: any), handleDone);
|
||||
startReadingFromStream(response, (r.body: any), handleDone, r);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
(r.body: any),
|
||||
close.bind(null, response),
|
||||
r,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -69,7 +69,7 @@ function startReadingFromStream(
|
||||
stream: Readable,
|
||||
onEnd: () => void,
|
||||
): void {
|
||||
const streamState = createStreamState();
|
||||
const streamState = createStreamState(response, stream);
|
||||
|
||||
stream.on('data', chunk => {
|
||||
if (typeof chunk === 'string') {
|
||||
|
||||
@@ -1239,17 +1239,24 @@ describe('ReactFlightDOMEdge', () => {
|
||||
name: 'Greeting',
|
||||
env: 'Server',
|
||||
});
|
||||
expect(lazyWrapper._debugInfo).toEqual([
|
||||
{time: 12},
|
||||
greetInfo,
|
||||
{time: 13},
|
||||
expect.objectContaining({
|
||||
name: 'Container',
|
||||
env: 'Server',
|
||||
owner: greetInfo,
|
||||
}),
|
||||
{time: 14},
|
||||
]);
|
||||
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',
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
}
|
||||
// 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]);
|
||||
|
||||
@@ -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();
|
||||
const streamState = createStreamState(response, stream);
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -148,8 +148,9 @@ function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
onDone: () => void,
|
||||
debugValue: mixed,
|
||||
): void {
|
||||
const streamState = createStreamState();
|
||||
const streamState = createStreamState(response, debugValue);
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -194,9 +195,14 @@ function createFromReadableStream<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, stream, handleDone);
|
||||
startReadingFromStream(response, stream, handleDone, stream);
|
||||
} else {
|
||||
startReadingFromStream(response, stream, close.bind(null, response));
|
||||
startReadingFromStream(
|
||||
response,
|
||||
stream,
|
||||
close.bind(null, response),
|
||||
stream,
|
||||
);
|
||||
}
|
||||
return getRoot(response);
|
||||
}
|
||||
@@ -225,12 +231,13 @@ function createFromFetch<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, (r.body: any), handleDone);
|
||||
startReadingFromStream(response, (r.body: any), handleDone, r);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
(r.body: any),
|
||||
close.bind(null, response),
|
||||
r,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -117,8 +117,9 @@ function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
onDone: () => void,
|
||||
debugValue: mixed,
|
||||
): void {
|
||||
const streamState = createStreamState();
|
||||
const streamState = createStreamState(response, debugValue);
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -160,9 +161,14 @@ function createFromReadableStream<T>(
|
||||
}
|
||||
};
|
||||
startReadingFromStream(response, options.debugChannel.readable, handleDone);
|
||||
startReadingFromStream(response, stream, handleDone);
|
||||
startReadingFromStream(response, stream, handleDone, stream);
|
||||
} else {
|
||||
startReadingFromStream(response, stream, close.bind(null, response));
|
||||
startReadingFromStream(
|
||||
response,
|
||||
stream,
|
||||
close.bind(null, response),
|
||||
stream,
|
||||
);
|
||||
}
|
||||
|
||||
return getRoot(response);
|
||||
@@ -192,12 +198,13 @@ function createFromFetch<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, (r.body: any), handleDone);
|
||||
startReadingFromStream(response, (r.body: any), handleDone, r);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
(r.body: any),
|
||||
close.bind(null, response),
|
||||
r,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -69,7 +69,7 @@ function startReadingFromStream(
|
||||
stream: Readable,
|
||||
onEnd: () => void,
|
||||
): void {
|
||||
const streamState = createStreamState();
|
||||
const streamState = createStreamState(response, stream);
|
||||
|
||||
stream.on('data', chunk => {
|
||||
if (typeof chunk === 'string') {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user