Compare commits
7 Commits
mcp-sync-r
...
pr33626
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5eda822159 | ||
|
|
2de9ddac73 | ||
|
|
6ef9e5bd49 | ||
|
|
440d8c2876 | ||
|
|
cee7939b00 | ||
|
|
b42341ddc7 | ||
|
|
7a3ffef703 |
@@ -285,7 +285,7 @@ function runWithEnvironment(
|
||||
}
|
||||
|
||||
if (env.config.validateRefAccessDuringRender) {
|
||||
validateNoRefAccessInRender(hir).unwrap();
|
||||
// validateNoRefAccessInRender(hir).unwrap();
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInRender) {
|
||||
|
||||
@@ -221,7 +221,6 @@ export function lower(
|
||||
params,
|
||||
fnType: bindings == null ? env.fnType : 'Other',
|
||||
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
|
||||
returnType: makeType(),
|
||||
returns: createTemporaryPlace(env, func.node.loc ?? GeneratedSource),
|
||||
body: builder.build(),
|
||||
context,
|
||||
|
||||
@@ -279,7 +279,6 @@ export type HIRFunction = {
|
||||
env: Environment;
|
||||
params: Array<Place | SpreadPattern>;
|
||||
returnTypeAnnotation: t.FlowType | t.TSType | null;
|
||||
returnType: Type;
|
||||
returns: Place;
|
||||
context: Array<Place>;
|
||||
effects: Array<FunctionEffect> | null;
|
||||
|
||||
@@ -54,6 +54,8 @@ export function printFunction(fn: HIRFunction): string {
|
||||
let definition = '';
|
||||
if (fn.id !== null) {
|
||||
definition += fn.id;
|
||||
} else {
|
||||
definition += '<<anonymous>>';
|
||||
}
|
||||
if (fn.params.length !== 0) {
|
||||
definition +=
|
||||
@@ -71,10 +73,8 @@ export function printFunction(fn: HIRFunction): string {
|
||||
} else {
|
||||
definition += '()';
|
||||
}
|
||||
if (definition.length !== 0) {
|
||||
output.push(definition);
|
||||
}
|
||||
output.push(`: ${printType(fn.returnType)} @ ${printPlace(fn.returns)}`);
|
||||
definition += `: ${printPlace(fn.returns)}`;
|
||||
output.push(definition);
|
||||
output.push(...fn.directives);
|
||||
output.push(printHIR(fn.body));
|
||||
return output.join('\n');
|
||||
|
||||
@@ -28,7 +28,9 @@ import {
|
||||
isMapType,
|
||||
isPrimitiveType,
|
||||
isRefOrRefValue,
|
||||
isRefValueType,
|
||||
isSetType,
|
||||
isUseRefType,
|
||||
makeIdentifierId,
|
||||
Phi,
|
||||
Place,
|
||||
@@ -219,6 +221,9 @@ export function inferMutationAliasingEffects(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fn.env.config.validateRefAccessDuringRender) {
|
||||
inferRefAccessEffects(fn, isFunctionExpression);
|
||||
}
|
||||
return Ok(undefined);
|
||||
}
|
||||
|
||||
@@ -2513,3 +2518,127 @@ export type AbstractValue = {
|
||||
kind: ValueKind;
|
||||
reason: ReadonlySet<ValueReason>;
|
||||
};
|
||||
|
||||
function inferRefAccessEffects(
|
||||
fn: HIRFunction,
|
||||
_isFunctionExpression: boolean,
|
||||
): void {
|
||||
const nullish = new Set<IdentifierId>();
|
||||
const nullishTest = new Map<IdentifierId, Place>();
|
||||
let guard: {ref: IdentifierId; fallthrough: BlockId} | null = null;
|
||||
const temporaries: Map<IdentifierId, Place> = new Map();
|
||||
|
||||
function visitOperand(operand: Place): AliasingEffect | null {
|
||||
const nullTestRef = nullishTest.get(operand.identifier.id);
|
||||
if (isRefValueType(operand.identifier) || nullTestRef != null) {
|
||||
const refOperand = nullTestRef ?? operand;
|
||||
return {
|
||||
kind: 'Impure',
|
||||
error: {
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason:
|
||||
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
|
||||
loc: refOperand.loc,
|
||||
description:
|
||||
refOperand.identifier.name !== null &&
|
||||
refOperand.identifier.name.kind === 'named'
|
||||
? `Cannot access ref value \`${refOperand.identifier.name.value}\``
|
||||
: null,
|
||||
suggestions: null,
|
||||
},
|
||||
place: refOperand,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
if (guard !== null && guard.fallthrough === block.id) {
|
||||
guard = null;
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
if (value.kind === 'LoadLocal' && isUseRefType(value.place.identifier)) {
|
||||
temporaries.set(lvalue.identifier.id, value.place);
|
||||
} else if (
|
||||
value.kind === 'StoreLocal' &&
|
||||
isUseRefType(value.value.identifier)
|
||||
) {
|
||||
temporaries.set(value.lvalue.place.identifier.id, value.value);
|
||||
temporaries.set(lvalue.identifier.id, value.value);
|
||||
} else if (
|
||||
value.kind === 'BinaryExpression' &&
|
||||
((isRefValueType(value.left.identifier) &&
|
||||
nullish.has(value.right.identifier.id)) ||
|
||||
(nullish.has(value.left.identifier.id) &&
|
||||
isRefValueType(value.right.identifier)))
|
||||
) {
|
||||
const refOperand = isRefValueType(value.left.identifier)
|
||||
? value.left
|
||||
: value.right;
|
||||
const operand = temporaries.get(refOperand.identifier.id) ?? refOperand;
|
||||
nullishTest.set(lvalue.identifier.id, operand);
|
||||
} else if (value.kind === 'Primitive' && value.value == null) {
|
||||
nullish.add(lvalue.identifier.id);
|
||||
} else if (
|
||||
value.kind === 'PropertyLoad' &&
|
||||
isUseRefType(value.object.identifier) &&
|
||||
value.property === 'current'
|
||||
) {
|
||||
const refOperand =
|
||||
temporaries.get(value.object.identifier.id) ?? value.object;
|
||||
temporaries.set(lvalue.identifier.id, refOperand);
|
||||
} else if (
|
||||
value.kind === 'PropertyStore' &&
|
||||
value.property === 'current' &&
|
||||
isUseRefType(value.object.identifier)
|
||||
) {
|
||||
const refOperand =
|
||||
temporaries.get(value.object.identifier.id) ?? value.object;
|
||||
if (guard != null && refOperand.identifier.id === guard.ref) {
|
||||
// Allow a single write within the guard
|
||||
guard = null;
|
||||
} else {
|
||||
instr.effects ??= [];
|
||||
instr.effects.push({
|
||||
kind: 'Impure',
|
||||
error: {
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason:
|
||||
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
|
||||
loc: value.loc,
|
||||
description:
|
||||
value.object.identifier.name !== null &&
|
||||
value.object.identifier.name.kind === 'named'
|
||||
? `Cannot access ref value \`${value.object.identifier.name.value}\``
|
||||
: null,
|
||||
suggestions: null,
|
||||
},
|
||||
place: value.object,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
const error = visitOperand(operand);
|
||||
if (error) {
|
||||
instr.effects ??= [];
|
||||
instr.effects.push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
guard == null &&
|
||||
block.terminal.kind === 'if' &&
|
||||
nullishTest.has(block.terminal.test.identifier.id)
|
||||
) {
|
||||
const ref = nullishTest.get(block.terminal.test.identifier.id)!;
|
||||
guard = {ref: ref.identifier.id, fallthrough: block.terminal.fallthrough};
|
||||
} else {
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
const _effect = visitOperand(operand);
|
||||
// TODO: need a place to store terminal effects generically
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
ValueKind,
|
||||
ValueReason,
|
||||
Place,
|
||||
isPrimitiveType,
|
||||
} from '../HIR/HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
@@ -471,15 +472,15 @@ export function inferMutationAliasingRanges(
|
||||
* Here we populate an effect to create the return value as well as populating alias/capture
|
||||
* effects for how data flows between the params, context vars, and return.
|
||||
*/
|
||||
const returns = fn.returns.identifier;
|
||||
functionEffects.push({
|
||||
kind: 'Create',
|
||||
into: fn.returns,
|
||||
value:
|
||||
fn.returnType.kind === 'Primitive'
|
||||
? ValueKind.Primitive
|
||||
: isJsxType(fn.returnType)
|
||||
? ValueKind.Frozen
|
||||
: ValueKind.Mutable,
|
||||
value: isPrimitiveType(returns)
|
||||
? ValueKind.Primitive
|
||||
: isJsxType(returns.type)
|
||||
? ValueKind.Frozen
|
||||
: ValueKind.Mutable,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
});
|
||||
/**
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
makeBlockId,
|
||||
makeInstructionId,
|
||||
makePropertyLiteral,
|
||||
makeType,
|
||||
markInstructionIds,
|
||||
promoteTemporary,
|
||||
reversePostorderBlocks,
|
||||
@@ -253,7 +252,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
env,
|
||||
params: [obj],
|
||||
returnTypeAnnotation: null,
|
||||
returnType: makeType(),
|
||||
returns: createTemporaryPlace(env, GeneratedSource),
|
||||
context: [],
|
||||
effects: null,
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
makeBlockId,
|
||||
makeIdentifierName,
|
||||
makeInstructionId,
|
||||
makeType,
|
||||
ObjectProperty,
|
||||
Place,
|
||||
promoteTemporary,
|
||||
@@ -368,7 +367,6 @@ function emitOutlinedFn(
|
||||
env,
|
||||
params: [propsObj],
|
||||
returnTypeAnnotation: null,
|
||||
returnType: makeType(),
|
||||
returns: createTemporaryPlace(env, GeneratedSource),
|
||||
context: [],
|
||||
effects: null,
|
||||
|
||||
@@ -349,11 +349,9 @@ function codegenReactiveFunction(
|
||||
fn: ReactiveFunction,
|
||||
): Result<CodegenFunction, CompilerError> {
|
||||
for (const param of fn.params) {
|
||||
if (param.kind === 'Identifier') {
|
||||
cx.temp.set(param.identifier.declarationId, null);
|
||||
} else {
|
||||
cx.temp.set(param.place.identifier.declarationId, null);
|
||||
}
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
cx.temp.set(place.identifier.declarationId, null);
|
||||
cx.declare(place.identifier);
|
||||
}
|
||||
|
||||
const params = fn.params.map(param => convertParameter(param));
|
||||
@@ -1183,7 +1181,7 @@ function codegenTerminal(
|
||||
? codegenPlaceToExpression(cx, case_.test)
|
||||
: null;
|
||||
const block = codegenBlock(cx, case_.block!);
|
||||
return t.switchCase(test, [block]);
|
||||
return t.switchCase(test, block.body.length === 0 ? [] : [block]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,6 +79,10 @@ export function extractScopeDeclarationsFromDestructuring(
|
||||
fn: ReactiveFunction,
|
||||
): void {
|
||||
const state = new State(fn.env);
|
||||
for (const param of fn.params) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
state.declared.add(place.identifier.declarationId);
|
||||
}
|
||||
visitReactiveFunction(fn, new Visitor(), state);
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,8 @@ function apply(func: HIRFunction, unifier: Unifier): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
func.returnType = unifier.get(func.returnType);
|
||||
const returns = func.returns.identifier;
|
||||
returns.type = unifier.get(returns.type);
|
||||
}
|
||||
|
||||
type TypeEquation = {
|
||||
@@ -143,12 +144,12 @@ function* generate(
|
||||
}
|
||||
}
|
||||
if (returnTypes.length > 1) {
|
||||
yield equation(func.returnType, {
|
||||
yield equation(func.returns.identifier.type, {
|
||||
kind: 'Phi',
|
||||
operands: returnTypes,
|
||||
});
|
||||
} else if (returnTypes.length === 1) {
|
||||
yield equation(func.returnType, returnTypes[0]!);
|
||||
yield equation(func.returns.identifier.type, returnTypes[0]!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,7 +408,7 @@ function* generateInstructionTypes(
|
||||
yield equation(left, {
|
||||
kind: 'Function',
|
||||
shapeId: BuiltInFunctionId,
|
||||
return: value.loweredFunc.func.returnType,
|
||||
return: value.loweredFunc.func.returns.identifier.type,
|
||||
isConstructor: false,
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -50,8 +50,7 @@ function Component(props) {
|
||||
console.log(handlers.value);
|
||||
break bb0;
|
||||
}
|
||||
default: {
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
t0 = handlers;
|
||||
|
||||
@@ -67,8 +67,7 @@ function Component(props) {
|
||||
case "b": {
|
||||
break bb1;
|
||||
}
|
||||
case "c": {
|
||||
}
|
||||
case "c":
|
||||
default: {
|
||||
x = 6;
|
||||
}
|
||||
|
||||
@@ -24,8 +24,6 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
4 | const ref = useRef();
|
||||
> 5 | useEffect(() => {}, [ref.current]);
|
||||
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
|
||||
|
||||
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
|
||||
6 | }
|
||||
7 |
|
||||
8 | export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -19,6 +19,8 @@ function Component(props) {
|
||||
3 | const ref = useRef(null);
|
||||
> 4 | const value = ref.current;
|
||||
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4)
|
||||
|
||||
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `value` (5:5)
|
||||
5 | return value;
|
||||
6 | }
|
||||
7 |
|
||||
|
||||
@@ -19,12 +19,17 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
7 | return <Foo item={item} current={current} />;
|
||||
8 | };
|
||||
> 9 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (9:9)
|
||||
10 | }
|
||||
11 |
|
||||
4 | const renderItem = item => {
|
||||
5 | const aliasedRef = ref;
|
||||
> 6 | const current = aliasedRef.current;
|
||||
| ^^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (6:6)
|
||||
|
||||
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `current` (7:7)
|
||||
|
||||
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (7:7)
|
||||
7 | return <Foo item={item} current={current} />;
|
||||
8 | };
|
||||
9 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
|
||||
```
|
||||
|
||||
|
||||
@@ -21,15 +21,13 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
7 | };
|
||||
8 | const changeRef = setRef;
|
||||
> 9 | changeRef();
|
||||
| ^^^^^^^^^ InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (9:9)
|
||||
|
||||
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (9:9)
|
||||
10 |
|
||||
11 | return <button ref={ref} />;
|
||||
12 | }
|
||||
4 |
|
||||
5 | const setRef = () => {
|
||||
> 6 | ref.current = false;
|
||||
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (6:6)
|
||||
7 | };
|
||||
8 | const changeRef = setRef;
|
||||
9 | changeRef();
|
||||
```
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ function Component({ref}) {
|
||||
2 | function Component({ref}) {
|
||||
> 3 | const value = ref.current;
|
||||
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (3:3)
|
||||
|
||||
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `value` (4:4)
|
||||
|
||||
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4)
|
||||
4 | return <div>{value}</div>;
|
||||
5 | }
|
||||
6 |
|
||||
|
||||
@@ -18,6 +18,10 @@ function Component(props) {
|
||||
2 | function Component(props) {
|
||||
> 3 | const value = props.ref.current;
|
||||
| ^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (3:3)
|
||||
|
||||
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `value` (4:4)
|
||||
|
||||
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4)
|
||||
4 | return <div>{value}</div>;
|
||||
5 | }
|
||||
6 |
|
||||
|
||||
@@ -18,12 +18,17 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
6 | return <Foo item={item} current={current} />;
|
||||
7 | };
|
||||
> 8 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (8:8)
|
||||
9 | }
|
||||
10 |
|
||||
3 | const ref = useRef(null);
|
||||
4 | const renderItem = item => {
|
||||
> 5 | const current = ref.current;
|
||||
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
|
||||
|
||||
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `current` (6:6)
|
||||
|
||||
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (6:6)
|
||||
6 | return <Foo item={item} current={current} />;
|
||||
7 | };
|
||||
8 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
|
||||
```
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@ function Component(props) {
|
||||
3 | const ref = useRef(null);
|
||||
> 4 | ref.current = props.value;
|
||||
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4)
|
||||
|
||||
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
|
||||
5 | return ref.current;
|
||||
6 | }
|
||||
7 |
|
||||
|
||||
@@ -27,9 +27,9 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
4 | component C() {
|
||||
5 | const r = useRef(null);
|
||||
> 6 | const guard = r.current == null;
|
||||
| ^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (6:6)
|
||||
| ^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `r` (6:6)
|
||||
|
||||
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `guard` (7:7)
|
||||
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (8:8)
|
||||
7 | if (guard) {
|
||||
8 | r.current = 1;
|
||||
9 | }
|
||||
|
||||
@@ -34,15 +34,13 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
15 | ref.current.inner = null;
|
||||
13 | // The ref is modified later, extending its range and preventing memoization of onChange
|
||||
14 | const reset = () => {
|
||||
> 15 | ref.current.inner = null;
|
||||
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (15:15)
|
||||
16 | };
|
||||
> 17 | reset();
|
||||
| ^^^^^ InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (17:17)
|
||||
|
||||
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (17:17)
|
||||
17 | reset();
|
||||
18 |
|
||||
19 | return <input onChange={onChange} />;
|
||||
20 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateRefAccessDuringRender false
|
||||
// @validateRefAccessDuringRender:false
|
||||
function VideoTab() {
|
||||
const ref = useRef();
|
||||
const t = ref.current;
|
||||
@@ -18,7 +18,7 @@ function VideoTab() {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender false
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender:false
|
||||
function VideoTab() {
|
||||
const $ = _c(1);
|
||||
const ref = useRef();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateRefAccessDuringRender false
|
||||
// @validateRefAccessDuringRender:false
|
||||
function VideoTab() {
|
||||
const ref = useRef();
|
||||
const t = ref.current;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component({other, ...props}, ref) {
|
||||
[props, ref] = useIdentity([props, ref]);
|
||||
return <Stringify props={props} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 'hello', children: <div>Hello</div>}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { Stringify, useIdentity } from "shared-runtime";
|
||||
|
||||
function Component(t0, ref) {
|
||||
const $ = _c(7);
|
||||
let props;
|
||||
if ($[0] !== t0) {
|
||||
let { other, ...t1 } = t0;
|
||||
props = t1;
|
||||
$[0] = t0;
|
||||
$[1] = props;
|
||||
} else {
|
||||
props = $[1];
|
||||
}
|
||||
let t1;
|
||||
if ($[2] !== props || $[3] !== ref) {
|
||||
t1 = [props, ref];
|
||||
$[2] = props;
|
||||
$[3] = ref;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
[props, ref] = useIdentity(t1);
|
||||
let t2;
|
||||
if ($[5] !== props) {
|
||||
t2 = <Stringify props={props} />;
|
||||
$[5] = props;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: 0, b: "hello", children: <div>Hello</div> }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"props":{"a":0,"b":"hello","children":{"type":"div","key":null,"props":{"children":"Hello"},"_owner":"[[ cyclic ref *3 ]]","_store":{}}}}</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
import {Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component({other, ...props}, ref) {
|
||||
[props, ref] = useIdentity([props, ref]);
|
||||
return <Stringify props={props} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 'hello', children: <div>Hello</div>}],
|
||||
};
|
||||
@@ -50,10 +50,8 @@ function Component(props) {
|
||||
case 1: {
|
||||
break bb0;
|
||||
}
|
||||
case 2: {
|
||||
}
|
||||
default: {
|
||||
}
|
||||
case 2:
|
||||
default:
|
||||
}
|
||||
} else {
|
||||
if (props.cond2) {
|
||||
|
||||
@@ -41,8 +41,7 @@ function foo() {
|
||||
case 2: {
|
||||
break bb0;
|
||||
}
|
||||
default: {
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,22 +43,17 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```javascript
|
||||
function foo(x) {
|
||||
bb0: switch (x) {
|
||||
case 0: {
|
||||
}
|
||||
case 1: {
|
||||
}
|
||||
case 0:
|
||||
case 1:
|
||||
case 2: {
|
||||
break bb0;
|
||||
}
|
||||
case 3: {
|
||||
break bb0;
|
||||
}
|
||||
case 4: {
|
||||
}
|
||||
case 5: {
|
||||
}
|
||||
default: {
|
||||
}
|
||||
case 4:
|
||||
case 5:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import type {
|
||||
TouchedViewDataAtPoint,
|
||||
ViewConfig,
|
||||
} from './ReactNativeTypes';
|
||||
import {create, diff} from './ReactNativeAttributePayloadFabric';
|
||||
import {dispatchEvent} from './ReactFabricEventEmitter';
|
||||
import {
|
||||
NoEventPriority,
|
||||
@@ -35,6 +34,8 @@ import {
|
||||
deepFreezeAndThrowOnMutationInDev,
|
||||
createPublicInstance,
|
||||
createPublicTextInstance,
|
||||
createAttributePayload,
|
||||
diffAttributePayloads,
|
||||
type PublicInstance as ReactNativePublicInstance,
|
||||
type PublicTextInstance,
|
||||
type PublicRootInstance,
|
||||
@@ -190,7 +191,10 @@ export function createInstance(
|
||||
}
|
||||
}
|
||||
|
||||
const updatePayload = create(props, viewConfig.validAttributes);
|
||||
const updatePayload = createAttributePayload(
|
||||
props,
|
||||
viewConfig.validAttributes,
|
||||
);
|
||||
|
||||
const node = createNode(
|
||||
tag, // reactTag
|
||||
@@ -456,7 +460,11 @@ export function cloneInstance(
|
||||
newChildSet: ?ChildSet,
|
||||
): Instance {
|
||||
const viewConfig = instance.canonical.viewConfig;
|
||||
const updatePayload = diff(oldProps, newProps, viewConfig.validAttributes);
|
||||
const updatePayload = diffAttributePayloads(
|
||||
oldProps,
|
||||
newProps,
|
||||
viewConfig.validAttributes,
|
||||
);
|
||||
// TODO: If the event handlers have changed, we need to update the current props
|
||||
// in the commit phase but there is no host config hook to do it yet.
|
||||
// So instead we hack it by updating it in the render phase.
|
||||
@@ -505,7 +513,7 @@ export function cloneHiddenInstance(
|
||||
): Instance {
|
||||
const viewConfig = instance.canonical.viewConfig;
|
||||
const node = instance.node;
|
||||
const updatePayload = create(
|
||||
const updatePayload = createAttributePayload(
|
||||
{style: {display: 'none'}},
|
||||
viewConfig.validAttributes,
|
||||
);
|
||||
|
||||
@@ -1,514 +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.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
// Modules provided by RN:
|
||||
import {
|
||||
deepDiffer,
|
||||
flattenStyle,
|
||||
} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
|
||||
import isArray from 'shared/isArray';
|
||||
|
||||
import type {AttributeConfiguration} from './ReactNativeTypes';
|
||||
|
||||
const emptyObject = {};
|
||||
|
||||
/**
|
||||
* Create a payload that contains all the updates between two sets of props.
|
||||
*
|
||||
* These helpers are all encapsulated into a single module, because they use
|
||||
* mutation as a performance optimization which leads to subtle shared
|
||||
* dependencies between the code paths. To avoid this mutable state leaking
|
||||
* across modules, I've kept them isolated to this module.
|
||||
*/
|
||||
|
||||
type NestedNode = Array<NestedNode> | Object;
|
||||
|
||||
// Tracks removed keys
|
||||
let removedKeys: {[string]: boolean} | null = null;
|
||||
let removedKeyCount = 0;
|
||||
|
||||
const deepDifferOptions = {
|
||||
unsafelyIgnoreFunctions: true,
|
||||
};
|
||||
|
||||
function defaultDiffer(prevProp: mixed, nextProp: mixed): boolean {
|
||||
if (typeof nextProp !== 'object' || nextProp === null) {
|
||||
// Scalars have already been checked for equality
|
||||
return true;
|
||||
} else {
|
||||
// For objects and arrays, the default diffing algorithm is a deep compare
|
||||
return deepDiffer(prevProp, nextProp, deepDifferOptions);
|
||||
}
|
||||
}
|
||||
|
||||
function restoreDeletedValuesInNestedArray(
|
||||
updatePayload: Object,
|
||||
node: NestedNode,
|
||||
validAttributes: AttributeConfiguration,
|
||||
) {
|
||||
if (isArray(node)) {
|
||||
let i = node.length;
|
||||
while (i-- && removedKeyCount > 0) {
|
||||
restoreDeletedValuesInNestedArray(
|
||||
updatePayload,
|
||||
node[i],
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
} else if (node && removedKeyCount > 0) {
|
||||
const obj = node;
|
||||
for (const propKey in removedKeys) {
|
||||
// $FlowFixMe[incompatible-use] found when upgrading Flow
|
||||
if (!removedKeys[propKey]) {
|
||||
continue;
|
||||
}
|
||||
let nextProp = obj[propKey];
|
||||
if (nextProp === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const attributeConfig = validAttributes[propKey];
|
||||
if (!attributeConfig) {
|
||||
continue; // not a valid native prop
|
||||
}
|
||||
|
||||
if (typeof nextProp === 'function') {
|
||||
// $FlowFixMe[incompatible-type] found when upgrading Flow
|
||||
nextProp = true;
|
||||
}
|
||||
if (typeof nextProp === 'undefined') {
|
||||
// $FlowFixMe[incompatible-type] found when upgrading Flow
|
||||
nextProp = null;
|
||||
}
|
||||
|
||||
if (typeof attributeConfig !== 'object') {
|
||||
// case: !Object is the default case
|
||||
updatePayload[propKey] = nextProp;
|
||||
} else if (
|
||||
typeof attributeConfig.diff === 'function' ||
|
||||
typeof attributeConfig.process === 'function'
|
||||
) {
|
||||
// case: CustomAttributeConfiguration
|
||||
const nextValue =
|
||||
typeof attributeConfig.process === 'function'
|
||||
? attributeConfig.process(nextProp)
|
||||
: nextProp;
|
||||
updatePayload[propKey] = nextValue;
|
||||
}
|
||||
// $FlowFixMe[incompatible-use] found when upgrading Flow
|
||||
removedKeys[propKey] = false;
|
||||
removedKeyCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function diffNestedArrayProperty(
|
||||
updatePayload: null | Object,
|
||||
prevArray: Array<NestedNode>,
|
||||
nextArray: Array<NestedNode>,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
const minLength =
|
||||
prevArray.length < nextArray.length ? prevArray.length : nextArray.length;
|
||||
let i;
|
||||
for (i = 0; i < minLength; i++) {
|
||||
// Diff any items in the array in the forward direction. Repeated keys
|
||||
// will be overwritten by later values.
|
||||
updatePayload = diffNestedProperty(
|
||||
updatePayload,
|
||||
prevArray[i],
|
||||
nextArray[i],
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
for (; i < prevArray.length; i++) {
|
||||
// Clear out all remaining properties.
|
||||
updatePayload = clearNestedProperty(
|
||||
updatePayload,
|
||||
prevArray[i],
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
for (; i < nextArray.length; i++) {
|
||||
// Add all remaining properties
|
||||
const nextProp = nextArray[i];
|
||||
if (!nextProp) {
|
||||
continue;
|
||||
}
|
||||
updatePayload = addNestedProperty(updatePayload, nextProp, validAttributes);
|
||||
}
|
||||
return updatePayload;
|
||||
}
|
||||
|
||||
function diffNestedProperty(
|
||||
updatePayload: null | Object,
|
||||
prevProp: NestedNode,
|
||||
nextProp: NestedNode,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
if (!updatePayload && prevProp === nextProp) {
|
||||
// If no properties have been added, then we can bail out quickly on object
|
||||
// equality.
|
||||
return updatePayload;
|
||||
}
|
||||
|
||||
if (!prevProp || !nextProp) {
|
||||
if (nextProp) {
|
||||
return addNestedProperty(updatePayload, nextProp, validAttributes);
|
||||
}
|
||||
if (prevProp) {
|
||||
return clearNestedProperty(updatePayload, prevProp, validAttributes);
|
||||
}
|
||||
return updatePayload;
|
||||
}
|
||||
|
||||
if (!isArray(prevProp) && !isArray(nextProp)) {
|
||||
// Both are leaves, we can diff the leaves.
|
||||
return diffProperties(updatePayload, prevProp, nextProp, validAttributes);
|
||||
}
|
||||
|
||||
if (isArray(prevProp) && isArray(nextProp)) {
|
||||
// Both are arrays, we can diff the arrays.
|
||||
return diffNestedArrayProperty(
|
||||
updatePayload,
|
||||
prevProp,
|
||||
nextProp,
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
|
||||
if (isArray(prevProp)) {
|
||||
return diffProperties(
|
||||
updatePayload,
|
||||
flattenStyle(prevProp),
|
||||
nextProp,
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
|
||||
return diffProperties(
|
||||
updatePayload,
|
||||
prevProp,
|
||||
flattenStyle(nextProp),
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* clearNestedProperty takes a single set of props and valid attributes. It
|
||||
* adds a null sentinel to the updatePayload, for each prop key.
|
||||
*/
|
||||
function clearNestedProperty(
|
||||
updatePayload: null | Object,
|
||||
prevProp: NestedNode,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
if (!prevProp) {
|
||||
return updatePayload;
|
||||
}
|
||||
|
||||
if (!isArray(prevProp)) {
|
||||
// Add each property of the leaf.
|
||||
return clearProperties(updatePayload, prevProp, validAttributes);
|
||||
}
|
||||
|
||||
for (let i = 0; i < prevProp.length; i++) {
|
||||
// Add all the properties of the array.
|
||||
updatePayload = clearNestedProperty(
|
||||
updatePayload,
|
||||
prevProp[i],
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
return updatePayload;
|
||||
}
|
||||
|
||||
/**
|
||||
* diffProperties takes two sets of props and a set of valid attributes
|
||||
* and write to updatePayload the values that changed or were deleted.
|
||||
* If no updatePayload is provided, a new one is created and returned if
|
||||
* anything changed.
|
||||
*/
|
||||
function diffProperties(
|
||||
updatePayload: null | Object,
|
||||
prevProps: Object,
|
||||
nextProps: Object,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
let attributeConfig;
|
||||
let nextProp;
|
||||
let prevProp;
|
||||
|
||||
for (const propKey in nextProps) {
|
||||
attributeConfig = validAttributes[propKey];
|
||||
if (!attributeConfig) {
|
||||
continue; // not a valid native prop
|
||||
}
|
||||
|
||||
prevProp = prevProps[propKey];
|
||||
nextProp = nextProps[propKey];
|
||||
|
||||
if (typeof nextProp === 'function') {
|
||||
const attributeConfigHasProcess =
|
||||
typeof attributeConfig === 'object' &&
|
||||
typeof attributeConfig.process === 'function';
|
||||
if (!attributeConfigHasProcess) {
|
||||
// functions are converted to booleans as markers that the associated
|
||||
// events should be sent from native.
|
||||
nextProp = (true: any);
|
||||
// If nextProp is not a function, then don't bother changing prevProp
|
||||
// since nextProp will win and go into the updatePayload regardless.
|
||||
if (typeof prevProp === 'function') {
|
||||
prevProp = (true: any);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An explicit value of undefined is treated as a null because it overrides
|
||||
// any other preceding value.
|
||||
if (typeof nextProp === 'undefined') {
|
||||
nextProp = (null: any);
|
||||
if (typeof prevProp === 'undefined') {
|
||||
prevProp = (null: any);
|
||||
}
|
||||
}
|
||||
|
||||
if (removedKeys) {
|
||||
removedKeys[propKey] = false;
|
||||
}
|
||||
|
||||
if (updatePayload && updatePayload[propKey] !== undefined) {
|
||||
// Something else already triggered an update to this key because another
|
||||
// value diffed. Since we're now later in the nested arrays our value is
|
||||
// more important so we need to calculate it and override the existing
|
||||
// value. It doesn't matter if nothing changed, we'll set it anyway.
|
||||
|
||||
// Pattern match on: attributeConfig
|
||||
if (typeof attributeConfig !== 'object') {
|
||||
// case: !Object is the default case
|
||||
updatePayload[propKey] = nextProp;
|
||||
} else if (
|
||||
typeof attributeConfig.diff === 'function' ||
|
||||
typeof attributeConfig.process === 'function'
|
||||
) {
|
||||
// case: CustomAttributeConfiguration
|
||||
const nextValue =
|
||||
typeof attributeConfig.process === 'function'
|
||||
? attributeConfig.process(nextProp)
|
||||
: nextProp;
|
||||
updatePayload[propKey] = nextValue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (prevProp === nextProp) {
|
||||
continue; // nothing changed
|
||||
}
|
||||
|
||||
// Pattern match on: attributeConfig
|
||||
if (typeof attributeConfig !== 'object') {
|
||||
// case: !Object is the default case
|
||||
if (defaultDiffer(prevProp, nextProp)) {
|
||||
// a normal leaf has changed
|
||||
(updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
|
||||
propKey
|
||||
] = nextProp;
|
||||
}
|
||||
} else if (
|
||||
typeof attributeConfig.diff === 'function' ||
|
||||
typeof attributeConfig.process === 'function'
|
||||
) {
|
||||
// case: CustomAttributeConfiguration
|
||||
const shouldUpdate =
|
||||
prevProp === undefined ||
|
||||
(typeof attributeConfig.diff === 'function'
|
||||
? attributeConfig.diff(prevProp, nextProp)
|
||||
: defaultDiffer(prevProp, nextProp));
|
||||
if (shouldUpdate) {
|
||||
const nextValue =
|
||||
typeof attributeConfig.process === 'function'
|
||||
? // $FlowFixMe[incompatible-use] found when upgrading Flow
|
||||
attributeConfig.process(nextProp)
|
||||
: nextProp;
|
||||
(updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
|
||||
propKey
|
||||
] = nextValue;
|
||||
}
|
||||
} else {
|
||||
// default: fallthrough case when nested properties are defined
|
||||
removedKeys = null;
|
||||
removedKeyCount = 0;
|
||||
// We think that attributeConfig is not CustomAttributeConfiguration at
|
||||
// this point so we assume it must be AttributeConfiguration.
|
||||
updatePayload = diffNestedProperty(
|
||||
updatePayload,
|
||||
prevProp,
|
||||
nextProp,
|
||||
((attributeConfig: any): AttributeConfiguration),
|
||||
);
|
||||
if (removedKeyCount > 0 && updatePayload) {
|
||||
restoreDeletedValuesInNestedArray(
|
||||
updatePayload,
|
||||
nextProp,
|
||||
((attributeConfig: any): AttributeConfiguration),
|
||||
);
|
||||
removedKeys = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also iterate through all the previous props to catch any that have been
|
||||
// removed and make sure native gets the signal so it can reset them to the
|
||||
// default.
|
||||
for (const propKey in prevProps) {
|
||||
if (nextProps[propKey] !== undefined) {
|
||||
continue; // we've already covered this key in the previous pass
|
||||
}
|
||||
attributeConfig = validAttributes[propKey];
|
||||
if (!attributeConfig) {
|
||||
continue; // not a valid native prop
|
||||
}
|
||||
|
||||
if (updatePayload && updatePayload[propKey] !== undefined) {
|
||||
// This was already updated to a diff result earlier.
|
||||
continue;
|
||||
}
|
||||
|
||||
prevProp = prevProps[propKey];
|
||||
if (prevProp === undefined) {
|
||||
continue; // was already empty anyway
|
||||
}
|
||||
// Pattern match on: attributeConfig
|
||||
if (
|
||||
typeof attributeConfig !== 'object' ||
|
||||
typeof attributeConfig.diff === 'function' ||
|
||||
typeof attributeConfig.process === 'function'
|
||||
) {
|
||||
// case: CustomAttributeConfiguration | !Object
|
||||
// Flag the leaf property for removal by sending a sentinel.
|
||||
(updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
|
||||
propKey
|
||||
] = null;
|
||||
if (!removedKeys) {
|
||||
removedKeys = ({}: {[string]: boolean});
|
||||
}
|
||||
if (!removedKeys[propKey]) {
|
||||
removedKeys[propKey] = true;
|
||||
removedKeyCount++;
|
||||
}
|
||||
} else {
|
||||
// default:
|
||||
// This is a nested attribute configuration where all the properties
|
||||
// were removed so we need to go through and clear out all of them.
|
||||
updatePayload = clearNestedProperty(
|
||||
updatePayload,
|
||||
prevProp,
|
||||
((attributeConfig: any): AttributeConfiguration),
|
||||
);
|
||||
}
|
||||
}
|
||||
return updatePayload;
|
||||
}
|
||||
|
||||
function addNestedProperty(
|
||||
payload: null | Object,
|
||||
props: Object,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
// Flatten nested style props.
|
||||
if (isArray(props)) {
|
||||
for (let i = 0; i < props.length; i++) {
|
||||
payload = addNestedProperty(payload, props[i], validAttributes);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
for (const propKey in props) {
|
||||
const prop = props[propKey];
|
||||
|
||||
const attributeConfig = ((validAttributes[
|
||||
propKey
|
||||
]: any): AttributeConfiguration);
|
||||
|
||||
if (attributeConfig == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let newValue;
|
||||
|
||||
if (prop === undefined) {
|
||||
// Discard the prop if it was previously defined.
|
||||
if (payload && payload[propKey] !== undefined) {
|
||||
newValue = null;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else if (typeof attributeConfig === 'object') {
|
||||
if (typeof attributeConfig.process === 'function') {
|
||||
// An atomic prop with custom processing.
|
||||
newValue = attributeConfig.process(prop);
|
||||
} else if (typeof attributeConfig.diff === 'function') {
|
||||
// An atomic prop with custom diffing. We don't need to do diffing when adding props.
|
||||
newValue = prop;
|
||||
}
|
||||
} else {
|
||||
if (typeof prop === 'function') {
|
||||
// A function prop. It represents an event handler. Pass it to native as 'true'.
|
||||
newValue = true;
|
||||
} else {
|
||||
// An atomic prop. Doesn't need to be flattened.
|
||||
newValue = prop;
|
||||
}
|
||||
}
|
||||
|
||||
if (newValue !== undefined) {
|
||||
if (!payload) {
|
||||
payload = ({}: {[string]: $FlowFixMe});
|
||||
}
|
||||
payload[propKey] = newValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
payload = addNestedProperty(payload, prop, attributeConfig);
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* clearProperties clears all the previous props by adding a null sentinel
|
||||
* to the payload for each valid key.
|
||||
*/
|
||||
function clearProperties(
|
||||
updatePayload: null | Object,
|
||||
prevProps: Object,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
return diffProperties(updatePayload, prevProps, emptyObject, validAttributes);
|
||||
}
|
||||
|
||||
export function create(
|
||||
props: Object,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
return addNestedProperty(null, props, validAttributes);
|
||||
}
|
||||
|
||||
export function diff(
|
||||
prevProps: Object,
|
||||
nextProps: Object,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
return diffProperties(
|
||||
null, // updatePayload
|
||||
prevProps,
|
||||
nextProps,
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
@@ -34,15 +34,6 @@ export type AttributeType<T, V> =
|
||||
export type AnyAttributeType = AttributeType<$FlowFixMe, $FlowFixMe>;
|
||||
|
||||
export type AttributeConfiguration = $ReadOnly<{
|
||||
[propName: string]: AnyAttributeType,
|
||||
style: $ReadOnly<{
|
||||
[propName: string]: AnyAttributeType,
|
||||
...
|
||||
}>,
|
||||
...
|
||||
}>;
|
||||
|
||||
export type PartialAttributeConfiguration = $ReadOnly<{
|
||||
[propName: string]: AnyAttributeType,
|
||||
style?: $ReadOnly<{
|
||||
[propName: string]: AnyAttributeType,
|
||||
@@ -83,7 +74,7 @@ export type PartialViewConfig = $ReadOnly<{
|
||||
directEventTypes?: ViewConfig['directEventTypes'],
|
||||
supportsRawText?: boolean,
|
||||
uiViewClassName: string,
|
||||
validAttributes?: PartialAttributeConfiguration,
|
||||
validAttributes?: AttributeConfiguration,
|
||||
}>;
|
||||
|
||||
type InspectorDataProps = $ReadOnly<{
|
||||
|
||||
@@ -59,4 +59,10 @@ module.exports = {
|
||||
get createPublicRootInstance() {
|
||||
return require('./createPublicRootInstance').default;
|
||||
},
|
||||
get createAttributePayload() {
|
||||
return require('./createAttributePayload').default;
|
||||
},
|
||||
get diffAttributePayloads() {
|
||||
return require('./diffAttributePayloads').default;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow strict
|
||||
*/
|
||||
|
||||
import type {AttributeConfiguration} from '../../../../ReactNativeTypes';
|
||||
|
||||
export default function create(
|
||||
props: Object,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
const {children, ...propsToPass} = props;
|
||||
return propsToPass;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow strict
|
||||
*/
|
||||
|
||||
import type {AttributeConfiguration} from '../../../../ReactNativeTypes';
|
||||
|
||||
import deepDiffer from './deepDiffer';
|
||||
|
||||
export default function diff(
|
||||
prevProps: Object,
|
||||
nextProps: Object,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
const {children: _prevChildren, ...prevPropsPassed} = prevProps;
|
||||
const {children: _nextChildren, ...nextPropsToPass} = nextProps;
|
||||
return deepDiffer(prevPropsPassed, nextPropsToPass) ? nextPropsToPass : null;
|
||||
}
|
||||
@@ -184,6 +184,10 @@ describe('ReactFabric', () => {
|
||||
nativeFabricUIManager.cloneNodeWithNewChildrenAndProps,
|
||||
).not.toBeCalled();
|
||||
|
||||
jest
|
||||
.spyOn(ReactNativePrivateInterface, 'diffAttributePayloads')
|
||||
.mockReturnValue({bar: 'b'});
|
||||
|
||||
await act(() => {
|
||||
ReactFabric.render(
|
||||
<Text foo="a" bar="b">
|
||||
@@ -203,6 +207,9 @@ describe('ReactFabric', () => {
|
||||
RCTText {"foo":"a","bar":"b"}
|
||||
RCTRawText {"text":"1"}`);
|
||||
|
||||
jest
|
||||
.spyOn(ReactNativePrivateInterface, 'diffAttributePayloads')
|
||||
.mockReturnValue({foo: 'b'});
|
||||
await act(() => {
|
||||
ReactFabric.render(
|
||||
<Text foo="b" bar="b">
|
||||
@@ -612,7 +619,7 @@ describe('ReactFabric', () => {
|
||||
ReactFabric.render(<Component chars={before} />, 11, null, true);
|
||||
});
|
||||
expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(`11
|
||||
RCTView null
|
||||
RCTView {}
|
||||
RCTView {"title":"a"}
|
||||
RCTView {"title":"b"}
|
||||
RCTView {"title":"c"}
|
||||
@@ -638,7 +645,7 @@ describe('ReactFabric', () => {
|
||||
ReactFabric.render(<Component chars={after} />, 11, null, true);
|
||||
});
|
||||
expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(`11
|
||||
RCTView null
|
||||
RCTView {}
|
||||
RCTView {"title":"m"}
|
||||
RCTView {"title":"x"}
|
||||
RCTView {"title":"h"}
|
||||
@@ -700,8 +707,8 @@ describe('ReactFabric', () => {
|
||||
});
|
||||
expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(
|
||||
`11
|
||||
RCTView null
|
||||
RCTView null
|
||||
RCTView {}
|
||||
RCTView {}
|
||||
RCTView {"title":"a"}
|
||||
RCTView {"title":"b"}
|
||||
RCTView {"title":"c"}
|
||||
@@ -732,8 +739,8 @@ describe('ReactFabric', () => {
|
||||
});
|
||||
});
|
||||
expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(`11
|
||||
RCTView null
|
||||
RCTView null
|
||||
RCTView {}
|
||||
RCTView {}
|
||||
RCTView {"title":"m"}
|
||||
RCTView {"title":"x"}
|
||||
RCTView {"title":"h"}
|
||||
|
||||
@@ -1,480 +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.
|
||||
*
|
||||
* @jest-environment node
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const {diff, create} = require('../ReactNativeAttributePayloadFabric');
|
||||
|
||||
describe('ReactNativeAttributePayloadFabric.create', () => {
|
||||
it('should work with simple example', () => {
|
||||
expect(create({b: 2, c: 3}, {a: true, b: true})).toEqual({
|
||||
b: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with complex example', () => {
|
||||
const validAttributes = {
|
||||
style: {
|
||||
position: true,
|
||||
zIndex: true,
|
||||
flexGrow: true,
|
||||
flexShrink: true,
|
||||
flexDirection: true,
|
||||
overflow: true,
|
||||
backgroundColor: true,
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
create(
|
||||
{
|
||||
style: [
|
||||
{
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
flexDirection: 'row',
|
||||
overflow: 'scroll',
|
||||
},
|
||||
[
|
||||
{position: 'relative', zIndex: 2},
|
||||
{flexGrow: 0},
|
||||
{backgroundColor: 'red'},
|
||||
],
|
||||
],
|
||||
},
|
||||
validAttributes,
|
||||
),
|
||||
).toEqual({
|
||||
flexGrow: 0,
|
||||
flexShrink: 1,
|
||||
flexDirection: 'row',
|
||||
overflow: 'scroll',
|
||||
position: 'relative',
|
||||
zIndex: 2,
|
||||
backgroundColor: 'red',
|
||||
});
|
||||
});
|
||||
|
||||
it('should nullify previously defined style prop that is subsequently set to null or undefined', () => {
|
||||
expect(
|
||||
create({style: [{a: 0}, {a: undefined}]}, {style: {a: true}}),
|
||||
).toEqual({a: null});
|
||||
expect(create({style: [{a: 0}, {a: null}]}, {style: {a: true}})).toEqual({
|
||||
a: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should ignore non-style fields that are set to undefined', () => {
|
||||
expect(create({}, {a: true})).toEqual(null);
|
||||
expect(create({a: undefined}, {a: true})).toEqual(null);
|
||||
expect(create({a: undefined, b: undefined}, {a: true, b: true})).toEqual(
|
||||
null,
|
||||
);
|
||||
expect(
|
||||
create({a: undefined, b: undefined, c: 1}, {a: true, b: true}),
|
||||
).toEqual(null);
|
||||
expect(
|
||||
create({a: undefined, b: undefined, c: 1}, {a: true, b: true, c: true}),
|
||||
).toEqual({c: 1});
|
||||
expect(
|
||||
create({a: 1, b: undefined, c: 2}, {a: true, b: true, c: true}),
|
||||
).toEqual({a: 1, c: 2});
|
||||
});
|
||||
|
||||
it('should ignore invalid fields', () => {
|
||||
expect(create({b: 2}, {})).toEqual(null);
|
||||
});
|
||||
|
||||
it('should not use the diff attribute', () => {
|
||||
const diffA = jest.fn();
|
||||
expect(create({a: [2]}, {a: {diff: diffA}})).toEqual({a: [2]});
|
||||
expect(diffA).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should use the process attribute', () => {
|
||||
const processA = jest.fn(a => a + 1);
|
||||
expect(create({a: 2}, {a: {process: processA}})).toEqual({a: 3});
|
||||
expect(processA).toBeCalledWith(2);
|
||||
});
|
||||
|
||||
it('should use the process attribute for functions as well', () => {
|
||||
const process = x => x;
|
||||
const nextFunction = () => {};
|
||||
expect(create({a: nextFunction}, {a: {process}})).toEqual({
|
||||
a: nextFunction,
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with undefined styles', () => {
|
||||
expect(create({style: undefined}, {style: {b: true}})).toEqual(null);
|
||||
expect(create({style: {a: '#ffffff', b: 1}}, {style: {b: true}})).toEqual({
|
||||
b: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should flatten nested styles and predefined styles', () => {
|
||||
const validStyleAttribute = {someStyle: {foo: true, bar: true}};
|
||||
expect(
|
||||
create({someStyle: [{foo: 1}, {bar: 2}]}, validStyleAttribute),
|
||||
).toEqual({foo: 1, bar: 2});
|
||||
expect(create({}, validStyleAttribute)).toEqual(null);
|
||||
const barStyle = {
|
||||
bar: 3,
|
||||
};
|
||||
expect(
|
||||
create(
|
||||
{someStyle: [[{foo: 1}, {foo: 2}], barStyle]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: 2, bar: 3});
|
||||
});
|
||||
|
||||
it('should not flatten nested props if attribute config is a primitive or only has diff/process', () => {
|
||||
expect(create({a: {foo: 1, bar: 2}}, {a: true})).toEqual({
|
||||
a: {foo: 1, bar: 2},
|
||||
});
|
||||
expect(create({a: [{foo: 1}, {bar: 2}]}, {a: true})).toEqual({
|
||||
a: [{foo: 1}, {bar: 2}],
|
||||
});
|
||||
expect(create({a: {foo: 1, bar: 2}}, {a: {diff: a => a}})).toEqual({
|
||||
a: {foo: 1, bar: 2},
|
||||
});
|
||||
expect(
|
||||
create({a: [{foo: 1}, {bar: 2}]}, {a: {diff: a => a, process: a => a}}),
|
||||
).toEqual({a: [{foo: 1}, {bar: 2}]});
|
||||
});
|
||||
|
||||
it('handles attributes defined multiple times', () => {
|
||||
const validAttributes = {foo: true, style: {foo: true}};
|
||||
expect(create({foo: 4, style: {foo: 2}}, validAttributes)).toEqual({
|
||||
foo: 2,
|
||||
});
|
||||
expect(create({style: {foo: 2}}, validAttributes)).toEqual({
|
||||
foo: 2,
|
||||
});
|
||||
expect(create({style: {foo: 2}, foo: 4}, validAttributes)).toEqual({
|
||||
foo: 4,
|
||||
});
|
||||
expect(create({foo: 4, style: {foo: null}}, validAttributes)).toEqual({
|
||||
foo: null, // this should ideally be null.
|
||||
});
|
||||
expect(
|
||||
create({foo: 4, style: [{foo: null}, {foo: 5}]}, validAttributes),
|
||||
).toEqual({
|
||||
foo: 5,
|
||||
});
|
||||
});
|
||||
|
||||
// Function properties are just markers to native that events should be sent.
|
||||
it('should convert functions to booleans', () => {
|
||||
expect(
|
||||
create(
|
||||
{
|
||||
a: function () {
|
||||
return 9;
|
||||
},
|
||||
b: function () {
|
||||
return 3;
|
||||
},
|
||||
},
|
||||
{a: true, b: true},
|
||||
),
|
||||
).toEqual({a: true, b: true});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ReactNativeAttributePayloadFabric.diff', () => {
|
||||
it('should work with simple example', () => {
|
||||
expect(diff({a: 1, c: 3}, {b: 2, c: 3}, {a: true, b: true})).toEqual({
|
||||
a: null,
|
||||
b: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip fields that are equal', () => {
|
||||
expect(
|
||||
diff(
|
||||
{a: 1, b: 'two', c: true, d: false, e: undefined, f: 0},
|
||||
{a: 1, b: 'two', c: true, d: false, e: undefined, f: 0},
|
||||
{a: true, b: true, c: true, d: true, e: true, f: true},
|
||||
),
|
||||
).toEqual(null);
|
||||
});
|
||||
|
||||
it('should remove fields', () => {
|
||||
expect(diff({a: 1}, {}, {a: true})).toEqual({a: null});
|
||||
});
|
||||
|
||||
it('should remove fields that are set to undefined', () => {
|
||||
expect(diff({a: 1}, {a: undefined}, {a: true})).toEqual({a: null});
|
||||
});
|
||||
|
||||
it('should ignore invalid fields', () => {
|
||||
expect(diff({a: 1}, {b: 2}, {})).toEqual(null);
|
||||
});
|
||||
|
||||
it('should use the diff attribute', () => {
|
||||
const diffA = jest.fn((a, b) => true);
|
||||
const diffB = jest.fn((a, b) => false);
|
||||
expect(
|
||||
diff(
|
||||
{a: [1], b: [3]},
|
||||
{a: [2], b: [4]},
|
||||
{a: {diff: diffA}, b: {diff: diffB}},
|
||||
),
|
||||
).toEqual({a: [2]});
|
||||
expect(diffA).toBeCalledWith([1], [2]);
|
||||
expect(diffB).toBeCalledWith([3], [4]);
|
||||
});
|
||||
|
||||
it('should not use the diff attribute on addition/removal', () => {
|
||||
const diffA = jest.fn();
|
||||
const diffB = jest.fn();
|
||||
expect(
|
||||
diff({a: [1]}, {b: [2]}, {a: {diff: diffA}, b: {diff: diffB}}),
|
||||
).toEqual({a: null, b: [2]});
|
||||
expect(diffA).not.toBeCalled();
|
||||
expect(diffB).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should do deep diffs of Objects by default', () => {
|
||||
expect(
|
||||
diff(
|
||||
{a: [1], b: {k: [3, 4]}, c: {k: [4, 4]}},
|
||||
{a: [2], b: {k: [3, 4]}, c: {k: [4, 5]}},
|
||||
{a: true, b: true, c: true},
|
||||
),
|
||||
).toEqual({a: [2], c: {k: [4, 5]}});
|
||||
});
|
||||
|
||||
it('should work with undefined styles', () => {
|
||||
expect(
|
||||
diff(
|
||||
{style: {a: '#ffffff', b: 1}},
|
||||
{style: undefined},
|
||||
{style: {b: true}},
|
||||
),
|
||||
).toEqual({b: null});
|
||||
expect(
|
||||
diff(
|
||||
{style: undefined},
|
||||
{style: {a: '#ffffff', b: 1}},
|
||||
{style: {b: true}},
|
||||
),
|
||||
).toEqual({b: 1});
|
||||
expect(
|
||||
diff({style: undefined}, {style: undefined}, {style: {b: true}}),
|
||||
).toEqual(null);
|
||||
});
|
||||
|
||||
it('should work with empty styles', () => {
|
||||
expect(diff({a: 1, c: 3}, {}, {a: true, b: true})).toEqual({a: null});
|
||||
expect(diff({}, {a: 1, c: 3}, {a: true, b: true})).toEqual({a: 1});
|
||||
expect(diff({}, {}, {a: true, b: true})).toEqual(null);
|
||||
});
|
||||
|
||||
it('should flatten nested styles and predefined styles', () => {
|
||||
const validStyleAttribute = {someStyle: {foo: true, bar: true}};
|
||||
|
||||
expect(
|
||||
diff({}, {someStyle: [{foo: 1}, {bar: 2}]}, validStyleAttribute),
|
||||
).toEqual({foo: 1, bar: 2});
|
||||
|
||||
expect(
|
||||
diff({someStyle: [{foo: 1}, {bar: 2}]}, {}, validStyleAttribute),
|
||||
).toEqual({foo: null, bar: null});
|
||||
|
||||
const barStyle = {
|
||||
bar: 3,
|
||||
};
|
||||
|
||||
expect(
|
||||
diff(
|
||||
{},
|
||||
{someStyle: [[{foo: 1}, {foo: 2}], barStyle]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: 2, bar: 3});
|
||||
});
|
||||
|
||||
it('should reset a value to a previous if it is removed', () => {
|
||||
const validStyleAttribute = {someStyle: {foo: true, bar: true}};
|
||||
|
||||
expect(
|
||||
diff(
|
||||
{someStyle: [{foo: 1}, {foo: 3}]},
|
||||
{someStyle: [{foo: 1}, {bar: 2}]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: 1, bar: 2});
|
||||
});
|
||||
|
||||
it('should not clear removed props if they are still in another slot', () => {
|
||||
const validStyleAttribute = {someStyle: {foo: true, bar: true}};
|
||||
|
||||
expect(
|
||||
diff(
|
||||
{someStyle: [{}, {foo: 3, bar: 2}]},
|
||||
{someStyle: [{foo: 3}, {bar: 2}]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: 3}); // this should ideally be null. heuristic tradeoff.
|
||||
|
||||
expect(
|
||||
diff(
|
||||
{someStyle: [{}, {foo: 3, bar: 2}]},
|
||||
{someStyle: [{foo: 1, bar: 1}, {bar: 2}]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({bar: 2, foo: 1});
|
||||
});
|
||||
|
||||
it('should clear a prop if a later style is explicit null/undefined', () => {
|
||||
const validStyleAttribute = {someStyle: {foo: true, bar: true}};
|
||||
expect(
|
||||
diff(
|
||||
{someStyle: [{}, {foo: 3, bar: 2}]},
|
||||
{someStyle: [{foo: 1}, {bar: 2, foo: null}]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: null});
|
||||
|
||||
expect(
|
||||
diff(
|
||||
{someStyle: [{foo: 3}, {foo: null, bar: 2}]},
|
||||
{someStyle: [{foo: null}, {bar: 2}]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: null});
|
||||
|
||||
expect(
|
||||
diff(
|
||||
{someStyle: [{foo: 1}, {foo: null}]},
|
||||
{someStyle: [{foo: 2}, {foo: null}]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: null}); // this should ideally be null. heuristic.
|
||||
|
||||
// Test the same case with object equality because an early bailout doesn't
|
||||
// work in this case.
|
||||
const fooObj = {foo: 3};
|
||||
expect(
|
||||
diff(
|
||||
{someStyle: [{foo: 1}, fooObj]},
|
||||
{someStyle: [{foo: 2}, fooObj]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: 3}); // this should ideally be null. heuristic.
|
||||
|
||||
expect(
|
||||
diff(
|
||||
{someStyle: [{foo: 1}, {foo: 3}]},
|
||||
{someStyle: [{foo: 2}, {foo: undefined}]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: null}); // this should ideally be null. heuristic.
|
||||
});
|
||||
|
||||
it('handles attributes defined multiple times', () => {
|
||||
const validAttributes = {foo: true, style: {foo: true}};
|
||||
expect(diff({}, {foo: 4, style: {foo: 2}}, validAttributes)).toEqual({
|
||||
foo: 2,
|
||||
});
|
||||
expect(diff({foo: 4}, {style: {foo: 2}}, validAttributes)).toEqual({
|
||||
foo: 2,
|
||||
});
|
||||
expect(diff({style: {foo: 2}}, {foo: 4}, validAttributes)).toEqual({
|
||||
foo: 4,
|
||||
});
|
||||
});
|
||||
|
||||
// Function properties are just markers to native that events should be sent.
|
||||
it('should convert functions to booleans', () => {
|
||||
// Note that if the property changes from one function to another, we don't
|
||||
// need to send an update.
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
a: function () {
|
||||
return 1;
|
||||
},
|
||||
b: function () {
|
||||
return 2;
|
||||
},
|
||||
c: 3,
|
||||
},
|
||||
{
|
||||
b: function () {
|
||||
return 9;
|
||||
},
|
||||
c: function () {
|
||||
return 3;
|
||||
},
|
||||
},
|
||||
{a: true, b: true, c: true},
|
||||
),
|
||||
).toEqual({a: null, c: true});
|
||||
});
|
||||
|
||||
it('should skip changed functions', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
a: function () {
|
||||
return 1;
|
||||
},
|
||||
},
|
||||
{
|
||||
a: function () {
|
||||
return 9;
|
||||
},
|
||||
},
|
||||
{a: true},
|
||||
),
|
||||
).toEqual(null);
|
||||
});
|
||||
|
||||
it('should skip deeply-nested changed functions', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
wrapper: {
|
||||
a: function () {
|
||||
return 1;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
wrapper: {
|
||||
a: function () {
|
||||
return 9;
|
||||
},
|
||||
},
|
||||
},
|
||||
{wrapper: true},
|
||||
),
|
||||
).toEqual(null);
|
||||
});
|
||||
|
||||
it('should use the process function config when prop is a function', () => {
|
||||
const process = jest.fn(a => a);
|
||||
const nextFunction = function () {};
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
a: function () {},
|
||||
},
|
||||
{
|
||||
a: nextFunction,
|
||||
},
|
||||
{a: {process}},
|
||||
),
|
||||
).toEqual({a: nextFunction});
|
||||
expect(process).toBeCalled();
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,7 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactComponentInfo} from 'shared/ReactTypes';
|
||||
import type {ReactComponentInfo, ReactAsyncInfo} from 'shared/ReactTypes';
|
||||
import type {LazyComponent} from 'react/src/ReactLazy';
|
||||
|
||||
import {
|
||||
@@ -37,7 +37,8 @@ export type ComponentStackNode = {
|
||||
| string
|
||||
| Function
|
||||
| LazyComponent<any, any>
|
||||
| ReactComponentInfo,
|
||||
| ReactComponentInfo
|
||||
| ReactAsyncInfo,
|
||||
owner?: null | ReactComponentInfo | ComponentStackNode, // DEV only
|
||||
stack?: null | string | Error, // DEV only
|
||||
};
|
||||
|
||||
102
packages/react-server/src/ReactFizzServer.js
vendored
102
packages/react-server/src/ReactFizzServer.js
vendored
@@ -21,6 +21,7 @@ import type {
|
||||
ReactFormState,
|
||||
ReactComponentInfo,
|
||||
ReactDebugInfo,
|
||||
ReactAsyncInfo,
|
||||
ViewTransitionProps,
|
||||
ActivityProps,
|
||||
SuspenseProps,
|
||||
@@ -181,6 +182,7 @@ import {
|
||||
enableAsyncIterableChildren,
|
||||
enableViewTransition,
|
||||
enableFizzBlockingRender,
|
||||
enableAsyncDebugInfo,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import assign from 'shared/assign';
|
||||
@@ -985,6 +987,45 @@ function getStackFromNode(stackNode: ComponentStackNode): string {
|
||||
return getStackByComponentStackNode(stackNode);
|
||||
}
|
||||
|
||||
function pushHaltedAwaitOnComponentStack(
|
||||
task: Task,
|
||||
debugInfo: void | null | ReactDebugInfo,
|
||||
): void {
|
||||
if (!__DEV__) {
|
||||
// eslint-disable-next-line react-internal/prod-error-codes
|
||||
throw new Error(
|
||||
'pushHaltedAwaitOnComponentStack should never be called in production. This is a bug in React.',
|
||||
);
|
||||
}
|
||||
if (debugInfo != null) {
|
||||
for (let i = debugInfo.length - 1; i >= 0; i--) {
|
||||
const info = debugInfo[i];
|
||||
if (typeof info.name === 'string') {
|
||||
// This is a Server Component. Any awaits in previous Server Components already resolved.
|
||||
break;
|
||||
}
|
||||
if (typeof info.time === 'number') {
|
||||
// This had an end time. Any awaits before this must have already resolved.
|
||||
break;
|
||||
}
|
||||
if (info.awaited != null) {
|
||||
const asyncInfo: ReactAsyncInfo = (info: any);
|
||||
const bestStack =
|
||||
asyncInfo.debugStack == null ? asyncInfo.awaited : asyncInfo;
|
||||
if (bestStack.debugStack !== undefined) {
|
||||
task.componentStack = {
|
||||
parent: task.componentStack,
|
||||
type: asyncInfo,
|
||||
owner: bestStack.owner,
|
||||
stack: bestStack.debugStack,
|
||||
};
|
||||
task.debugTask = (bestStack.debugTask: any);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pushServerComponentStack(
|
||||
task: Task,
|
||||
debugInfo: void | null | ReactDebugInfo,
|
||||
@@ -4612,6 +4653,20 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
||||
}
|
||||
|
||||
const errorInfo = getThrownInfo(task.componentStack);
|
||||
if (__DEV__ && enableAsyncDebugInfo) {
|
||||
// If the task is not rendering, then this is an async abort. Conceptually it's as if
|
||||
// the abort happened inside the async gap. The abort reason's stack frame won't have that
|
||||
// on the stack so instead we use the owner stack and debug task of any halted async debug info.
|
||||
const node: any = task.node;
|
||||
if (node !== null && typeof node === 'object') {
|
||||
// Push a fake component stack frame that represents the await.
|
||||
pushHaltedAwaitOnComponentStack(task, node._debugInfo);
|
||||
if (task.thenableState !== null) {
|
||||
// TODO: If we were stalled inside use() of a Client Component then we should
|
||||
// rerender to get the stack trace from the use() call.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (boundary === null) {
|
||||
if (request.status !== CLOSING && request.status !== CLOSED) {
|
||||
@@ -4631,7 +4686,12 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
||||
if (trackedPostpones !== null && segment !== null) {
|
||||
// We are prerendering. We don't want to fatal when the shell postpones
|
||||
// we just need to mark it as postponed.
|
||||
logPostpone(request, postponeInstance.message, errorInfo, null);
|
||||
logPostpone(
|
||||
request,
|
||||
postponeInstance.message,
|
||||
errorInfo,
|
||||
task.debugTask,
|
||||
);
|
||||
trackPostpone(request, trackedPostpones, task, segment);
|
||||
finishedTask(request, null, task.row, segment);
|
||||
} else {
|
||||
@@ -4639,8 +4699,8 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
||||
'The render was aborted with postpone when the shell is incomplete. Reason: ' +
|
||||
postponeInstance.message,
|
||||
);
|
||||
logRecoverableError(request, fatal, errorInfo, null);
|
||||
fatalError(request, fatal, errorInfo, null);
|
||||
logRecoverableError(request, fatal, errorInfo, task.debugTask);
|
||||
fatalError(request, fatal, errorInfo, task.debugTask);
|
||||
}
|
||||
} else if (
|
||||
enableHalt &&
|
||||
@@ -4650,12 +4710,12 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
||||
const trackedPostpones = request.trackedPostpones;
|
||||
// We are aborting a prerender and must treat the shell as halted
|
||||
// We log the error but we still resolve the prerender
|
||||
logRecoverableError(request, error, errorInfo, null);
|
||||
logRecoverableError(request, error, errorInfo, task.debugTask);
|
||||
trackPostpone(request, trackedPostpones, task, segment);
|
||||
finishedTask(request, null, task.row, segment);
|
||||
} else {
|
||||
logRecoverableError(request, error, errorInfo, null);
|
||||
fatalError(request, error, errorInfo, null);
|
||||
logRecoverableError(request, error, errorInfo, task.debugTask);
|
||||
fatalError(request, error, errorInfo, task.debugTask);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
@@ -4672,7 +4732,12 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
||||
error.$$typeof === REACT_POSTPONE_TYPE
|
||||
) {
|
||||
const postponeInstance: Postpone = (error: any);
|
||||
logPostpone(request, postponeInstance.message, errorInfo, null);
|
||||
logPostpone(
|
||||
request,
|
||||
postponeInstance.message,
|
||||
errorInfo,
|
||||
task.debugTask,
|
||||
);
|
||||
// TODO: Figure out a better signal than a magic digest value.
|
||||
errorDigest = 'POSTPONE';
|
||||
} else {
|
||||
@@ -4710,11 +4775,16 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
||||
error.$$typeof === REACT_POSTPONE_TYPE
|
||||
) {
|
||||
const postponeInstance: Postpone = (error: any);
|
||||
logPostpone(request, postponeInstance.message, errorInfo, null);
|
||||
logPostpone(
|
||||
request,
|
||||
postponeInstance.message,
|
||||
errorInfo,
|
||||
task.debugTask,
|
||||
);
|
||||
} else {
|
||||
// We are aborting a prerender and must halt this boundary.
|
||||
// We treat this like other postpones during prerendering
|
||||
logRecoverableError(request, error, errorInfo, null);
|
||||
logRecoverableError(request, error, errorInfo, task.debugTask);
|
||||
}
|
||||
trackPostpone(request, trackedPostpones, task, segment);
|
||||
// If this boundary was still pending then we haven't already cancelled its fallbacks.
|
||||
@@ -4737,7 +4807,12 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
||||
error.$$typeof === REACT_POSTPONE_TYPE
|
||||
) {
|
||||
const postponeInstance: Postpone = (error: any);
|
||||
logPostpone(request, postponeInstance.message, errorInfo, null);
|
||||
logPostpone(
|
||||
request,
|
||||
postponeInstance.message,
|
||||
errorInfo,
|
||||
task.debugTask,
|
||||
);
|
||||
if (request.trackedPostpones !== null && segment !== null) {
|
||||
trackPostpone(request, request.trackedPostpones, task, segment);
|
||||
finishedTask(request, task.blockedBoundary, task.row, segment);
|
||||
@@ -4753,7 +4828,12 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
||||
// TODO: Figure out a better signal than a magic digest value.
|
||||
errorDigest = 'POSTPONE';
|
||||
} else {
|
||||
errorDigest = logRecoverableError(request, error, errorInfo, null);
|
||||
errorDigest = logRecoverableError(
|
||||
request,
|
||||
error,
|
||||
errorInfo,
|
||||
task.debugTask,
|
||||
);
|
||||
}
|
||||
boundary.status = CLIENT_RENDERED;
|
||||
encodeErrorForBoundary(boundary, errorDigest, error, errorInfo, true);
|
||||
|
||||
175
packages/react-server/src/ReactFlightServer.js
vendored
175
packages/react-server/src/ReactFlightServer.js
vendored
@@ -433,7 +433,6 @@ export type Request = {
|
||||
nextChunkId: number,
|
||||
pendingChunks: number,
|
||||
hints: Hints,
|
||||
abortListeners: Set<(reason: mixed) => void>,
|
||||
abortableTasks: Set<Task>,
|
||||
pingedTasks: Array<Task>,
|
||||
completedImportChunks: Array<Chunk>,
|
||||
@@ -547,7 +546,6 @@ function RequestInstance(
|
||||
this.nextChunkId = 0;
|
||||
this.pendingChunks = 0;
|
||||
this.hints = hints;
|
||||
this.abortListeners = new Set();
|
||||
this.abortableTasks = abortSet;
|
||||
this.pingedTasks = pingedTasks;
|
||||
this.completedImportChunks = ([]: Array<Chunk>);
|
||||
@@ -839,13 +837,11 @@ function serializeThenable(
|
||||
if (request.status === ABORTING) {
|
||||
// We can no longer accept any resolved values
|
||||
request.abortableTasks.delete(newTask);
|
||||
newTask.status = ABORTED;
|
||||
if (enableHalt && request.type === PRERENDER) {
|
||||
request.pendingChunks--;
|
||||
haltTask(newTask, request);
|
||||
} else {
|
||||
const errorId: number = (request.fatalError: any);
|
||||
const model = stringify(serializeByValueID(errorId));
|
||||
emitModelChunk(request, newTask.id, model);
|
||||
abortTask(newTask, request, errorId);
|
||||
}
|
||||
return newTask.id;
|
||||
}
|
||||
@@ -936,29 +932,26 @@ function serializeReadableStream(
|
||||
__DEV__ ? task.debugStack : null,
|
||||
__DEV__ ? task.debugTask : null,
|
||||
);
|
||||
request.abortableTasks.delete(streamTask);
|
||||
|
||||
request.pendingChunks++; // The task represents the Start row. This adds a Stop row.
|
||||
|
||||
// The task represents the Stop row. This adds a Start row.
|
||||
request.pendingChunks++;
|
||||
const startStreamRow =
|
||||
streamTask.id.toString(16) + ':' + (supportsBYOB ? 'r' : 'R') + '\n';
|
||||
request.completedRegularChunks.push(stringToChunk(startStreamRow));
|
||||
|
||||
// There's a race condition between when the stream is aborted and when the promise
|
||||
// resolves so we track whether we already aborted it to avoid writing twice.
|
||||
let aborted = false;
|
||||
function progress(entry: {done: boolean, value: ReactClientValue, ...}) {
|
||||
if (aborted) {
|
||||
if (streamTask.status !== PENDING) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.done) {
|
||||
streamTask.status = COMPLETED;
|
||||
const endStreamRow = streamTask.id.toString(16) + ':C\n';
|
||||
request.completedRegularChunks.push(stringToChunk(endStreamRow));
|
||||
request.abortableTasks.delete(streamTask);
|
||||
request.cacheController.signal.removeEventListener('abort', abortStream);
|
||||
enqueueFlush(request);
|
||||
request.abortListeners.delete(abortStream);
|
||||
callOnAllReadyIfReady(request);
|
||||
aborted = true;
|
||||
} else {
|
||||
try {
|
||||
streamTask.model = entry.value;
|
||||
@@ -972,26 +965,28 @@ function serializeReadableStream(
|
||||
}
|
||||
}
|
||||
function error(reason: mixed) {
|
||||
if (aborted) {
|
||||
if (streamTask.status !== PENDING) {
|
||||
return;
|
||||
}
|
||||
aborted = true;
|
||||
request.abortListeners.delete(abortStream);
|
||||
request.cacheController.signal.removeEventListener('abort', abortStream);
|
||||
erroredTask(request, streamTask, reason);
|
||||
enqueueFlush(request);
|
||||
|
||||
// $FlowFixMe should be able to pass mixed
|
||||
reader.cancel(reason).then(error, error);
|
||||
}
|
||||
function abortStream(reason: mixed) {
|
||||
if (aborted) {
|
||||
function abortStream() {
|
||||
if (streamTask.status !== PENDING) {
|
||||
return;
|
||||
}
|
||||
aborted = true;
|
||||
request.abortListeners.delete(abortStream);
|
||||
const signal = request.cacheController.signal;
|
||||
signal.removeEventListener('abort', abortStream);
|
||||
const reason = signal.reason;
|
||||
if (enableHalt && request.type === PRERENDER) {
|
||||
request.pendingChunks--;
|
||||
haltTask(streamTask, request);
|
||||
request.abortableTasks.delete(streamTask);
|
||||
} else {
|
||||
// TODO: Make this use abortTask() instead.
|
||||
erroredTask(request, streamTask, reason);
|
||||
enqueueFlush(request);
|
||||
}
|
||||
@@ -999,7 +994,7 @@ function serializeReadableStream(
|
||||
reader.cancel(reason).then(error, error);
|
||||
}
|
||||
|
||||
request.abortListeners.add(abortStream);
|
||||
request.cacheController.signal.addEventListener('abort', abortStream);
|
||||
reader.read().then(progress, error);
|
||||
return serializeByValueID(streamTask.id);
|
||||
}
|
||||
@@ -1028,10 +1023,9 @@ function serializeAsyncIterable(
|
||||
__DEV__ ? task.debugStack : null,
|
||||
__DEV__ ? task.debugTask : null,
|
||||
);
|
||||
request.abortableTasks.delete(streamTask);
|
||||
|
||||
request.pendingChunks++; // The task represents the Start row. This adds a Stop row.
|
||||
|
||||
// The task represents the Stop row. This adds a Start row.
|
||||
request.pendingChunks++;
|
||||
const startStreamRow =
|
||||
streamTask.id.toString(16) + ':' + (isIterator ? 'x' : 'X') + '\n';
|
||||
request.completedRegularChunks.push(stringToChunk(startStreamRow));
|
||||
@@ -1043,19 +1037,17 @@ function serializeAsyncIterable(
|
||||
}
|
||||
}
|
||||
|
||||
// There's a race condition between when the stream is aborted and when the promise
|
||||
// resolves so we track whether we already aborted it to avoid writing twice.
|
||||
let aborted = false;
|
||||
function progress(
|
||||
entry:
|
||||
| {done: false, +value: ReactClientValue, ...}
|
||||
| {done: true, +value: ReactClientValue, ...},
|
||||
) {
|
||||
if (aborted) {
|
||||
if (streamTask.status !== PENDING) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.done) {
|
||||
streamTask.status = COMPLETED;
|
||||
let endStreamRow;
|
||||
if (entry.value === undefined) {
|
||||
endStreamRow = streamTask.id.toString(16) + ':C\n';
|
||||
@@ -1075,10 +1067,13 @@ function serializeAsyncIterable(
|
||||
}
|
||||
}
|
||||
request.completedRegularChunks.push(stringToChunk(endStreamRow));
|
||||
request.abortableTasks.delete(streamTask);
|
||||
request.cacheController.signal.removeEventListener(
|
||||
'abort',
|
||||
abortIterable,
|
||||
);
|
||||
enqueueFlush(request);
|
||||
request.abortListeners.delete(abortIterable);
|
||||
callOnAllReadyIfReady(request);
|
||||
aborted = true;
|
||||
} else {
|
||||
try {
|
||||
streamTask.model = entry.value;
|
||||
@@ -1097,11 +1092,10 @@ function serializeAsyncIterable(
|
||||
}
|
||||
}
|
||||
function error(reason: mixed) {
|
||||
if (aborted) {
|
||||
if (streamTask.status !== PENDING) {
|
||||
return;
|
||||
}
|
||||
aborted = true;
|
||||
request.abortListeners.delete(abortIterable);
|
||||
request.cacheController.signal.removeEventListener('abort', abortIterable);
|
||||
erroredTask(request, streamTask, reason);
|
||||
enqueueFlush(request);
|
||||
if (typeof (iterator: any).throw === 'function') {
|
||||
@@ -1110,16 +1104,19 @@ function serializeAsyncIterable(
|
||||
iterator.throw(reason).then(error, error);
|
||||
}
|
||||
}
|
||||
function abortIterable(reason: mixed) {
|
||||
if (aborted) {
|
||||
function abortIterable() {
|
||||
if (streamTask.status !== PENDING) {
|
||||
return;
|
||||
}
|
||||
aborted = true;
|
||||
request.abortListeners.delete(abortIterable);
|
||||
const signal = request.cacheController.signal;
|
||||
signal.removeEventListener('abort', abortIterable);
|
||||
const reason = signal.reason;
|
||||
if (enableHalt && request.type === PRERENDER) {
|
||||
request.pendingChunks--;
|
||||
haltTask(streamTask, request);
|
||||
request.abortableTasks.delete(streamTask);
|
||||
} else {
|
||||
erroredTask(request, streamTask, reason);
|
||||
// TODO: Make this use abortTask() instead.
|
||||
erroredTask(request, streamTask, signal.reason);
|
||||
enqueueFlush(request);
|
||||
}
|
||||
if (typeof (iterator: any).throw === 'function') {
|
||||
@@ -1128,7 +1125,7 @@ function serializeAsyncIterable(
|
||||
iterator.throw(reason).then(error, error);
|
||||
}
|
||||
}
|
||||
request.abortListeners.add(abortIterable);
|
||||
request.cacheController.signal.addEventListener('abort', abortIterable);
|
||||
if (__DEV__) {
|
||||
callIteratorInDEV(iterator, progress, error);
|
||||
} else {
|
||||
@@ -2152,7 +2149,11 @@ function visitAsyncNode(
|
||||
owner: node.owner,
|
||||
stack: filterStackTrace(request, node.stack),
|
||||
});
|
||||
markOperationEndTime(request, task, endTime);
|
||||
// Mark the end time of the await. If we're aborting then we don't emit this
|
||||
// to signal that this never resolved inside this render.
|
||||
if (request.status !== ABORTING) {
|
||||
markOperationEndTime(request, task, endTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2213,7 +2214,12 @@ function emitAsyncSequence(
|
||||
}
|
||||
}
|
||||
emitDebugChunk(request, task.id, debugInfo);
|
||||
markOperationEndTime(request, task, awaitedNode.end);
|
||||
// Mark the end time of the await. If we're aborting then we don't emit this
|
||||
// to signal that this never resolved inside this render.
|
||||
if (request.status !== ABORTING) {
|
||||
// If we're currently aborting, then this never resolved into user space.
|
||||
markOperationEndTime(request, task, awaitedNode.end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2675,16 +2681,14 @@ function serializeBlob(request: Request, blob: Blob): string {
|
||||
|
||||
const reader = blob.stream().getReader();
|
||||
|
||||
let aborted = false;
|
||||
function progress(
|
||||
entry: {done: false, value: Uint8Array} | {done: true, value: void},
|
||||
): Promise<void> | void {
|
||||
if (aborted) {
|
||||
if (newTask.status !== PENDING) {
|
||||
return;
|
||||
}
|
||||
if (entry.done) {
|
||||
request.abortListeners.delete(abortBlob);
|
||||
aborted = true;
|
||||
request.cacheController.signal.removeEventListener('abort', abortBlob);
|
||||
pingTask(request, newTask);
|
||||
return;
|
||||
}
|
||||
@@ -2694,25 +2698,26 @@ function serializeBlob(request: Request, blob: Blob): string {
|
||||
return reader.read().then(progress).catch(error);
|
||||
}
|
||||
function error(reason: mixed) {
|
||||
if (aborted) {
|
||||
if (newTask.status !== PENDING) {
|
||||
return;
|
||||
}
|
||||
aborted = true;
|
||||
request.abortListeners.delete(abortBlob);
|
||||
request.cacheController.signal.removeEventListener('abort', abortBlob);
|
||||
erroredTask(request, newTask, reason);
|
||||
enqueueFlush(request);
|
||||
// $FlowFixMe should be able to pass mixed
|
||||
reader.cancel(reason).then(error, error);
|
||||
}
|
||||
function abortBlob(reason: mixed) {
|
||||
if (aborted) {
|
||||
function abortBlob() {
|
||||
if (newTask.status !== PENDING) {
|
||||
return;
|
||||
}
|
||||
aborted = true;
|
||||
request.abortListeners.delete(abortBlob);
|
||||
const signal = request.cacheController.signal;
|
||||
signal.removeEventListener('abort', abortBlob);
|
||||
const reason = signal.reason;
|
||||
if (enableHalt && request.type === PRERENDER) {
|
||||
request.pendingChunks--;
|
||||
haltTask(newTask, request);
|
||||
} else {
|
||||
// TODO: Make this use abortTask() instead.
|
||||
erroredTask(request, newTask, reason);
|
||||
enqueueFlush(request);
|
||||
}
|
||||
@@ -2720,7 +2725,7 @@ function serializeBlob(request: Request, blob: Blob): string {
|
||||
reader.cancel(reason).then(error, error);
|
||||
}
|
||||
|
||||
request.abortListeners.add(abortBlob);
|
||||
request.cacheController.signal.addEventListener('abort', abortBlob);
|
||||
|
||||
// $FlowFixMe[incompatible-call]
|
||||
reader.read().then(progress).catch(error);
|
||||
@@ -3914,6 +3919,13 @@ function serializeIONode(
|
||||
// The environment name may have changed from when the I/O was actually started.
|
||||
const env = (0, request.environmentName)();
|
||||
|
||||
const endTime =
|
||||
ioNode.tag === UNRESOLVED_PROMISE_NODE
|
||||
? // Mark the end time as now. It's arbitrary since it's not resolved but this
|
||||
// marks when we stopped trying.
|
||||
performance.now()
|
||||
: ioNode.end;
|
||||
|
||||
request.pendingChunks++;
|
||||
const id = request.nextChunkId++;
|
||||
emitIOInfoChunk(
|
||||
@@ -3921,7 +3933,7 @@ function serializeIONode(
|
||||
id,
|
||||
name,
|
||||
ioNode.start,
|
||||
ioNode.end,
|
||||
endTime,
|
||||
value,
|
||||
env,
|
||||
owner,
|
||||
@@ -4745,7 +4757,6 @@ function forwardDebugInfoFromAbortedTask(request: Request, task: Task): void {
|
||||
env: env,
|
||||
};
|
||||
emitDebugChunk(request, task.id, asyncInfo);
|
||||
markOperationEndTime(request, task, performance.now());
|
||||
} else {
|
||||
emitAsyncSequence(request, task, sequence, debugInfo, null, null);
|
||||
}
|
||||
@@ -5005,16 +5016,15 @@ function retryTask(request: Request, task: Task): void {
|
||||
} catch (thrownValue) {
|
||||
if (request.status === ABORTING) {
|
||||
request.abortableTasks.delete(task);
|
||||
task.status = ABORTED;
|
||||
task.status = PENDING;
|
||||
if (enableHalt && request.type === PRERENDER) {
|
||||
// When aborting a prerener with halt semantics we don't emit
|
||||
// anything into the slot for a task that aborts, it remains unresolved
|
||||
request.pendingChunks--;
|
||||
haltTask(task, request);
|
||||
} else {
|
||||
// Otherwise we emit an error chunk into the task slot.
|
||||
const errorId: number = (request.fatalError: any);
|
||||
const model = stringify(serializeByValueID(errorId));
|
||||
emitModelChunk(request, task.id, model);
|
||||
abortTask(task, request, errorId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -5257,8 +5267,9 @@ function enqueueFlush(request: Request): void {
|
||||
}
|
||||
|
||||
function callOnAllReadyIfReady(request: Request): void {
|
||||
if (request.abortableTasks.size === 0 && request.abortListeners.size === 0) {
|
||||
request.onAllReady();
|
||||
if (request.abortableTasks.size === 0) {
|
||||
const onAllReady = request.onAllReady;
|
||||
onAllReady();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5294,6 +5305,7 @@ export function abort(request: Request, reason: mixed): void {
|
||||
if (request.status <= OPEN) {
|
||||
request.status = ABORTING;
|
||||
request.cacheController.abort(reason);
|
||||
callOnAllReadyIfReady(request);
|
||||
}
|
||||
const abortableTasks = request.abortableTasks;
|
||||
if (abortableTasks.size > 0) {
|
||||
@@ -5345,37 +5357,6 @@ export function abort(request: Request, reason: mixed): void {
|
||||
callOnAllReadyIfReady(request);
|
||||
}
|
||||
}
|
||||
const abortListeners = request.abortListeners;
|
||||
if (abortListeners.size > 0) {
|
||||
let error;
|
||||
if (
|
||||
enablePostpone &&
|
||||
typeof reason === 'object' &&
|
||||
reason !== null &&
|
||||
(reason: any).$$typeof === REACT_POSTPONE_TYPE
|
||||
) {
|
||||
// We aborted with a Postpone but since we're passing this to an
|
||||
// external handler, passing this object would leak it outside React.
|
||||
// We create an alternative reason for it instead.
|
||||
error = new Error('The render was aborted due to being postponed.');
|
||||
} else {
|
||||
error =
|
||||
reason === undefined
|
||||
? new Error(
|
||||
'The render was aborted by the server without a reason.',
|
||||
)
|
||||
: typeof reason === 'object' &&
|
||||
reason !== null &&
|
||||
typeof reason.then === 'function'
|
||||
? new Error(
|
||||
'The render was aborted by the server with a promise.',
|
||||
)
|
||||
: reason;
|
||||
}
|
||||
abortListeners.forEach(callback => callback(error));
|
||||
abortListeners.clear();
|
||||
callOnAllReadyIfReady(request);
|
||||
}
|
||||
if (request.destination !== null) {
|
||||
flushCompletedChunks(request, request.destination);
|
||||
}
|
||||
|
||||
10
scripts/flow/react-native-host-hooks.js
vendored
10
scripts/flow/react-native-host-hooks.js
vendored
@@ -32,6 +32,7 @@ type __MeasureLayoutOnSuccessCallback = (
|
||||
type __ReactNativeBaseComponentViewConfig = any;
|
||||
type __ViewConfigGetter = any;
|
||||
type __ViewConfig = any;
|
||||
type __AttributeConfiguration = any;
|
||||
|
||||
// libdefs cannot actually import. This is supposed to be the type imported
|
||||
// from 'react-native-renderer/src/legacy-events/TopLevelEventTypes';
|
||||
@@ -203,6 +204,15 @@ declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'
|
||||
declare export function getInternalInstanceHandleFromPublicInstance(
|
||||
publicInstance: PublicInstance,
|
||||
): ?Object;
|
||||
declare export function createAttributePayload(
|
||||
props: Object,
|
||||
validAttributes: __AttributeConfiguration,
|
||||
): null | Object;
|
||||
declare export function diffAttributePayloads(
|
||||
prevProps: Object,
|
||||
nextProps: Object,
|
||||
validAttributes: __AttributeConfiguration,
|
||||
): null | Object;
|
||||
}
|
||||
|
||||
declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInitializeCore' {
|
||||
|
||||
Reference in New Issue
Block a user