Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4211a7c12f | ||
|
|
8deecf5085 | ||
|
|
93fc57400b | ||
|
|
093b3246e1 | ||
|
|
3a495ae722 | ||
|
|
bbe3f4d322 | ||
|
|
1ea46df8ba | ||
|
|
8c15edd57c | ||
|
|
5e94655cbb | ||
|
|
db8273c12f |
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -954,6 +954,7 @@ function applyEffect(
|
||||
case ValueKind.Primitive: {
|
||||
break;
|
||||
}
|
||||
case ValueKind.MaybeFrozen:
|
||||
case ValueKind.Frozen: {
|
||||
sourceType = 'frozen';
|
||||
break;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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 |
|
||||
```
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
## 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
|
||||
@@ -0,0 +1,13 @@
|
||||
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;
|
||||
}
|
||||
59
packages/react-client/src/ReactFlightClient.js
vendored
59
packages/react-client/src/ReactFlightClient.js
vendored
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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', () => ({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
14
packages/react-server/src/ReactFlightServer.js
vendored
14
packages/react-server/src/ReactFlightServer.js
vendored
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user