Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7455473e3a |
@@ -325,15 +325,6 @@ function runWithEnvironment(
|
||||
outlineJSX(hir);
|
||||
}
|
||||
|
||||
if (env.config.enableNameAnonymousFunctions) {
|
||||
nameAnonymousFunctions(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'NameAnonymousFunctions',
|
||||
value: hir,
|
||||
});
|
||||
}
|
||||
|
||||
if (env.config.enableFunctionOutlining) {
|
||||
outlineFunctions(hir, fbtOperands);
|
||||
log({kind: 'hir', name: 'OutlineFunctions', value: hir});
|
||||
@@ -424,6 +415,15 @@ function runWithEnvironment(
|
||||
});
|
||||
}
|
||||
|
||||
if (env.config.enableNameAnonymousFunctions) {
|
||||
nameAnonymousFunctions(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'NameAnonymougFunctions',
|
||||
value: hir,
|
||||
});
|
||||
}
|
||||
|
||||
const reactiveFunction = buildReactiveFunction(hir);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
|
||||
@@ -47,7 +47,6 @@ import {
|
||||
makePropertyLiteral,
|
||||
makeType,
|
||||
promoteTemporary,
|
||||
validateIdentifierName,
|
||||
} from './HIR';
|
||||
import HIRBuilder, {Bindings, createTemporaryPlace} from './HIRBuilder';
|
||||
import {BuiltInArrayId} from './ObjectShape';
|
||||
@@ -214,16 +213,6 @@ export function lower(
|
||||
);
|
||||
}
|
||||
|
||||
let validatedId: HIRFunction['id'] = null;
|
||||
if (id != null) {
|
||||
const idResult = validateIdentifierName(id);
|
||||
if (idResult.isErr()) {
|
||||
builder.errors.merge(idResult.unwrapErr());
|
||||
} else {
|
||||
validatedId = idResult.unwrap().value;
|
||||
}
|
||||
}
|
||||
|
||||
if (builder.errors.hasAnyErrors()) {
|
||||
return Err(builder.errors);
|
||||
}
|
||||
@@ -245,8 +234,7 @@ export function lower(
|
||||
);
|
||||
|
||||
return Ok({
|
||||
id: validatedId,
|
||||
nameHint: null,
|
||||
id,
|
||||
params,
|
||||
fnType: bindings == null ? env.fnType : 'Other',
|
||||
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
|
||||
@@ -3575,14 +3563,19 @@ function lowerFunctionToValue(
|
||||
): InstructionValue {
|
||||
const exprNode = expr.node;
|
||||
const exprLoc = exprNode.loc ?? GeneratedSource;
|
||||
let name: string | null = null;
|
||||
if (expr.isFunctionExpression()) {
|
||||
name = expr.get('id')?.node?.name ?? null;
|
||||
} else if (expr.isFunctionDeclaration()) {
|
||||
name = expr.get('id')?.node?.name ?? null;
|
||||
}
|
||||
const loweredFunc = lowerFunction(builder, expr);
|
||||
if (!loweredFunc) {
|
||||
return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
|
||||
}
|
||||
return {
|
||||
kind: 'FunctionExpression',
|
||||
name: loweredFunc.func.id,
|
||||
nameHint: null,
|
||||
name,
|
||||
type: expr.node.type,
|
||||
loc: exprLoc,
|
||||
loweredFunc,
|
||||
|
||||
@@ -7,11 +7,7 @@
|
||||
|
||||
import {BindingKind} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {Environment, ReactFunctionType} from './Environment';
|
||||
import type {HookKind} from './ObjectShape';
|
||||
@@ -58,8 +54,7 @@ export type SourceLocation = t.SourceLocation | typeof GeneratedSource;
|
||||
*/
|
||||
export type ReactiveFunction = {
|
||||
loc: SourceLocation;
|
||||
id: ValidIdentifierName | null;
|
||||
nameHint: string | null;
|
||||
id: string | null;
|
||||
params: Array<Place | SpreadPattern>;
|
||||
generator: boolean;
|
||||
async: boolean;
|
||||
@@ -281,8 +276,7 @@ export type ReactiveTryTerminal = {
|
||||
// A function lowered to HIR form, ie where its body is lowered to an HIR control-flow graph
|
||||
export type HIRFunction = {
|
||||
loc: SourceLocation;
|
||||
id: ValidIdentifierName | null;
|
||||
nameHint: string | null;
|
||||
id: string | null;
|
||||
fnType: ReactFunctionType;
|
||||
env: Environment;
|
||||
params: Array<Place | SpreadPattern>;
|
||||
@@ -1130,8 +1124,7 @@ export type JsxAttribute =
|
||||
|
||||
export type FunctionExpression = {
|
||||
kind: 'FunctionExpression';
|
||||
name: ValidIdentifierName | null;
|
||||
nameHint: string | null;
|
||||
name: string | null;
|
||||
loweredFunc: LoweredFunction;
|
||||
type:
|
||||
| 'ArrowFunctionExpression'
|
||||
@@ -1308,41 +1301,11 @@ export function forkTemporaryIdentifier(
|
||||
|
||||
export function validateIdentifierName(
|
||||
name: string,
|
||||
): Result<ValidatedIdentifier, CompilerError> {
|
||||
if (isReservedWord(name)) {
|
||||
const error = new CompilerError();
|
||||
error.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Syntax,
|
||||
reason: 'Expected a non-reserved identifier name',
|
||||
description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: 'reserved word',
|
||||
}),
|
||||
);
|
||||
return Err(error);
|
||||
} else if (!t.isValidIdentifier(name)) {
|
||||
const error = new CompilerError();
|
||||
error.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Syntax,
|
||||
reason: `Expected a valid identifier name`,
|
||||
description: `\`${name}\` is not a valid JavaScript identifier`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: 'reserved word',
|
||||
}),
|
||||
);
|
||||
): Result<ValidIdentifierName, null> {
|
||||
if (isReservedWord(name) || !t.isValidIdentifier(name)) {
|
||||
return Err(null);
|
||||
}
|
||||
return Ok({
|
||||
kind: 'named',
|
||||
value: name as ValidIdentifierName,
|
||||
});
|
||||
return Ok(makeIdentifierName(name).value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1351,7 +1314,31 @@ export function validateIdentifierName(
|
||||
* original source code.
|
||||
*/
|
||||
export function makeIdentifierName(name: string): ValidatedIdentifier {
|
||||
return validateIdentifierName(name).unwrap();
|
||||
if (isReservedWord(name)) {
|
||||
CompilerError.throwInvalidJS({
|
||||
reason: 'Expected a non-reserved identifier name',
|
||||
loc: GeneratedSource,
|
||||
description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
|
||||
suggestions: null,
|
||||
});
|
||||
} else {
|
||||
CompilerError.invariant(t.isValidIdentifier(name), {
|
||||
reason: `Expected a valid identifier name`,
|
||||
description: `\`${name}\` is not a valid JavaScript identifier`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
return {
|
||||
kind: 'named',
|
||||
value: name as ValidIdentifierName,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -56,9 +56,6 @@ export function printFunction(fn: HIRFunction): string {
|
||||
} else {
|
||||
definition += '<<anonymous>>';
|
||||
}
|
||||
if (fn.nameHint != null) {
|
||||
definition += ` ${fn.nameHint}`;
|
||||
}
|
||||
if (fn.params.length !== 0) {
|
||||
definition +=
|
||||
'(' +
|
||||
|
||||
@@ -249,7 +249,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
const fn: HIRFunction = {
|
||||
loc: GeneratedSource,
|
||||
id: null,
|
||||
nameHint: null,
|
||||
fnType: 'Other',
|
||||
env,
|
||||
params: [obj],
|
||||
@@ -276,7 +275,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
value: {
|
||||
kind: 'FunctionExpression',
|
||||
name: null,
|
||||
nameHint: null,
|
||||
loweredFunc: {
|
||||
func: fn,
|
||||
},
|
||||
|
||||
@@ -31,9 +31,7 @@ export function outlineFunctions(
|
||||
) {
|
||||
const loweredFunc = value.loweredFunc.func;
|
||||
|
||||
const id = fn.env.generateGloballyUniqueIdentifierName(
|
||||
loweredFunc.id ?? loweredFunc.nameHint,
|
||||
);
|
||||
const id = fn.env.generateGloballyUniqueIdentifierName(loweredFunc.id);
|
||||
loweredFunc.id = id.value;
|
||||
|
||||
fn.env.outlineFunction(loweredFunc, null);
|
||||
|
||||
@@ -364,7 +364,6 @@ function emitOutlinedFn(
|
||||
const fn: HIRFunction = {
|
||||
loc: GeneratedSource,
|
||||
id: null,
|
||||
nameHint: null,
|
||||
fnType: 'Other',
|
||||
env,
|
||||
params: [propsObj],
|
||||
|
||||
@@ -44,7 +44,6 @@ export function buildReactiveFunction(fn: HIRFunction): ReactiveFunction {
|
||||
return {
|
||||
loc: fn.loc,
|
||||
id: fn.id,
|
||||
nameHint: fn.nameHint,
|
||||
params: fn.params,
|
||||
generator: fn.generator,
|
||||
async: fn.async,
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
ValidIdentifierName,
|
||||
getHookKind,
|
||||
makeIdentifierName,
|
||||
validateIdentifierName,
|
||||
} from '../HIR/HIR';
|
||||
import {printIdentifier, printInstruction, printPlace} from '../HIR/PrintHIR';
|
||||
import {eachPatternOperand} from '../HIR/visitors';
|
||||
@@ -61,7 +62,6 @@ export const EARLY_RETURN_SENTINEL = 'react.early_return_sentinel';
|
||||
export type CodegenFunction = {
|
||||
type: 'CodegenFunction';
|
||||
id: t.Identifier | null;
|
||||
nameHint: string | null;
|
||||
params: t.FunctionDeclaration['params'];
|
||||
body: t.BlockStatement;
|
||||
generator: boolean;
|
||||
@@ -384,7 +384,6 @@ function codegenReactiveFunction(
|
||||
type: 'CodegenFunction',
|
||||
loc: fn.loc,
|
||||
id: fn.id !== null ? t.identifier(fn.id) : null,
|
||||
nameHint: fn.nameHint,
|
||||
params,
|
||||
body,
|
||||
generator: fn.generator,
|
||||
@@ -2329,6 +2328,10 @@ function codegenInstructionValue(
|
||||
reactiveFunction,
|
||||
).unwrap();
|
||||
|
||||
const validatedName =
|
||||
instrValue.name != null
|
||||
? validateIdentifierName(instrValue.name)
|
||||
: Err(null);
|
||||
if (instrValue.type === 'ArrowFunctionExpression') {
|
||||
let body: t.BlockStatement | t.Expression = fn.body;
|
||||
if (body.body.length === 1 && loweredFunc.directives.length == 0) {
|
||||
@@ -2340,7 +2343,9 @@ function codegenInstructionValue(
|
||||
value = t.arrowFunctionExpression(fn.params, body, fn.async);
|
||||
} else {
|
||||
value = t.functionExpression(
|
||||
instrValue.name != null ? t.identifier(instrValue.name) : null,
|
||||
validatedName
|
||||
.map<t.Identifier | null>(name => t.identifier(name))
|
||||
.unwrapOr(null),
|
||||
fn.params,
|
||||
fn.body,
|
||||
fn.generator,
|
||||
@@ -2349,10 +2354,10 @@ function codegenInstructionValue(
|
||||
}
|
||||
if (
|
||||
cx.env.config.enableNameAnonymousFunctions &&
|
||||
instrValue.name == null &&
|
||||
instrValue.nameHint != null
|
||||
validatedName.isErr() &&
|
||||
instrValue.name != null
|
||||
) {
|
||||
const name = instrValue.nameHint;
|
||||
const name = instrValue.name;
|
||||
value = t.memberExpression(
|
||||
t.objectExpression([t.objectProperty(t.stringLiteral(name), value)]),
|
||||
t.stringLiteral(name),
|
||||
|
||||
@@ -26,8 +26,7 @@ export function nameAnonymousFunctions(fn: HIRFunction): void {
|
||||
* nesting depth.
|
||||
*/
|
||||
const name = `${prefix}${node.generatedName}]`;
|
||||
node.fn.nameHint = name;
|
||||
node.fn.loweredFunc.func.nameHint = name;
|
||||
node.fn.name = name;
|
||||
}
|
||||
/**
|
||||
* Whether or not we generated a name for the function at this node,
|
||||
|
||||
@@ -5,52 +5,21 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
Effect,
|
||||
ErrorSeverity,
|
||||
SourceLocation,
|
||||
} from '..';
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
ArrayExpression,
|
||||
BasicBlock,
|
||||
BlockId,
|
||||
FunctionExpression,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
Instruction,
|
||||
Place,
|
||||
isSetStateType,
|
||||
isUseEffectHookType,
|
||||
isUseStateType,
|
||||
GeneratedSource,
|
||||
} from '../HIR';
|
||||
import {eachInstructionOperand, eachInstructionLValue} from '../HIR/visitors';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
type SetStateCall = {
|
||||
loc: SourceLocation;
|
||||
derivedDep: DerivationMetadata;
|
||||
setStateId: IdentifierId;
|
||||
};
|
||||
|
||||
type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsOrState';
|
||||
|
||||
type DerivationMetadata = {
|
||||
typeOfValue: TypeOfValue;
|
||||
place: Place;
|
||||
sources: Set<Place>;
|
||||
};
|
||||
|
||||
type ErrorMetadata = {
|
||||
type: TypeOfValue;
|
||||
description: string | undefined;
|
||||
loc: SourceLocation;
|
||||
setStateName: string | undefined | null;
|
||||
};
|
||||
import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
|
||||
/**
|
||||
* Validates that useEffect is not used for derived computations which could/should
|
||||
@@ -79,45 +48,12 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
|
||||
const functions: Map<IdentifierId, FunctionExpression> = new Map();
|
||||
const locals: Map<IdentifierId, IdentifierId> = new Map();
|
||||
const derivationCache: Map<IdentifierId, DerivationMetadata> = new Map();
|
||||
|
||||
const effectSetStates: Map<
|
||||
string | undefined | null,
|
||||
Array<Place>
|
||||
> = new Map();
|
||||
const setStateCalls: Map<string | undefined | null, Array<Place>> = new Map();
|
||||
|
||||
const errors: Array<ErrorMetadata> = [];
|
||||
|
||||
if (fn.fnType === 'Hook') {
|
||||
for (const param of fn.params) {
|
||||
if (param.kind === 'Identifier') {
|
||||
derivationCache.set(param.identifier.id, {
|
||||
place: param,
|
||||
sources: new Set([param]),
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (fn.fnType === 'Component') {
|
||||
const props = fn.params[0];
|
||||
if (props != null && props.kind === 'Identifier') {
|
||||
derivationCache.set(props.identifier.id, {
|
||||
place: props,
|
||||
sources: new Set([props]),
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
}
|
||||
}
|
||||
const errors = new CompilerError();
|
||||
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
parseBlockPhi(block, derivationCache);
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
|
||||
parseInstr(instr, derivationCache, setStateCalls);
|
||||
|
||||
if (value.kind === 'LoadLocal') {
|
||||
locals.set(lvalue.identifier.id, value.place.identifier.id);
|
||||
} else if (value.kind === 'ArrayExpression') {
|
||||
@@ -130,7 +66,6 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
) {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
|
||||
if (
|
||||
isUseEffectHookType(callee.identifier) &&
|
||||
value.args.length === 2 &&
|
||||
@@ -162,8 +97,6 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
validateEffect(
|
||||
effectFunction.loweredFunc.func,
|
||||
dependencies,
|
||||
derivationCache,
|
||||
effectSetStates,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
@@ -171,294 +104,43 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const compilerError = generateCompilerError(
|
||||
setStateCalls,
|
||||
effectSetStates,
|
||||
errors,
|
||||
);
|
||||
|
||||
if (compilerError.hasErrors()) {
|
||||
throw compilerError;
|
||||
}
|
||||
}
|
||||
|
||||
function generateCompilerError(
|
||||
setStateCalls: Map<string | undefined | null, Array<Place>>,
|
||||
effectSetStates: Map<string | undefined | null, Array<Place>>,
|
||||
errors: Array<ErrorMetadata>,
|
||||
): CompilerError {
|
||||
const throwableErrors = new CompilerError();
|
||||
for (const error of errors) {
|
||||
let compilerDiagnostic: CompilerDiagnostic | undefined = undefined;
|
||||
let detailMessage = '';
|
||||
switch (error.type) {
|
||||
case 'fromProps':
|
||||
detailMessage = 'This state value shadows a value passed as a prop.';
|
||||
break;
|
||||
case 'fromPropsOrState':
|
||||
detailMessage =
|
||||
'This state value shadows a value passed as a prop or a value from state.';
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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.type !== 'fromState'
|
||||
) {
|
||||
compilerDiagnostic = CompilerDiagnostic.create({
|
||||
description: `${error.description} This state value shadows a value passed as a prop. Instead of shadowing the prop with local state, hoist the state to the parent component and update it there.`,
|
||||
category: `Local state shadows parent state.`,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: error.loc,
|
||||
message: 'this setState synchronizes the state',
|
||||
});
|
||||
|
||||
for (const [key, setStateCallArray] of effectSetStates) {
|
||||
if (setStateCallArray.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const nonUseEffectSetStateCalls = setStateCalls.get(key);
|
||||
if (nonUseEffectSetStateCalls) {
|
||||
for (const place of nonUseEffectSetStateCalls) {
|
||||
if (!setStateCallArray.includes(place)) {
|
||||
compilerDiagnostic.withDetail({
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message:
|
||||
'this setState updates the shadowed state, but should call an onChange event from the parent',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
compilerDiagnostic = CompilerDiagnostic.create({
|
||||
description: `${error.description} Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.`,
|
||||
category: `Derive values in render, not effects.`,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: error.loc,
|
||||
message: 'This should be computed during render, not in an effect',
|
||||
});
|
||||
}
|
||||
|
||||
if (compilerDiagnostic) {
|
||||
throwableErrors.pushDiagnostic(compilerDiagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
return throwableErrors;
|
||||
}
|
||||
|
||||
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: Array<DerivationMetadata> | undefined,
|
||||
typeOfValue: TypeOfValue | undefined,
|
||||
derivationCache: Map<IdentifierId, DerivationMetadata>,
|
||||
): void {
|
||||
let newValue: DerivationMetadata = {
|
||||
place: target,
|
||||
sources: new Set(),
|
||||
typeOfValue: typeOfValue ?? 'ignored',
|
||||
};
|
||||
|
||||
if (sources !== undefined) {
|
||||
for (const source of sources) {
|
||||
/*
|
||||
* If the identifier of the source is a promoted identifier, then
|
||||
* we should set the target as the source.
|
||||
*/
|
||||
for (const place of source.sources) {
|
||||
if (
|
||||
place.identifier.name === null ||
|
||||
place.identifier.name?.kind === 'promoted'
|
||||
) {
|
||||
newValue.sources.add(target);
|
||||
} else {
|
||||
newValue.sources.add(place);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
derivationCache.set(target.identifier.id, newValue);
|
||||
}
|
||||
|
||||
function parseInstr(
|
||||
instr: Instruction,
|
||||
derivationCache: Map<IdentifierId, DerivationMetadata>,
|
||||
setStateCalls: Map<string | undefined | null, Array<Place>>,
|
||||
): void {
|
||||
// Recursively parse function expressions
|
||||
if (instr.value.kind === 'FunctionExpression') {
|
||||
for (const [, block] of instr.value.loweredFunc.func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
parseInstr(instr, derivationCache, setStateCalls);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
|
||||
// Catch any useState hook calls
|
||||
let sources: Array<DerivationMetadata> = [];
|
||||
if (
|
||||
instr.value.kind === 'Destructure' &&
|
||||
instr.value.lvalue.pattern.kind === 'ArrayPattern' &&
|
||||
isUseStateType(instr.value.value.identifier)
|
||||
) {
|
||||
typeOfValue = 'fromState';
|
||||
|
||||
const stateValueSource = instr.value.lvalue.pattern.items[0];
|
||||
if (stateValueSource.kind === 'Identifier') {
|
||||
sources.push({
|
||||
place: stateValueSource,
|
||||
typeOfValue: typeOfValue,
|
||||
sources: new Set([stateValueSource]),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
const opSource = derivationCache.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, derivationCache);
|
||||
}
|
||||
|
||||
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,
|
||||
derivationCache,
|
||||
);
|
||||
}
|
||||
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,
|
||||
derivationCache: Map<IdentifierId, DerivationMetadata>,
|
||||
): void {
|
||||
for (const phi of block.phis) {
|
||||
for (const operand of phi.operands.values()) {
|
||||
const phiSource = derivationCache.get(operand.identifier.id);
|
||||
if (phiSource !== undefined) {
|
||||
updateDerivationMetadata(
|
||||
phi.place,
|
||||
[phiSource],
|
||||
phiSource?.typeOfValue,
|
||||
derivationCache,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (errors.hasAnyErrors()) {
|
||||
throw errors;
|
||||
}
|
||||
}
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
effectDeps: Array<IdentifierId>,
|
||||
derivationCache: Map<IdentifierId, DerivationMetadata>,
|
||||
effectSetStates: Map<string | undefined | null, Array<Place>>,
|
||||
errors: Array<ErrorMetadata>,
|
||||
errors: CompilerError,
|
||||
): void {
|
||||
let isUsingDerivedDeps = false;
|
||||
for (const operand of effectFunction.context) {
|
||||
if (isSetStateType(operand.identifier)) {
|
||||
continue;
|
||||
} else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
|
||||
continue;
|
||||
} else {
|
||||
// Captured something other than the effect dep or setState
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const dep of effectDeps) {
|
||||
const depMetadata = derivationCache.get(dep);
|
||||
if (
|
||||
effectFunction.context.find(operand => operand.identifier.id === dep) !=
|
||||
null ||
|
||||
(depMetadata !== undefined && depMetadata.typeOfValue !== 'ignored')
|
||||
effectFunction.context.find(operand => operand.identifier.id === dep) ==
|
||||
null
|
||||
) {
|
||||
isUsingDerivedDeps = true;
|
||||
// effect dep wasn't actually used in the function
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isUsingDerivedDeps) {
|
||||
// no prop/state derived deps were used in the body of the effect
|
||||
return;
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
|
||||
for (const dep of effectDeps) {
|
||||
values.set(dep, [dep]);
|
||||
}
|
||||
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
|
||||
const derivedSetStateCall: Array<SetStateCall> = [];
|
||||
const setStateLocations: Array<SourceLocation> = [];
|
||||
for (const block of effectFunction.body.blocks.values()) {
|
||||
for (const pred of block.preds) {
|
||||
if (!seenBlocks.has(pred)) {
|
||||
@@ -466,29 +148,21 @@ function validateEffect(
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
parseBlockPhi(block, derivationCache);
|
||||
|
||||
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,
|
||||
]);
|
||||
for (const phi of block.phis) {
|
||||
const aggregateDeps: Set<IdentifierId> = new Set();
|
||||
for (const operand of phi.operands.values()) {
|
||||
const deps = values.get(operand.identifier.id);
|
||||
if (deps != null) {
|
||||
for (const dep of deps) {
|
||||
aggregateDeps.add(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (aggregateDeps.size !== 0) {
|
||||
values.set(phi.place.identifier.id, Array.from(aggregateDeps));
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
switch (instr.value.kind) {
|
||||
case 'Primitive':
|
||||
case 'JSXText':
|
||||
@@ -496,6 +170,10 @@ function validateEffect(
|
||||
break;
|
||||
}
|
||||
case 'LoadLocal': {
|
||||
const deps = values.get(instr.value.place.identifier.id);
|
||||
if (deps != null) {
|
||||
values.set(instr.lvalue.identifier.id, deps);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ComputedLoad':
|
||||
@@ -504,56 +182,57 @@ function validateEffect(
|
||||
case 'TemplateLiteral':
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
const aggregateDeps: Set<IdentifierId> = new Set();
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
const deps = values.get(operand.identifier.id);
|
||||
if (deps != null) {
|
||||
for (const dep of deps) {
|
||||
aggregateDeps.add(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (aggregateDeps.size !== 0) {
|
||||
values.set(instr.lvalue.identifier.id, Array.from(aggregateDeps));
|
||||
}
|
||||
|
||||
if (
|
||||
instr.value.kind === 'CallExpression' &&
|
||||
isSetStateType(instr.value.callee.identifier) &&
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
const derivedDep = derivationCache.get(
|
||||
instr.value.args[0].identifier.id,
|
||||
);
|
||||
|
||||
if (derivedDep !== undefined) {
|
||||
derivedSetStateCall.push({
|
||||
loc: instr.value.callee.loc,
|
||||
setStateId: instr.value.callee.identifier.id,
|
||||
derivedDep: derivedDep,
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
if (values.has(operand.identifier.id)) {
|
||||
//
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
seenBlocks.add(block.id);
|
||||
}
|
||||
|
||||
for (const call of derivedSetStateCall) {
|
||||
const placeNames = Array.from(call.derivedDep.sources)
|
||||
.map(place => {
|
||||
return place.identifier.name?.value;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
|
||||
let errorDescription = '';
|
||||
|
||||
if (call.derivedDep.typeOfValue === 'fromProps') {
|
||||
errorDescription = `props [${placeNames}].`;
|
||||
} else if (call.derivedDep.typeOfValue === 'fromState') {
|
||||
errorDescription = `local state [${placeNames}].`;
|
||||
} else {
|
||||
errorDescription = `both props and local state [${placeNames}].`;
|
||||
}
|
||||
|
||||
for (const loc of setStateLocations) {
|
||||
errors.push({
|
||||
type: call.derivedDep.typeOfValue,
|
||||
description: `This setState() appears to derive a value from ${errorDescription}`,
|
||||
loc: call.loc,
|
||||
setStateName:
|
||||
call.loc !== GeneratedSource ? call.loc.identifierName : undefined,
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
IdentifierId,
|
||||
InstructionValue,
|
||||
ManualMemoDependency,
|
||||
Place,
|
||||
PrunedReactiveScopeBlock,
|
||||
ReactiveFunction,
|
||||
ReactiveInstruction,
|
||||
@@ -28,10 +29,7 @@ import {
|
||||
SourceLocation,
|
||||
} from '../HIR';
|
||||
import {printIdentifier, printManualMemoDependency} from '../HIR/PrintHIR';
|
||||
import {
|
||||
eachInstructionValueLValue,
|
||||
eachInstructionValueOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
import {collectMaybeMemoDependencies} from '../Inference/DropManualMemoization';
|
||||
import {
|
||||
ReactiveFunctionVisitor,
|
||||
@@ -339,53 +337,56 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
* @returns a @{ManualMemoDependency} representing the variable +
|
||||
* property reads represented by @value
|
||||
*/
|
||||
recordDepsInValue(value: ReactiveValue, state: VisitorState): void {
|
||||
recordDepsInValue(
|
||||
value: ReactiveValue,
|
||||
state: VisitorState,
|
||||
): ManualMemoDependency | null {
|
||||
switch (value.kind) {
|
||||
case 'SequenceExpression': {
|
||||
for (const instr of value.instructions) {
|
||||
this.visitInstruction(instr, state);
|
||||
}
|
||||
this.recordDepsInValue(value.value, state);
|
||||
break;
|
||||
const result = this.recordDepsInValue(value.value, state);
|
||||
return result;
|
||||
}
|
||||
case 'OptionalExpression': {
|
||||
this.recordDepsInValue(value.value, state);
|
||||
break;
|
||||
return this.recordDepsInValue(value.value, state);
|
||||
}
|
||||
case 'ConditionalExpression': {
|
||||
this.recordDepsInValue(value.test, state);
|
||||
this.recordDepsInValue(value.consequent, state);
|
||||
this.recordDepsInValue(value.alternate, state);
|
||||
break;
|
||||
return null;
|
||||
}
|
||||
case 'LogicalExpression': {
|
||||
this.recordDepsInValue(value.left, state);
|
||||
this.recordDepsInValue(value.right, state);
|
||||
break;
|
||||
return null;
|
||||
}
|
||||
default: {
|
||||
collectMaybeMemoDependencies(value, this.temporaries, false);
|
||||
if (
|
||||
value.kind === 'StoreLocal' ||
|
||||
value.kind === 'StoreContext' ||
|
||||
value.kind === 'Destructure'
|
||||
) {
|
||||
for (const storeTarget of eachInstructionValueLValue(value)) {
|
||||
state.manualMemoState?.decls.add(
|
||||
storeTarget.identifier.declarationId,
|
||||
);
|
||||
if (storeTarget.identifier.name?.kind === 'named') {
|
||||
this.temporaries.set(storeTarget.identifier.id, {
|
||||
root: {
|
||||
kind: 'NamedLocal',
|
||||
value: storeTarget,
|
||||
},
|
||||
path: [],
|
||||
});
|
||||
}
|
||||
const dep = collectMaybeMemoDependencies(
|
||||
value,
|
||||
this.temporaries,
|
||||
false,
|
||||
);
|
||||
if (value.kind === 'StoreLocal' || value.kind === 'StoreContext') {
|
||||
const storeTarget = value.lvalue.place;
|
||||
state.manualMemoState?.decls.add(
|
||||
storeTarget.identifier.declarationId,
|
||||
);
|
||||
if (storeTarget.identifier.name?.kind === 'named' && dep == null) {
|
||||
const dep: ManualMemoDependency = {
|
||||
root: {
|
||||
kind: 'NamedLocal',
|
||||
value: storeTarget,
|
||||
},
|
||||
path: [],
|
||||
};
|
||||
this.temporaries.set(storeTarget.identifier.id, dep);
|
||||
return dep;
|
||||
}
|
||||
}
|
||||
break;
|
||||
return dep;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -402,15 +403,19 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
state.manualMemoState.decls.add(lvalue.identifier.declarationId);
|
||||
}
|
||||
|
||||
this.recordDepsInValue(value, state);
|
||||
if (lvalue != null) {
|
||||
temporaries.set(lvalue.identifier.id, {
|
||||
root: {
|
||||
kind: 'NamedLocal',
|
||||
value: {...lvalue},
|
||||
},
|
||||
path: [],
|
||||
});
|
||||
const maybeDep = this.recordDepsInValue(value, state);
|
||||
if (lvalId != null) {
|
||||
if (maybeDep != null) {
|
||||
temporaries.set(lvalId, maybeDep);
|
||||
} else if (isNamedLocal) {
|
||||
temporaries.set(lvalId, {
|
||||
root: {
|
||||
kind: 'NamedLocal',
|
||||
value: {...(instr.lvalue as Place)},
|
||||
},
|
||||
path: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,15 +24,13 @@ function BadExample() {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Derive values in render, not effects.
|
||||
|
||||
This setState() appears to derive a value from local state [firstName, lastName]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
error.invalid-derived-computation-in-effect.ts:9:4
|
||||
7 | const [fullName, setFullName] = useState('');
|
||||
8 | useEffect(() => {
|
||||
> 9 | setFullName(capitalize(firstName + ' ' + lastName));
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
10 | }, [firstName, lastName]);
|
||||
11 |
|
||||
12 | return <div>{fullName}</div>;
|
||||
@@ -1,59 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
|
||||
/**
|
||||
* Repro from https://github.com/facebook/react/issues/34262
|
||||
*
|
||||
* The compiler memoizes more precisely than the original code, with two reactive scopes:
|
||||
* - One for `transform(input)` with `input` as dep
|
||||
* - One for `{value}` with `value` as dep
|
||||
*
|
||||
* When we validate preserving manual memoization we incorrectly reject this, because
|
||||
* the original memoization had `object` depending on `input` but our scope depends on
|
||||
* `value`.
|
||||
*
|
||||
* This fixture adds a later potential mutation, which extends the scope and should
|
||||
* fail validation. This confirms that even though we allow the dependency to diverge,
|
||||
* we still check that the output value is memoized.
|
||||
*/
|
||||
function useInputValue(input) {
|
||||
const object = React.useMemo(() => {
|
||||
const {value} = transform(input);
|
||||
return {value};
|
||||
}, [input]);
|
||||
mutate(object);
|
||||
return object;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Compilation Skipped: Existing memoization could not be preserved
|
||||
|
||||
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.
|
||||
|
||||
error.repro-preserve-memoization-inner-destructured-value-mistaken-as-dependency-later-mutation.ts:19:17
|
||||
17 | */
|
||||
18 | function useInputValue(input) {
|
||||
> 19 | const object = React.useMemo(() => {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 20 | const {value} = transform(input);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 21 | return {value};
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 22 | }, [input]);
|
||||
| ^^^^^^^^^^^^^^ Could not preserve existing memoization
|
||||
23 | mutate(object);
|
||||
24 | return object;
|
||||
25 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
|
||||
/**
|
||||
* Repro from https://github.com/facebook/react/issues/34262
|
||||
*
|
||||
* The compiler memoizes more precisely than the original code, with two reactive scopes:
|
||||
* - One for `transform(input)` with `input` as dep
|
||||
* - One for `{value}` with `value` as dep
|
||||
*
|
||||
* When we validate preserving manual memoization we incorrectly reject this, because
|
||||
* the original memoization had `object` depending on `input` but our scope depends on
|
||||
* `value`.
|
||||
*
|
||||
* This fixture adds a later potential mutation, which extends the scope and should
|
||||
* fail validation. This confirms that even though we allow the dependency to diverge,
|
||||
* we still check that the output value is memoized.
|
||||
*/
|
||||
function useInputValue(input) {
|
||||
const object = React.useMemo(() => {
|
||||
const {value} = transform(input);
|
||||
return {value};
|
||||
}, [input]);
|
||||
mutate(object);
|
||||
return object;
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
|
||||
import {identity, Stringify, useHook} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Repro from https://github.com/facebook/react/issues/34262
|
||||
*
|
||||
* The compiler memoizes more precisely than the original code, with two reactive scopes:
|
||||
* - One for `transform(input)` with `input` as dep
|
||||
* - One for `{value}` with `value` as dep
|
||||
*
|
||||
* When we validate preserving manual memoization we incorrectly reject this, because
|
||||
* the original memoization had `object` depending on `input` but our scope depends on
|
||||
* `value`.
|
||||
*/
|
||||
function useInputValue(input) {
|
||||
// Conflate the `identity(input, x)` call with something outside the useMemo,
|
||||
// to try and break memoization of `value`. This gets correctly flagged since
|
||||
// the dependency is being mutated
|
||||
let x = {};
|
||||
useHook();
|
||||
const object = React.useMemo(() => {
|
||||
const {value} = identity(input, x);
|
||||
return {value};
|
||||
}, [input, x]);
|
||||
return object;
|
||||
}
|
||||
|
||||
function Component() {
|
||||
return <Stringify value={useInputValue({value: 42}).value} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Compilation Skipped: Existing memoization could not be preserved
|
||||
|
||||
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly.
|
||||
|
||||
error.repro-preserve-memoization-inner-destructured-value-mistaken-as-dependency-mutated-dep.ts:25:13
|
||||
23 | const {value} = identity(input, x);
|
||||
24 | return {value};
|
||||
> 25 | }, [input, x]);
|
||||
| ^ This dependency may be modified later
|
||||
26 | return object;
|
||||
27 | }
|
||||
28 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
|
||||
import {identity, Stringify, useHook} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Repro from https://github.com/facebook/react/issues/34262
|
||||
*
|
||||
* The compiler memoizes more precisely than the original code, with two reactive scopes:
|
||||
* - One for `transform(input)` with `input` as dep
|
||||
* - One for `{value}` with `value` as dep
|
||||
*
|
||||
* When we validate preserving manual memoization we incorrectly reject this, because
|
||||
* the original memoization had `object` depending on `input` but our scope depends on
|
||||
* `value`.
|
||||
*/
|
||||
function useInputValue(input) {
|
||||
// Conflate the `identity(input, x)` call with something outside the useMemo,
|
||||
// to try and break memoization of `value`. This gets correctly flagged since
|
||||
// the dependency is being mutated
|
||||
let x = {};
|
||||
useHook();
|
||||
const object = React.useMemo(() => {
|
||||
const {value} = identity(input, x);
|
||||
return {value};
|
||||
}, [input, x]);
|
||||
return object;
|
||||
}
|
||||
|
||||
function Component() {
|
||||
return <Stringify value={useInputValue({value: 42}).value} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNameAnonymousFunctions
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const onClick = () => {
|
||||
console.log('hello!');
|
||||
};
|
||||
return <div onClick={onClick} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNameAnonymousFunctions
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(1);
|
||||
const onClick = _ComponentOnClick;
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <div onClick={onClick} />;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
function _ComponentOnClick() {
|
||||
console.log("hello!");
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 42 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div></div>
|
||||
@@ -1,14 +0,0 @@
|
||||
// @enableNameAnonymousFunctions
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const onClick = () => {
|
||||
console.log('hello!');
|
||||
};
|
||||
return <div onClick={onClick} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
};
|
||||
@@ -1,109 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
|
||||
import {identity, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Repro from https://github.com/facebook/react/issues/34262
|
||||
*
|
||||
* The compiler memoizes more precisely than the original code, with two reactive scopes:
|
||||
* - One for `transform(input)` with `input` as dep
|
||||
* - One for `{value}` with `value` as dep
|
||||
*
|
||||
* Previously ValidatePreservedManualMemoization rejected this input, because
|
||||
* the original memoization had `object` depending on `input` but we split the scope per above,
|
||||
* and the scope for the FinishMemoize instruction is the second scope which depends on `value`
|
||||
*/
|
||||
function useInputValue(input) {
|
||||
const object = React.useMemo(() => {
|
||||
const {value} = identity(input);
|
||||
return {value};
|
||||
}, [input]);
|
||||
return object;
|
||||
}
|
||||
|
||||
function Component() {
|
||||
return <Stringify value={useInputValue({value: 42}).value} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
|
||||
|
||||
import { identity, Stringify } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* Repro from https://github.com/facebook/react/issues/34262
|
||||
*
|
||||
* The compiler memoizes more precisely than the original code, with two reactive scopes:
|
||||
* - One for `transform(input)` with `input` as dep
|
||||
* - One for `{value}` with `value` as dep
|
||||
*
|
||||
* Previously ValidatePreservedManualMemoization rejected this input, because
|
||||
* the original memoization had `object` depending on `input` but we split the scope per above,
|
||||
* and the scope for the FinishMemoize instruction is the second scope which depends on `value`
|
||||
*/
|
||||
function useInputValue(input) {
|
||||
const $ = _c(4);
|
||||
let t0;
|
||||
if ($[0] !== input) {
|
||||
t0 = identity(input);
|
||||
$[0] = input;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const { value } = t0;
|
||||
let t1;
|
||||
if ($[2] !== value) {
|
||||
t1 = { value };
|
||||
$[2] = value;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
const object = t1;
|
||||
return object;
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const $ = _c(3);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = { value: 42 };
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const t1 = useInputValue(t0);
|
||||
let t2;
|
||||
if ($[1] !== t1.value) {
|
||||
t2 = <Stringify value={t1.value} />;
|
||||
$[1] = t1.value;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"value":42}</div>
|
||||
@@ -1,31 +0,0 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
|
||||
import {identity, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Repro from https://github.com/facebook/react/issues/34262
|
||||
*
|
||||
* The compiler memoizes more precisely than the original code, with two reactive scopes:
|
||||
* - One for `transform(input)` with `input` as dep
|
||||
* - One for `{value}` with `value` as dep
|
||||
*
|
||||
* Previously ValidatePreservedManualMemoization rejected this input, because
|
||||
* the original memoization had `object` depending on `input` but we split the scope per above,
|
||||
* and the scope for the FinishMemoize instruction is the second scope which depends on `value`
|
||||
*/
|
||||
function useInputValue(input) {
|
||||
const object = React.useMemo(() => {
|
||||
const {value} = identity(input);
|
||||
return {value};
|
||||
}, [input]);
|
||||
return object;
|
||||
}
|
||||
|
||||
function Component() {
|
||||
return <Stringify value={useInputValue({value: 42}).value} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -1,87 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({initialName}) {
|
||||
const [name, setName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setName(initialName);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={name} onChange={e => setName(e.target.value)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{initialName: 'John'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
const { initialName } = t0;
|
||||
const [name, setName] = useState("");
|
||||
let t1;
|
||||
if ($[0] !== initialName) {
|
||||
t1 = () => {
|
||||
setName(initialName);
|
||||
};
|
||||
$[0] = initialName;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = [];
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = (e) => setName(e.target.value);
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
let t4;
|
||||
if ($[4] !== name) {
|
||||
t4 = (
|
||||
<div>
|
||||
<input value={name} onChange={t3} />
|
||||
</div>
|
||||
);
|
||||
$[4] = name;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ initialName: "John" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div><input value="John"></div>
|
||||
@@ -1,21 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({initialName}) {
|
||||
const [name, setName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setName(initialName);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={name} onChange={e => setName(e.target.value)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{initialName: 'John'}],
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({prefix}) {
|
||||
const [name, setName] = useState('');
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayName(prefix + name);
|
||||
}, [prefix, name]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={name} onChange={e => setName(e.target.value)} />
|
||||
<div>{displayName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: 'Hello, '}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Derive values in render, not effects.
|
||||
|
||||
This setState() appears to derive a value from both props and local state [prefix, name]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.bug-derived-state-from-mixed-deps.ts:9:4
|
||||
7 |
|
||||
8 | useEffect(() => {
|
||||
> 9 | setDisplayName(prefix + name);
|
||||
| ^^^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
10 | }, [prefix, name]);
|
||||
11 |
|
||||
12 | return (
|
||||
```
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({prefix}) {
|
||||
const [name, setName] = useState('');
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayName(prefix + name);
|
||||
}, [prefix, name]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={name} onChange={e => setName(e.target.value)} />
|
||||
<div>{displayName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: 'Hello, '}],
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useState, useEffect} from 'react';
|
||||
|
||||
function Component({props, number}) {
|
||||
const nothing = 0;
|
||||
const missDirection = number;
|
||||
const [displayValue, setDisplayValue] = useState(props.prefix + missDirection + nothing);
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayValue(props.prefix + missDirection + nothing);
|
||||
}, [props.prefix, missDirection, nothing]);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
setDisplayValue('clicked');
|
||||
}}>
|
||||
{displayValue}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Local state shadows parent state.
|
||||
|
||||
This setState() appears to derive a value from props [props, number]. This state value shadows a value passed as a prop. Instead of shadowing the prop with local state, hoist the state to the parent component and update it there.
|
||||
|
||||
error.derived-state-from-shadowed-props.ts:10:4
|
||||
8 |
|
||||
9 | useEffect(() => {
|
||||
> 10 | setDisplayValue(props.prefix + missDirection + nothing);
|
||||
| ^^^^^^^^^^^^^^^ this setState synchronizes the state
|
||||
11 | }, [props.prefix, missDirection, nothing]);
|
||||
12 |
|
||||
13 | return (
|
||||
|
||||
error.derived-state-from-shadowed-props.ts:16:8
|
||||
14 | <div
|
||||
15 | onClick={() => {
|
||||
> 16 | setDisplayValue('clicked');
|
||||
| ^^^^^^^^^^^^^^^ this setState updates the shadowed state, but should call an onChange event from the parent
|
||||
17 | }}>
|
||||
18 | {displayValue}
|
||||
19 | </div>
|
||||
```
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useState, useEffect} from 'react';
|
||||
|
||||
function Component({props, number}) {
|
||||
const nothing = 0;
|
||||
const missDirection = number;
|
||||
const [displayValue, setDisplayValue] = useState(props.prefix + missDirection + nothing);
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayValue(props.prefix + missDirection + nothing);
|
||||
}, [props.prefix, missDirection, nothing]);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
setDisplayValue('clicked');
|
||||
}}>
|
||||
{displayValue}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value, enabled}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
setLocalValue(value);
|
||||
} else {
|
||||
setLocalValue('disabled');
|
||||
}
|
||||
}, [value, enabled]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test', enabled: true}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Derive values in render, not effects.
|
||||
|
||||
This setState() appears to derive a value from props [value]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.derived-state-with-conditional.ts:9:6
|
||||
7 | useEffect(() => {
|
||||
8 | if (enabled) {
|
||||
> 9 | setLocalValue(value);
|
||||
| ^^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
10 | } else {
|
||||
11 | setLocalValue('disabled');
|
||||
12 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value, enabled}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
setLocalValue(value);
|
||||
} else {
|
||||
setLocalValue('disabled');
|
||||
}
|
||||
}, [value, enabled]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test', enabled: true}],
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Value changed:', value);
|
||||
setLocalValue(value);
|
||||
document.title = `Value: ${value}`;
|
||||
}, [value]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Derive values in render, not effects.
|
||||
|
||||
This setState() appears to derive a value from props [value]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.derived-state-with-side-effects.ts:9:4
|
||||
7 | useEffect(() => {
|
||||
8 | console.log('Value changed:', value);
|
||||
> 9 | setLocalValue(value);
|
||||
| ^^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
10 | document.title = `Value: ${value}`;
|
||||
11 | }, [value]);
|
||||
12 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Value changed:', value);
|
||||
setLocalValue(value);
|
||||
document.title = `Value: ${value}`;
|
||||
}, [value]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test'}],
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component(props) {
|
||||
const [displayValue, setDisplayValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const computed = props.prefix + props.value + props.suffix;
|
||||
setDisplayValue(computed);
|
||||
}, [props.prefix, props.value, props.suffix]);
|
||||
|
||||
return <div>{displayValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: '[', value: 'test', suffix: ']'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Derive values in render, not effects.
|
||||
|
||||
This setState() appears to derive a value from props [props]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.invalid-derived-state-from-props-computed.ts:9:4
|
||||
7 | useEffect(() => {
|
||||
8 | const computed = props.prefix + props.value + props.suffix;
|
||||
> 9 | setDisplayValue(computed);
|
||||
| ^^^^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
10 | }, [props.prefix, props.value, props.suffix]);
|
||||
11 |
|
||||
12 | return <div>{displayValue}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component(props) {
|
||||
const [displayValue, setDisplayValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const computed = props.prefix + props.value + props.suffix;
|
||||
setDisplayValue(computed);
|
||||
}, [props.prefix, props.value, props.suffix]);
|
||||
|
||||
return <div>{displayValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: '[', value: 'test', suffix: ']'}],
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({props}) {
|
||||
const [fullName, setFullName] = useState(props.firstName + ' ' + props.lastName);
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(props.firstName + ' ' + props.lastName);
|
||||
}, [props.firstName, props.lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John', lastName: 'Doe'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Derive values in render, not effects.
|
||||
|
||||
This setState() appears to derive a value from props [props]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.invalid-derived-state-from-props-destructured.ts:8:4
|
||||
6 |
|
||||
7 | useEffect(() => {
|
||||
> 8 | setFullName(props.firstName + ' ' + props.lastName);
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
9 | }, [props.firstName, props.lastName]);
|
||||
10 |
|
||||
11 | return <div>{fullName}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({props}) {
|
||||
const [fullName, setFullName] = useState(props.firstName + ' ' + props.lastName);
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(props.firstName + ' ' + props.lastName);
|
||||
}, [props.firstName, props.lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John', lastName: 'Doe'}],
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({firstName, lastName}) {
|
||||
const [fullName, setFullName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John', lastName: 'Doe'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Derive values in render, not effects.
|
||||
|
||||
This setState() appears to derive a value from props [firstName, lastName]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.invalid-derived-state-from-props-in-effect.ts:8:4
|
||||
6 |
|
||||
7 | useEffect(() => {
|
||||
> 8 | setFullName(firstName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
9 | }, [firstName, lastName]);
|
||||
10 |
|
||||
11 | return <div>{fullName}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({firstName, lastName}) {
|
||||
const [fullName, setFullName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John', lastName: 'Doe'}],
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
|
||||
export default function InProductLobbyGeminiCard(
|
||||
input = 'empty',
|
||||
) {
|
||||
const [currInput, setCurrInput] = useState(input);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrInput(input)
|
||||
}, [input]);
|
||||
|
||||
return (
|
||||
<div>{currInput}</div>
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Derive values in render, not effects.
|
||||
|
||||
This setState() appears to derive a value from props [input]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.invalid-derived-state-from-props-with-default-value.ts:9:4
|
||||
7 |
|
||||
8 | useEffect(() => {
|
||||
> 9 | setCurrInput(input)
|
||||
| ^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
10 | }, [input]);
|
||||
11 |
|
||||
12 | return (
|
||||
```
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
|
||||
export default function InProductLobbyGeminiCard(
|
||||
input = 'empty',
|
||||
) {
|
||||
const [currInput, setCurrInput] = useState(input);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrInput(input)
|
||||
}, [input]);
|
||||
|
||||
return (
|
||||
<div>{currInput}</div>
|
||||
)
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [firstName, setFirstName] = useState('John');
|
||||
const [lastName, setLastName] = useState('Doe');
|
||||
const [fullName, setFullName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={firstName} onChange={e => setFirstName(e.target.value)} />
|
||||
<input value={lastName} onChange={e => setLastName(e.target.value)} />
|
||||
<div>{fullName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Derive values in render, not effects.
|
||||
|
||||
This setState() appears to derive a value from local state [firstName, lastName]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.invalid-derived-state-from-state-in-effect.ts:10:4
|
||||
8 |
|
||||
9 | useEffect(() => {
|
||||
> 10 | setFullName(firstName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
11 | }, [firstName, lastName]);
|
||||
12 |
|
||||
13 | return (
|
||||
```
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [firstName, setFirstName] = useState('John');
|
||||
const [lastName, setLastName] = useState('Doe');
|
||||
const [fullName, setFullName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={firstName} onChange={e => setFirstName(e.target.value)} />
|
||||
<input value={lastName} onChange={e => setLastName(e.target.value)} />
|
||||
<div>{fullName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
@@ -34,8 +34,4 @@ const configs = {
|
||||
},
|
||||
};
|
||||
|
||||
const rules = Object.fromEntries(
|
||||
Object.entries(allRules).map(([name, {rule}]) => [name, rule]),
|
||||
);
|
||||
|
||||
export {configs, rules, meta};
|
||||
export {configs, allRules as rules, meta};
|
||||
|
||||
@@ -21,8 +21,6 @@ import {Note} from './cjs/Note.js';
|
||||
|
||||
import {GenerateImage} from './GenerateImage.js';
|
||||
|
||||
import LargeContent from './LargeContent.js';
|
||||
|
||||
import {like, greet, increment} from './actions.js';
|
||||
|
||||
import {getServerState} from './ServerState.js';
|
||||
@@ -235,11 +233,6 @@ export default async function App({prerender, noCache}) {
|
||||
<Foo>{dedupedChild}</Foo>
|
||||
<Bar>{Promise.resolve([dedupedChild])}</Bar>
|
||||
<Navigate />
|
||||
{prerender ? null : ( // TODO: prerender is broken for large content for some reason.
|
||||
<React.Suspense fallback={null}>
|
||||
<LargeContent />
|
||||
</React.Suspense>
|
||||
)}
|
||||
</Container>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
391
packages/react-client/src/ReactFlightClient.js
vendored
391
packages/react-client/src/ReactFlightClient.js
vendored
@@ -366,7 +366,6 @@ type Response = {
|
||||
_debugRootOwner?: null | ReactComponentInfo, // DEV-only
|
||||
_debugRootStack?: null | Error, // DEV-only
|
||||
_debugRootTask?: null | ConsoleTask, // DEV-only
|
||||
_debugStartTime: number, // DEV-only
|
||||
_debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
|
||||
_debugChannel?: void | DebugChannel, // DEV-only
|
||||
_blockedConsole?: null | SomeChunk<ConsoleEntry>, // DEV-only
|
||||
@@ -823,7 +822,6 @@ type InitializationReference = {
|
||||
key: string,
|
||||
) => any,
|
||||
path: Array<string>,
|
||||
isDebug?: boolean, // DEV-only
|
||||
};
|
||||
type InitializationHandler = {
|
||||
parent: null | InitializationHandler,
|
||||
@@ -874,7 +872,6 @@ function initializeDebugChunk(
|
||||
response,
|
||||
initializeDebugInfo,
|
||||
[''], // path
|
||||
true,
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -897,7 +894,6 @@ function initializeDebugChunk(
|
||||
response,
|
||||
initializeDebugInfo,
|
||||
[''], // path
|
||||
true,
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -1411,6 +1407,8 @@ function fulfillReference(
|
||||
const mappedValue = map(response, value, parentObject, key);
|
||||
parentObject[key] = mappedValue;
|
||||
|
||||
transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
|
||||
|
||||
// If this is the root object for a model reference, where `handler.value`
|
||||
// is a stale `null`, the resolved value can be used directly.
|
||||
if (key === '' && handler.value === null) {
|
||||
@@ -1429,27 +1427,19 @@ function fulfillReference(
|
||||
const element: any = handler.value;
|
||||
switch (key) {
|
||||
case '3':
|
||||
transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
|
||||
element.props = mappedValue;
|
||||
break;
|
||||
case '4':
|
||||
// This path doesn't call transferReferencedDebugInfo because this reference is to a debug chunk.
|
||||
if (__DEV__) {
|
||||
element._owner = mappedValue;
|
||||
}
|
||||
break;
|
||||
case '5':
|
||||
// This path doesn't call transferReferencedDebugInfo because this reference is to a debug chunk.
|
||||
if (__DEV__) {
|
||||
element._debugStack = mappedValue;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
|
||||
break;
|
||||
}
|
||||
} else if (__DEV__ && !reference.isDebug) {
|
||||
transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
|
||||
}
|
||||
|
||||
handler.deps--;
|
||||
@@ -1528,7 +1518,6 @@ function waitForReference<T>(
|
||||
response: Response,
|
||||
map: (response: Response, model: any, parentObject: Object, key: string) => T,
|
||||
path: Array<string>,
|
||||
isAwaitingDebugInfo: boolean, // DEV-only
|
||||
): T {
|
||||
if (
|
||||
__DEV__ &&
|
||||
@@ -1573,9 +1562,6 @@ function waitForReference<T>(
|
||||
map,
|
||||
path,
|
||||
};
|
||||
if (__DEV__) {
|
||||
reference.isDebug = isAwaitingDebugInfo;
|
||||
}
|
||||
|
||||
// Add "listener".
|
||||
if (referencedChunk.value === null) {
|
||||
@@ -1808,21 +1794,13 @@ function transferReferencedDebugInfo(
|
||||
existingDebugInfo.push.apply(existingDebugInfo, referencedDebugInfo);
|
||||
}
|
||||
}
|
||||
// We also add the debug info to the initializing chunk since the resolution of that promise is
|
||||
// also blocked by the referenced debug info. By adding it to both we can track it even if the array/element
|
||||
// We also add it to the initializing chunk since the resolution of that promise is
|
||||
// also blocked by these. By adding it to both we can track it even if the array/element
|
||||
// is extracted, or if the root is rendered as is.
|
||||
if (parentChunk !== null) {
|
||||
const parentDebugInfo = parentChunk._debugInfo;
|
||||
for (let i = 0; i < referencedDebugInfo.length; ++i) {
|
||||
const debugInfoEntry = referencedDebugInfo[i];
|
||||
if (debugInfoEntry.name != null) {
|
||||
(debugInfoEntry: ReactComponentInfo);
|
||||
// We're not transferring Component info since we use Component info
|
||||
// in Debug info to fill in gaps between Fibers for the parent stack.
|
||||
} else {
|
||||
parentDebugInfo.push(debugInfoEntry);
|
||||
}
|
||||
}
|
||||
// $FlowFixMe[method-unbinding]
|
||||
parentDebugInfo.push.apply(parentDebugInfo, referencedDebugInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1879,7 +1857,6 @@ function getOutlinedModel<T>(
|
||||
response,
|
||||
map,
|
||||
path.slice(i - 1),
|
||||
false,
|
||||
);
|
||||
}
|
||||
case HALTED: {
|
||||
@@ -1925,27 +1902,11 @@ function getOutlinedModel<T>(
|
||||
value = value[path[i]];
|
||||
}
|
||||
const chunkValue = map(response, value, parentObject, key);
|
||||
if (
|
||||
parentObject[0] === REACT_ELEMENT_TYPE &&
|
||||
(key === '4' || key === '5')
|
||||
) {
|
||||
// If we're resolving the "owner" or "stack" slot of an Element array, we don't call
|
||||
// transferReferencedDebugInfo because this reference is to a debug chunk.
|
||||
} else {
|
||||
transferReferencedDebugInfo(initializingChunk, chunk, chunkValue);
|
||||
}
|
||||
transferReferencedDebugInfo(initializingChunk, chunk, chunkValue);
|
||||
return chunkValue;
|
||||
case PENDING:
|
||||
case BLOCKED:
|
||||
return waitForReference(
|
||||
chunk,
|
||||
parentObject,
|
||||
key,
|
||||
response,
|
||||
map,
|
||||
path,
|
||||
false,
|
||||
);
|
||||
return waitForReference(chunk, parentObject, key, response, map, path);
|
||||
case HALTED: {
|
||||
// Add a dependency that will never resolve.
|
||||
// TODO: Mark downstreams as halted too.
|
||||
@@ -2483,13 +2444,6 @@ function ResponseInstance(
|
||||
'"use ' + rootEnv.toLowerCase() + '"',
|
||||
);
|
||||
}
|
||||
if (enableAsyncDebugInfo) {
|
||||
// Track the start of the fetch to the best of our knowledge.
|
||||
// Note: createFromFetch allows this to be marked at the start of the fetch
|
||||
// where as if you use createFromReadableStream from the body of the fetch
|
||||
// then the start time is when the headers resolved.
|
||||
this._debugStartTime = performance.now();
|
||||
}
|
||||
this._debugFindSourceMapURL = findSourceMapURL;
|
||||
this._debugChannel = debugChannel;
|
||||
this._blockedConsole = null;
|
||||
@@ -2558,99 +2512,16 @@ export type StreamState = {
|
||||
_rowTag: number, // 0 indicates that we're currently parsing the row ID
|
||||
_rowLength: number, // remaining bytes in the row. 0 indicates that we're looking for a newline.
|
||||
_buffer: Array<Uint8Array>, // chunks received so far as part of this row
|
||||
_debugInfo: ReactIOInfo, // DEV-only
|
||||
_debugTargetChunkSize: number, // DEV-only
|
||||
};
|
||||
|
||||
export function createStreamState(
|
||||
weakResponse: WeakResponse, // DEV-only
|
||||
streamDebugValue: mixed, // DEV-only
|
||||
): StreamState {
|
||||
const streamState: StreamState = (({
|
||||
export function createStreamState(): StreamState {
|
||||
return {
|
||||
_rowState: 0,
|
||||
_rowID: 0,
|
||||
_rowTag: 0,
|
||||
_rowLength: 0,
|
||||
_buffer: [],
|
||||
}: Omit<StreamState, '_debugInfo' | '_debugTargetChunkSize'>): any);
|
||||
if (__DEV__ && enableAsyncDebugInfo) {
|
||||
const response = unwrapWeakResponse(weakResponse);
|
||||
// Create an entry for the I/O to load the stream itself.
|
||||
const debugValuePromise = Promise.resolve(streamDebugValue);
|
||||
(debugValuePromise: any).status = 'fulfilled';
|
||||
(debugValuePromise: any).value = streamDebugValue;
|
||||
streamState._debugInfo = {
|
||||
name: 'RSC stream',
|
||||
start: response._debugStartTime,
|
||||
end: response._debugStartTime, // will be updated once we finish a chunk
|
||||
byteSize: 0, // will be updated as we resolve a data chunk
|
||||
value: debugValuePromise,
|
||||
owner: response._debugRootOwner,
|
||||
debugStack: response._debugRootStack,
|
||||
debugTask: response._debugRootTask,
|
||||
};
|
||||
streamState._debugTargetChunkSize = MIN_CHUNK_SIZE;
|
||||
}
|
||||
return streamState;
|
||||
}
|
||||
|
||||
// Depending on set up the chunks of a TLS connection can vary in size. However in practice it's often
|
||||
// at 64kb or even multiples of 64kb. It can also be smaller but in practice it also happens that 64kb
|
||||
// is around what you can download on fast 4G connection in 300ms which is what we throttle reveals at
|
||||
// anyway. The net effect is that in practice, you won't really reveal anything in smaller units than
|
||||
// 64kb if they're revealing at maximum speed in production. Therefore we group smaller chunks into
|
||||
// these larger chunks since in production that's more realistic.
|
||||
// TODO: If the stream is compressed, then you could fit much more in a single 300ms so maybe it should
|
||||
// actually be larger.
|
||||
const MIN_CHUNK_SIZE = 65536;
|
||||
|
||||
function incrementChunkDebugInfo(
|
||||
streamState: StreamState,
|
||||
chunkLength: number,
|
||||
): void {
|
||||
if (__DEV__ && enableAsyncDebugInfo) {
|
||||
const debugInfo: ReactIOInfo = streamState._debugInfo;
|
||||
const endTime = performance.now();
|
||||
const previousEndTime = debugInfo.end;
|
||||
const newByteLength = ((debugInfo.byteSize: any): number) + chunkLength;
|
||||
if (
|
||||
newByteLength > streamState._debugTargetChunkSize ||
|
||||
endTime > previousEndTime + 10
|
||||
) {
|
||||
// This new chunk would overshoot the chunk size so therefore we treat it as its own new chunk
|
||||
// by cloning the old one. Similarly, if some time has passed we assume that it was actually
|
||||
// due to the server being unable to flush chunks faster e.g. due to I/O so it would be a
|
||||
// new chunk in production even if the buffer hasn't been reached.
|
||||
streamState._debugInfo = {
|
||||
name: debugInfo.name,
|
||||
start: debugInfo.start,
|
||||
end: endTime,
|
||||
byteSize: newByteLength,
|
||||
value: debugInfo.value,
|
||||
owner: debugInfo.owner,
|
||||
debugStack: debugInfo.debugStack,
|
||||
debugTask: debugInfo.debugTask,
|
||||
};
|
||||
streamState._debugTargetChunkSize = newByteLength + MIN_CHUNK_SIZE;
|
||||
} else {
|
||||
// Otherwise we reuse the old chunk but update the end time and byteSize to the latest.
|
||||
// $FlowFixMe[cannot-write]
|
||||
debugInfo.end = endTime;
|
||||
// $FlowFixMe[cannot-write]
|
||||
debugInfo.byteSize = newByteLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolveChunkDebugInfo(
|
||||
streamState: StreamState,
|
||||
chunk: SomeChunk<any>,
|
||||
): void {
|
||||
if (__DEV__ && enableAsyncDebugInfo) {
|
||||
// Push the currently resolving chunk's debug info representing the stream on the Promise
|
||||
// that was waiting on the stream.
|
||||
chunk._debugInfo.push({awaited: streamState._debugInfo});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function resolveDebugHalt(response: Response, id: number): void {
|
||||
@@ -2674,33 +2545,17 @@ function resolveModel(
|
||||
response: Response,
|
||||
id: number,
|
||||
model: UninitializedModel,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
if (!chunk) {
|
||||
const newChunk: ResolvedModelChunk<any> = createResolvedModelChunk(
|
||||
response,
|
||||
model,
|
||||
);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
chunks.set(id, createResolvedModelChunk(response, model));
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, chunk);
|
||||
}
|
||||
resolveModelChunk(response, chunk, model);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveText(
|
||||
response: Response,
|
||||
id: number,
|
||||
text: string,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
function resolveText(response: Response, id: number, text: string): void {
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
if (chunk && chunk.status !== PENDING) {
|
||||
@@ -2714,18 +2569,13 @@ function resolveText(
|
||||
if (chunk) {
|
||||
releasePendingChunk(response, chunk);
|
||||
}
|
||||
const newChunk = createInitializedTextChunk(response, text);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
chunks.set(id, createInitializedTextChunk(response, text));
|
||||
}
|
||||
|
||||
function resolveBuffer(
|
||||
response: Response,
|
||||
id: number,
|
||||
buffer: $ArrayBufferView | ArrayBuffer,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
@@ -2740,18 +2590,13 @@ function resolveBuffer(
|
||||
if (chunk) {
|
||||
releasePendingChunk(response, chunk);
|
||||
}
|
||||
const newChunk = createInitializedBufferChunk(response, buffer);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
chunks.set(id, createInitializedBufferChunk(response, buffer));
|
||||
}
|
||||
|
||||
function resolveModule(
|
||||
response: Response,
|
||||
id: number,
|
||||
model: UninitializedModel,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
@@ -2788,24 +2633,14 @@ function resolveModule(
|
||||
blockedChunk = (chunk: any);
|
||||
blockedChunk.status = BLOCKED;
|
||||
}
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, blockedChunk);
|
||||
}
|
||||
promise.then(
|
||||
() => resolveModuleChunk(response, blockedChunk, clientReference),
|
||||
error => triggerErrorOnChunk(response, blockedChunk, error),
|
||||
);
|
||||
} else {
|
||||
if (!chunk) {
|
||||
const newChunk = createResolvedModuleChunk(response, clientReference);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
chunks.set(id, createResolvedModuleChunk(response, clientReference));
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, chunk);
|
||||
}
|
||||
// This can't actually happen because we don't have any forward
|
||||
// references to modules.
|
||||
resolveModuleChunk(response, chunk, clientReference);
|
||||
@@ -2818,21 +2653,13 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
|
||||
id: number,
|
||||
stream: T,
|
||||
controller: FlightStreamController,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
if (!chunk) {
|
||||
const newChunk = createInitializedStreamChunk(response, stream, controller);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
chunks.set(id, createInitializedStreamChunk(response, stream, controller));
|
||||
return;
|
||||
}
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, chunk);
|
||||
}
|
||||
if (chunk.status !== PENDING) {
|
||||
// We already resolved. We didn't expect to see this.
|
||||
return;
|
||||
@@ -2888,7 +2715,6 @@ function startReadableStream<T>(
|
||||
response: Response,
|
||||
id: number,
|
||||
type: void | 'bytes',
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
let controller: ReadableStreamController = (null: any);
|
||||
const stream = new ReadableStream({
|
||||
@@ -2969,7 +2795,7 @@ function startReadableStream<T>(
|
||||
}
|
||||
},
|
||||
};
|
||||
resolveStream(response, id, stream, flightController, streamState);
|
||||
resolveStream(response, id, stream, flightController);
|
||||
}
|
||||
|
||||
function asyncIterator(this: $AsyncIterator<any, any, void>) {
|
||||
@@ -2995,7 +2821,6 @@ function startAsyncIterable<T>(
|
||||
response: Response,
|
||||
id: number,
|
||||
iterator: boolean,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
const buffer: Array<SomeChunk<IteratorResult<T, T>>> = [];
|
||||
let closed = false;
|
||||
@@ -3113,7 +2938,6 @@ function startAsyncIterable<T>(
|
||||
id,
|
||||
iterator ? iterable[ASYNC_ITERATOR]() : iterable,
|
||||
flightController,
|
||||
streamState,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3193,11 +3017,7 @@ function resolveErrorDev(
|
||||
return error;
|
||||
}
|
||||
|
||||
function resolvePostponeProd(
|
||||
response: Response,
|
||||
id: number,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
function resolvePostponeProd(response: Response, id: number): void {
|
||||
if (__DEV__) {
|
||||
// These errors should never make it into a build so we don't need to encode them in codes.json
|
||||
// eslint-disable-next-line react-internal/prod-error-codes
|
||||
@@ -3215,11 +3035,7 @@ function resolvePostponeProd(
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
if (!chunk) {
|
||||
const newChunk: ErroredChunk<any> = createErrorChunk(
|
||||
response,
|
||||
postponeInstance,
|
||||
);
|
||||
chunks.set(id, newChunk);
|
||||
chunks.set(id, createErrorChunk(response, postponeInstance));
|
||||
} else {
|
||||
triggerErrorOnChunk(response, chunk, postponeInstance);
|
||||
}
|
||||
@@ -3231,7 +3047,6 @@ function resolvePostponeDev(
|
||||
reason: string,
|
||||
stack: ReactStackTrace,
|
||||
env: string,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
if (!__DEV__) {
|
||||
// These errors should never make it into a build so we don't need to encode them in codes.json
|
||||
@@ -3259,18 +3074,8 @@ function resolvePostponeDev(
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
if (!chunk) {
|
||||
const newChunk: ErroredChunk<any> = createErrorChunk(
|
||||
response,
|
||||
postponeInstance,
|
||||
);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
chunks.set(id, createErrorChunk(response, postponeInstance));
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, chunk);
|
||||
}
|
||||
triggerErrorOnChunk(response, chunk, postponeInstance);
|
||||
}
|
||||
}
|
||||
@@ -3279,7 +3084,6 @@ function resolveErrorModel(
|
||||
response: Response,
|
||||
id: number,
|
||||
row: UninitializedModel,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
@@ -3293,18 +3097,8 @@ function resolveErrorModel(
|
||||
(error: any).digest = errorInfo.digest;
|
||||
const errorWithDigest: ErrorWithDigest = (error: any);
|
||||
if (!chunk) {
|
||||
const newChunk: ErroredChunk<any> = createErrorChunk(
|
||||
response,
|
||||
errorWithDigest,
|
||||
);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
chunks.set(id, createErrorChunk(response, errorWithDigest));
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, chunk);
|
||||
}
|
||||
triggerErrorOnChunk(response, chunk, errorWithDigest);
|
||||
}
|
||||
}
|
||||
@@ -4039,7 +3833,6 @@ function resolveTypedArray(
|
||||
lastChunk: Uint8Array,
|
||||
constructor: any,
|
||||
bytesPerElement: number,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
// If the view fits into one original buffer, we just reuse that buffer instead of
|
||||
// copying it out to a separate copy. This means that it's not always possible to
|
||||
@@ -4059,7 +3852,7 @@ function resolveTypedArray(
|
||||
chunk.byteOffset,
|
||||
chunk.byteLength / bytesPerElement,
|
||||
);
|
||||
resolveBuffer(response, id, view, streamState);
|
||||
resolveBuffer(response, id, view);
|
||||
}
|
||||
|
||||
function logComponentInfo(
|
||||
@@ -4376,7 +4169,6 @@ function flushInitialRenderPerformance(response: Response): void {
|
||||
|
||||
function processFullBinaryRow(
|
||||
response: Response,
|
||||
streamState: StreamState,
|
||||
id: number,
|
||||
tag: number,
|
||||
buffer: Array<Uint8Array>,
|
||||
@@ -4385,125 +4177,47 @@ function processFullBinaryRow(
|
||||
switch (tag) {
|
||||
case 65 /* "A" */:
|
||||
// We must always clone to extract it into a separate buffer instead of just a view.
|
||||
resolveBuffer(
|
||||
response,
|
||||
id,
|
||||
mergeBuffer(buffer, chunk).buffer,
|
||||
streamState,
|
||||
);
|
||||
resolveBuffer(response, id, mergeBuffer(buffer, chunk).buffer);
|
||||
return;
|
||||
case 79 /* "O" */:
|
||||
resolveTypedArray(response, id, buffer, chunk, Int8Array, 1, streamState);
|
||||
resolveTypedArray(response, id, buffer, chunk, Int8Array, 1);
|
||||
return;
|
||||
case 111 /* "o" */:
|
||||
resolveBuffer(
|
||||
response,
|
||||
id,
|
||||
buffer.length === 0 ? chunk : mergeBuffer(buffer, chunk),
|
||||
streamState,
|
||||
);
|
||||
return;
|
||||
case 85 /* "U" */:
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
Uint8ClampedArray,
|
||||
1,
|
||||
streamState,
|
||||
);
|
||||
resolveTypedArray(response, id, buffer, chunk, Uint8ClampedArray, 1);
|
||||
return;
|
||||
case 83 /* "S" */:
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
Int16Array,
|
||||
2,
|
||||
streamState,
|
||||
);
|
||||
resolveTypedArray(response, id, buffer, chunk, Int16Array, 2);
|
||||
return;
|
||||
case 115 /* "s" */:
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
Uint16Array,
|
||||
2,
|
||||
streamState,
|
||||
);
|
||||
resolveTypedArray(response, id, buffer, chunk, Uint16Array, 2);
|
||||
return;
|
||||
case 76 /* "L" */:
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
Int32Array,
|
||||
4,
|
||||
streamState,
|
||||
);
|
||||
resolveTypedArray(response, id, buffer, chunk, Int32Array, 4);
|
||||
return;
|
||||
case 108 /* "l" */:
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
Uint32Array,
|
||||
4,
|
||||
streamState,
|
||||
);
|
||||
resolveTypedArray(response, id, buffer, chunk, Uint32Array, 4);
|
||||
return;
|
||||
case 71 /* "G" */:
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
Float32Array,
|
||||
4,
|
||||
streamState,
|
||||
);
|
||||
resolveTypedArray(response, id, buffer, chunk, Float32Array, 4);
|
||||
return;
|
||||
case 103 /* "g" */:
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
Float64Array,
|
||||
8,
|
||||
streamState,
|
||||
);
|
||||
resolveTypedArray(response, id, buffer, chunk, Float64Array, 8);
|
||||
return;
|
||||
case 77 /* "M" */:
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
BigInt64Array,
|
||||
8,
|
||||
streamState,
|
||||
);
|
||||
resolveTypedArray(response, id, buffer, chunk, BigInt64Array, 8);
|
||||
return;
|
||||
case 109 /* "m" */:
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
BigUint64Array,
|
||||
8,
|
||||
streamState,
|
||||
);
|
||||
resolveTypedArray(response, id, buffer, chunk, BigUint64Array, 8);
|
||||
return;
|
||||
case 86 /* "V" */:
|
||||
resolveTypedArray(response, id, buffer, chunk, DataView, 1, streamState);
|
||||
resolveTypedArray(response, id, buffer, chunk, DataView, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4513,19 +4227,18 @@ function processFullBinaryRow(
|
||||
row += readPartialStringChunk(stringDecoder, buffer[i]);
|
||||
}
|
||||
row += readFinalStringChunk(stringDecoder, chunk);
|
||||
processFullStringRow(response, streamState, id, tag, row);
|
||||
processFullStringRow(response, id, tag, row);
|
||||
}
|
||||
|
||||
function processFullStringRow(
|
||||
response: Response,
|
||||
streamState: StreamState,
|
||||
id: number,
|
||||
tag: number,
|
||||
row: string,
|
||||
): void {
|
||||
switch (tag) {
|
||||
case 73 /* "I" */: {
|
||||
resolveModule(response, id, row, streamState);
|
||||
resolveModule(response, id, row);
|
||||
return;
|
||||
}
|
||||
case 72 /* "H" */: {
|
||||
@@ -4534,11 +4247,11 @@ function processFullStringRow(
|
||||
return;
|
||||
}
|
||||
case 69 /* "E" */: {
|
||||
resolveErrorModel(response, id, row, streamState);
|
||||
resolveErrorModel(response, id, row);
|
||||
return;
|
||||
}
|
||||
case 84 /* "T" */: {
|
||||
resolveText(response, id, row, streamState);
|
||||
resolveText(response, id, row);
|
||||
return;
|
||||
}
|
||||
case 78 /* "N" */: {
|
||||
@@ -4583,22 +4296,22 @@ function processFullStringRow(
|
||||
);
|
||||
}
|
||||
case 82 /* "R" */: {
|
||||
startReadableStream(response, id, undefined, streamState);
|
||||
startReadableStream(response, id, undefined);
|
||||
return;
|
||||
}
|
||||
// Fallthrough
|
||||
case 114 /* "r" */: {
|
||||
startReadableStream(response, id, 'bytes', streamState);
|
||||
startReadableStream(response, id, 'bytes');
|
||||
return;
|
||||
}
|
||||
// Fallthrough
|
||||
case 88 /* "X" */: {
|
||||
startAsyncIterable(response, id, false, streamState);
|
||||
startAsyncIterable(response, id, false);
|
||||
return;
|
||||
}
|
||||
// Fallthrough
|
||||
case 120 /* "x" */: {
|
||||
startAsyncIterable(response, id, true, streamState);
|
||||
startAsyncIterable(response, id, true);
|
||||
return;
|
||||
}
|
||||
// Fallthrough
|
||||
@@ -4617,10 +4330,9 @@ function processFullStringRow(
|
||||
postponeInfo.reason,
|
||||
postponeInfo.stack,
|
||||
postponeInfo.env,
|
||||
streamState,
|
||||
);
|
||||
} else {
|
||||
resolvePostponeProd(response, id, streamState);
|
||||
resolvePostponeProd(response, id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -4632,7 +4344,7 @@ function processFullStringRow(
|
||||
return;
|
||||
}
|
||||
// We assume anything else is JSON.
|
||||
resolveModel(response, id, row, streamState);
|
||||
resolveModel(response, id, row);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -4655,7 +4367,6 @@ export function processBinaryChunk(
|
||||
let rowLength = streamState._rowLength;
|
||||
const buffer = streamState._buffer;
|
||||
const chunkLength = chunk.length;
|
||||
incrementChunkDebugInfo(streamState, chunkLength);
|
||||
while (i < chunkLength) {
|
||||
let lastIdx = -1;
|
||||
switch (rowState) {
|
||||
@@ -4735,14 +4446,7 @@ export function processBinaryChunk(
|
||||
// We found the last chunk of the row
|
||||
const length = lastIdx - i;
|
||||
const lastChunk = new Uint8Array(chunk.buffer, offset, length);
|
||||
processFullBinaryRow(
|
||||
response,
|
||||
streamState,
|
||||
rowID,
|
||||
rowTag,
|
||||
buffer,
|
||||
lastChunk,
|
||||
);
|
||||
processFullBinaryRow(response, rowID, rowTag, buffer, lastChunk);
|
||||
// Reset state machine for a new row
|
||||
i = lastIdx;
|
||||
if (rowState === ROW_CHUNK_BY_NEWLINE) {
|
||||
@@ -4797,7 +4501,6 @@ export function processStringChunk(
|
||||
let rowLength = streamState._rowLength;
|
||||
const buffer = streamState._buffer;
|
||||
const chunkLength = chunk.length;
|
||||
incrementChunkDebugInfo(streamState, chunkLength);
|
||||
while (i < chunkLength) {
|
||||
let lastIdx = -1;
|
||||
switch (rowState) {
|
||||
@@ -4896,7 +4599,7 @@ export function processStringChunk(
|
||||
);
|
||||
}
|
||||
const lastChunk = chunk.slice(i, lastIdx);
|
||||
processFullStringRow(response, streamState, rowID, rowTag, lastChunk);
|
||||
processFullStringRow(response, rowID, rowTag, lastChunk);
|
||||
// Reset state machine for a new row
|
||||
i = lastIdx;
|
||||
if (rowState === ROW_CHUNK_BY_NEWLINE) {
|
||||
|
||||
@@ -85,11 +85,7 @@ function getDebugInfo(obj) {
|
||||
if (debugInfo) {
|
||||
const copy = [];
|
||||
for (let i = 0; i < debugInfo.length; i++) {
|
||||
if (debugInfo[i].awaited && debugInfo[i].awaited.name === 'RSC stream') {
|
||||
// Ignore RSC stream I/O info.
|
||||
} else {
|
||||
copy.push(normalizeComponentInfo(debugInfo[i]));
|
||||
}
|
||||
copy.push(normalizeComponentInfo(debugInfo[i]));
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
@@ -2836,7 +2832,7 @@ describe('ReactFlight', () => {
|
||||
transport: expect.arrayContaining([]),
|
||||
},
|
||||
},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 53 : 21},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 23 : 21},
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
@@ -2847,7 +2843,7 @@ describe('ReactFlight', () => {
|
||||
expect(getDebugInfo(thirdPartyChildren[0])).toEqual(
|
||||
__DEV__
|
||||
? [
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22}, // Clamped to the start
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, // Clamped to the start
|
||||
{
|
||||
name: 'ThirdPartyComponent',
|
||||
env: 'third-party',
|
||||
@@ -2855,15 +2851,15 @@ describe('ReactFlight', () => {
|
||||
stack: ' in Object.<anonymous> (at **)',
|
||||
props: {},
|
||||
},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 55 : 23}, // This last one is when the promise resolved into the first party.
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 25 : 23}, // This last one is when the promise resolved into the first party.
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
expect(getDebugInfo(thirdPartyChildren[1])).toEqual(
|
||||
__DEV__
|
||||
? [
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22}, // Clamped to the start
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, // Clamped to the start
|
||||
{
|
||||
name: 'ThirdPartyLazyComponent',
|
||||
env: 'third-party',
|
||||
@@ -2871,14 +2867,14 @@ describe('ReactFlight', () => {
|
||||
stack: ' in myLazy (at **)\n in lazyInitializer (at **)',
|
||||
props: {},
|
||||
},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
expect(getDebugInfo(thirdPartyChildren[2])).toEqual(
|
||||
__DEV__
|
||||
? [
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
|
||||
{
|
||||
name: 'ThirdPartyFragmentComponent',
|
||||
env: 'third-party',
|
||||
@@ -2886,7 +2882,7 @@ describe('ReactFlight', () => {
|
||||
stack: ' in Object.<anonymous> (at **)',
|
||||
props: {},
|
||||
},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
@@ -2964,10 +2960,17 @@ describe('ReactFlight', () => {
|
||||
{
|
||||
time: 16,
|
||||
},
|
||||
{
|
||||
env: 'third-party',
|
||||
key: null,
|
||||
name: 'ThirdPartyAsyncIterableComponent',
|
||||
props: {},
|
||||
stack: ' in Object.<anonymous> (at **)',
|
||||
},
|
||||
{
|
||||
time: 16,
|
||||
},
|
||||
{time: 31},
|
||||
{time: 17},
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
@@ -2976,7 +2979,7 @@ describe('ReactFlight', () => {
|
||||
expect(getDebugInfo(thirdPartyFragment)).toEqual(
|
||||
__DEV__
|
||||
? [
|
||||
{time: 32},
|
||||
{time: 18},
|
||||
{
|
||||
name: 'Keyed',
|
||||
env: 'Server',
|
||||
@@ -2987,14 +2990,19 @@ describe('ReactFlight', () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
time: 33,
|
||||
time: 19,
|
||||
},
|
||||
{
|
||||
time: 33,
|
||||
time: 19,
|
||||
},
|
||||
{
|
||||
time: 33,
|
||||
env: 'third-party',
|
||||
key: null,
|
||||
name: 'ThirdPartyAsyncIterableComponent',
|
||||
props: {},
|
||||
stack: ' in Object.<anonymous> (at **)',
|
||||
},
|
||||
{time: 19},
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
@@ -3002,7 +3010,7 @@ describe('ReactFlight', () => {
|
||||
expect(getDebugInfo(thirdPartyFragment.props.children)).toEqual(
|
||||
__DEV__
|
||||
? [
|
||||
{time: 33}, // Clamp to the start
|
||||
{time: 19}, // Clamp to the start
|
||||
{
|
||||
name: 'ThirdPartyAsyncIterableComponent',
|
||||
env: 'third-party',
|
||||
@@ -3010,7 +3018,7 @@ describe('ReactFlight', () => {
|
||||
stack: ' in Object.<anonymous> (at **)',
|
||||
props: {},
|
||||
},
|
||||
{time: 33},
|
||||
{time: 19},
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
@@ -3073,7 +3081,7 @@ describe('ReactFlight', () => {
|
||||
props: {},
|
||||
},
|
||||
{time: 16},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 17},
|
||||
{time: 17},
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
@@ -3839,115 +3847,4 @@ describe('ReactFlight', () => {
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(<div>not using props</div>);
|
||||
});
|
||||
|
||||
// @gate !__DEV__ || enableComponentPerformanceTrack
|
||||
it('produces correct parent stacks', async () => {
|
||||
function Container() {
|
||||
return ReactServer.createElement('div', null);
|
||||
}
|
||||
function ContainerParent() {
|
||||
return ReactServer.createElement(Container, null);
|
||||
}
|
||||
function App() {
|
||||
return ReactServer.createElement(
|
||||
'main',
|
||||
null,
|
||||
ReactServer.createElement(ContainerParent, null),
|
||||
);
|
||||
}
|
||||
|
||||
const transport = ReactNoopFlightServer.render({
|
||||
root: ReactServer.createElement(App, null),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const {root} = await ReactNoopFlightClient.read(transport);
|
||||
|
||||
ReactNoop.render(root);
|
||||
|
||||
expect(root.type).toBe('main');
|
||||
if (__DEV__) {
|
||||
const div = root.props.children;
|
||||
expect(getDebugInfo(div)).toEqual([
|
||||
{
|
||||
time: 14,
|
||||
},
|
||||
{
|
||||
env: 'Server',
|
||||
key: null,
|
||||
name: 'ContainerParent',
|
||||
owner: {
|
||||
env: 'Server',
|
||||
key: null,
|
||||
name: 'App',
|
||||
props: {},
|
||||
stack: ' in Object.<anonymous> (at **)',
|
||||
},
|
||||
props: {},
|
||||
stack: ' in App (at **)',
|
||||
},
|
||||
{
|
||||
time: 15,
|
||||
},
|
||||
{
|
||||
env: 'Server',
|
||||
key: null,
|
||||
name: 'Container',
|
||||
owner: {
|
||||
env: 'Server',
|
||||
key: null,
|
||||
name: 'ContainerParent',
|
||||
owner: {
|
||||
env: 'Server',
|
||||
key: null,
|
||||
name: 'App',
|
||||
props: {},
|
||||
stack: ' in Object.<anonymous> (at **)',
|
||||
},
|
||||
props: {},
|
||||
stack: ' in App (at **)',
|
||||
},
|
||||
props: {},
|
||||
stack: ' in ContainerParent (at **)',
|
||||
},
|
||||
{
|
||||
time: 16,
|
||||
},
|
||||
]);
|
||||
expect(getDebugInfo(root)).toEqual([
|
||||
{
|
||||
time: 12,
|
||||
},
|
||||
{
|
||||
env: 'Server',
|
||||
key: null,
|
||||
name: 'App',
|
||||
props: {},
|
||||
stack: ' in Object.<anonymous> (at **)',
|
||||
},
|
||||
{
|
||||
time: 13,
|
||||
},
|
||||
{
|
||||
time: 14,
|
||||
},
|
||||
{
|
||||
time: 15,
|
||||
},
|
||||
{
|
||||
time: 16,
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
expect(root._debugInfo).toBe(undefined);
|
||||
expect(root._owner).toBe(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<main>
|
||||
<div />
|
||||
</main>,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
24
packages/react-markup/src/ReactMarkupServer.js
vendored
24
packages/react-markup/src/ReactMarkupServer.js
vendored
@@ -81,18 +81,7 @@ export function experimental_renderToHTML(
|
||||
options?: MarkupOptions,
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const flightResponse = createFlightResponse(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
noServerCallOrFormAction,
|
||||
noServerCallOrFormAction,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
const streamState = createFlightStreamState(flightResponse, null);
|
||||
const streamState = createFlightStreamState();
|
||||
const flightDestination = {
|
||||
push(chunk: string | null): boolean {
|
||||
if (chunk !== null) {
|
||||
@@ -186,6 +175,17 @@ export function experimental_renderToHTML(
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
const flightResponse = createFlightResponse(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
noServerCallOrFormAction,
|
||||
noServerCallOrFormAction,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
const resumableState = createResumableState(
|
||||
options ? options.identifierPrefix : undefined,
|
||||
undefined,
|
||||
|
||||
@@ -77,7 +77,7 @@ function read<T>(source: Source, options: ReadOptions): Thenable<T> {
|
||||
? options.debugChannel.onMessage
|
||||
: undefined,
|
||||
);
|
||||
const streamState = createStreamState(response, source);
|
||||
const streamState = createStreamState();
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
processBinaryChunk(response, streamState, source[i], 0);
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ function startReadingFromUniversalStream(
|
||||
// This is the same as startReadingFromStream except this allows WebSocketStreams which
|
||||
// return ArrayBuffer and string chunks instead of Uint8Array chunks. We could potentially
|
||||
// always allow streams with variable chunk types.
|
||||
const streamState = createStreamState(response, stream);
|
||||
const streamState = createStreamState();
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -149,9 +149,8 @@ function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
onDone: () => void,
|
||||
debugValue: mixed,
|
||||
): void {
|
||||
const streamState = createStreamState(response, debugValue);
|
||||
const streamState = createStreamState();
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -195,14 +194,9 @@ function createFromReadableStream<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, stream, handleDone, stream);
|
||||
startReadingFromStream(response, stream, handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
stream,
|
||||
close.bind(null, response),
|
||||
stream,
|
||||
);
|
||||
startReadingFromStream(response, stream, close.bind(null, response));
|
||||
}
|
||||
return getRoot(response);
|
||||
}
|
||||
@@ -231,13 +225,12 @@ function createFromFetch<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, (r.body: any), handleDone, r);
|
||||
startReadingFromStream(response, (r.body: any), handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
(r.body: any),
|
||||
close.bind(null, response),
|
||||
r,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -66,7 +66,7 @@ function startReadingFromStream(
|
||||
stream: Readable,
|
||||
onEnd: () => void,
|
||||
): void {
|
||||
const streamState = createStreamState(response, stream);
|
||||
const streamState = createStreamState();
|
||||
|
||||
stream.on('data', chunk => {
|
||||
if (typeof chunk === 'string') {
|
||||
|
||||
@@ -141,7 +141,7 @@ function startReadingFromUniversalStream(
|
||||
// This is the same as startReadingFromStream except this allows WebSocketStreams which
|
||||
// return ArrayBuffer and string chunks instead of Uint8Array chunks. We could potentially
|
||||
// always allow streams with variable chunk types.
|
||||
const streamState = createStreamState(response, stream);
|
||||
const streamState = createStreamState();
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -175,9 +175,8 @@ function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
onDone: () => void,
|
||||
debugValue: mixed,
|
||||
): void {
|
||||
const streamState = createStreamState(response, debugValue);
|
||||
const streamState = createStreamState();
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -229,14 +228,9 @@ export function createFromReadableStream<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, stream, handleDone, stream);
|
||||
startReadingFromStream(response, stream, handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
stream,
|
||||
close.bind(null, response),
|
||||
stream,
|
||||
);
|
||||
startReadingFromStream(response, stream, close.bind(null, response));
|
||||
}
|
||||
return getRoot(response);
|
||||
}
|
||||
@@ -265,13 +259,12 @@ export function createFromFetch<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, (r.body: any), handleDone, r);
|
||||
startReadingFromStream(response, (r.body: any), handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
(r.body: any),
|
||||
close.bind(null, response),
|
||||
r,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -115,9 +115,8 @@ function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
onDone: () => void,
|
||||
debugValue: mixed,
|
||||
): void {
|
||||
const streamState = createStreamState(response, debugValue);
|
||||
const streamState = createStreamState();
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -159,14 +158,9 @@ export function createFromReadableStream<T>(
|
||||
}
|
||||
};
|
||||
startReadingFromStream(response, options.debugChannel.readable, handleDone);
|
||||
startReadingFromStream(response, stream, handleDone, stream);
|
||||
startReadingFromStream(response, stream, handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
stream,
|
||||
close.bind(null, response),
|
||||
stream,
|
||||
);
|
||||
startReadingFromStream(response, stream, close.bind(null, response));
|
||||
}
|
||||
|
||||
return getRoot(response);
|
||||
@@ -196,13 +190,12 @@ export function createFromFetch<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, (r.body: any), handleDone, r);
|
||||
startReadingFromStream(response, (r.body: any), handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
(r.body: any),
|
||||
close.bind(null, response),
|
||||
r,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -61,7 +61,7 @@ function startReadingFromStream(
|
||||
stream: Readable,
|
||||
onEnd: () => void,
|
||||
): void {
|
||||
const streamState = createStreamState(response, stream);
|
||||
const streamState = createStreamState();
|
||||
|
||||
stream.on('data', chunk => {
|
||||
if (typeof chunk === 'string') {
|
||||
|
||||
@@ -114,7 +114,7 @@ function startReadingFromUniversalStream(
|
||||
// This is the same as startReadingFromStream except this allows WebSocketStreams which
|
||||
// return ArrayBuffer and string chunks instead of Uint8Array chunks. We could potentially
|
||||
// always allow streams with variable chunk types.
|
||||
const streamState = createStreamState(response, stream);
|
||||
const streamState = createStreamState();
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -148,9 +148,8 @@ function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
onDone: () => void,
|
||||
debugValue: mixed,
|
||||
): void {
|
||||
const streamState = createStreamState(response, debugValue);
|
||||
const streamState = createStreamState();
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -195,14 +194,9 @@ function createFromReadableStream<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, stream, handleDone, stream);
|
||||
startReadingFromStream(response, stream, handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
stream,
|
||||
close.bind(null, response),
|
||||
stream,
|
||||
);
|
||||
startReadingFromStream(response, stream, close.bind(null, response));
|
||||
}
|
||||
return getRoot(response);
|
||||
}
|
||||
@@ -231,13 +225,12 @@ function createFromFetch<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, (r.body: any), handleDone, r);
|
||||
startReadingFromStream(response, (r.body: any), handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
(r.body: any),
|
||||
close.bind(null, response),
|
||||
r,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -117,9 +117,8 @@ function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
onDone: () => void,
|
||||
debugValue: mixed,
|
||||
): void {
|
||||
const streamState = createStreamState(response, debugValue);
|
||||
const streamState = createStreamState();
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -161,14 +160,9 @@ function createFromReadableStream<T>(
|
||||
}
|
||||
};
|
||||
startReadingFromStream(response, options.debugChannel.readable, handleDone);
|
||||
startReadingFromStream(response, stream, handleDone, stream);
|
||||
startReadingFromStream(response, stream, handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
stream,
|
||||
close.bind(null, response),
|
||||
stream,
|
||||
);
|
||||
startReadingFromStream(response, stream, close.bind(null, response));
|
||||
}
|
||||
|
||||
return getRoot(response);
|
||||
@@ -198,13 +192,12 @@ function createFromFetch<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, (r.body: any), handleDone, r);
|
||||
startReadingFromStream(response, (r.body: any), handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
(r.body: any),
|
||||
close.bind(null, response),
|
||||
r,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -69,7 +69,7 @@ function startReadingFromStream(
|
||||
stream: Readable,
|
||||
onEnd: () => void,
|
||||
): void {
|
||||
const streamState = createStreamState(response, stream);
|
||||
const streamState = createStreamState();
|
||||
|
||||
stream.on('data', chunk => {
|
||||
if (typeof chunk === 'string') {
|
||||
|
||||
@@ -1239,24 +1239,17 @@ describe('ReactFlightDOMEdge', () => {
|
||||
name: 'Greeting',
|
||||
env: 'Server',
|
||||
});
|
||||
if (gate(flags => flags.enableAsyncDebugInfo)) {
|
||||
expect(lazyWrapper._debugInfo).toEqual([
|
||||
{time: 12},
|
||||
greetInfo,
|
||||
{time: 13},
|
||||
expect.objectContaining({
|
||||
name: 'Container',
|
||||
env: 'Server',
|
||||
owner: greetInfo,
|
||||
}),
|
||||
{time: 14},
|
||||
expect.objectContaining({
|
||||
awaited: expect.objectContaining({
|
||||
name: 'RSC stream',
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
}
|
||||
expect(lazyWrapper._debugInfo).toEqual([
|
||||
{time: 12},
|
||||
greetInfo,
|
||||
{time: 13},
|
||||
expect.objectContaining({
|
||||
name: 'Container',
|
||||
env: 'Server',
|
||||
owner: greetInfo,
|
||||
}),
|
||||
{time: 14},
|
||||
]);
|
||||
// The owner that created the span was the outer server component.
|
||||
// We expect the debug info to be referentially equal to the owner.
|
||||
expect(greeting._owner).toBe(lazyWrapper._debugInfo[1]);
|
||||
|
||||
@@ -114,7 +114,7 @@ function startReadingFromUniversalStream(
|
||||
// This is the same as startReadingFromStream except this allows WebSocketStreams which
|
||||
// return ArrayBuffer and string chunks instead of Uint8Array chunks. We could potentially
|
||||
// always allow streams with variable chunk types.
|
||||
const streamState = createStreamState(response, stream);
|
||||
const streamState = createStreamState();
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -148,9 +148,8 @@ function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
onDone: () => void,
|
||||
debugValue: mixed,
|
||||
): void {
|
||||
const streamState = createStreamState(response, debugValue);
|
||||
const streamState = createStreamState();
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -195,14 +194,9 @@ function createFromReadableStream<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, stream, handleDone, stream);
|
||||
startReadingFromStream(response, stream, handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
stream,
|
||||
close.bind(null, response),
|
||||
stream,
|
||||
);
|
||||
startReadingFromStream(response, stream, close.bind(null, response));
|
||||
}
|
||||
return getRoot(response);
|
||||
}
|
||||
@@ -231,13 +225,12 @@ function createFromFetch<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, (r.body: any), handleDone, r);
|
||||
startReadingFromStream(response, (r.body: any), handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
(r.body: any),
|
||||
close.bind(null, response),
|
||||
r,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -117,9 +117,8 @@ function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
onDone: () => void,
|
||||
debugValue: mixed,
|
||||
): void {
|
||||
const streamState = createStreamState(response, debugValue);
|
||||
const streamState = createStreamState();
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -161,14 +160,9 @@ function createFromReadableStream<T>(
|
||||
}
|
||||
};
|
||||
startReadingFromStream(response, options.debugChannel.readable, handleDone);
|
||||
startReadingFromStream(response, stream, handleDone, stream);
|
||||
startReadingFromStream(response, stream, handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
stream,
|
||||
close.bind(null, response),
|
||||
stream,
|
||||
);
|
||||
startReadingFromStream(response, stream, close.bind(null, response));
|
||||
}
|
||||
|
||||
return getRoot(response);
|
||||
@@ -198,13 +192,12 @@ function createFromFetch<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, (r.body: any), handleDone, r);
|
||||
startReadingFromStream(response, (r.body: any), handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
(r.body: any),
|
||||
close.bind(null, response),
|
||||
r,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -69,7 +69,7 @@ function startReadingFromStream(
|
||||
stream: Readable,
|
||||
onEnd: () => void,
|
||||
): void {
|
||||
const streamState = createStreamState(response, stream);
|
||||
const streamState = createStreamState();
|
||||
|
||||
stream.on('data', chunk => {
|
||||
if (typeof chunk === 'string') {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user