Compare commits

..

11 Commits

Author SHA1 Message Date
Jorge Cabiedes Acosta
cd1d0dc2dc [Compiler] Don't count a setState in the dependency array of the effect it is called on as a usage
Summary:
The validation only allows setState declaration as a usage outside of the effect.

Another edge case is that if you add the setState being validated in the dependency array you also make the validation opt out since it counts as a usage outside of the effect.

Added a bit of logic to consider the effect's deps when creating the cache for setState usages within the effect

Test Plan:
Added a fixture
2025-11-13 16:03:11 -08:00
Hendrik Liebau
93fc57400b [Flight] Fix broken byte stream parsing caused by buffer detachment (#35127)
This PR fixes a critical bug where `ReadableStream({type: 'bytes'})`
instances passed through React Server Components (RSC) would stall after
reading only the first chunk or the first few chunks in the client. This
issue was masked by using `web-streams-polyfill` in tests, but manifests
with native Web Streams implementations.

The root cause is that when a chunk is enqueued to a
`ReadableByteStreamController`, the spec requires the underlying
ArrayBuffer to be synchronously transferred/detached. In the React
Flight Client's chunk parsing, embedded byte stream chunks are created
as views into the incoming RSC stream chunk buffer using `new
Uint8Array(chunk.buffer, offset, length)`. When embedded byte stream
chunks are enqueued, they can detach the shared buffer, leaving the RSC
stream parsing in a broken state.

The fix is to copy embedded byte stream chunks before enqueueing them,
preventing buffer detachment from affecting subsequent parsing. To not
affect performance too much, we use a zero-copy optimization: when a
chunk ends exactly at the end of the RSC stream chunk, or when the row
spans into the next RSC chunk, no further parsing will access that
buffer, so we can safely enqueue the view directly without copying.

We now also enqueue embedded byte stream chunks immediately as they are
parsed, without waiting for the full row to complete.

To simplify the logic in the client, we introduce a new `'b'` protocol
tag specifically for byte stream chunks. The server now emits `'b'`
instead of `'o'` for `Uint8Array` chunks from byte streams (detected via
`supportsBYOB`). This allows the client to recognize byte stream chunks
without needing to track stream IDs.

Tests now use the proper Jest environment with native Web Streams
instead of polyfills, exposing and validating the fix for this issue.
2025-11-13 21:23:02 +01:00
Sebastian "Sebbie" Silbermann
093b3246e1 [react-dom] Batch updates from resize until next frame (#35117) 2025-11-13 13:30:21 +01:00
Nathan
3a495ae722 [compiler] source location validator (#35109)
@josephsavona this was briefly discussed in an old thread, lmk your
thoughts on the approach. I have some fixes ready as well but wanted to
get this test case in first... there's some things I don't _love_ about
this approach, but end of the day it's just a tool for the test suite
rather than something for end user folks so even if it does a 70% good
enough job that's fine.

### refresher on the problem
when we generate coverage reports with jest (istanbul), our coverage
ends up completely out of whack due to the AST missing a ton of (let's
call them "important") source locations after the compiler pipeline has
run.

At the moment to get around this, we've been doing something a bit
unorthodox and also running our test suite with istanbul running before
the compiler -- which results in its own set of issues (for eg, things
being memoized differently, or the compiler completely bailing out on
the instrumented code, etc).

before getting in fixes, I wanted to set up a test case to start
chipping away on as you had recommended.

### how it works

The validator basically:
1. Traverses the original AST and collects the source locations for some
"important" node types
- (excludes useMemo/useCallback calls, as those are stripped out by the
compiler)
3. Traverses the generated AST and looks for nodes with matching source
locations.
4. Generates errors for source locations missing nodes in the generated
AST

### caveats/drawbacks

There are some things that don't work super well with this approach. A
more natural test fit I think would be just having some explicit
assertions made against an AST in a test file, as you can just bake all
of the assumptions/nuance in there that are difficult to handle in a
generic manner. However, this is maybe "good enough" for now.

1. Have to be careful what you put into the test fixture. If you put in
some code that the compiler just removes (for eg, a variable assignment
that is unused), you're creating a failure case that's impossible to
fix. I added a skip for useMemo/useCallback.
2. "Important" locations must exactly match for validation to pass.
- Might get tricky making sure things are mapped correctly when a node
type is completely changed, for eg, when a block statement arrow
function body gets turned into an implicit return via the body just
being an expression/identifier.
- This can/could result in scenarios where more changes are needed to
shuttle the locations through due to HIR not having a 1:1 mapping all
the babel nuances, even if some combination of other data might be good
enough even if not 10000% accurate. This might be the _right_ thing
anyways so we don't end up with edge cases having incorrect source
locations.
2025-11-12 19:02:46 -08:00
Ricky
bbe3f4d322 [flags] disableLegacyMode in native-fb (#35120)
this is failing test too because we need the legacy mode in the react
package until we fix the tests
2025-11-12 15:38:58 -05:00
Sebastian "Sebbie" Silbermann
1ea46df8ba [DevTools] Batch updates when updating component filters (#35093) 2025-11-11 23:20:22 +01:00
Sebastian "Sebbie" Silbermann
8c15edd57c [DevTools] Send root unmount as a regular removal operation (#35107) 2025-11-11 23:08:54 +01:00
Jorge Cabiedes
5e94655cbb [compiler] _exp version of ValidateNoDerivedComputationsInEffects take precedence over stable version when enabled (#35099)
Summary:
We should only run one version of the validation. I think it makes sense
that if the exp version is enable it takes precedence over the stable
one

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35099).
* __->__ #35099
* #35100
2025-11-11 10:16:20 -08:00
Jorge Cabiedes
db8273c12f [compiler] Update test snap to include fixture comment (#35100)
Summary:
I missed this test case failing and now having @loggerTestOnly after
landing some other PRs good to know they're not land blocking

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35100).
* #35099
* __->__ #35100
2025-11-11 10:16:04 -08:00
Ricky
04ee54cd12 [tests] add more portal activity tests (#35095)
I copied some tests from
[`Activity-test.js`](1d68bce19c/packages/react-reconciler/src/__tests__/Activity-test.js)
and made them portal specific just to confirm my understanding of how
Portals + Activity interact is correct. Seems good to include them.
2025-11-11 12:47:56 -05:00
Jorge Cabiedes
100fc4a8cf [compiler] Prevent local state source variables from depending on other state (#35044)
Summary:
When a local state is created sometimes it uses a `prop` or even other
local state for its initial value.

This value is only relevant on first render so we shouldn't consider it
part of our data flow

Test Plan:
Added tests
2025-11-10 12:29:34 -08:00
23 changed files with 1255 additions and 164 deletions

View File

@@ -105,6 +105,7 @@ 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}
@@ -272,12 +273,10 @@ 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) {
@@ -559,6 +558,10 @@ 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,6 +364,13 @@ 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

@@ -22,6 +22,7 @@ import {
BasicBlock,
isUseRefType,
SourceLocation,
ArrayExpression,
} from '../HIR';
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
@@ -36,11 +37,17 @@ type DerivationMetadata = {
isStateSource: boolean;
};
type EffectMetadata = {
effect: HIRFunction;
dependencies: ArrayExpression;
};
type ValidationContext = {
readonly functions: Map<IdentifierId, FunctionExpression>;
readonly candidateDependencies: Map<IdentifierId, ArrayExpression>;
readonly errors: CompilerError;
readonly derivationCache: DerivationCache;
readonly effects: Set<HIRFunction>;
readonly effectsCache: Map<IdentifierId, EffectMetadata>;
readonly setStateLoads: Map<IdentifierId, IdentifierId | null>;
readonly setStateUsages: Map<IdentifierId, Set<SourceLocation>>;
};
@@ -175,18 +182,20 @@ export function validateNoDerivedComputationsInEffects_exp(
fn: HIRFunction,
): Result<void, CompilerError> {
const functions: Map<IdentifierId, FunctionExpression> = new Map();
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
const derivationCache = new DerivationCache();
const errors = new CompilerError();
const effects: Set<HIRFunction> = new Set();
const effectsCache: Map<IdentifierId, EffectMetadata> = new Map();
const setStateLoads: Map<IdentifierId, IdentifierId> = new Map();
const setStateUsages: Map<IdentifierId, Set<SourceLocation>> = new Map();
const context: ValidationContext = {
functions,
candidateDependencies,
errors,
derivationCache,
effects,
effectsCache,
setStateLoads,
setStateUsages,
};
@@ -229,8 +238,8 @@ export function validateNoDerivedComputationsInEffects_exp(
isFirstPass = false;
} while (context.derivationCache.snapshot());
for (const effect of effects) {
validateEffect(effect, context);
for (const [, effect] of effectsCache) {
validateEffect(effect.effect, effect.dependencies, context);
}
return errors.asResult();
@@ -354,8 +363,14 @@ function recordInstructionDerivations(
value.args[1].kind === 'Identifier'
) {
const effectFunction = context.functions.get(value.args[0].identifier.id);
if (effectFunction != null) {
context.effects.add(effectFunction.loweredFunc.func);
const deps = context.candidateDependencies.get(
value.args[1].identifier.id,
);
if (effectFunction != null && deps != null) {
context.effectsCache.set(value.args[0].identifier.id, {
effect: effectFunction.loweredFunc.func,
dependencies: deps,
});
}
} else if (isUseStateType(lvalue.identifier) && value.args.length > 0) {
typeOfValue = 'fromState';
@@ -367,6 +382,8 @@ function recordInstructionDerivations(
);
return;
}
} else if (value.kind === 'ArrayExpression') {
context.candidateDependencies.set(lvalue.identifier.id, value);
}
for (const operand of eachInstructionOperand(instr)) {
@@ -596,6 +613,7 @@ function getFnLocalDeps(
function validateEffect(
effectFunction: HIRFunction,
dependencies: ArrayExpression,
context: ValidationContext,
): void {
const seenBlocks: Set<BlockId> = new Set();
@@ -612,6 +630,16 @@ function validateEffect(
Set<SourceLocation>
> = new Map();
// Consider setStates in the effect's dependency array as being part of effectSetStateUsages
for (const dep of dependencies.elements) {
if (dep.kind === 'Identifier') {
const root = getRootSetState(dep.identifier.id, context.setStateLoads);
if (root !== null) {
effectSetStateUsages.set(root, new Set([dep.loc]));
}
}
}
let cleanUpFunctionDeps: Set<IdentifierId> | undefined;
const globals: Set<IdentifierId> = new Set();

View File

@@ -0,0 +1,206 @@
/**
* 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,4 +12,5 @@ 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

@@ -0,0 +1,63 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
function Component({prop}) {
const [s, setS] = useState(0);
useEffect(() => {
setS(prop);
}, [prop, setS]);
return <div>{prop}</div>;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
function Component(t0) {
const $ = _c(5);
const { prop } = t0;
const [, setS] = useState(0);
let t1;
let t2;
if ($[0] !== prop) {
t1 = () => {
setS(prop);
};
t2 = [prop, setS];
$[0] = prop;
$[1] = t1;
$[2] = t2;
} else {
t1 = $[1];
t2 = $[2];
}
useEffect(t1, t2);
let t3;
if ($[3] !== prop) {
t3 = <div>{prop}</div>;
$[3] = prop;
$[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\nProps: [prop]\n\nData Flow Tree:\n└── prop (Prop)\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":6,"column":4,"index":150},"end":{"line":6,"column":8,"index":154},"filename":"effect-used-in-dep-array-still-errors.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":10,"column":1,"index":212},"filename":"effect-used-in-dep-array-still-errors.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,10 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
function Component({prop}) {
const [s, setS] = useState(0);
useEffect(() => {
setS(prop);
}, [prop, setS]);
return <div>{prop}</div>;
}

View File

@@ -2,17 +2,23 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
function Component({ prop }) {
const [s, setS] = useState(prop)
const [second, setSecond] = useState(prop)
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])
setS(second);
}, [second]);
return <div>{s}</div>
return <div>{s}</div>;
}
```
@@ -20,12 +26,12 @@ function Component({ prop }) {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
function Component(t0) {
const $ = _c(5);
const { prop } = t0;
const [s, setS] = useState(prop);
const [s, setS] = useState();
const [second] = useState(prop);
let t1;
let t2;
@@ -54,6 +60,13 @@ function Component(t0) {
}
```
## 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,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
function Component({prop}) {
const [s, setS] = useState();

View File

@@ -0,0 +1,224 @@
## 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

@@ -0,0 +1,29 @@
// @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

@@ -4857,6 +4857,7 @@ export function processBinaryChunk(
resolvedRowTag === 65 /* "A" */ ||
resolvedRowTag === 79 /* "O" */ ||
resolvedRowTag === 111 /* "o" */ ||
resolvedRowTag === 98 /* "b" */ ||
resolvedRowTag === 85 /* "U" */ ||
resolvedRowTag === 83 /* "S" */ ||
resolvedRowTag === 115 /* "s" */ ||
@@ -4916,14 +4917,31 @@ export function processBinaryChunk(
// We found the last chunk of the row
const length = lastIdx - i;
const lastChunk = new Uint8Array(chunk.buffer, offset, length);
processFullBinaryRow(
response,
streamState,
rowID,
rowTag,
buffer,
lastChunk,
);
// 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,
);
}
// Reset state machine for a new row
i = lastIdx;
if (rowState === ROW_CHUNK_BY_NEWLINE) {
@@ -4936,14 +4954,27 @@ export function processBinaryChunk(
rowLength = 0;
buffer.length = 0;
} else {
// 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.
// The rest of this row is in a future chunk.
const length = chunk.byteLength - i;
const remainingSlice = new Uint8Array(chunk.buffer, offset, length);
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;
// 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;
}
break;
}
}

View File

@@ -1576,7 +1576,6 @@ export function attach(
currentRoot = rootInstance;
unmountInstanceRecursively(rootInstance);
rootToFiberInstanceMap.delete(root);
flushPendingEvents();
currentRoot = (null: any);
});
@@ -1646,7 +1645,6 @@ export function attach(
currentRoot = newRoot;
setRootPseudoKey(currentRoot.id, root.current);
mountFiberRecursively(root.current, false);
flushPendingEvents();
currentRoot = (null: any);
});
@@ -2159,7 +2157,6 @@ 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__) {
@@ -2187,8 +2184,7 @@ export function attach(
pendingOperations.length === 0 &&
pendingRealUnmountedIDs.length === 0 &&
pendingRealUnmountedSuspenseIDs.length === 0 &&
pendingSuspenderChanges.size === 0 &&
pendingUnmountedRootID === null
pendingSuspenderChanges.size === 0
);
}
@@ -2250,9 +2246,7 @@ export function attach(
return;
}
const numUnmountIDs =
pendingRealUnmountedIDs.length +
(pendingUnmountedRootID === null ? 0 : 1);
const numUnmountIDs = pendingRealUnmountedIDs.length;
const numUnmountSuspenseIDs = pendingRealUnmountedSuspenseIDs.length;
const numSuspenderChanges = pendingSuspenderChanges.size;
@@ -2330,11 +2324,6 @@ 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.
@@ -2382,7 +2371,6 @@ export function attach(
pendingRealUnmountedIDs.length = 0;
pendingRealUnmountedSuspenseIDs.length = 0;
pendingSuspenderChanges.clear();
pendingUnmountedRootID = null;
pendingStringTable.clear();
pendingStringTableLength = 0;
}
@@ -2868,7 +2856,6 @@ export function attach(
// Already disconnected.
return;
}
const fiber = fiberInstance.data;
if (trackedPathMatchInstance === fiberInstance) {
// We're in the process of trying to restore previous selection.
@@ -2878,17 +2865,7 @@ export function attach(
}
const id = fiberInstance.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);
}
pendingRealUnmountedIDs.push(id);
}
function recordSuspenseResize(suspenseNode: SuspenseNode): void {
@@ -5772,11 +5749,12 @@ export function attach(
mountFiberRecursively(root.current, false);
flushPendingEvents();
needsToFlushComponentLogs = false;
currentRoot = (null: any);
});
flushPendingEvents();
needsToFlushComponentLogs = false;
}
}

View File

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

View File

@@ -10,11 +10,17 @@
'use strict';
let React;
let Activity;
let useState;
let ReactDOM;
let ReactDOMClient;
let Scheduler;
let act;
let Activity;
let useState;
let useLayoutEffect;
let useEffect;
let LegacyHidden;
let assertLog;
let Suspense;
describe('ReactDOMActivity', () => {
let container;
@@ -22,11 +28,19 @@ 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');
act = require('internal-test-utils').act;
const InternalTestUtils = require('internal-test-utils');
act = InternalTestUtils.act;
assertLog = InternalTestUtils.assertLog;
container = document.createElement('div');
document.body.appendChild(container);
});
@@ -35,6 +49,11 @@ 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 ' +
@@ -53,7 +72,7 @@ describe('ReactDOMActivity', () => {
);
}
function App({portalContents}) {
function App() {
return (
<Accordion>
<div>
@@ -99,7 +118,7 @@ describe('ReactDOMActivity', () => {
);
}
function App({portalContents}) {
function App() {
return (
<Activity mode="hidden">
<div>
@@ -131,4 +150,416 @@ 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,7 +40,6 @@ 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},
@@ -61,7 +60,6 @@ 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},
@@ -82,7 +80,6 @@ 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', () => ({
@@ -104,7 +101,6 @@ 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', () => ({
@@ -147,7 +143,6 @@ 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},
@@ -168,7 +163,6 @@ 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},
@@ -189,7 +183,6 @@ 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', () => ({
@@ -212,7 +205,6 @@ 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,18 +5,11 @@
* 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,17 +5,11 @@
* 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;
@@ -29,7 +23,6 @@ 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,18 +10,6 @@
'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;
@@ -127,8 +115,16 @@ 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;
@@ -1118,25 +1114,121 @@ 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 buffer = new Uint8Array([
const sourceBytes = [
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
]).buffer;
const buffers = [
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),
];
// Create separate buffers for each typed array to avoid ArrayBuffer
// transfer issues. Each view needs its own buffer because enqueue()
// transfers ownership.
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),
];
// 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',
@@ -1176,11 +1268,7 @@ 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(
buffers.flatMap(c =>
Array.from(new Uint8Array(c.buffer, c.byteOffset, c.byteLength)),
),
);
expect(streamedBuffers.flatMap(t => Array.from(t))).toEqual(expectedBytes);
});
// @gate !__DEV__ || enableComponentPerformanceTrack

View File

@@ -10,18 +10,6 @@
'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;
@@ -194,24 +182,33 @@ describe('ReactFlightDOMReplyEdge', () => {
});
it('should support BYOB binary ReadableStreams', async () => {
const buffer = new Uint8Array([
const sourceBytes = [
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
]).buffer;
const buffers = [
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),
];
// Create separate buffers for each typed array to avoid ArrayBuffer
// transfer issues. Each view needs its own buffer because enqueue()
// transfers ownership.
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),
];
// 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',
@@ -239,11 +236,7 @@ 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(
buffers.flatMap(c =>
Array.from(new Uint8Array(c.buffer, c.byteOffset, c.byteLength)),
),
);
expect(streamedBuffers.flatMap(t => Array.from(t))).toEqual(expectedBytes);
});
it('should abort when parsing an incomplete payload', async () => {

View File

@@ -1149,6 +1149,8 @@ function serializeReadableStream(
supportsBYOB = false;
}
}
// At this point supportsBYOB is guaranteed to be a boolean.
const isByteStream: boolean = supportsBYOB;
const reader = stream.getReader();
@@ -1172,7 +1174,7 @@ function serializeReadableStream(
// The task represents the Stop row. This adds a Start row.
request.pendingChunks++;
const startStreamRow =
streamTask.id.toString(16) + ':' + (supportsBYOB ? 'r' : 'R') + '\n';
streamTask.id.toString(16) + ':' + (isByteStream ? 'r' : 'R') + '\n';
request.completedRegularChunks.push(stringToChunk(startStreamRow));
function progress(entry: {done: boolean, value: ReactClientValue, ...}) {
@@ -1190,9 +1192,15 @@ function serializeReadableStream(
callOnAllReadyIfReady(request);
} else {
try {
streamTask.model = entry.value;
request.pendingChunks++;
tryStreamTask(request, streamTask);
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);
}
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 = true;
export const disableLegacyMode: boolean = false;
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 = true;
export const disableLegacyMode: boolean = false;
export const disableSchedulerTimeoutInWorkLoop: boolean = false;
export const disableTextareaChildren: boolean = false;
export const enableAsyncDebugInfo: boolean = false;