mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge c382e18ae5 into sapling-pr-archive-poteto
This commit is contained in:
@@ -577,6 +577,7 @@ module.exports = {
|
||||
$AsyncIterator: 'readonly',
|
||||
Iterator: 'readonly',
|
||||
AsyncIterator: 'readonly',
|
||||
IntervalID: 'readonly',
|
||||
IteratorResult: 'readonly',
|
||||
JSONValue: 'readonly',
|
||||
JSResourceReference: 'readonly',
|
||||
|
||||
@@ -17,6 +17,9 @@ import {
|
||||
updateSourceWithOverridePragma,
|
||||
} from '../../lib/configUtils';
|
||||
|
||||
// @ts-ignore - webpack asset/source loader handles .d.ts files as strings
|
||||
import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts';
|
||||
|
||||
loader.config({monaco});
|
||||
|
||||
export default function ConfigEditor(): JSX.Element {
|
||||
@@ -57,6 +60,41 @@ export default function ConfigEditor(): JSX.Element {
|
||||
_: editor.IStandaloneCodeEditor,
|
||||
monaco: Monaco,
|
||||
) => void = (_, monaco) => {
|
||||
// Add the babel-plugin-react-compiler type definitions to Monaco
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
// @ts-ignore
|
||||
compilerTypeDefs,
|
||||
'file:///node_modules/babel-plugin-react-compiler/dist/index.d.ts',
|
||||
);
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
// @ts-ignore
|
||||
compilerTypeDefs,
|
||||
'file:///node_modules/babel-plugin-react-compiler/dist/index.d.ts',
|
||||
);
|
||||
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
|
||||
target: monaco.languages.typescript.ScriptTarget.Latest,
|
||||
allowNonTsExtensions: true,
|
||||
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||
module: monaco.languages.typescript.ModuleKind.ESNext,
|
||||
noEmit: true,
|
||||
allowJs: true,
|
||||
checkJs: true,
|
||||
strict: false,
|
||||
esModuleInterop: true,
|
||||
allowSyntheticDefaultImports: true,
|
||||
jsx: monaco.languages.typescript.JsxEmit.React,
|
||||
});
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
||||
target: monaco.languages.typescript.ScriptTarget.Latest,
|
||||
allowNonTsExtensions: true,
|
||||
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||
module: monaco.languages.typescript.ModuleKind.ESNext,
|
||||
noEmit: true,
|
||||
strict: false,
|
||||
esModuleInterop: true,
|
||||
allowSyntheticDefaultImports: true,
|
||||
jsx: monaco.languages.typescript.JsxEmit.React,
|
||||
});
|
||||
setMonaco(monaco);
|
||||
|
||||
const uri = monaco.Uri.parse(`file:///config.js`);
|
||||
@@ -78,8 +116,8 @@ export default function ConfigEditor(): JSX.Element {
|
||||
enable={{right: true}}
|
||||
className="!h-[calc(100vh_-_3.5rem_-_4rem)]">
|
||||
<MonacoEditor
|
||||
path={'config.js'}
|
||||
language={'javascript'}
|
||||
path={'config.ts'}
|
||||
language={'typescript'}
|
||||
value={store.config}
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
|
||||
+1
-1
@@ -2119,7 +2119,7 @@ function computeSignatureForInstruction(
|
||||
effects.push({
|
||||
kind: 'Freeze',
|
||||
value: operand,
|
||||
reason: ValueReason.Other,
|
||||
reason: ValueReason.HookCaptured,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -175,6 +175,41 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
|
||||
if (node != null) {
|
||||
valueBlockNodes.set(fallthrough, node);
|
||||
}
|
||||
} else if (terminal.kind === 'goto') {
|
||||
/**
|
||||
* If we encounter a goto that is not to the natural fallthrough of the current
|
||||
* block (not the topmost fallthrough on the stack), then this is a goto to a
|
||||
* label. Any scopes that extend beyond the goto must be extended to include
|
||||
* the labeled range, so that the break statement doesn't accidentally jump
|
||||
* out of the scope. We do this by extending the start and end of the scope's
|
||||
* range to the label and its fallthrough respectively.
|
||||
*/
|
||||
const start = activeBlockFallthroughRanges.find(
|
||||
range => range.fallthrough === terminal.block,
|
||||
);
|
||||
if (start != null && start !== activeBlockFallthroughRanges.at(-1)) {
|
||||
const fallthroughBlock = fn.body.blocks.get(start.fallthrough)!;
|
||||
const firstId =
|
||||
fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id;
|
||||
for (const scope of activeScopes) {
|
||||
/**
|
||||
* activeScopes is only filtered at block start points, so some of the
|
||||
* scopes may not actually be active anymore, ie we've past their end
|
||||
* instruction. Only extend ranges for scopes that are actually active.
|
||||
*
|
||||
* TODO: consider pruning activeScopes per instruction
|
||||
*/
|
||||
if (scope.range.end <= terminal.id) {
|
||||
continue;
|
||||
}
|
||||
scope.range.start = makeInstructionId(
|
||||
Math.min(start.range.start, scope.range.start),
|
||||
);
|
||||
scope.range.end = makeInstructionId(
|
||||
Math.max(firstId, scope.range.end),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
+21
-7
@@ -411,7 +411,9 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
|
||||
this.state = state;
|
||||
this.options = {
|
||||
memoizeJsxElements: !this.env.config.enableForest,
|
||||
forceMemoizePrimitives: this.env.config.enableForest,
|
||||
forceMemoizePrimitives:
|
||||
this.env.config.enableForest ||
|
||||
this.env.config.enablePreserveExistingMemoizationGuarantees,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -534,9 +536,23 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
|
||||
case 'JSXText':
|
||||
case 'BinaryExpression':
|
||||
case 'UnaryExpression': {
|
||||
const level = options.forceMemoizePrimitives
|
||||
? MemoizationLevel.Memoized
|
||||
: MemoizationLevel.Never;
|
||||
if (options.forceMemoizePrimitives) {
|
||||
/**
|
||||
* Because these instructions produce primitives we usually don't consider
|
||||
* them as escape points: they are known to copy, not return references.
|
||||
* However if we're forcing memoization of primitives then we mark these
|
||||
* instructions as needing memoization and walk their rvalues to ensure
|
||||
* any scopes transitively reachable from the rvalues are considered for
|
||||
* memoization. Note: we may still prune primitive-producing scopes if
|
||||
* they don't ultimately escape at all.
|
||||
*/
|
||||
const level = MemoizationLevel.Conditional;
|
||||
return {
|
||||
lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
|
||||
rvalues: [...eachReactiveValueOperand(value)],
|
||||
};
|
||||
}
|
||||
const level = MemoizationLevel.Never;
|
||||
return {
|
||||
// All of these instructions return a primitive value and never need to be memoized
|
||||
lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
|
||||
@@ -685,9 +701,7 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
|
||||
}
|
||||
case 'ComputedLoad':
|
||||
case 'PropertyLoad': {
|
||||
const level = options.forceMemoizePrimitives
|
||||
? MemoizationLevel.Memoized
|
||||
: MemoizationLevel.Conditional;
|
||||
const level = MemoizationLevel.Conditional;
|
||||
return {
|
||||
// Indirection for the inner value, memoized if the value is
|
||||
lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
|
||||
|
||||
+9
-7
@@ -46,14 +46,16 @@ function useFoo(t0) {
|
||||
t1 = $[0];
|
||||
}
|
||||
let items = t1;
|
||||
bb0: if ($[1] !== cond) {
|
||||
if (cond) {
|
||||
items = [];
|
||||
} else {
|
||||
break bb0;
|
||||
}
|
||||
if ($[1] !== cond) {
|
||||
bb0: {
|
||||
if (cond) {
|
||||
items = [];
|
||||
} else {
|
||||
break bb0;
|
||||
}
|
||||
|
||||
items.push(2);
|
||||
items.push(2);
|
||||
}
|
||||
$[1] = cond;
|
||||
$[2] = items;
|
||||
} else {
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
|
||||
import {useMemo} from 'react';
|
||||
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const result = useMemo(
|
||||
() => makeObject(props.value).value + 1,
|
||||
[props.value]
|
||||
);
|
||||
console.log(result);
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
function makeObject(value) {
|
||||
console.log(value);
|
||||
return {value};
|
||||
}
|
||||
|
||||
export const TODO_FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [
|
||||
{value: 42},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
{value: 3.14},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
|
||||
import { useMemo } from "react";
|
||||
import { makeObject_Primitives, ValidateMemoization } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const result = makeObject(props.value).value + 1;
|
||||
|
||||
console.log(result);
|
||||
return "ok";
|
||||
}
|
||||
|
||||
function makeObject(value) {
|
||||
console.log(value);
|
||||
return { value };
|
||||
}
|
||||
|
||||
export const TODO_FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 42 }],
|
||||
sequentialRenders: [
|
||||
{ value: 42 },
|
||||
{ value: 42 },
|
||||
{ value: 3.14 },
|
||||
{ value: 3.14 },
|
||||
{ value: 42 },
|
||||
{ value: 3.14 },
|
||||
{ value: 42 },
|
||||
{ value: 3.14 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
|
||||
import {useMemo} from 'react';
|
||||
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const result = useMemo(
|
||||
() => makeObject(props.value).value + 1,
|
||||
[props.value]
|
||||
);
|
||||
console.log(result);
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
function makeObject(value) {
|
||||
console.log(value);
|
||||
return {value};
|
||||
}
|
||||
|
||||
export const TODO_FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [
|
||||
{value: 42},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
{value: 3.14},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
],
|
||||
};
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
|
||||
import {useMemo} from 'react';
|
||||
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const result = makeObject(props.value).value + 1;
|
||||
console.log(result);
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
function makeObject(value) {
|
||||
console.log(value);
|
||||
return {value};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [
|
||||
{value: 42},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
{value: 3.14},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
|
||||
import { useMemo } from "react";
|
||||
import { makeObject_Primitives, ValidateMemoization } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const result = makeObject(props.value).value + 1;
|
||||
console.log(result);
|
||||
return "ok";
|
||||
}
|
||||
|
||||
function makeObject(value) {
|
||||
console.log(value);
|
||||
return { value };
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 42 }],
|
||||
sequentialRenders: [
|
||||
{ value: 42 },
|
||||
{ value: 42 },
|
||||
{ value: 3.14 },
|
||||
{ value: 3.14 },
|
||||
{ value: 42 },
|
||||
{ value: 3.14 },
|
||||
{ value: 42 },
|
||||
{ value: 3.14 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) "ok"
|
||||
"ok"
|
||||
"ok"
|
||||
"ok"
|
||||
"ok"
|
||||
"ok"
|
||||
"ok"
|
||||
"ok"
|
||||
logs: [42,43,42,43,3.14,4.140000000000001,3.14,4.140000000000001,42,43,3.14,4.140000000000001,42,43,3.14,4.140000000000001]
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
|
||||
import {useMemo} from 'react';
|
||||
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const result = makeObject(props.value).value + 1;
|
||||
console.log(result);
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
function makeObject(value) {
|
||||
console.log(value);
|
||||
return {value};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [
|
||||
{value: 42},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
{value: 3.14},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
],
|
||||
};
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
|
||||
import {useMemo} from 'react';
|
||||
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const result = useMemo(() => {
|
||||
return makeObject(props.value).value + 1;
|
||||
}, [props.value]);
|
||||
return <ValidateMemoization inputs={[props.value]} output={result} />;
|
||||
}
|
||||
|
||||
function makeObject(value) {
|
||||
console.log(value);
|
||||
return {value};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [
|
||||
{value: 42},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
{value: 3.14},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
|
||||
import { useMemo } from "react";
|
||||
import { makeObject_Primitives, ValidateMemoization } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(7);
|
||||
let t0;
|
||||
if ($[0] !== props.value) {
|
||||
t0 = makeObject(props.value);
|
||||
$[0] = props.value;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const result = t0.value + 1;
|
||||
let t1;
|
||||
if ($[2] !== props.value) {
|
||||
t1 = [props.value];
|
||||
$[2] = props.value;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
let t2;
|
||||
if ($[4] !== result || $[5] !== t1) {
|
||||
t2 = <ValidateMemoization inputs={t1} output={result} />;
|
||||
$[4] = result;
|
||||
$[5] = t1;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
function makeObject(value) {
|
||||
console.log(value);
|
||||
return { value };
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 42 }],
|
||||
sequentialRenders: [
|
||||
{ value: 42 },
|
||||
{ value: 42 },
|
||||
{ value: 3.14 },
|
||||
{ value: 3.14 },
|
||||
{ value: 42 },
|
||||
{ value: 3.14 },
|
||||
{ value: 42 },
|
||||
{ value: 3.14 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"inputs":[42],"output":43}</div>
|
||||
<div>{"inputs":[42],"output":43}</div>
|
||||
<div>{"inputs":[3.14],"output":4.140000000000001}</div>
|
||||
<div>{"inputs":[3.14],"output":4.140000000000001}</div>
|
||||
<div>{"inputs":[42],"output":43}</div>
|
||||
<div>{"inputs":[3.14],"output":4.140000000000001}</div>
|
||||
<div>{"inputs":[42],"output":43}</div>
|
||||
<div>{"inputs":[3.14],"output":4.140000000000001}</div>
|
||||
logs: [42,3.14,42,3.14,42,3.14]
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
|
||||
import {useMemo} from 'react';
|
||||
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const result = useMemo(() => {
|
||||
return makeObject(props.value).value + 1;
|
||||
}, [props.value]);
|
||||
return <ValidateMemoization inputs={[props.value]} output={result} />;
|
||||
}
|
||||
|
||||
function makeObject(value) {
|
||||
console.log(value);
|
||||
return {value};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [
|
||||
{value: 42},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
{value: 3.14},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
{value: 42},
|
||||
{value: 3.14},
|
||||
],
|
||||
};
|
||||
+2
-1
@@ -2,6 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
import {makeArray} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
@@ -30,7 +31,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer"
|
||||
import { makeArray } from "shared-runtime";
|
||||
|
||||
function Component() {
|
||||
|
||||
+1
@@ -1,3 +1,4 @@
|
||||
// @compilationMode:"infer"
|
||||
import {makeArray} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
|
||||
+10
-9
@@ -49,12 +49,12 @@ import {
|
||||
} from "shared-runtime";
|
||||
|
||||
function useFoo(t0) {
|
||||
const $ = _c(3);
|
||||
const $ = _c(4);
|
||||
const { data } = t0;
|
||||
let obj;
|
||||
let myDiv = null;
|
||||
bb0: if (data.cond) {
|
||||
if ($[0] !== data.cond1) {
|
||||
if ($[0] !== data.cond || $[1] !== data.cond1) {
|
||||
bb0: if (data.cond) {
|
||||
obj = makeObject_Primitives();
|
||||
if (data.cond1) {
|
||||
myDiv = <Stringify value={mutateAndReturn(obj)} />;
|
||||
@@ -62,13 +62,14 @@ function useFoo(t0) {
|
||||
}
|
||||
|
||||
mutate(obj);
|
||||
$[0] = data.cond1;
|
||||
$[1] = obj;
|
||||
$[2] = myDiv;
|
||||
} else {
|
||||
obj = $[1];
|
||||
myDiv = $[2];
|
||||
}
|
||||
$[0] = data.cond;
|
||||
$[1] = data.cond1;
|
||||
$[2] = obj;
|
||||
$[3] = myDiv;
|
||||
} else {
|
||||
obj = $[2];
|
||||
myDiv = $[3];
|
||||
}
|
||||
return myDiv;
|
||||
}
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enablePreserveExistingMemoizationGuarantees
|
||||
import {fbt} from 'fbt';
|
||||
|
||||
function Component() {
|
||||
const buttonLabel = () => {
|
||||
if (!someCondition) {
|
||||
return <fbt desc="My label">{'Purchase as a gift'}</fbt>;
|
||||
} else if (
|
||||
!iconOnly &&
|
||||
showPrice &&
|
||||
item?.current_gift_offer?.price?.formatted != null
|
||||
) {
|
||||
return (
|
||||
<fbt desc="Gift button's label">
|
||||
{'Gift | '}
|
||||
<fbt:param name="price">
|
||||
{item?.current_gift_offer?.price?.formatted}
|
||||
</fbt:param>
|
||||
</fbt>
|
||||
);
|
||||
} else if (!iconOnly && !showPrice) {
|
||||
return <fbt desc="Gift button's label">{'Gift'}</fbt>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Button text={buttonLabel()} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees
|
||||
import { fbt } from "fbt";
|
||||
|
||||
function Component() {
|
||||
const $ = _c(1);
|
||||
const buttonLabel = _temp;
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = (
|
||||
<View>
|
||||
<Button text={buttonLabel()} />
|
||||
</View>
|
||||
);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
function _temp() {
|
||||
if (!someCondition) {
|
||||
return fbt._("Purchase as a gift", null, { hk: "1gHj4g" });
|
||||
} else {
|
||||
if (
|
||||
!iconOnly &&
|
||||
showPrice &&
|
||||
item?.current_gift_offer?.price?.formatted != null
|
||||
) {
|
||||
return fbt._(
|
||||
"Gift | {price}",
|
||||
[fbt._param("price", item?.current_gift_offer?.price?.formatted)],
|
||||
{ hk: "3GTnGE" },
|
||||
);
|
||||
} else {
|
||||
if (!iconOnly && !showPrice) {
|
||||
return fbt._("Gift", null, { hk: "3fqfrk" });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// @enablePreserveExistingMemoizationGuarantees
|
||||
import {fbt} from 'fbt';
|
||||
|
||||
function Component() {
|
||||
const buttonLabel = () => {
|
||||
if (!someCondition) {
|
||||
return <fbt desc="My label">{'Purchase as a gift'}</fbt>;
|
||||
} else if (
|
||||
!iconOnly &&
|
||||
showPrice &&
|
||||
item?.current_gift_offer?.price?.formatted != null
|
||||
) {
|
||||
return (
|
||||
<fbt desc="Gift button's label">
|
||||
{'Gift | '}
|
||||
<fbt:param name="price">
|
||||
{item?.current_gift_offer?.price?.formatted}
|
||||
</fbt:param>
|
||||
</fbt>
|
||||
);
|
||||
} else if (!iconOnly && !showPrice) {
|
||||
return <fbt desc="Gift button's label">{'Gift'}</fbt>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Button text={buttonLabel()} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
+15
-17
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableForest
|
||||
// @enablePreserveExistingMemoizationGuarantees
|
||||
function Component({base, start, increment, test}) {
|
||||
let value = base;
|
||||
for (let i = start; i < test; i += increment) {
|
||||
@@ -27,25 +27,23 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableForest
|
||||
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const $ = _c(2);
|
||||
const { base, start, increment, test } = t0;
|
||||
let value;
|
||||
if ($[0] !== base || $[1] !== increment || $[2] !== start || $[3] !== test) {
|
||||
value = base;
|
||||
for (let i = start; i < test; i = i + increment, i) {
|
||||
value = value + i;
|
||||
}
|
||||
$[0] = base;
|
||||
$[1] = increment;
|
||||
$[2] = start;
|
||||
$[3] = test;
|
||||
$[4] = value;
|
||||
} else {
|
||||
value = $[4];
|
||||
let value = base;
|
||||
for (let i = start; i < test; i = i + increment, i) {
|
||||
value = value + i;
|
||||
}
|
||||
return <div>{value}</div>;
|
||||
let t1;
|
||||
if ($[0] !== value) {
|
||||
t1 = <div>{value}</div>;
|
||||
$[0] = value;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// @enableForest
|
||||
// @enablePreserveExistingMemoizationGuarantees
|
||||
function Component({base, start, increment, test}) {
|
||||
let value = base;
|
||||
for (let i = start; i < test; i += increment) {
|
||||
|
||||
+17
-20
@@ -34,17 +34,16 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
|
||||
import { useMemo } from "react";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(6);
|
||||
const $ = _c(5);
|
||||
let t0;
|
||||
bb0: {
|
||||
let y;
|
||||
if (
|
||||
$[0] !== props.a ||
|
||||
$[1] !== props.b ||
|
||||
$[2] !== props.cond ||
|
||||
$[3] !== props.cond2
|
||||
) {
|
||||
y = [];
|
||||
if (
|
||||
$[0] !== props.a ||
|
||||
$[1] !== props.b ||
|
||||
$[2] !== props.cond ||
|
||||
$[3] !== props.cond2
|
||||
) {
|
||||
bb0: {
|
||||
const y = [];
|
||||
if (props.cond) {
|
||||
y.push(props.a);
|
||||
}
|
||||
@@ -54,17 +53,15 @@ function Component(props) {
|
||||
}
|
||||
|
||||
y.push(props.b);
|
||||
$[0] = props.a;
|
||||
$[1] = props.b;
|
||||
$[2] = props.cond;
|
||||
$[3] = props.cond2;
|
||||
$[4] = y;
|
||||
$[5] = t0;
|
||||
} else {
|
||||
y = $[4];
|
||||
t0 = $[5];
|
||||
t0 = y;
|
||||
}
|
||||
t0 = y;
|
||||
$[0] = props.a;
|
||||
$[1] = props.b;
|
||||
$[2] = props.cond;
|
||||
$[3] = props.cond2;
|
||||
$[4] = t0;
|
||||
} else {
|
||||
t0 = $[4];
|
||||
}
|
||||
const x = t0;
|
||||
return x;
|
||||
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useMemo} from 'react';
|
||||
import {
|
||||
makeObject_Primitives,
|
||||
mutate,
|
||||
Stringify,
|
||||
ValidateMemoization,
|
||||
} from 'shared-runtime';
|
||||
|
||||
function Component({cond}) {
|
||||
const memoized = useMemo(() => {
|
||||
const value = makeObject_Primitives();
|
||||
if (cond) {
|
||||
return value;
|
||||
} else {
|
||||
mutate(value);
|
||||
return value;
|
||||
}
|
||||
}, [cond]);
|
||||
return <ValidateMemoization inputs={[cond]} output={memoized} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{cond: false}],
|
||||
sequentialRenders: [
|
||||
{cond: false},
|
||||
{cond: false},
|
||||
{cond: true},
|
||||
{cond: true},
|
||||
{cond: false},
|
||||
{cond: true},
|
||||
{cond: false},
|
||||
{cond: true},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
makeObject_Primitives,
|
||||
mutate,
|
||||
Stringify,
|
||||
ValidateMemoization,
|
||||
} from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(7);
|
||||
const { cond } = t0;
|
||||
let t1;
|
||||
if ($[0] !== cond) {
|
||||
const value = makeObject_Primitives();
|
||||
if (cond) {
|
||||
t1 = value;
|
||||
} else {
|
||||
mutate(value);
|
||||
t1 = value;
|
||||
}
|
||||
$[0] = cond;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const memoized = t1;
|
||||
let t2;
|
||||
if ($[2] !== cond) {
|
||||
t2 = [cond];
|
||||
$[2] = cond;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
let t3;
|
||||
if ($[4] !== memoized || $[5] !== t2) {
|
||||
t3 = <ValidateMemoization inputs={t2} output={memoized} />;
|
||||
$[4] = memoized;
|
||||
$[5] = t2;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ cond: false }],
|
||||
sequentialRenders: [
|
||||
{ cond: false },
|
||||
{ cond: false },
|
||||
{ cond: true },
|
||||
{ cond: true },
|
||||
{ cond: false },
|
||||
{ cond: true },
|
||||
{ cond: false },
|
||||
{ cond: true },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"inputs":[false],"output":{"a":0,"b":"value1","c":true,"wat0":"joe"}}</div>
|
||||
<div>{"inputs":[false],"output":{"a":0,"b":"value1","c":true,"wat0":"joe"}}</div>
|
||||
<div>{"inputs":[true],"output":{"a":0,"b":"value1","c":true}}</div>
|
||||
<div>{"inputs":[true],"output":{"a":0,"b":"value1","c":true}}</div>
|
||||
<div>{"inputs":[false],"output":{"a":0,"b":"value1","c":true,"wat0":"joe"}}</div>
|
||||
<div>{"inputs":[true],"output":{"a":0,"b":"value1","c":true}}</div>
|
||||
<div>{"inputs":[false],"output":{"a":0,"b":"value1","c":true,"wat0":"joe"}}</div>
|
||||
<div>{"inputs":[true],"output":{"a":0,"b":"value1","c":true}}</div>
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import {useMemo} from 'react';
|
||||
import {
|
||||
makeObject_Primitives,
|
||||
mutate,
|
||||
Stringify,
|
||||
ValidateMemoization,
|
||||
} from 'shared-runtime';
|
||||
|
||||
function Component({cond}) {
|
||||
const memoized = useMemo(() => {
|
||||
const value = makeObject_Primitives();
|
||||
if (cond) {
|
||||
return value;
|
||||
} else {
|
||||
mutate(value);
|
||||
return value;
|
||||
}
|
||||
}, [cond]);
|
||||
return <ValidateMemoization inputs={[cond]} output={memoized} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{cond: false}],
|
||||
sequentialRenders: [
|
||||
{cond: false},
|
||||
{cond: false},
|
||||
{cond: true},
|
||||
{cond: true},
|
||||
{cond: false},
|
||||
{cond: true},
|
||||
{cond: false},
|
||||
{cond: true},
|
||||
],
|
||||
};
|
||||
+17
-20
@@ -33,17 +33,16 @@ import { c as _c } from "react/compiler-runtime";
|
||||
import { useMemo } from "react";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(6);
|
||||
const $ = _c(5);
|
||||
let t0;
|
||||
bb0: {
|
||||
let y;
|
||||
if (
|
||||
$[0] !== props.a ||
|
||||
$[1] !== props.b ||
|
||||
$[2] !== props.cond ||
|
||||
$[3] !== props.cond2
|
||||
) {
|
||||
y = [];
|
||||
if (
|
||||
$[0] !== props.a ||
|
||||
$[1] !== props.b ||
|
||||
$[2] !== props.cond ||
|
||||
$[3] !== props.cond2
|
||||
) {
|
||||
bb0: {
|
||||
const y = [];
|
||||
if (props.cond) {
|
||||
y.push(props.a);
|
||||
}
|
||||
@@ -53,17 +52,15 @@ function Component(props) {
|
||||
}
|
||||
|
||||
y.push(props.b);
|
||||
$[0] = props.a;
|
||||
$[1] = props.b;
|
||||
$[2] = props.cond;
|
||||
$[3] = props.cond2;
|
||||
$[4] = y;
|
||||
$[5] = t0;
|
||||
} else {
|
||||
y = $[4];
|
||||
t0 = $[5];
|
||||
t0 = y;
|
||||
}
|
||||
t0 = y;
|
||||
$[0] = props.a;
|
||||
$[1] = props.b;
|
||||
$[2] = props.cond;
|
||||
$[3] = props.cond2;
|
||||
$[4] = t0;
|
||||
} else {
|
||||
t0 = $[4];
|
||||
}
|
||||
const x = t0;
|
||||
return x;
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ const contentScriptsToInject = [
|
||||
js: ['build/proxy.js'],
|
||||
matches: ['<all_urls>'],
|
||||
persistAcrossSessions: true,
|
||||
runAt: 'document_end',
|
||||
runAt: 'document_start',
|
||||
world: chrome.scripting.ExecutionWorld.ISOLATED,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
/* global chrome */
|
||||
|
||||
export function executeScriptInIsolatedWorld({target, files}) {
|
||||
export function executeScriptInIsolatedWorld({
|
||||
target,
|
||||
files,
|
||||
}: {
|
||||
files: any,
|
||||
target: any,
|
||||
}): Promise<void> {
|
||||
return chrome.scripting.executeScript({
|
||||
target,
|
||||
files,
|
||||
@@ -8,10 +22,20 @@ export function executeScriptInIsolatedWorld({target, files}) {
|
||||
});
|
||||
}
|
||||
|
||||
export function executeScriptInMainWorld({target, files}) {
|
||||
export function executeScriptInMainWorld({
|
||||
target,
|
||||
files,
|
||||
injectImmediately,
|
||||
}: {
|
||||
files: any,
|
||||
target: any,
|
||||
// It's nice to have this required to make active choices.
|
||||
injectImmediately: boolean,
|
||||
}): Promise<void> {
|
||||
return chrome.scripting.executeScript({
|
||||
target,
|
||||
files,
|
||||
injectImmediately,
|
||||
world: chrome.scripting.ExecutionWorld.MAIN,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* global chrome */
|
||||
|
||||
import {__DEBUG__} from 'react-devtools-shared/src/constants';
|
||||
import setExtensionIconAndPopup from './setExtensionIconAndPopup';
|
||||
import {executeScriptInMainWorld} from './executeScript';
|
||||
|
||||
@@ -25,6 +26,7 @@ export function handleBackendManagerMessage(message, sender) {
|
||||
payload.versions.forEach(version => {
|
||||
if (EXTENSION_CONTAINED_VERSIONS.includes(version)) {
|
||||
executeScriptInMainWorld({
|
||||
injectImmediately: true,
|
||||
target: {tabId: sender.tab.id},
|
||||
files: [`/build/react_devtools_backend_${version}.js`],
|
||||
});
|
||||
@@ -79,9 +81,19 @@ export function handleDevToolsPageMessage(message) {
|
||||
}
|
||||
|
||||
executeScriptInMainWorld({
|
||||
injectImmediately: true,
|
||||
target: {tabId},
|
||||
files: ['/build/backendManager.js'],
|
||||
});
|
||||
}).then(
|
||||
() => {
|
||||
if (__DEBUG__) {
|
||||
console.log('Successfully injected backend manager');
|
||||
}
|
||||
},
|
||||
reason => {
|
||||
console.error('Failed to inject backend manager:', reason);
|
||||
},
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
/* global chrome */
|
||||
|
||||
'use strict';
|
||||
|
||||
window.addEventListener('pageshow', function ({target}) {
|
||||
function injectProxy({target}: {target: any}) {
|
||||
// Firefox's behaviour for injecting this content script can be unpredictable
|
||||
// While navigating the history, some content scripts might not be re-injected and still be alive
|
||||
if (!window.__REACT_DEVTOOLS_PROXY_INJECTED__) {
|
||||
@@ -14,7 +22,7 @@ window.addEventListener('pageshow', function ({target}) {
|
||||
// The backend waits to install the global hook until notified by the content script.
|
||||
// In the event of a page reload, the content script might be loaded before the backend manager is injected.
|
||||
// Because of this we need to poll the backend manager until it has been initialized.
|
||||
const intervalID = setInterval(() => {
|
||||
const intervalID: IntervalID = setInterval(() => {
|
||||
if (backendInitialized) {
|
||||
clearInterval(intervalID);
|
||||
} else {
|
||||
@@ -22,7 +30,11 @@ window.addEventListener('pageshow', function ({target}) {
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('pagereveal', injectProxy);
|
||||
// For backwards compat with browsers not implementing `pagereveal` which is a fairly new event.
|
||||
window.addEventListener('pageshow', injectProxy);
|
||||
|
||||
window.addEventListener('pagehide', function ({target}) {
|
||||
if (target !== window.document) {
|
||||
@@ -45,7 +57,7 @@ function sayHelloToBackendManager() {
|
||||
);
|
||||
}
|
||||
|
||||
function handleMessageFromDevtools(message) {
|
||||
function handleMessageFromDevtools(message: any) {
|
||||
window.postMessage(
|
||||
{
|
||||
source: 'react-devtools-content-script',
|
||||
@@ -55,7 +67,7 @@ function handleMessageFromDevtools(message) {
|
||||
);
|
||||
}
|
||||
|
||||
function handleMessageFromPage(event) {
|
||||
function handleMessageFromPage(event: any) {
|
||||
if (event.source !== window || !event.data) {
|
||||
return;
|
||||
}
|
||||
@@ -65,6 +77,7 @@ function handleMessageFromPage(event) {
|
||||
case 'react-devtools-bridge': {
|
||||
backendInitialized = true;
|
||||
|
||||
// $FlowFixMe[incompatible-use]
|
||||
port.postMessage(event.data.payload);
|
||||
break;
|
||||
}
|
||||
@@ -99,6 +112,8 @@ function connectPort() {
|
||||
|
||||
window.addEventListener('message', handleMessageFromPage);
|
||||
|
||||
// $FlowFixMe[incompatible-use]
|
||||
port.onMessage.addListener(handleMessageFromDevtools);
|
||||
// $FlowFixMe[incompatible-use]
|
||||
port.onDisconnect.addListener(handleDisconnect);
|
||||
}
|
||||
|
||||
@@ -987,6 +987,34 @@ describe('Store', () => {
|
||||
<Suspense name="two" rects={[{x:1,y:2,width:5,height:1}]}>
|
||||
<Suspense name="three" rects={[{x:1,y:2,width:5,height:1}]}>
|
||||
`);
|
||||
|
||||
await actAsync(() => {
|
||||
agent.overrideSuspenseMilestone({
|
||||
rendererID,
|
||||
rootID,
|
||||
suspendedSet: [],
|
||||
});
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
<Component key="Outside">
|
||||
▾ <Suspense name="parent">
|
||||
<Component key="Unrelated at Start">
|
||||
▾ <Suspense name="one">
|
||||
<Component key="Suspense 1 Content">
|
||||
▾ <Suspense name="two">
|
||||
<Component key="Suspense 2 Content">
|
||||
▾ <Suspense name="three">
|
||||
<Component key="Suspense 3 Content">
|
||||
<Component key="Unrelated at End">
|
||||
[shell]
|
||||
<Suspense name="parent" rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}>
|
||||
<Suspense name="one" rects={[{x:1,y:2,width:5,height:1}]}>
|
||||
<Suspense name="two" rects={[{x:1,y:2,width:5,height:1}]}>
|
||||
<Suspense name="three" rects={[{x:1,y:2,width:5,height:1}]}>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should display a partially rendered SuspenseList', async () => {
|
||||
|
||||
@@ -7519,6 +7519,9 @@ export function attach(
|
||||
}
|
||||
|
||||
// TODO: Allow overriding the timeline for the specified root.
|
||||
forceFallbackForFibers.forEach(fiber => {
|
||||
scheduleUpdate(fiber);
|
||||
});
|
||||
forceFallbackForFibers.clear();
|
||||
|
||||
for (let i = 0; i < suspendedSet.length; ++i) {
|
||||
|
||||
+14
-1
@@ -23,7 +23,7 @@ type Props = {
|
||||
symbolicatedSourcePromise: Promise<SourceMappedLocation | null>,
|
||||
};
|
||||
|
||||
function OpenInEditorButton({
|
||||
function OpenSymbolicatedSourceInEditorButton({
|
||||
editorURL,
|
||||
source,
|
||||
symbolicatedSourcePromise,
|
||||
@@ -45,4 +45,17 @@ function OpenInEditorButton({
|
||||
);
|
||||
}
|
||||
|
||||
function OpenInEditorButton(props: Props): React.Node {
|
||||
return (
|
||||
<React.Suspense
|
||||
fallback={
|
||||
<Button disabled={true} title="retrieving original source…">
|
||||
<ButtonIcon type="editor" />
|
||||
</Button>
|
||||
}>
|
||||
<OpenSymbolicatedSourceInEditorButton {...props} />
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
export default OpenInEditorButton;
|
||||
|
||||
+23
-22
@@ -45,10 +45,9 @@ function getSuspendableDocumentOrderSuspense(
|
||||
if (current === undefined) {
|
||||
continue;
|
||||
}
|
||||
// Don't include the root. It's currently not supported to suspend the shell.
|
||||
if (current !== suspense) {
|
||||
suspenseTreeList.push(current);
|
||||
}
|
||||
// Include the root even if we won't suspend it.
|
||||
// You should be able to see what suspended the shell.
|
||||
suspenseTreeList.push(current);
|
||||
// Add children in reverse order to maintain document order
|
||||
for (let j = current.children.length - 1; j >= 0; j--) {
|
||||
const childSuspense = store.getSuspenseByID(current.children[j]);
|
||||
@@ -126,25 +125,27 @@ function SuspenseTimelineInput({rootID}: {rootID: Element['id'] | void}) {
|
||||
}
|
||||
|
||||
function handleChange(event: SyntheticEvent) {
|
||||
const pendingValue = +event.currentTarget.value;
|
||||
for (let i = 0; i < timeline.length; i++) {
|
||||
const forceFallback = i > pendingValue;
|
||||
const suspense = timeline[i];
|
||||
const elementID = suspense.id;
|
||||
const rendererID = store.getRendererIDForElement(elementID);
|
||||
if (rendererID === null) {
|
||||
// TODO: Handle disconnected elements.
|
||||
console.warn(
|
||||
`No renderer ID found for element ${elementID} in suspense timeline.`,
|
||||
);
|
||||
} else {
|
||||
bridge.send('overrideSuspense', {
|
||||
id: elementID,
|
||||
rendererID,
|
||||
forceFallback,
|
||||
});
|
||||
}
|
||||
if (rootID === undefined) {
|
||||
return;
|
||||
}
|
||||
const rendererID = store.getRendererIDForElement(rootID);
|
||||
if (rendererID === null) {
|
||||
console.error(
|
||||
`No renderer ID found for root element ${rootID} in suspense timeline.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingValue = +event.currentTarget.value;
|
||||
const suspendedSet = timeline
|
||||
.slice(pendingValue)
|
||||
.map(suspense => suspense.id);
|
||||
|
||||
bridge.send('overrideSuspenseMilestone', {
|
||||
rendererID,
|
||||
rootID,
|
||||
suspendedSet,
|
||||
});
|
||||
|
||||
const suspense = timeline[pendingValue];
|
||||
const elementID = suspense.id;
|
||||
|
||||
+17
-3
@@ -73,6 +73,7 @@ import {
|
||||
includesSomeLane,
|
||||
isGestureRender,
|
||||
GestureLane,
|
||||
UpdateLanes,
|
||||
} from './ReactFiberLane';
|
||||
import {
|
||||
ContinuousEventPriority,
|
||||
@@ -2983,6 +2984,20 @@ function rerenderDeferredValue<T>(value: T, initialValue?: T): T {
|
||||
}
|
||||
}
|
||||
|
||||
function isRenderingDeferredWork(): boolean {
|
||||
if (!includesSomeLane(renderLanes, DeferredLane)) {
|
||||
// None of the render lanes are deferred lanes.
|
||||
return false;
|
||||
}
|
||||
// At least one of the render lanes are deferred lanes. However, if the
|
||||
// current render is also batched together with an update, then we can't
|
||||
// say that the render is wholly the result of deferred work. We can check
|
||||
// this by checking if the root render lanes contain any "update" lanes, i.e.
|
||||
// lanes that are only assigned to updates, like setState.
|
||||
const rootRenderLanes = getWorkInProgressRootRenderLanes();
|
||||
return !includesSomeLane(rootRenderLanes, UpdateLanes);
|
||||
}
|
||||
|
||||
function mountDeferredValueImpl<T>(hook: Hook, value: T, initialValue?: T): T {
|
||||
if (
|
||||
// When `initialValue` is provided, we defer the initial render even if the
|
||||
@@ -2991,7 +3006,7 @@ function mountDeferredValueImpl<T>(hook: Hook, value: T, initialValue?: T): T {
|
||||
// However, to avoid waterfalls, we do not defer if this render
|
||||
// was itself spawned by an earlier useDeferredValue. Check if DeferredLane
|
||||
// is part of the render lanes.
|
||||
!includesSomeLane(renderLanes, DeferredLane)
|
||||
!isRenderingDeferredWork()
|
||||
) {
|
||||
// Render with the initial value
|
||||
hook.memoizedState = initialValue;
|
||||
@@ -3038,8 +3053,7 @@ function updateDeferredValueImpl<T>(
|
||||
}
|
||||
|
||||
const shouldDeferValue =
|
||||
!includesOnlyNonUrgentLanes(renderLanes) &&
|
||||
!includesSomeLane(renderLanes, DeferredLane);
|
||||
!includesOnlyNonUrgentLanes(renderLanes) && !isRenderingDeferredWork();
|
||||
if (shouldDeferValue) {
|
||||
// This is an urgent update. Since the value has changed, keep using the
|
||||
// previous value and spawn a deferred render to update it later.
|
||||
|
||||
+41
-8
@@ -73,6 +73,20 @@ const TransitionLane12: Lane = /* */ 0b0000000000010000000
|
||||
const TransitionLane13: Lane = /* */ 0b0000000000100000000000000000000;
|
||||
const TransitionLane14: Lane = /* */ 0b0000000001000000000000000000000;
|
||||
|
||||
const TransitionUpdateLanes =
|
||||
TransitionLane1 |
|
||||
TransitionLane2 |
|
||||
TransitionLane3 |
|
||||
TransitionLane4 |
|
||||
TransitionLane5 |
|
||||
TransitionLane6 |
|
||||
TransitionLane7 |
|
||||
TransitionLane8 |
|
||||
TransitionLane9 |
|
||||
TransitionLane10;
|
||||
const TransitionDeferredLanes =
|
||||
TransitionLane11 | TransitionLane12 | TransitionLane13 | TransitionLane14;
|
||||
|
||||
const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000;
|
||||
const RetryLane1: Lane = /* */ 0b0000000010000000000000000000000;
|
||||
const RetryLane2: Lane = /* */ 0b0000000100000000000000000000000;
|
||||
@@ -94,7 +108,7 @@ export const DeferredLane: Lane = /* */ 0b1000000000000000000
|
||||
// Any lane that might schedule an update. This is used to detect infinite
|
||||
// update loops, so it doesn't include hydration lanes or retries.
|
||||
export const UpdateLanes: Lanes =
|
||||
SyncLane | InputContinuousLane | DefaultLane | TransitionLanes;
|
||||
SyncLane | InputContinuousLane | DefaultLane | TransitionUpdateLanes;
|
||||
|
||||
export const HydrationLanes =
|
||||
SyncHydrationLane |
|
||||
@@ -155,7 +169,8 @@ export function getLabelForLane(lane: Lane): string | void {
|
||||
|
||||
export const NoTimestamp = -1;
|
||||
|
||||
let nextTransitionLane: Lane = TransitionLane1;
|
||||
let nextTransitionUpdateLane: Lane = TransitionLane1;
|
||||
let nextTransitionDeferredLane: Lane = TransitionLane11;
|
||||
let nextRetryLane: Lane = RetryLane1;
|
||||
|
||||
function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
|
||||
@@ -190,11 +205,12 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
|
||||
case TransitionLane8:
|
||||
case TransitionLane9:
|
||||
case TransitionLane10:
|
||||
return lanes & TransitionUpdateLanes;
|
||||
case TransitionLane11:
|
||||
case TransitionLane12:
|
||||
case TransitionLane13:
|
||||
case TransitionLane14:
|
||||
return lanes & TransitionLanes;
|
||||
return lanes & TransitionDeferredLanes;
|
||||
case RetryLane1:
|
||||
case RetryLane2:
|
||||
case RetryLane3:
|
||||
@@ -679,14 +695,23 @@ export function isGestureRender(lanes: Lanes): boolean {
|
||||
return lanes === GestureLane;
|
||||
}
|
||||
|
||||
export function claimNextTransitionLane(): Lane {
|
||||
export function claimNextTransitionUpdateLane(): Lane {
|
||||
// Cycle through the lanes, assigning each new transition to the next lane.
|
||||
// In most cases, this means every transition gets its own lane, until we
|
||||
// run out of lanes and cycle back to the beginning.
|
||||
const lane = nextTransitionLane;
|
||||
nextTransitionLane <<= 1;
|
||||
if ((nextTransitionLane & TransitionLanes) === NoLanes) {
|
||||
nextTransitionLane = TransitionLane1;
|
||||
const lane = nextTransitionUpdateLane;
|
||||
nextTransitionUpdateLane <<= 1;
|
||||
if ((nextTransitionUpdateLane & TransitionUpdateLanes) === NoLanes) {
|
||||
nextTransitionUpdateLane = TransitionLane1;
|
||||
}
|
||||
return lane;
|
||||
}
|
||||
|
||||
export function claimNextTransitionDeferredLane(): Lane {
|
||||
const lane = nextTransitionDeferredLane;
|
||||
nextTransitionDeferredLane <<= 1;
|
||||
if ((nextTransitionDeferredLane & TransitionDeferredLanes) === NoLanes) {
|
||||
nextTransitionDeferredLane = TransitionLane11;
|
||||
}
|
||||
return lane;
|
||||
}
|
||||
@@ -952,6 +977,14 @@ function markSpawnedDeferredLane(
|
||||
// Entangle the spawned lane with the DeferredLane bit so that we know it
|
||||
// was the result of another render. This lets us avoid a useDeferredValue
|
||||
// waterfall — only the first level will defer.
|
||||
// TODO: Now that there is a reserved set of transition lanes that are used
|
||||
// exclusively for deferred work, we should get rid of this special
|
||||
// DeferredLane bit; the same information can be inferred by checking whether
|
||||
// the lane is one of the TransitionDeferredLanes. The only reason this still
|
||||
// exists is because we need to also do the same for OffscreenLane. That
|
||||
// requires additional changes because there are more places around the
|
||||
// codebase that treat OffscreenLane as a magic value; would need to check
|
||||
// for a new OffscreenDeferredLane, too. Will leave this for a follow-up.
|
||||
const spawnedLaneIndex = laneToIndex(spawnedLane);
|
||||
root.entangledLanes |= spawnedLane;
|
||||
root.entanglements[spawnedLaneIndex] |=
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
getNextLanes,
|
||||
includesSyncLane,
|
||||
markStarvedLanesAsExpired,
|
||||
claimNextTransitionLane,
|
||||
claimNextTransitionUpdateLane,
|
||||
getNextLanesToFlushSync,
|
||||
checkIfRootIsPrerendering,
|
||||
isGestureRender,
|
||||
@@ -716,7 +716,7 @@ export function requestTransitionLane(
|
||||
: // We may or may not be inside an async action scope. If we are, this
|
||||
// is the first update in that scope. Either way, we need to get a
|
||||
// fresh transition lane.
|
||||
claimNextTransitionLane();
|
||||
claimNextTransitionUpdateLane();
|
||||
}
|
||||
return currentEventTransitionLane;
|
||||
}
|
||||
|
||||
+2
-2
@@ -192,7 +192,7 @@ import {
|
||||
OffscreenLane,
|
||||
SyncUpdateLanes,
|
||||
UpdateLanes,
|
||||
claimNextTransitionLane,
|
||||
claimNextTransitionDeferredLane,
|
||||
checkIfRootIsPrerendering,
|
||||
includesOnlyViewTransitionEligibleLanes,
|
||||
isGestureRender,
|
||||
@@ -827,7 +827,7 @@ export function requestDeferredLane(): Lane {
|
||||
workInProgressDeferredLane = OffscreenLane;
|
||||
} else {
|
||||
// Everything else is spawned as a transition.
|
||||
workInProgressDeferredLane = claimNextTransitionLane();
|
||||
workInProgressDeferredLane = claimNextTransitionDeferredLane();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -608,6 +608,48 @@ describe('ReactDeferredValue', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
"regression: useDeferredValue's initial value argument works even if an unrelated " +
|
||||
'transition is suspended',
|
||||
async () => {
|
||||
// Simulates a previous bug where a new useDeferredValue hook is mounted
|
||||
// while some unrelated transition is suspended. In the regression case,
|
||||
// the initial values was skipped/ignored.
|
||||
|
||||
function Content({text}) {
|
||||
return (
|
||||
<AsyncText text={useDeferredValue(text, `Preview ${text}...`)} />
|
||||
);
|
||||
}
|
||||
|
||||
function App({text}) {
|
||||
// Use a key to force a new Content instance to be mounted each time
|
||||
// the text changes.
|
||||
return <Content key={text} text={text} />;
|
||||
}
|
||||
|
||||
const root = ReactNoop.createRoot();
|
||||
|
||||
// Render a previous UI using useDeferredValue. Suspend on the
|
||||
// final value.
|
||||
resolveText('Preview A...');
|
||||
await act(() => startTransition(() => root.render(<App text="A" />)));
|
||||
assertLog(['Preview A...', 'Suspend! [A]']);
|
||||
|
||||
// While it's still suspended, update the UI to show a different screen
|
||||
// with a different preview value. We should be able to show the new
|
||||
// preview even though the previous transition never finished.
|
||||
resolveText('Preview B...');
|
||||
await act(() => startTransition(() => root.render(<App text="B" />)));
|
||||
assertLog(['Preview B...', 'Suspend! [B]']);
|
||||
|
||||
// Now finish loading the final value.
|
||||
await act(() => resolveText('B'));
|
||||
assertLog(['B']);
|
||||
expect(root).toMatchRenderedOutput('B');
|
||||
},
|
||||
);
|
||||
|
||||
it('avoids a useDeferredValue waterfall when separated by a Suspense boundary', async () => {
|
||||
// Same as the previous test but with a Suspense boundary separating the
|
||||
// two useDeferredValue hooks.
|
||||
|
||||
Reference in New Issue
Block a user