Merge c382e18ae5 into sapling-pr-archive-poteto

This commit is contained in:
lauren
2025-09-04 14:30:21 -04:00
committed by GitHub
36 changed files with 1028 additions and 132 deletions
+1
View File
@@ -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}
@@ -2119,7 +2119,7 @@ function computeSignatureForInstruction(
effects.push({
kind: 'Freeze',
value: operand,
reason: ValueReason.Other,
reason: ValueReason.HookCaptured,
});
}
}
@@ -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),
);
}
}
}
/*
@@ -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}] : [],
@@ -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 {
@@ -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
@@ -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},
],
};
@@ -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]
@@ -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},
],
};
@@ -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]
@@ -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,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,3 +1,4 @@
// @compilationMode:"infer"
import {makeArray} from 'shared-runtime';
function Component() {
@@ -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;
}
@@ -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
@@ -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>
);
}
@@ -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,4 +1,4 @@
// @enableForest
// @enablePreserveExistingMemoizationGuarantees
function Component({base, start, increment, test}) {
let value = base;
for (let i = start; i < test; i += increment) {
@@ -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;
@@ -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>
@@ -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},
],
};
@@ -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;
@@ -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) {
@@ -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;
@@ -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
View File
@@ -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
View File
@@ -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] |=
+2 -2
View File
@@ -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
View File
@@ -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.