Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
631b1cfc74 | ||
|
|
e1dc03492e | ||
|
|
90bee81902 | ||
|
|
75e78d243f | ||
|
|
5d24c64cc9 | ||
|
|
6b7e207cab | ||
|
|
d60f77a533 | ||
|
|
12bc60f509 | ||
|
|
ed023cfc73 | ||
|
|
a00ca6f6b5 | ||
|
|
888ea60d8e |
41
.github/workflows/runtime_prereleases.yml
vendored
41
.github/workflows/runtime_prereleases.yml
vendored
@@ -17,6 +17,17 @@ on:
|
||||
description: 'Whether to notify the team on Discord when the release fails. Useful if this workflow is called from an automation.'
|
||||
required: false
|
||||
type: boolean
|
||||
only_packages:
|
||||
description: Packages to publish (space separated)
|
||||
type: string
|
||||
skip_packages:
|
||||
description: Packages to NOT publish (space separated)
|
||||
type: string
|
||||
dry:
|
||||
required: true
|
||||
description: Dry run instead of publish?
|
||||
type: boolean
|
||||
default: true
|
||||
secrets:
|
||||
DISCORD_WEBHOOK_URL:
|
||||
description: 'Discord webhook URL to notify on failure. Only required if enableFailureNotification is true.'
|
||||
@@ -61,10 +72,36 @@ jobs:
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- run: yarn --cwd scripts/release install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- run: cp ./scripts/release/ci-npmrc ~/.npmrc
|
||||
- run: |
|
||||
GH_TOKEN=${{ secrets.GH_TOKEN }} scripts/release/prepare-release-from-ci.js --skipTests -r ${{ inputs.release_channel }} --commit=${{ inputs.commit_sha }}
|
||||
cp ./scripts/release/ci-npmrc ~/.npmrc
|
||||
scripts/release/publish.js --ci --tags ${{ inputs.dist_tag }}
|
||||
- name: Check prepared files
|
||||
run: ls -R build/node_modules
|
||||
- if: '${{ inputs.only_packages }}'
|
||||
name: 'Publish ${{ inputs.only_packages }}'
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
--ci \
|
||||
--skipTests \
|
||||
--tags=${{ inputs.dist_tag }} \
|
||||
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry'}}
|
||||
- if: '${{ inputs.skip_packages }}'
|
||||
name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
--ci \
|
||||
--skipTests \
|
||||
--tags=${{ inputs.dist_tag }} \
|
||||
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry'}}
|
||||
- if: '${{ !(inputs.skip_packages && inputs.only_packages) }}'
|
||||
name: 'Publish all packages'
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
--ci \
|
||||
--tags=${{ inputs.dist_tag }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry'}}
|
||||
- name: Notify Discord on failure
|
||||
if: failure() && inputs.enableFailureNotification == true
|
||||
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
|
||||
|
||||
45
.github/workflows/runtime_prereleases_manual.yml
vendored
45
.github/workflows/runtime_prereleases_manual.yml
vendored
@@ -5,6 +5,25 @@ on:
|
||||
inputs:
|
||||
prerelease_commit_sha:
|
||||
required: true
|
||||
only_packages:
|
||||
description: Packages to publish (space separated)
|
||||
type: string
|
||||
skip_packages:
|
||||
description: Packages to NOT publish (space separated)
|
||||
type: string
|
||||
dry:
|
||||
required: true
|
||||
description: Dry run instead of publish?
|
||||
type: boolean
|
||||
default: true
|
||||
experimental_only:
|
||||
type: boolean
|
||||
description: Only publish to the experimental tag
|
||||
default: false
|
||||
force_notify:
|
||||
description: Force a Discord notification?
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -12,8 +31,26 @@ env:
|
||||
TZ: /usr/share/zoneinfo/America/Los_Angeles
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
if: ${{ inputs.force_notify || inputs.dry == false || inputs.dry == 'false' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
embed-author-name: ${{ github.event.sender.login }}
|
||||
embed-author-url: ${{ github.event.sender.html_url }}
|
||||
embed-author-icon-url: ${{ github.event.sender.avatar_url }}
|
||||
embed-title: "⚠️ Publishing ${{ inputs.experimental_only && 'EXPERIMENTAL' || 'CANARY & EXPERIMENTAL' }} release ${{ (inputs.dry && ' (dry run)') || '' }}"
|
||||
embed-description: |
|
||||
```json
|
||||
${{ toJson(inputs) }}
|
||||
```
|
||||
embed-url: https://github.com/facebook/react/actions/runs/${{ github.run_id }}
|
||||
|
||||
publish_prerelease_canary:
|
||||
if: ${{ !inputs.experimental_only }}
|
||||
name: Publish to Canary channel
|
||||
uses: facebook/react/.github/workflows/runtime_prereleases.yml@main
|
||||
permissions:
|
||||
@@ -33,6 +70,9 @@ jobs:
|
||||
# downstream consumers might still expect that tag. We can remove this
|
||||
# after some time has elapsed and the change has been communicated.
|
||||
dist_tag: canary,next
|
||||
only_packages: ${{ inputs.only_packages }}
|
||||
skip_packages: ${{ inputs.skip_packages }}
|
||||
dry: ${{ inputs.dry }}
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -48,10 +88,15 @@ jobs:
|
||||
# different versions of the same package, even if they use different
|
||||
# dist tags.
|
||||
needs: publish_prerelease_canary
|
||||
# Ensures the job runs even if canary is skipped
|
||||
if: always()
|
||||
with:
|
||||
commit_sha: ${{ inputs.prerelease_commit_sha }}
|
||||
release_channel: experimental
|
||||
dist_tag: experimental
|
||||
only_packages: ${{ inputs.only_packages }}
|
||||
skip_packages: ${{ inputs.skip_packages }}
|
||||
dry: ${{ inputs.dry }}
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -22,6 +22,7 @@ jobs:
|
||||
release_channel: stable
|
||||
dist_tag: canary,next
|
||||
enableFailureNotification: true
|
||||
dry: false
|
||||
secrets:
|
||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -43,6 +44,7 @@ jobs:
|
||||
release_channel: experimental
|
||||
dist_tag: experimental
|
||||
enableFailureNotification: true
|
||||
dry: false
|
||||
secrets:
|
||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
InstructionKind,
|
||||
LabelTerminal,
|
||||
Place,
|
||||
isStatementBlockKind,
|
||||
makeInstructionId,
|
||||
promoteTemporary,
|
||||
reversePostorderBlocks,
|
||||
@@ -90,100 +91,106 @@ export function inlineImmediatelyInvokedFunctionExpressions(
|
||||
*/
|
||||
const queue = Array.from(fn.body.blocks.values());
|
||||
queue: for (const block of queue) {
|
||||
for (let ii = 0; ii < block.instructions.length; ii++) {
|
||||
const instr = block.instructions[ii]!;
|
||||
switch (instr.value.kind) {
|
||||
case 'FunctionExpression': {
|
||||
if (instr.lvalue.identifier.name === null) {
|
||||
functions.set(instr.lvalue.identifier.id, instr.value);
|
||||
/*
|
||||
* We can't handle labels inside expressions yet, so we don't inline IIFEs if they are in an
|
||||
* expression block.
|
||||
*/
|
||||
if (isStatementBlockKind(block.kind)) {
|
||||
for (let ii = 0; ii < block.instructions.length; ii++) {
|
||||
const instr = block.instructions[ii]!;
|
||||
switch (instr.value.kind) {
|
||||
case 'FunctionExpression': {
|
||||
if (instr.lvalue.identifier.name === null) {
|
||||
functions.set(instr.lvalue.identifier.id, instr.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'CallExpression': {
|
||||
if (instr.value.args.length !== 0) {
|
||||
// We don't support inlining when there are arguments
|
||||
continue;
|
||||
case 'CallExpression': {
|
||||
if (instr.value.args.length !== 0) {
|
||||
// We don't support inlining when there are arguments
|
||||
continue;
|
||||
}
|
||||
const body = functions.get(instr.value.callee.identifier.id);
|
||||
if (body === undefined) {
|
||||
// Not invoking a local function expression, can't inline
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
body.loweredFunc.func.params.length > 0 ||
|
||||
body.loweredFunc.func.async ||
|
||||
body.loweredFunc.func.generator
|
||||
) {
|
||||
// Can't inline functions with params, or async/generator functions
|
||||
continue;
|
||||
}
|
||||
|
||||
// We know this function is used for an IIFE and can prune it later
|
||||
inlinedFunctions.add(instr.value.callee.identifier.id);
|
||||
|
||||
// Create a new block which will contain code following the IIFE call
|
||||
const continuationBlockId = fn.env.nextBlockId;
|
||||
const continuationBlock: BasicBlock = {
|
||||
id: continuationBlockId,
|
||||
instructions: block.instructions.slice(ii + 1),
|
||||
kind: block.kind,
|
||||
phis: new Set(),
|
||||
preds: new Set(),
|
||||
terminal: block.terminal,
|
||||
};
|
||||
fn.body.blocks.set(continuationBlockId, continuationBlock);
|
||||
|
||||
/*
|
||||
* Trim the original block to contain instructions up to (but not including)
|
||||
* the IIFE
|
||||
*/
|
||||
block.instructions.length = ii;
|
||||
|
||||
/*
|
||||
* To account for complex control flow within the lambda, we treat the lambda
|
||||
* as if it were a single labeled statement, and replace all returns with gotos
|
||||
* to the label fallthrough.
|
||||
*/
|
||||
const newTerminal: LabelTerminal = {
|
||||
block: body.loweredFunc.func.body.entry,
|
||||
id: makeInstructionId(0),
|
||||
kind: 'label',
|
||||
fallthrough: continuationBlockId,
|
||||
loc: block.terminal.loc,
|
||||
};
|
||||
block.terminal = newTerminal;
|
||||
|
||||
// We store the result in the IIFE temporary
|
||||
const result = instr.lvalue;
|
||||
|
||||
// Declare the IIFE temporary
|
||||
declareTemporary(fn.env, block, result);
|
||||
|
||||
// Promote the temporary with a name as we require this to persist
|
||||
promoteTemporary(result.identifier);
|
||||
|
||||
/*
|
||||
* Rewrite blocks from the lambda to replace any `return` with a
|
||||
* store to the result and `goto` the continuation block
|
||||
*/
|
||||
for (const [id, block] of body.loweredFunc.func.body.blocks) {
|
||||
block.preds.clear();
|
||||
rewriteBlock(fn.env, block, continuationBlockId, result);
|
||||
fn.body.blocks.set(id, block);
|
||||
}
|
||||
|
||||
/*
|
||||
* Ensure we visit the continuation block, since there may have been
|
||||
* sequential IIFEs that need to be visited.
|
||||
*/
|
||||
queue.push(continuationBlock);
|
||||
continue queue;
|
||||
}
|
||||
const body = functions.get(instr.value.callee.identifier.id);
|
||||
if (body === undefined) {
|
||||
// Not invoking a local function expression, can't inline
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
body.loweredFunc.func.params.length > 0 ||
|
||||
body.loweredFunc.func.async ||
|
||||
body.loweredFunc.func.generator
|
||||
) {
|
||||
// Can't inline functions with params, or async/generator functions
|
||||
continue;
|
||||
}
|
||||
|
||||
// We know this function is used for an IIFE and can prune it later
|
||||
inlinedFunctions.add(instr.value.callee.identifier.id);
|
||||
|
||||
// Create a new block which will contain code following the IIFE call
|
||||
const continuationBlockId = fn.env.nextBlockId;
|
||||
const continuationBlock: BasicBlock = {
|
||||
id: continuationBlockId,
|
||||
instructions: block.instructions.slice(ii + 1),
|
||||
kind: block.kind,
|
||||
phis: new Set(),
|
||||
preds: new Set(),
|
||||
terminal: block.terminal,
|
||||
};
|
||||
fn.body.blocks.set(continuationBlockId, continuationBlock);
|
||||
|
||||
/*
|
||||
* Trim the original block to contain instructions up to (but not including)
|
||||
* the IIFE
|
||||
*/
|
||||
block.instructions.length = ii;
|
||||
|
||||
/*
|
||||
* To account for complex control flow within the lambda, we treat the lambda
|
||||
* as if it were a single labeled statement, and replace all returns with gotos
|
||||
* to the label fallthrough.
|
||||
*/
|
||||
const newTerminal: LabelTerminal = {
|
||||
block: body.loweredFunc.func.body.entry,
|
||||
id: makeInstructionId(0),
|
||||
kind: 'label',
|
||||
fallthrough: continuationBlockId,
|
||||
loc: block.terminal.loc,
|
||||
};
|
||||
block.terminal = newTerminal;
|
||||
|
||||
// We store the result in the IIFE temporary
|
||||
const result = instr.lvalue;
|
||||
|
||||
// Declare the IIFE temporary
|
||||
declareTemporary(fn.env, block, result);
|
||||
|
||||
// Promote the temporary with a name as we require this to persist
|
||||
promoteTemporary(result.identifier);
|
||||
|
||||
/*
|
||||
* Rewrite blocks from the lambda to replace any `return` with a
|
||||
* store to the result and `goto` the continuation block
|
||||
*/
|
||||
for (const [id, block] of body.loweredFunc.func.body.blocks) {
|
||||
block.preds.clear();
|
||||
rewriteBlock(fn.env, block, continuationBlockId, result);
|
||||
fn.body.blocks.set(id, block);
|
||||
}
|
||||
|
||||
/*
|
||||
* Ensure we visit the continuation block, since there may have been
|
||||
* sequential IIFEs that need to be visited.
|
||||
*/
|
||||
queue.push(continuationBlock);
|
||||
continue queue;
|
||||
}
|
||||
default: {
|
||||
for (const place of eachInstructionValueOperand(instr.value)) {
|
||||
// Any other use of a function expression means it isn't an IIFE
|
||||
functions.delete(place.identifier.id);
|
||||
default: {
|
||||
for (const place of eachInstructionValueOperand(instr.value)) {
|
||||
// Any other use of a function expression means it isn't an IIFE
|
||||
functions.delete(place.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useRef, useEffect} from 'react';
|
||||
|
||||
/**
|
||||
* The postfix increment operator should return the value before incrementing.
|
||||
* ```js
|
||||
* const id = count.current; // 0
|
||||
* count.current = count.current + 1; // 1
|
||||
* return id;
|
||||
* ```
|
||||
* The bug is that we currently increment the value before the expression is evaluated.
|
||||
* This bug does not trigger when the incremented value is a plain primitive.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
|
||||
* logs: ['id = 0','count = 1']
|
||||
* Forget:
|
||||
* (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
|
||||
* logs: ['id = 1','count = 1']
|
||||
*/
|
||||
function useFoo() {
|
||||
const count = useRef(0);
|
||||
const updateCountPostfix = () => {
|
||||
const id = count.current++;
|
||||
return id;
|
||||
};
|
||||
const updateCountPrefix = () => {
|
||||
const id = ++count.current;
|
||||
return id;
|
||||
};
|
||||
useEffect(() => {
|
||||
const id = updateCountPostfix();
|
||||
console.log(`id = ${id}`);
|
||||
console.log(`count = ${count.current}`);
|
||||
}, []);
|
||||
return {count, updateCountPostfix, updateCountPrefix};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useRef, useEffect } from "react";
|
||||
|
||||
/**
|
||||
* The postfix increment operator should return the value before incrementing.
|
||||
* ```js
|
||||
* const id = count.current; // 0
|
||||
* count.current = count.current + 1; // 1
|
||||
* return id;
|
||||
* ```
|
||||
* The bug is that we currently increment the value before the expression is evaluated.
|
||||
* This bug does not trigger when the incremented value is a plain primitive.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
|
||||
* logs: ['id = 0','count = 1']
|
||||
* Forget:
|
||||
* (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
|
||||
* logs: ['id = 1','count = 1']
|
||||
*/
|
||||
function useFoo() {
|
||||
const $ = _c(5);
|
||||
const count = useRef(0);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
count.current = count.current + 1;
|
||||
const id = count.current;
|
||||
return id;
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const updateCountPostfix = t0;
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = () => {
|
||||
const id_0 = (count.current = count.current + 1);
|
||||
return id_0;
|
||||
};
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const updateCountPrefix = t1;
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = () => {
|
||||
const id_1 = updateCountPostfix();
|
||||
console.log(`id = ${id_1}`);
|
||||
console.log(`count = ${count.current}`);
|
||||
};
|
||||
t3 = [];
|
||||
$[2] = t2;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
t3 = $[3];
|
||||
}
|
||||
useEffect(t2, t3);
|
||||
let t4;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = { count, updateCountPostfix, updateCountPrefix };
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import {useRef, useEffect} from 'react';
|
||||
|
||||
/**
|
||||
* The postfix increment operator should return the value before incrementing.
|
||||
* ```js
|
||||
* const id = count.current; // 0
|
||||
* count.current = count.current + 1; // 1
|
||||
* return id;
|
||||
* ```
|
||||
* The bug is that we currently increment the value before the expression is evaluated.
|
||||
* This bug does not trigger when the incremented value is a plain primitive.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
|
||||
* logs: ['id = 0','count = 1']
|
||||
* Forget:
|
||||
* (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
|
||||
* logs: ['id = 1','count = 1']
|
||||
*/
|
||||
function useFoo() {
|
||||
const count = useRef(0);
|
||||
const updateCountPostfix = () => {
|
||||
const id = count.current++;
|
||||
return id;
|
||||
};
|
||||
const updateCountPrefix = () => {
|
||||
const id = ++count.current;
|
||||
return id;
|
||||
};
|
||||
useEffect(() => {
|
||||
const id = updateCountPostfix();
|
||||
console.log(`id = ${id}`);
|
||||
console.log(`count = ${count.current}`);
|
||||
}, []);
|
||||
return {count, updateCountPostfix, updateCountPrefix};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component(props) {
|
||||
return (
|
||||
useMemo(() => {
|
||||
return [props.value];
|
||||
}) || []
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
1 | function Component(props) {
|
||||
2 | return (
|
||||
> 3 | useMemo(() => {
|
||||
| ^^^^^^^^^^^^^^^
|
||||
> 4 | return [props.value];
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 5 | }) || []
|
||||
| ^^^^^^^^^^^^^ Todo: Support labeled statements combined with value blocks (conditional, logical, optional chaining, etc) (3:5)
|
||||
6 | );
|
||||
7 | }
|
||||
8 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
function Component(props) {
|
||||
return (
|
||||
useMemo(() => {
|
||||
return [props.value];
|
||||
}) || []
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component(props) {
|
||||
const x = props.foo
|
||||
? 1
|
||||
: (() => {
|
||||
throw new Error('Did not receive 1');
|
||||
})();
|
||||
return items;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{foo: true}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
function Component(props) {
|
||||
props.foo ? 1 : _temp();
|
||||
return items;
|
||||
}
|
||||
function _temp() {
|
||||
throw new Error("Did not receive 1");
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ foo: true }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) items is not defined
|
||||
@@ -0,0 +1,13 @@
|
||||
function Component(props) {
|
||||
const x = props.foo
|
||||
? 1
|
||||
: (() => {
|
||||
throw new Error('Did not receive 1');
|
||||
})();
|
||||
return items;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{foo: true}],
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useMemo} from 'react';
|
||||
function Component(props) {
|
||||
return (
|
||||
useMemo(() => {
|
||||
return [props.value];
|
||||
}) || []
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 1}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useMemo } from "react";
|
||||
function Component(props) {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== props.value) {
|
||||
t0 = (() => [props.value])() || [];
|
||||
$[0] = props.value;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 1 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) [1]
|
||||
@@ -0,0 +1,13 @@
|
||||
import {useMemo} from 'react';
|
||||
function Component(props) {
|
||||
return (
|
||||
useMemo(() => {
|
||||
return [props.value];
|
||||
}) || []
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 1}],
|
||||
};
|
||||
@@ -460,6 +460,7 @@ const skipFilter = new Set([
|
||||
'fbt/bug-fbt-plural-multiple-function-calls',
|
||||
'fbt/bug-fbt-plural-multiple-mixed-call-tag',
|
||||
'bug-invalid-phi-as-dependency',
|
||||
'bug-ref-prefix-postfix-operator',
|
||||
|
||||
// 'react-compiler-runtime' not yet supported
|
||||
'flag-enable-emit-hook-guards',
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
use,
|
||||
} from 'react';
|
||||
|
||||
import Chrome from './Chrome';
|
||||
|
||||
36
fixtures/view-transition/src/components/NestedReveal.js
Normal file
36
fixtures/view-transition/src/components/NestedReveal.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import React, {Suspense, use} from 'react';
|
||||
|
||||
async function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function Use({useable}) {
|
||||
use(useable);
|
||||
return null;
|
||||
}
|
||||
|
||||
let delay1;
|
||||
let delay2;
|
||||
|
||||
export default function NestedReveal({}) {
|
||||
if (!delay1) {
|
||||
delay1 = sleep(100);
|
||||
// Needs to happen before the throttled reveal of delay 1
|
||||
delay2 = sleep(200);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="swipe-recognizer">
|
||||
Shell
|
||||
<Suspense fallback="Loading level 1">
|
||||
<div>Level 1</div>
|
||||
<Use useable={delay1} />
|
||||
|
||||
<Suspense fallback="Loading level 2">
|
||||
<div>Level 2</div>
|
||||
<Use useable={delay2} />
|
||||
</Suspense>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import SwipeRecognizer from './SwipeRecognizer';
|
||||
import './Page.css';
|
||||
|
||||
import transitions from './Transitions.module.css';
|
||||
import NestedReveal from './NestedReveal';
|
||||
|
||||
async function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
@@ -241,6 +242,7 @@ export default function Page({url, navigate}) {
|
||||
</div>
|
||||
</ViewTransition>
|
||||
</SwipeRecognizer>
|
||||
<NestedReveal />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ const rules = {
|
||||
const configRules = {
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
'react-hooks/react-compiler': 'error',
|
||||
} satisfies Linter.RulesRecord;
|
||||
|
||||
// Flat config
|
||||
|
||||
@@ -41,7 +41,8 @@ import {useExtensionComponentsPanelVisibility} from 'react-devtools-shared/src/f
|
||||
import {useChangeOwnerAction} from './OwnersListContext';
|
||||
|
||||
// Never indent more than this number of pixels (even if we have the room).
|
||||
const DEFAULT_INDENTATION_SIZE = 12;
|
||||
const MAX_INDENTATION_SIZE = 12;
|
||||
const MIN_INDENTATION_SIZE = 4;
|
||||
|
||||
export type ItemData = {
|
||||
isNavigatingWithKeyboard: boolean,
|
||||
@@ -490,11 +491,11 @@ function updateIndentationSizeVar(
|
||||
|
||||
// Reset the max indentation size if the width of the tree has increased.
|
||||
if (listWidth > prevListWidthRef.current) {
|
||||
indentationSizeRef.current = DEFAULT_INDENTATION_SIZE;
|
||||
indentationSizeRef.current = MAX_INDENTATION_SIZE;
|
||||
}
|
||||
prevListWidthRef.current = listWidth;
|
||||
|
||||
let maxIndentationSize: number = indentationSizeRef.current;
|
||||
let indentationSize: number = indentationSizeRef.current;
|
||||
|
||||
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
||||
for (const child of innerDiv.children) {
|
||||
@@ -517,12 +518,13 @@ function updateIndentationSizeVar(
|
||||
|
||||
const remainingWidth = Math.max(0, listWidth - childWidth);
|
||||
|
||||
maxIndentationSize = Math.min(maxIndentationSize, remainingWidth / depth);
|
||||
indentationSize = Math.min(indentationSize, remainingWidth / depth);
|
||||
}
|
||||
|
||||
indentationSizeRef.current = maxIndentationSize;
|
||||
indentationSize = Math.max(indentationSize, MIN_INDENTATION_SIZE);
|
||||
indentationSizeRef.current = indentationSize;
|
||||
|
||||
list.style.setProperty('--indentation-size', `${maxIndentationSize}px`);
|
||||
list.style.setProperty('--indentation-size', `${indentationSize}px`);
|
||||
}
|
||||
|
||||
// $FlowFixMe[missing-local-annot]
|
||||
@@ -545,7 +547,7 @@ function InnerElementType({children, style}) {
|
||||
// The user may have resized the window specifically to make more room for DevTools.
|
||||
// In either case, this should reset our max indentation size logic.
|
||||
// 2. The second is when the user enters or exits an owner tree.
|
||||
const indentationSizeRef = useRef<number>(DEFAULT_INDENTATION_SIZE);
|
||||
const indentationSizeRef = useRef<number>(MAX_INDENTATION_SIZE);
|
||||
const prevListWidthRef = useRef<number>(0);
|
||||
const prevOwnerIDRef = useRef<number | null>(ownerID);
|
||||
const divRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -554,7 +556,7 @@ function InnerElementType({children, style}) {
|
||||
// so when the user opens the "owners tree" view, we should discard the previous width.
|
||||
if (ownerID !== prevOwnerIDRef.current) {
|
||||
prevOwnerIDRef.current = ownerID;
|
||||
indentationSizeRef.current = DEFAULT_INDENTATION_SIZE;
|
||||
indentationSizeRef.current = MAX_INDENTATION_SIZE;
|
||||
}
|
||||
|
||||
// When we render new content, measure to see if we need to shrink indentation to fit it.
|
||||
|
||||
@@ -1,74 +1,213 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>React DevTools</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
#target {
|
||||
flex: 1;
|
||||
border: none;
|
||||
}
|
||||
#devtools {
|
||||
height: 400px;
|
||||
max-height: 50%;
|
||||
overflow: hidden;
|
||||
z-index: 10000001;
|
||||
}
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
|
||||
sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.optionsRow {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding: 0.25rem;
|
||||
background: aliceblue;
|
||||
border-bottom: 1px solid lightblue;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.optionsRowSpacer {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="optionsRow">
|
||||
<button id="mountButton">Unmount test app</button>
|
||||
<div class="optionsRowSpacer"> </div>
|
||||
<span>
|
||||
<a href="/multi.html">multi DevTools</a>
|
||||
|
|
||||
<a href="/e2e.html">e2e tests</a>
|
||||
|
|
||||
<a href="/e2e-regression.html">e2e regression tests</a>
|
||||
|
|
||||
<a href="/perf-regression.html">perf regression tests</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>React DevTools</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
#panes {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#divider {
|
||||
position: absolute;
|
||||
z-index: 10000002;
|
||||
background-color: #ccc;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
#divider:hover,
|
||||
#divider.dragging {
|
||||
background-color: #aaa;
|
||||
}
|
||||
|
||||
#divider.horizontal-divider {
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
#divider.vertical-divider {
|
||||
width: 5px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
#target {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#devtools {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
z-index: 10000001;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
contain: strict;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
|
||||
sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.optionsRow {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding: 0.25rem;
|
||||
background: aliceblue;
|
||||
border-bottom: 1px solid lightblue;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.optionsRowSpacer {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="optionsRow">
|
||||
<button id="mountButton">Unmount test app</button>
|
||||
<div class="optionsRowSpacer"> </div>
|
||||
<span>
|
||||
<a href="/multi.html">multi DevTools</a>
|
||||
|
|
||||
<a href="/e2e.html">e2e tests</a>
|
||||
|
|
||||
<a href="/e2e-regression.html">e2e regression tests</a>
|
||||
|
|
||||
<a href="/perf-regression.html">perf regression tests</a>
|
||||
</span>
|
||||
|
||||
<label style="margin-left: 4px">
|
||||
Layout:
|
||||
<select id="layout">
|
||||
<option value="leftright">Left/Right Split</option>
|
||||
<option value="topbottom">Top/Bottom Split</option>
|
||||
</select></label>
|
||||
</div>
|
||||
|
||||
<div id="panes">
|
||||
<!-- React test app (shells/dev/app) is injected here -->
|
||||
<!-- DevTools backend (shells/dev/src) is injected here -->
|
||||
<!-- global "hook" is defined on the iframe's contentWindow -->
|
||||
<iframe id="target"></iframe>
|
||||
|
||||
<!-- Draggable divider between panes -->
|
||||
<div id="divider"></div>
|
||||
|
||||
<!-- DevTools frontend UI (shells/dev/src) renders here -->
|
||||
<div id="devtools"></div>
|
||||
</div>
|
||||
|
||||
<!-- This script installs the hook, injects the backend, and renders the DevTools UI -->
|
||||
<!-- In DEV mode, this file is served by the Webpack dev server -->
|
||||
<!-- For production builds, it's built by Webpack and uploaded from the local file system -->
|
||||
<script src="dist/app-devtools.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
<!-- This script installs the hook, injects the backend, and renders the DevTools UI -->
|
||||
<!-- In DEV mode, this file is served by the Webpack dev server -->
|
||||
<!-- For production builds, it's built by Webpack and uploaded from the local file system -->
|
||||
<script src="dist/app-devtools.js"></script>
|
||||
<script type="module">
|
||||
let layoutType = 'leftright';
|
||||
let splitRatio = 0.5;
|
||||
let isDragging = false;
|
||||
|
||||
// handle layout changes
|
||||
const layout = document.getElementById('layout');
|
||||
function setLayout(layoutType, splitRatio) {
|
||||
const panes = document.getElementById('panes');
|
||||
if (layoutType === 'topbottom') {
|
||||
panes.style.gridTemplateColumns = '100%'; // Full width for each row
|
||||
panes.style.gridTemplateRows = `${splitRatio * 100}% ${(1 - splitRatio) * 100}%`;
|
||||
} else if (layoutType === 'leftright') {
|
||||
panes.style.gridTemplateRows = '100%'; // Full height for each column
|
||||
panes.style.gridTemplateColumns = `${splitRatio * 100}% ${(1 - splitRatio) * 100}%`;
|
||||
}
|
||||
}
|
||||
layout.addEventListener('change', () => {
|
||||
layoutType = layout.value;
|
||||
setLayout(layoutType, splitRatio);
|
||||
updateDividerPosition(); // Ensure divider updates when layout changes
|
||||
});
|
||||
|
||||
// handle changing the split ratio
|
||||
const divider = document.getElementById('divider');
|
||||
function updateDividerPosition() {
|
||||
if (layoutType === 'topbottom') {
|
||||
// For top/bottom layout, divider should be horizontal (spanning across)
|
||||
divider.className = 'horizontal-divider';
|
||||
divider.style.top = `calc(${splitRatio * 100}% - 2.5px)`;
|
||||
divider.style.left = '0';
|
||||
} else {
|
||||
// For left/right layout, divider should be vertical (spanning down)
|
||||
divider.className = 'vertical-divider';
|
||||
divider.style.left = `calc(${splitRatio * 100}% - 2.5px)`;
|
||||
divider.style.top = '0';
|
||||
}
|
||||
}
|
||||
|
||||
// Add event listeners for dragging
|
||||
divider.addEventListener('mousedown', (e) => {
|
||||
isDragging = true;
|
||||
divider.classList.add('dragging');
|
||||
|
||||
// Disable pointer events on the iframe to prevent it from capturing mouse events
|
||||
const iframe = document.getElementById('target');
|
||||
iframe.style.pointerEvents = 'none';
|
||||
|
||||
e.preventDefault(); // Prevent text selection during drag
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!isDragging) return;
|
||||
|
||||
const panes = document.getElementById('panes');
|
||||
const rect = panes.getBoundingClientRect();
|
||||
|
||||
if (layoutType === 'topbottom') {
|
||||
// Calculate new split ratio based on vertical position
|
||||
const newRatio = Math.max(0.1, Math.min(0.9, (e.clientY - rect.top) / rect.height));
|
||||
splitRatio = newRatio;
|
||||
} else {
|
||||
// Calculate new split ratio based on horizontal position
|
||||
const newRatio = Math.max(0.1, Math.min(0.9, (e.clientX - rect.left) / rect.width));
|
||||
splitRatio = newRatio;
|
||||
}
|
||||
|
||||
// Update layout and divider position
|
||||
setLayout(layoutType, splitRatio);
|
||||
updateDividerPosition();
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
divider.classList.remove('dragging');
|
||||
|
||||
// Re-enable pointer events on the iframe
|
||||
const iframe = document.getElementById('target');
|
||||
iframe.style.pointerEvents = 'auto';
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize
|
||||
setLayout(
|
||||
layoutType,
|
||||
splitRatio,
|
||||
);
|
||||
updateDividerPosition();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -6,7 +6,7 @@ export const markShellTime =
|
||||
export const clientRenderBoundary =
|
||||
'$RX=function(b,c,d,e,f){var a=document.getElementById(b);a&&(b=a.previousSibling,b.data="$!",a=a.dataset,c&&(a.dgst=c),d&&(a.msg=d),e&&(a.stck=e),f&&(a.cstck=f),b._reactRetry&&b._reactRetry())};';
|
||||
export const completeBoundary =
|
||||
'$RB=[];$RV=function(b){$RT=performance.now();for(var a=0;a<b.length;a+=2){var c=b[a],h=b[a+1],e=c.parentNode;if(e){var f=c.previousSibling,g=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d||"/&"===d)if(0===g)break;else g--;else"$"!==d&&"$?"!==d&&"$~"!==d&&"$!"!==d&&"&"!==d||g++}d=c.nextSibling;e.removeChild(c);c=d}while(c);for(;h.firstChild;)e.insertBefore(h.firstChild,c);f.data="$";f._reactRetry&&f._reactRetry()}}b.length=0};$RC=function(b,a){if(a=document.getElementById(a))if(a.parentNode.removeChild(a),b=document.getElementById(b))b.previousSibling.data="$~",$RB.push(b,a),2===$RB.length&&(b="number"!==typeof $RT?0:$RT,a=performance.now(),setTimeout($RV.bind(null,$RB),2300>a&&2E3<a?2300-a:b+300-a))};';
|
||||
'$RB=[];$RV=function(b){$RT=performance.now();for(var a=0;a<b.length;a+=2){var c=b[a],e=b[a+1];e.parentNode.removeChild(e);var f=c.parentNode;if(f){var g=c.previousSibling,h=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d||"/&"===d)if(0===h)break;else h--;else"$"!==d&&"$?"!==d&&"$~"!==d&&"$!"!==d&&"&"!==d||h++}d=c.nextSibling;f.removeChild(c);c=d}while(c);for(;e.firstChild;)f.insertBefore(e.firstChild,c);g.data="$";g._reactRetry&&g._reactRetry()}}b.length=0};$RC=function(b,a){if(a=document.getElementById(a))(b=document.getElementById(b))?(b.previousSibling.data="$~",$RB.push(b,a),2===$RB.length&&(b="number"!==typeof $RT?0:$RT,a=performance.now(),setTimeout($RV.bind(null,$RB),2300>a&&2E3<a?2300-a:b+300-a))):a.parentNode.removeChild(a)};';
|
||||
export const completeBoundaryUpgradeToViewTransitions =
|
||||
'$RV=function(A,g){function k(a,b){var e=a.getAttribute(b);e&&(b=a.style,l.push(a,b.viewTransitionName,b.viewTransitionClass),"auto"!==e&&(b.viewTransitionClass=e),(a=a.getAttribute("vt-name"))||(a="_T_"+K++ +"_"),b.viewTransitionName=a,B=!0)}var B=!1,K=0,l=[];try{var f=document.__reactViewTransition;if(f){f.finished.finally($RV.bind(null,g));return}var m=new Map;for(f=1;f<g.length;f+=2)for(var h=g[f].querySelectorAll("[vt-share]"),d=0;d<h.length;d++){var c=h[d];m.set(c.getAttribute("vt-name"),c)}var u=[];for(h=0;h<g.length;h+=2){var C=g[h],x=C.parentNode;if(x){var v=x.getBoundingClientRect();if(v.left||v.top||v.width||v.height){c=C;for(f=0;c;){if(8===c.nodeType){var r=c.data;if("/$"===r)if(0===f)break;else f--;else"$"!==r&&"$?"!==r&&"$~"!==r&&"$!"!==r||f++}else if(1===c.nodeType){d=c;var D=d.getAttribute("vt-name"),y=m.get(D);k(d,y?"vt-share":"vt-exit");y&&(k(y,"vt-share"),m.set(D,null));var E=d.querySelectorAll("[vt-share]");for(d=0;d<E.length;d++){var F=E[d],G=F.getAttribute("vt-name"),\nH=m.get(G);H&&(k(F,"vt-share"),k(H,"vt-share"),m.set(G,null))}}c=c.nextSibling}for(var I=g[h+1],t=I.firstElementChild;t;)null!==m.get(t.getAttribute("vt-name"))&&k(t,"vt-enter"),t=t.nextElementSibling;c=x;do for(var n=c.firstElementChild;n;){var J=n.getAttribute("vt-update");J&&"none"!==J&&!l.includes(n)&&k(n,"vt-update");n=n.nextElementSibling}while((c=c.parentNode)&&1===c.nodeType&&"none"!==c.getAttribute("vt-update"));u.push.apply(u,I.querySelectorAll(\'img[src]:not([loading="lazy"])\'))}}}if(B){var z=\ndocument.__reactViewTransition=document.startViewTransition({update:function(){A(g);for(var a=[document.documentElement.clientHeight,document.fonts.ready],b={},e=0;e<u.length;b={g:b.g},e++)if(b.g=u[e],!b.g.complete){var p=b.g.getBoundingClientRect();0<p.bottom&&0<p.right&&p.top<window.innerHeight&&p.left<window.innerWidth&&(p=new Promise(function(w){return function(q){w.g.addEventListener("load",q);w.g.addEventListener("error",q)}}(b)),a.push(p))}return Promise.race([Promise.all(a),new Promise(function(w){var q=\nperformance.now();setTimeout(w,2300>q&&2E3<q?2300-q:500)})])},types:[]});z.ready.finally(function(){for(var a=l.length-3;0<=a;a-=3){var b=l[a],e=b.style;e.viewTransitionName=l[a+1];e.viewTransitionClass=l[a+1];""===b.getAttribute("style")&&b.removeAttribute("style")}});z.finished.finally(function(){document.__reactViewTransition===z&&(document.__reactViewTransition=null)});$RB=[];return}}catch(a){}A(g)}.bind(null,$RV);';
|
||||
export const completeBoundaryWithStyles =
|
||||
|
||||
@@ -34,6 +34,10 @@ export function revealCompletedBoundaries(batch) {
|
||||
for (let i = 0; i < batch.length; i += 2) {
|
||||
const suspenseIdNode = batch[i];
|
||||
const contentNode = batch[i + 1];
|
||||
// We can detach the content now.
|
||||
// Completions of boundaries within this contentNode will now find the boundary
|
||||
// in its designated place.
|
||||
contentNode.parentNode.removeChild(contentNode);
|
||||
|
||||
// Clear all the existing children. This is complicated because
|
||||
// there can be embedded Suspense boundaries in the fallback.
|
||||
@@ -385,13 +389,16 @@ export function completeBoundary(suspenseBoundaryID, contentID) {
|
||||
// the segment. Regardless we can ignore this case.
|
||||
return;
|
||||
}
|
||||
// We'll detach the content node so that regardless of what happens next we don't leave in the tree.
|
||||
// This might also help by not causing recalcing each time we move a child from here to the target.
|
||||
contentNodeOuter.parentNode.removeChild(contentNodeOuter);
|
||||
|
||||
// Find the fallback's first element.
|
||||
const suspenseIdNodeOuter = document.getElementById(suspenseBoundaryID);
|
||||
if (!suspenseIdNodeOuter) {
|
||||
// We'll never reveal this boundary so we can remove its content immediately.
|
||||
// Otherwise we'll leave it in until we reveal it.
|
||||
// This is important in case this specific boundary contains other boundaries
|
||||
// that may get completed before we reveal this one.
|
||||
contentNodeOuter.parentNode.removeChild(contentNodeOuter);
|
||||
|
||||
// The user must have already navigated away from this tree.
|
||||
// E.g. because the parent was hydrated. That's fine there's nothing to do
|
||||
// but we have to make sure that we already deleted the container node.
|
||||
|
||||
@@ -70,6 +70,7 @@ type Options = {
|
||||
environmentName?: string | (() => string),
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
identifierPrefix?: string,
|
||||
signal?: AbortSignal,
|
||||
onError?: (error: mixed) => void,
|
||||
onPostpone?: (reason: string) => void,
|
||||
};
|
||||
@@ -87,6 +88,18 @@ function render(model: ReactClientValue, options?: Options): Destination {
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
);
|
||||
const signal = options ? options.signal : undefined;
|
||||
if (signal) {
|
||||
if (signal.aborted) {
|
||||
ReactNoopFlightServer.abort(request, (signal: any).reason);
|
||||
} else {
|
||||
const listener = () => {
|
||||
ReactNoopFlightServer.abort(request, (signal: any).reason);
|
||||
signal.removeEventListener('abort', listener);
|
||||
};
|
||||
signal.addEventListener('abort', listener);
|
||||
}
|
||||
}
|
||||
ReactNoopFlightServer.startWork(request);
|
||||
ReactNoopFlightServer.startFlowing(request, destination);
|
||||
return destination;
|
||||
|
||||
8
packages/react-reconciler/src/ReactFiber.js
vendored
8
packages/react-reconciler/src/ReactFiber.js
vendored
@@ -40,7 +40,6 @@ import {
|
||||
enableScopeAPI,
|
||||
enableLegacyHidden,
|
||||
enableTransitionTracing,
|
||||
enableDO_NOT_USE_disableStrictPassiveEffect,
|
||||
disableLegacyMode,
|
||||
enableObjectFiber,
|
||||
enableViewTransition,
|
||||
@@ -92,7 +91,6 @@ import {
|
||||
ProfileMode,
|
||||
StrictLegacyMode,
|
||||
StrictEffectsMode,
|
||||
NoStrictPassiveEffectsMode,
|
||||
SuspenseyImagesMode,
|
||||
} from './ReactTypeOfMode';
|
||||
import {
|
||||
@@ -599,12 +597,6 @@ export function createFiberFromTypeAndProps(
|
||||
if (disableLegacyMode || (mode & ConcurrentMode) !== NoMode) {
|
||||
// Strict effects should never run on legacy roots
|
||||
mode |= StrictEffectsMode;
|
||||
if (
|
||||
enableDO_NOT_USE_disableStrictPassiveEffect &&
|
||||
pendingProps.DO_NOT_USE_disableStrictPassiveEffect
|
||||
) {
|
||||
mode |= NoStrictPassiveEffectsMode;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case REACT_PROFILER_TYPE:
|
||||
|
||||
@@ -25,8 +25,14 @@ function getCacheForType<T>(resourceType: () => T): T {
|
||||
return cacheForType;
|
||||
}
|
||||
|
||||
function cacheSignal(): null | AbortSignal {
|
||||
const cache: Cache = readContext(CacheContext);
|
||||
return cache.controller.signal;
|
||||
}
|
||||
|
||||
export const DefaultAsyncDispatcher: AsyncDispatcher = ({
|
||||
getCacheForType,
|
||||
cacheSignal,
|
||||
}: any);
|
||||
|
||||
if (__DEV__) {
|
||||
|
||||
@@ -55,7 +55,6 @@ import {
|
||||
ConcurrentMode,
|
||||
StrictEffectsMode,
|
||||
StrictLegacyMode,
|
||||
NoStrictPassiveEffectsMode,
|
||||
} from './ReactTypeOfMode';
|
||||
import {
|
||||
NoLane,
|
||||
@@ -2672,8 +2671,7 @@ function mountEffect(
|
||||
): void {
|
||||
if (
|
||||
__DEV__ &&
|
||||
(currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode &&
|
||||
(currentlyRenderingFiber.mode & NoStrictPassiveEffectsMode) === NoMode
|
||||
(currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode
|
||||
) {
|
||||
mountEffectImpl(
|
||||
MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,
|
||||
|
||||
@@ -123,7 +123,6 @@ import {
|
||||
ConcurrentMode,
|
||||
StrictLegacyMode,
|
||||
StrictEffectsMode,
|
||||
NoStrictPassiveEffectsMode,
|
||||
} from './ReactTypeOfMode';
|
||||
import {
|
||||
HostRoot,
|
||||
@@ -4607,21 +4606,13 @@ function recursivelyTraverseAndDoubleInvokeEffectsInDEV(
|
||||
}
|
||||
|
||||
// Unconditionally disconnects and connects passive and layout effects.
|
||||
function doubleInvokeEffectsOnFiber(
|
||||
root: FiberRoot,
|
||||
fiber: Fiber,
|
||||
shouldDoubleInvokePassiveEffects: boolean = true,
|
||||
) {
|
||||
function doubleInvokeEffectsOnFiber(root: FiberRoot, fiber: Fiber) {
|
||||
setIsStrictModeForDevtools(true);
|
||||
try {
|
||||
disappearLayoutEffects(fiber);
|
||||
if (shouldDoubleInvokePassiveEffects) {
|
||||
disconnectPassiveEffect(fiber);
|
||||
}
|
||||
disconnectPassiveEffect(fiber);
|
||||
reappearLayoutEffects(root, fiber.alternate, fiber, false);
|
||||
if (shouldDoubleInvokePassiveEffects) {
|
||||
reconnectPassiveEffects(root, fiber, NoLanes, null, false, 0);
|
||||
}
|
||||
reconnectPassiveEffects(root, fiber, NoLanes, null, false, 0);
|
||||
} finally {
|
||||
setIsStrictModeForDevtools(false);
|
||||
}
|
||||
@@ -4640,13 +4631,7 @@ function doubleInvokeEffectsInDEVIfNecessary(
|
||||
if (fiber.tag !== OffscreenComponent) {
|
||||
if (fiber.flags & PlacementDEV) {
|
||||
if (isInStrictMode) {
|
||||
runWithFiberInDEV(
|
||||
fiber,
|
||||
doubleInvokeEffectsOnFiber,
|
||||
root,
|
||||
fiber,
|
||||
(fiber.mode & NoStrictPassiveEffectsMode) === NoMode,
|
||||
);
|
||||
runWithFiberInDEV(fiber, doubleInvokeEffectsOnFiber, root, fiber);
|
||||
}
|
||||
} else {
|
||||
recursivelyTraverseAndDoubleInvokeEffectsInDEV(
|
||||
|
||||
@@ -459,6 +459,7 @@ export type Dispatcher = {
|
||||
|
||||
export type AsyncDispatcher = {
|
||||
getCacheForType: <T>(resourceType: () => T) => T,
|
||||
cacheSignal: () => null | AbortSignal,
|
||||
// DEV-only
|
||||
getOwner: () => null | Fiber | ReactComponentInfo | ComponentStackNode,
|
||||
};
|
||||
|
||||
@@ -16,7 +16,6 @@ export const ProfileMode = /* */ 0b0000010;
|
||||
//export const DebugTracingMode = /* */ 0b0000100; // Removed
|
||||
export const StrictLegacyMode = /* */ 0b0001000;
|
||||
export const StrictEffectsMode = /* */ 0b0010000;
|
||||
export const NoStrictPassiveEffectsMode = /* */ 0b1000000;
|
||||
// Keep track of if we're in a SuspenseyImages eligible subtree.
|
||||
// TODO: Remove this when enableSuspenseyImages ship where it's always on.
|
||||
export const SuspenseyImagesMode = /* */ 0b0100000;
|
||||
|
||||
@@ -55,28 +55,6 @@ describe('Activity StrictMode', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate __DEV__ && enableActivity && enableDO_NOT_USE_disableStrictPassiveEffect
|
||||
it('does not trigger strict effects when disableStrictPassiveEffect is presented on StrictMode', async () => {
|
||||
await act(() => {
|
||||
ReactNoop.render(
|
||||
<React.StrictMode DO_NOT_USE_disableStrictPassiveEffect={true}>
|
||||
<Activity>
|
||||
<Component label="A" />
|
||||
</Activity>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(log).toEqual([
|
||||
'A: render',
|
||||
'A: render',
|
||||
'A: useLayoutEffect mount',
|
||||
'A: useEffect mount',
|
||||
'A: useLayoutEffect unmount',
|
||||
'A: useLayoutEffect mount',
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate __DEV__ && enableActivity
|
||||
it('should not trigger strict effects when offscreen is hidden', async () => {
|
||||
await act(() => {
|
||||
|
||||
@@ -14,6 +14,7 @@ let React;
|
||||
let ReactNoopFlightServer;
|
||||
let ReactNoopFlightClient;
|
||||
let cache;
|
||||
let cacheSignal;
|
||||
|
||||
describe('ReactCache', () => {
|
||||
beforeEach(() => {
|
||||
@@ -25,6 +26,7 @@ describe('ReactCache', () => {
|
||||
ReactNoopFlightClient = require('react-noop-renderer/flight-client');
|
||||
|
||||
cache = React.cache;
|
||||
cacheSignal = React.cacheSignal;
|
||||
|
||||
jest.resetModules();
|
||||
__unmockReact();
|
||||
@@ -220,4 +222,86 @@ describe('ReactCache', () => {
|
||||
expect(cachedFoo.length).toBe(0);
|
||||
expect(cachedFoo.displayName).toBe(undefined);
|
||||
});
|
||||
|
||||
it('cacheSignal() returns null outside a render', async () => {
|
||||
expect(cacheSignal()).toBe(null);
|
||||
});
|
||||
|
||||
it('cacheSignal() aborts when the render finishes normally', async () => {
|
||||
let renderedCacheSignal = null;
|
||||
|
||||
let resolve;
|
||||
const promise = new Promise(r => (resolve = r));
|
||||
|
||||
async function Test() {
|
||||
renderedCacheSignal = cacheSignal();
|
||||
await promise;
|
||||
return 'Hi';
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const errors = [];
|
||||
const result = ReactNoopFlightServer.render(<Test />, {
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x);
|
||||
},
|
||||
});
|
||||
expect(errors).toEqual([]);
|
||||
expect(renderedCacheSignal).not.toBe(controller.signal); // In the future we might make these the same
|
||||
expect(renderedCacheSignal.aborted).toBe(false);
|
||||
await resolve();
|
||||
await 0;
|
||||
await 0;
|
||||
|
||||
expect(await ReactNoopFlightClient.read(result)).toBe('Hi');
|
||||
|
||||
expect(errors).toEqual([]);
|
||||
expect(renderedCacheSignal.aborted).toBe(true);
|
||||
expect(renderedCacheSignal.reason.message).toContain(
|
||||
'This render completed successfully.',
|
||||
);
|
||||
});
|
||||
|
||||
it('cacheSignal() aborts when the render is aborted', async () => {
|
||||
let renderedCacheSignal = null;
|
||||
|
||||
const promise = new Promise(() => {});
|
||||
|
||||
async function Test() {
|
||||
renderedCacheSignal = cacheSignal();
|
||||
await promise;
|
||||
return 'Hi';
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const errors = [];
|
||||
const result = ReactNoopFlightServer.render(<Test />, {
|
||||
signal: controller.signal,
|
||||
onError(x) {
|
||||
errors.push(x);
|
||||
return 'hi';
|
||||
},
|
||||
});
|
||||
expect(errors).toEqual([]);
|
||||
expect(renderedCacheSignal).not.toBe(controller.signal); // In the future we might make these the same
|
||||
expect(renderedCacheSignal.aborted).toBe(false);
|
||||
const reason = new Error('Timed out');
|
||||
controller.abort(reason);
|
||||
expect(errors).toEqual([reason]);
|
||||
expect(renderedCacheSignal.aborted).toBe(true);
|
||||
expect(renderedCacheSignal.reason).toBe(reason);
|
||||
|
||||
let clientError = null;
|
||||
try {
|
||||
await ReactNoopFlightClient.read(result);
|
||||
} catch (x) {
|
||||
clientError = x;
|
||||
}
|
||||
expect(clientError).not.toBe(null);
|
||||
if (__DEV__) {
|
||||
expect(clientError.message).toBe('Timed out');
|
||||
}
|
||||
expect(clientError.digest).toBe('hi');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,8 +16,13 @@ function getCacheForType<T>(resourceType: () => T): T {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
|
||||
function cacheSignal(): null | AbortSignal {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
|
||||
export const DefaultAsyncDispatcher: AsyncDispatcher = ({
|
||||
getCacheForType,
|
||||
cacheSignal,
|
||||
}: any);
|
||||
|
||||
if (__DEV__) {
|
||||
|
||||
20
packages/react-server/src/ReactFlightServer.js
vendored
20
packages/react-server/src/ReactFlightServer.js
vendored
@@ -419,6 +419,7 @@ export type Request = {
|
||||
destination: null | Destination,
|
||||
bundlerConfig: ClientManifest,
|
||||
cache: Map<Function, mixed>,
|
||||
cacheController: AbortController,
|
||||
nextChunkId: number,
|
||||
pendingChunks: number,
|
||||
hints: Hints,
|
||||
@@ -529,6 +530,7 @@ function RequestInstance(
|
||||
this.destination = null;
|
||||
this.bundlerConfig = bundlerConfig;
|
||||
this.cache = new Map();
|
||||
this.cacheController = new AbortController();
|
||||
this.nextChunkId = 0;
|
||||
this.pendingChunks = 0;
|
||||
this.hints = hints;
|
||||
@@ -604,7 +606,7 @@ export function createRequest(
|
||||
model: ReactClientValue,
|
||||
bundlerConfig: ClientManifest,
|
||||
onError: void | ((error: mixed) => ?string),
|
||||
identifierPrefix?: string,
|
||||
identifierPrefix: void | string,
|
||||
onPostpone: void | ((reason: string) => void),
|
||||
temporaryReferences: void | TemporaryReferenceSet,
|
||||
environmentName: void | string | (() => string), // DEV-only
|
||||
@@ -636,7 +638,7 @@ export function createPrerenderRequest(
|
||||
onAllReady: () => void,
|
||||
onFatalError: () => void,
|
||||
onError: void | ((error: mixed) => ?string),
|
||||
identifierPrefix?: string,
|
||||
identifierPrefix: void | string,
|
||||
onPostpone: void | ((reason: string) => void),
|
||||
temporaryReferences: void | TemporaryReferenceSet,
|
||||
environmentName: void | string | (() => string), // DEV-only
|
||||
@@ -3369,6 +3371,13 @@ function fatalError(request: Request, error: mixed): void {
|
||||
request.status = CLOSING;
|
||||
request.fatalError = error;
|
||||
}
|
||||
const abortReason = new Error(
|
||||
'The render was aborted due to a fatal error.',
|
||||
{
|
||||
cause: error,
|
||||
},
|
||||
);
|
||||
request.cacheController.abort(abortReason);
|
||||
}
|
||||
|
||||
function emitPostponeChunk(
|
||||
@@ -4840,6 +4849,12 @@ function flushCompletedChunks(
|
||||
if (enableTaint) {
|
||||
cleanupTaintQueue(request);
|
||||
}
|
||||
if (request.status < ABORTING) {
|
||||
const abortReason = new Error(
|
||||
'This render completed successfully. All cacheSignals are now aborted to allow clean up of any unused resources.',
|
||||
);
|
||||
request.cacheController.abort(abortReason);
|
||||
}
|
||||
request.status = CLOSED;
|
||||
close(destination);
|
||||
request.destination = null;
|
||||
@@ -4921,6 +4936,7 @@ export function abort(request: Request, reason: mixed): void {
|
||||
// We define any status below OPEN as OPEN equivalent
|
||||
if (request.status <= OPEN) {
|
||||
request.status = ABORTING;
|
||||
request.cacheController.abort(reason);
|
||||
}
|
||||
const abortableTasks = request.abortableTasks;
|
||||
if (abortableTasks.size > 0) {
|
||||
|
||||
@@ -31,6 +31,13 @@ export const DefaultAsyncDispatcher: AsyncDispatcher = ({
|
||||
}
|
||||
return entry;
|
||||
},
|
||||
cacheSignal(): null | AbortSignal {
|
||||
const request = resolveRequest();
|
||||
if (request) {
|
||||
return request.cacheController.signal;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
}: any);
|
||||
|
||||
if (__DEV__) {
|
||||
|
||||
@@ -22,6 +22,9 @@ export function waitForSuspense<T>(fn: () => T): Promise<T> {
|
||||
}
|
||||
return entry;
|
||||
},
|
||||
cacheSignal(): null {
|
||||
return null;
|
||||
},
|
||||
getOwner(): null {
|
||||
return null;
|
||||
},
|
||||
|
||||
@@ -44,6 +44,7 @@ export {
|
||||
lazy,
|
||||
memo,
|
||||
cache,
|
||||
cacheSignal,
|
||||
startTransition,
|
||||
unstable_LegacyHidden,
|
||||
unstable_Activity,
|
||||
|
||||
@@ -27,6 +27,7 @@ export {
|
||||
lazy,
|
||||
memo,
|
||||
cache,
|
||||
cacheSignal,
|
||||
startTransition,
|
||||
unstable_Activity,
|
||||
unstable_postpone,
|
||||
|
||||
@@ -27,6 +27,7 @@ export {
|
||||
lazy,
|
||||
memo,
|
||||
cache,
|
||||
cacheSignal,
|
||||
startTransition,
|
||||
unstable_Activity,
|
||||
unstable_postpone,
|
||||
|
||||
@@ -14,6 +14,7 @@ export {
|
||||
__COMPILER_RUNTIME,
|
||||
act,
|
||||
cache,
|
||||
cacheSignal,
|
||||
Children,
|
||||
cloneElement,
|
||||
Component,
|
||||
|
||||
@@ -44,6 +44,7 @@ export {
|
||||
lazy,
|
||||
memo,
|
||||
cache,
|
||||
cacheSignal,
|
||||
startTransition,
|
||||
unstable_LegacyHidden,
|
||||
unstable_Activity,
|
||||
|
||||
@@ -27,6 +27,7 @@ export {
|
||||
lazy,
|
||||
memo,
|
||||
cache,
|
||||
cacheSignal,
|
||||
unstable_useCacheRefresh,
|
||||
startTransition,
|
||||
useId,
|
||||
|
||||
@@ -27,6 +27,7 @@ export {
|
||||
lazy,
|
||||
memo,
|
||||
cache,
|
||||
cacheSignal,
|
||||
unstable_useCacheRefresh,
|
||||
startTransition,
|
||||
useId,
|
||||
|
||||
@@ -8,9 +8,12 @@
|
||||
*/
|
||||
|
||||
import {disableClientCache} from 'shared/ReactFeatureFlags';
|
||||
import {cache as cacheImpl} from './ReactCacheImpl';
|
||||
import {
|
||||
cache as cacheImpl,
|
||||
cacheSignal as cacheSignalImpl,
|
||||
} from './ReactCacheImpl';
|
||||
|
||||
export function noopCache<A: Iterable<mixed>, T>(fn: (...A) => T): (...A) => T {
|
||||
function noopCache<A: Iterable<mixed>, T>(fn: (...A) => T): (...A) => T {
|
||||
// On the client (i.e. not a Server Components environment) `cache` has
|
||||
// no caching behavior. We just return the function as-is.
|
||||
//
|
||||
@@ -32,3 +35,11 @@ export function noopCache<A: Iterable<mixed>, T>(fn: (...A) => T): (...A) => T {
|
||||
export const cache: typeof noopCache = disableClientCache
|
||||
? noopCache
|
||||
: cacheImpl;
|
||||
|
||||
function noopCacheSignal(): null | AbortSignal {
|
||||
return null;
|
||||
}
|
||||
|
||||
export const cacheSignal: () => null | AbortSignal = disableClientCache
|
||||
? noopCacheSignal
|
||||
: cacheSignalImpl;
|
||||
|
||||
@@ -126,3 +126,15 @@ export function cache<A: Iterable<mixed>, T>(fn: (...A) => T): (...A) => T {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function cacheSignal(): null | AbortSignal {
|
||||
const dispatcher = ReactSharedInternals.A;
|
||||
if (!dispatcher) {
|
||||
// If there is no dispatcher, then we treat this as not having an AbortSignal
|
||||
// since in the same context, a cached function will be allowed to be called
|
||||
// but it won't be cached. So it's neither an infinite AbortSignal nor an
|
||||
// already resolved one.
|
||||
return null;
|
||||
}
|
||||
return dispatcher.cacheSignal();
|
||||
}
|
||||
|
||||
@@ -7,4 +7,4 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export {cache} from './ReactCacheImpl';
|
||||
export {cache, cacheSignal} from './ReactCacheImpl';
|
||||
|
||||
@@ -33,7 +33,7 @@ import {createContext} from './ReactContext';
|
||||
import {lazy} from './ReactLazy';
|
||||
import {forwardRef} from './ReactForwardRef';
|
||||
import {memo} from './ReactMemo';
|
||||
import {cache} from './ReactCacheClient';
|
||||
import {cache, cacheSignal} from './ReactCacheClient';
|
||||
import {postpone} from './ReactPostpone';
|
||||
import {
|
||||
getCacheForType,
|
||||
@@ -83,6 +83,7 @@ export {
|
||||
lazy,
|
||||
memo,
|
||||
cache,
|
||||
cacheSignal,
|
||||
postpone as unstable_postpone,
|
||||
useCallback,
|
||||
useContext,
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
import {forwardRef} from './ReactForwardRef';
|
||||
import {lazy} from './ReactLazy';
|
||||
import {memo} from './ReactMemo';
|
||||
import {cache} from './ReactCacheServer';
|
||||
import {cache, cacheSignal} from './ReactCacheServer';
|
||||
import {startTransition} from './ReactStartTransition';
|
||||
import {postpone} from './ReactPostpone';
|
||||
import {captureOwnerStack} from './ReactOwnerStack';
|
||||
@@ -70,6 +70,7 @@ export {
|
||||
lazy,
|
||||
memo,
|
||||
cache,
|
||||
cacheSignal,
|
||||
startTransition,
|
||||
getCacheForType as unstable_getCacheForType,
|
||||
postpone as unstable_postpone,
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
import {forwardRef} from './ReactForwardRef';
|
||||
import {lazy} from './ReactLazy';
|
||||
import {memo} from './ReactMemo';
|
||||
import {cache} from './ReactCacheServer';
|
||||
import {cache, cacheSignal} from './ReactCacheServer';
|
||||
import {startTransition} from './ReactStartTransition';
|
||||
import {postpone} from './ReactPostpone';
|
||||
import version from 'shared/ReactVersion';
|
||||
@@ -70,6 +70,7 @@ export {
|
||||
lazy,
|
||||
memo,
|
||||
cache,
|
||||
cacheSignal,
|
||||
startTransition,
|
||||
getCacheForType as unstable_getCacheForType,
|
||||
postpone as unstable_postpone,
|
||||
|
||||
@@ -27,7 +27,7 @@ import {use, useId, useCallback, useDebugValue, useMemo} from './ReactHooks';
|
||||
import {forwardRef} from './ReactForwardRef';
|
||||
import {lazy} from './ReactLazy';
|
||||
import {memo} from './ReactMemo';
|
||||
import {cache} from './ReactCacheServer';
|
||||
import {cache, cacheSignal} from './ReactCacheServer';
|
||||
import version from 'shared/ReactVersion';
|
||||
|
||||
const Children = {
|
||||
@@ -58,6 +58,7 @@ export {
|
||||
lazy,
|
||||
memo,
|
||||
cache,
|
||||
cacheSignal,
|
||||
useId,
|
||||
useCallback,
|
||||
useDebugValue,
|
||||
|
||||
@@ -26,7 +26,7 @@ import {use, useId, useCallback, useDebugValue, useMemo} from './ReactHooks';
|
||||
import {forwardRef} from './ReactForwardRef';
|
||||
import {lazy} from './ReactLazy';
|
||||
import {memo} from './ReactMemo';
|
||||
import {cache} from './ReactCacheServer';
|
||||
import {cache, cacheSignal} from './ReactCacheServer';
|
||||
import version from 'shared/ReactVersion';
|
||||
import {captureOwnerStack} from './ReactOwnerStack';
|
||||
|
||||
@@ -53,6 +53,7 @@ export {
|
||||
lazy,
|
||||
memo,
|
||||
cache,
|
||||
cacheSignal,
|
||||
useId,
|
||||
useCallback,
|
||||
useDebugValue,
|
||||
|
||||
@@ -104,52 +104,6 @@ describe('ReactStrictMode', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate enableDO_NOT_USE_disableStrictPassiveEffect
|
||||
it('should include legacy + strict effects mode, but not strict passive effect with disableStrictPassiveEffect', async () => {
|
||||
await act(() => {
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
root.render(
|
||||
<React.StrictMode DO_NOT_USE_disableStrictPassiveEffect={true}>
|
||||
<Component label="A" />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(log).toEqual([
|
||||
'A: render',
|
||||
'A: render',
|
||||
'A: useLayoutEffect mount',
|
||||
'A: useEffect mount',
|
||||
'A: useLayoutEffect unmount',
|
||||
'A: useLayoutEffect mount',
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate enableDO_NOT_USE_disableStrictPassiveEffect
|
||||
it('should include legacy + strict effects mode, but not strict passive effect with disableStrictPassiveEffect in Suspense', async () => {
|
||||
await act(() => {
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
root.render(
|
||||
<React.StrictMode DO_NOT_USE_disableStrictPassiveEffect={true}>
|
||||
<React.Suspense>
|
||||
<Component label="A" />
|
||||
</React.Suspense>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(log).toEqual([
|
||||
'A: render',
|
||||
'A: render',
|
||||
'A: useLayoutEffect mount',
|
||||
'A: useEffect mount',
|
||||
'A: useLayoutEffect unmount',
|
||||
'A: useLayoutEffect mount',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should allow level to be increased with nesting', async () => {
|
||||
await act(() => {
|
||||
const container = document.createElement('div');
|
||||
|
||||
@@ -260,7 +260,4 @@ export const enableAsyncDebugInfo = __EXPERIMENTAL__;
|
||||
// Track which Fiber(s) schedule render work.
|
||||
export const enableUpdaterTracking = __PROFILE__;
|
||||
|
||||
// Internal only.
|
||||
export const enableDO_NOT_USE_disableStrictPassiveEffect = false;
|
||||
|
||||
export const ownerStackLimit = 1e4;
|
||||
|
||||
@@ -44,7 +44,6 @@ export const enableAsyncDebugInfo = false;
|
||||
export const enableAsyncIterableChildren = false;
|
||||
export const enableCPUSuspense = true;
|
||||
export const enableCreateEventHandleAPI = false;
|
||||
export const enableDO_NOT_USE_disableStrictPassiveEffect = false;
|
||||
export const enableMoveBefore = true;
|
||||
export const enableFizzExternalRuntime = true;
|
||||
export const enableHalt = false;
|
||||
|
||||
@@ -29,7 +29,6 @@ export const enableAsyncDebugInfo = false;
|
||||
export const enableAsyncIterableChildren = false;
|
||||
export const enableCPUSuspense = false;
|
||||
export const enableCreateEventHandleAPI = false;
|
||||
export const enableDO_NOT_USE_disableStrictPassiveEffect = false;
|
||||
export const enableMoveBefore = true;
|
||||
export const enableFizzExternalRuntime = true;
|
||||
export const enableHalt = false;
|
||||
|
||||
@@ -49,7 +49,6 @@ export const enableLegacyHidden = false;
|
||||
|
||||
export const enableTransitionTracing = false;
|
||||
|
||||
export const enableDO_NOT_USE_disableStrictPassiveEffect = false;
|
||||
export const enableFizzExternalRuntime = true;
|
||||
|
||||
export const alwaysThrottleRetries = true;
|
||||
|
||||
@@ -24,7 +24,6 @@ export const enableAsyncDebugInfo = false;
|
||||
export const enableAsyncIterableChildren = false;
|
||||
export const enableCPUSuspense = true;
|
||||
export const enableCreateEventHandleAPI = false;
|
||||
export const enableDO_NOT_USE_disableStrictPassiveEffect = false;
|
||||
export const enableMoveBefore = false;
|
||||
export const enableFizzExternalRuntime = true;
|
||||
export const enableHalt = false;
|
||||
|
||||
@@ -50,7 +50,6 @@ export const enableLegacyHidden = false;
|
||||
|
||||
export const enableTransitionTracing = false;
|
||||
|
||||
export const enableDO_NOT_USE_disableStrictPassiveEffect = false;
|
||||
export const enableFizzExternalRuntime = false;
|
||||
|
||||
export const alwaysThrottleRetries = true;
|
||||
|
||||
@@ -17,7 +17,6 @@ export const alwaysThrottleRetries = __VARIANT__;
|
||||
export const disableDefaultPropsExceptForClasses = __VARIANT__;
|
||||
export const disableLegacyContextForFunctionComponents = __VARIANT__;
|
||||
export const disableSchedulerTimeoutInWorkLoop = __VARIANT__;
|
||||
export const enableDO_NOT_USE_disableStrictPassiveEffect = __VARIANT__;
|
||||
export const enableHiddenSubtreeInsertionEffectCleanup = __VARIANT__;
|
||||
export const enableNoCloningMemoCache = __VARIANT__;
|
||||
export const enableObjectFiber = __VARIANT__;
|
||||
|
||||
@@ -19,7 +19,6 @@ export const {
|
||||
disableDefaultPropsExceptForClasses,
|
||||
disableLegacyContextForFunctionComponents,
|
||||
disableSchedulerTimeoutInWorkLoop,
|
||||
enableDO_NOT_USE_disableStrictPassiveEffect,
|
||||
enableHiddenSubtreeInsertionEffectCleanup,
|
||||
enableInfiniteRenderLoopDetection,
|
||||
enableNoCloningMemoCache,
|
||||
|
||||
@@ -546,5 +546,7 @@
|
||||
"558": "Client rendering an Activity suspended it again. This is a bug in React.",
|
||||
"559": "Expected to find a host node. This is a bug in React.",
|
||||
"560": "Cannot use a startGestureTransition() with a comment node root.",
|
||||
"561": "This rendered a large document (>%s kB) without any Suspense boundaries around most of it. That can delay initial paint longer than necessary. To improve load performance, add a <Suspense> or <SuspenseList> around the content you expect to be below the header or below the fold. In the meantime, the content will deopt to paint arbitrary incomplete pieces of HTML."
|
||||
"561": "This rendered a large document (>%s kB) without any Suspense boundaries around most of it. That can delay initial paint longer than necessary. To improve load performance, add a <Suspense> or <SuspenseList> around the content you expect to be below the header or below the fold. In the meantime, the content will deopt to paint arbitrary incomplete pieces of HTML.",
|
||||
"562": "The render was aborted due to a fatal error.",
|
||||
"563": "This render completed successfully. All cacheSignals are now aborted to allow clean up of any unused resources."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user