From 0df428a381d1a586b9dec345a5ca708cd9108a83 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Thu, 10 Aug 2023 09:17:21 -0700 Subject: [PATCH] ESM implementation of module preinitialization --- fixtures/flight-esm/.nvmrc | 1 + fixtures/flight-esm/server/global.js | 38 ++++++++++++++----- fixtures/flight-esm/src/App.js | 20 +--------- fixtures/flight-esm/yarn.lock | 22 +++++------ ...ReactFlightClientConfig.dom-browser-esm.js | 3 +- .../ReactFlightClientConfig.dom-node-esm.js | 5 ++- .../src/shared/ReactFlightClientConfigDOM.js | 14 +++++++ ...s => ReactFlightClientConfigBundlerESM.js} | 21 +++++++++- ...ReactFlightClientConfigTargetESMBrowser.js | 18 +++++++++ .../ReactFlightClientConfigTargetESMServer.js | 35 +++++++++++++++++ .../src/ReactFlightDOMClientBrowser.js | 2 + .../src/ReactFlightDOMClientNode.js | 14 ++++++- scripts/shared/inlinedHostConfigs.js | 4 +- 13 files changed, 149 insertions(+), 48 deletions(-) create mode 100644 fixtures/flight-esm/.nvmrc rename packages/react-server-dom-esm/src/{ReactFlightClientConfigESMBundler.js => ReactFlightClientConfigBundlerESM.js} (74%) create mode 100644 packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMBrowser.js create mode 100644 packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMServer.js 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-esm/yarn.lock b/fixtures/flight-esm/yarn.lock index 409a5592ba966..a00d5c244d88d 100644 --- a/fixtures/flight-esm/yarn.lock +++ b/fixtures/flight-esm/yarn.lock @@ -540,17 +540,17 @@ raw-body@2.5.2: unpipe "1.0.0" react-dom@experimental: - version "0.0.0-experimental-018c58c9c-20230601" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-0.0.0-experimental-018c58c9c-20230601.tgz#2cc0ac824b83bab2ac1c6187f241dbd5dcd5201b" - integrity sha512-hwRsyoG1R3Tub0nUa72YvNcqPvU+pTcr9dadOnUCKKfSiYVbBCy7LxmkqLauCD8OjNJMlwtMgG4UAgtidclYGQ== + version "0.0.0-experimental-b9be4537c-20230905" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-0.0.0-experimental-b9be4537c-20230905.tgz#b078d6d06041e0c98ce5a2f5e9ff26a2e308eb41" + integrity sha512-veAFNVj81lUYhYlucYm3kbj2BhakG57XYkWC/QHVEZDk4Hm2qxM9RUk7gn8dWs9Eq7KR6Q+JWiSH3ZbObQTV9g== dependencies: loose-envify "^1.1.0" - scheduler "0.0.0-experimental-018c58c9c-20230601" + scheduler "0.0.0-experimental-b9be4537c-20230905" react@experimental: - version "0.0.0-experimental-018c58c9c-20230601" - resolved "https://registry.yarnpkg.com/react/-/react-0.0.0-experimental-018c58c9c-20230601.tgz#ab04d1243c8f83b0166ed342056fa6b38ab2cd23" - integrity sha512-nSQIBsZ26Ii899pZ9cRt/6uQLbIUEAcDIivvAQyaHp4pWm289aB+7AK7VCWojAJIf4OStCuWs2berZsk4mzLVg== + version "0.0.0-experimental-b9be4537c-20230905" + resolved "https://registry.yarnpkg.com/react/-/react-0.0.0-experimental-b9be4537c-20230905.tgz#3c2352b42b8024544a12dcd96f2700313cebcb6b" + integrity sha512-QNeK74S7AU94j4vCxet2S76HqxpF6CJo1pG3XcgY2NravyXdWYszrRDNHrfu86gGNwAQvSU+YpStYn/i0b9tLA== dependencies: loose-envify "^1.1.0" @@ -588,10 +588,10 @@ safe-buffer@5.1.2: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -scheduler@0.0.0-experimental-018c58c9c-20230601: - version "0.0.0-experimental-018c58c9c-20230601" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.0.0-experimental-018c58c9c-20230601.tgz#4f083614f8e857bab63dd90b4b37b03783dafe6b" - integrity sha512-otUM7AAAnCoJ5/0jTQwUQ7NhxjgcPEdrfzW7NfkpocrDoTUbql1kIGIhj9L9POMVFDI/wcZzRNK/oIEWsB4DPw== +scheduler@0.0.0-experimental-b9be4537c-20230905: + version "0.0.0-experimental-b9be4537c-20230905" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.0.0-experimental-b9be4537c-20230905.tgz#f0fe5a710ce15a9d637c28e9f019a4100e1f3f34" + integrity sha512-V5P9LOS+c5CG7qaCJu+Qgcz9eh/dP4nBszj3w1MCgZnMtAna6+J8ZuuUnRDMeY86F8KH+cY8Q5beIvAL2noMzA== dependencies: loose-envify "^1.1.0" 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 48f4b24a362f2..3ef89a47f9593 100644 --- a/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js +++ b/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js @@ -113,6 +113,20 @@ function refineModel(code: T, model: HintModel): HintModel { return model; } +export function preinitModuleForSSR( + href: string, + nonce: ?string, + crossOrigin: ?string, +) { + const dispatcher = ReactDOMCurrentDispatcher.current; + if (dispatcher) { + dispatcher.preinitModuleScript(href, { + crossOrigin: getCrossOriginString(crossOrigin), + nonce, + }); + } +} + export function preinitScriptForSSR( href: string, nonce: ?string, diff --git a/packages/react-server-dom-esm/src/ReactFlightClientConfigESMBundler.js b/packages/react-server-dom-esm/src/ReactFlightClientConfigBundlerESM.js similarity index 74% rename from packages/react-server-dom-esm/src/ReactFlightClientConfigESMBundler.js rename to packages/react-server-dom-esm/src/ReactFlightClientConfigBundlerESM.js index 55deba3073677..5db99dc8fff05 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,22 @@ 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, + nonce: ?string, + metadata: ClientReferenceMetadata, +) { + prepareDestinationForModuleImpl(moduleLoading, metadata[0], nonce); +} + 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..0ba181c4cdfea --- /dev/null +++ b/packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMBrowser.js @@ -0,0 +1,18 @@ +/** + * 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, + nonce: ?string, +) { + // 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..c435e40b0b0f8 --- /dev/null +++ b/packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMServer.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 {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, + nonce: ?string, +) { + if (typeof moduleLoading === 'string') { + preinitModuleForSSR(moduleLoading + mod, nonce, undefined); + } else if (moduleLoading !== null) { + preinitModuleForSSR( + moduleLoading.prefix + mod, + nonce, + moduleLoading.crossOrigin, + ); + } +} diff --git a/packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js index e3ebaf3fb1aae..181328e93fda5 100644 --- a/packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js @@ -36,7 +36,9 @@ export type Options = { function createResponseFromOptions(options: void | Options) { return createResponse( options && options.moduleBaseURL ? options.moduleBaseURL : '', + null, options && options.callServer ? options.callServer : undefined, + undefined, // nonce ); } diff --git a/packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js b/packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js index 4288d5878a928..dbc9ed8e3d2a0 100644 --- a/packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js @@ -38,12 +38,22 @@ export function createServerReference, T>( return createServerReferenceImpl(id, noServerCall); } +export type Options = { + nonce?: string, +}; + function createFromNodeStream( stream: Readable, moduleRootPath: string, - moduleBaseURL: string, // TODO: Used for preloading hints + moduleBaseURL: string, + options?: Options, ): Thenable { - const response: Response = createResponse(moduleRootPath, noServerCall); + const response: Response = createResponse( + moduleRootPath, + moduleBaseURL, + noServerCall, + options && typeof options.nonce === 'string' ? options.nonce : undefined, + ); stream.on('data', chunk => { processBinaryChunk(response, chunk); }); diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index 56d92a94bbe31..c958a4a4ccfae 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -116,6 +116,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', @@ -221,7 +222,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',