Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54d7abde6e |
@@ -105,7 +105,6 @@ import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRan
|
||||
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
|
||||
import {validateNoDerivedComputationsInEffects_exp} from '../Validation/ValidateNoDerivedComputationsInEffects_exp';
|
||||
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
|
||||
import {validateSourceLocations} from '../Validation/ValidateSourceLocations';
|
||||
|
||||
export type CompilerPipelineValue =
|
||||
| {kind: 'ast'; name: string; value: CodegenFunction}
|
||||
@@ -558,10 +557,6 @@ function runWithEnvironment(
|
||||
log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
|
||||
}
|
||||
|
||||
if (env.config.validateSourceLocations) {
|
||||
validateSourceLocations(func, ast).unwrap();
|
||||
}
|
||||
|
||||
/**
|
||||
* This flag should be only set for unit / fixture tests to check
|
||||
* that Forget correctly handles unexpected errors (e.g. exceptions
|
||||
|
||||
@@ -364,13 +364,6 @@ export const EnvironmentConfigSchema = z.object({
|
||||
validateNoCapitalizedCalls: z.nullable(z.array(z.string())).default(null),
|
||||
validateBlocklistedImports: z.nullable(z.array(z.string())).default(null),
|
||||
|
||||
/**
|
||||
* Validates that AST nodes generated during codegen have proper source locations.
|
||||
* This is useful for debugging issues with source maps and Istanbul coverage.
|
||||
* When enabled, the compiler will error if important source locations are missing in the generated AST.
|
||||
*/
|
||||
validateSourceLocations: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validate against impure functions called during render
|
||||
*/
|
||||
|
||||
@@ -1875,6 +1875,14 @@ export function isSetStateType(id: Identifier): boolean {
|
||||
return id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetState';
|
||||
}
|
||||
|
||||
export function isAnySetStateType(id: Identifier): boolean {
|
||||
return (
|
||||
id.type.kind === 'Function' &&
|
||||
(id.type.shapeId === 'InferredSetState' ||
|
||||
id.type.shapeId === 'BuiltInSetState')
|
||||
);
|
||||
}
|
||||
|
||||
export function isUseActionStateType(id: Identifier): boolean {
|
||||
return (
|
||||
id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseActionState'
|
||||
|
||||
@@ -386,6 +386,7 @@ export const BuiltInJsxId = 'BuiltInJsx';
|
||||
export const BuiltInObjectId = 'BuiltInObject';
|
||||
export const BuiltInUseStateId = 'BuiltInUseState';
|
||||
export const BuiltInSetStateId = 'BuiltInSetState';
|
||||
export const InferredSetState = 'InferredSetState';
|
||||
export const BuiltInUseActionStateId = 'BuiltInUseActionState';
|
||||
export const BuiltInSetActionStateId = 'BuiltInSetActionState';
|
||||
export const BuiltInUseRefId = 'BuiltInUseRefId';
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
ReactiveScopeDependencies,
|
||||
Terminal,
|
||||
isUseRefType,
|
||||
isSetStateType,
|
||||
isAnySetStateType,
|
||||
isFireFunctionType,
|
||||
makeScopeId,
|
||||
HIR,
|
||||
@@ -223,7 +223,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
for (const maybeDep of minimalDeps) {
|
||||
if (
|
||||
((isUseRefType(maybeDep.identifier) ||
|
||||
isSetStateType(maybeDep.identifier)) &&
|
||||
isAnySetStateType(maybeDep.identifier)) &&
|
||||
!reactiveIds.has(maybeDep.identifier.id)) ||
|
||||
isFireFunctionType(maybeDep.identifier) ||
|
||||
isEffectEventFunctionType(maybeDep.identifier)
|
||||
|
||||
@@ -31,8 +31,8 @@ import {
|
||||
BuiltInObjectId,
|
||||
BuiltInPropsId,
|
||||
BuiltInRefValueId,
|
||||
BuiltInSetStateId,
|
||||
BuiltInUseRefId,
|
||||
InferredSetState,
|
||||
} from '../HIR/ObjectShape';
|
||||
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
@@ -281,7 +281,7 @@ function* generateInstructionTypes(
|
||||
if (env.config.enableTreatSetIdentifiersAsStateSetters) {
|
||||
const name = getName(names, value.callee.identifier.id);
|
||||
if (name.startsWith('set')) {
|
||||
shapeId = BuiltInSetStateId;
|
||||
shapeId = InferredSetState;
|
||||
}
|
||||
}
|
||||
yield equation(value.callee.identifier.type, {
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
BasicBlock,
|
||||
isUseRefType,
|
||||
SourceLocation,
|
||||
ArrayExpression,
|
||||
} from '../HIR';
|
||||
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
@@ -37,17 +36,11 @@ 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 effectsCache: Map<IdentifierId, EffectMetadata>;
|
||||
readonly effects: Set<HIRFunction>;
|
||||
readonly setStateLoads: Map<IdentifierId, IdentifierId | null>;
|
||||
readonly setStateUsages: Map<IdentifierId, Set<SourceLocation>>;
|
||||
};
|
||||
@@ -182,20 +175,18 @@ 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 effectsCache: Map<IdentifierId, EffectMetadata> = new Map();
|
||||
const effects: Set<HIRFunction> = new Set();
|
||||
|
||||
const setStateLoads: Map<IdentifierId, IdentifierId> = new Map();
|
||||
const setStateUsages: Map<IdentifierId, Set<SourceLocation>> = new Map();
|
||||
|
||||
const context: ValidationContext = {
|
||||
functions,
|
||||
candidateDependencies,
|
||||
errors,
|
||||
derivationCache,
|
||||
effectsCache,
|
||||
effects,
|
||||
setStateLoads,
|
||||
setStateUsages,
|
||||
};
|
||||
@@ -238,8 +229,8 @@ export function validateNoDerivedComputationsInEffects_exp(
|
||||
isFirstPass = false;
|
||||
} while (context.derivationCache.snapshot());
|
||||
|
||||
for (const [, effect] of effectsCache) {
|
||||
validateEffect(effect.effect, effect.dependencies, context);
|
||||
for (const effect of effects) {
|
||||
validateEffect(effect, context);
|
||||
}
|
||||
|
||||
return errors.asResult();
|
||||
@@ -363,14 +354,8 @@ function recordInstructionDerivations(
|
||||
value.args[1].kind === 'Identifier'
|
||||
) {
|
||||
const effectFunction = context.functions.get(value.args[0].identifier.id);
|
||||
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,
|
||||
});
|
||||
if (effectFunction != null) {
|
||||
context.effects.add(effectFunction.loweredFunc.func);
|
||||
}
|
||||
} else if (isUseStateType(lvalue.identifier) && value.args.length > 0) {
|
||||
typeOfValue = 'fromState';
|
||||
@@ -382,8 +367,6 @@ function recordInstructionDerivations(
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else if (value.kind === 'ArrayExpression') {
|
||||
context.candidateDependencies.set(lvalue.identifier.id, value);
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
@@ -613,7 +596,6 @@ function getFnLocalDeps(
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
dependencies: ArrayExpression,
|
||||
context: ValidationContext,
|
||||
): void {
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
@@ -630,16 +612,6 @@ 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();
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {Environment} from '../HIR/Environment';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {HIRFunction, IdentifierId, isSetStateType} from '../HIR';
|
||||
import {HIRFunction, IdentifierId, isAnySetStateType} from '../HIR';
|
||||
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
import {Result} from '../Utils/Result';
|
||||
@@ -85,7 +86,7 @@ function validateNoSetStateInRenderImpl(
|
||||
// faster-path to check if the function expression references a setState
|
||||
[...eachInstructionValueOperand(instr.value)].some(
|
||||
operand =>
|
||||
isSetStateType(operand.identifier) ||
|
||||
isAnySetStateType(operand.identifier) ||
|
||||
unconditionalSetStateFunctions.has(operand.identifier.id),
|
||||
) &&
|
||||
// if yes, does it unconditonally call it?
|
||||
@@ -136,7 +137,7 @@ function validateNoSetStateInRenderImpl(
|
||||
case 'CallExpression': {
|
||||
const callee = instr.value.callee;
|
||||
if (
|
||||
isSetStateType(callee.identifier) ||
|
||||
isAnySetStateType(callee.identifier) ||
|
||||
unconditionalSetStateFunctions.has(callee.identifier.id)
|
||||
) {
|
||||
if (activeManualMemoId !== null) {
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {CompilerDiagnostic, CompilerError, ErrorCategory} from '..';
|
||||
import {CodegenFunction} from '../ReactiveScopes';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
/**
|
||||
* IMPORTANT: This validation is only intended for use in unit tests.
|
||||
* It is not intended for use in production.
|
||||
*
|
||||
* This validation is used to ensure that the generated AST has proper source locations
|
||||
* for "important" original nodes.
|
||||
*
|
||||
* There's one big gotcha with this validation: it only works if the "important" original nodes
|
||||
* are not optimized away by the compiler.
|
||||
*
|
||||
* When that scenario happens, we should just update the fixture to not include a node that has no
|
||||
* corresponding node in the generated AST due to being completely removed during compilation.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Some common node types that are important for coverage tracking.
|
||||
* Based on istanbul-lib-instrument
|
||||
*/
|
||||
const IMPORTANT_INSTRUMENTED_TYPES = new Set([
|
||||
'ArrowFunctionExpression',
|
||||
'AssignmentPattern',
|
||||
'ObjectMethod',
|
||||
'ExpressionStatement',
|
||||
'BreakStatement',
|
||||
'ContinueStatement',
|
||||
'ReturnStatement',
|
||||
'ThrowStatement',
|
||||
'TryStatement',
|
||||
'VariableDeclarator',
|
||||
'IfStatement',
|
||||
'ForStatement',
|
||||
'ForInStatement',
|
||||
'ForOfStatement',
|
||||
'WhileStatement',
|
||||
'DoWhileStatement',
|
||||
'SwitchStatement',
|
||||
'SwitchCase',
|
||||
'WithStatement',
|
||||
'FunctionDeclaration',
|
||||
'FunctionExpression',
|
||||
'LabeledStatement',
|
||||
'ConditionalExpression',
|
||||
'LogicalExpression',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Check if a node is a manual memoization call that the compiler optimizes away.
|
||||
* These include useMemo and useCallback calls, which are intentionally removed
|
||||
* by the DropManualMemoization pass.
|
||||
*/
|
||||
function isManualMemoization(node: t.Node): boolean {
|
||||
// Check if this is a useMemo/useCallback call expression
|
||||
if (t.isCallExpression(node)) {
|
||||
const callee = node.callee;
|
||||
if (t.isIdentifier(callee)) {
|
||||
return callee.name === 'useMemo' || callee.name === 'useCallback';
|
||||
}
|
||||
if (
|
||||
t.isMemberExpression(callee) &&
|
||||
t.isIdentifier(callee.property) &&
|
||||
t.isIdentifier(callee.object)
|
||||
) {
|
||||
return (
|
||||
callee.object.name === 'React' &&
|
||||
(callee.property.name === 'useMemo' ||
|
||||
callee.property.name === 'useCallback')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a location key for comparison. We compare by line/column/source,
|
||||
* not by object identity.
|
||||
*/
|
||||
function locationKey(loc: t.SourceLocation): string {
|
||||
return `${loc.start.line}:${loc.start.column}-${loc.end.line}:${loc.end.column}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that important source locations from the original code are preserved
|
||||
* in the generated AST. This ensures that Istanbul coverage instrumentation can
|
||||
* properly map back to the original source code.
|
||||
*
|
||||
* The validator:
|
||||
* 1. Collects locations from "important" nodes in the original AST (those that
|
||||
* Istanbul instruments for coverage tracking)
|
||||
* 2. Exempts known compiler optimizations (useMemo/useCallback removal)
|
||||
* 3. Verifies that all important locations appear somewhere in the generated AST
|
||||
*
|
||||
* Missing locations can cause Istanbul to fail to track coverage for certain
|
||||
* code paths, leading to inaccurate coverage reports.
|
||||
*/
|
||||
export function validateSourceLocations(
|
||||
func: NodePath<
|
||||
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
|
||||
>,
|
||||
generatedAst: CodegenFunction,
|
||||
): Result<void, CompilerError> {
|
||||
const errors = new CompilerError();
|
||||
|
||||
// Step 1: Collect important locations from the original source
|
||||
const importantOriginalLocations = new Map<
|
||||
string,
|
||||
{loc: t.SourceLocation; nodeType: string}
|
||||
>();
|
||||
|
||||
func.traverse({
|
||||
enter(path) {
|
||||
const node = path.node;
|
||||
|
||||
// Only track node types that Istanbul instruments
|
||||
if (!IMPORTANT_INSTRUMENTED_TYPES.has(node.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip manual memoization that the compiler intentionally removes
|
||||
if (isManualMemoization(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect the location if it exists
|
||||
if (node.loc) {
|
||||
const key = locationKey(node.loc);
|
||||
importantOriginalLocations.set(key, {
|
||||
loc: node.loc,
|
||||
nodeType: node.type,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Step 2: Collect all locations from the generated AST
|
||||
const generatedLocations = new Set<string>();
|
||||
|
||||
function collectGeneratedLocations(node: t.Node): void {
|
||||
if (node.loc) {
|
||||
generatedLocations.add(locationKey(node.loc));
|
||||
}
|
||||
|
||||
// Use Babel's VISITOR_KEYS to traverse only actual node properties
|
||||
const keys = t.VISITOR_KEYS[node.type as keyof typeof t.VISITOR_KEYS];
|
||||
|
||||
if (!keys) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
const value = (node as any)[key];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
if (t.isNode(item)) {
|
||||
collectGeneratedLocations(item);
|
||||
}
|
||||
}
|
||||
} else if (t.isNode(value)) {
|
||||
collectGeneratedLocations(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect from main function body
|
||||
collectGeneratedLocations(generatedAst.body);
|
||||
|
||||
// Collect from outlined functions
|
||||
for (const outlined of generatedAst.outlined) {
|
||||
collectGeneratedLocations(outlined.fn.body);
|
||||
}
|
||||
|
||||
// Step 3: Validate that all important locations are preserved
|
||||
for (const [key, {loc, nodeType}] of importantOriginalLocations) {
|
||||
if (!generatedLocations.has(key)) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Todo,
|
||||
reason: 'Important source location missing in generated code',
|
||||
description:
|
||||
`Source location for ${nodeType} is missing in the generated output. This can cause coverage instrumentation ` +
|
||||
`to fail to track this code properly, resulting in inaccurate coverage reports.`,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return errors.asResult();
|
||||
}
|
||||
@@ -12,5 +12,4 @@ export {validateNoCapitalizedCalls} from './ValidateNoCapitalizedCalls';
|
||||
export {validateNoRefAccessInRender} from './ValidateNoRefAccessInRender';
|
||||
export {validateNoSetStateInRender} from './ValidateNoSetStateInRender';
|
||||
export {validatePreservedManualMemoization} from './ValidatePreservedManualMemoization';
|
||||
export {validateSourceLocations} from './ValidateSourceLocations';
|
||||
export {validateUseMemo} from './ValidateUseMemo';
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
|
||||
## 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
|
||||
@@ -1,10 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
function Component({prop}) {
|
||||
const [s, setS] = useState(0);
|
||||
useEffect(() => {
|
||||
setS(prop);
|
||||
}, [prop, setS]);
|
||||
|
||||
return <div>{prop}</div>;
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateSourceLocations
|
||||
import {useEffect, useCallback} from 'react';
|
||||
|
||||
function Component({prop1, prop2}) {
|
||||
const x = prop1 + prop2;
|
||||
const y = x * 2;
|
||||
const arr = [x, y];
|
||||
const obj = {x, y};
|
||||
const [a, b] = arr;
|
||||
const {x: c, y: d} = obj;
|
||||
|
||||
useEffect(() => {
|
||||
if (a > 10) {
|
||||
console.log(a);
|
||||
}
|
||||
}, [a]);
|
||||
|
||||
const foo = useCallback(() => {
|
||||
return a + b;
|
||||
}, [a, b]);
|
||||
|
||||
function bar() {
|
||||
return (c + d) * 2;
|
||||
}
|
||||
|
||||
console.log('Hello, world!');
|
||||
|
||||
return [y, foo, bar];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 13 errors:
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:5:8
|
||||
3 |
|
||||
4 | function Component({prop1, prop2}) {
|
||||
> 5 | const x = prop1 + prop2;
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
6 | const y = x * 2;
|
||||
7 | const arr = [x, y];
|
||||
8 | const obj = {x, y};
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:6:8
|
||||
4 | function Component({prop1, prop2}) {
|
||||
5 | const x = prop1 + prop2;
|
||||
> 6 | const y = x * 2;
|
||||
| ^^^^^^^^^
|
||||
7 | const arr = [x, y];
|
||||
8 | const obj = {x, y};
|
||||
9 | const [a, b] = arr;
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:7:8
|
||||
5 | const x = prop1 + prop2;
|
||||
6 | const y = x * 2;
|
||||
> 7 | const arr = [x, y];
|
||||
| ^^^^^^^^^^^^
|
||||
8 | const obj = {x, y};
|
||||
9 | const [a, b] = arr;
|
||||
10 | const {x: c, y: d} = obj;
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:8:8
|
||||
6 | const y = x * 2;
|
||||
7 | const arr = [x, y];
|
||||
> 8 | const obj = {x, y};
|
||||
| ^^^^^^^^^^^^
|
||||
9 | const [a, b] = arr;
|
||||
10 | const {x: c, y: d} = obj;
|
||||
11 |
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:9:8
|
||||
7 | const arr = [x, y];
|
||||
8 | const obj = {x, y};
|
||||
> 9 | const [a, b] = arr;
|
||||
| ^^^^^^^^^^^^
|
||||
10 | const {x: c, y: d} = obj;
|
||||
11 |
|
||||
12 | useEffect(() => {
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:10:8
|
||||
8 | const obj = {x, y};
|
||||
9 | const [a, b] = arr;
|
||||
> 10 | const {x: c, y: d} = obj;
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
11 |
|
||||
12 | useEffect(() => {
|
||||
13 | if (a > 10) {
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for ExpressionStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:12:2
|
||||
10 | const {x: c, y: d} = obj;
|
||||
11 |
|
||||
> 12 | useEffect(() => {
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 13 | if (a > 10) {
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 14 | console.log(a);
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 15 | }
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 16 | }, [a]);
|
||||
| ^^^^^^^^^^^
|
||||
17 |
|
||||
18 | const foo = useCallback(() => {
|
||||
19 | return a + b;
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for ExpressionStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:14:6
|
||||
12 | useEffect(() => {
|
||||
13 | if (a > 10) {
|
||||
> 14 | console.log(a);
|
||||
| ^^^^^^^^^^^^^^^
|
||||
15 | }
|
||||
16 | }, [a]);
|
||||
17 |
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:18:8
|
||||
16 | }, [a]);
|
||||
17 |
|
||||
> 18 | const foo = useCallback(() => {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 19 | return a + b;
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 20 | }, [a, b]);
|
||||
| ^^^^^^^^^^^^^
|
||||
21 |
|
||||
22 | function bar() {
|
||||
23 | return (c + d) * 2;
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for ReturnStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:19:4
|
||||
17 |
|
||||
18 | const foo = useCallback(() => {
|
||||
> 19 | return a + b;
|
||||
| ^^^^^^^^^^^^^
|
||||
20 | }, [a, b]);
|
||||
21 |
|
||||
22 | function bar() {
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for ReturnStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:23:4
|
||||
21 |
|
||||
22 | function bar() {
|
||||
> 23 | return (c + d) * 2;
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
24 | }
|
||||
25 |
|
||||
26 | console.log('Hello, world!');
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for ExpressionStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:26:2
|
||||
24 | }
|
||||
25 |
|
||||
> 26 | console.log('Hello, world!');
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
27 |
|
||||
28 | return [y, foo, bar];
|
||||
29 | }
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for ReturnStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:28:2
|
||||
26 | console.log('Hello, world!');
|
||||
27 |
|
||||
> 28 | return [y, foo, bar];
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
29 | }
|
||||
30 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
// @validateSourceLocations
|
||||
import {useEffect, useCallback} from 'react';
|
||||
|
||||
function Component({prop1, prop2}) {
|
||||
const x = prop1 + prop2;
|
||||
const y = x * 2;
|
||||
const arr = [x, y];
|
||||
const obj = {x, y};
|
||||
const [a, b] = arr;
|
||||
const {x: c, y: d} = obj;
|
||||
|
||||
useEffect(() => {
|
||||
if (a > 10) {
|
||||
console.log(a);
|
||||
}
|
||||
}, [a]);
|
||||
|
||||
const foo = useCallback(() => {
|
||||
return a + b;
|
||||
}, [a, b]);
|
||||
|
||||
function bar() {
|
||||
return (c + d) * 2;
|
||||
}
|
||||
|
||||
console.log('Hello, world!');
|
||||
|
||||
return [y, foo, bar];
|
||||
}
|
||||
59
packages/react-client/src/ReactFlightClient.js
vendored
59
packages/react-client/src/ReactFlightClient.js
vendored
@@ -4857,7 +4857,6 @@ export function processBinaryChunk(
|
||||
resolvedRowTag === 65 /* "A" */ ||
|
||||
resolvedRowTag === 79 /* "O" */ ||
|
||||
resolvedRowTag === 111 /* "o" */ ||
|
||||
resolvedRowTag === 98 /* "b" */ ||
|
||||
resolvedRowTag === 85 /* "U" */ ||
|
||||
resolvedRowTag === 83 /* "S" */ ||
|
||||
resolvedRowTag === 115 /* "s" */ ||
|
||||
@@ -4917,31 +4916,14 @@ export function processBinaryChunk(
|
||||
// We found the last chunk of the row
|
||||
const length = lastIdx - i;
|
||||
const lastChunk = new Uint8Array(chunk.buffer, offset, length);
|
||||
|
||||
// Check if this is a Uint8Array for a byte stream. We enqueue it
|
||||
// immediately but need to determine if we can use zero-copy or must copy.
|
||||
if (rowTag === 98 /* "b" */) {
|
||||
resolveBuffer(
|
||||
response,
|
||||
rowID,
|
||||
// If we're at the end of the RSC chunk, no more parsing will access
|
||||
// this buffer and we don't need to copy the chunk to allow detaching
|
||||
// the buffer, otherwise we need to copy.
|
||||
lastIdx === chunkLength ? lastChunk : lastChunk.slice(),
|
||||
streamState,
|
||||
);
|
||||
} else {
|
||||
// Process all other row types.
|
||||
processFullBinaryRow(
|
||||
response,
|
||||
streamState,
|
||||
rowID,
|
||||
rowTag,
|
||||
buffer,
|
||||
lastChunk,
|
||||
);
|
||||
}
|
||||
|
||||
processFullBinaryRow(
|
||||
response,
|
||||
streamState,
|
||||
rowID,
|
||||
rowTag,
|
||||
buffer,
|
||||
lastChunk,
|
||||
);
|
||||
// Reset state machine for a new row
|
||||
i = lastIdx;
|
||||
if (rowState === ROW_CHUNK_BY_NEWLINE) {
|
||||
@@ -4954,27 +4936,14 @@ export function processBinaryChunk(
|
||||
rowLength = 0;
|
||||
buffer.length = 0;
|
||||
} else {
|
||||
// The rest of this row is in a future chunk.
|
||||
// The rest of this row is in a future chunk. We stash the rest of the
|
||||
// current chunk until we can process the full row.
|
||||
const length = chunk.byteLength - i;
|
||||
const remainingSlice = new Uint8Array(chunk.buffer, offset, length);
|
||||
|
||||
// For byte streams, we can enqueue the partial row immediately without
|
||||
// copying since we're at the end of the RSC chunk and no more parsing
|
||||
// will access this buffer.
|
||||
if (rowTag === 98 /* "b" */) {
|
||||
// Update how many bytes we're still waiting for. We need to do this
|
||||
// before enqueueing, as enqueue will detach the buffer and byteLength
|
||||
// will become 0.
|
||||
rowLength -= remainingSlice.byteLength;
|
||||
resolveBuffer(response, rowID, remainingSlice, streamState);
|
||||
} else {
|
||||
// For other row types, stash the rest of the current chunk until we can
|
||||
// process the full row.
|
||||
buffer.push(remainingSlice);
|
||||
// Update how many bytes we're still waiting for. If we're looking for
|
||||
// a newline, this doesn't hurt since we'll just ignore it.
|
||||
rowLength -= remainingSlice.byteLength;
|
||||
}
|
||||
buffer.push(remainingSlice);
|
||||
// Update how many bytes we're still waiting for. If we're looking for
|
||||
// a newline, this doesn't hurt since we'll just ignore it.
|
||||
rowLength -= remainingSlice.byteLength;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1576,6 +1576,7 @@ export function attach(
|
||||
currentRoot = rootInstance;
|
||||
unmountInstanceRecursively(rootInstance);
|
||||
rootToFiberInstanceMap.delete(root);
|
||||
flushPendingEvents();
|
||||
currentRoot = (null: any);
|
||||
});
|
||||
|
||||
@@ -1645,6 +1646,7 @@ export function attach(
|
||||
currentRoot = newRoot;
|
||||
setRootPseudoKey(currentRoot.id, root.current);
|
||||
mountFiberRecursively(root.current, false);
|
||||
flushPendingEvents();
|
||||
currentRoot = (null: any);
|
||||
});
|
||||
|
||||
@@ -2157,6 +2159,7 @@ export function attach(
|
||||
let pendingOperationsQueue: Array<OperationsArray> | null = [];
|
||||
const pendingStringTable: Map<string, StringTableEntry> = new Map();
|
||||
let pendingStringTableLength: number = 0;
|
||||
let pendingUnmountedRootID: FiberInstance['id'] | null = null;
|
||||
|
||||
function pushOperation(op: number): void {
|
||||
if (__DEV__) {
|
||||
@@ -2184,7 +2187,8 @@ export function attach(
|
||||
pendingOperations.length === 0 &&
|
||||
pendingRealUnmountedIDs.length === 0 &&
|
||||
pendingRealUnmountedSuspenseIDs.length === 0 &&
|
||||
pendingSuspenderChanges.size === 0
|
||||
pendingSuspenderChanges.size === 0 &&
|
||||
pendingUnmountedRootID === null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2246,7 +2250,9 @@ export function attach(
|
||||
return;
|
||||
}
|
||||
|
||||
const numUnmountIDs = pendingRealUnmountedIDs.length;
|
||||
const numUnmountIDs =
|
||||
pendingRealUnmountedIDs.length +
|
||||
(pendingUnmountedRootID === null ? 0 : 1);
|
||||
const numUnmountSuspenseIDs = pendingRealUnmountedSuspenseIDs.length;
|
||||
const numSuspenderChanges = pendingSuspenderChanges.size;
|
||||
|
||||
@@ -2324,6 +2330,11 @@ export function attach(
|
||||
for (let j = 0; j < pendingRealUnmountedIDs.length; j++) {
|
||||
operations[i++] = pendingRealUnmountedIDs[j];
|
||||
}
|
||||
// The root ID should always be unmounted last.
|
||||
if (pendingUnmountedRootID !== null) {
|
||||
operations[i] = pendingUnmountedRootID;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// Fill in pending operations.
|
||||
@@ -2371,6 +2382,7 @@ export function attach(
|
||||
pendingRealUnmountedIDs.length = 0;
|
||||
pendingRealUnmountedSuspenseIDs.length = 0;
|
||||
pendingSuspenderChanges.clear();
|
||||
pendingUnmountedRootID = null;
|
||||
pendingStringTable.clear();
|
||||
pendingStringTableLength = 0;
|
||||
}
|
||||
@@ -2856,6 +2868,7 @@ export function attach(
|
||||
// Already disconnected.
|
||||
return;
|
||||
}
|
||||
const fiber = fiberInstance.data;
|
||||
|
||||
if (trackedPathMatchInstance === fiberInstance) {
|
||||
// We're in the process of trying to restore previous selection.
|
||||
@@ -2865,7 +2878,17 @@ export function attach(
|
||||
}
|
||||
|
||||
const id = fiberInstance.id;
|
||||
pendingRealUnmountedIDs.push(id);
|
||||
const isRoot = fiber.tag === HostRoot;
|
||||
if (isRoot) {
|
||||
// Roots must be removed only after all children have been removed.
|
||||
// So we track it separately.
|
||||
pendingUnmountedRootID = id;
|
||||
} else {
|
||||
// To maintain child-first ordering,
|
||||
// we'll push it into one of these queues,
|
||||
// and later arrange them in the correct order.
|
||||
pendingRealUnmountedIDs.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
function recordSuspenseResize(suspenseNode: SuspenseNode): void {
|
||||
@@ -5749,12 +5772,11 @@ export function attach(
|
||||
|
||||
mountFiberRecursively(root.current, false);
|
||||
|
||||
flushPendingEvents();
|
||||
|
||||
needsToFlushComponentLogs = false;
|
||||
currentRoot = (null: any);
|
||||
});
|
||||
|
||||
flushPendingEvents();
|
||||
|
||||
needsToFlushComponentLogs = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -341,6 +341,7 @@ export function getEventPriority(domEventName: DOMEventName): EventPriority {
|
||||
case 'pointerup':
|
||||
case 'ratechange':
|
||||
case 'reset':
|
||||
case 'resize':
|
||||
case 'seeked':
|
||||
case 'submit':
|
||||
case 'toggle':
|
||||
@@ -379,7 +380,6 @@ export function getEventPriority(domEventName: DOMEventName): EventPriority {
|
||||
case 'pointermove':
|
||||
case 'pointerout':
|
||||
case 'pointerover':
|
||||
case 'resize':
|
||||
case 'scroll':
|
||||
case 'touchmove':
|
||||
case 'wheel':
|
||||
|
||||
@@ -40,6 +40,7 @@ describe('created with ReactFabric called with ReactNative', () => {
|
||||
require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface').getNativeTagFromPublicInstance;
|
||||
});
|
||||
|
||||
// @gate !disableLegacyMode
|
||||
it('find Fabric instances with the RN renderer', () => {
|
||||
const View = createReactNativeComponentClass('RCTView', () => ({
|
||||
validAttributes: {title: true},
|
||||
@@ -60,6 +61,7 @@ describe('created with ReactFabric called with ReactNative', () => {
|
||||
expect(getNativeTagFromPublicInstance(instance)).toBe(2);
|
||||
});
|
||||
|
||||
// @gate !disableLegacyMode
|
||||
it('find Fabric nodes with the RN renderer', () => {
|
||||
const View = createReactNativeComponentClass('RCTView', () => ({
|
||||
validAttributes: {title: true},
|
||||
@@ -80,6 +82,7 @@ describe('created with ReactFabric called with ReactNative', () => {
|
||||
expect(handle).toBe(2);
|
||||
});
|
||||
|
||||
// @gate !disableLegacyMode
|
||||
it('dispatches commands on Fabric nodes with the RN renderer', () => {
|
||||
nativeFabricUIManager.dispatchCommand.mockClear();
|
||||
const View = createReactNativeComponentClass('RCTView', () => ({
|
||||
@@ -101,6 +104,7 @@ describe('created with ReactFabric called with ReactNative', () => {
|
||||
expect(UIManager.dispatchViewManagerCommand).not.toBeCalled();
|
||||
});
|
||||
|
||||
// @gate !disableLegacyMode
|
||||
it('dispatches sendAccessibilityEvent on Fabric nodes with the RN renderer', () => {
|
||||
nativeFabricUIManager.sendAccessibilityEvent.mockClear();
|
||||
const View = createReactNativeComponentClass('RCTView', () => ({
|
||||
@@ -143,6 +147,7 @@ describe('created with ReactNative called with ReactFabric', () => {
|
||||
.ReactNativeViewConfigRegistry.register;
|
||||
});
|
||||
|
||||
// @gate !disableLegacyMode
|
||||
it('find Paper instances with the Fabric renderer', () => {
|
||||
const View = createReactNativeComponentClass('RCTView', () => ({
|
||||
validAttributes: {title: true},
|
||||
@@ -163,6 +168,7 @@ describe('created with ReactNative called with ReactFabric', () => {
|
||||
expect(instance._nativeTag).toBe(3);
|
||||
});
|
||||
|
||||
// @gate !disableLegacyMode
|
||||
it('find Paper nodes with the Fabric renderer', () => {
|
||||
const View = createReactNativeComponentClass('RCTView', () => ({
|
||||
validAttributes: {title: true},
|
||||
@@ -183,6 +189,7 @@ describe('created with ReactNative called with ReactFabric', () => {
|
||||
expect(handle).toBe(3);
|
||||
});
|
||||
|
||||
// @gate !disableLegacyMode
|
||||
it('dispatches commands on Paper nodes with the Fabric renderer', () => {
|
||||
UIManager.dispatchViewManagerCommand.mockReset();
|
||||
const View = createReactNativeComponentClass('RCTView', () => ({
|
||||
@@ -205,6 +212,7 @@ describe('created with ReactNative called with ReactFabric', () => {
|
||||
expect(nativeFabricUIManager.dispatchCommand).not.toBeCalled();
|
||||
});
|
||||
|
||||
// @gate !disableLegacyMode
|
||||
it('dispatches sendAccessibilityEvent on Paper nodes with the Fabric renderer', () => {
|
||||
ReactNativePrivateInterface.legacySendAccessibilityEvent.mockReset();
|
||||
const View = createReactNativeComponentClass('RCTView', () => ({
|
||||
|
||||
@@ -5,11 +5,18 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
* @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// Polyfills for test environment
|
||||
global.ReadableStream =
|
||||
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
|
||||
global.WritableStream =
|
||||
require('web-streams-polyfill/ponyfill/es6').WritableStream;
|
||||
global.TextEncoder = require('util').TextEncoder;
|
||||
global.TextDecoder = require('util').TextDecoder;
|
||||
|
||||
let clientExports;
|
||||
let turbopackMap;
|
||||
let turbopackModules;
|
||||
|
||||
@@ -5,11 +5,17 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
* @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// Polyfills for test environment
|
||||
global.ReadableStream =
|
||||
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
|
||||
global.TextEncoder = require('util').TextEncoder;
|
||||
global.TextDecoder = require('util').TextDecoder;
|
||||
|
||||
// let serverExports;
|
||||
let turbopackServerMap;
|
||||
let ReactServerDOMServer;
|
||||
let ReactServerDOMClient;
|
||||
@@ -23,6 +29,7 @@ describe('ReactFlightDOMTurbopackReply', () => {
|
||||
require('react-server-dom-turbopack/server.edge'),
|
||||
);
|
||||
const TurbopackMock = require('./utils/TurbopackMock');
|
||||
// serverExports = TurbopackMock.serverExports;
|
||||
turbopackServerMap = TurbopackMock.turbopackServerMap;
|
||||
ReactServerDOMServer = require('react-server-dom-turbopack/server.edge');
|
||||
jest.resetModules();
|
||||
|
||||
@@ -10,6 +10,18 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
// Polyfills for test environment
|
||||
global.ReadableStream =
|
||||
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
|
||||
global.WritableStream =
|
||||
require('web-streams-polyfill/ponyfill/es6').WritableStream;
|
||||
global.TextEncoder = require('util').TextEncoder;
|
||||
global.TextDecoder = require('util').TextDecoder;
|
||||
global.Blob = require('buffer').Blob;
|
||||
if (typeof File === 'undefined' || typeof FormData === 'undefined') {
|
||||
global.File = require('buffer').File || require('undici').File;
|
||||
global.FormData = require('undici').FormData;
|
||||
}
|
||||
// Patch for Edge environments for global scope
|
||||
global.AsyncLocalStorage = require('async_hooks').AsyncLocalStorage;
|
||||
|
||||
@@ -115,16 +127,8 @@ describe('ReactFlightDOMEdge', () => {
|
||||
chunk.set(prevChunk, 0);
|
||||
chunk.set(value, prevChunk.length);
|
||||
if (chunk.length > 50) {
|
||||
// Copy the part we're keeping (prevChunk) to avoid buffer
|
||||
// transfer. When we enqueue the partial chunk below, downstream
|
||||
// consumers (like byte streams in the Flight Client) may detach
|
||||
// the underlying buffer. Since prevChunk would share the same
|
||||
// buffer, we copy it first so it has its own independent buffer.
|
||||
// TODO: Should we just use {type: 'bytes'} for this stream to
|
||||
// always transfer ownership, and not only "accidentally" when we
|
||||
// enqueue in the Flight Client?
|
||||
prevChunk = chunk.slice(chunk.length - 50);
|
||||
controller.enqueue(chunk.subarray(0, chunk.length - 50));
|
||||
prevChunk = chunk.subarray(chunk.length - 50);
|
||||
} else {
|
||||
// Wait to see if we get some more bytes to join in.
|
||||
prevChunk = chunk;
|
||||
@@ -1114,121 +1118,25 @@ describe('ReactFlightDOMEdge', () => {
|
||||
expect(streamedBuffers).toEqual(buffers);
|
||||
});
|
||||
|
||||
it('should support binary ReadableStreams', async () => {
|
||||
const encoder = new TextEncoder();
|
||||
const words = ['Hello', 'streaming', 'world'];
|
||||
|
||||
const stream = new ReadableStream({
|
||||
type: 'bytes',
|
||||
async start(controller) {
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
const chunk = encoder.encode(words[i] + ' ');
|
||||
controller.enqueue(chunk);
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
const rscStream = await serverAct(() =>
|
||||
ReactServerDOMServer.renderToReadableStream(stream, {}),
|
||||
);
|
||||
|
||||
const result = await ReactServerDOMClient.createFromReadableStream(
|
||||
rscStream,
|
||||
{
|
||||
serverConsumerManifest: {
|
||||
moduleMap: null,
|
||||
moduleLoading: null,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const reader = result.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
let text = '';
|
||||
let entry;
|
||||
while (!(entry = await reader.read()).done) {
|
||||
text += decoder.decode(entry.value);
|
||||
}
|
||||
|
||||
expect(text).toBe('Hello streaming world ');
|
||||
});
|
||||
|
||||
it('should support large binary ReadableStreams', async () => {
|
||||
const chunkCount = 100;
|
||||
const chunkSize = 1024;
|
||||
const expectedBytes = [];
|
||||
|
||||
const stream = new ReadableStream({
|
||||
type: 'bytes',
|
||||
start(controller) {
|
||||
for (let i = 0; i < chunkCount; i++) {
|
||||
const chunk = new Uint8Array(chunkSize);
|
||||
for (let j = 0; j < chunkSize; j++) {
|
||||
chunk[j] = (i + j) % 256;
|
||||
}
|
||||
expectedBytes.push(...Array.from(chunk));
|
||||
controller.enqueue(chunk);
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
const rscStream = await serverAct(() =>
|
||||
ReactServerDOMServer.renderToReadableStream(stream, {}),
|
||||
);
|
||||
|
||||
const result = await ReactServerDOMClient.createFromReadableStream(
|
||||
// Use passThrough to split and rejoin chunks at arbitrary boundaries.
|
||||
passThrough(rscStream),
|
||||
{
|
||||
serverConsumerManifest: {
|
||||
moduleMap: null,
|
||||
moduleLoading: null,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const reader = result.getReader();
|
||||
const receivedBytes = [];
|
||||
let entry;
|
||||
while (!(entry = await reader.read()).done) {
|
||||
expect(entry.value instanceof Uint8Array).toBe(true);
|
||||
receivedBytes.push(...Array.from(entry.value));
|
||||
}
|
||||
|
||||
expect(receivedBytes).toEqual(expectedBytes);
|
||||
});
|
||||
|
||||
it('should support BYOB binary ReadableStreams', async () => {
|
||||
const sourceBytes = [
|
||||
const buffer = new Uint8Array([
|
||||
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
|
||||
];
|
||||
|
||||
// Create separate buffers for each typed array to avoid ArrayBuffer
|
||||
// transfer issues. Each view needs its own buffer because enqueue()
|
||||
// transfers ownership.
|
||||
]).buffer;
|
||||
const buffers = [
|
||||
new Int8Array(sourceBytes.slice(1)),
|
||||
new Uint8Array(sourceBytes.slice(2)),
|
||||
new Uint8ClampedArray(sourceBytes.slice(2)),
|
||||
new Int16Array(new Uint8Array(sourceBytes.slice(2)).buffer),
|
||||
new Uint16Array(new Uint8Array(sourceBytes.slice(2)).buffer),
|
||||
new Int32Array(new Uint8Array(sourceBytes.slice(4)).buffer),
|
||||
new Uint32Array(new Uint8Array(sourceBytes.slice(4)).buffer),
|
||||
new Float32Array(new Uint8Array(sourceBytes.slice(4)).buffer),
|
||||
new Float64Array(new Uint8Array(sourceBytes.slice(0)).buffer),
|
||||
new BigInt64Array(new Uint8Array(sourceBytes.slice(0)).buffer),
|
||||
new BigUint64Array(new Uint8Array(sourceBytes.slice(0)).buffer),
|
||||
new DataView(new Uint8Array(sourceBytes.slice(3)).buffer),
|
||||
new Int8Array(buffer, 1),
|
||||
new Uint8Array(buffer, 2),
|
||||
new Uint8ClampedArray(buffer, 2),
|
||||
new Int16Array(buffer, 2),
|
||||
new Uint16Array(buffer, 2),
|
||||
new Int32Array(buffer, 4),
|
||||
new Uint32Array(buffer, 4),
|
||||
new Float32Array(buffer, 4),
|
||||
new Float64Array(buffer, 0),
|
||||
new BigInt64Array(buffer, 0),
|
||||
new BigUint64Array(buffer, 0),
|
||||
new DataView(buffer, 3),
|
||||
];
|
||||
|
||||
// Save expected bytes before enqueueing (which will detach the buffers).
|
||||
const expectedBytes = buffers.flatMap(c =>
|
||||
Array.from(new Uint8Array(c.buffer, c.byteOffset, c.byteLength)),
|
||||
);
|
||||
|
||||
// This a binary stream where each chunk ends up as Uint8Array.
|
||||
const s = new ReadableStream({
|
||||
type: 'bytes',
|
||||
@@ -1268,7 +1176,11 @@ describe('ReactFlightDOMEdge', () => {
|
||||
|
||||
// The streamed buffers might be in different chunks and in Uint8Array form but
|
||||
// the concatenated bytes should be the same.
|
||||
expect(streamedBuffers.flatMap(t => Array.from(t))).toEqual(expectedBytes);
|
||||
expect(streamedBuffers.flatMap(t => Array.from(t))).toEqual(
|
||||
buffers.flatMap(c =>
|
||||
Array.from(new Uint8Array(c.buffer, c.byteOffset, c.byteLength)),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// @gate !__DEV__ || enableComponentPerformanceTrack
|
||||
|
||||
@@ -10,6 +10,18 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
// Polyfills for test environment
|
||||
global.ReadableStream =
|
||||
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
|
||||
global.TextEncoder = require('util').TextEncoder;
|
||||
global.TextDecoder = require('util').TextDecoder;
|
||||
|
||||
global.Blob = require('buffer').Blob;
|
||||
if (typeof File === 'undefined' || typeof FormData === 'undefined') {
|
||||
global.File = require('buffer').File || require('undici').File;
|
||||
global.FormData = require('undici').FormData;
|
||||
}
|
||||
|
||||
let serverExports;
|
||||
let webpackServerMap;
|
||||
let ReactServerDOMServer;
|
||||
@@ -182,33 +194,24 @@ describe('ReactFlightDOMReplyEdge', () => {
|
||||
});
|
||||
|
||||
it('should support BYOB binary ReadableStreams', async () => {
|
||||
const sourceBytes = [
|
||||
const buffer = new Uint8Array([
|
||||
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
|
||||
];
|
||||
|
||||
// Create separate buffers for each typed array to avoid ArrayBuffer
|
||||
// transfer issues. Each view needs its own buffer because enqueue()
|
||||
// transfers ownership.
|
||||
]).buffer;
|
||||
const buffers = [
|
||||
new Int8Array(sourceBytes.slice(1)),
|
||||
new Uint8Array(sourceBytes.slice(2)),
|
||||
new Uint8ClampedArray(sourceBytes.slice(2)),
|
||||
new Int16Array(new Uint8Array(sourceBytes.slice(2)).buffer),
|
||||
new Uint16Array(new Uint8Array(sourceBytes.slice(2)).buffer),
|
||||
new Int32Array(new Uint8Array(sourceBytes.slice(4)).buffer),
|
||||
new Uint32Array(new Uint8Array(sourceBytes.slice(4)).buffer),
|
||||
new Float32Array(new Uint8Array(sourceBytes.slice(4)).buffer),
|
||||
new Float64Array(new Uint8Array(sourceBytes.slice(0)).buffer),
|
||||
new BigInt64Array(new Uint8Array(sourceBytes.slice(0)).buffer),
|
||||
new BigUint64Array(new Uint8Array(sourceBytes.slice(0)).buffer),
|
||||
new DataView(new Uint8Array(sourceBytes.slice(3)).buffer),
|
||||
new Int8Array(buffer, 1),
|
||||
new Uint8Array(buffer, 2),
|
||||
new Uint8ClampedArray(buffer, 2),
|
||||
new Int16Array(buffer, 2),
|
||||
new Uint16Array(buffer, 2),
|
||||
new Int32Array(buffer, 4),
|
||||
new Uint32Array(buffer, 4),
|
||||
new Float32Array(buffer, 4),
|
||||
new Float64Array(buffer, 0),
|
||||
new BigInt64Array(buffer, 0),
|
||||
new BigUint64Array(buffer, 0),
|
||||
new DataView(buffer, 3),
|
||||
];
|
||||
|
||||
// Save expected bytes before enqueueing (which will detach the buffers).
|
||||
const expectedBytes = buffers.flatMap(c =>
|
||||
Array.from(new Uint8Array(c.buffer, c.byteOffset, c.byteLength)),
|
||||
);
|
||||
|
||||
// This a binary stream where each chunk ends up as Uint8Array.
|
||||
const s = new ReadableStream({
|
||||
type: 'bytes',
|
||||
@@ -236,7 +239,11 @@ describe('ReactFlightDOMReplyEdge', () => {
|
||||
|
||||
// The streamed buffers might be in different chunks and in Uint8Array form but
|
||||
// the concatenated bytes should be the same.
|
||||
expect(streamedBuffers.flatMap(t => Array.from(t))).toEqual(expectedBytes);
|
||||
expect(streamedBuffers.flatMap(t => Array.from(t))).toEqual(
|
||||
buffers.flatMap(c =>
|
||||
Array.from(new Uint8Array(c.buffer, c.byteOffset, c.byteLength)),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should abort when parsing an incomplete payload', async () => {
|
||||
|
||||
14
packages/react-server/src/ReactFlightServer.js
vendored
14
packages/react-server/src/ReactFlightServer.js
vendored
@@ -1149,8 +1149,6 @@ function serializeReadableStream(
|
||||
supportsBYOB = false;
|
||||
}
|
||||
}
|
||||
// At this point supportsBYOB is guaranteed to be a boolean.
|
||||
const isByteStream: boolean = supportsBYOB;
|
||||
|
||||
const reader = stream.getReader();
|
||||
|
||||
@@ -1174,7 +1172,7 @@ function serializeReadableStream(
|
||||
// The task represents the Stop row. This adds a Start row.
|
||||
request.pendingChunks++;
|
||||
const startStreamRow =
|
||||
streamTask.id.toString(16) + ':' + (isByteStream ? 'r' : 'R') + '\n';
|
||||
streamTask.id.toString(16) + ':' + (supportsBYOB ? 'r' : 'R') + '\n';
|
||||
request.completedRegularChunks.push(stringToChunk(startStreamRow));
|
||||
|
||||
function progress(entry: {done: boolean, value: ReactClientValue, ...}) {
|
||||
@@ -1192,15 +1190,9 @@ function serializeReadableStream(
|
||||
callOnAllReadyIfReady(request);
|
||||
} else {
|
||||
try {
|
||||
request.pendingChunks++;
|
||||
streamTask.model = entry.value;
|
||||
if (isByteStream) {
|
||||
// Chunks of byte streams are always Uint8Array instances.
|
||||
const chunk: Uint8Array = (streamTask.model: any);
|
||||
emitTypedArrayChunk(request, streamTask.id, 'b', chunk, false);
|
||||
} else {
|
||||
tryStreamTask(request, streamTask);
|
||||
}
|
||||
request.pendingChunks++;
|
||||
tryStreamTask(request, streamTask);
|
||||
enqueueFlush(request);
|
||||
reader.read().then(progress, error);
|
||||
} catch (x) {
|
||||
|
||||
@@ -36,7 +36,7 @@ export const disableCommentsAsDOMContainers: boolean = true;
|
||||
export const disableInputAttributeSyncing: boolean = false;
|
||||
export const disableLegacyContext: boolean = false;
|
||||
export const disableLegacyContextForFunctionComponents: boolean = false;
|
||||
export const disableLegacyMode: boolean = false;
|
||||
export const disableLegacyMode: boolean = true;
|
||||
export const disableSchedulerTimeoutInWorkLoop: boolean = false;
|
||||
export const disableTextareaChildren: boolean = false;
|
||||
export const enableAsyncDebugInfo: boolean = false;
|
||||
|
||||
@@ -21,7 +21,7 @@ export const disableCommentsAsDOMContainers: boolean = true;
|
||||
export const disableInputAttributeSyncing: boolean = false;
|
||||
export const disableLegacyContext: boolean = true;
|
||||
export const disableLegacyContextForFunctionComponents: boolean = true;
|
||||
export const disableLegacyMode: boolean = false;
|
||||
export const disableLegacyMode: boolean = true;
|
||||
export const disableSchedulerTimeoutInWorkLoop: boolean = false;
|
||||
export const disableTextareaChildren: boolean = false;
|
||||
export const enableAsyncDebugInfo: boolean = false;
|
||||
|
||||
Reference in New Issue
Block a user