Compare commits

..

5 Commits

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

That change was sufficient for the original case like

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

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

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

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

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

* If the value is frozen, it's an ImmutableCapture edge
* If the values are mutable, it's a Capture
* If it's a context->context, context->mutable, or mutable->context, count it as MaybeAlias.
2025-09-09 13:18:05 -07:00
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
28 changed files with 917 additions and 524 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}
@@ -414,6 +415,15 @@ function runWithEnvironment(
});
}
if (env.config.enableNameAnonymousFunctions) {
nameAnonymousFunctions(hir);
log({
kind: 'hir',
name: 'NameAnonymougFunctions',
value: hir,
});
}
const reactiveFunction = buildReactiveFunction(hir);
log({
kind: 'reactive',

View File

@@ -3566,6 +3566,8 @@ function lowerFunctionToValue(
let name: string | null = null;
if (expr.isFunctionExpression()) {
name = expr.get('id')?.node?.name ?? null;
} else if (expr.isFunctionDeclaration()) {
name = expr.get('id')?.node?.name ?? null;
}
const loweredFunc = lowerFunction(builder, expr);
if (!loweredFunc) {

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

@@ -15,6 +15,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';
/*
* *******************************************************************************************
@@ -1298,6 +1299,15 @@ export function forkTemporaryIdentifier(
};
}
export function validateIdentifierName(
name: string,
): Result<ValidIdentifierName, null> {
if (isReservedWord(name) || !t.isValidIdentifier(name)) {
return Err(null);
}
return Ok(makeIdentifierName(name).value);
}
/**
* 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

View File

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

View File

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

View File

@@ -43,6 +43,7 @@ import {
ValidIdentifierName,
getHookKind,
makeIdentifierName,
validateIdentifierName,
} from '../HIR/HIR';
import {printIdentifier, printInstruction, printPlace} from '../HIR/PrintHIR';
import {eachPatternOperand} from '../HIR/visitors';
@@ -2326,6 +2327,11 @@ function codegenInstructionValue(
),
reactiveFunction,
).unwrap();
const validatedName =
instrValue.name != null
? validateIdentifierName(instrValue.name)
: Err(null);
if (instrValue.type === 'ArrowFunctionExpression') {
let body: t.BlockStatement | t.Expression = fn.body;
if (body.body.length === 1 && loweredFunc.directives.length == 0) {
@@ -2337,14 +2343,28 @@ function codegenInstructionValue(
value = t.arrowFunctionExpression(fn.params, body, fn.async);
} else {
value = t.functionExpression(
fn.id ??
(instrValue.name != null ? t.identifier(instrValue.name) : null),
validatedName
.map<t.Identifier | null>(name => t.identifier(name))
.unwrapOr(null),
fn.params,
fn.body,
fn.generator,
fn.async,
);
}
if (
cx.env.config.enableNameAnonymousFunctions &&
validatedName.isErr() &&
instrValue.name != null
) {
const name = instrValue.name;
value = t.memberExpression(
t.objectExpression([t.objectProperty(t.stringLiteral(name), value)]),
t.stringLiteral(name),
true,
false,
);
}
break;
}
case 'TaggedTemplateExpression': {

View File

@@ -0,0 +1,173 @@
/**
* 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.name = 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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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