mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge ef44cd1702 into sapling-pr-archive-mofeiZ
This commit is contained in:
@@ -3595,31 +3595,40 @@ function lowerAssignment(
|
||||
|
||||
let temporary;
|
||||
if (builder.isContextIdentifier(lvalue)) {
|
||||
if (kind !== InstructionKind.Reassign && !isHoistedIdentifier) {
|
||||
if (kind === InstructionKind.Const) {
|
||||
builder.errors.push({
|
||||
reason: `Expected \`const\` declaration not to be reassigned`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
loc: lvalue.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'DeclareContext',
|
||||
lvalue: {
|
||||
kind: InstructionKind.Let,
|
||||
place: {...place},
|
||||
},
|
||||
loc: place.loc,
|
||||
if (kind === InstructionKind.Const && !isHoistedIdentifier) {
|
||||
builder.errors.push({
|
||||
reason: `Expected \`const\` declaration not to be reassigned`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
loc: lvalue.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
temporary = lowerValueToTemporary(builder, {
|
||||
kind: 'StoreContext',
|
||||
lvalue: {place: {...place}, kind: InstructionKind.Reassign},
|
||||
value,
|
||||
loc,
|
||||
});
|
||||
if (
|
||||
kind !== InstructionKind.Const &&
|
||||
kind !== InstructionKind.Reassign &&
|
||||
kind !== InstructionKind.Let &&
|
||||
kind !== InstructionKind.Function
|
||||
) {
|
||||
builder.errors.push({
|
||||
reason: `Unexpected context variable kind`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
loc: lvalue.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
temporary = lowerValueToTemporary(builder, {
|
||||
kind: 'UnsupportedNode',
|
||||
node: lvalueNode,
|
||||
loc: lvalueNode.loc ?? GeneratedSource,
|
||||
});
|
||||
} else {
|
||||
temporary = lowerValueToTemporary(builder, {
|
||||
kind: 'StoreContext',
|
||||
lvalue: {place: {...place}, kind},
|
||||
value,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const typeAnnotation = lvalue.get('typeAnnotation');
|
||||
let type: t.FlowType | t.TSType | null;
|
||||
|
||||
@@ -746,6 +746,27 @@ export enum InstructionKind {
|
||||
Function = 'Function',
|
||||
}
|
||||
|
||||
export function convertHoistedLValueKind(
|
||||
kind: InstructionKind,
|
||||
): InstructionKind | null {
|
||||
switch (kind) {
|
||||
case InstructionKind.HoistedLet:
|
||||
return InstructionKind.Let;
|
||||
case InstructionKind.HoistedConst:
|
||||
return InstructionKind.Const;
|
||||
case InstructionKind.HoistedFunction:
|
||||
return InstructionKind.Function;
|
||||
case InstructionKind.Let:
|
||||
case InstructionKind.Const:
|
||||
case InstructionKind.Function:
|
||||
case InstructionKind.Reassign:
|
||||
case InstructionKind.Catch:
|
||||
return null;
|
||||
default:
|
||||
assertExhaustive(kind, 'Unexpected lvalue kind');
|
||||
}
|
||||
}
|
||||
|
||||
function _staticInvariantInstructionValueHasLocation(
|
||||
value: InstructionValue,
|
||||
): SourceLocation {
|
||||
@@ -880,8 +901,20 @@ export type InstructionValue =
|
||||
| StoreLocal
|
||||
| {
|
||||
kind: 'StoreContext';
|
||||
/**
|
||||
* StoreContext kinds:
|
||||
* Reassign: context variable reassignment in source
|
||||
* Const: const declaration + assignment in source
|
||||
* ('const' context vars are ones whose declarations are hoisted)
|
||||
* Let: let declaration + assignment in source
|
||||
* Function: function declaration in source (similar to `const`)
|
||||
*/
|
||||
lvalue: {
|
||||
kind: InstructionKind.Reassign;
|
||||
kind:
|
||||
| InstructionKind.Reassign
|
||||
| InstructionKind.Const
|
||||
| InstructionKind.Let
|
||||
| InstructionKind.Function;
|
||||
place: Place;
|
||||
};
|
||||
value: Place;
|
||||
|
||||
+38
-14
@@ -23,6 +23,7 @@ import {
|
||||
FunctionExpression,
|
||||
ObjectMethod,
|
||||
PropertyLiteral,
|
||||
convertHoistedLValueKind,
|
||||
} from './HIR';
|
||||
import {
|
||||
collectHoistablePropertyLoads,
|
||||
@@ -464,6 +465,9 @@ class Context {
|
||||
}
|
||||
this.#reassignments.set(identifier, decl);
|
||||
}
|
||||
hasDeclared(identifier: Identifier): boolean {
|
||||
return this.#declarations.has(identifier.declarationId);
|
||||
}
|
||||
|
||||
// Checks if identifier is a valid dependency in the current scope
|
||||
#checkValidDependency(maybeDependency: ReactiveScopeDependency): boolean {
|
||||
@@ -662,21 +666,21 @@ function handleInstruction(instr: Instruction, context: Context): void {
|
||||
});
|
||||
} else if (value.kind === 'DeclareLocal' || value.kind === 'DeclareContext') {
|
||||
/*
|
||||
* Some variables may be declared and never initialized. We need
|
||||
* to retain (and hoist) these declarations if they are included
|
||||
* in a reactive scope. One approach is to simply add all `DeclareLocal`s
|
||||
* as scope declarations.
|
||||
* Some variables may be declared and never initialized. We need to retain
|
||||
* (and hoist) these declarations if they are included in a reactive scope.
|
||||
* One approach is to simply add all `DeclareLocal`s as scope declarations.
|
||||
*
|
||||
* Context variables with hoisted declarations only become live after their
|
||||
* first assignment. We only declare real DeclareLocal / DeclareContext
|
||||
* instructions (not hoisted ones) to avoid generating dependencies on
|
||||
* hoisted declarations.
|
||||
*/
|
||||
|
||||
/*
|
||||
* We add context variable declarations here, not at `StoreContext`, since
|
||||
* context Store / Loads are modeled as reads and mutates to the underlying
|
||||
* variable reference (instead of through intermediate / inlined temporaries)
|
||||
*/
|
||||
context.declare(value.lvalue.place.identifier, {
|
||||
id,
|
||||
scope: context.currentScope,
|
||||
});
|
||||
if (convertHoistedLValueKind(value.lvalue.kind) === null) {
|
||||
context.declare(value.lvalue.place.identifier, {
|
||||
id,
|
||||
scope: context.currentScope,
|
||||
});
|
||||
}
|
||||
} else if (value.kind === 'Destructure') {
|
||||
context.visitOperand(value.value);
|
||||
for (const place of eachPatternOperand(value.lvalue.pattern)) {
|
||||
@@ -688,6 +692,26 @@ function handleInstruction(instr: Instruction, context: Context): void {
|
||||
scope: context.currentScope,
|
||||
});
|
||||
}
|
||||
} else if (value.kind === 'StoreContext') {
|
||||
/**
|
||||
* Some StoreContext variables have hoisted declarations. If we're storing
|
||||
* to a context variable that hasn't yet been declared, the StoreContext is
|
||||
* the declaration.
|
||||
* (see corresponding logic in PruneHoistedContext)
|
||||
*/
|
||||
if (
|
||||
!context.hasDeclared(value.lvalue.place.identifier) ||
|
||||
value.lvalue.kind !== InstructionKind.Reassign
|
||||
) {
|
||||
context.declare(value.lvalue.place.identifier, {
|
||||
id,
|
||||
scope: context.currentScope,
|
||||
});
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
context.visitOperand(operand);
|
||||
}
|
||||
} else {
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
context.visitOperand(operand);
|
||||
|
||||
+8
-2
@@ -176,9 +176,15 @@ export function inferMutableLifetimes(
|
||||
if (
|
||||
instr.value.kind === 'DeclareContext' ||
|
||||
(instr.value.kind === 'StoreContext' &&
|
||||
instr.value.lvalue.kind !== InstructionKind.Reassign)
|
||||
instr.value.lvalue.kind !== InstructionKind.Reassign &&
|
||||
!contextVariableDeclarationInstructions.has(
|
||||
instr.value.lvalue.place.identifier,
|
||||
))
|
||||
) {
|
||||
// Save declarations of context variables
|
||||
/**
|
||||
* Save declarations of context variables if they hasn't already been
|
||||
* declared (due to hoisted declarations).
|
||||
*/
|
||||
contextVariableDeclarationInstructions.set(
|
||||
instr.value.lvalue.place.identifier,
|
||||
instr.id,
|
||||
|
||||
+14
-2
@@ -394,9 +394,13 @@ class InferenceState {
|
||||
|
||||
freezeValues(values: Set<InstructionValue>, reason: Set<ValueReason>): void {
|
||||
for (const value of values) {
|
||||
if (value.kind === 'DeclareContext') {
|
||||
if (
|
||||
value.kind === 'DeclareContext' ||
|
||||
(value.kind === 'StoreContext' &&
|
||||
value.lvalue.kind === InstructionKind.Let)
|
||||
) {
|
||||
/**
|
||||
* Avoid freezing hoisted context declarations
|
||||
* Avoid freezing context variable declarations, hoisted or otherwise
|
||||
* function Component() {
|
||||
* const cb = useBar(() => foo(2)); // produces a hoisted context declaration
|
||||
* const foo = useFoo(); // reassigns to the context variable
|
||||
@@ -1591,6 +1595,14 @@ function inferBlock(
|
||||
);
|
||||
|
||||
const lvalue = instr.lvalue;
|
||||
if (instrValue.lvalue.kind !== InstructionKind.Reassign) {
|
||||
state.initialize(instrValue, {
|
||||
kind: ValueKind.Mutable,
|
||||
reason: new Set([ValueReason.Other]),
|
||||
context: new Set(),
|
||||
});
|
||||
state.define(instrValue.lvalue.place, instrValue);
|
||||
}
|
||||
state.alias(lvalue, instrValue.value);
|
||||
lvalue.effect = Effect.Store;
|
||||
continuation = {kind: 'funeffects'};
|
||||
|
||||
+16
@@ -998,6 +998,14 @@ function codegenTerminal(
|
||||
lval = codegenLValue(cx, iterableItem.value.lvalue.pattern);
|
||||
break;
|
||||
}
|
||||
case 'StoreContext': {
|
||||
CompilerError.throwTodo({
|
||||
reason: 'Support non-trivial for..in inits',
|
||||
description: null,
|
||||
loc: terminal.init.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
default:
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Expected a StoreLocal or Destructure to be assigned to the collection`,
|
||||
@@ -1090,6 +1098,14 @@ function codegenTerminal(
|
||||
lval = codegenLValue(cx, iterableItem.value.lvalue.pattern);
|
||||
break;
|
||||
}
|
||||
case 'StoreContext': {
|
||||
CompilerError.throwTodo({
|
||||
reason: 'Support non-trivial for..of inits',
|
||||
description: null,
|
||||
loc: terminal.init.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
default:
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Expected a StoreLocal or Destructure to be assigned to the collection`,
|
||||
|
||||
+125
-112
@@ -7,12 +7,19 @@
|
||||
|
||||
import {CompilerError} from '..';
|
||||
import {
|
||||
DeclarationId,
|
||||
convertHoistedLValueKind,
|
||||
Environment,
|
||||
IdentifierId,
|
||||
InstructionId,
|
||||
InstructionKind,
|
||||
Place,
|
||||
ReactiveFunction,
|
||||
ReactiveInstruction,
|
||||
ReactiveScope,
|
||||
ReactiveScopeBlock,
|
||||
ReactiveStatement,
|
||||
} from '../HIR';
|
||||
import {empty, Stack} from '../Utils/Stack';
|
||||
import {
|
||||
ReactiveFunctionTransform,
|
||||
Transformed,
|
||||
@@ -22,138 +29,144 @@ import {
|
||||
/*
|
||||
* Prunes DeclareContexts lowered for HoistedConsts, and transforms any references back to its
|
||||
* original instruction kind.
|
||||
*
|
||||
* Detects and bails out on context variables which are:
|
||||
* - function declarations, which are hoisted by JS engines to the nearest block scope
|
||||
* - referenced before they are defined (i.e. having a `DeclareContext HoistedConst`)
|
||||
* - declared
|
||||
*/
|
||||
export function pruneHoistedContexts(fn: ReactiveFunction): void {
|
||||
const hoistedIdentifiers: HoistedIdentifiers = new Map();
|
||||
visitReactiveFunction(fn, new Visitor(), hoistedIdentifiers);
|
||||
visitReactiveFunction(fn, new Visitor(), {
|
||||
activeScopes: empty(),
|
||||
exceptions: new Set(),
|
||||
uninitialized: new Map(),
|
||||
});
|
||||
}
|
||||
|
||||
const REWRITTEN_HOISTED_CONST: unique symbol = Symbol(
|
||||
'REWRITTEN_HOISTED_CONST',
|
||||
);
|
||||
const REWRITTEN_HOISTED_LET: unique symbol = Symbol('REWRITTEN_HOISTED_LET');
|
||||
type VisitorState = {
|
||||
activeScopes: Stack<Set<IdentifierId>>;
|
||||
exceptions: Set<Place>;
|
||||
uninitialized: Map<
|
||||
IdentifierId,
|
||||
| {
|
||||
kind: 'maybefunc';
|
||||
}
|
||||
| {
|
||||
kind: 'func';
|
||||
definition: Place | null;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
type HoistedIdentifiers = Map<
|
||||
DeclarationId,
|
||||
| InstructionKind
|
||||
| typeof REWRITTEN_HOISTED_CONST
|
||||
| typeof REWRITTEN_HOISTED_LET
|
||||
>;
|
||||
/**
|
||||
* Oh man what about declarations in nested scopes?? t.t
|
||||
* We *might* encounter the following.
|
||||
* scope @0
|
||||
*/
|
||||
class Visitor extends ReactiveFunctionTransform<VisitorState> {
|
||||
override visitScope(scope: ReactiveScopeBlock, state: VisitorState): void {
|
||||
state.activeScopes = state.activeScopes.push(
|
||||
new Set(scope.scope.declarations.keys()),
|
||||
);
|
||||
for (const decl of scope.scope.declarations.values()) {
|
||||
state.uninitialized.set(decl.identifier.id, {kind: 'maybefunc'});
|
||||
}
|
||||
this.traverseScope(scope, state);
|
||||
state.activeScopes.pop();
|
||||
|
||||
class Visitor extends ReactiveFunctionTransform<HoistedIdentifiers> {
|
||||
/**
|
||||
* References to hoisted functions are now "safe" as it has been assigned
|
||||
*/
|
||||
for (const id of scope.scope.declarations.keys()) {
|
||||
state.uninitialized.delete(id);
|
||||
}
|
||||
}
|
||||
override visitPlace(
|
||||
_id: InstructionId,
|
||||
place: Place,
|
||||
state: VisitorState,
|
||||
): void {
|
||||
const maybeHoistedFn = state.uninitialized.get(place.identifier.id);
|
||||
if (
|
||||
maybeHoistedFn?.kind === 'func' &&
|
||||
maybeHoistedFn.definition !== place
|
||||
) {
|
||||
CompilerError.throwTodo({
|
||||
reason: '[PruneHoistedContexts] Rewrite hoisted function references',
|
||||
loc: place.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
override transformInstruction(
|
||||
instruction: ReactiveInstruction,
|
||||
state: HoistedIdentifiers,
|
||||
state: VisitorState,
|
||||
): Transformed<ReactiveStatement> {
|
||||
this.visitInstruction(instruction, state);
|
||||
|
||||
/**
|
||||
* Remove hoisted declarations to preserve TDZ
|
||||
*/
|
||||
if (
|
||||
instruction.value.kind === 'DeclareContext' &&
|
||||
instruction.value.lvalue.kind === 'HoistedConst'
|
||||
) {
|
||||
state.set(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
InstructionKind.Const,
|
||||
if (instruction.value.kind === 'DeclareContext') {
|
||||
const maybeNonHoisted = convertHoistedLValueKind(
|
||||
instruction.value.lvalue.kind,
|
||||
);
|
||||
return {kind: 'remove'};
|
||||
}
|
||||
|
||||
if (
|
||||
instruction.value.kind === 'DeclareContext' &&
|
||||
instruction.value.lvalue.kind === 'HoistedLet'
|
||||
) {
|
||||
state.set(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
InstructionKind.Let,
|
||||
);
|
||||
return {kind: 'remove'};
|
||||
}
|
||||
|
||||
if (
|
||||
instruction.value.kind === 'DeclareContext' &&
|
||||
instruction.value.lvalue.kind === 'HoistedFunction'
|
||||
) {
|
||||
state.set(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
InstructionKind.Function,
|
||||
);
|
||||
return {kind: 'remove'};
|
||||
}
|
||||
|
||||
if (instruction.value.kind === 'StoreContext') {
|
||||
const kind = state.get(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
);
|
||||
if (kind != null) {
|
||||
CompilerError.invariant(kind !== REWRITTEN_HOISTED_CONST, {
|
||||
reason: 'Expected exactly one store to a hoisted const variable',
|
||||
loc: instruction.loc,
|
||||
});
|
||||
if (maybeNonHoisted != null) {
|
||||
if (
|
||||
kind === InstructionKind.Const ||
|
||||
kind === InstructionKind.Function
|
||||
maybeNonHoisted === InstructionKind.Function &&
|
||||
state.uninitialized.has(instruction.value.lvalue.place.identifier.id)
|
||||
) {
|
||||
state.set(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
REWRITTEN_HOISTED_CONST,
|
||||
);
|
||||
return {
|
||||
kind: 'replace',
|
||||
value: {
|
||||
kind: 'instruction',
|
||||
instruction: {
|
||||
...instruction,
|
||||
value: {
|
||||
...instruction.value,
|
||||
lvalue: {
|
||||
...instruction.value.lvalue,
|
||||
kind,
|
||||
},
|
||||
type: null,
|
||||
kind: 'StoreLocal',
|
||||
},
|
||||
},
|
||||
state.uninitialized.set(
|
||||
instruction.value.lvalue.place.identifier.id,
|
||||
{
|
||||
kind: 'func',
|
||||
definition: null,
|
||||
},
|
||||
};
|
||||
} else if (kind !== REWRITTEN_HOISTED_LET) {
|
||||
/**
|
||||
* Context variables declared with let may have reassignments. Only
|
||||
* insert a `DeclareContext` for the first encountered `StoreContext`
|
||||
* instruction.
|
||||
*/
|
||||
state.set(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
REWRITTEN_HOISTED_LET,
|
||||
);
|
||||
return {
|
||||
kind: 'replace-many',
|
||||
value: [
|
||||
{
|
||||
kind: 'instruction',
|
||||
instruction: {
|
||||
id: instruction.id,
|
||||
lvalue: null,
|
||||
value: {
|
||||
kind: 'DeclareContext',
|
||||
lvalue: {
|
||||
kind: InstructionKind.Let,
|
||||
place: {...instruction.value.lvalue.place},
|
||||
},
|
||||
loc: instruction.value.loc,
|
||||
},
|
||||
loc: instruction.loc,
|
||||
},
|
||||
},
|
||||
{kind: 'instruction', instruction},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {kind: 'remove'};
|
||||
}
|
||||
}
|
||||
if (
|
||||
instruction.value.kind === 'StoreContext' &&
|
||||
instruction.value.lvalue.kind !== InstructionKind.Reassign
|
||||
) {
|
||||
/**
|
||||
* Rewrite StoreContexts let/const that will be pre-declared in
|
||||
* codegen to reassignments.
|
||||
*/
|
||||
const lvalueId = instruction.value.lvalue.place.identifier.id;
|
||||
const isDeclaredByScope = state.activeScopes.find(scope =>
|
||||
scope.has(lvalueId),
|
||||
);
|
||||
if (isDeclaredByScope) {
|
||||
if (
|
||||
instruction.value.lvalue.kind === InstructionKind.Let ||
|
||||
instruction.value.lvalue.kind === InstructionKind.Const
|
||||
) {
|
||||
instruction.value.lvalue.kind = InstructionKind.Reassign;
|
||||
} else if (instruction.value.lvalue.kind === InstructionKind.Function) {
|
||||
state.exceptions.add(instruction.value.lvalue.place);
|
||||
const maybeHoistedFn = state.uninitialized.get(lvalueId);
|
||||
if (maybeHoistedFn != null) {
|
||||
CompilerError.invariant(maybeHoistedFn.kind === 'func', {
|
||||
reason: '[PruneHoistedContexts] Unexpected hoisted function',
|
||||
loc: instruction.loc,
|
||||
});
|
||||
maybeHoistedFn.definition = instruction.value.lvalue.place;
|
||||
}
|
||||
} else {
|
||||
CompilerError.throwTodo({
|
||||
reason: '[PruneHoistedContexts] Unexpected kind ',
|
||||
description: `(${instruction.value.lvalue.kind})`,
|
||||
loc: instruction.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.visitInstruction(instruction, state);
|
||||
return {kind: 'keep'};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For functions whose declarations span block boundaries,
|
||||
*/
|
||||
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Fixture currently fails with
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) <div>{"result":{"value":2},"fn":{"kind":"Function","result":{"value":2}},"shouldInvokeFns":true}</div>
|
||||
* Forget:
|
||||
* (kind: exception) bar is not a function
|
||||
*/
|
||||
function Foo({value}) {
|
||||
const result = bar();
|
||||
function bar() {
|
||||
return {value};
|
||||
}
|
||||
return <Stringify result={result} fn={bar} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{value: 2}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* Fixture currently fails with
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) <div>{"result":{"value":2},"fn":{"kind":"Function","result":{"value":2}},"shouldInvokeFns":true}</div>
|
||||
* Forget:
|
||||
* (kind: exception) bar is not a function
|
||||
*/
|
||||
function Foo(t0) {
|
||||
const $ = _c(6);
|
||||
const { value } = t0;
|
||||
let bar;
|
||||
let result;
|
||||
if ($[0] !== value) {
|
||||
result = bar();
|
||||
bar = function bar() {
|
||||
return { value };
|
||||
};
|
||||
$[0] = value;
|
||||
$[1] = bar;
|
||||
$[2] = result;
|
||||
} else {
|
||||
bar = $[1];
|
||||
result = $[2];
|
||||
}
|
||||
let t1;
|
||||
if ($[3] !== bar || $[4] !== result) {
|
||||
t1 = <Stringify result={result} fn={bar} shouldInvokeFns={true} />;
|
||||
$[3] = bar;
|
||||
$[4] = result;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{ value: 2 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
+1
-2
@@ -34,8 +34,7 @@ function bar(a, b) {
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
const x = [a, b];
|
||||
y = {};
|
||||
let t;
|
||||
t = {};
|
||||
let t = {};
|
||||
|
||||
y = x[0][1];
|
||||
t = x[1][0];
|
||||
|
||||
+1
-2
@@ -35,8 +35,7 @@ function bar(a, b) {
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
const x = [a, b];
|
||||
y = {};
|
||||
let t;
|
||||
t = {};
|
||||
let t = {};
|
||||
const f0 = function () {
|
||||
y = x[0][1];
|
||||
t = x[1][0];
|
||||
|
||||
+1
-2
@@ -33,8 +33,7 @@ function useTest() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
let w;
|
||||
w = {};
|
||||
let w = {};
|
||||
|
||||
const t1 = (w = 42);
|
||||
const t2 = w;
|
||||
|
||||
+1
-2
@@ -30,8 +30,7 @@ function Component(props) {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
let x;
|
||||
x = null;
|
||||
let x = null;
|
||||
const callback = () => {
|
||||
console.log(x);
|
||||
};
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Fixture currently fails with
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) <div>{"result":{"value":2},"fn":{"kind":"Function","result":{"value":2}},"shouldInvokeFns":true}</div>
|
||||
* Forget:
|
||||
* (kind: exception) bar is not a function
|
||||
*/
|
||||
function Foo({value}) {
|
||||
const result = bar();
|
||||
function bar() {
|
||||
return {value};
|
||||
}
|
||||
return <Stringify result={result} fn={bar} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{value: 2}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
10 | */
|
||||
11 | function Foo({value}) {
|
||||
> 12 | const result = bar();
|
||||
| ^^^ Todo: [PruneHoistedContexts] Rewrite hoisted function references (12:12)
|
||||
13 | function bar() {
|
||||
14 | return {value};
|
||||
15 | }
|
||||
```
|
||||
|
||||
|
||||
+39
-28
@@ -2,13 +2,22 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const data = useData();
|
||||
const data = useIdentity(
|
||||
new Map([
|
||||
[0, 'value0'],
|
||||
[1, 'value1'],
|
||||
])
|
||||
);
|
||||
const items = [];
|
||||
// NOTE: `i` is a context variable because it's reassigned and also referenced
|
||||
// within a closure, the `onClick` handler of each item
|
||||
for (let i = MIN; i <= MAX; i += INCREMENT) {
|
||||
items.push(<div key={i} onClick={() => data.set(i)} />);
|
||||
items.push(
|
||||
<Stringify key={i} onClick={() => data.get(i)} shouldInvokeFns={true} />
|
||||
);
|
||||
}
|
||||
return <>{items}</>;
|
||||
}
|
||||
@@ -17,10 +26,6 @@ const MIN = 0;
|
||||
const MAX = 3;
|
||||
const INCREMENT = 1;
|
||||
|
||||
function useData() {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
params: [],
|
||||
fn: Component,
|
||||
@@ -32,41 +37,47 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { Stringify, useIdentity } from "shared-runtime";
|
||||
|
||||
function Component() {
|
||||
const $ = _c(2);
|
||||
const data = useData();
|
||||
const $ = _c(3);
|
||||
let t0;
|
||||
if ($[0] !== data) {
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = new Map([
|
||||
[0, "value0"],
|
||||
[1, "value1"],
|
||||
]);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const data = useIdentity(t0);
|
||||
let t1;
|
||||
if ($[1] !== data) {
|
||||
const items = [];
|
||||
for (let i = MIN; i <= MAX; i = i + INCREMENT, i) {
|
||||
items.push(<div key={i} onClick={() => data.set(i)} />);
|
||||
items.push(
|
||||
<Stringify
|
||||
key={i}
|
||||
onClick={() => data.get(i)}
|
||||
shouldInvokeFns={true}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
t0 = <>{items}</>;
|
||||
$[0] = data;
|
||||
$[1] = t0;
|
||||
t1 = <>{items}</>;
|
||||
$[1] = data;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
t1 = $[2];
|
||||
}
|
||||
return t0;
|
||||
return t1;
|
||||
}
|
||||
|
||||
const MIN = 0;
|
||||
const MAX = 3;
|
||||
const INCREMENT = 1;
|
||||
|
||||
function useData() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = new Map();
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
params: [],
|
||||
fn: Component,
|
||||
@@ -75,4 +86,4 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div></div><div></div><div></div><div></div>
|
||||
(kind: ok) <div>{"onClick":{"kind":"Function","result":"value0"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function"},"shouldInvokeFns":true}</div>
|
||||
+11
-6
@@ -1,10 +1,19 @@
|
||||
import {Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const data = useData();
|
||||
const data = useIdentity(
|
||||
new Map([
|
||||
[0, 'value0'],
|
||||
[1, 'value1'],
|
||||
])
|
||||
);
|
||||
const items = [];
|
||||
// NOTE: `i` is a context variable because it's reassigned and also referenced
|
||||
// within a closure, the `onClick` handler of each item
|
||||
for (let i = MIN; i <= MAX; i += INCREMENT) {
|
||||
items.push(<div key={i} onClick={() => data.set(i)} />);
|
||||
items.push(
|
||||
<Stringify key={i} onClick={() => data.get(i)} shouldInvokeFns={true} />
|
||||
);
|
||||
}
|
||||
return <>{items}</>;
|
||||
}
|
||||
@@ -13,10 +22,6 @@ const MIN = 0;
|
||||
const MAX = 3;
|
||||
const INCREMENT = 1;
|
||||
|
||||
function useData() {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
params: [],
|
||||
fn: Component,
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {CONST_TRUE, useIdentity} from 'shared-runtime';
|
||||
|
||||
const hidden = CONST_TRUE;
|
||||
function useFoo() {
|
||||
const makeCb = useIdentity(() => {
|
||||
const logIntervalId = () => {
|
||||
log(intervalId);
|
||||
};
|
||||
|
||||
let intervalId;
|
||||
if (!hidden) {
|
||||
intervalId = 2;
|
||||
}
|
||||
return () => {
|
||||
logIntervalId();
|
||||
};
|
||||
});
|
||||
|
||||
return <Stringify fn={makeCb()} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { CONST_TRUE, useIdentity } from "shared-runtime";
|
||||
|
||||
const hidden = CONST_TRUE;
|
||||
function useFoo() {
|
||||
const $ = _c(4);
|
||||
const makeCb = useIdentity(_temp);
|
||||
let t0;
|
||||
if ($[0] !== makeCb) {
|
||||
t0 = makeCb();
|
||||
$[0] = makeCb;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
let t1;
|
||||
if ($[2] !== t0) {
|
||||
t1 = <Stringify fn={t0} shouldInvokeFns={true} />;
|
||||
$[2] = t0;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
function _temp() {
|
||||
const logIntervalId = () => {
|
||||
log(intervalId);
|
||||
};
|
||||
let intervalId;
|
||||
if (!hidden) {
|
||||
intervalId = 2;
|
||||
}
|
||||
return () => {
|
||||
logIntervalId();
|
||||
};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Stringify is not defined
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import {CONST_TRUE, useIdentity} from 'shared-runtime';
|
||||
|
||||
const hidden = CONST_TRUE;
|
||||
function useFoo() {
|
||||
const makeCb = useIdentity(() => {
|
||||
const logIntervalId = () => {
|
||||
log(intervalId);
|
||||
};
|
||||
|
||||
let intervalId;
|
||||
if (!hidden) {
|
||||
intervalId = 2;
|
||||
}
|
||||
return () => {
|
||||
logIntervalId();
|
||||
};
|
||||
});
|
||||
|
||||
return <Stringify fn={makeCb()} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
+1
-2
@@ -30,8 +30,7 @@ function Foo() {
|
||||
getX = () => x;
|
||||
console.log(getX());
|
||||
|
||||
let x;
|
||||
x = 4;
|
||||
let x = 4;
|
||||
x = x + 5;
|
||||
$[0] = getX;
|
||||
} else {
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {CONST_NUMBER1, Stringify} from 'shared-runtime';
|
||||
|
||||
function useHook({cond}) {
|
||||
'use memo';
|
||||
const getX = () => x;
|
||||
|
||||
let x;
|
||||
if (cond) {
|
||||
x = CONST_NUMBER1;
|
||||
}
|
||||
return <Stringify getX={getX} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: () => {},
|
||||
params: [{cond: true}],
|
||||
sequentialRenders: [{cond: true}, {cond: true}, {cond: false}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { CONST_NUMBER1, Stringify } from "shared-runtime";
|
||||
|
||||
function useHook(t0) {
|
||||
"use memo";
|
||||
const $ = _c(2);
|
||||
const { cond } = t0;
|
||||
let t1;
|
||||
if ($[0] !== cond) {
|
||||
const getX = () => x;
|
||||
|
||||
let x;
|
||||
if (cond) {
|
||||
x = CONST_NUMBER1;
|
||||
}
|
||||
|
||||
t1 = <Stringify getX={getX} shouldInvokeFns={true} />;
|
||||
$[0] = cond;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: () => {},
|
||||
params: [{ cond: true }],
|
||||
sequentialRenders: [{ cond: true }, { cond: true }, { cond: false }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok)
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import {CONST_NUMBER1, Stringify} from 'shared-runtime';
|
||||
|
||||
function useHook({cond}) {
|
||||
'use memo';
|
||||
const getX = () => x;
|
||||
|
||||
let x;
|
||||
if (cond) {
|
||||
x = CONST_NUMBER1;
|
||||
}
|
||||
return <Stringify getX={getX} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: () => {},
|
||||
params: [{cond: true}],
|
||||
sequentialRenders: [{cond: true}, {cond: true}, {cond: false}],
|
||||
};
|
||||
+1
-2
@@ -36,8 +36,7 @@ function hoisting(cond) {
|
||||
items.push(bar());
|
||||
};
|
||||
|
||||
let bar;
|
||||
bar = _temp;
|
||||
let bar = _temp;
|
||||
foo();
|
||||
}
|
||||
$[0] = cond;
|
||||
|
||||
+2
-4
@@ -41,11 +41,9 @@ function hoisting() {
|
||||
return result;
|
||||
};
|
||||
|
||||
let foo;
|
||||
foo = () => bar + baz;
|
||||
let foo = () => bar + baz;
|
||||
|
||||
let bar;
|
||||
bar = 3;
|
||||
let bar = 3;
|
||||
const baz = 2;
|
||||
t0 = qux();
|
||||
$[0] = t0;
|
||||
|
||||
+1
-2
@@ -37,8 +37,7 @@ function useHook(t0) {
|
||||
if ($[0] !== cond) {
|
||||
const getX = () => x;
|
||||
|
||||
let x;
|
||||
x = CONST_NUMBER0;
|
||||
let x = CONST_NUMBER0;
|
||||
if (cond) {
|
||||
x = x + CONST_NUMBER1;
|
||||
x;
|
||||
|
||||
+1
-2
@@ -38,8 +38,7 @@ function useHook(t0) {
|
||||
if ($[0] !== cond) {
|
||||
const getX = () => x;
|
||||
|
||||
let x;
|
||||
x = CONST_NUMBER0;
|
||||
let x = CONST_NUMBER0;
|
||||
if (cond) {
|
||||
x = x + CONST_NUMBER1;
|
||||
x;
|
||||
|
||||
+2
-4
@@ -29,10 +29,8 @@ function hoisting() {
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
foo = () => bar + baz;
|
||||
|
||||
let bar;
|
||||
bar = 3;
|
||||
let baz;
|
||||
baz = 2;
|
||||
let bar = 3;
|
||||
let baz = 2;
|
||||
$[0] = foo;
|
||||
} else {
|
||||
foo = $[0];
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component({prop1, prop2}) {
|
||||
'use memo';
|
||||
|
||||
const data = useIdentity(
|
||||
new Map([
|
||||
[0, 'value0'],
|
||||
[1, 'value1'],
|
||||
])
|
||||
);
|
||||
let i = 0;
|
||||
const items = [];
|
||||
items.push(
|
||||
<Stringify
|
||||
key={i}
|
||||
onClick={() => data.get(i) + prop1}
|
||||
shouldInvokeFns={true}
|
||||
/>
|
||||
);
|
||||
i = i + 1;
|
||||
items.push(
|
||||
<Stringify
|
||||
key={i}
|
||||
onClick={() => data.get(i) + prop2}
|
||||
shouldInvokeFns={true}
|
||||
/>
|
||||
);
|
||||
return <>{items}</>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prop1: 'prop1', prop2: 'prop2'}],
|
||||
sequentialRenders: [
|
||||
{prop1: 'prop1', prop2: 'prop2'},
|
||||
{prop1: 'prop1', prop2: 'prop2'},
|
||||
{prop1: 'changed', prop2: 'prop2'},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { Stringify, useIdentity } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
"use memo";
|
||||
const $ = _c(12);
|
||||
const { prop1, prop2 } = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = new Map([
|
||||
[0, "value0"],
|
||||
[1, "value1"],
|
||||
]);
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const data = useIdentity(t1);
|
||||
let t2;
|
||||
if ($[1] !== data || $[2] !== prop1 || $[3] !== prop2) {
|
||||
let i = 0;
|
||||
const items = [];
|
||||
items.push(
|
||||
<Stringify
|
||||
key={i}
|
||||
onClick={() => data.get(i) + prop1}
|
||||
shouldInvokeFns={true}
|
||||
/>,
|
||||
);
|
||||
i = i + 1;
|
||||
|
||||
const t3 = i;
|
||||
let t4;
|
||||
if ($[5] !== data || $[6] !== i || $[7] !== prop2) {
|
||||
t4 = () => data.get(i) + prop2;
|
||||
$[5] = data;
|
||||
$[6] = i;
|
||||
$[7] = prop2;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
let t5;
|
||||
if ($[9] !== t3 || $[10] !== t4) {
|
||||
t5 = <Stringify key={t3} onClick={t4} shouldInvokeFns={true} />;
|
||||
$[9] = t3;
|
||||
$[10] = t4;
|
||||
$[11] = t5;
|
||||
} else {
|
||||
t5 = $[11];
|
||||
}
|
||||
items.push(t5);
|
||||
t2 = <>{items}</>;
|
||||
$[1] = data;
|
||||
$[2] = prop1;
|
||||
$[3] = prop2;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ prop1: "prop1", prop2: "prop2" }],
|
||||
sequentialRenders: [
|
||||
{ prop1: "prop1", prop2: "prop2" },
|
||||
{ prop1: "prop1", prop2: "prop2" },
|
||||
{ prop1: "changed", prop2: "prop2" },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"onClick":{"kind":"Function","result":"value1prop1"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1prop2"},"shouldInvokeFns":true}</div>
|
||||
<div>{"onClick":{"kind":"Function","result":"value1prop1"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1prop2"},"shouldInvokeFns":true}</div>
|
||||
<div>{"onClick":{"kind":"Function","result":"value1changed"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1prop2"},"shouldInvokeFns":true}</div>
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import {Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component({prop1, prop2}) {
|
||||
'use memo';
|
||||
|
||||
const data = useIdentity(
|
||||
new Map([
|
||||
[0, 'value0'],
|
||||
[1, 'value1'],
|
||||
])
|
||||
);
|
||||
let i = 0;
|
||||
const items = [];
|
||||
items.push(
|
||||
<Stringify
|
||||
key={i}
|
||||
onClick={() => data.get(i) + prop1}
|
||||
shouldInvokeFns={true}
|
||||
/>
|
||||
);
|
||||
i = i + 1;
|
||||
items.push(
|
||||
<Stringify
|
||||
key={i}
|
||||
onClick={() => data.get(i) + prop2}
|
||||
shouldInvokeFns={true}
|
||||
/>
|
||||
);
|
||||
return <>{items}</>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prop1: 'prop1', prop2: 'prop2'}],
|
||||
sequentialRenders: [
|
||||
{prop1: 'prop1', prop2: 'prop2'},
|
||||
{prop1: 'prop1', prop2: 'prop2'},
|
||||
{prop1: 'changed', prop2: 'prop2'},
|
||||
],
|
||||
};
|
||||
+1
-2
@@ -37,8 +37,7 @@ function Component() {
|
||||
}
|
||||
const x = t0;
|
||||
|
||||
let x_0;
|
||||
x_0 = 56;
|
||||
let x_0 = 56;
|
||||
const fn = function () {
|
||||
x_0 = 42;
|
||||
};
|
||||
|
||||
+1
-2
@@ -33,8 +33,7 @@ function component(a) {
|
||||
m(x);
|
||||
};
|
||||
|
||||
let x;
|
||||
x = { a };
|
||||
let x = { a };
|
||||
m(x);
|
||||
$[0] = a;
|
||||
$[1] = y;
|
||||
|
||||
+1
-2
@@ -65,8 +65,7 @@ function useBar(t0, cond) {
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
let x;
|
||||
x = useIdentity(t1);
|
||||
let x = useIdentity(t1);
|
||||
if (cond) {
|
||||
x = b;
|
||||
}
|
||||
|
||||
+1
-2
@@ -47,8 +47,7 @@ function Foo(t0) {
|
||||
if ($[0] !== arr1 || $[1] !== arr2 || $[2] !== foo) {
|
||||
const x = [arr1];
|
||||
|
||||
let y;
|
||||
y = [];
|
||||
let y = [];
|
||||
|
||||
getVal1 = _temp;
|
||||
|
||||
|
||||
+1
-2
@@ -47,8 +47,7 @@ function Foo(t0) {
|
||||
if ($[0] !== arr1 || $[1] !== arr2 || $[2] !== foo) {
|
||||
const x = [arr1];
|
||||
|
||||
let y;
|
||||
y = [];
|
||||
let y = [];
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
|
||||
+1
-2
@@ -79,8 +79,7 @@ function Component(props) {
|
||||
|
||||
function Inner(props) {
|
||||
const $ = _c(7);
|
||||
let input;
|
||||
input = null;
|
||||
let input = null;
|
||||
if (props.cond) {
|
||||
input = use(FooContext);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, {
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
unstable_Activity as Activity,
|
||||
unstable_useSwipeTransition as useSwipeTransition,
|
||||
useLayoutEffect,
|
||||
useEffect,
|
||||
useState,
|
||||
useId,
|
||||
@@ -32,7 +33,7 @@ const b = (
|
||||
function Component() {
|
||||
return (
|
||||
<ViewTransition
|
||||
className={
|
||||
default={
|
||||
transitions['enter-slide-right'] + ' ' + transitions['exit-slide-left']
|
||||
}>
|
||||
<p className="roboto-font">Slide In from Left, Slide Out to Right</p>
|
||||
@@ -68,6 +69,16 @@ export default function Page({url, navigate}) {
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// Calling a default update should not interrupt ViewTransitions but
|
||||
// a flushSync will.
|
||||
// Promise.resolve().then(() => {
|
||||
// flushSync(() => {
|
||||
setCounter(c => c + 10);
|
||||
// });
|
||||
// });
|
||||
}, [show]);
|
||||
|
||||
const exclamation = (
|
||||
<ViewTransition name="exclamation" onShare={onTransition}>
|
||||
<span>!</span>
|
||||
@@ -86,17 +97,17 @@ export default function Page({url, navigate}) {
|
||||
}}>
|
||||
{url === '/?b' ? 'Goto A' : 'Goto B'}
|
||||
</button>
|
||||
<ViewTransition className="none">
|
||||
<ViewTransition default="none">
|
||||
<div>
|
||||
<ViewTransition>
|
||||
<div>
|
||||
<ViewTransition className={transitions['slide-on-nav']}>
|
||||
<ViewTransition default={transitions['slide-on-nav']}>
|
||||
<h1>{!show ? 'A' : 'B' + counter}</h1>
|
||||
</ViewTransition>
|
||||
</div>
|
||||
</ViewTransition>
|
||||
<ViewTransition
|
||||
className={{
|
||||
default={{
|
||||
'navigation-back': transitions['slide-right'],
|
||||
'navigation-forward': transitions['slide-left'],
|
||||
}}>
|
||||
|
||||
+6
-4
@@ -538,14 +538,16 @@ export function hasInstanceAffectedParent(
|
||||
}
|
||||
|
||||
export function startViewTransition() {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
export type RunningGestureTransition = null;
|
||||
export type RunningViewTransition = null;
|
||||
|
||||
export function startGestureTransition() {}
|
||||
export function startGestureTransition() {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function stopGestureTransition(transition: RunningGestureTransition) {}
|
||||
export function stopViewTransition(transition: RunningViewTransition) {}
|
||||
|
||||
export type ViewTransitionInstance = null | {name: string, ...};
|
||||
|
||||
|
||||
@@ -1687,7 +1687,7 @@ export function startViewTransition(
|
||||
spawnedWorkCallback: () => void,
|
||||
passiveCallback: () => mixed,
|
||||
errorCallback: mixed => void,
|
||||
): boolean {
|
||||
): null | RunningViewTransition {
|
||||
const ownerDocument: Document =
|
||||
rootContainer.nodeType === DOCUMENT_NODE
|
||||
? (rootContainer: any)
|
||||
@@ -1764,7 +1764,7 @@ export function startViewTransition(
|
||||
}
|
||||
passiveCallback();
|
||||
});
|
||||
return true;
|
||||
return transition;
|
||||
} catch (x) {
|
||||
// We use the error as feature detection.
|
||||
// The only thing that should throw is if startViewTransition is missing
|
||||
@@ -1772,11 +1772,17 @@ export function startViewTransition(
|
||||
// I.e. it's before the View Transitions v2 spec. We only support View
|
||||
// Transitions v2 otherwise we fallback to not animating to ensure that
|
||||
// we're not animating with the wrong animation mapped.
|
||||
return false;
|
||||
// Flush remaining work synchronously.
|
||||
mutationCallback();
|
||||
layoutCallback();
|
||||
// Skip afterMutationCallback(). We don't need it since we're not animating.
|
||||
spawnedWorkCallback();
|
||||
// Skip passiveCallback(). Spawned work will schedule a task.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export type RunningGestureTransition = {
|
||||
export type RunningViewTransition = {
|
||||
skipTransition(): void,
|
||||
...
|
||||
};
|
||||
@@ -1900,7 +1906,7 @@ export function startGestureTransition(
|
||||
mutationCallback: () => void,
|
||||
animateCallback: () => void,
|
||||
errorCallback: mixed => void,
|
||||
): null | RunningGestureTransition {
|
||||
): null | RunningViewTransition {
|
||||
const ownerDocument: Document =
|
||||
rootContainer.nodeType === DOCUMENT_NODE
|
||||
? (rootContainer: any)
|
||||
@@ -2072,13 +2078,14 @@ export function startGestureTransition(
|
||||
}
|
||||
}
|
||||
|
||||
export function stopGestureTransition(transition: RunningGestureTransition) {
|
||||
export function stopViewTransition(transition: RunningViewTransition) {
|
||||
transition.skipTransition();
|
||||
}
|
||||
|
||||
interface ViewTransitionPseudoElementType extends Animatable {
|
||||
_scope: HTMLElement;
|
||||
_selector: string;
|
||||
getComputedStyle(): CSSStyleDeclaration;
|
||||
}
|
||||
|
||||
function ViewTransitionPseudoElement(
|
||||
@@ -2132,6 +2139,14 @@ ViewTransitionPseudoElement.prototype.getAnimations = function (
|
||||
}
|
||||
return result;
|
||||
};
|
||||
// $FlowFixMe[prop-missing]
|
||||
ViewTransitionPseudoElement.prototype.getComputedStyle = function (
|
||||
this: ViewTransitionPseudoElementType,
|
||||
): CSSStyleDeclaration {
|
||||
const scope = this._scope;
|
||||
const selector = this._selector;
|
||||
return getComputedStyle(scope, selector);
|
||||
};
|
||||
|
||||
export function createViewTransitionInstance(
|
||||
name: string,
|
||||
|
||||
@@ -4664,7 +4664,7 @@ describe('ReactDOMFizzServer', () => {
|
||||
// client-side rendering.
|
||||
await clientResolve();
|
||||
await waitForAll([
|
||||
"onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
|
||||
"onRecoverableError: Hydration failed because the server rendered text didn't match the client.",
|
||||
]);
|
||||
expect(getVisibleChildren(container)).toEqual(
|
||||
<div>
|
||||
@@ -4712,7 +4712,7 @@ describe('ReactDOMFizzServer', () => {
|
||||
},
|
||||
});
|
||||
await waitForAll([
|
||||
"onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
|
||||
"onRecoverableError: Hydration failed because the server rendered text didn't match the client.",
|
||||
]);
|
||||
|
||||
expect(getVisibleChildren(container)).toEqual(
|
||||
@@ -10179,7 +10179,7 @@ describe('ReactDOMFizzServer', () => {
|
||||
);
|
||||
expect(recoverableErrors).toEqual([
|
||||
expect.stringContaining(
|
||||
"Hydration failed because the server rendered HTML didn't match the client.",
|
||||
"Hydration failed because the server rendered text didn't match the client.",
|
||||
),
|
||||
]);
|
||||
} else {
|
||||
|
||||
@@ -127,7 +127,7 @@ describe('ReactDOMServerHydration', () => {
|
||||
if (gate(flags => flags.favorSafetyOverHydrationPerf)) {
|
||||
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
|
||||
[
|
||||
"Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
|
||||
"Caught [Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
|
||||
|
||||
- A server/client branch \`if (typeof window !== 'undefined')\`.
|
||||
- Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
|
||||
@@ -196,7 +196,7 @@ describe('ReactDOMServerHydration', () => {
|
||||
if (gate(flags => flags.favorSafetyOverHydrationPerf)) {
|
||||
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
|
||||
[
|
||||
"Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
|
||||
"Caught [Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
|
||||
|
||||
- A server/client branch \`if (typeof window !== 'undefined')\`.
|
||||
- Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
|
||||
@@ -743,7 +743,7 @@ describe('ReactDOMServerHydration', () => {
|
||||
if (gate(flags => flags.favorSafetyOverHydrationPerf)) {
|
||||
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
|
||||
[
|
||||
"Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
|
||||
"Caught [Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
|
||||
|
||||
- A server/client branch \`if (typeof window !== 'undefined')\`.
|
||||
- Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
|
||||
|
||||
@@ -3897,7 +3897,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
||||
});
|
||||
});
|
||||
assertLog([
|
||||
"onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
|
||||
"onRecoverableError: Hydration failed because the server rendered text didn't match the client.",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -3936,7 +3936,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
||||
);
|
||||
});
|
||||
assertLog([
|
||||
"onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
|
||||
"onRecoverableError: Hydration failed because the server rendered text didn't match the client.",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -320,7 +320,7 @@ describe('rendering React components at document', () => {
|
||||
assertLog(
|
||||
favorSafetyOverHydrationPerf
|
||||
? [
|
||||
"onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
|
||||
"onRecoverableError: Hydration failed because the server rendered text didn't match the client.",
|
||||
]
|
||||
: [],
|
||||
);
|
||||
|
||||
@@ -653,11 +653,16 @@ export function startViewTransition(
|
||||
spawnedWorkCallback: () => void,
|
||||
passiveCallback: () => mixed,
|
||||
errorCallback: mixed => void,
|
||||
): boolean {
|
||||
return false;
|
||||
): null | RunningViewTransition {
|
||||
mutationCallback();
|
||||
layoutCallback();
|
||||
// Skip afterMutationCallback(). We don't need it since we're not animating.
|
||||
spawnedWorkCallback();
|
||||
// Skip passiveCallback(). Spawned work will schedule a task.
|
||||
return null;
|
||||
}
|
||||
|
||||
export type RunningGestureTransition = null;
|
||||
export type RunningViewTransition = null;
|
||||
|
||||
export function startGestureTransition(
|
||||
rootContainer: Container,
|
||||
@@ -668,13 +673,13 @@ export function startGestureTransition(
|
||||
mutationCallback: () => void,
|
||||
animateCallback: () => void,
|
||||
errorCallback: mixed => void,
|
||||
): RunningGestureTransition {
|
||||
): null | RunningViewTransition {
|
||||
mutationCallback();
|
||||
animateCallback();
|
||||
return null;
|
||||
}
|
||||
|
||||
export function stopGestureTransition(transition: RunningGestureTransition) {}
|
||||
export function stopViewTransition(transition: RunningViewTransition) {}
|
||||
|
||||
export type ViewTransitionInstance = null | {name: string, ...};
|
||||
|
||||
|
||||
+12
-6
@@ -93,7 +93,7 @@ export type TransitionStatus = mixed;
|
||||
|
||||
export type FormInstance = Instance;
|
||||
|
||||
export type RunningGestureTransition = null;
|
||||
export type RunningViewTransition = null;
|
||||
|
||||
export type ViewTransitionInstance = null | {name: string, ...};
|
||||
|
||||
@@ -826,12 +826,18 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
||||
rootContainer: Container,
|
||||
transitionTypes: null | TransitionTypes,
|
||||
mutationCallback: () => void,
|
||||
afterMutationCallback: () => void,
|
||||
layoutCallback: () => void,
|
||||
afterMutationCallback: () => void,
|
||||
spawnedWorkCallback: () => void,
|
||||
passiveCallback: () => mixed,
|
||||
errorCallback: mixed => void,
|
||||
): boolean {
|
||||
return false;
|
||||
): null | RunningViewTransition {
|
||||
mutationCallback();
|
||||
layoutCallback();
|
||||
// Skip afterMutationCallback(). We don't need it since we're not animating.
|
||||
spawnedWorkCallback();
|
||||
// Skip passiveCallback(). Spawned work will schedule a task.
|
||||
return null;
|
||||
},
|
||||
|
||||
startGestureTransition(
|
||||
@@ -843,13 +849,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
||||
mutationCallback: () => void,
|
||||
animateCallback: () => void,
|
||||
errorCallback: mixed => void,
|
||||
): RunningGestureTransition {
|
||||
): null | RunningViewTransition {
|
||||
mutationCallback();
|
||||
animateCallback();
|
||||
return null;
|
||||
},
|
||||
|
||||
stopGestureTransition(transition: RunningGestureTransition) {},
|
||||
stopViewTransition(transition: RunningViewTransition) {},
|
||||
|
||||
createViewTransitionInstance(name: string): ViewTransitionInstance {
|
||||
return null;
|
||||
|
||||
+6
-6
@@ -151,7 +151,7 @@ function trackDeletedPairViewTransitions(deletion: Fiber): void {
|
||||
// and can stop searching (size reaches zero).
|
||||
pairs.delete(name);
|
||||
const className: ?string = getViewTransitionClassName(
|
||||
props.className,
|
||||
props.default,
|
||||
props.share,
|
||||
);
|
||||
if (className !== 'none') {
|
||||
@@ -196,7 +196,7 @@ function trackEnterViewTransitions(deletion: Fiber): void {
|
||||
? appearingViewTransitions.get(name)
|
||||
: undefined;
|
||||
const className: ?string = getViewTransitionClassName(
|
||||
props.className,
|
||||
props.default,
|
||||
pair !== undefined ? props.share : props.enter,
|
||||
);
|
||||
if (className !== 'none') {
|
||||
@@ -259,7 +259,7 @@ function applyAppearingPairViewTransition(child: Fiber): void {
|
||||
// Note that this class name that doesn't actually really matter because the
|
||||
// "new" side will be the one that wins in practice.
|
||||
const className: ?string = getViewTransitionClassName(
|
||||
props.className,
|
||||
props.default,
|
||||
props.share,
|
||||
);
|
||||
if (className !== 'none') {
|
||||
@@ -282,7 +282,7 @@ function applyExitViewTransition(placement: Fiber): void {
|
||||
const props: ViewTransitionProps = placement.memoizedProps;
|
||||
const name = getViewTransitionName(props, state);
|
||||
const className: ?string = getViewTransitionClassName(
|
||||
props.className,
|
||||
props.default,
|
||||
// Note that just because we don't have a pair yet doesn't mean we won't find one
|
||||
// later. However, that doesn't matter because if we do the class name that wins
|
||||
// is the one applied by the "new" side anyway.
|
||||
@@ -307,7 +307,7 @@ function applyNestedViewTransition(child: Fiber): void {
|
||||
const props: ViewTransitionProps = child.memoizedProps;
|
||||
const name = getViewTransitionName(props, state);
|
||||
const className: ?string = getViewTransitionClassName(
|
||||
props.className,
|
||||
props.default,
|
||||
props.update,
|
||||
);
|
||||
if (className !== 'none') {
|
||||
@@ -336,7 +336,7 @@ function applyUpdateViewTransition(current: Fiber, finishedWork: Fiber): void {
|
||||
// want the props from "current" since that's the class that would've won if
|
||||
// it was the normal direction. To preserve the same effect in either direction.
|
||||
const className: ?string = getViewTransitionClassName(
|
||||
newProps.className,
|
||||
newProps.default,
|
||||
newProps.update,
|
||||
);
|
||||
if (className === 'none') {
|
||||
|
||||
@@ -322,6 +322,7 @@ export let didWarnAboutReassigningProps: boolean;
|
||||
let didWarnAboutRevealOrder;
|
||||
let didWarnAboutTailOptions;
|
||||
let didWarnAboutDefaultPropsOnFunctionComponent;
|
||||
let didWarnAboutClassNameOnViewTransition;
|
||||
|
||||
if (__DEV__) {
|
||||
didWarnAboutBadClass = ({}: {[string]: boolean});
|
||||
@@ -332,6 +333,7 @@ if (__DEV__) {
|
||||
didWarnAboutRevealOrder = ({}: {[empty]: boolean});
|
||||
didWarnAboutTailOptions = ({}: {[string]: boolean});
|
||||
didWarnAboutDefaultPropsOnFunctionComponent = ({}: {[string]: boolean});
|
||||
didWarnAboutClassNameOnViewTransition = ({}: {[string]: boolean});
|
||||
}
|
||||
|
||||
export function reconcileChildren(
|
||||
@@ -3295,6 +3297,25 @@ function updateViewTransition(
|
||||
pushMaterializedTreeId(workInProgress);
|
||||
}
|
||||
}
|
||||
if (__DEV__) {
|
||||
// $FlowFixMe[prop-missing]
|
||||
if (pendingProps.className !== undefined) {
|
||||
const example =
|
||||
typeof pendingProps.className === 'string'
|
||||
? JSON.stringify(pendingProps.className)
|
||||
: '{...}';
|
||||
if (!didWarnAboutClassNameOnViewTransition[example]) {
|
||||
didWarnAboutClassNameOnViewTransition[example] = true;
|
||||
console.error(
|
||||
'<ViewTransition> doesn\'t accept a "className" prop. It has been renamed to "default".\n' +
|
||||
'- <ViewTransition className=%s>\n' +
|
||||
'+ <ViewTransition default=%s>',
|
||||
example,
|
||||
example,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (current !== null && current.memoizedProps.name !== pendingProps.name) {
|
||||
// If the name changes, we schedule a ref effect to create a new ref instance.
|
||||
workInProgress.flags |= Ref | RefStatic;
|
||||
|
||||
@@ -67,6 +67,20 @@ export function trackAppearingViewTransition(
|
||||
appearingViewTransitions.set(name, state);
|
||||
}
|
||||
|
||||
export function trackEnterViewTransitions(placement: Fiber): void {
|
||||
if (
|
||||
placement.tag === ViewTransitionComponent ||
|
||||
(placement.subtreeFlags & ViewTransitionStatic) !== NoFlags
|
||||
) {
|
||||
// If an inserted or appearing Fiber is a ViewTransition component or has one as
|
||||
// an immediate child, then that will trigger as an "Enter" in future passes.
|
||||
// We don't do anything else for that case in the "before mutation" phase but we
|
||||
// still have to mark it as needing to call startViewTransition if nothing else
|
||||
// updates.
|
||||
shouldStartViewTransition = true;
|
||||
}
|
||||
}
|
||||
|
||||
// We can't cancel view transition children until we know that their parent also
|
||||
// don't need to transition.
|
||||
export let viewTransitionCancelableChildren: null | Array<
|
||||
@@ -119,7 +133,6 @@ function applyViewTransitionToHostInstancesRecursive(
|
||||
let inViewport = false;
|
||||
while (child !== null) {
|
||||
if (child.tag === HostComponent) {
|
||||
shouldStartViewTransition = true;
|
||||
const instance: Instance = child.stateNode;
|
||||
if (collectMeasurements !== null) {
|
||||
const measurement = measureInstance(instance);
|
||||
@@ -132,6 +145,7 @@ function applyViewTransitionToHostInstancesRecursive(
|
||||
inViewport = true;
|
||||
}
|
||||
}
|
||||
shouldStartViewTransition = true;
|
||||
applyViewTransitionName(
|
||||
instance,
|
||||
viewTransitionHostInstanceIdx === 0
|
||||
@@ -228,7 +242,7 @@ function commitAppearingPairViewTransitions(placement: Fiber): void {
|
||||
}
|
||||
const name = props.name;
|
||||
const className: ?string = getViewTransitionClassName(
|
||||
props.className,
|
||||
props.default,
|
||||
props.share,
|
||||
);
|
||||
if (className !== 'none') {
|
||||
@@ -267,7 +281,7 @@ export function commitEnterViewTransitions(
|
||||
const props: ViewTransitionProps = placement.memoizedProps;
|
||||
const name = getViewTransitionName(props, state);
|
||||
const className: ?string = getViewTransitionClassName(
|
||||
props.className,
|
||||
props.default,
|
||||
state.paired ? props.share : props.enter,
|
||||
);
|
||||
if (className !== 'none') {
|
||||
@@ -337,7 +351,7 @@ function commitDeletedPairViewTransitions(deletion: Fiber): void {
|
||||
const pair = pairs.get(name);
|
||||
if (pair !== undefined) {
|
||||
const className: ?string = getViewTransitionClassName(
|
||||
props.className,
|
||||
props.default,
|
||||
props.share,
|
||||
);
|
||||
if (className !== 'none') {
|
||||
@@ -389,7 +403,7 @@ export function commitExitViewTransitions(deletion: Fiber): void {
|
||||
? appearingViewTransitions.get(name)
|
||||
: undefined;
|
||||
const className: ?string = getViewTransitionClassName(
|
||||
props.className,
|
||||
props.default,
|
||||
pair !== undefined ? props.share : props.exit,
|
||||
);
|
||||
if (className !== 'none') {
|
||||
@@ -470,7 +484,7 @@ export function commitBeforeUpdateViewTransition(
|
||||
// a layout only change, then the "foo" class will be applied even though
|
||||
// it was not actually an update. Which is a bug.
|
||||
const className: ?string = getViewTransitionClassName(
|
||||
newProps.className,
|
||||
newProps.default,
|
||||
newProps.update,
|
||||
);
|
||||
if (className === 'none') {
|
||||
@@ -495,7 +509,7 @@ export function commitNestedViewTransitions(changedParent: Fiber): void {
|
||||
const props: ViewTransitionProps = child.memoizedProps;
|
||||
const name = getViewTransitionName(props, child.stateNode);
|
||||
const className: ?string = getViewTransitionClassName(
|
||||
props.className,
|
||||
props.default,
|
||||
props.update,
|
||||
);
|
||||
if (className !== 'none') {
|
||||
@@ -735,7 +749,7 @@ export function measureUpdateViewTransition(
|
||||
const oldName = getViewTransitionName(oldFiber.memoizedProps, state);
|
||||
// Whether it ends up having been updated or relayout we apply the update class name.
|
||||
const className: ?string = getViewTransitionClassName(
|
||||
props.className,
|
||||
props.default,
|
||||
props.update,
|
||||
);
|
||||
if (className === 'none') {
|
||||
@@ -787,7 +801,7 @@ export function measureNestedViewTransitions(
|
||||
const state: ViewTransitionState = child.stateNode;
|
||||
const name = getViewTransitionName(props, state);
|
||||
const className: ?string = getViewTransitionClassName(
|
||||
props.className,
|
||||
props.default,
|
||||
props.update,
|
||||
);
|
||||
let previousMeasurements: null | Array<InstanceMeasurement>;
|
||||
|
||||
@@ -235,6 +235,7 @@ import {
|
||||
commitFragmentInstanceInsertionEffects,
|
||||
} from './ReactFiberCommitHostEffects';
|
||||
import {
|
||||
trackEnterViewTransitions,
|
||||
commitEnterViewTransitions,
|
||||
commitExitViewTransitions,
|
||||
commitBeforeUpdateViewTransition,
|
||||
@@ -338,6 +339,9 @@ function commitBeforeMutationEffects_begin(isViewTransitionEligible: boolean) {
|
||||
// to trigger updates of any nested view transitions and we shouldn't
|
||||
// have any other before mutation effects since snapshot effects are
|
||||
// only applied to updates. TODO: Model this using only flags.
|
||||
if (isViewTransitionEligible) {
|
||||
trackEnterViewTransitions(fiber);
|
||||
}
|
||||
commitBeforeMutationEffects_complete(isViewTransitionEligible);
|
||||
continue;
|
||||
}
|
||||
@@ -367,6 +371,9 @@ function commitBeforeMutationEffects_begin(isViewTransitionEligible: boolean) {
|
||||
// to trigger updates of any nested view transitions and we shouldn't
|
||||
// have any other before mutation effects since snapshot effects are
|
||||
// only applied to updates. TODO: Model this using only flags.
|
||||
if (isViewTransitionEligible) {
|
||||
trackEnterViewTransitions(fiber);
|
||||
}
|
||||
commitBeforeMutationEffects_complete(isViewTransitionEligible);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -51,9 +51,9 @@ export const wasInstanceInViewport = shim;
|
||||
export const hasInstanceChanged = shim;
|
||||
export const hasInstanceAffectedParent = shim;
|
||||
export const startViewTransition = shim;
|
||||
export type RunningGestureTransition = null;
|
||||
export type RunningViewTransition = null;
|
||||
export const startGestureTransition = shim;
|
||||
export const stopGestureTransition = shim;
|
||||
export const stopViewTransition = shim;
|
||||
export type ViewTransitionInstance = null | {name: string, ...};
|
||||
export const createViewTransitionInstance = shim;
|
||||
export type GestureTimeline = any;
|
||||
|
||||
@@ -8,10 +8,7 @@
|
||||
*/
|
||||
|
||||
import type {FiberRoot} from './ReactInternalTypes';
|
||||
import type {
|
||||
GestureTimeline,
|
||||
RunningGestureTransition,
|
||||
} from './ReactFiberConfig';
|
||||
import type {GestureTimeline, RunningViewTransition} from './ReactFiberConfig';
|
||||
|
||||
import {
|
||||
GestureLane,
|
||||
@@ -21,7 +18,7 @@ import {
|
||||
import {ensureRootIsScheduled} from './ReactFiberRootScheduler';
|
||||
import {
|
||||
subscribeToGestureDirection,
|
||||
stopGestureTransition,
|
||||
stopViewTransition,
|
||||
} from './ReactFiberConfig';
|
||||
|
||||
// This type keeps track of any scheduled or active gestures.
|
||||
@@ -33,7 +30,7 @@ export type ScheduledGesture = {
|
||||
rangeCurrent: number, // The starting offset along the timeline.
|
||||
rangeNext: number, // The end along the timeline where the next state is reached.
|
||||
cancel: () => void, // Cancel the subscription to direction change.
|
||||
running: null | RunningGestureTransition, // Used to cancel the running transition after we're done.
|
||||
running: null | RunningViewTransition, // Used to cancel the running transition after we're done.
|
||||
prev: null | ScheduledGesture, // The previous scheduled gesture in the queue for this root.
|
||||
next: null | ScheduledGesture, // The next scheduled gesture in the queue for this root.
|
||||
};
|
||||
@@ -144,7 +141,7 @@ export function cancelScheduledGesture(
|
||||
} else {
|
||||
gesture.running = null;
|
||||
// If there's no work scheduled so we can stop the View Transition right away.
|
||||
stopGestureTransition(runningTransition);
|
||||
stopViewTransition(runningTransition);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,7 +180,7 @@ export function stopCompletedGestures(root: FiberRoot) {
|
||||
root.stoppingGestures = null;
|
||||
while (gesture !== null) {
|
||||
if (gesture.running !== null) {
|
||||
stopGestureTransition(gesture.running);
|
||||
stopViewTransition(gesture.running);
|
||||
gesture.running = null;
|
||||
}
|
||||
const nextGesture = gesture.next;
|
||||
|
||||
@@ -308,7 +308,7 @@ export const HydrationMismatchException: mixed = new Error(
|
||||
"userspace. If you're seeing this, it's likely a bug in React.",
|
||||
);
|
||||
|
||||
function throwOnHydrationMismatch(fiber: Fiber) {
|
||||
function throwOnHydrationMismatch(fiber: Fiber, fromText: boolean = false) {
|
||||
let diff = '';
|
||||
if (__DEV__) {
|
||||
// Consume the diff root for this mismatch.
|
||||
@@ -320,7 +320,8 @@ function throwOnHydrationMismatch(fiber: Fiber) {
|
||||
}
|
||||
}
|
||||
const error = new Error(
|
||||
"Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:\n" +
|
||||
`Hydration failed because the server rendered ${fromText ? 'text' : 'HTML'} didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
|
||||
` +
|
||||
'\n' +
|
||||
"- A server/client branch `if (typeof window !== 'undefined')`.\n" +
|
||||
"- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
|
||||
@@ -481,7 +482,7 @@ function prepareToHydrateHostInstance(
|
||||
fiber,
|
||||
);
|
||||
if (!didHydrate && favorSafetyOverHydrationPerf) {
|
||||
throwOnHydrationMismatch(fiber);
|
||||
throwOnHydrationMismatch(fiber, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,7 +548,7 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): void {
|
||||
parentProps,
|
||||
);
|
||||
if (!didHydrate && favorSafetyOverHydrationPerf) {
|
||||
throwOnHydrationMismatch(fiber);
|
||||
throwOnHydrationMismatch(fiber, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -310,7 +310,12 @@ function processRootScheduleInMicrotask() {
|
||||
|
||||
// At the end of the microtask, flush any pending synchronous work. This has
|
||||
// to come at the end, because it does actual rendering work that might throw.
|
||||
flushSyncWorkAcrossRoots_impl(syncTransitionLanes, false);
|
||||
// If we're in the middle of a View Transition async sequence, we don't want to
|
||||
// interrupt that sequence. Instead, we'll flush any remaining work when it
|
||||
// completes.
|
||||
if (!hasPendingCommitEffects()) {
|
||||
flushSyncWorkAcrossRoots_impl(syncTransitionLanes, false);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleTaskForRootDuringMicrotask(
|
||||
|
||||
@@ -21,15 +21,19 @@ import {getIsHydrating} from './ReactFiberHydrationContext';
|
||||
import {getTreeId} from './ReactFiberTreeContext';
|
||||
|
||||
export type ViewTransitionClassPerType = {
|
||||
[transitionType: 'default' | string]: 'none' | string,
|
||||
[transitionType: 'default' | string]: 'none' | 'auto' | string,
|
||||
};
|
||||
|
||||
export type ViewTransitionClass = 'none' | string | ViewTransitionClassPerType;
|
||||
export type ViewTransitionClass =
|
||||
| 'none'
|
||||
| 'auto'
|
||||
| string
|
||||
| ViewTransitionClassPerType;
|
||||
|
||||
export type ViewTransitionProps = {
|
||||
name?: string,
|
||||
children?: ReactNodeList,
|
||||
className?: ViewTransitionClass,
|
||||
default?: ViewTransitionClass,
|
||||
enter?: ViewTransitionClass,
|
||||
exit?: ViewTransitionClass,
|
||||
share?: ViewTransitionClass,
|
||||
@@ -127,13 +131,10 @@ export function getViewTransitionClassName(
|
||||
const className: ?string = getClassNameByType(defaultClass);
|
||||
const eventClassName: ?string = getClassNameByType(eventClass);
|
||||
if (eventClassName == null) {
|
||||
return className;
|
||||
return className === 'auto' ? null : className;
|
||||
}
|
||||
if (eventClassName === 'none') {
|
||||
return eventClassName;
|
||||
}
|
||||
if (className != null && className !== 'none') {
|
||||
return className + ' ' + eventClassName;
|
||||
if (eventClassName === 'auto') {
|
||||
return null;
|
||||
}
|
||||
return eventClassName;
|
||||
}
|
||||
|
||||
+34
-7
@@ -21,7 +21,11 @@ import type {
|
||||
TransitionAbort,
|
||||
} from './ReactFiberTracingMarkerComponent';
|
||||
import type {OffscreenInstance} from './ReactFiberActivityComponent';
|
||||
import type {Resource, ViewTransitionInstance} from './ReactFiberConfig';
|
||||
import type {
|
||||
Resource,
|
||||
ViewTransitionInstance,
|
||||
RunningViewTransition,
|
||||
} from './ReactFiberConfig';
|
||||
import type {RootState} from './ReactFiberRoot';
|
||||
import {
|
||||
getViewTransitionName,
|
||||
@@ -102,6 +106,7 @@ import {
|
||||
trackSchedulerEvent,
|
||||
startViewTransition,
|
||||
startGestureTransition,
|
||||
stopViewTransition,
|
||||
createViewTransitionInstance,
|
||||
} from './ReactFiberConfig';
|
||||
|
||||
@@ -665,6 +670,7 @@ let pendingEffectsRemainingLanes: Lanes = NoLanes;
|
||||
let pendingEffectsRenderEndTime: number = -0; // Profiling-only
|
||||
let pendingPassiveTransitions: Array<Transition> | null = null;
|
||||
let pendingRecoverableErrors: null | Array<CapturedValue<mixed>> = null;
|
||||
let pendingViewTransition: null | RunningViewTransition = null;
|
||||
let pendingViewTransitionEvents: Array<(types: Array<string>) => void> | null =
|
||||
null;
|
||||
let pendingTransitionTypes: null | TransitionTypes = null;
|
||||
@@ -3503,10 +3509,8 @@ function commitRoot(
|
||||
}
|
||||
|
||||
pendingEffectsStatus = PENDING_MUTATION_PHASE;
|
||||
const startedViewTransition =
|
||||
enableViewTransition &&
|
||||
willStartViewTransition &&
|
||||
startViewTransition(
|
||||
if (enableViewTransition && willStartViewTransition) {
|
||||
pendingViewTransition = startViewTransition(
|
||||
root.containerInfo,
|
||||
pendingTransitionTypes,
|
||||
flushMutationEffects,
|
||||
@@ -3516,7 +3520,7 @@ function commitRoot(
|
||||
flushPassiveEffects,
|
||||
reportViewTransitionError,
|
||||
);
|
||||
if (!startedViewTransition) {
|
||||
} else {
|
||||
// Flush synchronously.
|
||||
flushMutationEffects();
|
||||
flushLayoutEffects();
|
||||
@@ -3646,6 +3650,8 @@ function flushSpawnedWork(): void {
|
||||
}
|
||||
pendingEffectsStatus = NO_PENDING_EFFECTS;
|
||||
|
||||
pendingViewTransition = null; // The view transition has now fully started.
|
||||
|
||||
// Tell Scheduler to yield at the end of the frame, so the browser has an
|
||||
// opportunity to paint.
|
||||
requestPaint();
|
||||
@@ -3915,7 +3921,7 @@ function commitGestureOnRoot(
|
||||
pendingTransitionTypes = null;
|
||||
pendingEffectsStatus = PENDING_GESTURE_MUTATION_PHASE;
|
||||
|
||||
finishedGesture.running = startGestureTransition(
|
||||
pendingViewTransition = finishedGesture.running = startGestureTransition(
|
||||
root.containerInfo,
|
||||
finishedGesture.provider,
|
||||
finishedGesture.rangeCurrent,
|
||||
@@ -3975,6 +3981,8 @@ function flushGestureAnimations(): void {
|
||||
pendingFinishedWork = (null: any); // Clear for GC purposes.
|
||||
pendingEffectsLanes = NoLanes;
|
||||
|
||||
pendingViewTransition = null; // The view transition has now fully started.
|
||||
|
||||
const prevTransition = ReactSharedInternals.T;
|
||||
ReactSharedInternals.T = null;
|
||||
const previousPriority = getCurrentUpdatePriority();
|
||||
@@ -4025,8 +4033,27 @@ function releaseRootPooledCache(root: FiberRoot, remainingLanes: Lanes) {
|
||||
}
|
||||
}
|
||||
|
||||
let didWarnAboutInterruptedViewTransitions = false;
|
||||
|
||||
export function flushPendingEffects(wasDelayedCommit?: boolean): boolean {
|
||||
// Returns whether passive effects were flushed.
|
||||
if (enableViewTransition && pendingViewTransition !== null) {
|
||||
// If we forced a flush before the View Transition full started then we skip it.
|
||||
// This ensures that we're not running a partial animation.
|
||||
stopViewTransition(pendingViewTransition);
|
||||
if (__DEV__) {
|
||||
if (!didWarnAboutInterruptedViewTransitions) {
|
||||
didWarnAboutInterruptedViewTransitions = true;
|
||||
console.warn(
|
||||
'A flushSync update cancelled a View Transition because it was called ' +
|
||||
'while the View Transition was still preparing. To preserve the synchronous ' +
|
||||
'semantics, React had to skip the View Transition. If you can, try to avoid ' +
|
||||
"flushSync() in a scenario that's likely to interfere.",
|
||||
);
|
||||
}
|
||||
}
|
||||
pendingViewTransition = null;
|
||||
}
|
||||
flushGestureMutations();
|
||||
flushGestureAnimations();
|
||||
flushMutationEffects();
|
||||
|
||||
@@ -40,7 +40,7 @@ export opaque type NoTimeout = mixed;
|
||||
export opaque type RendererInspectionConfig = mixed;
|
||||
export opaque type TransitionStatus = mixed;
|
||||
export opaque type FormInstance = mixed;
|
||||
export type RunningGestureTransition = mixed;
|
||||
export type RunningViewTransition = mixed;
|
||||
export type ViewTransitionInstance = null | {name: string, ...};
|
||||
export opaque type InstanceMeasurement = mixed;
|
||||
export type EventResponder = any;
|
||||
@@ -155,7 +155,7 @@ export const hasInstanceChanged = $$$config.hasInstanceChanged;
|
||||
export const hasInstanceAffectedParent = $$$config.hasInstanceAffectedParent;
|
||||
export const startViewTransition = $$$config.startViewTransition;
|
||||
export const startGestureTransition = $$$config.startGestureTransition;
|
||||
export const stopGestureTransition = $$$config.stopGestureTransition;
|
||||
export const stopViewTransition = $$$config.stopViewTransition;
|
||||
export const getCurrentGestureOffset = $$$config.getCurrentGestureOffset;
|
||||
export const subscribeToGestureDirection =
|
||||
$$$config.subscribeToGestureDirection;
|
||||
|
||||
@@ -422,11 +422,16 @@ export function startViewTransition(
|
||||
spawnedWorkCallback: () => void,
|
||||
passiveCallback: () => mixed,
|
||||
errorCallback: mixed => void,
|
||||
): boolean {
|
||||
return false;
|
||||
): null | RunningViewTransition {
|
||||
mutationCallback();
|
||||
layoutCallback();
|
||||
// Skip afterMutationCallback(). We don't need it since we're not animating.
|
||||
spawnedWorkCallback();
|
||||
// Skip passiveCallback(). Spawned work will schedule a task.
|
||||
return null;
|
||||
}
|
||||
|
||||
export type RunningGestureTransition = null;
|
||||
export type RunningViewTransition = null;
|
||||
|
||||
export function startGestureTransition(
|
||||
rootContainer: Container,
|
||||
@@ -437,13 +442,13 @@ export function startGestureTransition(
|
||||
mutationCallback: () => void,
|
||||
animateCallback: () => void,
|
||||
errorCallback: mixed => void,
|
||||
): RunningGestureTransition {
|
||||
): null | RunningViewTransition {
|
||||
mutationCallback();
|
||||
animateCallback();
|
||||
return null;
|
||||
}
|
||||
|
||||
export function stopGestureTransition(transition: RunningGestureTransition) {}
|
||||
export function stopViewTransition(transition: RunningViewTransition) {}
|
||||
|
||||
export type ViewTransitionInstance = null | {name: string, ...};
|
||||
|
||||
|
||||
@@ -403,7 +403,7 @@
|
||||
"415": "Error parsing the data. It's probably an error code or network corruption.",
|
||||
"416": "This environment don't support binary chunks.",
|
||||
"417": "React currently only supports piping to one writable stream.",
|
||||
"418": "Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:\n\n- A server/client branch `if (typeof window !== 'undefined')`.\n- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n- Date formatting in a user's locale which doesn't match the server.\n- External changing data without sending a snapshot of it along with the HTML.\n- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\nhttps://react.dev/link/hydration-mismatch%s",
|
||||
"418": "Hydration failed because the server rendered %s didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:\n\n- A server/client branch `if (typeof window !== 'undefined')`.\n- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n- Date formatting in a user's locale which doesn't match the server.\n- External changing data without sending a snapshot of it along with the HTML.\n- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\nhttps://react.dev/link/hydration-mismatch%s",
|
||||
"419": "The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.",
|
||||
"420": "ServerContext: %s already defined",
|
||||
"421": "This Suspense boundary received an update before it finished hydrating. This caused the boundary to switch to client rendering. The usual way to fix this is to wrap the original update in startTransition.",
|
||||
|
||||
Reference in New Issue
Block a user