Compare commits

..

1 Commits

Author SHA1 Message Date
Jorge Cabiedes Acosta
4c9faa3ec3 [compiler] Update ValidateNoDerivedComputationsInEffects_exp to log the error instead of throwing
Summary:
TSIA

Simple change to log errors in Pipeline.ts instead of throwing in the validation

Test Plan:
updated snap tests
2025-11-10 12:10:22 -08:00
26 changed files with 189 additions and 1507 deletions

View File

@@ -105,7 +105,6 @@ import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRan
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
import {validateNoDerivedComputationsInEffects_exp} from '../Validation/ValidateNoDerivedComputationsInEffects_exp';
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
import {validateSourceLocations} from '../Validation/ValidateSourceLocations';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -273,10 +272,12 @@ function runWithEnvironment(
validateNoSetStateInRender(hir).unwrap();
}
if (env.config.validateNoDerivedComputationsInEffects) {
validateNoDerivedComputationsInEffects(hir);
}
if (env.config.validateNoDerivedComputationsInEffects_exp) {
env.logErrors(validateNoDerivedComputationsInEffects_exp(hir));
} else if (env.config.validateNoDerivedComputationsInEffects) {
validateNoDerivedComputationsInEffects(hir);
}
if (env.config.validateNoSetStateInEffects) {
@@ -558,10 +559,6 @@ function runWithEnvironment(
log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
}
if (env.config.validateSourceLocations) {
validateSourceLocations(func, ast).unwrap();
}
/**
* This flag should be only set for unit / fixture tests to check
* that Forget correctly handles unexpected errors (e.g. exceptions

View File

@@ -364,13 +364,6 @@ export const EnvironmentConfigSchema = z.object({
validateNoCapitalizedCalls: z.nullable(z.array(z.string())).default(null),
validateBlocklistedImports: z.nullable(z.array(z.string())).default(null),
/**
* Validates that AST nodes generated during codegen have proper source locations.
* This is useful for debugging issues with source maps and Istanbul coverage.
* When enabled, the compiler will error if important source locations are missing in the generated AST.
*/
validateSourceLocations: z.boolean().default(false),
/**
* Validate against impure functions called during render
*/

View File

@@ -954,7 +954,6 @@ function applyEffect(
case ValueKind.Primitive: {
break;
}
case ValueKind.MaybeFrozen:
case ValueKind.Frozen: {
sourceType = 'frozen';
break;

View File

@@ -21,6 +21,7 @@ import {
isUseStateType,
BasicBlock,
isUseRefType,
GeneratedSource,
SourceLocation,
} from '../HIR';
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
@@ -41,8 +42,8 @@ type ValidationContext = {
readonly errors: CompilerError;
readonly derivationCache: DerivationCache;
readonly effects: Set<HIRFunction>;
readonly setStateLoads: Map<IdentifierId, IdentifierId | null>;
readonly setStateUsages: Map<IdentifierId, Set<SourceLocation>>;
readonly setStateCache: Map<string | undefined | null, Array<Place>>;
readonly effectSetStateCache: Map<string | undefined | null, Array<Place>>;
};
class DerivationCache {
@@ -179,16 +180,19 @@ export function validateNoDerivedComputationsInEffects_exp(
const errors = new CompilerError();
const effects: Set<HIRFunction> = new Set();
const setStateLoads: Map<IdentifierId, IdentifierId> = new Map();
const setStateUsages: Map<IdentifierId, Set<SourceLocation>> = new Map();
const setStateCache: Map<string | undefined | null, Array<Place>> = new Map();
const effectSetStateCache: Map<
string | undefined | null,
Array<Place>
> = new Map();
const context: ValidationContext = {
functions,
errors,
derivationCache,
effects,
setStateLoads,
setStateUsages,
setStateCache,
effectSetStateCache,
};
if (fn.fnType === 'Hook') {
@@ -277,61 +281,11 @@ function joinValue(
return 'fromPropsAndState';
}
function getRootSetState(
key: IdentifierId,
loads: Map<IdentifierId, IdentifierId | null>,
visited: Set<IdentifierId> = new Set(),
): IdentifierId | null {
if (visited.has(key)) {
return null;
}
visited.add(key);
const parentId = loads.get(key);
if (parentId === undefined) {
return null;
}
if (parentId === null) {
return key;
}
return getRootSetState(parentId, loads, visited);
}
function maybeRecordSetState(
instr: Instruction,
loads: Map<IdentifierId, IdentifierId | null>,
usages: Map<IdentifierId, Set<SourceLocation>>,
): void {
for (const operand of eachInstructionLValue(instr)) {
if (
instr.value.kind === 'LoadLocal' &&
loads.has(instr.value.place.identifier.id)
) {
loads.set(operand.identifier.id, instr.value.place.identifier.id);
} else {
if (isSetStateType(operand.identifier)) {
// this is a root setState
loads.set(operand.identifier.id, null);
}
}
const rootSetState = getRootSetState(operand.identifier.id, loads);
if (rootSetState !== null && usages.get(rootSetState) === undefined) {
usages.set(rootSetState, new Set([operand.loc]));
}
}
}
function recordInstructionDerivations(
instr: Instruction,
context: ValidationContext,
isFirstPass: boolean,
): void {
maybeRecordSetState(instr, context.setStateLoads, context.setStateUsages);
let typeOfValue: TypeOfValue = 'ignored';
let isSource: boolean = false;
const sources: Set<IdentifierId> = new Set();
@@ -358,25 +312,21 @@ function recordInstructionDerivations(
context.effects.add(effectFunction.loweredFunc.func);
}
} else if (isUseStateType(lvalue.identifier) && value.args.length > 0) {
typeOfValue = 'fromState';
context.derivationCache.addDerivationEntry(
lvalue,
new Set(),
typeOfValue,
true,
);
return;
isSource = true;
typeOfValue = joinValue(typeOfValue, 'fromState');
}
}
for (const operand of eachInstructionOperand(instr)) {
if (context.setStateLoads.has(operand.identifier.id)) {
const rootSetStateId = getRootSetState(
operand.identifier.id,
context.setStateLoads,
);
if (rootSetStateId !== null) {
context.setStateUsages.get(rootSetStateId)?.add(operand.loc);
if (
isSetStateType(operand.identifier) &&
operand.loc !== GeneratedSource &&
isFirstPass
) {
if (context.setStateCache.has(operand.loc.identifierName)) {
context.setStateCache.get(operand.loc.identifierName)!.push(operand);
} else {
context.setStateCache.set(operand.loc.identifierName, [operand]);
}
}
@@ -574,26 +524,6 @@ function renderTree(
return result;
}
function getFnLocalDeps(
fn: FunctionExpression | undefined,
): Set<IdentifierId> | undefined {
if (!fn) {
return undefined;
}
const deps: Set<IdentifierId> = new Set();
for (const [, block] of fn.loweredFunc.func.body.blocks) {
for (const instr of block.instructions) {
if (instr.value.kind === 'LoadLocal') {
deps.add(instr.value.place.identifier.id);
}
}
}
return deps;
}
function validateEffect(
effectFunction: HIRFunction,
context: ValidationContext,
@@ -602,33 +532,13 @@ function validateEffect(
const effectDerivedSetStateCalls: Array<{
value: CallExpression;
id: IdentifierId;
loc: SourceLocation;
sourceIds: Set<IdentifierId>;
typeOfValue: TypeOfValue;
}> = [];
const effectSetStateUsages: Map<
IdentifierId,
Set<SourceLocation>
> = new Map();
let cleanUpFunctionDeps: Set<IdentifierId> | undefined;
const globals: Set<IdentifierId> = new Set();
for (const block of effectFunction.body.blocks.values()) {
/*
* if the block is in an effect and is of type return then its an effect's cleanup function
* if the cleanup function depends on a value from which effect-set state is derived then
* we can't validate
*/
if (
block.terminal.kind === 'return' &&
block.terminal.returnVariant === 'Explicit'
) {
cleanUpFunctionDeps = getFnLocalDeps(
context.functions.get(block.terminal.value.identifier.id),
);
}
for (const pred of block.preds) {
if (!seenBlocks.has(pred)) {
// skip if block has a back edge
@@ -642,16 +552,19 @@ function validateEffect(
return;
}
maybeRecordSetState(instr, context.setStateLoads, effectSetStateUsages);
for (const operand of eachInstructionOperand(instr)) {
if (context.setStateLoads.has(operand.identifier.id)) {
const rootSetStateId = getRootSetState(
operand.identifier.id,
context.setStateLoads,
);
if (rootSetStateId !== null) {
effectSetStateUsages.get(rootSetStateId)?.add(operand.loc);
if (
isSetStateType(operand.identifier) &&
operand.loc !== GeneratedSource
) {
if (context.effectSetStateCache.has(operand.loc.identifierName)) {
context.effectSetStateCache
.get(operand.loc.identifierName)!
.push(operand);
} else {
context.effectSetStateCache.set(operand.loc.identifierName, [
operand,
]);
}
}
}
@@ -669,7 +582,7 @@ function validateEffect(
if (argMetadata !== undefined) {
effectDerivedSetStateCalls.push({
value: instr.value,
id: instr.value.callee.identifier.id,
loc: instr.value.callee.loc,
sourceIds: argMetadata.sourcesIds,
typeOfValue: argMetadata.typeOfValue,
});
@@ -703,17 +616,15 @@ function validateEffect(
}
for (const derivedSetStateCall of effectDerivedSetStateCalls) {
const rootSetStateCall = getRootSetState(
derivedSetStateCall.id,
context.setStateLoads,
);
if (
rootSetStateCall !== null &&
effectSetStateUsages.has(rootSetStateCall) &&
context.setStateUsages.has(rootSetStateCall) &&
effectSetStateUsages.get(rootSetStateCall)!.size ===
context.setStateUsages.get(rootSetStateCall)!.size - 1
derivedSetStateCall.loc !== GeneratedSource &&
context.effectSetStateCache.has(derivedSetStateCall.loc.identifierName) &&
context.setStateCache.has(derivedSetStateCall.loc.identifierName) &&
context.effectSetStateCache.get(derivedSetStateCall.loc.identifierName)!
.length ===
context.setStateCache.get(derivedSetStateCall.loc.identifierName)!
.length -
1
) {
const propsSet = new Set<string>();
const stateSet = new Set<string>();
@@ -739,12 +650,6 @@ function validateEffect(
),
);
for (const dep of derivedSetStateCall.sourceIds) {
if (cleanUpFunctionDeps !== undefined && cleanUpFunctionDeps.has(dep)) {
return;
}
}
const propsArr = Array.from(propsSet);
const stateArr = Array.from(stateSet);

View File

@@ -1,206 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerDiagnostic, CompilerError, ErrorCategory} from '..';
import {CodegenFunction} from '../ReactiveScopes';
import {Result} from '../Utils/Result';
/**
* IMPORTANT: This validation is only intended for use in unit tests.
* It is not intended for use in production.
*
* This validation is used to ensure that the generated AST has proper source locations
* for "important" original nodes.
*
* There's one big gotcha with this validation: it only works if the "important" original nodes
* are not optimized away by the compiler.
*
* When that scenario happens, we should just update the fixture to not include a node that has no
* corresponding node in the generated AST due to being completely removed during compilation.
*/
/**
* Some common node types that are important for coverage tracking.
* Based on istanbul-lib-instrument
*/
const IMPORTANT_INSTRUMENTED_TYPES = new Set([
'ArrowFunctionExpression',
'AssignmentPattern',
'ObjectMethod',
'ExpressionStatement',
'BreakStatement',
'ContinueStatement',
'ReturnStatement',
'ThrowStatement',
'TryStatement',
'VariableDeclarator',
'IfStatement',
'ForStatement',
'ForInStatement',
'ForOfStatement',
'WhileStatement',
'DoWhileStatement',
'SwitchStatement',
'SwitchCase',
'WithStatement',
'FunctionDeclaration',
'FunctionExpression',
'LabeledStatement',
'ConditionalExpression',
'LogicalExpression',
]);
/**
* Check if a node is a manual memoization call that the compiler optimizes away.
* These include useMemo and useCallback calls, which are intentionally removed
* by the DropManualMemoization pass.
*/
function isManualMemoization(node: t.Node): boolean {
// Check if this is a useMemo/useCallback call expression
if (t.isCallExpression(node)) {
const callee = node.callee;
if (t.isIdentifier(callee)) {
return callee.name === 'useMemo' || callee.name === 'useCallback';
}
if (
t.isMemberExpression(callee) &&
t.isIdentifier(callee.property) &&
t.isIdentifier(callee.object)
) {
return (
callee.object.name === 'React' &&
(callee.property.name === 'useMemo' ||
callee.property.name === 'useCallback')
);
}
}
return false;
}
/**
* Create a location key for comparison. We compare by line/column/source,
* not by object identity.
*/
function locationKey(loc: t.SourceLocation): string {
return `${loc.start.line}:${loc.start.column}-${loc.end.line}:${loc.end.column}`;
}
/**
* Validates that important source locations from the original code are preserved
* in the generated AST. This ensures that Istanbul coverage instrumentation can
* properly map back to the original source code.
*
* The validator:
* 1. Collects locations from "important" nodes in the original AST (those that
* Istanbul instruments for coverage tracking)
* 2. Exempts known compiler optimizations (useMemo/useCallback removal)
* 3. Verifies that all important locations appear somewhere in the generated AST
*
* Missing locations can cause Istanbul to fail to track coverage for certain
* code paths, leading to inaccurate coverage reports.
*/
export function validateSourceLocations(
func: NodePath<
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
generatedAst: CodegenFunction,
): Result<void, CompilerError> {
const errors = new CompilerError();
// Step 1: Collect important locations from the original source
const importantOriginalLocations = new Map<
string,
{loc: t.SourceLocation; nodeType: string}
>();
func.traverse({
enter(path) {
const node = path.node;
// Only track node types that Istanbul instruments
if (!IMPORTANT_INSTRUMENTED_TYPES.has(node.type)) {
return;
}
// Skip manual memoization that the compiler intentionally removes
if (isManualMemoization(node)) {
return;
}
// Collect the location if it exists
if (node.loc) {
const key = locationKey(node.loc);
importantOriginalLocations.set(key, {
loc: node.loc,
nodeType: node.type,
});
}
},
});
// Step 2: Collect all locations from the generated AST
const generatedLocations = new Set<string>();
function collectGeneratedLocations(node: t.Node): void {
if (node.loc) {
generatedLocations.add(locationKey(node.loc));
}
// Use Babel's VISITOR_KEYS to traverse only actual node properties
const keys = t.VISITOR_KEYS[node.type as keyof typeof t.VISITOR_KEYS];
if (!keys) {
return;
}
for (const key of keys) {
const value = (node as any)[key];
if (Array.isArray(value)) {
for (const item of value) {
if (t.isNode(item)) {
collectGeneratedLocations(item);
}
}
} else if (t.isNode(value)) {
collectGeneratedLocations(value);
}
}
}
// Collect from main function body
collectGeneratedLocations(generatedAst.body);
// Collect from outlined functions
for (const outlined of generatedAst.outlined) {
collectGeneratedLocations(outlined.fn.body);
}
// Step 3: Validate that all important locations are preserved
for (const [key, {loc, nodeType}] of importantOriginalLocations) {
if (!generatedLocations.has(key)) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Todo,
reason: 'Important source location missing in generated code',
description:
`Source location for ${nodeType} is missing in the generated output. This can cause coverage instrumentation ` +
`to fail to track this code properly, resulting in inaccurate coverage reports.`,
}).withDetails({
kind: 'error',
loc,
message: null,
}),
);
}
}
return errors.asResult();
}

View File

@@ -12,5 +12,4 @@ export {validateNoCapitalizedCalls} from './ValidateNoCapitalizedCalls';
export {validateNoRefAccessInRender} from './ValidateNoRefAccessInRender';
export {validateNoSetStateInRender} from './ValidateNoSetStateInRender';
export {validatePreservedManualMemoization} from './ValidatePreservedManualMemoization';
export {validateSourceLocations} from './ValidateSourceLocations';
export {validateUseMemo} from './ValidateUseMemo';

View File

@@ -1,76 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component(file: File) {
const [imageUrl, setImageUrl] = useState(null);
/*
* Cleaning up the variable or a source of the variable used to setState
* inside the effect communicates that we always need to clean up something
* which is a valid use case for useEffect. In which case we want to
* avoid an throwing
*/
useEffect(() => {
const imageUrlPrepared = URL.createObjectURL(file);
setImageUrl(imageUrlPrepared);
return () => URL.revokeObjectURL(imageUrlPrepared);
}, [file]);
return <Image src={imageUrl} xstyle={styles.imageSizeLimits} />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import { useEffect, useState } from "react";
function Component(file) {
const $ = _c(5);
const [imageUrl, setImageUrl] = useState(null);
let t0;
let t1;
if ($[0] !== file) {
t0 = () => {
const imageUrlPrepared = URL.createObjectURL(file);
setImageUrl(imageUrlPrepared);
return () => URL.revokeObjectURL(imageUrlPrepared);
};
t1 = [file];
$[0] = file;
$[1] = t0;
$[2] = t1;
} else {
t0 = $[1];
t1 = $[2];
}
useEffect(t0, t1);
let t2;
if ($[3] !== imageUrl) {
t2 = <Image src={imageUrl} xstyle={styles.imageSizeLimits} />;
$[3] = imageUrl;
$[4] = t2;
} else {
t2 = $[4];
}
return t2;
}
```
## Logs
```
{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":108},"end":{"line":21,"column":1,"index":700},"filename":"effect-with-cleanup-function-depending-on-derived-computation-value.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,21 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component(file: File) {
const [imageUrl, setImageUrl] = useState(null);
/*
* Cleaning up the variable or a source of the variable used to setState
* inside the effect communicates that we always need to clean up something
* which is a valid use case for useEffect. In which case we want to
* avoid an throwing
*/
useEffect(() => {
const imageUrlPrepared = URL.createObjectURL(file);
setImageUrl(imageUrlPrepared);
return () => URL.revokeObjectURL(imageUrlPrepared);
}, [file]);
return <Image src={imageUrl} xstyle={styles.imageSizeLimits} />;
}

View File

@@ -1,72 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
function Component({prop}) {
const [s, setS] = useState();
const [second, setSecond] = useState(prop);
/*
* `second` is a source of state. It will inherit the value of `prop` in
* the first render, but after that it will no longer be updated when
* `prop` changes. So we shouldn't consider `second` as being derived from
* `prop`
*/
useEffect(() => {
setS(second);
}, [second]);
return <div>{s}</div>;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
function Component(t0) {
const $ = _c(5);
const { prop } = t0;
const [s, setS] = useState();
const [second] = useState(prop);
let t1;
let t2;
if ($[0] !== second) {
t1 = () => {
setS(second);
};
t2 = [second];
$[0] = second;
$[1] = t1;
$[2] = t2;
} else {
t1 = $[1];
t2 = $[2];
}
useEffect(t1, t2);
let t3;
if ($[3] !== s) {
t3 = <div>{s}</div>;
$[3] = s;
$[4] = t3;
} else {
t3 = $[4];
}
return t3;
}
```
## Logs
```
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nState: [second]\n\nData Flow Tree:\n└── second (State)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":14,"column":4,"index":443},"end":{"line":14,"column":8,"index":447},"filename":"usestate-derived-from-prop-no-show-in-data-flow-tree.ts","identifierName":"setS"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":3,"column":0,"index":64},"end":{"line":18,"column":1,"index":500},"filename":"usestate-derived-from-prop-no-show-in-data-flow-tree.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,18 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
function Component({prop}) {
const [s, setS] = useState();
const [second, setSecond] = useState(prop);
/*
* `second` is a source of state. It will inherit the value of `prop` in
* the first render, but after that it will no longer be updated when
* `prop` changes. So we shouldn't consider `second` as being derived from
* `prop`
*/
useEffect(() => {
setS(second);
}, [second]);
return <div>{s}</div>;
}

View File

@@ -1,224 +0,0 @@
## Input
```javascript
// @validateSourceLocations
import {useEffect, useCallback} from 'react';
function Component({prop1, prop2}) {
const x = prop1 + prop2;
const y = x * 2;
const arr = [x, y];
const obj = {x, y};
const [a, b] = arr;
const {x: c, y: d} = obj;
useEffect(() => {
if (a > 10) {
console.log(a);
}
}, [a]);
const foo = useCallback(() => {
return a + b;
}, [a, b]);
function bar() {
return (c + d) * 2;
}
console.log('Hello, world!');
return [y, foo, bar];
}
```
## Error
```
Found 13 errors:
Todo: Important source location missing in generated code
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:5:8
3 |
4 | function Component({prop1, prop2}) {
> 5 | const x = prop1 + prop2;
| ^^^^^^^^^^^^^^^^^
6 | const y = x * 2;
7 | const arr = [x, y];
8 | const obj = {x, y};
Todo: Important source location missing in generated code
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:6:8
4 | function Component({prop1, prop2}) {
5 | const x = prop1 + prop2;
> 6 | const y = x * 2;
| ^^^^^^^^^
7 | const arr = [x, y];
8 | const obj = {x, y};
9 | const [a, b] = arr;
Todo: Important source location missing in generated code
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:7:8
5 | const x = prop1 + prop2;
6 | const y = x * 2;
> 7 | const arr = [x, y];
| ^^^^^^^^^^^^
8 | const obj = {x, y};
9 | const [a, b] = arr;
10 | const {x: c, y: d} = obj;
Todo: Important source location missing in generated code
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:8:8
6 | const y = x * 2;
7 | const arr = [x, y];
> 8 | const obj = {x, y};
| ^^^^^^^^^^^^
9 | const [a, b] = arr;
10 | const {x: c, y: d} = obj;
11 |
Todo: Important source location missing in generated code
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:9:8
7 | const arr = [x, y];
8 | const obj = {x, y};
> 9 | const [a, b] = arr;
| ^^^^^^^^^^^^
10 | const {x: c, y: d} = obj;
11 |
12 | useEffect(() => {
Todo: Important source location missing in generated code
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:10:8
8 | const obj = {x, y};
9 | const [a, b] = arr;
> 10 | const {x: c, y: d} = obj;
| ^^^^^^^^^^^^^^^^^^
11 |
12 | useEffect(() => {
13 | if (a > 10) {
Todo: Important source location missing in generated code
Source location for ExpressionStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:12:2
10 | const {x: c, y: d} = obj;
11 |
> 12 | useEffect(() => {
| ^^^^^^^^^^^^^^^^^
> 13 | if (a > 10) {
| ^^^^^^^^^^^^^^^^^
> 14 | console.log(a);
| ^^^^^^^^^^^^^^^^^
> 15 | }
| ^^^^^^^^^^^^^^^^^
> 16 | }, [a]);
| ^^^^^^^^^^^
17 |
18 | const foo = useCallback(() => {
19 | return a + b;
Todo: Important source location missing in generated code
Source location for ExpressionStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:14:6
12 | useEffect(() => {
13 | if (a > 10) {
> 14 | console.log(a);
| ^^^^^^^^^^^^^^^
15 | }
16 | }, [a]);
17 |
Todo: Important source location missing in generated code
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:18:8
16 | }, [a]);
17 |
> 18 | const foo = useCallback(() => {
| ^^^^^^^^^^^^^^^^^^^^^^^^^
> 19 | return a + b;
| ^^^^^^^^^^^^^^^^^
> 20 | }, [a, b]);
| ^^^^^^^^^^^^^
21 |
22 | function bar() {
23 | return (c + d) * 2;
Todo: Important source location missing in generated code
Source location for ReturnStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:19:4
17 |
18 | const foo = useCallback(() => {
> 19 | return a + b;
| ^^^^^^^^^^^^^
20 | }, [a, b]);
21 |
22 | function bar() {
Todo: Important source location missing in generated code
Source location for ReturnStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:23:4
21 |
22 | function bar() {
> 23 | return (c + d) * 2;
| ^^^^^^^^^^^^^^^^^^^
24 | }
25 |
26 | console.log('Hello, world!');
Todo: Important source location missing in generated code
Source location for ExpressionStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:26:2
24 | }
25 |
> 26 | console.log('Hello, world!');
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
27 |
28 | return [y, foo, bar];
29 | }
Todo: Important source location missing in generated code
Source location for ReturnStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:28:2
26 | console.log('Hello, world!');
27 |
> 28 | return [y, foo, bar];
| ^^^^^^^^^^^^^^^^^^^^^
29 | }
30 |
```

View File

@@ -1,29 +0,0 @@
// @validateSourceLocations
import {useEffect, useCallback} from 'react';
function Component({prop1, prop2}) {
const x = prop1 + prop2;
const y = x * 2;
const arr = [x, y];
const obj = {x, y};
const [a, b] = arr;
const {x: c, y: d} = obj;
useEffect(() => {
if (a > 10) {
console.log(a);
}
}, [a]);
const foo = useCallback(() => {
return a + b;
}, [a, b]);
function bar() {
return (c + d) * 2;
}
console.log('Hello, world!');
return [y, foo, bar];
}

View File

@@ -1,45 +0,0 @@
## Input
```javascript
export function useFormatRelativeTime(opts = {}) {
const {timeZone, minimal} = opts;
const format = useCallback(function formatWithUnit() {}, [minimal]);
// We previously recorded `{timeZone}` as capturing timeZone into the object,
// then assumed that dateTimeFormat() mutates that object,
// which in turn could mutate timeZone and the object it came from,
// which meanteans that the value `minimal` is derived from can change.
//
// The fix was to record a Capture from a maybefrozen value as an ImmutableCapture
// which doesn't propagate mutations
dateTimeFormat({timeZone});
return format;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
export function useFormatRelativeTime(t0) {
const $ = _c(1);
const opts = t0 === undefined ? {} : t0;
const { timeZone, minimal } = opts;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = function formatWithUnit() {};
$[0] = t1;
} else {
t1 = $[0];
}
const format = t1;
dateTimeFormat({ timeZone });
return format;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,13 +0,0 @@
export function useFormatRelativeTime(opts = {}) {
const {timeZone, minimal} = opts;
const format = useCallback(function formatWithUnit() {}, [minimal]);
// We previously recorded `{timeZone}` as capturing timeZone into the object,
// then assumed that dateTimeFormat() mutates that object,
// which in turn could mutate timeZone and the object it came from,
// which meanteans that the value `minimal` is derived from can change.
//
// The fix was to record a Capture from a maybefrozen value as an ImmutableCapture
// which doesn't propagate mutations
dateTimeFormat({timeZone});
return format;
}

View File

@@ -4857,7 +4857,6 @@ export function processBinaryChunk(
resolvedRowTag === 65 /* "A" */ ||
resolvedRowTag === 79 /* "O" */ ||
resolvedRowTag === 111 /* "o" */ ||
resolvedRowTag === 98 /* "b" */ ||
resolvedRowTag === 85 /* "U" */ ||
resolvedRowTag === 83 /* "S" */ ||
resolvedRowTag === 115 /* "s" */ ||
@@ -4917,31 +4916,14 @@ export function processBinaryChunk(
// We found the last chunk of the row
const length = lastIdx - i;
const lastChunk = new Uint8Array(chunk.buffer, offset, length);
// Check if this is a Uint8Array for a byte stream. We enqueue it
// immediately but need to determine if we can use zero-copy or must copy.
if (rowTag === 98 /* "b" */) {
resolveBuffer(
response,
rowID,
// If we're at the end of the RSC chunk, no more parsing will access
// this buffer and we don't need to copy the chunk to allow detaching
// the buffer, otherwise we need to copy.
lastIdx === chunkLength ? lastChunk : lastChunk.slice(),
streamState,
);
} else {
// Process all other row types.
processFullBinaryRow(
response,
streamState,
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) {
@@ -4954,27 +4936,14 @@ export function processBinaryChunk(
rowLength = 0;
buffer.length = 0;
} else {
// The rest of this row is in a future chunk.
// The rest of this row is in a future chunk. We stash the rest of the
// current chunk until we can process the full row.
const length = chunk.byteLength - i;
const remainingSlice = new Uint8Array(chunk.buffer, offset, length);
// For byte streams, we can enqueue the partial row immediately without
// copying since we're at the end of the RSC chunk and no more parsing
// will access this buffer.
if (rowTag === 98 /* "b" */) {
// Update how many bytes we're still waiting for. We need to do this
// before enqueueing, as enqueue will detach the buffer and byteLength
// will become 0.
rowLength -= remainingSlice.byteLength;
resolveBuffer(response, rowID, remainingSlice, streamState);
} else {
// For other row types, stash the rest of the current chunk until we can
// process the full row.
buffer.push(remainingSlice);
// Update how many bytes we're still waiting for. If we're looking for
// a newline, this doesn't hurt since we'll just ignore it.
rowLength -= remainingSlice.byteLength;
}
buffer.push(remainingSlice);
// Update how many bytes we're still waiting for. If we're looking for
// a newline, this doesn't hurt since we'll just ignore it.
rowLength -= remainingSlice.byteLength;
break;
}
}

View File

@@ -1576,6 +1576,7 @@ export function attach(
currentRoot = rootInstance;
unmountInstanceRecursively(rootInstance);
rootToFiberInstanceMap.delete(root);
flushPendingEvents();
currentRoot = (null: any);
});
@@ -1645,6 +1646,7 @@ export function attach(
currentRoot = newRoot;
setRootPseudoKey(currentRoot.id, root.current);
mountFiberRecursively(root.current, false);
flushPendingEvents();
currentRoot = (null: any);
});
@@ -2157,6 +2159,7 @@ export function attach(
let pendingOperationsQueue: Array<OperationsArray> | null = [];
const pendingStringTable: Map<string, StringTableEntry> = new Map();
let pendingStringTableLength: number = 0;
let pendingUnmountedRootID: FiberInstance['id'] | null = null;
function pushOperation(op: number): void {
if (__DEV__) {
@@ -2184,7 +2187,8 @@ export function attach(
pendingOperations.length === 0 &&
pendingRealUnmountedIDs.length === 0 &&
pendingRealUnmountedSuspenseIDs.length === 0 &&
pendingSuspenderChanges.size === 0
pendingSuspenderChanges.size === 0 &&
pendingUnmountedRootID === null
);
}
@@ -2246,7 +2250,9 @@ export function attach(
return;
}
const numUnmountIDs = pendingRealUnmountedIDs.length;
const numUnmountIDs =
pendingRealUnmountedIDs.length +
(pendingUnmountedRootID === null ? 0 : 1);
const numUnmountSuspenseIDs = pendingRealUnmountedSuspenseIDs.length;
const numSuspenderChanges = pendingSuspenderChanges.size;
@@ -2324,6 +2330,11 @@ export function attach(
for (let j = 0; j < pendingRealUnmountedIDs.length; j++) {
operations[i++] = pendingRealUnmountedIDs[j];
}
// The root ID should always be unmounted last.
if (pendingUnmountedRootID !== null) {
operations[i] = pendingUnmountedRootID;
i++;
}
}
// Fill in pending operations.
@@ -2371,6 +2382,7 @@ export function attach(
pendingRealUnmountedIDs.length = 0;
pendingRealUnmountedSuspenseIDs.length = 0;
pendingSuspenderChanges.clear();
pendingUnmountedRootID = null;
pendingStringTable.clear();
pendingStringTableLength = 0;
}
@@ -2856,6 +2868,7 @@ export function attach(
// Already disconnected.
return;
}
const fiber = fiberInstance.data;
if (trackedPathMatchInstance === fiberInstance) {
// We're in the process of trying to restore previous selection.
@@ -2865,7 +2878,17 @@ export function attach(
}
const id = fiberInstance.id;
pendingRealUnmountedIDs.push(id);
const isRoot = fiber.tag === HostRoot;
if (isRoot) {
// Roots must be removed only after all children have been removed.
// So we track it separately.
pendingUnmountedRootID = id;
} else {
// To maintain child-first ordering,
// we'll push it into one of these queues,
// and later arrange them in the correct order.
pendingRealUnmountedIDs.push(id);
}
}
function recordSuspenseResize(suspenseNode: SuspenseNode): void {
@@ -5749,12 +5772,11 @@ export function attach(
mountFiberRecursively(root.current, false);
flushPendingEvents();
needsToFlushComponentLogs = false;
currentRoot = (null: any);
});
flushPendingEvents();
needsToFlushComponentLogs = false;
}
}

View File

@@ -341,6 +341,7 @@ export function getEventPriority(domEventName: DOMEventName): EventPriority {
case 'pointerup':
case 'ratechange':
case 'reset':
case 'resize':
case 'seeked':
case 'submit':
case 'toggle':
@@ -379,7 +380,6 @@ export function getEventPriority(domEventName: DOMEventName): EventPriority {
case 'pointermove':
case 'pointerout':
case 'pointerover':
case 'resize':
case 'scroll':
case 'touchmove':
case 'wheel':

View File

@@ -10,17 +10,11 @@
'use strict';
let React;
let ReactDOM;
let ReactDOMClient;
let Scheduler;
let act;
let Activity;
let useState;
let useLayoutEffect;
let useEffect;
let LegacyHidden;
let assertLog;
let Suspense;
let ReactDOM;
let ReactDOMClient;
let act;
describe('ReactDOMActivity', () => {
let container;
@@ -28,19 +22,11 @@ describe('ReactDOMActivity', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
Scheduler = require('scheduler/unstable_mock');
Activity = React.Activity;
useState = React.useState;
Suspense = React.Suspense;
useState = React.useState;
LegacyHidden = React.unstable_LegacyHidden;
useLayoutEffect = React.useLayoutEffect;
useEffect = React.useEffect;
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
const InternalTestUtils = require('internal-test-utils');
act = InternalTestUtils.act;
assertLog = InternalTestUtils.assertLog;
act = require('internal-test-utils').act;
container = document.createElement('div');
document.body.appendChild(container);
});
@@ -49,11 +35,6 @@ describe('ReactDOMActivity', () => {
document.body.removeChild(container);
});
function Text(props) {
Scheduler.log(props.text);
return <span prop={props.text}>{props.children}</span>;
}
// @gate enableActivity
it(
'hiding an Activity boundary also hides the direct children of any ' +
@@ -72,7 +53,7 @@ describe('ReactDOMActivity', () => {
);
}
function App() {
function App({portalContents}) {
return (
<Accordion>
<div>
@@ -118,7 +99,7 @@ describe('ReactDOMActivity', () => {
);
}
function App() {
function App({portalContents}) {
return (
<Activity mode="hidden">
<div>
@@ -150,416 +131,4 @@ describe('ReactDOMActivity', () => {
);
},
);
// @gate enableActivity
it('hides new portals added to an already hidden tree', async () => {
function Child() {
return <Text text="Child" />;
}
const portalContainer = document.createElement('div');
function Portal({children}) {
return <div>{ReactDOM.createPortal(children, portalContainer)}</div>;
}
const root = ReactDOMClient.createRoot(container);
// Mount hidden tree.
await act(() => {
root.render(
<Activity mode="hidden">
<Text text="Parent" />
</Activity>,
);
});
assertLog(['Parent']);
expect(container.innerHTML).toBe(
'<span prop="Parent" style="display: none;"></span>',
);
expect(portalContainer.innerHTML).toBe('');
// Add a portal inside the hidden tree.
await act(() => {
root.render(
<Activity mode="hidden">
<Text text="Parent" />
<Portal>
<Child />
</Portal>
</Activity>,
);
});
assertLog(['Parent', 'Child']);
expect(container.innerHTML).toBe(
'<span prop="Parent" style="display: none;"></span><div style="display: none;"></div>',
);
expect(portalContainer.innerHTML).toBe(
'<span prop="Child" style="display: none;"></span>',
);
// Now reveal it.
await act(() => {
root.render(
<Activity mode="visible">
<Text text="Parent" />
<Portal>
<Child />
</Portal>
</Activity>,
);
});
assertLog(['Parent', 'Child']);
expect(container.innerHTML).toBe(
'<span prop="Parent" style=""></span><div style=""></div>',
);
expect(portalContainer.innerHTML).toBe(
'<span prop="Child" style=""></span>',
);
});
// @gate enableActivity
it('hides new insertions inside an already hidden portal', async () => {
function Child({text}) {
useLayoutEffect(() => {
Scheduler.log(`Mount layout ${text}`);
return () => {
Scheduler.log(`Unmount layout ${text}`);
};
}, [text]);
return <Text text={text} />;
}
const portalContainer = document.createElement('div');
function Portal({children}) {
return <div>{ReactDOM.createPortal(children, portalContainer)}</div>;
}
const root = ReactDOMClient.createRoot(container);
// Mount hidden tree.
await act(() => {
root.render(
<Activity mode="hidden">
<Portal>
<Child text="A" />
</Portal>
</Activity>,
);
});
assertLog(['A']);
expect(container.innerHTML).toBe('<div style="display: none;"></div>');
expect(portalContainer.innerHTML).toBe(
'<span prop="A" style="display: none;"></span>',
);
// Add a node inside the hidden portal.
await act(() => {
root.render(
<Activity mode="hidden">
<Portal>
<Child text="A" />
<Child text="B" />
</Portal>
</Activity>,
);
});
assertLog(['A', 'B']);
expect(container.innerHTML).toBe('<div style="display: none;"></div>');
expect(portalContainer.innerHTML).toBe(
'<span prop="A" style="display: none;"></span><span prop="B" style="display: none;"></span>',
);
// Now reveal it.
await act(() => {
root.render(
<Activity mode="visible">
<Portal>
<Child text="A" />
<Child text="B" />
</Portal>
</Activity>,
);
});
assertLog(['A', 'B', 'Mount layout A', 'Mount layout B']);
expect(container.innerHTML).toBe('<div style=""></div>');
expect(portalContainer.innerHTML).toBe(
'<span prop="A" style=""></span><span prop="B" style=""></span>',
);
});
// @gate enableActivity
it('reveal an inner Suspense boundary without revealing an outer Activity on the same host child', async () => {
const promise = new Promise(() => {});
function Child({showInner}) {
useLayoutEffect(() => {
Scheduler.log('Mount layout');
return () => {
Scheduler.log('Unmount layout');
};
}, []);
return (
<>
{showInner ? null : promise}
<Text text="Child" />
</>
);
}
const portalContainer = document.createElement('div');
function Portal({children}) {
return <div>{ReactDOM.createPortal(children, portalContainer)}</div>;
}
const root = ReactDOMClient.createRoot(container);
// Prerender the whole tree.
await act(() => {
root.render(
<Activity mode="hidden">
<Portal>
<Suspense name="Inner" fallback={<span>Loading</span>}>
<Child showInner={true} />
</Suspense>
</Portal>
</Activity>,
);
});
assertLog(['Child']);
expect(container.innerHTML).toBe('<div style="display: none;"></div>');
expect(portalContainer.innerHTML).toBe(
'<span prop="Child" style="display: none;"></span>',
);
// Re-suspend the inner.
await act(() => {
root.render(
<Activity mode="hidden">
<Portal>
<Suspense name="Inner" fallback={<span>Loading</span>}>
<Child showInner={false} />
</Suspense>
</Portal>
</Activity>,
);
});
assertLog([]);
expect(container.innerHTML).toBe('<div style="display: none;"></div>');
expect(portalContainer.innerHTML).toBe(
'<span prop="Child" style="display: none;"></span><span style="display: none;">Loading</span>',
);
// Toggle to visible while suspended.
await act(() => {
root.render(
<Activity mode="visible">
<Portal>
<Suspense name="Inner" fallback={<span>Loading</span>}>
<Child showInner={false} />
</Suspense>
</Portal>
</Activity>,
);
});
assertLog([]);
expect(container.innerHTML).toBe('<div style=""></div>');
expect(portalContainer.innerHTML).toBe(
'<span prop="Child" style="display: none;"></span><span style="">Loading</span>',
);
// Now reveal.
await act(() => {
root.render(
<Activity mode="visible">
<Portal>
<Suspense name="Inner" fallback={<span>Loading</span>}>
<Child showInner={true} />
</Suspense>
</Portal>
</Activity>,
);
});
assertLog(['Child', 'Mount layout']);
expect(container.innerHTML).toBe('<div style=""></div>');
expect(portalContainer.innerHTML).toBe(
'<span prop="Child" style=""></span>',
);
});
// @gate enableActivity
it('mounts/unmounts layout effects in portal when visibility changes (starting visible)', async () => {
function Child() {
useLayoutEffect(() => {
Scheduler.log('Mount layout');
return () => {
Scheduler.log('Unmount layout');
};
}, []);
return <Text text="Child" />;
}
const portalContainer = document.createElement('div');
function Portal({children}) {
return <div>{ReactDOM.createPortal(children, portalContainer)}</div>;
}
const root = ReactDOMClient.createRoot(container);
// Mount visible tree.
await act(() => {
root.render(
<Activity mode="visible">
<Portal>
<Child />
</Portal>
</Activity>,
);
});
assertLog(['Child', 'Mount layout']);
expect(container.innerHTML).toBe('<div></div>');
expect(portalContainer.innerHTML).toBe('<span prop="Child"></span>');
// Hide the tree. The layout effect is unmounted.
await act(() => {
root.render(
<Activity mode="hidden">
<Portal>
<Child />
</Portal>
</Activity>,
);
});
assertLog(['Unmount layout', 'Child']);
expect(container.innerHTML).toBe('<div style="display: none;"></div>');
expect(portalContainer.innerHTML).toBe(
'<span prop="Child" style="display: none;"></span>',
);
});
// @gate enableActivity
it('mounts/unmounts layout effects in portal when visibility changes (starting hidden)', async () => {
function Child() {
useLayoutEffect(() => {
Scheduler.log('Mount layout');
return () => {
Scheduler.log('Unmount layout');
};
}, []);
return <Text text="Child" />;
}
const portalContainer = document.createElement('div');
function Portal({children}) {
return <div>{ReactDOM.createPortal(children, portalContainer)}</div>;
}
const root = ReactDOMClient.createRoot(container);
// Mount hidden tree.
await act(() => {
root.render(
<Activity mode="hidden">
<Portal>
<Child />
</Portal>
</Activity>,
);
});
// No layout effect.
assertLog(['Child']);
expect(container.innerHTML).toBe('<div style="display: none;"></div>');
expect(portalContainer.innerHTML).toBe(
'<span prop="Child" style="display: none;"></span>',
);
// Unhide the tree. The layout effect is mounted.
await act(() => {
root.render(
<Activity mode="visible">
<Portal>
<Child />
</Portal>
</Activity>,
);
});
assertLog(['Child', 'Mount layout']);
expect(container.innerHTML).toBe('<div style=""></div>');
expect(portalContainer.innerHTML).toBe(
'<span prop="Child" style=""></span>',
);
});
// @gate enableLegacyHidden
it('does not toggle effects or hide nodes for LegacyHidden component inside portal', async () => {
function Child() {
useLayoutEffect(() => {
Scheduler.log('Mount layout');
return () => {
Scheduler.log('Unmount layout');
};
}, []);
useEffect(() => {
Scheduler.log('Mount passive');
return () => {
Scheduler.log('Unmount passive');
};
}, []);
return <Text text="Child" />;
}
const portalContainer = document.createElement('div');
function Portal({children}) {
return <div>{ReactDOM.createPortal(children, portalContainer)}</div>;
}
const root = ReactDOMClient.createRoot(container);
// Mount visible tree.
await act(() => {
root.render(
<LegacyHidden mode="visible">
<Portal>
<Child />
</Portal>
</LegacyHidden>,
);
});
assertLog(['Child', 'Mount layout', 'Mount passive']);
expect(container.innerHTML).toBe('<div></div>');
expect(portalContainer.innerHTML).toBe('<span prop="Child"></span>');
// Hide the tree.
await act(() => {
root.render(
<LegacyHidden mode="hidden">
<Portal>
<Child />
</Portal>
</LegacyHidden>,
);
});
// Effects not unmounted.
assertLog(['Child']);
expect(container.innerHTML).toBe('<div></div>');
expect(portalContainer.innerHTML).toBe('<span prop="Child"></span>');
// Unhide the tree.
await act(() => {
root.render(
<LegacyHidden mode="visible">
<Portal>
<Child />
</Portal>
</LegacyHidden>,
);
});
// Effects already mounted.
assertLog(['Child']);
expect(container.innerHTML).toBe('<div></div>');
expect(portalContainer.innerHTML).toBe('<span prop="Child"></span>');
});
});

View File

@@ -40,6 +40,7 @@ describe('created with ReactFabric called with ReactNative', () => {
require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface').getNativeTagFromPublicInstance;
});
// @gate !disableLegacyMode
it('find Fabric instances with the RN renderer', () => {
const View = createReactNativeComponentClass('RCTView', () => ({
validAttributes: {title: true},
@@ -60,6 +61,7 @@ describe('created with ReactFabric called with ReactNative', () => {
expect(getNativeTagFromPublicInstance(instance)).toBe(2);
});
// @gate !disableLegacyMode
it('find Fabric nodes with the RN renderer', () => {
const View = createReactNativeComponentClass('RCTView', () => ({
validAttributes: {title: true},
@@ -80,6 +82,7 @@ describe('created with ReactFabric called with ReactNative', () => {
expect(handle).toBe(2);
});
// @gate !disableLegacyMode
it('dispatches commands on Fabric nodes with the RN renderer', () => {
nativeFabricUIManager.dispatchCommand.mockClear();
const View = createReactNativeComponentClass('RCTView', () => ({
@@ -101,6 +104,7 @@ describe('created with ReactFabric called with ReactNative', () => {
expect(UIManager.dispatchViewManagerCommand).not.toBeCalled();
});
// @gate !disableLegacyMode
it('dispatches sendAccessibilityEvent on Fabric nodes with the RN renderer', () => {
nativeFabricUIManager.sendAccessibilityEvent.mockClear();
const View = createReactNativeComponentClass('RCTView', () => ({
@@ -143,6 +147,7 @@ describe('created with ReactNative called with ReactFabric', () => {
.ReactNativeViewConfigRegistry.register;
});
// @gate !disableLegacyMode
it('find Paper instances with the Fabric renderer', () => {
const View = createReactNativeComponentClass('RCTView', () => ({
validAttributes: {title: true},
@@ -163,6 +168,7 @@ describe('created with ReactNative called with ReactFabric', () => {
expect(instance._nativeTag).toBe(3);
});
// @gate !disableLegacyMode
it('find Paper nodes with the Fabric renderer', () => {
const View = createReactNativeComponentClass('RCTView', () => ({
validAttributes: {title: true},
@@ -183,6 +189,7 @@ describe('created with ReactNative called with ReactFabric', () => {
expect(handle).toBe(3);
});
// @gate !disableLegacyMode
it('dispatches commands on Paper nodes with the Fabric renderer', () => {
UIManager.dispatchViewManagerCommand.mockReset();
const View = createReactNativeComponentClass('RCTView', () => ({
@@ -205,6 +212,7 @@ describe('created with ReactNative called with ReactFabric', () => {
expect(nativeFabricUIManager.dispatchCommand).not.toBeCalled();
});
// @gate !disableLegacyMode
it('dispatches sendAccessibilityEvent on Paper nodes with the Fabric renderer', () => {
ReactNativePrivateInterface.legacySendAccessibilityEvent.mockReset();
const View = createReactNativeComponentClass('RCTView', () => ({

View File

@@ -5,11 +5,18 @@
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
* @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
*/
'use strict';
// Polyfills for test environment
global.ReadableStream =
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
global.WritableStream =
require('web-streams-polyfill/ponyfill/es6').WritableStream;
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;
let clientExports;
let turbopackMap;
let turbopackModules;

View File

@@ -5,11 +5,17 @@
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
* @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
*/
'use strict';
// Polyfills for test environment
global.ReadableStream =
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;
// let serverExports;
let turbopackServerMap;
let ReactServerDOMServer;
let ReactServerDOMClient;
@@ -23,6 +29,7 @@ describe('ReactFlightDOMTurbopackReply', () => {
require('react-server-dom-turbopack/server.edge'),
);
const TurbopackMock = require('./utils/TurbopackMock');
// serverExports = TurbopackMock.serverExports;
turbopackServerMap = TurbopackMock.turbopackServerMap;
ReactServerDOMServer = require('react-server-dom-turbopack/server.edge');
jest.resetModules();

View File

@@ -10,6 +10,18 @@
'use strict';
// Polyfills for test environment
global.ReadableStream =
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
global.WritableStream =
require('web-streams-polyfill/ponyfill/es6').WritableStream;
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;
global.Blob = require('buffer').Blob;
if (typeof File === 'undefined' || typeof FormData === 'undefined') {
global.File = require('buffer').File || require('undici').File;
global.FormData = require('undici').FormData;
}
// Patch for Edge environments for global scope
global.AsyncLocalStorage = require('async_hooks').AsyncLocalStorage;
@@ -115,16 +127,8 @@ describe('ReactFlightDOMEdge', () => {
chunk.set(prevChunk, 0);
chunk.set(value, prevChunk.length);
if (chunk.length > 50) {
// Copy the part we're keeping (prevChunk) to avoid buffer
// transfer. When we enqueue the partial chunk below, downstream
// consumers (like byte streams in the Flight Client) may detach
// the underlying buffer. Since prevChunk would share the same
// buffer, we copy it first so it has its own independent buffer.
// TODO: Should we just use {type: 'bytes'} for this stream to
// always transfer ownership, and not only "accidentally" when we
// enqueue in the Flight Client?
prevChunk = chunk.slice(chunk.length - 50);
controller.enqueue(chunk.subarray(0, chunk.length - 50));
prevChunk = chunk.subarray(chunk.length - 50);
} else {
// Wait to see if we get some more bytes to join in.
prevChunk = chunk;
@@ -1114,121 +1118,25 @@ describe('ReactFlightDOMEdge', () => {
expect(streamedBuffers).toEqual(buffers);
});
it('should support binary ReadableStreams', async () => {
const encoder = new TextEncoder();
const words = ['Hello', 'streaming', 'world'];
const stream = new ReadableStream({
type: 'bytes',
async start(controller) {
for (let i = 0; i < words.length; i++) {
const chunk = encoder.encode(words[i] + ' ');
controller.enqueue(chunk);
}
controller.close();
},
});
const rscStream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(stream, {}),
);
const result = await ReactServerDOMClient.createFromReadableStream(
rscStream,
{
serverConsumerManifest: {
moduleMap: null,
moduleLoading: null,
},
},
);
const reader = result.getReader();
const decoder = new TextDecoder();
let text = '';
let entry;
while (!(entry = await reader.read()).done) {
text += decoder.decode(entry.value);
}
expect(text).toBe('Hello streaming world ');
});
it('should support large binary ReadableStreams', async () => {
const chunkCount = 100;
const chunkSize = 1024;
const expectedBytes = [];
const stream = new ReadableStream({
type: 'bytes',
start(controller) {
for (let i = 0; i < chunkCount; i++) {
const chunk = new Uint8Array(chunkSize);
for (let j = 0; j < chunkSize; j++) {
chunk[j] = (i + j) % 256;
}
expectedBytes.push(...Array.from(chunk));
controller.enqueue(chunk);
}
controller.close();
},
});
const rscStream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(stream, {}),
);
const result = await ReactServerDOMClient.createFromReadableStream(
// Use passThrough to split and rejoin chunks at arbitrary boundaries.
passThrough(rscStream),
{
serverConsumerManifest: {
moduleMap: null,
moduleLoading: null,
},
},
);
const reader = result.getReader();
const receivedBytes = [];
let entry;
while (!(entry = await reader.read()).done) {
expect(entry.value instanceof Uint8Array).toBe(true);
receivedBytes.push(...Array.from(entry.value));
}
expect(receivedBytes).toEqual(expectedBytes);
});
it('should support BYOB binary ReadableStreams', async () => {
const sourceBytes = [
const buffer = new Uint8Array([
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
];
// Create separate buffers for each typed array to avoid ArrayBuffer
// transfer issues. Each view needs its own buffer because enqueue()
// transfers ownership.
]).buffer;
const buffers = [
new Int8Array(sourceBytes.slice(1)),
new Uint8Array(sourceBytes.slice(2)),
new Uint8ClampedArray(sourceBytes.slice(2)),
new Int16Array(new Uint8Array(sourceBytes.slice(2)).buffer),
new Uint16Array(new Uint8Array(sourceBytes.slice(2)).buffer),
new Int32Array(new Uint8Array(sourceBytes.slice(4)).buffer),
new Uint32Array(new Uint8Array(sourceBytes.slice(4)).buffer),
new Float32Array(new Uint8Array(sourceBytes.slice(4)).buffer),
new Float64Array(new Uint8Array(sourceBytes.slice(0)).buffer),
new BigInt64Array(new Uint8Array(sourceBytes.slice(0)).buffer),
new BigUint64Array(new Uint8Array(sourceBytes.slice(0)).buffer),
new DataView(new Uint8Array(sourceBytes.slice(3)).buffer),
new Int8Array(buffer, 1),
new Uint8Array(buffer, 2),
new Uint8ClampedArray(buffer, 2),
new Int16Array(buffer, 2),
new Uint16Array(buffer, 2),
new Int32Array(buffer, 4),
new Uint32Array(buffer, 4),
new Float32Array(buffer, 4),
new Float64Array(buffer, 0),
new BigInt64Array(buffer, 0),
new BigUint64Array(buffer, 0),
new DataView(buffer, 3),
];
// Save expected bytes before enqueueing (which will detach the buffers).
const expectedBytes = buffers.flatMap(c =>
Array.from(new Uint8Array(c.buffer, c.byteOffset, c.byteLength)),
);
// This a binary stream where each chunk ends up as Uint8Array.
const s = new ReadableStream({
type: 'bytes',
@@ -1268,7 +1176,11 @@ describe('ReactFlightDOMEdge', () => {
// The streamed buffers might be in different chunks and in Uint8Array form but
// the concatenated bytes should be the same.
expect(streamedBuffers.flatMap(t => Array.from(t))).toEqual(expectedBytes);
expect(streamedBuffers.flatMap(t => Array.from(t))).toEqual(
buffers.flatMap(c =>
Array.from(new Uint8Array(c.buffer, c.byteOffset, c.byteLength)),
),
);
});
// @gate !__DEV__ || enableComponentPerformanceTrack

View File

@@ -10,6 +10,18 @@
'use strict';
// Polyfills for test environment
global.ReadableStream =
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;
global.Blob = require('buffer').Blob;
if (typeof File === 'undefined' || typeof FormData === 'undefined') {
global.File = require('buffer').File || require('undici').File;
global.FormData = require('undici').FormData;
}
let serverExports;
let webpackServerMap;
let ReactServerDOMServer;
@@ -182,33 +194,24 @@ describe('ReactFlightDOMReplyEdge', () => {
});
it('should support BYOB binary ReadableStreams', async () => {
const sourceBytes = [
const buffer = new Uint8Array([
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
];
// Create separate buffers for each typed array to avoid ArrayBuffer
// transfer issues. Each view needs its own buffer because enqueue()
// transfers ownership.
]).buffer;
const buffers = [
new Int8Array(sourceBytes.slice(1)),
new Uint8Array(sourceBytes.slice(2)),
new Uint8ClampedArray(sourceBytes.slice(2)),
new Int16Array(new Uint8Array(sourceBytes.slice(2)).buffer),
new Uint16Array(new Uint8Array(sourceBytes.slice(2)).buffer),
new Int32Array(new Uint8Array(sourceBytes.slice(4)).buffer),
new Uint32Array(new Uint8Array(sourceBytes.slice(4)).buffer),
new Float32Array(new Uint8Array(sourceBytes.slice(4)).buffer),
new Float64Array(new Uint8Array(sourceBytes.slice(0)).buffer),
new BigInt64Array(new Uint8Array(sourceBytes.slice(0)).buffer),
new BigUint64Array(new Uint8Array(sourceBytes.slice(0)).buffer),
new DataView(new Uint8Array(sourceBytes.slice(3)).buffer),
new Int8Array(buffer, 1),
new Uint8Array(buffer, 2),
new Uint8ClampedArray(buffer, 2),
new Int16Array(buffer, 2),
new Uint16Array(buffer, 2),
new Int32Array(buffer, 4),
new Uint32Array(buffer, 4),
new Float32Array(buffer, 4),
new Float64Array(buffer, 0),
new BigInt64Array(buffer, 0),
new BigUint64Array(buffer, 0),
new DataView(buffer, 3),
];
// Save expected bytes before enqueueing (which will detach the buffers).
const expectedBytes = buffers.flatMap(c =>
Array.from(new Uint8Array(c.buffer, c.byteOffset, c.byteLength)),
);
// This a binary stream where each chunk ends up as Uint8Array.
const s = new ReadableStream({
type: 'bytes',
@@ -236,7 +239,11 @@ describe('ReactFlightDOMReplyEdge', () => {
// The streamed buffers might be in different chunks and in Uint8Array form but
// the concatenated bytes should be the same.
expect(streamedBuffers.flatMap(t => Array.from(t))).toEqual(expectedBytes);
expect(streamedBuffers.flatMap(t => Array.from(t))).toEqual(
buffers.flatMap(c =>
Array.from(new Uint8Array(c.buffer, c.byteOffset, c.byteLength)),
),
);
});
it('should abort when parsing an incomplete payload', async () => {

View File

@@ -1149,8 +1149,6 @@ function serializeReadableStream(
supportsBYOB = false;
}
}
// At this point supportsBYOB is guaranteed to be a boolean.
const isByteStream: boolean = supportsBYOB;
const reader = stream.getReader();
@@ -1174,7 +1172,7 @@ function serializeReadableStream(
// The task represents the Stop row. This adds a Start row.
request.pendingChunks++;
const startStreamRow =
streamTask.id.toString(16) + ':' + (isByteStream ? 'r' : 'R') + '\n';
streamTask.id.toString(16) + ':' + (supportsBYOB ? 'r' : 'R') + '\n';
request.completedRegularChunks.push(stringToChunk(startStreamRow));
function progress(entry: {done: boolean, value: ReactClientValue, ...}) {
@@ -1192,15 +1190,9 @@ function serializeReadableStream(
callOnAllReadyIfReady(request);
} else {
try {
request.pendingChunks++;
streamTask.model = entry.value;
if (isByteStream) {
// Chunks of byte streams are always Uint8Array instances.
const chunk: Uint8Array = (streamTask.model: any);
emitTypedArrayChunk(request, streamTask.id, 'b', chunk, false);
} else {
tryStreamTask(request, streamTask);
}
request.pendingChunks++;
tryStreamTask(request, streamTask);
enqueueFlush(request);
reader.read().then(progress, error);
} catch (x) {

View File

@@ -36,7 +36,7 @@ export const disableCommentsAsDOMContainers: boolean = true;
export const disableInputAttributeSyncing: boolean = false;
export const disableLegacyContext: boolean = false;
export const disableLegacyContextForFunctionComponents: boolean = false;
export const disableLegacyMode: boolean = false;
export const disableLegacyMode: boolean = true;
export const disableSchedulerTimeoutInWorkLoop: boolean = false;
export const disableTextareaChildren: boolean = false;
export const enableAsyncDebugInfo: boolean = false;

View File

@@ -21,7 +21,7 @@ export const disableCommentsAsDOMContainers: boolean = true;
export const disableInputAttributeSyncing: boolean = false;
export const disableLegacyContext: boolean = true;
export const disableLegacyContextForFunctionComponents: boolean = true;
export const disableLegacyMode: boolean = false;
export const disableLegacyMode: boolean = true;
export const disableSchedulerTimeoutInWorkLoop: boolean = false;
export const disableTextareaChildren: boolean = false;
export const enableAsyncDebugInfo: boolean = false;