From 0763c049c6b9533368b725fe7f57617fa603f9be Mon Sep 17 00:00:00 2001 From: Josh Story Date: Wed, 5 Apr 2023 13:21:48 -0700 Subject: [PATCH] Flight support for Float Supporting Float methods such as ReactDOM.preload() are challenging for flight because it does not have an easy means to convey direct executions in other environments. Because the flight wire format is a JSON-like serialization that is expected to be rendered it currently only describes renderable elements. We need a way to convey a function invocation that gets run in the context of the client environment whether that is Fizz or Fiber. Fiber is somewhat straightforward because the HostDispatcher is always active and we can just have the FlightClient dispatch the serialized directive. Fizz is much more challenging becaue the dispatcher is always scoped but the specific request the dispatch belongs to is not readily available. Environments that support AsyncLocalStorage (or in the future AsyncContext) we will use this to be able to resolve directives in Fizz to the appropriate Request. For other environments directives will be elided. Right now this is pragmatic and non-breaking because all directives are opportunistic and non-critical. If this changes in the future we will need to reconsider how widespread support for async context tracking is. For Flight, if AsyncLocalStorage is available Float methods can be called before and after await points and be expected to work. If AsyncLocalStorage is not available float methods called in the sync phase of a component render will be captured but anything after an await point will be a noop. If a float call is dropped in this manner a DEV warning should help you realize your code may need to be modified. --- .../react-client/src/ReactFlightClient.js | 12 + .../src/ReactFlightClientStream.js | 5 + .../forks/ReactFlightClientConfig.custom.js | 1 + .../src/client/ReactFiberConfigDOM.js | 20 +- .../ReactDOMLegacyServerStreamConfig.js | 3 - .../src/server/ReactFizzConfigDOM.js | 117 +++++---- .../src/server/ReactFizzConfigDOMLegacy.js | 3 +- .../src/server/ReactFlightServerConfigDOM.js | 104 ++++++++ .../src/shared/ReactFlightClientConfigDOM.js | 36 ++- packages/react-dom/src/ReactDOMDispatcher.js | 30 +++ packages/react-dom/src/ReactDOMFloat.js | 57 ++++- .../react-dom/src/ReactDOMSharedInternals.js | 4 +- .../src/__tests__/ReactDOMFloat-test.js | 2 +- packages/react-dom/src/client/ReactDOMRoot.js | 10 +- .../src/server/ReactFizzConfigNative.js | 3 +- .../server/ReactFlightServerConfigNative.js | 4 + .../src/ReactNoopFlightServer.js | 1 + .../src/ReactNoopServer.js | 3 +- .../src/ReactFlightDOMRelayClient.js | 4 + .../src/ReactFlightDOMRelayProtocol.js | 2 + .../src/ReactFlightServerConfigDOMRelay.js | 13 +- .../src/ReactServerStreamConfigFB.js | 3 +- .../src/__tests__/ReactFlightDOM-test.js | 229 ++++++++++++++++++ .../__tests__/ReactFlightDOMBrowser-test.js | 128 ++++++++++ .../src/ReactFlightClientConfigNativeRelay.js | 2 + .../src/ReactFlightServerConfigNativeRelay.js | 13 +- .../src/ReactFizzCurrentRequest.js | 34 +++ .../react-server/src/ReactFizzResources.js | 26 ++ packages/react-server/src/ReactFizzServer.js | 31 ++- packages/react-server/src/ReactFlightCache.js | 25 +- .../src/ReactFlightCurrentRequest.js | 37 +++ .../react-server/src/ReactFlightDirectives.js | 25 ++ .../react-server/src/ReactFlightServer.js | 58 ++++- .../ReactFlightServerConfigBundlerCustom.js | 1 + .../src/ReactFlightServerConfigStream.js | 16 +- .../src/ReactServerStreamConfigBrowser.js | 4 - .../src/ReactServerStreamConfigBun.js | 4 - .../src/ReactServerStreamConfigEdge.js | 5 - .../src/ReactServerStreamConfigNode.js | 6 +- .../src/forks/ReactFizzConfig.custom.js | 8 +- .../src/forks/ReactFizzConfig.dom-browser.js | 4 + .../src/forks/ReactFizzConfig.dom-bun.js | 4 + .../forks/ReactFizzConfig.dom-edge-webpack.js | 7 + .../src/forks/ReactFizzConfig.dom-legacy.js | 4 + .../forks/ReactFizzConfig.dom-node-webpack.js | 7 + .../src/forks/ReactFizzConfig.dom-node.js | 8 + .../src/forks/ReactFizzConfig.dom-relay.js | 4 + .../src/forks/ReactFizzConfig.native-relay.js | 4 + .../forks/ReactFlightServerConfig.custom.js | 6 + .../ReactFlightServerConfig.dom-browser.js | 5 + .../forks/ReactFlightServerConfig.dom-bun.js | 5 + ...eactFlightServerConfig.dom-edge-webpack.js | 7 + .../ReactFlightServerConfig.dom-legacy.js | 5 + ...eactFlightServerConfig.dom-node-webpack.js | 7 + .../forks/ReactFlightServerConfig.dom-node.js | 8 + .../forks/ReactServerStreamConfig.custom.js | 2 - scripts/error-codes/codes.json | 3 +- scripts/rollup/bundles.js | 8 +- 58 files changed, 1016 insertions(+), 171 deletions(-) create mode 100644 packages/react-dom/src/ReactDOMDispatcher.js create mode 100644 packages/react-server/src/ReactFizzCurrentRequest.js create mode 100644 packages/react-server/src/ReactFizzResources.js create mode 100644 packages/react-server/src/ReactFlightCurrentRequest.js create mode 100644 packages/react-server/src/ReactFlightDirectives.js diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index ec1e5d34e7c7c..4b14a3f60fbe5 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -18,11 +18,14 @@ import type { SSRManifest, } from './ReactFlightClientConfig'; +import type {Directive} from 'react-server/src/ReactFlightServerConfig'; + import { resolveClientReference, preloadModule, requireModule, parseModel, + dispatchDirective, } from './ReactFlightClientConfig'; import {knownServerReferences} from './ReactFlightServerReferenceRegistry'; @@ -778,6 +781,15 @@ export function resolveErrorDev( } } +export function resolveDirective( + response: Response, + id: number, + model: UninitializedModel, +): void { + const directive = parseModel(response, model); + dispatchDirective(directive); +} + export function close(response: Response): void { // In case there are any remaining unresolved chunks, they won't // be resolved now. So we need to issue an error to those. diff --git a/packages/react-client/src/ReactFlightClientStream.js b/packages/react-client/src/ReactFlightClientStream.js index d261da8b88b21..63ebe3b788635 100644 --- a/packages/react-client/src/ReactFlightClientStream.js +++ b/packages/react-client/src/ReactFlightClientStream.js @@ -16,6 +16,7 @@ import { resolveModel, resolveErrorProd, resolveErrorDev, + resolveDirective, createResponse as createResponseBase, parseModelString, parseModelTuple, @@ -46,6 +47,10 @@ function processFullRow(response: Response, row: string): void { resolveModule(response, id, row.slice(colon + 2)); return; } + case '!': { + resolveDirective(response, id, row.slice(colon + 2)); + return; + } case 'E': { const errorInfo = JSON.parse(row.slice(colon + 2)); if (__DEV__) { diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.custom.js b/packages/react-client/src/forks/ReactFlightClientConfig.custom.js index 4990e84e7cb09..f4a4b9d4c0187 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.custom.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.custom.js @@ -35,6 +35,7 @@ export const resolveClientReference = $$$config.resolveClientReference; export const resolveServerReference = $$$config.resolveServerReference; export const preloadModule = $$$config.preloadModule; export const requireModule = $$$config.requireModule; +export const dispatchDirective = $$$config.dispatchDirective; export opaque type Source = mixed; diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 8c991026bc2e1..b2c7d6153f0ec 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -7,6 +7,7 @@ * @flow */ +import type {HostDispatcher} from 'react-dom/src/ReactDOMDispatcher'; import type {EventPriority} from 'react-reconciler/src/ReactEventPriorities'; import type {DOMEventName} from '../events/DOMEventNames'; import type {Fiber, FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; @@ -1868,10 +1869,6 @@ export function clearSingleton(instance: Instance): void { export const supportsResources = true; -// The resource types we support. currently they match the form for the as argument. -// In the future this may need to change, especially when modules / scripts are supported -type ResourceType = 'style' | 'font' | 'script'; - type HoistableTagType = 'link' | 'meta' | 'title'; type TResource< T: 'stylesheet' | 'style' | 'script' | 'void', @@ -1962,7 +1959,7 @@ function getDocumentFromRoot(root: HoistableRoot): Document { // We want this to be the default dispatcher on ReactDOMSharedInternals but we don't want to mutate // internals in Module scope. Instead we export it and Internals will import it. There is already a cycle // from Internals -> ReactDOM -> HostConfig -> Internals so this doesn't introduce a new one. -export const ReactDOMClientDispatcher = { +export const ReactDOMClientDispatcher: HostDispatcher = { prefetchDNS, preconnect, preload, @@ -2036,7 +2033,10 @@ function prefetchDNS(href: string, options?: mixed) { preconnectAs('dns-prefetch', null, href); } -function preconnect(href: string, options?: {crossOrigin?: string}) { +function preconnect(href: string, options: ?{crossOrigin?: string}) { + if (!enableFloat) { + return; + } if (__DEV__) { if (typeof href !== 'string' || !href) { console.error( @@ -2064,9 +2064,8 @@ function preconnect(href: string, options?: {crossOrigin?: string}) { preconnectAs('preconnect', crossOrigin, href); } -type PreloadAs = ResourceType; type PreloadOptions = { - as: PreloadAs, + as: string, crossOrigin?: string, integrity?: string, type?: string, @@ -2115,7 +2114,7 @@ function preload(href: string, options: PreloadOptions) { function preloadPropsFromPreloadOptions( href: string, - as: ResourceType, + as: string, options: PreloadOptions, ): PreloadProps { return { @@ -2128,9 +2127,8 @@ function preloadPropsFromPreloadOptions( }; } -type PreinitAs = 'style' | 'script'; type PreinitOptions = { - as: PreinitAs, + as: string, precedence?: string, crossOrigin?: string, integrity?: string, diff --git a/packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js b/packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js index f5a5356312fbe..242c3a79f382c 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js +++ b/packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js @@ -21,9 +21,6 @@ export function scheduleWork(callback: () => void) { export function flushBuffered(destination: Destination) {} -export const supportsRequestStorage = false; -export const requestStorage: AsyncLocalStorage = (null: any); - export function beginWriting(destination: Destination) {} export function writeChunk( diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 25d296dbd0d60..46836d61e4c68 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -38,6 +38,10 @@ import { stringToPrecomputedChunk, clonePrecomputedChunk, } from 'react-server/src/ReactServerStreamConfig'; +import { + resolveResources, + pingRequest, +} from 'react-server/src/ReactFizzResources'; import isAttributeNameSafe from '../shared/isAttributeNameSafe'; import isUnitlessNumber from '../shared/isUnitlessNumber'; @@ -79,30 +83,15 @@ import { import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals'; const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher; -const ReactDOMServerDispatcher = enableFloat - ? { - prefetchDNS, - preconnect, - preload, - preinit, - } - : {}; - -let currentResources: null | Resources = null; -const currentResourcesStack = []; - -export function prepareToRender(resources: Resources): mixed { - currentResourcesStack.push(currentResources); - currentResources = resources; +const ReactDOMServerDispatcher = { + prefetchDNS, + preconnect, + preload, + preinit, +}; - const previousHostDispatcher = ReactDOMCurrentDispatcher.current; +export function prepareHostDispatcher() { ReactDOMCurrentDispatcher.current = ReactDOMServerDispatcher; - return previousHostDispatcher; -} - -export function cleanupAfterRender(previousDispatcher: mixed) { - currentResources = currentResourcesStack.pop(); - ReactDOMCurrentDispatcher.current = previousDispatcher; } // Used to distinguish these contexts from ones used in other renderers. @@ -4804,16 +4793,18 @@ function getResourceKey(as: string, href: string): string { } export function prefetchDNS(href: string, options?: mixed) { - if (!currentResources) { - // While we expect that preconnect calls are primarily going to be observed - // during render because effects and events don't run on the server it is - // still possible that these get called in module scope. This is valid on - // the client since there is still a document to interact with but on the - // server we need a request to associate the call to. Because of this we - // simply return and do not warn. + if (!enableFloat) { + return; + } + const resources = resolveResources(); + if (!resources) { + // In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also + // possibly get them from the stack if we are not in an async context. Since we were not able to resolve + // the resources for this call in either case we opt to do nothing. We can consider making this a warning + // but there may be times where calling a function outside of render is intentional (i.e. to warm up data + // fetching) and we don't want to warn in those cases. return; } - const resources = currentResources; if (__DEV__) { if (typeof href !== 'string' || !href) { console.error( @@ -4858,17 +4849,19 @@ export function prefetchDNS(href: string, options?: mixed) { } } -export function preconnect(href: string, options?: {crossOrigin?: string}) { - if (!currentResources) { - // While we expect that preconnect calls are primarily going to be observed - // during render because effects and events don't run on the server it is - // still possible that these get called in module scope. This is valid on - // the client since there is still a document to interact with but on the - // server we need a request to associate the call to. Because of this we - // simply return and do not warn. +export function preconnect(href: string, options?: ?{crossOrigin?: string}) { + if (!enableFloat) { + return; + } + const resources = resolveResources(); + if (!resources) { + // In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also + // possibly get them from the stack if we are not in an async context. Since we were not able to resolve + // the resources for this call in either case we opt to do nothing. We can consider making this a warning + // but there may be times where calling a function outside of render is intentional (i.e. to warm up data + // fetching) and we don't want to warn in those cases. return; } - const resources = currentResources; if (__DEV__) { if (typeof href !== 'string' || !href) { console.error( @@ -4917,24 +4910,25 @@ export function preconnect(href: string, options?: {crossOrigin?: string}) { } } -type PreloadAs = 'style' | 'font' | 'script'; type PreloadOptions = { - as: PreloadAs, + as: string, crossOrigin?: string, integrity?: string, type?: string, }; export function preload(href: string, options: PreloadOptions) { - if (!currentResources) { - // While we expect that preload calls are primarily going to be observed - // during render because effects and events don't run on the server it is - // still possible that these get called in module scope. This is valid on - // the client since there is still a document to interact with but on the - // server we need a request to associate the call to. Because of this we - // simply return and do not warn. + if (!enableFloat) { + return; + } + const resources = resolveResources(); + if (!resources) { + // In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also + // possibly get them from the stack if we are not in an async context. Since we were not able to resolve + // the resources for this call in either case we opt to do nothing. We can consider making this a warning + // but there may be times where calling a function outside of render is intentional (i.e. to warm up data + // fetching) and we don't want to warn in those cases. return; } - const resources = currentResources; if (__DEV__) { if (typeof href !== 'string' || !href) { console.error( @@ -5055,27 +5049,30 @@ export function preload(href: string, options: PreloadOptions) { resources.explicitOtherPreloads.add(resource); } } + pingRequest(); } } -type PreinitAs = 'style' | 'script'; type PreinitOptions = { - as: PreinitAs, + as: string, precedence?: string, crossOrigin?: string, integrity?: string, }; export function preinit(href: string, options: PreinitOptions): void { - if (!currentResources) { - // While we expect that preinit calls are primarily going to be observed - // during render because effects and events don't run on the server it is - // still possible that these get called in module scope. This is valid on - // the client since there is still a document to interact with but on the - // server we need a request to associate the call to. Because of this we - // simply return and do not warn. + if (!enableFloat) { + return; + } + const resources = resolveResources(); + if (!resources) { + // In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also + // possibly get them from the stack if we are not in an async context. Since we were not able to resolve + // the resources for this call in either case we opt to do nothing. We can consider making this a warning + // but there may be times where calling a function outside of render is intentional (i.e. to warm up data + // fetching) and we don't want to warn in those cases. return; } - preinitImpl(currentResources, href, options); + preinitImpl(resources, href, options); } // On the server, preinit may be called outside of render when sending an @@ -5297,7 +5294,7 @@ function preinitImpl( function preloadPropsFromPreloadOptions( href: string, - as: PreloadAs, + as: string, options: PreloadOptions, ): PreloadProps { return { diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js index 1707b4d2c13e3..4feafb782dae6 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js @@ -137,8 +137,7 @@ export { writePostamble, hoistResources, setCurrentlyRenderingBoundaryResourcesTarget, - prepareToRender, - cleanupAfterRender, + prepareHostDispatcher, } from './ReactFizzConfigDOM'; import {stringToChunk} from 'react-server/src/ReactServerStreamConfig'; diff --git a/packages/react-dom-bindings/src/server/ReactFlightServerConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFlightServerConfigDOM.js index e2daf606c8ebd..d637a43b5ae2b 100644 --- a/packages/react-dom-bindings/src/server/ReactFlightServerConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFlightServerConfigDOM.js @@ -7,6 +7,110 @@ * @flow */ +import {enableFloat} from 'shared/ReactFeatureFlags'; + +import {dispatchDirective} from 'react-server/src/ReactFlightDirectives'; + +import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals'; +const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher; + +const ReactDOMFlightServerDispatcher = { + prefetchDNS, + preconnect, + preload, + preinit, +}; + +export function prepareHostDispatcher(): void { + ReactDOMCurrentDispatcher.current = ReactDOMFlightServerDispatcher; +} + // Used to distinguish these contexts from ones used in other renderers. // E.g. this can be used to distinguish legacy renderers from this modern one. export const isPrimaryRenderer = true; + +type PrefetchDNSDirective = ['prefetchDNS', string]; +type PreconnectDirective = + | ['preconnect', string] + | ['preconnect', string, {crossOrigin?: string}]; +type PreloadDirective = ['preload', string, PreloadOptions]; +type PreinitDirective = ['preinit', string, PreinitOptions]; + +export type Directive = + | PrefetchDNSDirective + | PreconnectDirective + | PreloadDirective + | PreinitDirective; + +let didWarnAsyncEnvironmentDev = false; + +function dispatch(directive: Directive) { + const successful = dispatchDirective(directive); + if (__DEV__) { + if (!successful && !didWarnAsyncEnvironmentDev) { + didWarnAsyncEnvironmentDev = true; + const methodName = `ReactDOM.${directive[0]}()`; + console.error( + '%s: React expected to be able to associate this call to a specific Request but cannot. It is possible that this call was invoked outside of a React component. If you are calling it from within a React component that is an async function after the first `await` then you are in an environment which does not support AsyncLocalStorage. In this kind of environment %s does not do anything when called in an async manner. Try moving this function call above the first `await` within the component or remove this call. In environments that support AsyncLocalStorage such as Node.js you can call this method anywhere in a React component even after `await` operator.', + methodName, + methodName, + ); + } + } +} + +export function prefetchDNS(href: string, options?: ?{}) { + if (enableFloat) { + if (typeof href === 'string') { + // We want to pass through options for validation purposes. + // We could __DEV__ gate this but currently consider keeping the serialization + // identical in prod safer + const directive: PrefetchDNSDirective = ['prefetchDNS', href]; + if (options) { + (directive: any).push(options); + } + dispatch(directive); + } + } +} + +export function preconnect(href: string, options: ?{crossOrigin?: string}) { + if (enableFloat) { + if (typeof href === 'string') { + if (options) { + dispatch(['preconnect', href, options]); + } else { + dispatch(['preconnect', href]); + } + } + } +} + +type PreloadOptions = { + as: string, + crossOrigin?: string, + integrity?: string, + type?: string, +}; + +export function preload(href: string, options: PreloadOptions) { + if (enableFloat) { + if (typeof href === 'string') { + dispatch(['preload', href, options]); + } + } +} + +type PreinitOptions = { + as: string, + precedence?: string, + crossOrigin?: string, + integrity?: string, +}; +export function preinit(href: string, options: PreinitOptions): void { + if (enableFloat) { + if (typeof href === 'string') { + dispatch(['preinit', href, options]); + } + } +} diff --git a/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js b/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js index d92d7d50ea060..3c242c83061d3 100644 --- a/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js +++ b/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js @@ -10,4 +10,38 @@ // This client file is in the shared folder because it applies to both SSR and browser contexts. // It is the configuraiton of the FlightClient behavior which can run in either environment. -// In a future update this is where we will implement `dispatchDirective` such as for Float methods +import type {Directive} from '../server/ReactFlightServerConfigDOM'; + +import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals'; +const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher; + +export function dispatchDirective(directive: Directive): void { + const dispatcher = ReactDOMCurrentDispatcher.current; + if (dispatcher) { + const options = directive.length === 3 ? directive[2] : undefined; + switch (directive[0]) { + case 'prefetchDNS': { + // $FlowFixMe[incompatible-call] unable to refine on array indices + dispatcher.prefetchDNS(directive[1], options); + return; + } + case 'preconnect': { + // $FlowFixMe[prop-missing] unable to refine on array indices + dispatcher.preconnect(directive[1], options); + return; + } + case 'preload': { + // $FlowFixMe[prop-missing] unable to refine on array indices + // $FlowFixMe[incompatible-call] unable to refine on array indices + dispatcher.preload(directive[1], options); + return; + } + case 'preinit': { + // $FlowFixMe[prop-missing] unable to refine on array indices + // $FlowFixMe[incompatible-call] unable to refine on array indices + dispatcher.preinit(directive[1], options); + return; + } + } + } +} diff --git a/packages/react-dom/src/ReactDOMDispatcher.js b/packages/react-dom/src/ReactDOMDispatcher.js new file mode 100644 index 0000000000000..f5fecc913ea4f --- /dev/null +++ b/packages/react-dom/src/ReactDOMDispatcher.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export type PrefetchDNSOptions = void; +export type PreconnectOptions = {crossOrigin?: string}; +export type PreloadOptions = { + as: string, + crossOrigin?: string, + integrity?: string, + type?: string, +}; +export type PreinitOptions = { + as: string, + precedence?: string, + crossOrigin?: string, + integrity?: string, +}; + +export type HostDispatcher = { + prefetchDNS: (href: string, options?: ?PrefetchDNSOptions) => void, + preconnect: (href: string, options: ?PreconnectOptions) => void, + preload: (href: string, options: PreloadOptions) => void, + preinit: (href: string, options: PreinitOptions) => void, +}; diff --git a/packages/react-dom/src/ReactDOMFloat.js b/packages/react-dom/src/ReactDOMFloat.js index 99a867286d361..25a03fcd092e9 100644 --- a/packages/react-dom/src/ReactDOMFloat.js +++ b/packages/react-dom/src/ReactDOMFloat.js @@ -1,39 +1,72 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ +import type { + PreconnectOptions, + PreloadOptions, + PreinitOptions, +} from './ReactDOMDispatcher'; + import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals'; +const Dispatcher = ReactDOMSharedInternals.Dispatcher; -export function prefetchDNS() { - const dispatcher = ReactDOMSharedInternals.Dispatcher.current; +export function prefetchDNS(href: string) { + let passedOptionArg: any; + if (__DEV__) { + if (arguments[1] !== undefined) { + passedOptionArg = arguments[1]; + } + } + const dispatcher = Dispatcher.current; if (dispatcher) { - dispatcher.prefetchDNS.apply(this, arguments); + if (__DEV__) { + if (passedOptionArg !== undefined) { + // prefetchDNS will warn if you pass reserved options arg. We pass it along in Dev only to + // elicit the warning. In prod we do not forward since it is not a part of the interface. + // @TODO move all arg validation into this file. It needs to be universal anyway so may as well lock down the interace here and + // let the rest of the codebase trust the types + dispatcher.prefetchDNS(href, passedOptionArg); + } else { + dispatcher.prefetchDNS(href); + } + } else { + dispatcher.prefetchDNS(href); + } } // We don't error because preconnect needs to be resilient to being called in a variety of scopes // and the runtime may not be capable of responding. The function is optimistic and not critical // so we favor silent bailout over warning or erroring. } -export function preconnect() { - const dispatcher = ReactDOMSharedInternals.Dispatcher.current; +export function preconnect(href: string, options?: ?PreconnectOptions) { + const dispatcher = Dispatcher.current; if (dispatcher) { - dispatcher.preconnect.apply(this, arguments); + dispatcher.preconnect(href, options); } // We don't error because preconnect needs to be resilient to being called in a variety of scopes // and the runtime may not be capable of responding. The function is optimistic and not critical // so we favor silent bailout over warning or erroring. } -export function preload() { - const dispatcher = ReactDOMSharedInternals.Dispatcher.current; +export function preload(href: string, options: PreloadOptions) { + const dispatcher = Dispatcher.current; if (dispatcher) { - dispatcher.preload.apply(this, arguments); + dispatcher.preload(href, options); } // We don't error because preload needs to be resilient to being called in a variety of scopes // and the runtime may not be capable of responding. The function is optimistic and not critical // so we favor silent bailout over warning or erroring. } -export function preinit() { - const dispatcher = ReactDOMSharedInternals.Dispatcher.current; +export function preinit(href: string, options: PreinitOptions) { + const dispatcher = Dispatcher.current; if (dispatcher) { - dispatcher.preinit.apply(this, arguments); + dispatcher.preinit(href, options); } // We don't error because preinit needs to be resilient to being called in a variety of scopes // and the runtime may not be capable of responding. The function is optimistic and not critical diff --git a/packages/react-dom/src/ReactDOMSharedInternals.js b/packages/react-dom/src/ReactDOMSharedInternals.js index a9e0407b006b2..cf2909ffac2ff 100644 --- a/packages/react-dom/src/ReactDOMSharedInternals.js +++ b/packages/react-dom/src/ReactDOMSharedInternals.js @@ -7,11 +7,13 @@ * @flow */ +import type {HostDispatcher} from './ReactDOMDispatcher'; + type InternalsType = { usingClientEntryPoint: boolean, Events: [any, any, any, any, any, any], Dispatcher: { - current: mixed, + current: null | HostDispatcher, }, }; diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index e93af6fad421d..0aeef3357bd83 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -3981,7 +3981,7 @@ body { }); // @gate enableFloat - it('creates a preload resource when ReactDOM.preinit(..., {as: "script" }) is called outside of render on the client', async () => { + it('creates a script resource when ReactDOM.preinit(..., {as: "script" }) is called outside of render on the client', async () => { function App() { React.useEffect(() => { ReactDOM.preinit('foo', {as: 'script'}); diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js index e79fa16452bda..2167175a90b1b 100644 --- a/packages/react-dom/src/client/ReactDOMRoot.js +++ b/packages/react-dom/src/client/ReactDOMRoot.js @@ -13,8 +13,6 @@ import type { TransitionTracingCallbacks, } from 'react-reconciler/src/ReactInternalTypes'; -import ReactDOMSharedInternals from '../ReactDOMSharedInternals'; -const {Dispatcher} = ReactDOMSharedInternals; import {ReactDOMClientDispatcher} from 'react-dom-bindings/src/client/ReactFiberConfigDOM'; import {queueExplicitHydrationTarget} from 'react-dom-bindings/src/events/ReactDOMEventReplaying'; import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; @@ -25,13 +23,19 @@ import { disableCommentsAsDOMContainers, } from 'shared/ReactFeatureFlags'; +import ReactDOMSharedInternals from '../ReactDOMSharedInternals'; +const {Dispatcher} = ReactDOMSharedInternals; +if (enableFloat && typeof document !== 'undefined') { + // Set the default dispatcher to the client dispatcher + Dispatcher.current = ReactDOMClientDispatcher; +} + export type RootType = { render(children: ReactNodeList): void, unmount(): void, _internalRoot: FiberRoot | null, ... }; - export type CreateRootOptions = { unstable_strictMode?: boolean, unstable_concurrentUpdatesByDefault?: boolean, diff --git a/packages/react-native-renderer/src/server/ReactFizzConfigNative.js b/packages/react-native-renderer/src/server/ReactFizzConfigNative.js index 54650e851f614..4e348d1e8ac20 100644 --- a/packages/react-native-renderer/src/server/ReactFizzConfigNative.js +++ b/packages/react-native-renderer/src/server/ReactFizzConfigNative.js @@ -339,8 +339,7 @@ export function hoistResources( boundaryResources: BoundaryResources, ) {} -export function prepareToRender(resources: Resources) {} -export function cleanupAfterRender(previousDispatcher: mixed) {} +export function prepareHostDispatcher() {} export function createResources() {} export function createBoundaryResources() {} export function setCurrentlyRenderingBoundaryResourcesTarget( diff --git a/packages/react-native-renderer/src/server/ReactFlightServerConfigNative.js b/packages/react-native-renderer/src/server/ReactFlightServerConfigNative.js index cc92e90d8b342..7a8dc23d39cc2 100644 --- a/packages/react-native-renderer/src/server/ReactFlightServerConfigNative.js +++ b/packages/react-native-renderer/src/server/ReactFlightServerConfigNative.js @@ -8,3 +8,7 @@ */ export const isPrimaryRenderer = true; + +export type Directive = void; + +export function prepareHostDispatcher() {} diff --git a/packages/react-noop-renderer/src/ReactNoopFlightServer.js b/packages/react-noop-renderer/src/ReactNoopFlightServer.js index 52794a6221c3f..32b3aa2c4f21e 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightServer.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightServer.js @@ -63,6 +63,7 @@ const ReactNoopFlightServer = ReactFlightServer({ ) { return saveModule(reference.value); }, + prepareHostDispatcher() {}, }); type Options = { diff --git a/packages/react-noop-renderer/src/ReactNoopServer.js b/packages/react-noop-renderer/src/ReactNoopServer.js index cd5edd94d29d6..c8f4fcd2ca4d7 100644 --- a/packages/react-noop-renderer/src/ReactNoopServer.js +++ b/packages/react-noop-renderer/src/ReactNoopServer.js @@ -279,8 +279,7 @@ const ReactNoopServer = ReactFizzServer({ setCurrentlyRenderingBoundaryResourcesTarget(resources: BoundaryResources) {}, - prepareToRender() {}, - cleanupAfterRender() {}, + prepareHostDispatcher() {}, }); type Options = { diff --git a/packages/react-server-dom-relay/src/ReactFlightDOMRelayClient.js b/packages/react-server-dom-relay/src/ReactFlightDOMRelayClient.js index 43d24623e53bb..6ffbde3b500d3 100644 --- a/packages/react-server-dom-relay/src/ReactFlightDOMRelayClient.js +++ b/packages/react-server-dom-relay/src/ReactFlightDOMRelayClient.js @@ -17,6 +17,7 @@ import { resolveModule, resolveErrorDev, resolveErrorProd, + resolveDirective, close, getRoot, } from 'react-client/src/ReactFlightClient'; @@ -30,6 +31,9 @@ export function resolveRow(response: Response, chunk: RowEncoding): void { } else if (chunk[0] === 'I') { // $FlowFixMe[incompatible-call] unable to refine on array indices resolveModule(response, chunk[1], chunk[2]); + } else if (chunk[0] === '!') { + // $FlowFixMe[incompatible-call] unable to refine on array indices + resolveDirective(response, chunk[1], chunk[2]); } else { if (__DEV__) { resolveErrorDev( diff --git a/packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js b/packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js index 73b793f0d715c..cf4ee041c7dc9 100644 --- a/packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js +++ b/packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js @@ -7,6 +7,7 @@ * @flow */ +import type {Directive} from 'react-server/src/ReactFlightServerConfig'; import type {ClientReferenceMetadata} from 'ReactFlightDOMRelayServerIntegration'; export type JSONValue = @@ -20,6 +21,7 @@ export type JSONValue = export type RowEncoding = | ['O', number, JSONValue] | ['I', number, ClientReferenceMetadata] + | ['!', number, Directive] | ['P', number, string] | ['S', number, string] | [ diff --git a/packages/react-server-dom-relay/src/ReactFlightServerConfigDOMRelay.js b/packages/react-server-dom-relay/src/ReactFlightServerConfigDOMRelay.js index 0f709b7dbcc24..b56f51638bd45 100644 --- a/packages/react-server-dom-relay/src/ReactFlightServerConfigDOMRelay.js +++ b/packages/react-server-dom-relay/src/ReactFlightServerConfigDOMRelay.js @@ -7,6 +7,7 @@ * @flow */ +import type {Directive} from 'react-server/src/ReactFlightServerConfig'; import type {RowEncoding, JSONValue} from './ReactFlightDOMRelayProtocol'; import type { @@ -191,6 +192,15 @@ export function processImportChunk( return ['I', id, clientReferenceMetadata]; } +export function processDirectiveChunk( + request: Request, + id: number, + directive: Directive, +): Chunk { + // The directive is already a JSON serializable value. + return ['!', id, directive]; +} + export function scheduleWork(callback: () => void) { callback(); } @@ -198,8 +208,7 @@ export function scheduleWork(callback: () => void) { export function flushBuffered(destination: Destination) {} export const supportsRequestStorage = false; -export const requestStorage: AsyncLocalStorage> = - (null: any); +export const requestStorage: AsyncLocalStorage = (null: any); export function beginWriting(destination: Destination) {} diff --git a/packages/react-server-dom-relay/src/ReactServerStreamConfigFB.js b/packages/react-server-dom-relay/src/ReactServerStreamConfigFB.js index bc550d63ea7ea..88d7d3c52ae40 100644 --- a/packages/react-server-dom-relay/src/ReactServerStreamConfigFB.js +++ b/packages/react-server-dom-relay/src/ReactServerStreamConfigFB.js @@ -24,8 +24,7 @@ export function scheduleWork(callback: () => void) { export function flushBuffered(destination: Destination) {} export const supportsRequestStorage = false; -export const requestStorage: AsyncLocalStorage> = - (null: any); +export const requestStorage: AsyncLocalStorage = (null: any); export function beginWriting(destination: Destination) {} diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index cb30243215fc5..2290765cb4e9f 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -25,9 +25,11 @@ let clientModuleError; let webpackMap; let Stream; let React; +let ReactDOM; let ReactDOMClient; let ReactServerDOMServer; let ReactServerDOMClient; +let ReactDOMFizzServer; let Suspense; let ErrorBoundary; @@ -42,6 +44,8 @@ describe('ReactFlightDOM', () => { Stream = require('stream'); React = require('react'); + ReactDOM = require('react-dom'); + ReactDOMFizzServer = require('react-dom/server.node'); use = React.use; Suspense = React.Suspense; ReactDOMClient = require('react-dom/client'); @@ -1153,4 +1157,229 @@ describe('ReactFlightDOM', () => { ); expect(reportedErrors).toEqual([theError]); }); + + // @gate enableUseHook + it('should support ReactDOM.preload when rendering in Fiber', async () => { + function Component() { + return

hello world

; + } + + const ClientComponent = clientExports(Component); + + async function ServerComponent() { + ReactDOM.preload('before', {as: 'style'}); + await 1; + ReactDOM.preload('after', {as: 'style'}); + return ; + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + pipe(writable); + + let response = null; + function getResponse() { + if (response === null) { + response = ReactServerDOMClient.createFromReadableStream(readable); + } + return response; + } + + function App() { + return getResponse(); + } + + // We pause to allow the float call after the await point to process before the + // HostDispatcher gets set for Fiber by createRoot. This is only needed in testing + // because the module graphs are not different and the HostDispatcher is shared. + // In a real environment the Fiber and Flight code would each have their own independent + // dispatcher. + // @TODO consider what happens when Server-Components-On-The-Client exist. we probably + // want to use the Fiber HostDispatcher there too since it is more about the host than the runtime + // but we need to make sure that actually makes sense + await 1; + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(document.head.innerHTML).toBe( + '' + + '', + ); + expect(container.innerHTML).toBe('

hello world

'); + }); + + // @gate enableUseHook + it('should support ReactDOM.preload when rendering in Fizz', async () => { + function Component() { + return

hello world

; + } + + const ClientComponent = clientExports(Component); + + async function ServerComponent() { + ReactDOM.preload('before', {as: 'style'}); + await 1; + ReactDOM.preload('after', {as: 'style'}); + return ; + } + + const {writable: flightWritable, readable: flightReadable} = + getTestStream(); + const {writable: fizzWritable, readable: fizzReadable} = getTestStream(); + + // In a real environment you would want to call the render during the Fizz render. + // The reason we cannot do this in our test is because we don't actually have two separate + // module graphs and we are contriving the sequencing to work in a way where + // the right HostDispatcher is in scope during the Flight Server Float calls and the + // Flight Client directive dispatches + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + pipe(flightWritable); + + let response = null; + function getResponse() { + if (response === null) { + response = + ReactServerDOMClient.createFromReadableStream(flightReadable); + } + return response; + } + + function App() { + return ( + + {getResponse()} + + ); + } + + await act(async () => { + ReactDOMFizzServer.renderToPipeableStream().pipe(fizzWritable); + }); + + const decoder = new TextDecoder(); + const reader = fizzReadable.getReader(); + let content = ''; + while (true) { + const {done, value} = await reader.read(); + if (done) { + content += decoder.decode(); + break; + } + content += decoder.decode(value, {stream: true}); + } + + expect(content).toEqual( + '' + + '

hello world

', + ); + }); + + it('supports Float directives from concurrent Flight -> Fizz renders', async () => { + function Component() { + return

hello world

; + } + + const ClientComponent = clientExports(Component); + + async function ServerComponent1() { + ReactDOM.preload('before1', {as: 'style'}); + await 1; + ReactDOM.preload('after1', {as: 'style'}); + return ; + } + + async function ServerComponent2() { + ReactDOM.preload('before2', {as: 'style'}); + await 1; + ReactDOM.preload('after2', {as: 'style'}); + return ; + } + + const {writable: flightWritable1, readable: flightReadable1} = + getTestStream(); + const {writable: flightWritable2, readable: flightReadable2} = + getTestStream(); + + ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ).pipe(flightWritable1); + + ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ).pipe(flightWritable2); + + const responses = new Map(); + function getResponse(stream) { + let response = responses.get(stream); + if (!response) { + response = ReactServerDOMClient.createFromReadableStream(stream); + responses.set(stream, response); + } + return response; + } + + function App({stream}) { + return ( + + {getResponse(stream)} + + ); + } + + // pausing to let Flight runtime tick. This is a test only artifact of the fact that + // we aren't operating separate module graphs for flight and fiber. In a real app + // each would have their own dispatcher and there would be no cross dispatching. + await 1; + + const {writable: fizzWritable1, readable: fizzReadable1} = getTestStream(); + const {writable: fizzWritable2, readable: fizzReadable2} = getTestStream(); + await act(async () => { + ReactDOMFizzServer.renderToPipeableStream( + , + ).pipe(fizzWritable1); + ReactDOMFizzServer.renderToPipeableStream( + , + ).pipe(fizzWritable2); + }); + + async function read(stream) { + const decoder = new TextDecoder(); + const reader = stream.getReader(); + let buffer = ''; + while (true) { + const {done, value} = await reader.read(); + if (done) { + buffer += decoder.decode(); + break; + } + buffer += decoder.decode(value, {stream: true}); + } + return buffer; + } + + const [content1, content2] = await Promise.all([ + read(fizzReadable1), + read(fizzReadable2), + ]); + + expect(content1).toEqual( + '' + + '

hello world

', + ); + expect(content2).toEqual( + '' + + '

hello world

', + ); + }); }); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index 2847801d9499c..b1bcdbef607f6 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -21,7 +21,9 @@ let webpackMap; let webpackServerMap; let act; let React; +let ReactDOM; let ReactDOMClient; +let ReactDOMFizzServer; let ReactServerDOMServer; let ReactServerDOMClient; let Suspense; @@ -37,7 +39,9 @@ describe('ReactFlightDOMBrowser', () => { webpackMap = WebpackMock.webpackMap; webpackServerMap = WebpackMock.webpackServerMap; React = require('react'); + ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); + ReactDOMFizzServer = require('react-dom/server.browser'); ReactServerDOMServer = require('react-server-dom-webpack/server.browser'); ReactServerDOMClient = require('react-server-dom-webpack/client'); Suspense = React.Suspense; @@ -1062,4 +1066,128 @@ describe('ReactFlightDOMBrowser', () => { expect(thrownError.digest).toBe('test-error-digest'); } }); + + it('supports Float directives before the first await in server components in Fiber', async () => { + function Component() { + return

hello world

; + } + + const ClientComponent = clientExports(Component); + + async function ServerComponent() { + ReactDOM.preload('before', {as: 'style'}); + await 1; + ReactDOM.preload('after', {as: 'style'}); + return ; + } + + const stream = ReactServerDOMServer.renderToReadableStream( + , + webpackMap, + ); + + let response = null; + function getResponse() { + if (response === null) { + response = ReactServerDOMClient.createFromReadableStream(stream); + } + return response; + } + + function App() { + return getResponse(); + } + + // pausing to let Flight runtime tick. This is a test only artifact of the fact that + // we aren't operating separate module graphs for flight and fiber. In a real app + // each would have their own dispatcher and there would be no cross dispatching. + await expect(async () => { + await 1; + }).toErrorDev( + 'ReactDOM.preload(): React expected to be able to associate this call to a specific Request but cannot. It is possible that this call was invoked outside of a React component. If you are calling it from within a React component that is an async function after the first `await` then you are in an environment which does not support AsyncLocalStorage. In this kind of environment ReactDOM.preload() does not do anything when called in an async manner. Try moving this function call above the first `await` within the component or remove this call. In environments that support AsyncLocalStorage such as Node.js you can call this method anywhere in a React component even after `await` operator.', + {withoutStack: true}, + ); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(document.head.innerHTML).toBe( + '', + ); + expect(container.innerHTML).toBe('

hello world

'); + }); + + it('Does not support Float directives in server components anywhere in Fizz', async () => { + // In environments that do not support AsyncLocalStorage the Flight client has no ability + // to scope directive dispatching to a specific Request. In Fiber this isn't a problem because + // the Browser scope acts like a singleton and we can dispatch away. But in Fizz we need to have + // a reference to Resources and this is only possible during render unless you support AsyncLocalStorage. + function Component() { + return

hello world

; + } + + const ClientComponent = clientExports(Component); + + async function ServerComponent() { + ReactDOM.preload('before', {as: 'style'}); + await 1; + ReactDOM.preload('after', {as: 'style'}); + return ; + } + + const stream = ReactServerDOMServer.renderToReadableStream( + , + webpackMap, + ); + + let response = null; + function getResponse() { + if (response === null) { + response = ReactServerDOMClient.createFromReadableStream(stream); + } + return response; + } + + function App() { + return ( + + {getResponse()} + + ); + } + + // pausing to let Flight runtime tick. This is a test only artifact of the fact that + // we aren't operating separate module graphs for flight and fiber. In a real app + // each would have their own dispatcher and there would be no cross dispatching. + await expect(async () => { + await 1; + }).toErrorDev( + 'ReactDOM.preload(): React expected to be able to associate this call to a specific Request but cannot. It is possible that this call was invoked outside of a React component. If you are calling it from within a React component that is an async function after the first `await` then you are in an environment which does not support AsyncLocalStorage. In this kind of environment ReactDOM.preload() does not do anything when called in an async manner. Try moving this function call above the first `await` within the component or remove this call. In environments that support AsyncLocalStorage such as Node.js you can call this method anywhere in a React component even after `await` operator.', + {withoutStack: true}, + ); + + let fizzStream; + await act(async () => { + fizzStream = await ReactDOMFizzServer.renderToReadableStream(); + }); + + const decoder = new TextDecoder(); + const reader = fizzStream.getReader(); + let content = ''; + while (true) { + const {done, value} = await reader.read(); + if (done) { + content += decoder.decode(); + break; + } + content += decoder.decode(value, {stream: true}); + } + + expect(content).toEqual( + '' + + '

hello world

', + ); + }); }); diff --git a/packages/react-server-native-relay/src/ReactFlightClientConfigNativeRelay.js b/packages/react-server-native-relay/src/ReactFlightClientConfigNativeRelay.js index e21df4d13e80c..75bb6d66d9d00 100644 --- a/packages/react-server-native-relay/src/ReactFlightClientConfigNativeRelay.js +++ b/packages/react-server-native-relay/src/ReactFlightClientConfigNativeRelay.js @@ -95,3 +95,5 @@ const dummy = {}; export function parseModel(response: Response, json: UninitializedModel): T { return (parseModelRecursively(response, dummy, '', json): any); } + +export function dispatchDirective(payload: mixed) {} diff --git a/packages/react-server-native-relay/src/ReactFlightServerConfigNativeRelay.js b/packages/react-server-native-relay/src/ReactFlightServerConfigNativeRelay.js index ab815ae2f0144..6da0c2c260356 100644 --- a/packages/react-server-native-relay/src/ReactFlightServerConfigNativeRelay.js +++ b/packages/react-server-native-relay/src/ReactFlightServerConfigNativeRelay.js @@ -187,6 +187,16 @@ export function processImportChunk( return ['I', id, clientReferenceMetadata]; } +export function processDirectiveChunk( + request: Request, + id: number, + model: ReactClientValue, +): Chunk { + throw new Error( + 'React Internal Error: processDirectiveChunk is not implemented for Native-Relay. The fact that this method was called means there is a but in React.', + ); +} + export function scheduleWork(callback: () => void) { callback(); } @@ -194,8 +204,7 @@ export function scheduleWork(callback: () => void) { export function flushBuffered(destination: Destination) {} export const supportsRequestStorage = false; -export const requestStorage: AsyncLocalStorage> = - (null: any); +export const requestStorage: AsyncLocalStorage = (null: any); export function beginWriting(destination: Destination) {} diff --git a/packages/react-server/src/ReactFizzCurrentRequest.js b/packages/react-server/src/ReactFizzCurrentRequest.js new file mode 100644 index 0000000000000..6d3e217f6984c --- /dev/null +++ b/packages/react-server/src/ReactFizzCurrentRequest.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {supportsRequestStorage, requestStorage} from './ReactFizzConfig'; + +import type {Request} from './ReactFizzServer'; + +export function resolveCurrentRequest(): null | Request { + if (currentRequest) return currentRequest; + if (supportsRequestStorage) { + const store = requestStorage.getStore(); + if (store) return store; + } + // If we do not have a store with directives we can't resolve them. + // Callers need to handle cases where directives are unavailable + return null; +} + +let currentRequest: null | Request = null; + +export function setCurrentRequest(request: null | Request): null | Request { + currentRequest = request; + return currentRequest; +} + +export function getCurrentRequest(): null | Request { + return currentRequest; +} diff --git a/packages/react-server/src/ReactFizzResources.js b/packages/react-server/src/ReactFizzResources.js new file mode 100644 index 0000000000000..e22561c8b97da --- /dev/null +++ b/packages/react-server/src/ReactFizzResources.js @@ -0,0 +1,26 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Resources} from './ReactFizzConfig'; + +import {resolveCurrentRequest} from './ReactFizzCurrentRequest'; +import {pingRequest as ping, getRequestResources} from './ReactFizzServer'; + +export function resolveResources(): null | Resources { + const request = resolveCurrentRequest(); + if (request) return getRequestResources(request); + return null; +} + +export function pingRequest(): void { + const request = resolveCurrentRequest(); + if (request) { + ping(request); + } +} diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index eab8c6c50e366..1b61805cd255f 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -71,11 +71,12 @@ import { writeHoistables, writePostamble, hoistResources, - prepareToRender, - cleanupAfterRender, setCurrentlyRenderingBoundaryResourcesTarget, createResources, createBoundaryResources, + prepareHostDispatcher, + supportsRequestStorage, + requestStorage, } from './ReactFizzConfig'; import { constructClassInstance, @@ -105,6 +106,7 @@ import { getThenableStateAfterSuspending, unwrapThenable, } from './ReactFizzHooks'; +import {getCurrentRequest, setCurrentRequest} from './ReactFizzCurrentRequest'; import {DefaultCacheDispatcher} from './ReactFizzCache'; import {getStackByComponentStackNode} from './ReactFizzComponentStack'; import {emptyTreeContext, pushTreeContext} from './ReactFizzTreeContext'; @@ -277,6 +279,7 @@ export function createRequest( onShellError: void | ((error: mixed) => void), onFatalError: void | ((error: mixed) => void), ): Request { + prepareHostDispatcher(); const pingedTasks: Array = []; const abortSet: Set = new Set(); const resources: Resources = createResources(); @@ -1947,7 +1950,9 @@ export function performWork(request: Request): void { ReactCurrentCache.current = DefaultCacheDispatcher; } - const previousHostDispatcher = prepareToRender(request.resources); + const prevRequest = getCurrentRequest(); + setCurrentRequest(request); + let prevGetCurrentStackImpl; if (__DEV__) { prevGetCurrentStackImpl = ReactDebugCurrentFrame.getCurrentStack; @@ -1975,7 +1980,6 @@ export function performWork(request: Request): void { if (enableCache) { ReactCurrentCache.current = prevCacheDispatcher; } - cleanupAfterRender(previousHostDispatcher); if (__DEV__) { ReactDebugCurrentFrame.getCurrentStack = prevGetCurrentStackImpl; @@ -1990,6 +1994,7 @@ export function performWork(request: Request): void { // we'll to restore the context to what it was before returning. switchContext(prevContext); } + setCurrentRequest(prevRequest); } } @@ -2411,7 +2416,11 @@ function flushCompletedQueues( } export function startWork(request: Request): void { - scheduleWork(() => performWork(request)); + if (supportsRequestStorage) { + scheduleWork(() => requestStorage.run(request, performWork, request)); + } else { + scheduleWork(() => performWork(request)); + } } export function startFlowing(request: Request, destination: Destination): void { @@ -2456,3 +2465,15 @@ export function abort(request: Request, reason: mixed): void { fatalError(request, error); } } + +export function getRequestResources(request: Request): Resources { + return request.resources; +} + +// If new resources are identified out of band with a task we can still potentially +// emit them early by pinging them. +export function pingRequest(request: Request): void { + if (request.pingedTasks.length === 0) { + scheduleWork(() => performWork(request)); + } +} diff --git a/packages/react-server/src/ReactFlightCache.js b/packages/react-server/src/ReactFlightCache.js index 7ac8aaa66222f..edd6847a46ad4 100644 --- a/packages/react-server/src/ReactFlightCache.js +++ b/packages/react-server/src/ReactFlightCache.js @@ -9,21 +9,15 @@ import type {CacheDispatcher} from 'react-reconciler/src/ReactInternalTypes'; -import { - supportsRequestStorage, - requestStorage, -} from './ReactFlightServerConfig'; +import {resolveCurrentRequest} from './ReactFlightCurrentRequest'; function createSignal(): AbortSignal { return new AbortController().signal; } function resolveCache(): Map { - if (currentCache) return currentCache; - if (supportsRequestStorage) { - const cache = requestStorage.getStore(); - if (cache) return cache; - } + const request = resolveCurrentRequest(); + if (request) return request.cache; // Since we override the dispatcher all the time, we're effectively always // active and so to support cache() and fetch() outside of render, we yield // an empty Map. @@ -51,16 +45,3 @@ export const DefaultCacheDispatcher: CacheDispatcher = { return entry; }, }; - -let currentCache: Map | null = null; - -export function setCurrentCache( - cache: Map | null, -): Map | null { - currentCache = cache; - return currentCache; -} - -export function getCurrentCache(): Map | null { - return currentCache; -} diff --git a/packages/react-server/src/ReactFlightCurrentRequest.js b/packages/react-server/src/ReactFlightCurrentRequest.js new file mode 100644 index 0000000000000..61e58ba368df0 --- /dev/null +++ b/packages/react-server/src/ReactFlightCurrentRequest.js @@ -0,0 +1,37 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import { + supportsRequestStorage, + requestStorage, +} from './ReactFlightServerConfig'; + +import type {Request} from './ReactFlightServer'; + +export function resolveCurrentRequest(): null | Request { + if (currentRequest) return currentRequest; + if (supportsRequestStorage) { + const store = requestStorage.getStore(); + if (store) return store; + } + // If we do not have a store with directives we can't resolve them. + // Callers need to handle cases where directives are unavailable + return null; +} + +let currentRequest: null | Request = null; + +export function setCurrentRequest(request: null | Request): null | Request { + currentRequest = request; + return currentRequest; +} + +export function getCurrentRequest(): null | Request { + return currentRequest; +} diff --git a/packages/react-server/src/ReactFlightDirectives.js b/packages/react-server/src/ReactFlightDirectives.js new file mode 100644 index 0000000000000..9a5edcddc0124 --- /dev/null +++ b/packages/react-server/src/ReactFlightDirectives.js @@ -0,0 +1,25 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Directive} from './ReactFlightServerConfig'; + +import {resolveCurrentRequest} from './ReactFlightCurrentRequest'; +import {serializeDirective} from './ReactFlightServer'; + +let didWarnAsyncEnvironmentDev = false; + +export function dispatchDirective(directive: Directive): boolean { + const request = resolveCurrentRequest(); + if (request) { + serializeDirective(request, directive); + return true; + } else { + return false; + } +} diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index dc37f750ae277..d0ad60f982923 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -16,6 +16,7 @@ import type { ClientReferenceKey, ServerReference, ServerReferenceId, + Directive, } from './ReactFlightServerConfig'; import type {ContextSnapshot} from './ReactFlightNewContext'; import type {ThenableState} from './ReactFlightThenable'; @@ -44,6 +45,7 @@ import { processErrorChunkProd, processErrorChunkDev, processReferenceChunk, + processDirectiveChunk, resolveClientReferenceMetadata, getServerReferenceId, getServerReferenceBoundArguments, @@ -52,6 +54,7 @@ import { isServerReference, supportsRequestStorage, requestStorage, + prepareHostDispatcher, } from './ReactFlightServerConfig'; import { @@ -61,11 +64,11 @@ import { getThenableStateAfterSuspending, resetHooksForRequest, } from './ReactFlightHooks'; +import {DefaultCacheDispatcher} from './ReactFlightCache'; import { - DefaultCacheDispatcher, - getCurrentCache, - setCurrentCache, -} from './ReactFlightCache'; + getCurrentRequest, + setCurrentRequest, +} from './ReactFlightCurrentRequest'; import { pushProvider, popProvider, @@ -157,6 +160,7 @@ export type Request = { abortableTasks: Set, pingedTasks: Array, completedImportChunks: Array, + completedDirectiveChunks: Array, completedJSONChunks: Array, completedErrorChunks: Array, writtenSymbols: Map, @@ -196,6 +200,7 @@ export function createRequest( 'Currently React only supports one RSC renderer at a time.', ); } + prepareHostDispatcher(); ReactCurrentCache.current = DefaultCacheDispatcher; const abortSet: Set = new Set(); @@ -211,6 +216,7 @@ export function createRequest( abortableTasks: abortSet, pingedTasks: pingedTasks, completedImportChunks: ([]: Array), + completedDirectiveChunks: ([]: Array), completedJSONChunks: ([]: Array), completedErrorChunks: ([]: Array), writtenSymbols: new Map(), @@ -320,6 +326,17 @@ function serializeThenable(request: Request, thenable: Thenable): number { return newTask.id; } +export function serializeDirective(request: Request, directive: Directive) { + emitDirectiveChunk(request, directive); + + if (request.pingedTasks.length === 0) { + // There is no already scheduled work so we need to schedule it. + // There are no tasks but at the end of perform work all queues will + // be emptied + } + scheduleWork(() => performWork(request)); +} + function readThenable(thenable: Thenable): T { if (thenable.status === 'fulfilled') { return thenable.value; @@ -1082,6 +1099,15 @@ function emitImportChunk( request.completedImportChunks.push(processedChunk); } +function emitDirectiveChunk(request: Request, directive: Directive): void { + const processedChunk = processDirectiveChunk( + request, + request.nextChunkId++, + directive, + ); + request.completedDirectiveChunks.push(processedChunk); +} + function emitSymbolChunk(request: Request, id: number, name: string): void { const symbolReference = serializeSymbolReference(name); const processedChunk = processReferenceChunk(request, id, symbolReference); @@ -1195,9 +1221,9 @@ function retryTask(request: Request, task: Task): void { function performWork(request: Request): void { const prevDispatcher = ReactCurrentDispatcher.current; - const prevCache = getCurrentCache(); ReactCurrentDispatcher.current = HooksDispatcher; - setCurrentCache(request.cache); + const prevRequest = getCurrentRequest(); + setCurrentRequest(request); prepareToUseHooksForRequest(request); try { @@ -1215,8 +1241,8 @@ function performWork(request: Request): void { fatalError(request, error); } finally { ReactCurrentDispatcher.current = prevDispatcher; - setCurrentCache(prevCache); resetHooksForRequest(); + setCurrentRequest(prevRequest); } } @@ -1250,6 +1276,21 @@ function flushCompletedChunks( } } importsChunks.splice(0, i); + + // Next comes directives. + const directiveChunks = request.completedDirectiveChunks; + i = 0; + for (; i < directiveChunks.length; i++) { + const chunk = directiveChunks[i]; + const keepWriting: boolean = writeChunkAndReturn(destination, chunk); + if (!keepWriting) { + request.destination = null; + i++; + break; + } + } + directiveChunks.splice(0, i); + // Next comes model data. const jsonChunks = request.completedJSONChunks; i = 0; @@ -1264,6 +1305,7 @@ function flushCompletedChunks( } } jsonChunks.splice(0, i); + // Finally, errors are sent. The idea is that it's ok to delay // any error messages and prioritize display of other parts of // the page. @@ -1292,7 +1334,7 @@ function flushCompletedChunks( export function startWork(request: Request): void { if (supportsRequestStorage) { - scheduleWork(() => requestStorage.run(request.cache, performWork, request)); + scheduleWork(() => requestStorage.run(request, performWork, request)); } else { scheduleWork(() => performWork(request)); } diff --git a/packages/react-server/src/ReactFlightServerConfigBundlerCustom.js b/packages/react-server/src/ReactFlightServerConfigBundlerCustom.js index e11c154d05f32..b1fbc93ee61b1 100644 --- a/packages/react-server/src/ReactFlightServerConfigBundlerCustom.js +++ b/packages/react-server/src/ReactFlightServerConfigBundlerCustom.js @@ -23,3 +23,4 @@ export const resolveClientReferenceMetadata = export const getServerReferenceId = $$$config.getServerReferenceId; export const getServerReferenceBoundArguments = $$$config.getServerReferenceBoundArguments; +export const prepareHostDispatcher = $$$config.prepareHostDispatcher; diff --git a/packages/react-server/src/ReactFlightServerConfigStream.js b/packages/react-server/src/ReactFlightServerConfigStream.js index 4377a313b374a..f9d9c49b98741 100644 --- a/packages/react-server/src/ReactFlightServerConfigStream.js +++ b/packages/react-server/src/ReactFlightServerConfigStream.js @@ -75,11 +75,6 @@ import type {Chunk} from './ReactServerStreamConfig'; export type {Destination, Chunk} from './ReactServerStreamConfig'; -export { - supportsRequestStorage, - requestStorage, -} from './ReactServerStreamConfig'; - const stringify = JSON.stringify; function serializeRowHeader(tag: string, id: number) { @@ -156,6 +151,17 @@ export function processImportChunk( return stringToChunk(row); } +export function processDirectiveChunk( + request: Request, + id: number, + model: ReactClientValue, +): Chunk { + // $FlowFixMe[incompatible-type] stringify can return null + const json: string = stringify(model, request.toJSON); + const row = serializeRowHeader('!', id) + json + '\n'; + return stringToChunk(row); +} + export { scheduleWork, flushBuffered, diff --git a/packages/react-server/src/ReactServerStreamConfigBrowser.js b/packages/react-server/src/ReactServerStreamConfigBrowser.js index aa9cac7b2c373..28c16a92ede25 100644 --- a/packages/react-server/src/ReactServerStreamConfigBrowser.js +++ b/packages/react-server/src/ReactServerStreamConfigBrowser.js @@ -21,10 +21,6 @@ export function flushBuffered(destination: Destination) { // transform streams. https://github.com/whatwg/streams/issues/960 } -export const supportsRequestStorage = false; -export const requestStorage: AsyncLocalStorage> = - (null: any); - const VIEW_SIZE = 512; let currentView = null; let writtenBytes = 0; diff --git a/packages/react-server/src/ReactServerStreamConfigBun.js b/packages/react-server/src/ReactServerStreamConfigBun.js index 9cc88c4086475..b71b6542f36eb 100644 --- a/packages/react-server/src/ReactServerStreamConfigBun.js +++ b/packages/react-server/src/ReactServerStreamConfigBun.js @@ -26,10 +26,6 @@ export function flushBuffered(destination: Destination) { // transform streams. https://github.com/whatwg/streams/issues/960 } -// AsyncLocalStorage is not available in bun -export const supportsRequestStorage = false; -export const requestStorage = (null: any); - export function beginWriting(destination: Destination) {} export function writeChunk( diff --git a/packages/react-server/src/ReactServerStreamConfigEdge.js b/packages/react-server/src/ReactServerStreamConfigEdge.js index db6bfb14fee8b..e41bf7940134b 100644 --- a/packages/react-server/src/ReactServerStreamConfigEdge.js +++ b/packages/react-server/src/ReactServerStreamConfigEdge.js @@ -21,11 +21,6 @@ export function flushBuffered(destination: Destination) { // transform streams. https://github.com/whatwg/streams/issues/960 } -// For now, we get this from the global scope, but this will likely move to a module. -export const supportsRequestStorage = typeof AsyncLocalStorage === 'function'; -export const requestStorage: AsyncLocalStorage> = - supportsRequestStorage ? new AsyncLocalStorage() : (null: any); - const VIEW_SIZE = 512; let currentView = null; let writtenBytes = 0; diff --git a/packages/react-server/src/ReactServerStreamConfigNode.js b/packages/react-server/src/ReactServerStreamConfigNode.js index 5d33ce7d6576d..0313daf307a12 100644 --- a/packages/react-server/src/ReactServerStreamConfigNode.js +++ b/packages/react-server/src/ReactServerStreamConfigNode.js @@ -8,8 +8,8 @@ */ import type {Writable} from 'stream'; + import {TextEncoder} from 'util'; -import {AsyncLocalStorage} from 'async_hooks'; interface MightBeFlushable { flush?: () => void; @@ -34,10 +34,6 @@ export function flushBuffered(destination: Destination) { } } -export const supportsRequestStorage = true; -export const requestStorage: AsyncLocalStorage> = - new AsyncLocalStorage(); - const VIEW_SIZE = 2048; let currentView = null; let writtenBytes = 0; diff --git a/packages/react-server/src/forks/ReactFizzConfig.custom.js b/packages/react-server/src/forks/ReactFizzConfig.custom.js index f761eb392f3c0..4b44462d9d412 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.custom.js +++ b/packages/react-server/src/forks/ReactFizzConfig.custom.js @@ -23,6 +23,8 @@ // So `$$$config` looks like a global variable, but it's // really an argument to a top-level wrapping function. +import type {Request} from 'react-server/src/ReactFizzServer'; + declare var $$$config: any; export opaque type Destination = mixed; // eslint-disable-line no-undef export opaque type ResponseState = mixed; @@ -33,6 +35,9 @@ export opaque type SuspenseBoundaryID = mixed; export const isPrimaryRenderer = false; +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); + export const getChildFormatContext = $$$config.getChildFormatContext; export const UNINITIALIZED_SUSPENSE_BOUNDARY_ID = $$$config.UNINITIALIZED_SUSPENSE_BOUNDARY_ID; @@ -68,8 +73,7 @@ export const writeCompletedBoundaryInstruction = $$$config.writeCompletedBoundaryInstruction; export const writeClientRenderBoundaryInstruction = $$$config.writeClientRenderBoundaryInstruction; -export const prepareToRender = $$$config.prepareToRender; -export const cleanupAfterRender = $$$config.cleanupAfterRender; +export const prepareHostDispatcher = $$$config.prepareHostDispatcher; // ------------------------- // Resources diff --git a/packages/react-server/src/forks/ReactFizzConfig.dom-browser.js b/packages/react-server/src/forks/ReactFizzConfig.dom-browser.js index f2cf57ab5942f..5f887770d211e 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.dom-browser.js +++ b/packages/react-server/src/forks/ReactFizzConfig.dom-browser.js @@ -6,5 +6,9 @@ * * @flow */ +import type {Request} from 'react-server/src/ReactFizzServer'; export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); diff --git a/packages/react-server/src/forks/ReactFizzConfig.dom-bun.js b/packages/react-server/src/forks/ReactFizzConfig.dom-bun.js index f2cf57ab5942f..5f887770d211e 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.dom-bun.js +++ b/packages/react-server/src/forks/ReactFizzConfig.dom-bun.js @@ -6,5 +6,9 @@ * * @flow */ +import type {Request} from 'react-server/src/ReactFizzServer'; export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); diff --git a/packages/react-server/src/forks/ReactFizzConfig.dom-edge-webpack.js b/packages/react-server/src/forks/ReactFizzConfig.dom-edge-webpack.js index f2cf57ab5942f..67c8d7c13a78c 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.dom-edge-webpack.js +++ b/packages/react-server/src/forks/ReactFizzConfig.dom-edge-webpack.js @@ -6,5 +6,12 @@ * * @flow */ +import type {Request} from 'react-server/src/ReactFizzServer'; export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; + +// For now, we get this from the global scope, but this will likely move to a module. +export const supportsRequestStorage = typeof AsyncLocalStorage === 'function'; +export const requestStorage: AsyncLocalStorage = supportsRequestStorage + ? new AsyncLocalStorage() + : (null: any); diff --git a/packages/react-server/src/forks/ReactFizzConfig.dom-legacy.js b/packages/react-server/src/forks/ReactFizzConfig.dom-legacy.js index 4760bb843ea89..903250ce22db2 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.dom-legacy.js +++ b/packages/react-server/src/forks/ReactFizzConfig.dom-legacy.js @@ -6,5 +6,9 @@ * * @flow */ +import type {Request} from 'react-server/src/ReactFizzServer'; export * from 'react-dom-bindings/src/server/ReactFizzConfigDOMLegacy'; + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); diff --git a/packages/react-server/src/forks/ReactFizzConfig.dom-node-webpack.js b/packages/react-server/src/forks/ReactFizzConfig.dom-node-webpack.js index f2cf57ab5942f..71c6ab5a5586c 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.dom-node-webpack.js +++ b/packages/react-server/src/forks/ReactFizzConfig.dom-node-webpack.js @@ -6,5 +6,12 @@ * * @flow */ +import {AsyncLocalStorage} from 'async_hooks'; + +import type {Request} from 'react-server/src/ReactFizzServer'; export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; + +export const supportsRequestStorage = true; +export const requestStorage: AsyncLocalStorage = + new AsyncLocalStorage(); diff --git a/packages/react-server/src/forks/ReactFizzConfig.dom-node.js b/packages/react-server/src/forks/ReactFizzConfig.dom-node.js index f2cf57ab5942f..99d0d74a7b76a 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.dom-node.js +++ b/packages/react-server/src/forks/ReactFizzConfig.dom-node.js @@ -7,4 +7,12 @@ * @flow */ +import {AsyncLocalStorage} from 'async_hooks'; + +import type {Request} from 'react-server/src/ReactFizzServer'; + export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; + +export const supportsRequestStorage = true; +export const requestStorage: AsyncLocalStorage = + new AsyncLocalStorage(); diff --git a/packages/react-server/src/forks/ReactFizzConfig.dom-relay.js b/packages/react-server/src/forks/ReactFizzConfig.dom-relay.js index f2cf57ab5942f..5f887770d211e 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.dom-relay.js +++ b/packages/react-server/src/forks/ReactFizzConfig.dom-relay.js @@ -6,5 +6,9 @@ * * @flow */ +import type {Request} from 'react-server/src/ReactFizzServer'; export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); diff --git a/packages/react-server/src/forks/ReactFizzConfig.native-relay.js b/packages/react-server/src/forks/ReactFizzConfig.native-relay.js index c4981f9edf140..df90c935a9c92 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.native-relay.js +++ b/packages/react-server/src/forks/ReactFizzConfig.native-relay.js @@ -6,5 +6,9 @@ * * @flow */ +import type {Request} from 'react-server/src/ReactFizzServer'; export * from 'react-native-renderer/src/server/ReactFizzConfigNative'; + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.custom.js b/packages/react-server/src/forks/ReactFlightServerConfig.custom.js index 28977b2357cc6..741bd9d007d0d 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.custom.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.custom.js @@ -6,8 +6,14 @@ * * @flow */ +import type {Request} from 'react-server/src/ReactFlightServer'; export * from '../ReactFlightServerConfigStream'; export * from '../ReactFlightServerConfigBundlerCustom'; +export type Directive = void; export const isPrimaryRenderer = false; +export const prepareHostDispatcher = () => {}; + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js index 5304ae8c21af8..a532e31b6c206 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js @@ -7,6 +7,11 @@ * @flow */ +import type {Request} from 'react-server/src/ReactFlightServer'; + export * from '../ReactFlightServerConfigStream'; export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler'; export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js index 3778ad89ee89c..31db7c12414eb 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js @@ -7,6 +7,11 @@ * @flow */ +import type {Request} from 'react-server/src/ReactFlightServer'; + export * from '../ReactFlightServerConfigStream'; export * from '../ReactFlightServerConfigBundlerCustom'; export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-webpack.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-webpack.js index 5304ae8c21af8..036c0b7dc3a38 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-webpack.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-webpack.js @@ -6,7 +6,14 @@ * * @flow */ +import type {Request} from 'react-server/src/ReactFlightServer'; export * from '../ReactFlightServerConfigStream'; export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler'; export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; + +// For now, we get this from the global scope, but this will likely move to a module. +export const supportsRequestStorage = typeof AsyncLocalStorage === 'function'; +export const requestStorage: AsyncLocalStorage = supportsRequestStorage + ? new AsyncLocalStorage() + : (null: any); diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js index 5304ae8c21af8..a532e31b6c206 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js @@ -7,6 +7,11 @@ * @flow */ +import type {Request} from 'react-server/src/ReactFlightServer'; + export * from '../ReactFlightServerConfigStream'; export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler'; export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-webpack.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-webpack.js index 5304ae8c21af8..62a70db4abe6f 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-webpack.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-webpack.js @@ -6,7 +6,14 @@ * * @flow */ +import {AsyncLocalStorage} from 'async_hooks'; + +import type {Request} from 'react-server/src/ReactFlightServer'; export * from '../ReactFlightServerConfigStream'; export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler'; export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; + +export const supportsRequestStorage = true; +export const requestStorage: AsyncLocalStorage = + new AsyncLocalStorage(); diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js index 5304ae8c21af8..ccbacc8c1c3a0 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js @@ -7,6 +7,14 @@ * @flow */ +import {AsyncLocalStorage} from 'async_hooks'; + +import type {Request} from 'react-server/src/ReactFlightServer'; + export * from '../ReactFlightServerConfigStream'; export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler'; export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; + +export const supportsRequestStorage = true; +export const requestStorage: AsyncLocalStorage = + new AsyncLocalStorage(); diff --git a/packages/react-server/src/forks/ReactServerStreamConfig.custom.js b/packages/react-server/src/forks/ReactServerStreamConfig.custom.js index ed00afa434e7e..f62c2a54035ba 100644 --- a/packages/react-server/src/forks/ReactServerStreamConfig.custom.js +++ b/packages/react-server/src/forks/ReactServerStreamConfig.custom.js @@ -35,8 +35,6 @@ export const writeChunk = $$$config.writeChunk; export const writeChunkAndReturn = $$$config.writeChunkAndReturn; export const completeWriting = $$$config.completeWriting; export const flushBuffered = $$$config.flushBuffered; -export const supportsRequestStorage = $$$config.supportsRequestStorage; -export const requestStorage = $$$config.requestStorage; export const close = $$$config.close; export const closeWithError = $$$config.closeWithError; export const stringToChunk = $$$config.stringToChunk; diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index a2080dc38ed8f..541c41b6000a0 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -461,5 +461,6 @@ "473": "React doesn't accept base64 encoded file uploads because we don't except form data passed from a browser to ever encode data that way. If that's the wrong assumption, we can easily fix it.", "474": "Suspense Exception: This is not a real error, and should not leak into userspace. If you're seeing this, it's likely a bug in React.", "475": "Internal React Error: suspendedState null when it was expected to exists. Please report this as a React bug.", - "476": "Expected the form instance to be a HostComponent. This is a bug in React." + "476": "Expected the form instance to be a HostComponent. This is a bug in React.", + "477": "React Internal Error: processDirectiveChunk is not implemented for Native-Relay. The fact that this method was called means there is a but in React." } diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index f29fe5e17a433..23b779ddf8eb6 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -386,7 +386,7 @@ const bundles = [ global: 'ReactServerDOMClient', minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, - externals: ['react'], + externals: ['react', 'react-dom'], }, { bundleTypes: [NODE_DEV, NODE_PROD], @@ -395,7 +395,7 @@ const bundles = [ global: 'ReactServerDOMClient', minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, - externals: ['react', 'util'], + externals: ['react', 'react-dom', 'util'], }, { bundleTypes: [NODE_DEV, NODE_PROD], @@ -404,7 +404,7 @@ const bundles = [ global: 'ReactServerDOMClient', minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, - externals: ['react', 'util'], + externals: ['react', 'react-dom', 'util'], }, { bundleTypes: [NODE_DEV, NODE_PROD], @@ -413,7 +413,7 @@ const bundles = [ global: 'ReactServerDOMClient', minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, - externals: ['react'], + externals: ['react', 'react-dom'], }, /******* React Server DOM Webpack Plugin *******/