diff --git a/fixtures/flight-esm/.nvmrc b/fixtures/flight-esm/.nvmrc new file mode 100644 index 0000000000000..3f430af82b3df --- /dev/null +++ b/fixtures/flight-esm/.nvmrc @@ -0,0 +1 @@ +v18 diff --git a/fixtures/flight-esm/server/global.js b/fixtures/flight-esm/server/global.js index d6aaf4cc8ca17..1088d42967a2f 100644 --- a/fixtures/flight-esm/server/global.js +++ b/fixtures/flight-esm/server/global.js @@ -10,6 +10,7 @@ const compress = require('compression'); const chalk = require('chalk'); const express = require('express'); const http = require('http'); +const React = require('react'); const {renderToPipeableStream} = require('react-dom/server'); const {createFromNodeStream} = require('react-server-dom-esm/client'); @@ -62,23 +63,39 @@ app.all('/', async function (req, res, next) { if (req.accepts('text/html')) { try { const rscResponse = await promiseForData; - const moduleBaseURL = '/src'; // For HTML, we're a "client" emulator that runs the client code, // so we start by consuming the RSC payload. This needs the local file path // to load the source files from as well as the URL path for preloads. - const root = await createFromNodeStream( - rscResponse, - moduleBasePath, - moduleBaseURL - ); + + let root; + let Root = () => { + if (root) { + return React.use(root); + } + + return React.use( + (root = createFromNodeStream( + rscResponse, + moduleBasePath, + moduleBaseURL + )) + ); + }; // Render it into HTML by resolving the client components res.set('Content-type', 'text/html'); - const {pipe} = renderToPipeableStream(root, { - // TODO: bootstrapModules inserts a preload before the importmap which causes - // the import map to be invalid. We need to fix that in Float somehow. - // bootstrapModules: ['/src/index.js'], + const {pipe} = renderToPipeableStream(React.createElement(Root), { + importMap: { + imports: { + react: 'https://esm.sh/react@experimental?pin=v124&dev', + 'react-dom': 'https://esm.sh/react-dom@experimental?pin=v124&dev', + 'react-dom/': 'https://esm.sh/react-dom@experimental&pin=v124&dev/', + 'react-server-dom-esm/client': + '/node_modules/react-server-dom-esm/esm/react-server-dom-esm-client.browser.development.js', + }, + }, + bootstrapModules: ['/src/index.js'], }); pipe(res); } catch (e) { @@ -89,6 +106,7 @@ app.all('/', async function (req, res, next) { } else { try { const rscResponse = await promiseForData; + // For other request, we pass-through the RSC payload. res.set('Content-type', 'text/x-component'); rscResponse.on('data', data => { diff --git a/fixtures/flight-esm/src/App.js b/fixtures/flight-esm/src/App.js index 161776eddd616..d5945280469bc 100644 --- a/fixtures/flight-esm/src/App.js +++ b/fixtures/flight-esm/src/App.js @@ -9,16 +9,6 @@ import {getServerState} from './ServerState.js'; const h = React.createElement; -const importMap = { - imports: { - react: 'https://esm.sh/react@experimental?pin=v124&dev', - 'react-dom': 'https://esm.sh/react-dom@experimental?pin=v124&dev', - 'react-dom/': 'https://esm.sh/react-dom@experimental&pin=v124&dev/', - 'react-server-dom-esm/client': - '/node_modules/react-server-dom-esm/esm/react-server-dom-esm-client.browser.development.js', - }, -}; - export default async function App() { const res = await fetch('http://localhost:3001/todos'); const todos = await res.json(); @@ -42,12 +32,6 @@ export default async function App() { rel: 'stylesheet', href: '/src/style.css', precedence: 'default', - }), - h('script', { - type: 'importmap', - dangerouslySetInnerHTML: { - __html: JSON.stringify(importMap), - }, }) ), h( @@ -84,9 +68,7 @@ export default async function App() { 'Like' ) ) - ), - // TODO: Move this to bootstrapModules. - h('script', {type: 'module', src: '/src/index.js'}) + ) ) ); } diff --git a/fixtures/flight/server/global.js b/fixtures/flight/server/global.js index 9310c2c14a771..27da72159eba0 100644 --- a/fixtures/flight/server/global.js +++ b/fixtures/flight/server/global.js @@ -147,7 +147,7 @@ app.all('/', async function (req, res, next) { let root; let Root = () => { if (root) { - return root; + return React.use(root); } return React.use( (root = createFromNodeStream( diff --git a/fixtures/flight/server/region.js b/fixtures/flight/server/region.js index 4beae03ff35ee..31b66f92f4d46 100644 --- a/fixtures/flight/server/region.js +++ b/fixtures/flight/server/region.js @@ -95,6 +95,9 @@ async function renderApp(res, returnValue) { // For client-invoked server actions we refresh the tree and return a return value. const payload = returnValue ? {returnValue, root} : root; const {pipe} = renderToPipeableStream(payload, moduleMap); + await new Promise(res => { + setTimeout(res, 1000); + }); pipe(res); } diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js index 53058d0d18841..ec71cd94382cc 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js @@ -8,6 +8,7 @@ */ export * from 'react-client/src/ReactFlightClientConfigBrowser'; -export * from 'react-server-dom-esm/src/ReactFlightClientConfigESMBundler'; +export * from 'react-server-dom-esm/src/ReactFlightClientConfigBundlerESM'; +export * from 'react-server-dom-esm/src/ReactFlightClientConfigTargetESMBrowser'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export const usedWithSSR = false; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js index 8390c4c06b439..016ac820d356c 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js @@ -7,7 +7,8 @@ * @flow */ -export * from 'react-client/src/ReactFlightClientConfigBrowser'; -export * from 'react-server-dom-esm/src/ReactFlightClientConfigESMBundler'; +export * from 'react-client/src/ReactFlightClientConfigNode'; +export * from 'react-server-dom-esm/src/ReactFlightClientConfigBundlerESM'; +export * from 'react-server-dom-esm/src/ReactFlightClientConfigTargetESMServer'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export const usedWithSSR = true; diff --git a/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js b/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js index 9c3d5c7506832..1eaabb948d631 100644 --- a/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js +++ b/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js @@ -64,7 +64,18 @@ export function dispatchHint(code: string, model: HintModel): void { } } -export function preinitModulesForSSR(href: string, crossOrigin: ?string) { +export function preinitModuleForSSR(href: string, crossOrigin: ?string) { + const dispatcher = ReactDOMCurrentDispatcher.current; + if (dispatcher) { + if (typeof crossOrigin === 'string') { + dispatcher.preinitModule(href, {crossOrigin}); + } else { + dispatcher.preinitModule(href); + } + } +} + +export function preinitScriptForSSR(href: string, crossOrigin: ?string) { const dispatcher = ReactDOMCurrentDispatcher.current; if (dispatcher) { if (typeof crossOrigin === 'string') { diff --git a/packages/react-server-dom-esm/src/ReactFlightClientConfigESMBundler.js b/packages/react-server-dom-esm/src/ReactFlightClientConfigBundlerESM.js similarity index 75% rename from packages/react-server-dom-esm/src/ReactFlightClientConfigESMBundler.js rename to packages/react-server-dom-esm/src/ReactFlightClientConfigBundlerESM.js index 55deba3073677..87f8f06bc848f 100644 --- a/packages/react-server-dom-esm/src/ReactFlightClientConfigESMBundler.js +++ b/packages/react-server-dom-esm/src/ReactFlightClientConfigBundlerESM.js @@ -12,13 +12,16 @@ import type { FulfilledThenable, RejectedThenable, } from 'shared/ReactTypes'; +import type {ModuleLoading} from 'react-client/src/ReactFlightClientConfig'; -export type SSRManifest = string; // Module root path +export type SSRModuleMap = string; // Module root path export type ServerManifest = string; // Module root path export type ServerReferenceId = string; +import {prepareDestinationForModuleImpl} from 'react-client/src/ReactFlightClientConfig'; + export opaque type ClientReferenceMetadata = [ string, // module path string, // export name @@ -30,8 +33,21 @@ export opaque type ClientReference = { name: string, }; +// The reason this function needs to defined here in this file instead of just +// being exported directly from the WebpackDestination... file is because the +// ClientReferenceMetadata is opaque and we can't unwrap it there. +// This should get inlined and we could also just implement an unwrapping function +// though that risks it getting used in places it shouldn't be. This is unfortunate +// but currently it seems to be the best option we have. +export function prepareDestinationForModule( + moduleLoading: ModuleLoading, + metadata: ClientReferenceMetadata, +) { + prepareDestinationForModuleImpl(moduleLoading, metadata[0]); +} + export function resolveClientReference( - bundlerConfig: SSRManifest, + bundlerConfig: SSRModuleMap, metadata: ClientReferenceMetadata, ): ClientReference { const baseURL = bundlerConfig; diff --git a/packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMBrowser.js b/packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMBrowser.js new file mode 100644 index 0000000000000..2906fa52758bd --- /dev/null +++ b/packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMBrowser.js @@ -0,0 +1,17 @@ +/** + * 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 ModuleLoading = null; + +export function prepareDestinationForModuleImpl( + moduleLoading: ModuleLoading, + chunks: mixed, +) { + // In the browser we don't need to prepare our destination since the browser is the Destination +} diff --git a/packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMServer.js b/packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMServer.js new file mode 100644 index 0000000000000..cc549b581d9be --- /dev/null +++ b/packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMServer.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 + */ + +import {preinitModuleForSSR} from 'react-client/src/ReactFlightClientConfig'; + +export type ModuleLoading = + | null + | string + | { + prefix: string, + crossOrigin?: string, + }; + +export function prepareDestinationForModuleImpl( + moduleLoading: ModuleLoading, + // Chunks are double-indexed [..., idx, filenamex, idy, filenamey, ...] + mod: string, +) { + if (typeof moduleLoading === 'string') { + preinitModuleForSSR(moduleLoading + mod, undefined); + } else if (moduleLoading !== null) { + preinitModuleForSSR(moduleLoading.prefix + mod, moduleLoading.crossOrigin); + } +} diff --git a/packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js index e3ebaf3fb1aae..701286d32f348 100644 --- a/packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js @@ -36,6 +36,7 @@ export type Options = { function createResponseFromOptions(options: void | Options) { return createResponse( options && options.moduleBaseURL ? options.moduleBaseURL : '', + null, options && options.callServer ? options.callServer : undefined, ); } diff --git a/packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js b/packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js index 4288d5878a928..7a8a77bffa95a 100644 --- a/packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js @@ -41,9 +41,13 @@ export function createServerReference, T>( function createFromNodeStream( stream: Readable, moduleRootPath: string, - moduleBaseURL: string, // TODO: Used for preloading hints + moduleBaseURL: string, ): Thenable { - const response: Response = createResponse(moduleRootPath, noServerCall); + const response: Response = createResponse( + moduleRootPath, + moduleBaseURL, + noServerCall, + ); stream.on('data', chunk => { processBinaryChunk(response, chunk); }); diff --git a/packages/react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer.js b/packages/react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer.js index 5115d72f93091..55c763bd01271 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer.js +++ b/packages/react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer.js @@ -7,7 +7,7 @@ * @flow */ -import {preinitModulesForSSR} from 'react-client/src/ReactFlightClientConfig'; +import {preinitScriptForSSR} from 'react-client/src/ReactFlightClientConfig'; export type ModuleLoading = null | { prefix: string, @@ -21,7 +21,7 @@ export function prepareDestinationWithChunks( ) { if (moduleLoading !== null) { for (let i = 1; i < chunks.length; i += 2) { - preinitModulesForSSR( + preinitScriptForSSR( moduleLoading.prefix + chunks[i], moduleLoading.crossOrigin, ); diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index 7137503a16be2..8073b5a3441aa 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -112,6 +112,7 @@ module.exports = [ 'react-server-dom-esm', 'react-server-dom-esm/client', 'react-server-dom-esm/client.browser', + 'react-server-dom-esm/src/ReactFlightDOMClientBrowser.js', // react-server-dom-esm/client.browser 'react-devtools', 'react-devtools-core', 'react-devtools-shell', @@ -214,7 +215,8 @@ module.exports = [ 'react-server-dom-esm/client.node', 'react-server-dom-esm/server', 'react-server-dom-esm/server.node', - 'react-server-dom-esm/src/ReactFlightDOMServerNode.js', // react-server-dom-webpack/server.node + 'react-server-dom-esm/src/ReactFlightDOMServerNode.js', // react-server-dom-esm/server.node + 'react-server-dom-esm/src/ReactFlightDOMClientNode.js', // react-server-dom-esm/client.node 'react-devtools', 'react-devtools-core', 'react-devtools-shell',