Compare commits

..

15 Commits

Author SHA1 Message Date
Jorge Cabiedes
d651f69bc1 [compiler] Added validation for local state and refined error messages 2025-09-09 14:11:27 -07:00
Jorge Cabiedes
853550e7c8 [compiler] Added check for if the same invalid setSate within an effect is used elsewhere 2025-09-09 14:11:24 -07:00
Jorge Cabiedes
5cf71b322d [compiler] Validation for values derived from props in useEffect ready 2025-09-09 14:11:22 -07:00
Jorge Cabiedes
f807ce6492 [compiler] Basic solution for instruction based prop derivation validation 2025-09-09 14:11:19 -07:00
Lauren Tan
7b38acca0b [compiler][wip] Extend ValidateNoDerivedComputationsInEffects for props derived effects
This PR adds infra to disambiguate between two types of derived state in effects:
  1. State derived from props
  2. State derived from other state

TODO:
- [ ] Props tracking through destructuring and property access does not seem to be propagated correctly inside of Functions' instructions (or i might be misunderstanding how we track aliasing effects)
- [ ] compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/invalid-derived-state-from-props-computed.js should be failing
- [ ] Handle "mixed" case where deps flow from at least one prop AND state. Should probably have a different error reason, to aid with categorization
2025-09-09 14:10:44 -07:00
Lauren Tan
1d9c3927ea [compiler] new tests for props derived
Adds some new test cases for ValidateNoDerivedComputationsInEffects.
2025-09-09 14:09:57 -07:00
Sebastian Markbåge
969a9790ad [Flight] Track I/O Entry for the RSC Stream itself (#34425)
One thing that can suspend is the downloading of the RSC stream itself.
This tracks an I/O entry for each Promise (`SomeChunk<T>`) that
represents the request to the RSC stream. As the value we use the
`Response` for `createFromFetch` (or the `ReadableStream` for
`createFromReadableStream`). The start time is when you called those.

Since we're not awaiting the whole stream, each I/O entry represents the
part of the stream up until it got unblocked. However, in a production
environment with TLS packets and buffering in practice the chunks
received by the client isn't exactly at the boundary of each row. It's a
bit longer into larger chunks. From testing, it seems like multiples of
16kb or 64kb uncompressed are common. To simulate a production
environment we group into roughly 64kb chunks if they happen in rapid
sequence. Note that this might be too small to give a good idea because
of the throttle many boundaries might be skipped anyway so this might
show too many.

The React DevTools will see each I/O entry as separate but dedupe if an
outer boundary already depends on the same chunk. This deduping makes it
so that small boundaries that are blocked on the same chunk, don't get
treated as having unique suspenders. If you have a boundary with large
content, then that content will likely be in a separate chunk which is
not in the parent and then it gets marked as.

This is all just an approximation. The goal of this is just to highlight
that very large boundaries will very likely suspend even if they don't
suspend on any I/O on the server. In practice, these boundaries can
float around a lot and it's really any Suspense boundary that might
suspend but some are more likely than others which this is meant to
highlight.

It also just lets you inspect how many bytes needs to be transferred
before you can show a particular part of the content, to give you an
idea that it's not just I/O on the server that might suspend.

If you don't use the debug channel it can be misleading since the data
in development mode stream will have a lot more data in it which leads
to more chunking.

Similarly to "client references" these I/O infos don't have an "env"
since it's the client that has the I/O and so those are excluded from
flushing in the Server performance tracks.

Note that currently the same Response can appear many times in the same
Instance of SuspenseNode in DevTools when there are multiple chunks. In
a follow up I'll show only the last one per Response at any given level.

Note that when a separate debugChannel is used it has its own I/O entry
that's on the `_debugInfo` for the debug chunks in that channel.
However, if everything works correctly these should never leak into the
DevTools UI since they should never be propagated from a debug chunk to
the values waited by the runtime. This is easy to break though.
2025-09-09 16:46:11 -04:00
Joseph Savona
665de2ed28 [compiler] Improve name hints for outlined functions (#34434)
The previous PR added name hints for anonymous functions, but didn't
handle the case of outlined functions. Here we do some cleanup around
function `id` and name hints:
* Make `HIRFunction.id` a ValidatedIdentifierName, which involved some
cleanup of the validation helpers
* Add `HIRFunction.nameHint: string` as a place to store the generated
name hints which are not valid identifiers
* Update Codegen to always use the `id` as the actual function name, and
only use nameHint as part of generating the object+property wrapper for
debug purposes.

This ensures we don't conflate synthesized hints with real function
names. Then, we also update OutlineFunctions to use the function name
_or_ the nameHint as the input to generating a unique identifier. This
isn't quite as nice as the object form since we lose our formatting, but
it's a simple step that gives more context to the developer than `_temp`
does.

Switching to output the object+property lookup form for outlined
functions is a bit more involved, let's do that in a follow-up.
2025-09-09 12:14:09 -07:00
mofeiZ
eda778b8ae [compiler] Fix false positive memo validation (alternative) (#34319)
Alternative to #34276

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

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

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

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

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

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

Co-authored-by: Joe Savona <joesavona@fb.com>
2025-09-09 14:26:52 -04:00
Jorge Cabiedes
1836b46fff [compiler] Have react-compiler eslint plugin return a RuleModule (#34421)
Eslint is expecting a map of [string] => RuleModule. Before we were
passing {rule: RuleModule, severity: ErrorSeverity} which was breaking
legacy Eslint configurations
2025-09-09 11:18:37 -07:00
Sebastian "Sebbie" Silbermann
eec50b17b3 [Flight] Only use debug component info for parent stacks (#34431) 2025-09-09 19:58:02 +02:00
Joseph Savona
a9410fb487 [compiler] Option to infer names for anonymous functions (#34410)
Adds a `@enableNameAnonymousFunctions` feature to infer helpful names
for anonymous functions within components and hooks. The logic is
inspired by a custom Next.js transform, flagged to us by @eps1lon, that
does something similar. Implementing this transform within React
Compiler means that all React (Compiler) users can benefit from more
helpful names when debugging.

The idea builds on the fact that JS engines try to infer helpful names
for anonymous functions (in stack traces) when those functions are
accessed through an object property lookup:

```js
({'a[xyz]': () => {
  throw new Error('hello!')
} }['a[xyz]'])()

// Stack trace:
Uncaught Error: hello!
    at a[xyz] (<anonymous>:1:26) // <-- note the name here
    at <anonymous>:1:60
```

The new NameAnonymousFunctions transform is gated by the above flag,
which is off by default. It attemps to infer names for functions as
follows:

First, determine a "local" name:
* Assigning a function to a named variable uses the variable name.
`const f = () => {}` gets the name "f".
* Passing the function as an argument to a function gets the name of the
function, ie `foo(() => ...)` get the name "foo()", `foo.bar(() => ...)`
gets the name "foo.bar()". Note the parenthesis to help understand that
it was part of a call.
* Passing the function to a known hook uses the name of the hook,
`useEffect(() => ...)` uses "useEffect()".
* Passing the function as a JSX prop uses the element and attr name, eg
`<div onClick={() => ...}` uses "<div>.onClick".

Second, the local name is combined with the name of the outer
component/hook, so the final names will be strings like `Component[f]`
or `useMyHook[useEffect()]`.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34410).
* #34434
* __->__ #34410
2025-09-09 10:22:19 -07:00
Sebastian "Sebbie" Silbermann
6b70072c4f [DevTools] Finalize heuristic for naming unnamed <Suspense> (#34428) 2025-09-09 17:56:26 +02:00
Ruslan Lesiutin
b2cff47472 [DevTools] feat: propagate fetchFileWithCaching from initialization options for Fusebox (#34429)
Each integrator: browser extension, Chrome DevTools Frontend fork,
Electron shell must define and provide `fetchFileWithCaching` in order
for DevTools to be able to fetch application resources, such as scripts
or source maps.

More specifically, if this is available, React DevTools will be able to
symbolicate source locations for component frames, owner stacks,
"suspended by" Promises call frames.

This will be available with the next release of React DevTools.
2025-09-09 13:00:53 +01:00
Sebastian "Sebbie" Silbermann
8943025358 [DevTools] Fix handling of host roots on mount (#34400) 2025-09-08 22:53:02 +02:00
62 changed files with 4040 additions and 635 deletions

View File

@@ -103,6 +103,7 @@ import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoF
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -324,6 +325,15 @@ function runWithEnvironment(
outlineJSX(hir);
}
if (env.config.enableNameAnonymousFunctions) {
nameAnonymousFunctions(hir);
log({
kind: 'hir',
name: 'NameAnonymousFunctions',
value: hir,
});
}
if (env.config.enableFunctionOutlining) {
outlineFunctions(hir, fbtOperands);
log({kind: 'hir', name: 'OutlineFunctions', value: hir});

View File

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

View File

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

View File

@@ -7,7 +7,11 @@
import {BindingKind} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerError} from '../CompilerError';
import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
} from '../CompilerError';
import {assertExhaustive} from '../Utils/utils';
import {Environment, ReactFunctionType} from './Environment';
import type {HookKind} from './ObjectShape';
@@ -15,6 +19,7 @@ import {Type, makeType} from './Types';
import {z} from 'zod';
import type {AliasingEffect} from '../Inference/AliasingEffects';
import {isReservedWord} from '../Utils/Keyword';
import {Err, Ok, Result} from '../Utils/Result';
/*
* *******************************************************************************************
@@ -53,7 +58,8 @@ export type SourceLocation = t.SourceLocation | typeof GeneratedSource;
*/
export type ReactiveFunction = {
loc: SourceLocation;
id: string | null;
id: ValidIdentifierName | null;
nameHint: string | null;
params: Array<Place | SpreadPattern>;
generator: boolean;
async: boolean;
@@ -275,7 +281,8 @@ export type ReactiveTryTerminal = {
// A function lowered to HIR form, ie where its body is lowered to an HIR control-flow graph
export type HIRFunction = {
loc: SourceLocation;
id: string | null;
id: ValidIdentifierName | null;
nameHint: string | null;
fnType: ReactFunctionType;
env: Environment;
params: Array<Place | SpreadPattern>;
@@ -1123,7 +1130,8 @@ export type JsxAttribute =
export type FunctionExpression = {
kind: 'FunctionExpression';
name: string | null;
name: ValidIdentifierName | null;
nameHint: string | null;
loweredFunc: LoweredFunction;
type:
| 'ArrowFunctionExpression'
@@ -1298,37 +1306,52 @@ export function forkTemporaryIdentifier(
};
}
export function validateIdentifierName(
name: string,
): Result<ValidatedIdentifier, CompilerError> {
if (isReservedWord(name)) {
const error = new CompilerError();
error.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Syntax,
reason: 'Expected a non-reserved identifier name',
description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
suggestions: null,
}).withDetails({
kind: 'error',
loc: GeneratedSource,
message: 'reserved word',
}),
);
return Err(error);
} else if (!t.isValidIdentifier(name)) {
const error = new CompilerError();
error.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Syntax,
reason: `Expected a valid identifier name`,
description: `\`${name}\` is not a valid JavaScript identifier`,
suggestions: null,
}).withDetails({
kind: 'error',
loc: GeneratedSource,
message: 'reserved word',
}),
);
}
return Ok({
kind: 'named',
value: name as ValidIdentifierName,
});
}
/**
* Creates a valid identifier name. This should *not* be used for synthesizing
* identifier names: only call this method for identifier names that appear in the
* original source code.
*/
export function makeIdentifierName(name: string): ValidatedIdentifier {
if (isReservedWord(name)) {
CompilerError.throwInvalidJS({
reason: 'Expected a non-reserved identifier name',
loc: GeneratedSource,
description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
suggestions: null,
});
} else {
CompilerError.invariant(t.isValidIdentifier(name), {
reason: `Expected a valid identifier name`,
description: `\`${name}\` is not a valid JavaScript identifier`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
suggestions: null,
});
}
return {
kind: 'named',
value: name as ValidIdentifierName,
};
return validateIdentifierName(name).unwrap();
}
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff