diff --git a/.github/workflows/runtime_prereleases.yml b/.github/workflows/runtime_prereleases.yml
index ee8dd72ce9..a97add8cbc 100644
--- a/.github/workflows/runtime_prereleases.yml
+++ b/.github/workflows/runtime_prereleases.yml
@@ -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
diff --git a/.github/workflows/runtime_prereleases_manual.yml b/.github/workflows/runtime_prereleases_manual.yml
index 71e25ba073..407d931e90 100644
--- a/.github/workflows/runtime_prereleases_manual.yml
+++ b/.github/workflows/runtime_prereleases_manual.yml
@@ -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 }}
diff --git a/.github/workflows/runtime_prereleases_nightly.yml b/.github/workflows/runtime_prereleases_nightly.yml
index a38e241d53..f13a92e46f 100644
--- a/.github/workflows/runtime_prereleases_nightly.yml
+++ b/.github/workflows/runtime_prereleases_nightly.yml
@@ -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 }}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-ref-prefix-postfix-operator.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-ref-prefix-postfix-operator.expect.md
new file mode 100644
index 0000000000..ccfc451750
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-ref-prefix-postfix-operator.expect.md
@@ -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: [],
+};
+
+```
+
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-ref-prefix-postfix-operator.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-ref-prefix-postfix-operator.js
new file mode 100644
index 0000000000..a7c1fad8bf
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-ref-prefix-postfix-operator.js
@@ -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: [],
+};
diff --git a/compiler/packages/snap/src/SproutTodoFilter.ts b/compiler/packages/snap/src/SproutTodoFilter.ts
index 3db3210a99..02cb3775cb 100644
--- a/compiler/packages/snap/src/SproutTodoFilter.ts
+++ b/compiler/packages/snap/src/SproutTodoFilter.ts
@@ -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',
diff --git a/fixtures/flight/src/App.js b/fixtures/flight/src/App.js
index 833c655cbf..2f29de7aba 100644
--- a/fixtures/flight/src/App.js
+++ b/fixtures/flight/src/App.js
@@ -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
{children}
;
}
diff --git a/fixtures/view-transition/src/components/App.js b/fixtures/view-transition/src/components/App.js
index 275e594d87..dd8dcb73a2 100644
--- a/fixtures/view-transition/src/components/App.js
+++ b/fixtures/view-transition/src/components/App.js
@@ -4,6 +4,7 @@ import React, {
useEffect,
useState,
unstable_addTransitionType as addTransitionType,
+ use,
} from 'react';
import Chrome from './Chrome';
diff --git a/fixtures/view-transition/src/components/NestedReveal.js b/fixtures/view-transition/src/components/NestedReveal.js
new file mode 100644
index 0000000000..497f4430f6
--- /dev/null
+++ b/fixtures/view-transition/src/components/NestedReveal.js
@@ -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 (
+
+ Shell
+
+ Level 1
+
+
+
+ Level 2
+
+
+
+
+ );
+}
diff --git a/fixtures/view-transition/src/components/Page.js b/fixtures/view-transition/src/components/Page.js
index 39d0803af7..c0d6f7a0a2 100644
--- a/fixtures/view-transition/src/components/Page.js
+++ b/fixtures/view-transition/src/components/Page.js
@@ -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}) {
+
);
}
diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js
index a69ede9efd..1171f933d2 100644
--- a/packages/react-client/src/ReactFlightClient.js
+++ b/packages/react-client/src/ReactFlightClient.js
@@ -2902,6 +2902,46 @@ function resolveTypedArray(
resolveBuffer(response, id, view);
}
+function logComponentInfo(
+ response: Response,
+ root: SomeChunk,
+ 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,
@@ -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;
diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js
index 6a0a37b787..40de7ca51e 100644
--- a/packages/react-client/src/ReactFlightReplyClient.js
+++ b/packages/react-client/src/ReactFlightReplyClient.js
@@ -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),
diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js
index eb354aba58..49968b3359 100644
--- a/packages/react-client/src/__tests__/ReactFlight-test.js
+++ b/packages/react-client/src/__tests__/ReactFlight-test.js
@@ -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 {text.toUpperCase()}
;
+ }
+
+ const thirdPartyTransport = ReactNoopFlightServer.render(
+ ,
+ {
+ environmentName: 'third-party',
+ },
+ );
+
+ const transport = ReactNoopFlightServer.render(
+ ,
+ );
+
+ await act(async () => {
+ const promise = ReactNoopFlightClient.read(transport);
+ expect(getDebugInfo(promise)).toEqual(
+ __DEV__
+ ? [
+ {time: 16},
+ {
+ name: 'ServerComponent',
+ env: 'Server',
+ key: null,
+ stack: ' in Object. (at **)',
+ props: {
+ transport: expect.arrayContaining([]),
+ },
+ },
+ {time: 16},
+ {
+ name: 'ThirdPartyComponent',
+ env: 'third-party',
+ key: null,
+ stack: ' in Object. (at **)',
+ props: {},
+ },
+ {time: 16},
+ {time: 17},
+ ]
+ : undefined,
+ );
+ const result = await promise;
+ ReactNoop.render(result);
+ });
+
+ expect(ReactNoop).toMatchRenderedOutput(HI
);
+ });
+
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.
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js
index 1ba61c52dd..67cf50a074 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js
@@ -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(DEFAULT_INDENTATION_SIZE);
+ const indentationSizeRef = useRef(MAX_INDENTATION_SIZE);
const prevListWidthRef = useRef(0);
const prevOwnerIDRef = useRef(ownerID);
const divRef = useRef(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.
diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js
index 0536a821c7..5d92a86e2e 100644
--- a/packages/react-devtools-shared/src/utils.js
+++ b/packages/react-devtools-shared/src/utils.js
@@ -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> = 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';
diff --git a/packages/react-devtools-shell/index.html b/packages/react-devtools-shell/index.html
index 4cde55278a..ce381a7345 100644
--- a/packages/react-devtools-shell/index.html
+++ b/packages/react-devtools-shell/index.html
@@ -1,74 +1,213 @@
-
-
- React DevTools
-
-
-
-
-
+
+
+ React DevTools
+
+
+
+
+
+
+
+
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+
+
+
+
+