diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 32fb4f1c907db..6ad31a613e169 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -23,6 +23,7 @@ import { preloadModule, requireModule, parseModel, + dispatchDirective, } from './ReactFlightClientHostConfig'; import {knownServerReferences} from './ReactFlightServerReferenceRegistry'; @@ -758,6 +759,15 @@ export function resolveErrorDev( } } +export function resolveDirective( + response: Response, + id: number, + model: string, +): void { + const payload = JSON.parse(model); + dispatchDirective(payload); +} + 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 56772ec4a42b3..618c14bbf9e49 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.substring(colon + 2)); return; } + case 'D': { + resolveDirective(response, id, row.substring(colon + 2)); + return; + } case 'E': { const errorInfo = JSON.parse(row.substring(colon + 2)); if (__DEV__) { diff --git a/packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js b/packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js index 30810a69ebeb0..fe8c2a785fd2c 100644 --- a/packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js +++ b/packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js @@ -35,6 +35,7 @@ export const resolveClientReference = $$$hostConfig.resolveClientReference; export const resolveServerReference = $$$hostConfig.resolveServerReference; export const preloadModule = $$$hostConfig.preloadModule; export const requireModule = $$$hostConfig.requireModule; +export const dispatchDirective = $$$hostConfig.dispatchDirective; export opaque type Source = mixed; diff --git a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-browser.js b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-browser.js index 4aae8141fd56e..6d99059b62b71 100644 --- a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-browser.js +++ b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-browser.js @@ -10,3 +10,4 @@ export * from 'react-client/src/ReactFlightClientHostConfigBrowser'; export * from 'react-client/src/ReactFlightClientHostConfigStream'; export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig'; +export * from 'react-dom-bindings/src/shared/ReactDOMFlightClientHostConfig'; diff --git a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-bun.js b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-bun.js index 28a1f34997f91..5a535251a79c1 100644 --- a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-bun.js +++ b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-bun.js @@ -9,6 +9,7 @@ export * from 'react-client/src/ReactFlightClientHostConfigBrowser'; export * from 'react-client/src/ReactFlightClientHostConfigStream'; +export * from 'react-dom-bindings/src/shared/ReactDOMFlightClientHostConfig'; export type Response = any; export opaque type SSRManifest = mixed; diff --git a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-edge-webpack.js b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-edge-webpack.js index 4aae8141fd56e..6d99059b62b71 100644 --- a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-edge-webpack.js +++ b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-edge-webpack.js @@ -10,3 +10,4 @@ export * from 'react-client/src/ReactFlightClientHostConfigBrowser'; export * from 'react-client/src/ReactFlightClientHostConfigStream'; export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig'; +export * from 'react-dom-bindings/src/shared/ReactDOMFlightClientHostConfig'; diff --git a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-legacy.js b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-legacy.js index 4aae8141fd56e..6d99059b62b71 100644 --- a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-legacy.js +++ b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-legacy.js @@ -10,3 +10,4 @@ export * from 'react-client/src/ReactFlightClientHostConfigBrowser'; export * from 'react-client/src/ReactFlightClientHostConfigStream'; export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig'; +export * from 'react-dom-bindings/src/shared/ReactDOMFlightClientHostConfig'; diff --git a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-node-webpack.js b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-node-webpack.js index 8b9b2defedff5..f12539929eb6b 100644 --- a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-node-webpack.js +++ b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-node-webpack.js @@ -10,3 +10,4 @@ export * from 'react-client/src/ReactFlightClientHostConfigNode'; export * from 'react-client/src/ReactFlightClientHostConfigStream'; export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig'; +export * from 'react-dom-bindings/src/shared/ReactDOMFlightClientHostConfig'; diff --git a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-node.js b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-node.js index 5c20adb286414..c5ceb09ffeec5 100644 --- a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-node.js +++ b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-node.js @@ -10,3 +10,4 @@ export * from 'react-client/src/ReactFlightClientHostConfigNode'; export * from 'react-client/src/ReactFlightClientHostConfigStream'; export * from 'react-server-dom-webpack/src/ReactFlightClientNodeBundlerConfig'; +export * from 'react-dom-bindings/src/shared/ReactDOMFlightClientHostConfig'; diff --git a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-relay.js b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-relay.js index ccde93cb29597..d5f0e4e755c64 100644 --- a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-relay.js +++ b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-relay.js @@ -9,3 +9,4 @@ export * from 'react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig'; export * from '../ReactFlightClientHostConfigNoStream'; +export * from 'react-dom-bindings/src/shared/ReactDOMFlightClientHostConfig'; diff --git a/packages/react-dom-bindings/src/client/ReactDOMHostConfig.js b/packages/react-dom-bindings/src/client/ReactDOMHostConfig.js index b8e8807ec3957..2be0d0e8eb7ca 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom-bindings/src/client/ReactDOMHostConfig.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'; @@ -1865,10 +1866,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', @@ -1959,7 +1956,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, @@ -2033,7 +2030,7 @@ 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; } @@ -2064,9 +2061,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 +2111,7 @@ function preload(href: string, options: PreloadOptions) { function preloadPropsFromPreloadOptions( href: string, - as: ResourceType, + as: string, options: PreloadOptions, ): PreloadProps { return { @@ -2128,9 +2124,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/ReactDOMFlightServerFormatConfig.js b/packages/react-dom-bindings/src/server/ReactDOMFlightServerFormatConfig.js new file mode 100644 index 0000000000000..85b2c8f6f6eff --- /dev/null +++ b/packages/react-dom-bindings/src/server/ReactDOMFlightServerFormatConfig.js @@ -0,0 +1,92 @@ +/** + * 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 {enableFloat} from 'shared/ReactFeatureFlags'; + +import {resolveDirectives} 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; + +let didWarnAsyncEnvironmentDev = false; + +export function prefetchDNS(href: string, options?: mixed) { + if (enableFloat) { + pushDirective('prefetchDNS', href, options); + } +} + +export function preconnect(href: string, options?: ?{crossOrigin?: string}) { + if (enableFloat) { + pushDirective('preconnect', href, options); + } +} + +type PreloadOptions = { + as: string, + crossOrigin?: string, + integrity?: string, + type?: string, +}; + +export function preload(href: string, options: PreloadOptions) { + if (enableFloat) { + pushDirective('preload', href, options); + } +} + +type PreinitOptions = { + as: string, + precedence?: string, + crossOrigin?: string, + integrity?: string, +}; +export function preinit(href: string, options: PreinitOptions): void { + if (enableFloat) { + pushDirective('preinit', href, options); + } +} + +function pushDirective( + method: 'prefetchDNS' | 'preconnect' | 'preload' | 'preinit', + href: string, + options: mixed, +): void { + const directives = resolveDirectives(); + if (directives === null) { + if (__DEV__) { + if (!didWarnAsyncEnvironmentDev) { + didWarnAsyncEnvironmentDev = true; + console.error( + 'ReactDOM.%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 ReactDOM.%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.', + method, + method, + ); + } + } + return; + } + // @TODO need to escape + directives.push(JSON.stringify({method, args: [href, options]})); +} diff --git a/packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js b/packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js index f5a5356312fbe..c07bd229e43ce 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js +++ b/packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js @@ -23,6 +23,7 @@ export function flushBuffered(destination: Destination) {} export const supportsRequestStorage = false; export const requestStorage: AsyncLocalStorage = (null: any); +export const requestStorage2: AsyncLocalStorage = (null: any); export function beginWriting(destination: Destination) {} diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js index 9f1aba93a9a21..fad0162a601a9 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js @@ -37,6 +37,11 @@ import { stringToPrecomputedChunk, clonePrecomputedChunk, } from 'react-server/src/ReactServerStreamConfig'; +import { + resolveResources, + setCurrentResources, + getCurrentResources, +} from 'react-server/src/ReactFizzResources'; import isAttributeNameSafe from '../shared/isAttributeNameSafe'; import isUnitlessNumber from '../shared/isUnitlessNumber'; @@ -78,30 +83,34 @@ import { import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals'; const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher; -const ReactDOMServerDispatcher = enableFloat - ? { - prefetchDNS, - preconnect, - preload, - preinit, - } - : {}; +const ReactDOMServerDispatcher = { + prefetchDNS, + preconnect, + preload, + preinit, +}; -let currentResources: null | Resources = null; const currentResourcesStack = []; -export function prepareToRender(resources: Resources): mixed { - currentResourcesStack.push(currentResources); - currentResources = resources; +function pushResources(resources: null | Resources) { + currentResourcesStack.push(getCurrentResources()); + setCurrentResources(resources); +} - const previousHostDispatcher = ReactDOMCurrentDispatcher.current; +function popResources() { + setCurrentResources(currentResourcesStack.pop()); +} + +export function prepareHostDispatcher() { ReactDOMCurrentDispatcher.current = ReactDOMServerDispatcher; - return previousHostDispatcher; } -export function cleanupAfterRender(previousDispatcher: mixed) { - currentResources = currentResourcesStack.pop(); - ReactDOMCurrentDispatcher.current = previousDispatcher; +export function prepareToRender(resources: Resources): mixed { + pushResources(resources); +} + +export function cleanupAfterRender() { + popResources(); } // Used to distinguish these contexts from ones used in other renderers. @@ -4490,16 +4499,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( @@ -4544,17 +4555,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( @@ -4603,24 +4616,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( @@ -4744,24 +4758,26 @@ export function preload(href: string, options: PreloadOptions) { } } -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; } - preinitImpl(currentResources, href, options); + 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(resources, href, options); } // On the server, preinit may be called outside of render when sending an @@ -4983,7 +4999,7 @@ function preinitImpl( function preloadPropsFromPreloadOptions( href: string, - as: PreloadAs, + as: string, options: PreloadOptions, ): PreloadProps { return { diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerLegacyFormatConfig.js b/packages/react-dom-bindings/src/server/ReactDOMServerLegacyFormatConfig.js index 5ca51fbcbbac0..68879489addfe 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMServerLegacyFormatConfig.js +++ b/packages/react-dom-bindings/src/server/ReactDOMServerLegacyFormatConfig.js @@ -137,6 +137,7 @@ export { writePostamble, hoistResources, setCurrentlyRenderingBoundaryResourcesTarget, + prepareHostDispatcher, prepareToRender, cleanupAfterRender, } from './ReactDOMServerFormatConfig'; diff --git a/packages/react-dom-bindings/src/shared/ReactDOMFlightClientHostConfig.js b/packages/react-dom-bindings/src/shared/ReactDOMFlightClientHostConfig.js new file mode 100644 index 0000000000000..72d449da64fbd --- /dev/null +++ b/packages/react-dom-bindings/src/shared/ReactDOMFlightClientHostConfig.js @@ -0,0 +1,51 @@ +/** + * 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 + */ + +// 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. + +import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals'; +const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher; + +type DirectivePayload = mixed; + +export function dispatchDirective(payload: DirectivePayload): void { + const dispatcher = ReactDOMCurrentDispatcher.current; + if ( + payload === null || + typeof payload !== 'object' || + typeof payload.method !== 'string' || + !Array.isArray(payload.args) + ) { + if (__DEV__) { + console.error( + 'React expected a directive to be an object with method (string) and args (array) properties but encountered something else instead. It is likely this is a bug in React.', + ); + } + return; + } + const method = payload.method; + const args = payload.args; + if (dispatcher) { + switch (method) { + case 'prefetchDNS': + dispatcher.prefetchDNS.apply(dispatcher, (args: any)); + return; + case 'preconnect': + dispatcher.preconnect.apply(dispatcher, (args: any)); + return; + case 'preload': + dispatcher.preload.apply(dispatcher, (args: any)); + return; + case 'preinit': + dispatcher.preinit.apply(dispatcher, (args: any)); + 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 edc450adc8dce..f008e41ed9326 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -3896,7 +3896,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 dfd73fd9d7998..49c8ff56f8423 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/ReactDOMHostConfig'; 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) { + // 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/ReactNativeFlightServerFormatConfig.js b/packages/react-native-renderer/src/server/ReactNativeFlightServerFormatConfig.js new file mode 100644 index 0000000000000..de31b817ca2ae --- /dev/null +++ b/packages/react-native-renderer/src/server/ReactNativeFlightServerFormatConfig.js @@ -0,0 +1,12 @@ +/** + * 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 const isPrimaryRenderer = true; + +export function prepareHostDispatcher() {} diff --git a/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js b/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js index 54650e851f614..c1c3ac8e517f5 100644 --- a/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js +++ b/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js @@ -339,6 +339,7 @@ export function hoistResources( boundaryResources: BoundaryResources, ) {} +export function prepareHostDispatcher() {} export function prepareToRender(resources: Resources) {} export function cleanupAfterRender(previousDispatcher: mixed) {} export function createResources() {} 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..cb125e0c112ce 100644 --- a/packages/react-noop-renderer/src/ReactNoopServer.js +++ b/packages/react-noop-renderer/src/ReactNoopServer.js @@ -279,6 +279,7 @@ const ReactNoopServer = ReactFizzServer({ setCurrentlyRenderingBoundaryResourcesTarget(resources: BoundaryResources) {}, + prepareHostDispatcher() {}, prepareToRender() {}, cleanupAfterRender() {}, }); diff --git a/packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js b/packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js index 73b793f0d715c..6145178a8730d 100644 --- a/packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js +++ b/packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js @@ -20,6 +20,7 @@ export type JSONValue = export type RowEncoding = | ['O', number, JSONValue] | ['I', number, ClientReferenceMetadata] + | ['D', number, string] | ['P', number, string] | ['S', number, string] | [ diff --git a/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js b/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js index 0f709b7dbcc24..44f931b1aa635 100644 --- a/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js +++ b/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js @@ -7,6 +7,7 @@ * @flow */ +import type {Resources} from 'react-server/src/ReactServerFormatConfig'; 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, + payload: string, +): Chunk { + // The clientReferenceMetadata is already a JSON serializable value. + return ['D', id, payload]; +} + export function scheduleWork(callback: () => void) { callback(); } @@ -198,8 +208,11 @@ export function scheduleWork(callback: () => void) { export function flushBuffered(destination: Destination) {} export const supportsRequestStorage = false; -export const requestStorage: AsyncLocalStorage> = - (null: any); +export const requestStorage: AsyncLocalStorage<{ + cache: Map, + directives: Array, +}> = (null: any); +export const requestStorage2: 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..bc441171901a1 100644 --- a/packages/react-server-dom-relay/src/ReactServerStreamConfigFB.js +++ b/packages/react-server-dom-relay/src/ReactServerStreamConfigFB.js @@ -7,6 +7,8 @@ * @flow */ +import type {Resources} from 'react-server/src/ReactServerFormatConfig'; + export type Destination = { buffer: string, done: boolean, @@ -24,8 +26,11 @@ export function scheduleWork(callback: () => void) { export function flushBuffered(destination: Destination) {} export const supportsRequestStorage = false; -export const requestStorage: AsyncLocalStorage> = - (null: any); +export const requestStorage: AsyncLocalStorage<{ + cache: Map, + directives: Array, +}> = (null: any); +export const requestStorage2: 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 b2a25dec436da..abf723955d0e9 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'); @@ -1114,4 +1118,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 d53a9df2e3eb8..272d170208c1c 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; @@ -929,4 +933,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/ReactFlightNativeRelayClientHostConfig.js b/packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js index e21df4d13e80c..75bb6d66d9d00 100644 --- a/packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js +++ b/packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.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/ReactFlightNativeRelayServerHostConfig.js b/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js index ab815ae2f0144..ad2a1eb90afad 100644 --- a/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js +++ b/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js @@ -187,6 +187,16 @@ export function processImportChunk( return ['I', id, clientReferenceMetadata]; } +export function processDirectiveChunk( + request: Request, + id: number, + directive: string, +): 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,10 @@ export function scheduleWork(callback: () => void) { export function flushBuffered(destination: Destination) {} export const supportsRequestStorage = false; -export const requestStorage: AsyncLocalStorage> = - (null: any); +export const requestStorage: AsyncLocalStorage<{ + cache: Map, + directives: Array, +}> = (null: any); export function beginWriting(destination: Destination) {} diff --git a/packages/react-server/src/ReactFizzResources.js b/packages/react-server/src/ReactFizzResources.js new file mode 100644 index 0000000000000..ede23dddb2f6e --- /dev/null +++ b/packages/react-server/src/ReactFizzResources.js @@ -0,0 +1,35 @@ +/** + * 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 './ReactServerFormatConfig'; + +import { + supportsRequestStorage, + requestStorage2, +} from './ReactServerStreamConfig'; + +export function resolveResources(): null | Resources { + if (currentResources) return currentResources; + if (supportsRequestStorage) { + const store = requestStorage2.getStore(); + return store || null; + } + return null; +} + +let currentResources: null | Resources = null; + +export function setCurrentResources(store: null | Resources): null | Resources { + currentResources = store; + return currentResources; +} + +export function getCurrentResources(): null | Resources { + return currentResources; +} diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 3bd4f1d02d9d0..8915997013699 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -42,6 +42,8 @@ import { flushBuffered, close, closeWithError, + supportsRequestStorage, + requestStorage2, } from './ReactServerStreamConfig'; import { writeCompletedRoot, @@ -76,6 +78,7 @@ import { setCurrentlyRenderingBoundaryResourcesTarget, createResources, createBoundaryResources, + prepareHostDispatcher, } from './ReactServerFormatConfig'; import { constructClassInstance, @@ -277,6 +280,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 +1951,7 @@ export function performWork(request: Request): void { ReactCurrentCache.current = DefaultCacheDispatcher; } - const previousHostDispatcher = prepareToRender(request.resources); + prepareToRender(request.resources); let prevGetCurrentStackImpl; if (__DEV__) { prevGetCurrentStackImpl = ReactDebugCurrentFrame.getCurrentStack; @@ -1975,7 +1979,7 @@ export function performWork(request: Request): void { if (enableCache) { ReactCurrentCache.current = prevCacheDispatcher; } - cleanupAfterRender(previousHostDispatcher); + cleanupAfterRender(); if (__DEV__) { ReactDebugCurrentFrame.getCurrentStack = prevGetCurrentStackImpl; @@ -2411,7 +2415,13 @@ function flushCompletedQueues( } export function startWork(request: Request): void { - scheduleWork(() => performWork(request)); + if (supportsRequestStorage) { + scheduleWork(() => + requestStorage2.run(request.resources, performWork, request), + ); + } else { + scheduleWork(() => performWork(request)); + } } export function startFlowing(request: Request, destination: Destination): void { diff --git a/packages/react-server/src/ReactFlightCache.js b/packages/react-server/src/ReactFlightCache.js index 7ac8aaa66222f..589d0984d09ab 100644 --- a/packages/react-server/src/ReactFlightCache.js +++ b/packages/react-server/src/ReactFlightCache.js @@ -21,8 +21,8 @@ function createSignal(): AbortSignal { function resolveCache(): Map { if (currentCache) return currentCache; if (supportsRequestStorage) { - const cache = requestStorage.getStore(); - if (cache) return cache; + const store = requestStorage.getStore(); + if (store) return store.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 diff --git a/packages/react-server/src/ReactFlightDirectives.js b/packages/react-server/src/ReactFlightDirectives.js new file mode 100644 index 0000000000000..9248aa203f299 --- /dev/null +++ b/packages/react-server/src/ReactFlightDirectives.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'; + +export function resolveDirectives(): null | Array { + if (currentDirectives) return currentDirectives; + if (supportsRequestStorage) { + const store = requestStorage.getStore(); + if (store) return store.directives; + } + // 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 currentDirectives: null | Array = null; + +export function setCurrentDirectives( + directives: null | Array, +): null | Array { + currentDirectives = directives; + return currentDirectives; +} + +export function getCurrentDirectives(): null | Array { + return currentDirectives; +} diff --git a/packages/react-server/src/ReactFlightNewContext.js b/packages/react-server/src/ReactFlightNewContext.js index 1775110f13dc2..6412e7c2c7e8c 100644 --- a/packages/react-server/src/ReactFlightNewContext.js +++ b/packages/react-server/src/ReactFlightNewContext.js @@ -13,7 +13,7 @@ import type { } from 'shared/ReactTypes'; import {REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED} from 'shared/ReactSymbols'; -import {isPrimaryRenderer} from './ReactServerFormatConfig'; +import {isPrimaryRenderer} from './ReactFlightServerConfig'; let rendererSigil; if (__DEV__) { diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 1c127c886b7be..ab4223d6c8e88 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -44,6 +44,7 @@ import { processErrorChunkProd, processErrorChunkDev, processReferenceChunk, + processDirectiveChunk, resolveClientReferenceMetadata, getServerReferenceId, getServerReferenceBoundArguments, @@ -52,6 +53,7 @@ import { isServerReference, supportsRequestStorage, requestStorage, + prepareHostDispatcher, } from './ReactFlightServerConfig'; import { @@ -66,6 +68,10 @@ import { getCurrentCache, setCurrentCache, } from './ReactFlightCache'; +import { + getCurrentDirectives, + setCurrentDirectives, +} from './ReactFlightDirectives'; import { pushProvider, popProvider, @@ -146,12 +152,17 @@ type Task = { thenableState: ThenableState | null, }; +type Store = { + cache: Map, + directives: Array, +}; + export type Request = { status: 0 | 1 | 2, fatalError: mixed, destination: null | Destination, bundlerConfig: ClientManifest, - cache: Map, + store: Store, nextChunkId: number, pendingChunks: number, abortableTasks: Set, @@ -196,6 +207,7 @@ export function createRequest( 'Currently React only supports one RSC renderer at a time.', ); } + prepareHostDispatcher(); ReactCurrentCache.current = DefaultCacheDispatcher; const abortSet: Set = new Set(); @@ -205,7 +217,10 @@ export function createRequest( fatalError: null, destination: null, bundlerConfig, - cache: new Map(), + store: { + cache: new Map(), + directives: [], + }, nextChunkId: 0, pendingChunks: 0, abortableTasks: abortSet, @@ -1153,7 +1168,9 @@ function performWork(request: Request): void { const prevDispatcher = ReactCurrentDispatcher.current; const prevCache = getCurrentCache(); ReactCurrentDispatcher.current = HooksDispatcher; - setCurrentCache(request.cache); + setCurrentCache(request.store.cache); + const prevDirectives = getCurrentDirectives(); + setCurrentDirectives(request.store.directives); prepareToUseHooksForRequest(request); try { @@ -1172,6 +1189,7 @@ function performWork(request: Request): void { } finally { ReactCurrentDispatcher.current = prevDispatcher; setCurrentCache(prevCache); + setCurrentDirectives(prevDirectives); resetHooksForRequest(); } } @@ -1206,6 +1224,23 @@ function flushCompletedChunks( } } importsChunks.splice(0, i); + // Next comes directives data. + const directives = request.store.directives; + i = 0; + for (; i < directives.length; i++) { + const rowChunk = processDirectiveChunk( + request, + request.nextChunkId++, + directives[i], + ); + const keepWriting: boolean = writeChunkAndReturn(destination, rowChunk); + if (!keepWriting) { + request.destination = null; + i++; + break; + } + } + directives.splice(0, i); // Next comes model data. const jsonChunks = request.completedJSONChunks; i = 0; @@ -1248,7 +1283,7 @@ function flushCompletedChunks( export function startWork(request: Request): void { if (supportsRequestStorage) { - scheduleWork(() => requestStorage.run(request.cache, performWork, request)); + scheduleWork(() => requestStorage.run(request.store, performWork, request)); } else { scheduleWork(() => performWork(request)); } diff --git a/packages/react-server/src/ReactFlightServerBundlerConfigCustom.js b/packages/react-server/src/ReactFlightServerBundlerConfigCustom.js index b8254bb51d3cd..044d511945403 100644 --- a/packages/react-server/src/ReactFlightServerBundlerConfigCustom.js +++ b/packages/react-server/src/ReactFlightServerBundlerConfigCustom.js @@ -23,3 +23,4 @@ export const resolveClientReferenceMetadata = export const getServerReferenceId = $$$hostConfig.getServerReferenceId; export const getServerReferenceBoundArguments = $$$hostConfig.getServerReferenceBoundArguments; +export const prepareHostDispatcher = $$$hostConfig.prepareHostDispatcher; diff --git a/packages/react-server/src/ReactFlightServerConfigStream.js b/packages/react-server/src/ReactFlightServerConfigStream.js index 4377a313b374a..4ed64b952fe35 100644 --- a/packages/react-server/src/ReactFlightServerConfigStream.js +++ b/packages/react-server/src/ReactFlightServerConfigStream.js @@ -156,6 +156,15 @@ export function processImportChunk( return stringToChunk(row); } +export function processDirectiveChunk( + request: Request, + id: number, + payload: string, +): Chunk { + const row = serializeRowHeader('D', id) + payload + '\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..659cf522aaed1 100644 --- a/packages/react-server/src/ReactServerStreamConfigBrowser.js +++ b/packages/react-server/src/ReactServerStreamConfigBrowser.js @@ -7,6 +7,8 @@ * @flow */ +import type {Resources} from './ReactServerFormatConfig'; + export type Destination = ReadableStreamController; export type PrecomputedChunk = Uint8Array; @@ -22,8 +24,11 @@ export function flushBuffered(destination: Destination) { } export const supportsRequestStorage = false; -export const requestStorage: AsyncLocalStorage> = - (null: any); +export const requestStorage: AsyncLocalStorage<{ + cache: Map, + directives: Array, +}> = (null: any); +export const requestStorage2: AsyncLocalStorage = (null: any); const VIEW_SIZE = 512; let currentView = null; diff --git a/packages/react-server/src/ReactServerStreamConfigBun.js b/packages/react-server/src/ReactServerStreamConfigBun.js index 9cc88c4086475..0dfdb46b3caf4 100644 --- a/packages/react-server/src/ReactServerStreamConfigBun.js +++ b/packages/react-server/src/ReactServerStreamConfigBun.js @@ -7,6 +7,8 @@ * @flow */ +import type {Resources} from './ReactServerFormatConfig'; + type BunReadableStreamController = ReadableStreamController & { end(): mixed, write(data: Chunk): void, @@ -28,7 +30,11 @@ export function flushBuffered(destination: Destination) { // AsyncLocalStorage is not available in bun export const supportsRequestStorage = false; -export const requestStorage = (null: any); +export const requestStorage: AsyncLocalStorage<{ + cache: Map, + directives: Array, +}> = (null: any); +export const requestStorage2: AsyncLocalStorage = (null: any); export function beginWriting(destination: Destination) {} diff --git a/packages/react-server/src/ReactServerStreamConfigEdge.js b/packages/react-server/src/ReactServerStreamConfigEdge.js index db6bfb14fee8b..61f98a648ee83 100644 --- a/packages/react-server/src/ReactServerStreamConfigEdge.js +++ b/packages/react-server/src/ReactServerStreamConfigEdge.js @@ -7,6 +7,8 @@ * @flow */ +import type {Resources} from './ReactServerFormatConfig'; + export type Destination = ReadableStreamController; export type PrecomputedChunk = Uint8Array; @@ -23,7 +25,11 @@ export function flushBuffered(destination: Destination) { // 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> = +export const requestStorage: AsyncLocalStorage<{ + cache: Map, + directives: Array, +}> = supportsRequestStorage ? new AsyncLocalStorage() : (null: any); +export const requestStorage2: AsyncLocalStorage = supportsRequestStorage ? new AsyncLocalStorage() : (null: any); const VIEW_SIZE = 512; diff --git a/packages/react-server/src/ReactServerStreamConfigNode.js b/packages/react-server/src/ReactServerStreamConfigNode.js index 5d33ce7d6576d..5b0d6ad5ff4fc 100644 --- a/packages/react-server/src/ReactServerStreamConfigNode.js +++ b/packages/react-server/src/ReactServerStreamConfigNode.js @@ -8,6 +8,8 @@ */ import type {Writable} from 'stream'; +import type {Resources} from './ReactServerFormatConfig'; + import {TextEncoder} from 'util'; import {AsyncLocalStorage} from 'async_hooks'; @@ -34,8 +36,17 @@ export function flushBuffered(destination: Destination) { } } +// Conceptually we don't need two request storages because should have +// separate modules graphs for Flight and Fizz and we could type the store +// for the runtime. However, for testing and typing it is simpler to just +// have two stores. I am calling the second one requestStorage2 to hint +// that it is not not inherent that there is a Fizz storage and a Flight storage. export const supportsRequestStorage = true; -export const requestStorage: AsyncLocalStorage> = +export const requestStorage: AsyncLocalStorage<{ + cache: Map, + directives: Array, +}> = new AsyncLocalStorage(); +export const requestStorage2: AsyncLocalStorage = new AsyncLocalStorage(); const VIEW_SIZE = 2048; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.custom.js b/packages/react-server/src/forks/ReactFlightServerConfig.custom.js index f85cfb5f78b25..675153bf2afb2 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.custom.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.custom.js @@ -9,3 +9,6 @@ export * from '../ReactFlightServerConfigStream'; export * from '../ReactFlightServerBundlerConfigCustom'; + +export const isPrimaryRenderer = false; +export function prepareHostDispatcher() {} diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js index 99c541a937d63..d2949ecde49d3 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js @@ -9,3 +9,4 @@ export * from '../ReactFlightServerConfigStream'; export * from 'react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig'; +export * from 'react-dom-bindings/src/server/ReactDOMFlightServerFormatConfig'; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js index f85cfb5f78b25..ae46ce7a12e9d 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js @@ -9,3 +9,4 @@ export * from '../ReactFlightServerConfigStream'; export * from '../ReactFlightServerBundlerConfigCustom'; +export * from 'react-dom-bindings/src/server/ReactDOMFlightServerFormatConfig'; 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 99c541a937d63..d2949ecde49d3 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-webpack.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-webpack.js @@ -9,3 +9,4 @@ export * from '../ReactFlightServerConfigStream'; export * from 'react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig'; +export * from 'react-dom-bindings/src/server/ReactDOMFlightServerFormatConfig'; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js index 99c541a937d63..d2949ecde49d3 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js @@ -9,3 +9,4 @@ export * from '../ReactFlightServerConfigStream'; export * from 'react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig'; +export * from 'react-dom-bindings/src/server/ReactDOMFlightServerFormatConfig'; 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 99c541a937d63..d2949ecde49d3 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-webpack.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-webpack.js @@ -9,3 +9,4 @@ export * from '../ReactFlightServerConfigStream'; export * from 'react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig'; +export * from 'react-dom-bindings/src/server/ReactDOMFlightServerFormatConfig'; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js index 99c541a937d63..d2949ecde49d3 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js @@ -9,3 +9,4 @@ export * from '../ReactFlightServerConfigStream'; export * from 'react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig'; +export * from 'react-dom-bindings/src/server/ReactDOMFlightServerFormatConfig'; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-relay.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-relay.js index a6ad653f3a7a1..371bd030044cb 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-relay.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-relay.js @@ -8,3 +8,4 @@ */ export * from 'react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig'; +export * from 'react-dom-bindings/src/server/ReactDOMFlightServerFormatConfig'; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.native-relay.js b/packages/react-server/src/forks/ReactFlightServerConfig.native-relay.js index ab64c4324654f..5a9a09c44d81d 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.native-relay.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.native-relay.js @@ -8,3 +8,4 @@ */ export * from 'react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig'; +export * from 'react-native-renderer/src/server/ReactNativeFlightServerFormatConfig'; diff --git a/packages/react-server/src/forks/ReactServerFormatConfig.custom.js b/packages/react-server/src/forks/ReactServerFormatConfig.custom.js index 33f6c5aa65ddb..d396ebaccc474 100644 --- a/packages/react-server/src/forks/ReactServerFormatConfig.custom.js +++ b/packages/react-server/src/forks/ReactServerFormatConfig.custom.js @@ -68,6 +68,7 @@ export const writeCompletedBoundaryInstruction = $$$hostConfig.writeCompletedBoundaryInstruction; export const writeClientRenderBoundaryInstruction = $$$hostConfig.writeClientRenderBoundaryInstruction; +export const prepareHostDispatcher = $$$hostConfig.prepareHostDispatcher; export const prepareToRender = $$$hostConfig.prepareToRender; export const cleanupAfterRender = $$$hostConfig.cleanupAfterRender; diff --git a/packages/react-server/src/forks/ReactServerStreamConfig.custom.js b/packages/react-server/src/forks/ReactServerStreamConfig.custom.js index 0e7bafab961aa..03bfcefbb7cf0 100644 --- a/packages/react-server/src/forks/ReactServerStreamConfig.custom.js +++ b/packages/react-server/src/forks/ReactServerStreamConfig.custom.js @@ -37,6 +37,7 @@ export const completeWriting = $$$hostConfig.completeWriting; export const flushBuffered = $$$hostConfig.flushBuffered; export const supportsRequestStorage = $$$hostConfig.supportsRequestStorage; export const requestStorage = $$$hostConfig.requestStorage; +export const requestStorage2 = $$$hostConfig.requestStorage2; export const close = $$$hostConfig.close; export const closeWithError = $$$hostConfig.closeWithError; export const stringToChunk = $$$hostConfig.stringToChunk; diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index d48354c974202..c2d5851c99916 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -460,5 +460,6 @@ "472": "Type %s is not supported as an argument to a Server Function.", "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." + "475": "Internal React Error: suspendedState null when it was expected to exists. Please report this as a React bug.", + "476": "React Internal Error: processDirectiveChunk is not implemented for Native-Relay. The fact that this method was called means there is a but in React." }