Skip to content

Commit

Permalink
ESM implementation of module preinitialization
Browse files Browse the repository at this point in the history
  • Loading branch information
gnoff committed Sep 20, 2023
1 parent b060ed9 commit 0df428a
Show file tree
Hide file tree
Showing 13 changed files with 149 additions and 48 deletions.
1 change: 1 addition & 0 deletions fixtures/flight-esm/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v18
38 changes: 28 additions & 10 deletions fixtures/flight-esm/server/global.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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) {
Expand All @@ -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 => {
Expand Down
20 changes: 1 addition & 19 deletions fixtures/flight-esm/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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(
Expand Down Expand Up @@ -84,9 +68,7 @@ export default async function App() {
'Like'
)
)
),
// TODO: Move this to bootstrapModules.
h('script', {type: 'module', src: '/src/index.js'})
)
)
);
}
22 changes: 11 additions & 11 deletions fixtures/flight-esm/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,20 @@ function refineModel<T>(code: T, model: HintModel<any>): HintModel<T> {
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,8 +33,22 @@ export opaque type ClientReference<T> = {
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<T>(
bundlerConfig: SSRManifest,
bundlerConfig: SSRModuleMap,
metadata: ClientReferenceMetadata,
): ClientReference<T> {
const baseURL = bundlerConfig;
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}

Expand Down
14 changes: 12 additions & 2 deletions packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,22 @@ export function createServerReference<A: Iterable<any>, T>(
return createServerReferenceImpl(id, noServerCall);
}

export type Options = {
nonce?: string,
};

function createFromNodeStream<T>(
stream: Readable,
moduleRootPath: string,
moduleBaseURL: string, // TODO: Used for preloading hints
moduleBaseURL: string,
options?: Options,
): Thenable<T> {
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);
});
Expand Down
4 changes: 3 additions & 1 deletion scripts/shared/inlinedHostConfigs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down

0 comments on commit 0df428a

Please sign in to comment.