mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge 5bb47ef2f6 into sapling-pr-archive-josephsavona
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
+132
@@ -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: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
+42
@@ -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: [],
|
||||
};
|
||||
@@ -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',
|
||||
|
||||
@@ -37,8 +37,19 @@ async function delay(text, ms) {
|
||||
return new Promise(resolve => setTimeout(() => resolve(text), ms));
|
||||
}
|
||||
|
||||
async function delayTwice() {
|
||||
await delay('', 20);
|
||||
await delay('', 10);
|
||||
}
|
||||
|
||||
async function delayTrice() {
|
||||
const p = delayTwice();
|
||||
await delay('', 40);
|
||||
return p;
|
||||
}
|
||||
|
||||
async function Bar({children}) {
|
||||
await delay('deferred text', 10);
|
||||
await delayTrice();
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
use,
|
||||
} from 'react';
|
||||
|
||||
import Chrome from './Chrome';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
+97
-70
@@ -2902,6 +2902,46 @@ function resolveTypedArray(
|
||||
resolveBuffer(response, id, view);
|
||||
}
|
||||
|
||||
function logComponentInfo(
|
||||
response: Response,
|
||||
root: SomeChunk<any>,
|
||||
componentInfo: ReactComponentInfo,
|
||||
trackIdx: number,
|
||||
startTime: number,
|
||||
componentEndTime: number,
|
||||
childrenEndTime: number,
|
||||
isLastComponent: boolean,
|
||||
): void {
|
||||
// $FlowFixMe: Refined.
|
||||
if (
|
||||
isLastComponent &&
|
||||
root.status === ERRORED &&
|
||||
root.reason !== response._closedReason
|
||||
) {
|
||||
// If this is the last component to render before this chunk rejected, then conceptually
|
||||
// this component errored. If this was a cancellation then it wasn't this component that
|
||||
// errored.
|
||||
logComponentErrored(
|
||||
componentInfo,
|
||||
trackIdx,
|
||||
startTime,
|
||||
componentEndTime,
|
||||
childrenEndTime,
|
||||
response._rootEnvironmentName,
|
||||
root.reason,
|
||||
);
|
||||
} else {
|
||||
logComponentRender(
|
||||
componentInfo,
|
||||
trackIdx,
|
||||
startTime,
|
||||
componentEndTime,
|
||||
childrenEndTime,
|
||||
response._rootEnvironmentName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function flushComponentPerformance(
|
||||
response: Response,
|
||||
root: SomeChunk<any>,
|
||||
@@ -2957,21 +2997,20 @@ function flushComponentPerformance(
|
||||
// in parallel with the previous.
|
||||
const debugInfo = __DEV__ && root._debugInfo;
|
||||
if (debugInfo) {
|
||||
for (let i = 1; i < debugInfo.length; i++) {
|
||||
let startTime = 0;
|
||||
for (let i = 0; i < debugInfo.length; i++) {
|
||||
const info = debugInfo[i];
|
||||
if (typeof info.time === 'number') {
|
||||
startTime = info.time;
|
||||
}
|
||||
if (typeof info.name === 'string') {
|
||||
// $FlowFixMe: Refined.
|
||||
const startTimeInfo = debugInfo[i - 1];
|
||||
if (typeof startTimeInfo.time === 'number') {
|
||||
const startTime = startTimeInfo.time;
|
||||
if (startTime < trackTime) {
|
||||
// The start time of this component is before the end time of the previous
|
||||
// component on this track so we need to bump the next one to a parallel track.
|
||||
trackIdx++;
|
||||
}
|
||||
trackTime = startTime;
|
||||
break;
|
||||
if (startTime < trackTime) {
|
||||
// The start time of this component is before the end time of the previous
|
||||
// component on this track so we need to bump the next one to a parallel track.
|
||||
trackIdx++;
|
||||
}
|
||||
trackTime = startTime;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (let i = debugInfo.length - 1; i >= 0; i--) {
|
||||
@@ -2979,6 +3018,7 @@ function flushComponentPerformance(
|
||||
if (typeof info.time === 'number') {
|
||||
if (info.time > parentEndTime) {
|
||||
parentEndTime = info.time;
|
||||
break; // We assume the highest number is at the end.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3006,85 +3046,72 @@ function flushComponentPerformance(
|
||||
}
|
||||
childTrackIdx = childResult.track;
|
||||
const childEndTime = childResult.endTime;
|
||||
childTrackTime = childEndTime;
|
||||
if (childEndTime > childTrackTime) {
|
||||
childTrackTime = childEndTime;
|
||||
}
|
||||
if (childEndTime > childrenEndTime) {
|
||||
childrenEndTime = childEndTime;
|
||||
}
|
||||
}
|
||||
|
||||
if (debugInfo) {
|
||||
let endTime = 0;
|
||||
// Write debug info in reverse order (just like stack traces).
|
||||
let componentEndTime = 0;
|
||||
let isLastComponent = true;
|
||||
let endTime = -1;
|
||||
let endTimeIdx = -1;
|
||||
for (let i = debugInfo.length - 1; i >= 0; i--) {
|
||||
const info = debugInfo[i];
|
||||
if (typeof info.time === 'number') {
|
||||
if (info.time > childrenEndTime) {
|
||||
childrenEndTime = info.time;
|
||||
}
|
||||
if (endTime === 0) {
|
||||
// Last timestamp is the end of the last component.
|
||||
endTime = info.time;
|
||||
}
|
||||
if (typeof info.time !== 'number') {
|
||||
continue;
|
||||
}
|
||||
if (typeof info.name === 'string' && i > 0) {
|
||||
// $FlowFixMe: Refined.
|
||||
const componentInfo: ReactComponentInfo = info;
|
||||
const startTimeInfo = debugInfo[i - 1];
|
||||
if (typeof startTimeInfo.time === 'number') {
|
||||
const startTime = startTimeInfo.time;
|
||||
if (
|
||||
isLastComponent &&
|
||||
root.status === ERRORED &&
|
||||
root.reason !== response._closedReason
|
||||
) {
|
||||
// If this is the last component to render before this chunk rejected, then conceptually
|
||||
// this component errored. If this was a cancellation then it wasn't this component that
|
||||
// errored.
|
||||
logComponentErrored(
|
||||
if (componentEndTime === 0) {
|
||||
// Last timestamp is the end of the last component.
|
||||
componentEndTime = info.time;
|
||||
}
|
||||
const time = info.time;
|
||||
if (endTimeIdx > -1) {
|
||||
// Now that we know the start and end time, we can emit the entries between.
|
||||
for (let j = endTimeIdx - 1; j > i; j--) {
|
||||
const candidateInfo = debugInfo[j];
|
||||
if (typeof candidateInfo.name === 'string') {
|
||||
if (componentEndTime > childrenEndTime) {
|
||||
childrenEndTime = componentEndTime;
|
||||
}
|
||||
// $FlowFixMe: Refined.
|
||||
const componentInfo: ReactComponentInfo = candidateInfo;
|
||||
logComponentInfo(
|
||||
response,
|
||||
root,
|
||||
componentInfo,
|
||||
trackIdx,
|
||||
startTime,
|
||||
endTime,
|
||||
time,
|
||||
componentEndTime,
|
||||
childrenEndTime,
|
||||
response._rootEnvironmentName,
|
||||
root.reason,
|
||||
isLastComponent,
|
||||
);
|
||||
} else {
|
||||
logComponentRender(
|
||||
componentInfo,
|
||||
componentEndTime = time; // The end time of previous component is the start time of the next.
|
||||
// Track the root most component of the result for deduping logging.
|
||||
result.component = componentInfo;
|
||||
isLastComponent = false;
|
||||
} else if (candidateInfo.awaited) {
|
||||
if (endTime > childrenEndTime) {
|
||||
childrenEndTime = endTime;
|
||||
}
|
||||
// $FlowFixMe: Refined.
|
||||
const asyncInfo: ReactAsyncInfo = candidateInfo;
|
||||
logComponentAwait(
|
||||
asyncInfo,
|
||||
trackIdx,
|
||||
startTime,
|
||||
time,
|
||||
endTime,
|
||||
childrenEndTime,
|
||||
response._rootEnvironmentName,
|
||||
);
|
||||
}
|
||||
// Track the root most component of the result for deduping logging.
|
||||
result.component = componentInfo;
|
||||
// Set the end time of the previous component to the start of the previous.
|
||||
endTime = startTime;
|
||||
}
|
||||
isLastComponent = false;
|
||||
} else if (info.awaited && i > 0 && i < debugInfo.length - 2) {
|
||||
// $FlowFixMe: Refined.
|
||||
const asyncInfo: ReactAsyncInfo = info;
|
||||
const startTimeInfo = debugInfo[i - 1];
|
||||
const endTimeInfo = debugInfo[i + 1];
|
||||
if (
|
||||
typeof startTimeInfo.time === 'number' &&
|
||||
typeof endTimeInfo.time === 'number'
|
||||
) {
|
||||
const awaitStartTime = startTimeInfo.time;
|
||||
const awaitEndTime = endTimeInfo.time;
|
||||
logComponentAwait(
|
||||
asyncInfo,
|
||||
trackIdx,
|
||||
awaitStartTime,
|
||||
awaitEndTime,
|
||||
response._rootEnvironmentName,
|
||||
);
|
||||
}
|
||||
}
|
||||
endTime = time; // The end time of the next entry is this time.
|
||||
endTimeIdx = i;
|
||||
}
|
||||
}
|
||||
result.endTime = childrenEndTime;
|
||||
|
||||
+1
-7
@@ -18,13 +18,10 @@ import type {
|
||||
import type {LazyComponent} from 'react/src/ReactLazy';
|
||||
import type {TemporaryReferenceSet} from './ReactFlightTemporaryReferences';
|
||||
|
||||
import {enableRenderableContext} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import {
|
||||
REACT_ELEMENT_TYPE,
|
||||
REACT_LAZY_TYPE,
|
||||
REACT_CONTEXT_TYPE,
|
||||
REACT_PROVIDER_TYPE,
|
||||
getIteratorFn,
|
||||
ASYNC_ITERATOR,
|
||||
} from 'shared/ReactSymbols';
|
||||
@@ -699,10 +696,7 @@ export function processReply(
|
||||
return serializeTemporaryReferenceMarker();
|
||||
}
|
||||
if (__DEV__) {
|
||||
if (
|
||||
(value: any).$$typeof ===
|
||||
(enableRenderableContext ? REACT_CONTEXT_TYPE : REACT_PROVIDER_TYPE)
|
||||
) {
|
||||
if ((value: any).$$typeof === REACT_CONTEXT_TYPE) {
|
||||
console.error(
|
||||
'React Context Providers cannot be passed to Server Functions from the Client.%s',
|
||||
describeObjectForErrorMessage(parent, key),
|
||||
|
||||
@@ -2991,6 +2991,64 @@ describe('ReactFlight', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// @gate !__DEV__ || enableComponentPerformanceTrack
|
||||
it('preserves debug info for server-to-server through use()', async () => {
|
||||
function ThirdPartyComponent() {
|
||||
return 'hi';
|
||||
}
|
||||
|
||||
function ServerComponent({transport}) {
|
||||
// This is a Server Component that receives other Server Components from a third party.
|
||||
const text = ReactServer.use(ReactNoopFlightClient.read(transport));
|
||||
return <div>{text.toUpperCase()}</div>;
|
||||
}
|
||||
|
||||
const thirdPartyTransport = ReactNoopFlightServer.render(
|
||||
<ThirdPartyComponent />,
|
||||
{
|
||||
environmentName: 'third-party',
|
||||
},
|
||||
);
|
||||
|
||||
const transport = ReactNoopFlightServer.render(
|
||||
<ServerComponent transport={thirdPartyTransport} />,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const promise = ReactNoopFlightClient.read(transport);
|
||||
expect(getDebugInfo(promise)).toEqual(
|
||||
__DEV__
|
||||
? [
|
||||
{time: 16},
|
||||
{
|
||||
name: 'ServerComponent',
|
||||
env: 'Server',
|
||||
key: null,
|
||||
stack: ' in Object.<anonymous> (at **)',
|
||||
props: {
|
||||
transport: expect.arrayContaining([]),
|
||||
},
|
||||
},
|
||||
{time: 16},
|
||||
{
|
||||
name: 'ThirdPartyComponent',
|
||||
env: 'third-party',
|
||||
key: null,
|
||||
stack: ' in Object.<anonymous> (at **)',
|
||||
props: {},
|
||||
},
|
||||
{time: 16},
|
||||
{time: 17},
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
const result = await promise;
|
||||
ReactNoop.render(result);
|
||||
});
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(<div>HI</div>);
|
||||
});
|
||||
|
||||
it('preserves error stacks passed through server-to-server with source maps', async () => {
|
||||
async function ServerComponent({transport}) {
|
||||
// This is a Server Component that receives other Server Components from a third party.
|
||||
|
||||
@@ -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.
|
||||
|
||||
+5
-11
@@ -19,14 +19,12 @@ import {
|
||||
REACT_MEMO_TYPE,
|
||||
REACT_PORTAL_TYPE,
|
||||
REACT_PROFILER_TYPE,
|
||||
REACT_PROVIDER_TYPE,
|
||||
REACT_STRICT_MODE_TYPE,
|
||||
REACT_SUSPENSE_LIST_TYPE,
|
||||
REACT_SUSPENSE_TYPE,
|
||||
REACT_TRACING_MARKER_TYPE,
|
||||
REACT_VIEW_TRANSITION_TYPE,
|
||||
} from 'shared/ReactSymbols';
|
||||
import {enableRenderableContext} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
TREE_OPERATION_ADD,
|
||||
TREE_OPERATION_REMOVE,
|
||||
@@ -87,6 +85,9 @@ const encodedStringCache: LRUCache<string, Array<number>> = new LRU({
|
||||
max: 1000,
|
||||
});
|
||||
|
||||
// Previously, the type of `Context.Provider`.
|
||||
const LEGACY_REACT_PROVIDER_TYPE: symbol = Symbol.for('react.provider');
|
||||
|
||||
export function alphaSortKeys(
|
||||
a: string | number | symbol,
|
||||
b: string | number | symbol,
|
||||
@@ -712,14 +713,7 @@ function typeOfWithLegacyElementSymbol(object: any): mixed {
|
||||
case REACT_MEMO_TYPE:
|
||||
return $$typeofType;
|
||||
case REACT_CONSUMER_TYPE:
|
||||
if (enableRenderableContext) {
|
||||
return $$typeofType;
|
||||
}
|
||||
// Fall through
|
||||
case REACT_PROVIDER_TYPE:
|
||||
if (!enableRenderableContext) {
|
||||
return $$typeofType;
|
||||
}
|
||||
return $$typeofType;
|
||||
// Fall through
|
||||
default:
|
||||
return $$typeof;
|
||||
@@ -740,7 +734,7 @@ export function getDisplayNameForReactElement(
|
||||
switch (elementType) {
|
||||
case REACT_CONSUMER_TYPE:
|
||||
return 'ContextConsumer';
|
||||
case REACT_PROVIDER_TYPE:
|
||||
case LEGACY_REACT_PROVIDER_TYPE:
|
||||
return 'ContextProvider';
|
||||
case REACT_CONTEXT_TYPE:
|
||||
return 'Context';
|
||||
|
||||
@@ -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>
|
||||
|
||||
+1
-1
@@ -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 =
|
||||
|
||||
Vendored
+10
-3
@@ -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.
|
||||
|
||||
@@ -295,12 +295,6 @@ describe('ReactDOMServerIntegration', () => {
|
||||
});
|
||||
|
||||
itRenders('should treat Context as Context.Provider', async render => {
|
||||
// The `itRenders` helpers don't work with the gate pragma, so we have to do
|
||||
// this instead.
|
||||
if (gate(flags => !flags.enableRenderableContext)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Theme = React.createContext('dark');
|
||||
const Language = React.createContext('french');
|
||||
|
||||
|
||||
+371
-1
@@ -7,7 +7,6 @@
|
||||
* @emails react-core
|
||||
* @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
|
||||
*/
|
||||
|
||||
let JSDOM;
|
||||
let React;
|
||||
let ReactDOMClient;
|
||||
@@ -24,6 +23,8 @@ let buffer = '';
|
||||
let hasErrored = false;
|
||||
let fatalError = undefined;
|
||||
let waitForPaint;
|
||||
let SuspenseList;
|
||||
let assertConsoleErrorDev;
|
||||
|
||||
describe('useId', () => {
|
||||
beforeEach(() => {
|
||||
@@ -32,11 +33,16 @@ describe('useId', () => {
|
||||
React = require('react');
|
||||
ReactDOMClient = require('react-dom/client');
|
||||
clientAct = require('internal-test-utils').act;
|
||||
assertConsoleErrorDev =
|
||||
require('internal-test-utils').assertConsoleErrorDev;
|
||||
ReactDOMFizzServer = require('react-dom/server');
|
||||
Stream = require('stream');
|
||||
Suspense = React.Suspense;
|
||||
useId = React.useId;
|
||||
useState = React.useState;
|
||||
if (gate(flags => flags.enableSuspenseList)) {
|
||||
SuspenseList = React.unstable_SuspenseList;
|
||||
}
|
||||
|
||||
const InternalTestUtils = require('internal-test-utils');
|
||||
waitForPaint = InternalTestUtils.waitForPaint;
|
||||
@@ -375,6 +381,370 @@ describe('useId', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
// @gate enableSuspenseList
|
||||
it('Supports SuspenseList (reveal order independent)', async () => {
|
||||
function Baz({id, children}) {
|
||||
return <span id={id}>{children}</span>;
|
||||
}
|
||||
|
||||
function Bar({children}) {
|
||||
const id = useId();
|
||||
return <Baz id={id}>{children}</Baz>;
|
||||
}
|
||||
|
||||
function Foo() {
|
||||
return (
|
||||
<SuspenseList revealOrder="independent">
|
||||
<Bar>A</Bar>
|
||||
<Bar>B</Bar>
|
||||
</SuspenseList>
|
||||
);
|
||||
}
|
||||
|
||||
await serverAct(async () => {
|
||||
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
|
||||
pipe(writable);
|
||||
});
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<span
|
||||
id="_R_1_"
|
||||
>
|
||||
A
|
||||
</span>
|
||||
<span
|
||||
id="_R_2_"
|
||||
>
|
||||
B
|
||||
</span>
|
||||
</div>
|
||||
`);
|
||||
|
||||
await clientAct(async () => {
|
||||
ReactDOMClient.hydrateRoot(container, <Foo />);
|
||||
});
|
||||
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<span
|
||||
id="_R_1_"
|
||||
>
|
||||
A
|
||||
</span>
|
||||
<span
|
||||
id="_R_2_"
|
||||
>
|
||||
B
|
||||
</span>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
// @gate enableSuspenseList
|
||||
it('Supports SuspenseList (reveal order "together")', async () => {
|
||||
function Baz({id, children}) {
|
||||
return <span id={id}>{children}</span>;
|
||||
}
|
||||
|
||||
function Bar({children}) {
|
||||
const id = useId();
|
||||
return <Baz id={id}>{children}</Baz>;
|
||||
}
|
||||
|
||||
function Foo() {
|
||||
return (
|
||||
<SuspenseList revealOrder="together">
|
||||
<Bar>A</Bar>
|
||||
<Bar>B</Bar>
|
||||
</SuspenseList>
|
||||
);
|
||||
}
|
||||
|
||||
await serverAct(async () => {
|
||||
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
|
||||
pipe(writable);
|
||||
});
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<span
|
||||
id="_R_1_"
|
||||
>
|
||||
A
|
||||
</span>
|
||||
<span
|
||||
id="_R_2_"
|
||||
>
|
||||
B
|
||||
</span>
|
||||
</div>
|
||||
`);
|
||||
|
||||
await clientAct(async () => {
|
||||
ReactDOMClient.hydrateRoot(container, <Foo />);
|
||||
});
|
||||
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<span
|
||||
id="_R_1_"
|
||||
>
|
||||
A
|
||||
</span>
|
||||
<span
|
||||
id="_R_2_"
|
||||
>
|
||||
B
|
||||
</span>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
// @gate enableSuspenseList
|
||||
it('Supports SuspenseList (reveal order "forwards")', async () => {
|
||||
function Baz({id, children}) {
|
||||
return <span id={id}>{children}</span>;
|
||||
}
|
||||
|
||||
function Bar({children}) {
|
||||
const id = useId();
|
||||
return <Baz id={id}>{children}</Baz>;
|
||||
}
|
||||
|
||||
function Foo() {
|
||||
return (
|
||||
<SuspenseList revealOrder="forwards" tail="visible">
|
||||
<Bar>A</Bar>
|
||||
<Bar>B</Bar>
|
||||
</SuspenseList>
|
||||
);
|
||||
}
|
||||
|
||||
await serverAct(async () => {
|
||||
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
|
||||
pipe(writable);
|
||||
});
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<span
|
||||
id="_R_1_"
|
||||
>
|
||||
A
|
||||
</span>
|
||||
<span
|
||||
id="_R_2_"
|
||||
>
|
||||
B
|
||||
</span>
|
||||
</div>
|
||||
`);
|
||||
|
||||
await clientAct(async () => {
|
||||
ReactDOMClient.hydrateRoot(container, <Foo />);
|
||||
});
|
||||
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<span
|
||||
id="_R_1_"
|
||||
>
|
||||
A
|
||||
</span>
|
||||
<span
|
||||
id="_R_2_"
|
||||
>
|
||||
B
|
||||
</span>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
// @gate enableSuspenseList
|
||||
it('Supports SuspenseList (reveal order "backwards") with a single child in a list of many', async () => {
|
||||
function Baz({id, children}) {
|
||||
return <span id={id}>{children}</span>;
|
||||
}
|
||||
|
||||
function Bar({children}) {
|
||||
const id = useId();
|
||||
return <Baz id={id}>{children}</Baz>;
|
||||
}
|
||||
|
||||
function Foo() {
|
||||
return (
|
||||
<SuspenseList revealOrder="unstable_legacy-backwards" tail="visible">
|
||||
{null}
|
||||
<Bar>A</Bar>
|
||||
{null}
|
||||
</SuspenseList>
|
||||
);
|
||||
}
|
||||
|
||||
await serverAct(async () => {
|
||||
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
|
||||
pipe(writable);
|
||||
});
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<span
|
||||
id="_R_2_"
|
||||
>
|
||||
A
|
||||
</span>
|
||||
<!-- -->
|
||||
</div>
|
||||
`);
|
||||
|
||||
await clientAct(async () => {
|
||||
ReactDOMClient.hydrateRoot(container, <Foo />);
|
||||
});
|
||||
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<span
|
||||
id="_R_2_"
|
||||
>
|
||||
A
|
||||
</span>
|
||||
<!-- -->
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
// @gate enableSuspenseList
|
||||
it('Supports SuspenseList (reveal order "backwards")', async () => {
|
||||
function Baz({id, children}) {
|
||||
return <span id={id}>{children}</span>;
|
||||
}
|
||||
|
||||
function Bar({children}) {
|
||||
const id = useId();
|
||||
return <Baz id={id}>{children}</Baz>;
|
||||
}
|
||||
|
||||
function Foo() {
|
||||
return (
|
||||
<SuspenseList revealOrder="unstable_legacy-backwards" tail="visible">
|
||||
<Bar>A</Bar>
|
||||
<Bar>B</Bar>
|
||||
</SuspenseList>
|
||||
);
|
||||
}
|
||||
|
||||
await serverAct(async () => {
|
||||
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
|
||||
pipe(writable);
|
||||
});
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<span
|
||||
id="_R_1_"
|
||||
>
|
||||
A
|
||||
</span>
|
||||
<span
|
||||
id="_R_2_"
|
||||
>
|
||||
B
|
||||
</span>
|
||||
</div>
|
||||
`);
|
||||
|
||||
if (gate(flags => flags.favorSafetyOverHydrationPerf)) {
|
||||
// TODO: This is a bug with revealOrder="backwards" in that it hydrates in reverse.
|
||||
await expect(async () => {
|
||||
await clientAct(async () => {
|
||||
ReactDOMClient.hydrateRoot(container, <Foo />);
|
||||
});
|
||||
}).rejects.toThrowError(
|
||||
`Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client.`,
|
||||
);
|
||||
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<span
|
||||
id="_r_1_"
|
||||
>
|
||||
A
|
||||
</span>
|
||||
<span
|
||||
id="_r_0_"
|
||||
>
|
||||
B
|
||||
</span>
|
||||
</div>
|
||||
`);
|
||||
} else {
|
||||
await clientAct(async () => {
|
||||
ReactDOMClient.hydrateRoot(container, <Foo />);
|
||||
});
|
||||
|
||||
// TODO: This is a bug with revealOrder="backwards" in that it hydrates in reverse.
|
||||
assertConsoleErrorDev([
|
||||
`A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:
|
||||
|
||||
- A server/client branch \`if (typeof window !== 'undefined')\`.
|
||||
- Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
|
||||
- Date formatting in a user's locale which doesn't match the server.
|
||||
- External changing data without sending a snapshot of it along with the HTML.
|
||||
- Invalid HTML tag nesting.
|
||||
|
||||
It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
|
||||
|
||||
https://react.dev/link/hydration-mismatch
|
||||
|
||||
<Foo>
|
||||
<SuspenseList revealOrder="unstable_l..." tail="visible">
|
||||
<Bar>
|
||||
<Bar>
|
||||
<Baz id="_R_2_">
|
||||
<span
|
||||
+ id="_R_2_"
|
||||
- id="_R_1_"
|
||||
>
|
||||
+ B
|
||||
- A
|
||||
`,
|
||||
]);
|
||||
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<span
|
||||
id="_R_1_"
|
||||
>
|
||||
A
|
||||
</span>
|
||||
<span
|
||||
id="_R_2_"
|
||||
>
|
||||
B
|
||||
</span>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
||||
it('basic incremental hydration', async () => {
|
||||
function App() {
|
||||
return (
|
||||
|
||||
@@ -932,7 +932,6 @@ describe('ReactDOMServer', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate enableRenderableContext || !__DEV__
|
||||
it('should warn if an invalid contextType is defined', () => {
|
||||
const Context = React.createContext();
|
||||
class ComponentA extends React.Component {
|
||||
|
||||
Vendored
+6
-28
@@ -18,7 +18,6 @@ import {
|
||||
REACT_MEMO_TYPE,
|
||||
REACT_PORTAL_TYPE,
|
||||
REACT_PROFILER_TYPE,
|
||||
REACT_PROVIDER_TYPE,
|
||||
REACT_CONSUMER_TYPE,
|
||||
REACT_STRICT_MODE_TYPE,
|
||||
REACT_SUSPENSE_TYPE,
|
||||
@@ -30,7 +29,6 @@ import {
|
||||
} from 'shared/ReactSymbols';
|
||||
|
||||
import {
|
||||
enableRenderableContext,
|
||||
enableScopeAPI,
|
||||
enableTransitionTracing,
|
||||
enableLegacyHidden,
|
||||
@@ -64,14 +62,7 @@ export function typeOf(object: any): mixed {
|
||||
case REACT_MEMO_TYPE:
|
||||
return $$typeofType;
|
||||
case REACT_CONSUMER_TYPE:
|
||||
if (enableRenderableContext) {
|
||||
return $$typeofType;
|
||||
}
|
||||
// Fall through
|
||||
case REACT_PROVIDER_TYPE:
|
||||
if (!enableRenderableContext) {
|
||||
return $$typeofType;
|
||||
}
|
||||
return $$typeofType;
|
||||
// Fall through
|
||||
default:
|
||||
return $$typeof;
|
||||
@@ -85,12 +76,8 @@ export function typeOf(object: any): mixed {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const ContextConsumer: symbol = enableRenderableContext
|
||||
? REACT_CONSUMER_TYPE
|
||||
: REACT_CONTEXT_TYPE;
|
||||
export const ContextProvider: symbol = enableRenderableContext
|
||||
? REACT_CONTEXT_TYPE
|
||||
: REACT_PROVIDER_TYPE;
|
||||
export const ContextConsumer: symbol = REACT_CONSUMER_TYPE;
|
||||
export const ContextProvider: symbol = REACT_CONTEXT_TYPE;
|
||||
export const Element = REACT_ELEMENT_TYPE;
|
||||
export const ForwardRef = REACT_FORWARD_REF_TYPE;
|
||||
export const Fragment = REACT_FRAGMENT_TYPE;
|
||||
@@ -127,8 +114,7 @@ export function isValidElementType(type: mixed): boolean {
|
||||
type.$$typeof === REACT_LAZY_TYPE ||
|
||||
type.$$typeof === REACT_MEMO_TYPE ||
|
||||
type.$$typeof === REACT_CONTEXT_TYPE ||
|
||||
(!enableRenderableContext && type.$$typeof === REACT_PROVIDER_TYPE) ||
|
||||
(enableRenderableContext && type.$$typeof === REACT_CONSUMER_TYPE) ||
|
||||
type.$$typeof === REACT_CONSUMER_TYPE ||
|
||||
type.$$typeof === REACT_FORWARD_REF_TYPE ||
|
||||
// This needs to include all possible module reference object
|
||||
// types supported by any Flight configuration anywhere since
|
||||
@@ -145,18 +131,10 @@ export function isValidElementType(type: mixed): boolean {
|
||||
}
|
||||
|
||||
export function isContextConsumer(object: any): boolean {
|
||||
if (enableRenderableContext) {
|
||||
return typeOf(object) === REACT_CONSUMER_TYPE;
|
||||
} else {
|
||||
return typeOf(object) === REACT_CONTEXT_TYPE;
|
||||
}
|
||||
return typeOf(object) === REACT_CONSUMER_TYPE;
|
||||
}
|
||||
export function isContextProvider(object: any): boolean {
|
||||
if (enableRenderableContext) {
|
||||
return typeOf(object) === REACT_CONTEXT_TYPE;
|
||||
} else {
|
||||
return typeOf(object) === REACT_PROVIDER_TYPE;
|
||||
}
|
||||
return typeOf(object) === REACT_CONTEXT_TYPE;
|
||||
}
|
||||
export function isElement(object: any): boolean {
|
||||
return (
|
||||
|
||||
+4
-19
@@ -41,7 +41,6 @@ import {
|
||||
enableLegacyHidden,
|
||||
enableTransitionTracing,
|
||||
enableDO_NOT_USE_disableStrictPassiveEffect,
|
||||
enableRenderableContext,
|
||||
disableLegacyMode,
|
||||
enableObjectFiber,
|
||||
enableViewTransition,
|
||||
@@ -101,7 +100,6 @@ import {
|
||||
REACT_FRAGMENT_TYPE,
|
||||
REACT_STRICT_MODE_TYPE,
|
||||
REACT_PROFILER_TYPE,
|
||||
REACT_PROVIDER_TYPE,
|
||||
REACT_CONTEXT_TYPE,
|
||||
REACT_CONSUMER_TYPE,
|
||||
REACT_SUSPENSE_TYPE,
|
||||
@@ -638,25 +636,12 @@ export function createFiberFromTypeAndProps(
|
||||
default: {
|
||||
if (typeof type === 'object' && type !== null) {
|
||||
switch (type.$$typeof) {
|
||||
case REACT_PROVIDER_TYPE:
|
||||
if (!enableRenderableContext) {
|
||||
fiberTag = ContextProvider;
|
||||
break getTag;
|
||||
}
|
||||
// Fall through
|
||||
case REACT_CONTEXT_TYPE:
|
||||
if (enableRenderableContext) {
|
||||
fiberTag = ContextProvider;
|
||||
break getTag;
|
||||
} else {
|
||||
fiberTag = ContextConsumer;
|
||||
break getTag;
|
||||
}
|
||||
fiberTag = ContextProvider;
|
||||
break getTag;
|
||||
case REACT_CONSUMER_TYPE:
|
||||
if (enableRenderableContext) {
|
||||
fiberTag = ContextConsumer;
|
||||
break getTag;
|
||||
}
|
||||
fiberTag = ContextConsumer;
|
||||
break getTag;
|
||||
// Fall through
|
||||
case REACT_FORWARD_REF_TYPE:
|
||||
fiberTag = ForwardRef;
|
||||
|
||||
+12
-25
@@ -116,7 +116,6 @@ import {
|
||||
enableLegacyHidden,
|
||||
enableCPUSuspense,
|
||||
enablePostpone,
|
||||
enableRenderableContext,
|
||||
disableLegacyMode,
|
||||
disableDefaultPropsExceptForClasses,
|
||||
enableHydrationLaneScheduling,
|
||||
@@ -3342,6 +3341,7 @@ function initSuspenseListRenderState(
|
||||
tail: null | Fiber,
|
||||
lastContentRow: null | Fiber,
|
||||
tailMode: SuspenseListTailMode,
|
||||
treeForkCount: number,
|
||||
): void {
|
||||
const renderState: null | SuspenseListRenderState =
|
||||
workInProgress.memoizedState;
|
||||
@@ -3353,6 +3353,7 @@ function initSuspenseListRenderState(
|
||||
last: lastContentRow,
|
||||
tail: tail,
|
||||
tailMode: tailMode,
|
||||
treeForkCount: treeForkCount,
|
||||
}: SuspenseListRenderState);
|
||||
} else {
|
||||
// We can reuse the existing object from previous renders.
|
||||
@@ -3362,6 +3363,7 @@ function initSuspenseListRenderState(
|
||||
renderState.last = lastContentRow;
|
||||
renderState.tail = tail;
|
||||
renderState.tailMode = tailMode;
|
||||
renderState.treeForkCount = treeForkCount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3404,6 +3406,8 @@ function updateSuspenseListComponent(
|
||||
validateSuspenseListChildren(newChildren, revealOrder);
|
||||
|
||||
reconcileChildren(current, workInProgress, newChildren, renderLanes);
|
||||
// Read how many children forks this set pushed so we can push it every time we retry.
|
||||
const treeForkCount = getIsHydrating() ? getForksAtLevel(workInProgress) : 0;
|
||||
|
||||
if (!shouldForceFallback) {
|
||||
const didSuspendBefore =
|
||||
@@ -3446,6 +3450,7 @@ function updateSuspenseListComponent(
|
||||
tail,
|
||||
lastContentRow,
|
||||
tailMode,
|
||||
treeForkCount,
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -3478,6 +3483,7 @@ function updateSuspenseListComponent(
|
||||
tail,
|
||||
null, // last
|
||||
tailMode,
|
||||
treeForkCount,
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -3488,6 +3494,7 @@ function updateSuspenseListComponent(
|
||||
null, // tail
|
||||
null, // last
|
||||
undefined,
|
||||
treeForkCount,
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -3583,12 +3590,7 @@ function updateContextProvider(
|
||||
workInProgress: Fiber,
|
||||
renderLanes: Lanes,
|
||||
) {
|
||||
let context: ReactContext<any>;
|
||||
if (enableRenderableContext) {
|
||||
context = workInProgress.type;
|
||||
} else {
|
||||
context = workInProgress.type._context;
|
||||
}
|
||||
const context: ReactContext<any> = workInProgress.type;
|
||||
const newProps = workInProgress.pendingProps;
|
||||
const newValue = newProps.value;
|
||||
|
||||
@@ -3615,18 +3617,8 @@ function updateContextConsumer(
|
||||
workInProgress: Fiber,
|
||||
renderLanes: Lanes,
|
||||
) {
|
||||
let context: ReactContext<any>;
|
||||
if (enableRenderableContext) {
|
||||
const consumerType: ReactConsumerType<any> = workInProgress.type;
|
||||
context = consumerType._context;
|
||||
} else {
|
||||
context = workInProgress.type;
|
||||
if (__DEV__) {
|
||||
if ((context: any)._context !== undefined) {
|
||||
context = (context: any)._context;
|
||||
}
|
||||
}
|
||||
}
|
||||
const consumerType: ReactConsumerType<any> = workInProgress.type;
|
||||
const context: ReactContext<any> = consumerType._context;
|
||||
const newProps = workInProgress.pendingProps;
|
||||
const render = newProps.children;
|
||||
|
||||
@@ -3870,12 +3862,7 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
|
||||
break;
|
||||
case ContextProvider: {
|
||||
const newValue = workInProgress.memoizedProps.value;
|
||||
let context: ReactContext<any>;
|
||||
if (enableRenderableContext) {
|
||||
context = workInProgress.type;
|
||||
} else {
|
||||
context = workInProgress.type._context;
|
||||
}
|
||||
const context: ReactContext<any> = workInProgress.type;
|
||||
pushProvider(workInProgress, context, newValue);
|
||||
break;
|
||||
}
|
||||
|
||||
+10
-8
@@ -38,7 +38,6 @@ import {
|
||||
enablePersistedModeClonedFlag,
|
||||
enableProfilerTimer,
|
||||
enableTransitionTracing,
|
||||
enableRenderableContext,
|
||||
passChildrenWhenCloningPersistedNodes,
|
||||
disableLegacyMode,
|
||||
enableViewTransition,
|
||||
@@ -184,7 +183,7 @@ import {resetChildFibers} from './ReactChildFiber';
|
||||
import {createScopeInstance} from './ReactFiberScope';
|
||||
import {transferActualDuration} from './ReactProfilerTimer';
|
||||
import {popCacheProvider} from './ReactFiberCacheComponent';
|
||||
import {popTreeContext} from './ReactFiberTreeContext';
|
||||
import {popTreeContext, pushTreeFork} from './ReactFiberTreeContext';
|
||||
import {popRootTransition, popTransition} from './ReactFiberTransition';
|
||||
import {
|
||||
popMarkerInstance,
|
||||
@@ -1667,12 +1666,7 @@ function completeWork(
|
||||
return null;
|
||||
case ContextProvider:
|
||||
// Pop provider fiber
|
||||
let context: ReactContext<any>;
|
||||
if (enableRenderableContext) {
|
||||
context = workInProgress.type;
|
||||
} else {
|
||||
context = workInProgress.type._context;
|
||||
}
|
||||
const context: ReactContext<any> = workInProgress.type;
|
||||
popProvider(context, workInProgress);
|
||||
bubbleProperties(workInProgress);
|
||||
return null;
|
||||
@@ -1764,6 +1758,10 @@ function completeWork(
|
||||
ForceSuspenseFallback,
|
||||
),
|
||||
);
|
||||
if (getIsHydrating()) {
|
||||
// Re-apply tree fork since we popped the tree fork context in the beginning of this function.
|
||||
pushTreeFork(workInProgress, renderState.treeForkCount);
|
||||
}
|
||||
// Don't bubble properties in this case.
|
||||
return workInProgress.child;
|
||||
}
|
||||
@@ -1890,6 +1888,10 @@ function completeWork(
|
||||
}
|
||||
pushSuspenseListContext(workInProgress, suspenseContext);
|
||||
// Do a pass over the next row.
|
||||
if (getIsHydrating()) {
|
||||
// Re-apply tree fork since we popped the tree fork context in the beginning of this function.
|
||||
pushTreeFork(workInProgress, renderState.treeForkCount);
|
||||
}
|
||||
// Don't bubble properties in this case.
|
||||
return next;
|
||||
}
|
||||
|
||||
+1
-8
@@ -29,7 +29,6 @@ import {
|
||||
} from './ReactFiberFlags';
|
||||
|
||||
import is from 'shared/objectIs';
|
||||
import {enableRenderableContext} from 'shared/ReactFeatureFlags';
|
||||
import {getHostTransitionProvider} from './ReactFiberHostContext';
|
||||
|
||||
const valueCursor: StackCursor<mixed> = createCursor(null);
|
||||
@@ -389,13 +388,7 @@ function propagateParentContextChanges(
|
||||
|
||||
const oldProps = currentParent.memoizedProps;
|
||||
if (oldProps !== null) {
|
||||
let context: ReactContext<any>;
|
||||
if (enableRenderableContext) {
|
||||
context = parent.type;
|
||||
} else {
|
||||
context = parent.type._context;
|
||||
}
|
||||
|
||||
const context: ReactContext<any> = parent.type;
|
||||
const newProps = parent.pendingProps;
|
||||
const newValue = newProps.value;
|
||||
|
||||
|
||||
+2
-8
@@ -22,10 +22,7 @@ import {
|
||||
import {isFiberSuspenseAndTimedOut} from './ReactFiberTreeReflection';
|
||||
|
||||
import {HostComponent, ScopeComponent, ContextProvider} from './ReactWorkTags';
|
||||
import {
|
||||
enableScopeAPI,
|
||||
enableRenderableContext,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {enableScopeAPI} from 'shared/ReactFeatureFlags';
|
||||
|
||||
function getSuspenseFallbackChild(fiber: Fiber): Fiber | null {
|
||||
return ((((fiber.child: any): Fiber).sibling: any): Fiber).child;
|
||||
@@ -116,10 +113,7 @@ function collectNearestContextValues<T>(
|
||||
context: ReactContext<T>,
|
||||
childContextValues: Array<T>,
|
||||
): void {
|
||||
if (
|
||||
node.tag === ContextProvider &&
|
||||
(enableRenderableContext ? node.type : node.type._context) === context
|
||||
) {
|
||||
if (node.tag === ContextProvider && node.type === context) {
|
||||
const contextValue = node.memoizedProps.value;
|
||||
childContextValues.push(contextValue);
|
||||
} else {
|
||||
|
||||
@@ -54,6 +54,8 @@ export type SuspenseListRenderState = {
|
||||
tail: null | Fiber,
|
||||
// Tail insertions setting.
|
||||
tailMode: SuspenseListTailMode,
|
||||
// Keep track of total number of forks during multiple passes
|
||||
treeForkCount: number,
|
||||
};
|
||||
|
||||
export type RetryQueue = Set<Wakeable>;
|
||||
|
||||
+2
-13
@@ -36,7 +36,6 @@ import {NoMode, ProfileMode} from './ReactTypeOfMode';
|
||||
import {
|
||||
enableProfilerTimer,
|
||||
enableTransitionTracing,
|
||||
enableRenderableContext,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import {popHostContainer, popHostContext} from './ReactFiberHostContext';
|
||||
@@ -189,12 +188,7 @@ function unwindWork(
|
||||
popHostContainer(workInProgress);
|
||||
return null;
|
||||
case ContextProvider:
|
||||
let context: ReactContext<any>;
|
||||
if (enableRenderableContext) {
|
||||
context = workInProgress.type;
|
||||
} else {
|
||||
context = workInProgress.type._context;
|
||||
}
|
||||
const context: ReactContext<any> = workInProgress.type;
|
||||
popProvider(context, workInProgress);
|
||||
return null;
|
||||
case OffscreenComponent:
|
||||
@@ -286,12 +280,7 @@ function unwindInterruptedWork(
|
||||
popSuspenseListContext(interruptedWork);
|
||||
break;
|
||||
case ContextProvider:
|
||||
let context: ReactContext<any>;
|
||||
if (enableRenderableContext) {
|
||||
context = interruptedWork.type;
|
||||
} else {
|
||||
context = interruptedWork.type._context;
|
||||
}
|
||||
const context: ReactContext<any> = interruptedWork.type;
|
||||
popProvider(context, interruptedWork);
|
||||
break;
|
||||
case OffscreenComponent:
|
||||
|
||||
@@ -941,15 +941,11 @@ describe('ReactLazy', () => {
|
||||
</Suspense>,
|
||||
);
|
||||
await waitForThrow(
|
||||
gate('enableRenderableContext')
|
||||
? 'Element type is invalid. Received a promise that resolves to: Context.Provider. ' +
|
||||
'Lazy element type must resolve to a class or function.'
|
||||
: 'Element type is invalid. Received a promise that resolves to: Context.Consumer. ' +
|
||||
'Lazy element type must resolve to a class or function.',
|
||||
'Element type is invalid. Received a promise that resolves to: Context. ' +
|
||||
'Lazy element type must resolve to a class or function.',
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableRenderableContext
|
||||
it('throws with a useful error when wrapping Context.Consumer with lazy()', async () => {
|
||||
const Context = React.createContext(null);
|
||||
const BadLazy = lazy(() => fakeImport(Context.Consumer));
|
||||
|
||||
@@ -1358,7 +1358,6 @@ describe('ReactNewContext', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableRenderableContext || !__DEV__
|
||||
it('warns when passed a consumer', async () => {
|
||||
const Context = React.createContext(0);
|
||||
function Foo() {
|
||||
@@ -1657,7 +1656,6 @@ Context fuzz tester error! Copy and paste the following line into the test suite
|
||||
});
|
||||
});
|
||||
|
||||
// @gate enableRenderableContext
|
||||
it('should treat Context as Context.Provider', async () => {
|
||||
const BarContext = React.createContext({value: 'bar-initial'});
|
||||
expect(BarContext.Provider).toBe(BarContext);
|
||||
|
||||
+4
-15
@@ -13,7 +13,6 @@ import type {Fiber} from './ReactInternalTypes';
|
||||
import {
|
||||
disableLegacyMode,
|
||||
enableLegacyHidden,
|
||||
enableRenderableContext,
|
||||
enableViewTransition,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
|
||||
@@ -91,21 +90,11 @@ export default function getComponentNameFromFiber(fiber: Fiber): string | null {
|
||||
case CacheComponent:
|
||||
return 'Cache';
|
||||
case ContextConsumer:
|
||||
if (enableRenderableContext) {
|
||||
const consumer: ReactConsumerType<any> = (type: any);
|
||||
return getContextName(consumer._context) + '.Consumer';
|
||||
} else {
|
||||
const context: ReactContext<any> = (type: any);
|
||||
return getContextName(context) + '.Consumer';
|
||||
}
|
||||
const consumer: ReactConsumerType<any> = (type: any);
|
||||
return getContextName(consumer._context) + '.Consumer';
|
||||
case ContextProvider:
|
||||
if (enableRenderableContext) {
|
||||
const context: ReactContext<any> = (type: any);
|
||||
return getContextName(context) + '.Provider';
|
||||
} else {
|
||||
const provider = (type: any);
|
||||
return getContextName(provider._context) + '.Provider';
|
||||
}
|
||||
const context: ReactContext<any> = (type: any);
|
||||
return getContextName(context);
|
||||
case DehydratedFragment:
|
||||
return 'DehydratedFragment';
|
||||
case ForwardRef:
|
||||
|
||||
@@ -161,6 +161,9 @@ const deepProxyHandlers = {
|
||||
// reference.
|
||||
case 'defaultProps':
|
||||
return undefined;
|
||||
// React looks for debugInfo on thenables.
|
||||
case '_debugInfo':
|
||||
return undefined;
|
||||
// Avoid this attempting to be serialized.
|
||||
case 'toJSON':
|
||||
return undefined;
|
||||
@@ -210,6 +213,9 @@ function getReference(target: Function, name: string | symbol): $FlowFixMe {
|
||||
// reference.
|
||||
case 'defaultProps':
|
||||
return undefined;
|
||||
// React looks for debugInfo on thenables.
|
||||
case '_debugInfo':
|
||||
return undefined;
|
||||
// Avoid this attempting to be serialized.
|
||||
case 'toJSON':
|
||||
return undefined;
|
||||
|
||||
@@ -162,6 +162,9 @@ const deepProxyHandlers = {
|
||||
// reference.
|
||||
case 'defaultProps':
|
||||
return undefined;
|
||||
// React looks for debugInfo on thenables.
|
||||
case '_debugInfo':
|
||||
return undefined;
|
||||
// Avoid this attempting to be serialized.
|
||||
case 'toJSON':
|
||||
return undefined;
|
||||
@@ -211,6 +214,9 @@ function getReference(target: Function, name: string | symbol): $FlowFixMe {
|
||||
// reference.
|
||||
case 'defaultProps':
|
||||
return undefined;
|
||||
// React looks for debugInfo on thenables.
|
||||
case '_debugInfo':
|
||||
return undefined;
|
||||
// Avoid this attempting to be serialized.
|
||||
case 'toJSON':
|
||||
return undefined;
|
||||
|
||||
+7
-31
@@ -163,7 +163,6 @@ import {
|
||||
REACT_FRAGMENT_TYPE,
|
||||
REACT_FORWARD_REF_TYPE,
|
||||
REACT_MEMO_TYPE,
|
||||
REACT_PROVIDER_TYPE,
|
||||
REACT_CONTEXT_TYPE,
|
||||
REACT_CONSUMER_TYPE,
|
||||
REACT_SCOPE_TYPE,
|
||||
@@ -178,7 +177,6 @@ import {
|
||||
enableScopeAPI,
|
||||
enablePostpone,
|
||||
enableHalt,
|
||||
enableRenderableContext,
|
||||
disableDefaultPropsExceptForClasses,
|
||||
enableAsyncIterableChildren,
|
||||
enableViewTransition,
|
||||
@@ -2959,38 +2957,16 @@ function renderElement(
|
||||
renderMemo(request, task, keyPath, type, props, ref);
|
||||
return;
|
||||
}
|
||||
case REACT_PROVIDER_TYPE: {
|
||||
if (!enableRenderableContext) {
|
||||
const context: ReactContext<any> = (type: any)._context;
|
||||
renderContextProvider(request, task, keyPath, context, props);
|
||||
return;
|
||||
}
|
||||
// Fall through
|
||||
}
|
||||
case REACT_CONTEXT_TYPE: {
|
||||
if (enableRenderableContext) {
|
||||
const context = type;
|
||||
renderContextProvider(request, task, keyPath, context, props);
|
||||
return;
|
||||
} else {
|
||||
let context: ReactContext<any> = (type: any);
|
||||
if (__DEV__) {
|
||||
if ((context: any)._context !== undefined) {
|
||||
context = (context: any)._context;
|
||||
}
|
||||
}
|
||||
renderContextConsumer(request, task, keyPath, context, props);
|
||||
return;
|
||||
}
|
||||
const context = type;
|
||||
renderContextProvider(request, task, keyPath, context, props);
|
||||
return;
|
||||
}
|
||||
case REACT_CONSUMER_TYPE: {
|
||||
if (enableRenderableContext) {
|
||||
const context: ReactContext<any> = (type: ReactConsumerType<any>)
|
||||
._context;
|
||||
renderContextConsumer(request, task, keyPath, context, props);
|
||||
return;
|
||||
}
|
||||
// Fall through
|
||||
const context: ReactContext<any> = (type: ReactConsumerType<any>)
|
||||
._context;
|
||||
renderContextConsumer(request, task, keyPath, context, props);
|
||||
return;
|
||||
}
|
||||
case REACT_LAZY_TYPE: {
|
||||
renderLazyComponent(request, task, keyPath, type, props, ref);
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ export type PromiseNode = {
|
||||
start: number, // start time when the Promise was created
|
||||
end: number, // end time when the Promise was resolved.
|
||||
awaited: null | AsyncSequence, // the thing that ended up resolving this promise
|
||||
previous: null, // where we created the promise is not interesting since creating it doesn't mean waiting.
|
||||
previous: null | AsyncSequence, // represents what the last return of an async function depended on before returning
|
||||
};
|
||||
|
||||
export type AwaitNode = {
|
||||
|
||||
@@ -58,6 +58,12 @@ export function getThenableStateAfterSuspending(): ThenableState {
|
||||
return state;
|
||||
}
|
||||
|
||||
export function getTrackedThenablesAfterRendering(): null | Array<
|
||||
Thenable<any>,
|
||||
> {
|
||||
return thenableState;
|
||||
}
|
||||
|
||||
export const HooksDispatcher: Dispatcher = {
|
||||
readContext: (unsupportedContext: any),
|
||||
|
||||
|
||||
+190
-133
@@ -91,6 +91,7 @@ import {
|
||||
initAsyncDebugInfo,
|
||||
markAsyncSequenceRootTask,
|
||||
getCurrentAsyncSequence,
|
||||
getAsyncSequenceFromPromise,
|
||||
parseStackTrace,
|
||||
supportsComponentStorage,
|
||||
componentStorage,
|
||||
@@ -106,6 +107,7 @@ import {
|
||||
prepareToUseHooksForRequest,
|
||||
prepareToUseHooksForComponent,
|
||||
getThenableStateAfterSuspending,
|
||||
getTrackedThenablesAfterRendering,
|
||||
resetHooksForRequest,
|
||||
} from './ReactFlightHooks';
|
||||
import {DefaultAsyncDispatcher} from './flight/ReactFlightAsyncDispatcher';
|
||||
@@ -690,26 +692,14 @@ function serializeThenable(
|
||||
|
||||
switch (thenable.status) {
|
||||
case 'fulfilled': {
|
||||
if (__DEV__) {
|
||||
// If this came from Flight, forward any debug info into this new row.
|
||||
const debugInfo: ?ReactDebugInfo = (thenable: any)._debugInfo;
|
||||
if (debugInfo) {
|
||||
forwardDebugInfo(request, newTask, debugInfo);
|
||||
}
|
||||
}
|
||||
forwardDebugInfoFromThenable(request, newTask, thenable, null, null);
|
||||
// We have the resolved value, we can go ahead and schedule it for serialization.
|
||||
newTask.model = thenable.value;
|
||||
pingTask(request, newTask);
|
||||
return newTask.id;
|
||||
}
|
||||
case 'rejected': {
|
||||
if (__DEV__) {
|
||||
// If this came from Flight, forward any debug info into this new row.
|
||||
const debugInfo: ?ReactDebugInfo = (thenable: any)._debugInfo;
|
||||
if (debugInfo) {
|
||||
forwardDebugInfo(request, newTask, debugInfo);
|
||||
}
|
||||
}
|
||||
forwardDebugInfoFromThenable(request, newTask, thenable, null, null);
|
||||
const x = thenable.reason;
|
||||
erroredTask(request, newTask, x);
|
||||
return newTask.id;
|
||||
@@ -758,25 +748,16 @@ function serializeThenable(
|
||||
|
||||
thenable.then(
|
||||
value => {
|
||||
if (__DEV__) {
|
||||
// If this came from Flight, forward any debug info into this new row.
|
||||
const debugInfo: ?ReactDebugInfo = (thenable: any)._debugInfo;
|
||||
if (debugInfo) {
|
||||
forwardDebugInfo(request, newTask, debugInfo);
|
||||
}
|
||||
}
|
||||
forwardDebugInfoFromCurrentContext(request, newTask, thenable);
|
||||
newTask.model = value;
|
||||
pingTask(request, newTask);
|
||||
},
|
||||
reason => {
|
||||
if (__DEV__) {
|
||||
// If this came from Flight, forward any debug info into this new row.
|
||||
const debugInfo: ?ReactDebugInfo = (thenable: any)._debugInfo;
|
||||
if (debugInfo) {
|
||||
forwardDebugInfo(request, newTask, debugInfo);
|
||||
}
|
||||
}
|
||||
if (newTask.status === PENDING) {
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
// If this is async we need to time when this task finishes.
|
||||
newTask.timed = true;
|
||||
}
|
||||
// We expect that the only status it might be otherwise is ABORTED.
|
||||
// When we abort we emit chunks in each pending task slot and don't need
|
||||
// to do so again here.
|
||||
@@ -786,11 +767,6 @@ function serializeThenable(
|
||||
},
|
||||
);
|
||||
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
// If this is async we need to time when this task finishes.
|
||||
newTask.timed = true;
|
||||
}
|
||||
|
||||
return newTask.id;
|
||||
}
|
||||
|
||||
@@ -1056,13 +1032,21 @@ function readThenable<T>(thenable: Thenable<T>): T {
|
||||
throw thenable;
|
||||
}
|
||||
|
||||
function createLazyWrapperAroundWakeable(wakeable: Wakeable) {
|
||||
function createLazyWrapperAroundWakeable(
|
||||
request: Request,
|
||||
task: Task,
|
||||
wakeable: Wakeable,
|
||||
) {
|
||||
// This is a temporary fork of the `use` implementation until we accept
|
||||
// promises everywhere.
|
||||
const thenable: Thenable<mixed> = (wakeable: any);
|
||||
switch (thenable.status) {
|
||||
case 'fulfilled':
|
||||
case 'fulfilled': {
|
||||
forwardDebugInfoFromThenable(request, task, thenable, null, null);
|
||||
return thenable.value;
|
||||
}
|
||||
case 'rejected':
|
||||
forwardDebugInfoFromThenable(request, task, thenable, null, null);
|
||||
break;
|
||||
default: {
|
||||
if (typeof thenable.status === 'string') {
|
||||
@@ -1075,6 +1059,7 @@ function createLazyWrapperAroundWakeable(wakeable: Wakeable) {
|
||||
pendingThenable.status = 'pending';
|
||||
pendingThenable.then(
|
||||
fulfilledValue => {
|
||||
forwardDebugInfoFromCurrentContext(request, task, thenable);
|
||||
if (thenable.status === 'pending') {
|
||||
const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
|
||||
fulfilledThenable.status = 'fulfilled';
|
||||
@@ -1082,6 +1067,7 @@ function createLazyWrapperAroundWakeable(wakeable: Wakeable) {
|
||||
}
|
||||
},
|
||||
(error: mixed) => {
|
||||
forwardDebugInfoFromCurrentContext(request, task, thenable);
|
||||
if (thenable.status === 'pending') {
|
||||
const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
|
||||
rejectedThenable.status = 'rejected';
|
||||
@@ -1097,10 +1083,6 @@ function createLazyWrapperAroundWakeable(wakeable: Wakeable) {
|
||||
_payload: thenable,
|
||||
_init: readThenable,
|
||||
};
|
||||
if (__DEV__) {
|
||||
// If this came from React, transfer the debug info.
|
||||
lazyType._debugInfo = (thenable: any)._debugInfo || [];
|
||||
}
|
||||
return lazyType;
|
||||
}
|
||||
|
||||
@@ -1179,12 +1161,9 @@ function processServerComponentReturnValue(
|
||||
}
|
||||
}, voidHandler);
|
||||
}
|
||||
if (thenable.status === 'fulfilled') {
|
||||
return thenable.value;
|
||||
}
|
||||
// TODO: Once we accept Promises as children on the client, we can just return
|
||||
// the thenable here.
|
||||
return createLazyWrapperAroundWakeable(result);
|
||||
return createLazyWrapperAroundWakeable(request, task, result);
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
@@ -1341,12 +1320,7 @@ function renderFunctionComponent<Props>(
|
||||
|
||||
// Track when we started rendering this component.
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
task.timed = true;
|
||||
emitTimingChunk(
|
||||
request,
|
||||
componentDebugID,
|
||||
(task.time = performance.now()),
|
||||
);
|
||||
advanceTaskTime(request, task, performance.now());
|
||||
}
|
||||
|
||||
emitDebugChunk(request, componentDebugID, componentDebugInfo);
|
||||
@@ -1392,6 +1366,7 @@ function renderFunctionComponent<Props>(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
componentDebugInfo = (null: any);
|
||||
prepareToUseHooksForComponent(prevThenableState, null);
|
||||
// The secondArg is always undefined in Server Components since refs error early.
|
||||
const secondArg = undefined;
|
||||
@@ -1414,6 +1389,34 @@ function renderFunctionComponent<Props>(
|
||||
throw null;
|
||||
}
|
||||
|
||||
if (
|
||||
__DEV__ ||
|
||||
(enableProfilerTimer &&
|
||||
enableComponentPerformanceTrack &&
|
||||
enableAsyncDebugInfo)
|
||||
) {
|
||||
// Forward any debug information for any Promises that we use():ed during the render.
|
||||
// We do this at the end so that we don't keep doing this for each retry.
|
||||
const trackedThenables = getTrackedThenablesAfterRendering();
|
||||
if (trackedThenables !== null) {
|
||||
const stacks: Array<Error> =
|
||||
__DEV__ && enableAsyncDebugInfo
|
||||
? (trackedThenables: any)._stacks ||
|
||||
((trackedThenables: any)._stacks = [])
|
||||
: (null: any);
|
||||
for (let i = 0; i < trackedThenables.length; i++) {
|
||||
const stack = __DEV__ && enableAsyncDebugInfo ? stacks[i] : null;
|
||||
forwardDebugInfoFromThenable(
|
||||
request,
|
||||
task,
|
||||
trackedThenables[i],
|
||||
__DEV__ ? componentDebugInfo : null,
|
||||
stack,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply special cases.
|
||||
result = processServerComponentReturnValue(request, task, Component, result);
|
||||
|
||||
@@ -1890,8 +1893,8 @@ function visitAsyncNode(
|
||||
request: Request,
|
||||
task: Task,
|
||||
node: AsyncSequence,
|
||||
visited: Set<AsyncSequence | ReactDebugInfo>,
|
||||
cutOff: number,
|
||||
visited: Set<AsyncSequence>,
|
||||
): null | PromiseNode | IONode {
|
||||
if (visited.has(node)) {
|
||||
// It's possible to visit them same node twice when it's part of both an "awaited" path
|
||||
@@ -1900,11 +1903,11 @@ function visitAsyncNode(
|
||||
}
|
||||
visited.add(node);
|
||||
// First visit anything that blocked this sequence to start in the first place.
|
||||
if (node.previous !== null) {
|
||||
if (node.previous !== null && node.end > request.timeOrigin) {
|
||||
// We ignore the return value here because if it wasn't awaited in user space, then we don't log it.
|
||||
// It also means that it can just have been part of a previous component's render.
|
||||
// TODO: This means that some I/O can get lost that was still blocking the sequence.
|
||||
visitAsyncNode(request, task, node.previous, cutOff, visited);
|
||||
visitAsyncNode(request, task, node.previous, visited, cutOff);
|
||||
}
|
||||
switch (node.tag) {
|
||||
case IO_NODE: {
|
||||
@@ -1923,24 +1926,23 @@ function visitAsyncNode(
|
||||
const awaited = node.awaited;
|
||||
let match = null;
|
||||
if (awaited !== null) {
|
||||
const ioNode = visitAsyncNode(request, task, awaited, cutOff, visited);
|
||||
const ioNode = visitAsyncNode(request, task, awaited, visited, cutOff);
|
||||
if (ioNode !== null) {
|
||||
// This Promise was blocked on I/O. That's a signal that this Promise is interesting to log.
|
||||
// We don't log it yet though. We return it to be logged by the point where it's awaited.
|
||||
// The ioNode might be another PromiseNode in the case where none of the AwaitNode had
|
||||
// unfiltered stacks.
|
||||
if (
|
||||
if (ioNode.tag === PROMISE_NODE) {
|
||||
// If the ioNode was a Promise, then that means we found one in user space since otherwise
|
||||
// we would've returned an IO node. We assume this has the best stack.
|
||||
match = ioNode;
|
||||
} else if (
|
||||
filterStackTrace(request, parseStackTrace(node.stack, 1)).length ===
|
||||
0
|
||||
) {
|
||||
// Typically we assume that the outer most Promise that was awaited in user space has the
|
||||
// most actionable stack trace for the start of the operation. However, if this Promise
|
||||
// was created inside only third party code, then try to use the inner node instead.
|
||||
// This could happen if you pass a first party Promise into a third party to be awaited there.
|
||||
if (ioNode.end < 0) {
|
||||
// If we haven't defined an end time, use the resolve of the outer Promise.
|
||||
ioNode.end = node.end;
|
||||
}
|
||||
// If this Promise was created inside only third party code, then try to use
|
||||
// the inner I/O node instead. This could happen if third party calls into first
|
||||
// party to perform some I/O.
|
||||
match = ioNode;
|
||||
} else {
|
||||
match = node;
|
||||
@@ -1950,35 +1952,23 @@ function visitAsyncNode(
|
||||
// We need to forward after we visit awaited nodes because what ever I/O we requested that's
|
||||
// the thing that generated this node and its virtual children.
|
||||
const debugInfo = node.debugInfo;
|
||||
if (debugInfo !== null) {
|
||||
if (debugInfo !== null && !visited.has(debugInfo)) {
|
||||
visited.add(debugInfo);
|
||||
forwardDebugInfo(request, task, debugInfo);
|
||||
}
|
||||
return match;
|
||||
}
|
||||
case UNRESOLVED_AWAIT_NODE:
|
||||
// We could be inside the .then() which is about to resolve this node.
|
||||
// TODO: We could call emitAsyncSequence in a microtask to avoid this issue.
|
||||
// Fallthrough to the resolved path.
|
||||
case UNRESOLVED_AWAIT_NODE: {
|
||||
return null;
|
||||
}
|
||||
case AWAIT_NODE: {
|
||||
const awaited = node.awaited;
|
||||
let match = null;
|
||||
if (awaited !== null) {
|
||||
const ioNode = visitAsyncNode(request, task, awaited, cutOff, visited);
|
||||
const ioNode = visitAsyncNode(request, task, awaited, visited, cutOff);
|
||||
if (ioNode !== null) {
|
||||
const startTime: number = node.start;
|
||||
let endTime: number;
|
||||
if (node.tag === UNRESOLVED_AWAIT_NODE) {
|
||||
// If we haven't defined an end time, use the resolve of the inner Promise.
|
||||
// This can happen because the ping gets invoked before the await gets resolved.
|
||||
if (ioNode.end < node.start) {
|
||||
// If we're awaiting a resolved Promise it could have finished before we started.
|
||||
endTime = node.start;
|
||||
} else {
|
||||
endTime = ioNode.end;
|
||||
}
|
||||
} else {
|
||||
endTime = node.end;
|
||||
}
|
||||
const endTime: number = node.end;
|
||||
if (endTime <= request.timeOrigin) {
|
||||
// This was already resolved when we started this render. It must have been either something
|
||||
// that's part of a start up sequence or externally cached data. We exclude that information.
|
||||
@@ -2002,14 +1992,12 @@ function visitAsyncNode(
|
||||
match = ioNode;
|
||||
} else {
|
||||
// Outline the IO node.
|
||||
if (ioNode.end < 0) {
|
||||
ioNode.end = endTime;
|
||||
}
|
||||
serializeIONode(request, ioNode);
|
||||
|
||||
// We log the environment at the time when the last promise pigned ping which may
|
||||
// be later than what the environment was when we actually started awaiting.
|
||||
const env = (0, request.environmentName)();
|
||||
emitTimingChunk(request, task.id, startTime);
|
||||
advanceTaskTime(request, task, startTime);
|
||||
// Then emit a reference to us awaiting it in the current task.
|
||||
request.pendingChunks++;
|
||||
emitDebugChunk(request, task.id, {
|
||||
@@ -2018,24 +2006,16 @@ function visitAsyncNode(
|
||||
owner: node.owner,
|
||||
stack: stack,
|
||||
});
|
||||
emitTimingChunk(request, task.id, endTime);
|
||||
markOperationEndTime(request, task, endTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// We need to forward after we visit awaited nodes because what ever I/O we requested that's
|
||||
// the thing that generated this node and its virtual children.
|
||||
let debugInfo: null | ReactDebugInfo;
|
||||
if (node.tag === UNRESOLVED_AWAIT_NODE) {
|
||||
const promise = node.debugInfo.deref();
|
||||
debugInfo =
|
||||
promise === undefined || promise._debugInfo === undefined
|
||||
? null
|
||||
: promise._debugInfo;
|
||||
} else {
|
||||
debugInfo = node.debugInfo;
|
||||
}
|
||||
if (debugInfo !== null) {
|
||||
const debugInfo = node.debugInfo;
|
||||
if (debugInfo !== null && !visited.has(debugInfo)) {
|
||||
visited.add(debugInfo);
|
||||
forwardDebugInfo(request, task, debugInfo);
|
||||
}
|
||||
return match;
|
||||
@@ -2051,37 +2031,40 @@ function emitAsyncSequence(
|
||||
request: Request,
|
||||
task: Task,
|
||||
node: AsyncSequence,
|
||||
cutOff: number,
|
||||
alreadyForwardedDebugInfo: ?ReactDebugInfo,
|
||||
owner: null | ReactComponentInfo,
|
||||
stack: null | Error,
|
||||
): void {
|
||||
const visited: Set<AsyncSequence> = new Set();
|
||||
const awaitedNode = visitAsyncNode(request, task, node, cutOff, visited);
|
||||
const visited: Set<AsyncSequence | ReactDebugInfo> = new Set();
|
||||
if (__DEV__ && alreadyForwardedDebugInfo) {
|
||||
visited.add(alreadyForwardedDebugInfo);
|
||||
}
|
||||
const awaitedNode = visitAsyncNode(request, task, node, visited, task.time);
|
||||
if (awaitedNode !== null) {
|
||||
// Nothing in user space (unfiltered stack) awaited this.
|
||||
if (awaitedNode.end < 0) {
|
||||
// If this was I/O directly without a Promise, then it means that some custom Thenable
|
||||
// called our ping directly and not from a native .then(). We use the current ping time
|
||||
// as the end time and treat it as an await with no stack.
|
||||
// TODO: If this I/O is recurring then we really should have different entries for
|
||||
// each occurrence. Right now we'll only track the first time it is invoked.
|
||||
awaitedNode.end = performance.now();
|
||||
}
|
||||
serializeIONode(request, awaitedNode);
|
||||
request.pendingChunks++;
|
||||
// We log the environment at the time when we ping which may be later than what the
|
||||
// environment was when we actually started awaiting.
|
||||
const env = (0, request.environmentName)();
|
||||
// If we don't have any thing awaited, the time we started awaiting was internal
|
||||
// when we yielded after rendering. The cutOff time is basically that.
|
||||
const awaitStartTime = cutOff;
|
||||
// If the end time finished before we started, it could've been a cached thing so
|
||||
// we clamp it to the cutOff time. Effectively leading to a zero-time await.
|
||||
const awaitEndTime = awaitedNode.end < cutOff ? cutOff : awaitedNode.end;
|
||||
emitTimingChunk(request, task.id, awaitStartTime);
|
||||
emitDebugChunk(request, task.id, {
|
||||
// when we yielded after rendering. The current task time is basically that.
|
||||
const debugInfo: ReactAsyncInfo = {
|
||||
awaited: ((awaitedNode: any): ReactIOInfo), // This is deduped by this reference.
|
||||
env: env,
|
||||
});
|
||||
emitTimingChunk(request, task.id, awaitEndTime);
|
||||
};
|
||||
if (__DEV__) {
|
||||
if (owner != null) {
|
||||
// $FlowFixMe[cannot-write]
|
||||
debugInfo.owner = owner;
|
||||
}
|
||||
if (stack != null) {
|
||||
// $FlowFixMe[cannot-write]
|
||||
debugInfo.stack = filterStackTrace(request, parseStackTrace(stack, 1));
|
||||
}
|
||||
}
|
||||
emitDebugChunk(request, task.id, debugInfo);
|
||||
markOperationEndTime(request, task, awaitedNode.end);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2089,12 +2072,6 @@ function pingTask(request: Request, task: Task): void {
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
// If this was async we need to emit the time when it completes.
|
||||
task.timed = true;
|
||||
if (enableAsyncDebugInfo) {
|
||||
const sequence = getCurrentAsyncSequence();
|
||||
if (sequence !== null) {
|
||||
emitAsyncSequence(request, task, sequence, task.time);
|
||||
}
|
||||
}
|
||||
}
|
||||
const pingedTasks = request.pingedTasks;
|
||||
pingedTasks.push(task);
|
||||
@@ -4295,19 +4272,13 @@ function forwardDebugInfo(
|
||||
debugInfo: ReactDebugInfo,
|
||||
) {
|
||||
const id = task.id;
|
||||
const minimumTime =
|
||||
enableProfilerTimer && enableComponentPerformanceTrack ? task.time : 0;
|
||||
for (let i = 0; i < debugInfo.length; i++) {
|
||||
const info = debugInfo[i];
|
||||
if (typeof info.time === 'number') {
|
||||
// When forwarding time we need to ensure to convert it to the time space of the payload.
|
||||
// We clamp the time to the starting render of the current component. It's as if it took
|
||||
// no time to render and await if we reuse cached content.
|
||||
emitTimingChunk(
|
||||
request,
|
||||
id,
|
||||
info.time < minimumTime ? minimumTime : info.time,
|
||||
);
|
||||
markOperationEndTime(request, task, info.time);
|
||||
} else {
|
||||
if (typeof info.name === 'string') {
|
||||
// We outline this model eagerly so that we can refer to by reference as an owner.
|
||||
@@ -4367,6 +4338,58 @@ function forwardDebugInfo(
|
||||
}
|
||||
}
|
||||
|
||||
function forwardDebugInfoFromThenable(
|
||||
request: Request,
|
||||
task: Task,
|
||||
thenable: Thenable<any>,
|
||||
owner: null | ReactComponentInfo, // DEV-only
|
||||
stack: null | Error, // DEV-only
|
||||
): void {
|
||||
let debugInfo: ?ReactDebugInfo;
|
||||
if (__DEV__) {
|
||||
// If this came from Flight, forward any debug info into this new row.
|
||||
debugInfo = thenable._debugInfo;
|
||||
if (debugInfo) {
|
||||
forwardDebugInfo(request, task, debugInfo);
|
||||
}
|
||||
}
|
||||
if (
|
||||
enableProfilerTimer &&
|
||||
enableComponentPerformanceTrack &&
|
||||
enableAsyncDebugInfo
|
||||
) {
|
||||
const sequence = getAsyncSequenceFromPromise(thenable);
|
||||
if (sequence !== null) {
|
||||
emitAsyncSequence(request, task, sequence, debugInfo, owner, stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function forwardDebugInfoFromCurrentContext(
|
||||
request: Request,
|
||||
task: Task,
|
||||
thenable: Thenable<any>,
|
||||
): void {
|
||||
let debugInfo: ?ReactDebugInfo;
|
||||
if (__DEV__) {
|
||||
// If this came from Flight, forward any debug info into this new row.
|
||||
debugInfo = thenable._debugInfo;
|
||||
if (debugInfo) {
|
||||
forwardDebugInfo(request, task, debugInfo);
|
||||
}
|
||||
}
|
||||
if (
|
||||
enableProfilerTimer &&
|
||||
enableComponentPerformanceTrack &&
|
||||
enableAsyncDebugInfo
|
||||
) {
|
||||
const sequence = getCurrentAsyncSequence();
|
||||
if (sequence !== null) {
|
||||
emitAsyncSequence(request, task, sequence, debugInfo, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function emitTimingChunk(
|
||||
request: Request,
|
||||
id: number,
|
||||
@@ -4384,6 +4407,40 @@ function emitTimingChunk(
|
||||
request.completedRegularChunks.push(processedChunk);
|
||||
}
|
||||
|
||||
function advanceTaskTime(
|
||||
request: Request,
|
||||
task: Task,
|
||||
timestamp: number,
|
||||
): void {
|
||||
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
|
||||
return;
|
||||
}
|
||||
// Emits a timing chunk, if the new timestamp is higher than the previous timestamp of this task.
|
||||
if (timestamp > task.time) {
|
||||
emitTimingChunk(request, task.id, timestamp);
|
||||
task.time = timestamp;
|
||||
} else if (!task.timed) {
|
||||
// If it wasn't timed before, e.g. an outlined object, we need to emit the first timestamp and
|
||||
// it is now timed.
|
||||
emitTimingChunk(request, task.id, task.time);
|
||||
}
|
||||
task.timed = true;
|
||||
}
|
||||
|
||||
function markOperationEndTime(request: Request, task: Task, timestamp: number) {
|
||||
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
|
||||
return;
|
||||
}
|
||||
// This is like advanceTaskTime() but always emits a timing chunk even if it doesn't advance.
|
||||
// This ensures that the end time of the previous entry isn't implied to be the start of the next one.
|
||||
if (timestamp > task.time) {
|
||||
emitTimingChunk(request, task.id, timestamp);
|
||||
task.time = timestamp;
|
||||
} else {
|
||||
emitTimingChunk(request, task.id, task.time);
|
||||
}
|
||||
}
|
||||
|
||||
function emitChunk(
|
||||
request: Request,
|
||||
task: Task,
|
||||
@@ -4475,7 +4532,7 @@ function emitChunk(
|
||||
function erroredTask(request: Request, task: Task, error: mixed): void {
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
if (task.timed) {
|
||||
emitTimingChunk(request, task.id, (task.time = performance.now()));
|
||||
markOperationEndTime(request, task, performance.now());
|
||||
}
|
||||
}
|
||||
task.status = ERRORED;
|
||||
@@ -4558,7 +4615,7 @@ function retryTask(request: Request, task: Task): void {
|
||||
// We've finished rendering. Log the end time.
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
if (task.timed) {
|
||||
emitTimingChunk(request, task.id, (task.time = performance.now()));
|
||||
markOperationEndTime(request, task, performance.now());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4685,7 +4742,7 @@ function abortTask(task: Task, request: Request, errorId: number): void {
|
||||
// Track when we aborted this task as its end time.
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
if (task.timed) {
|
||||
emitTimingChunk(request, task.id, (task.time = performance.now()));
|
||||
markOperationEndTime(request, task, performance.now());
|
||||
}
|
||||
}
|
||||
// Instead of emitting an error per task.id, we emit a model that only
|
||||
|
||||
+108
-24
@@ -24,12 +24,36 @@ import {
|
||||
UNRESOLVED_AWAIT_NODE,
|
||||
} from './ReactFlightAsyncSequence';
|
||||
import {resolveOwner} from './flight/ReactFlightCurrentOwner';
|
||||
import {createHook, executionAsyncId} from 'async_hooks';
|
||||
import {createHook, executionAsyncId, AsyncResource} from 'async_hooks';
|
||||
import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
|
||||
|
||||
// $FlowFixMe[method-unbinding]
|
||||
const getAsyncId = AsyncResource.prototype.asyncId;
|
||||
|
||||
const pendingOperations: Map<number, AsyncSequence> =
|
||||
__DEV__ && enableAsyncDebugInfo ? new Map() : (null: any);
|
||||
|
||||
// Keep the last resolved await as a workaround for async functions missing data.
|
||||
let lastRanAwait: null | AwaitNode = null;
|
||||
|
||||
function resolvePromiseOrAwaitNode(
|
||||
unresolvedNode: UnresolvedAwaitNode | UnresolvedPromiseNode,
|
||||
endTime: number,
|
||||
): AwaitNode | PromiseNode {
|
||||
const resolvedNode: AwaitNode | PromiseNode = (unresolvedNode: any);
|
||||
resolvedNode.tag = ((unresolvedNode.tag === UNRESOLVED_PROMISE_NODE
|
||||
? PROMISE_NODE
|
||||
: AWAIT_NODE): any);
|
||||
// The Promise can be garbage collected after this so we should extract debugInfo first.
|
||||
const promise = unresolvedNode.debugInfo.deref();
|
||||
resolvedNode.debugInfo =
|
||||
promise === undefined || promise._debugInfo === undefined
|
||||
? null
|
||||
: promise._debugInfo;
|
||||
resolvedNode.end = endTime;
|
||||
return resolvedNode;
|
||||
}
|
||||
|
||||
// Initialize the tracing of async operations.
|
||||
// We do this globally since the async work can potentially eagerly
|
||||
// start before the first request and once requests start they can interleave.
|
||||
@@ -129,42 +153,76 @@ export function initAsyncDebugInfo(): void {
|
||||
}
|
||||
pendingOperations.set(asyncId, node);
|
||||
},
|
||||
before(asyncId: number): void {
|
||||
const node = pendingOperations.get(asyncId);
|
||||
if (node !== undefined) {
|
||||
switch (node.tag) {
|
||||
case IO_NODE: {
|
||||
lastRanAwait = null;
|
||||
// Log the end time when we resolved the I/O. This can happen
|
||||
// more than once if it's a recurring resource like a connection.
|
||||
const ioNode: IONode = (node: any);
|
||||
ioNode.end = performance.now();
|
||||
break;
|
||||
}
|
||||
case UNRESOLVED_AWAIT_NODE: {
|
||||
// If we begin before we resolve, that means that this is actually already resolved but
|
||||
// the promiseResolve hook is called at the end of the execution. So we track the time
|
||||
// in the before call instead.
|
||||
// $FlowFixMe
|
||||
lastRanAwait = resolvePromiseOrAwaitNode(node, performance.now());
|
||||
break;
|
||||
}
|
||||
case AWAIT_NODE: {
|
||||
lastRanAwait = node;
|
||||
break;
|
||||
}
|
||||
case UNRESOLVED_PROMISE_NODE: {
|
||||
// We typically don't expected Promises to have an execution scope since only the awaits
|
||||
// have a then() callback. However, this can happen for native async functions. The last
|
||||
// piece of code that executes the return after the last await has the execution context
|
||||
// of the Promise.
|
||||
const resolvedNode = resolvePromiseOrAwaitNode(
|
||||
node,
|
||||
performance.now(),
|
||||
);
|
||||
// We are missing information about what this was unblocked by but we can guess that it
|
||||
// was whatever await we ran last since this will continue in a microtask after that.
|
||||
// This is not perfect because there could potentially be other microtasks getting in
|
||||
// between.
|
||||
resolvedNode.previous = lastRanAwait;
|
||||
lastRanAwait = null;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
lastRanAwait = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
promiseResolve(asyncId: number): void {
|
||||
const node = pendingOperations.get(asyncId);
|
||||
if (node !== undefined) {
|
||||
let resolvedNode: AwaitNode | PromiseNode;
|
||||
switch (node.tag) {
|
||||
case UNRESOLVED_AWAIT_NODE: {
|
||||
const awaitNode: AwaitNode = (node: any);
|
||||
awaitNode.tag = AWAIT_NODE;
|
||||
resolvedNode = awaitNode;
|
||||
break;
|
||||
}
|
||||
case UNRESOLVED_AWAIT_NODE:
|
||||
case UNRESOLVED_PROMISE_NODE: {
|
||||
const promiseNode: PromiseNode = (node: any);
|
||||
promiseNode.tag = PROMISE_NODE;
|
||||
resolvedNode = promiseNode;
|
||||
resolvedNode = resolvePromiseOrAwaitNode(node, performance.now());
|
||||
break;
|
||||
}
|
||||
case IO_NODE:
|
||||
case AWAIT_NODE:
|
||||
case PROMISE_NODE: {
|
||||
// We already resolved this in the before hook.
|
||||
resolvedNode = node;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// eslint-disable-next-line react-internal/prod-error-codes
|
||||
throw new Error(
|
||||
'A Promise should never be an IO_NODE. This is a bug in React.',
|
||||
);
|
||||
default:
|
||||
// eslint-disable-next-line react-internal/prod-error-codes
|
||||
throw new Error(
|
||||
'A Promise should never be resolved twice. This is a bug in React or Node.js.',
|
||||
);
|
||||
}
|
||||
// Log the end time when we resolved the promise.
|
||||
resolvedNode.end = performance.now();
|
||||
// The Promise can be garbage collected after this so we should extract debugInfo first.
|
||||
const promise = node.debugInfo.deref();
|
||||
resolvedNode.debugInfo =
|
||||
promise === undefined || promise._debugInfo === undefined
|
||||
? null
|
||||
: promise._debugInfo;
|
||||
const currentAsyncId = executionAsyncId();
|
||||
if (asyncId !== currentAsyncId) {
|
||||
// If the promise was not resolved by itself, then that means that
|
||||
@@ -205,3 +263,29 @@ export function getCurrentAsyncSequence(): null | AsyncSequence {
|
||||
}
|
||||
return currentNode;
|
||||
}
|
||||
|
||||
export function getAsyncSequenceFromPromise(
|
||||
promise: any,
|
||||
): null | AsyncSequence {
|
||||
if (!__DEV__ || !enableAsyncDebugInfo) {
|
||||
return null;
|
||||
}
|
||||
// A Promise is conceptually an AsyncResource but doesn't have its own methods.
|
||||
// We use this hack to extract the internal asyncId off the Promise.
|
||||
let asyncId: void | number;
|
||||
try {
|
||||
asyncId = getAsyncId.call(promise);
|
||||
} catch (x) {
|
||||
// Ignore errors extracting the ID. We treat it as missing.
|
||||
// This could happen if our hack stops working or in the case where this is
|
||||
// a Proxy that throws such as our own ClientReference proxies.
|
||||
}
|
||||
if (asyncId === undefined) {
|
||||
return null;
|
||||
}
|
||||
const node = pendingOperations.get(asyncId);
|
||||
if (node === undefined) {
|
||||
return null;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
@@ -15,3 +15,8 @@ export function markAsyncSequenceRootTask(): void {}
|
||||
export function getCurrentAsyncSequence(): null | AsyncSequence {
|
||||
return null;
|
||||
}
|
||||
export function getAsyncSequenceFromPromise(
|
||||
promise: any,
|
||||
): null | AsyncSequence {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,9 @@ const proxyHandlers = {
|
||||
// reference.
|
||||
case 'defaultProps':
|
||||
return undefined;
|
||||
// React looks for debugInfo on thenables.
|
||||
case '_debugInfo':
|
||||
return undefined;
|
||||
// Avoid this attempting to be serialized.
|
||||
case 'toJSON':
|
||||
return undefined;
|
||||
|
||||
+8
-1
@@ -20,9 +20,11 @@ import type {
|
||||
RejectedThenable,
|
||||
} from 'shared/ReactTypes';
|
||||
|
||||
import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import noop from 'shared/noop';
|
||||
|
||||
export opaque type ThenableState = Array<Thenable<any>>;
|
||||
export type ThenableState = Array<Thenable<any>>;
|
||||
|
||||
// An error that is thrown (e.g. by `use`) to trigger Suspense. If we
|
||||
// detect this is caught by userspace, we'll log a warning in development.
|
||||
@@ -50,6 +52,11 @@ export function trackUsedThenable<T>(
|
||||
const previous = thenableState[index];
|
||||
if (previous === undefined) {
|
||||
thenableState.push(thenable);
|
||||
if (__DEV__ && enableAsyncDebugInfo) {
|
||||
const stacks: Array<Error> =
|
||||
(thenableState: any)._stacks || ((thenableState: any)._stacks = []);
|
||||
stacks.push(new Error());
|
||||
}
|
||||
} else {
|
||||
if (previous !== thenable) {
|
||||
// Reuse the previous thenable, and drop the new one. We can assume
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,14 +7,9 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import {
|
||||
REACT_PROVIDER_TYPE,
|
||||
REACT_CONSUMER_TYPE,
|
||||
REACT_CONTEXT_TYPE,
|
||||
} from 'shared/ReactSymbols';
|
||||
import {REACT_CONSUMER_TYPE, REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
|
||||
|
||||
import type {ReactContext} from 'shared/ReactTypes';
|
||||
import {enableRenderableContext} from 'shared/ReactFeatureFlags';
|
||||
|
||||
export function createContext<T>(defaultValue: T): ReactContext<T> {
|
||||
// TODO: Second argument used to be an optional `calculateChangedBits`
|
||||
@@ -37,73 +32,11 @@ export function createContext<T>(defaultValue: T): ReactContext<T> {
|
||||
Consumer: (null: any),
|
||||
};
|
||||
|
||||
if (enableRenderableContext) {
|
||||
context.Provider = context;
|
||||
context.Consumer = {
|
||||
$$typeof: REACT_CONSUMER_TYPE,
|
||||
_context: context,
|
||||
};
|
||||
} else {
|
||||
(context: any).Provider = {
|
||||
$$typeof: REACT_PROVIDER_TYPE,
|
||||
_context: context,
|
||||
};
|
||||
if (__DEV__) {
|
||||
const Consumer: any = {
|
||||
$$typeof: REACT_CONTEXT_TYPE,
|
||||
_context: context,
|
||||
};
|
||||
Object.defineProperties(Consumer, {
|
||||
Provider: {
|
||||
get() {
|
||||
return context.Provider;
|
||||
},
|
||||
set(_Provider: any) {
|
||||
context.Provider = _Provider;
|
||||
},
|
||||
},
|
||||
_currentValue: {
|
||||
get() {
|
||||
return context._currentValue;
|
||||
},
|
||||
set(_currentValue: T) {
|
||||
context._currentValue = _currentValue;
|
||||
},
|
||||
},
|
||||
_currentValue2: {
|
||||
get() {
|
||||
return context._currentValue2;
|
||||
},
|
||||
set(_currentValue2: T) {
|
||||
context._currentValue2 = _currentValue2;
|
||||
},
|
||||
},
|
||||
_threadCount: {
|
||||
get() {
|
||||
return context._threadCount;
|
||||
},
|
||||
set(_threadCount: number) {
|
||||
context._threadCount = _threadCount;
|
||||
},
|
||||
},
|
||||
Consumer: {
|
||||
get() {
|
||||
return context.Consumer;
|
||||
},
|
||||
},
|
||||
displayName: {
|
||||
get() {
|
||||
return context.displayName;
|
||||
},
|
||||
set(displayName: void | string) {},
|
||||
},
|
||||
});
|
||||
(context: any).Consumer = Consumer;
|
||||
} else {
|
||||
(context: any).Consumer = context;
|
||||
}
|
||||
}
|
||||
|
||||
context.Provider = context;
|
||||
context.Consumer = {
|
||||
$$typeof: REACT_CONSUMER_TYPE,
|
||||
_context: context,
|
||||
};
|
||||
if (__DEV__) {
|
||||
context._currentRenderer = null;
|
||||
context._currentRenderer2 = null;
|
||||
|
||||
@@ -490,7 +490,6 @@ describe('ReactContextValidator', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate enableRenderableContext || !__DEV__
|
||||
it('should warn if an invalid contextType is defined', async () => {
|
||||
const Context = React.createContext();
|
||||
class ComponentA extends React.Component {
|
||||
|
||||
@@ -204,9 +204,6 @@ export const enableReactTestRendererWarning = true;
|
||||
// before removing them in stable in the next Major
|
||||
export const disableLegacyMode = true;
|
||||
|
||||
// Make <Context> equivalent to <Context.Provider> instead of <Context.Consumer>
|
||||
export const enableRenderableContext = true;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Chopping Block
|
||||
//
|
||||
|
||||
@@ -22,7 +22,6 @@ export const REACT_PORTAL_TYPE: symbol = Symbol.for('react.portal');
|
||||
export const REACT_FRAGMENT_TYPE: symbol = Symbol.for('react.fragment');
|
||||
export const REACT_STRICT_MODE_TYPE: symbol = Symbol.for('react.strict_mode');
|
||||
export const REACT_PROFILER_TYPE: symbol = Symbol.for('react.profiler');
|
||||
export const REACT_PROVIDER_TYPE: symbol = Symbol.for('react.provider'); // TODO: Delete with enableRenderableContext
|
||||
export const REACT_CONSUMER_TYPE: symbol = Symbol.for('react.consumer');
|
||||
export const REACT_CONTEXT_TYPE: symbol = Symbol.for('react.context');
|
||||
export const REACT_FORWARD_REF_TYPE: symbol = Symbol.for('react.forward_ref');
|
||||
|
||||
@@ -58,7 +58,6 @@ export const enableProfilerCommitHooks = __PROFILE__;
|
||||
export const enableProfilerNestedUpdatePhase = __PROFILE__;
|
||||
export const enableProfilerTimer = __PROFILE__;
|
||||
export const enableReactTestRendererWarning = false;
|
||||
export const enableRenderableContext = true;
|
||||
export const enableRetryLaneExpiration = false;
|
||||
export const enableSchedulingProfiler = __PROFILE__;
|
||||
export const enableComponentPerformanceTrack = false;
|
||||
|
||||
@@ -43,7 +43,6 @@ export const enableObjectFiber = false;
|
||||
export const enablePersistedModeClonedFlag = false;
|
||||
export const enablePostpone = false;
|
||||
export const enableReactTestRendererWarning = false;
|
||||
export const enableRenderableContext = true;
|
||||
export const enableRetryLaneExpiration = false;
|
||||
export const enableSchedulingProfiler = __PROFILE__;
|
||||
export const enableComponentPerformanceTrack = false;
|
||||
|
||||
@@ -89,7 +89,6 @@ export const enableFragmentRefs = false;
|
||||
export const disableLegacyMode = true;
|
||||
export const disableLegacyContext = true;
|
||||
export const disableLegacyContextForFunctionComponents = true;
|
||||
export const enableRenderableContext = true;
|
||||
export const enableReactTestRendererWarning = true;
|
||||
export const disableDefaultPropsExceptForClasses = true;
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ export const enableProfilerCommitHooks = __PROFILE__;
|
||||
export const enableProfilerNestedUpdatePhase = __PROFILE__;
|
||||
export const enableProfilerTimer = __PROFILE__;
|
||||
export const enableReactTestRendererWarning = false;
|
||||
export const enableRenderableContext = true;
|
||||
export const enableRetryLaneExpiration = false;
|
||||
export const enableSchedulingProfiler = __PROFILE__;
|
||||
export const enableComponentPerformanceTrack = false;
|
||||
|
||||
@@ -38,7 +38,6 @@ export const enableUseEffectEventHook = false;
|
||||
export const favorSafetyOverHydrationPerf = true;
|
||||
export const enableLegacyFBSupport = false;
|
||||
export const enableMoveBefore = false;
|
||||
export const enableRenderableContext = false;
|
||||
export const enableHiddenSubtreeInsertionEffectCleanup = true;
|
||||
|
||||
export const enableRetryLaneExpiration = false;
|
||||
|
||||
@@ -21,7 +21,6 @@ export const enableDO_NOT_USE_disableStrictPassiveEffect = __VARIANT__;
|
||||
export const enableHiddenSubtreeInsertionEffectCleanup = __VARIANT__;
|
||||
export const enableNoCloningMemoCache = __VARIANT__;
|
||||
export const enableObjectFiber = __VARIANT__;
|
||||
export const enableRenderableContext = __VARIANT__;
|
||||
export const enableRetryLaneExpiration = __VARIANT__;
|
||||
export const enableTransitionTracing = __VARIANT__;
|
||||
export const favorSafetyOverHydrationPerf = __VARIANT__;
|
||||
|
||||
@@ -24,7 +24,6 @@ export const {
|
||||
enableInfiniteRenderLoopDetection,
|
||||
enableNoCloningMemoCache,
|
||||
enableObjectFiber,
|
||||
enableRenderableContext,
|
||||
enableRetryLaneExpiration,
|
||||
enableTransitionTracing,
|
||||
enableTrustedTypesIntegration,
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
REACT_PORTAL_TYPE,
|
||||
REACT_MEMO_TYPE,
|
||||
REACT_PROFILER_TYPE,
|
||||
REACT_PROVIDER_TYPE,
|
||||
REACT_STRICT_MODE_TYPE,
|
||||
REACT_SUSPENSE_TYPE,
|
||||
REACT_SUSPENSE_LIST_TYPE,
|
||||
@@ -30,7 +29,6 @@ import {
|
||||
|
||||
import {
|
||||
enableTransitionTracing,
|
||||
enableRenderableContext,
|
||||
enableViewTransition,
|
||||
} from './ReactFeatureFlags';
|
||||
|
||||
@@ -106,27 +104,12 @@ export default function getComponentNameFromType(type: mixed): string | null {
|
||||
switch (type.$$typeof) {
|
||||
case REACT_PORTAL_TYPE:
|
||||
return 'Portal';
|
||||
case REACT_PROVIDER_TYPE:
|
||||
if (enableRenderableContext) {
|
||||
return null;
|
||||
} else {
|
||||
const provider = (type: any);
|
||||
return getContextName(provider._context) + '.Provider';
|
||||
}
|
||||
case REACT_CONTEXT_TYPE:
|
||||
const context: ReactContext<any> = (type: any);
|
||||
if (enableRenderableContext) {
|
||||
return getContextName(context) + '.Provider';
|
||||
} else {
|
||||
return getContextName(context) + '.Consumer';
|
||||
}
|
||||
return getContextName(context);
|
||||
case REACT_CONSUMER_TYPE:
|
||||
if (enableRenderableContext) {
|
||||
const consumer: ReactConsumerType<any> = (type: any);
|
||||
return getContextName(consumer._context) + '.Consumer';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
const consumer: ReactConsumerType<any> = (type: any);
|
||||
return getContextName(consumer._context) + '.Consumer';
|
||||
case REACT_FORWARD_REF_TYPE:
|
||||
return getWrappedName(type, type.render, 'ForwardRef');
|
||||
case REACT_MEMO_TYPE:
|
||||
|
||||
@@ -356,7 +356,9 @@ declare module 'async_hooks' {
|
||||
run<R>(store: T, callback: (...args: any[]) => R, ...args: any[]): R;
|
||||
enterWith(store: T): void;
|
||||
}
|
||||
declare interface AsyncResource {}
|
||||
declare class AsyncResource {
|
||||
asyncId(): number;
|
||||
}
|
||||
declare function executionAsyncId(): number;
|
||||
declare function executionAsyncResource(): AsyncResource;
|
||||
declare function triggerAsyncId(): number;
|
||||
|
||||
Reference in New Issue
Block a user