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 Aug 30, 2023
1 parent bee5c3e commit ebd48ac
Show file tree
Hide file tree
Showing 19 changed files with 141 additions and 59 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
3 changes: 3 additions & 0 deletions fixtures/flight-esm/server/region.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,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, moduleBasePath);
await new Promise(res => {
setTimeout(res, 1000);
});
pipe(res);
}

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'})
)
)
);
}
15 changes: 10 additions & 5 deletions fixtures/flight/server/global.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,17 @@ app.all('/', async function (req, res, next) {
let root;
let Root = () => {
if (root) {
return root;
return React.use(root);
}
root = createFromNodeStream(rscResponse, ssrBundleConfig.ssrManifest, {
moduleLoading: ssrBundleConfig.moduleLoading,
});
return root;
return React.use(
(root = createFromNodeStream(
rscResponse,
ssrBundleConfig.ssrManifest,
{
moduleLoading: ssrBundleConfig.moduleLoading,
}
))
);
};
// Render it into HTML by resolving the client components
res.set('Content-type', 'text/html');
Expand Down
3 changes: 3 additions & 0 deletions fixtures/flight/server/region.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

declare var $$$config: any;

export opaque type ModuleLoading = mixed;
export opaque type SSRManifest = mixed;
export opaque type ServerManifest = mixed;
export opaque type ServerReferenceId = string;
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 @@ -5328,6 +5328,7 @@ function preinit(href: string, options: PreinitOptions): void {
}
return;
}
case 'module':
case 'script': {
const src = href;
const key = getResourceKey(as, src);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
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 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,6 +33,19 @@ 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,
metadata: ClientReferenceMetadata,
) {
prepareDestinationForModuleImpl(moduleLoading, metadata[0]);
}

export function resolveClientReference<T>(
bundlerConfig: SSRManifest,
metadata: ClientReferenceMetadata,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}
Expand Down
8 changes: 6 additions & 2 deletions packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,13 @@ export function createServerReference<A: Iterable<any>, T>(
function createFromNodeStream<T>(
stream: Readable,
moduleRootPath: string,
moduleBaseURL: string, // TODO: Used for preloading hints
moduleBaseURL: string,
): Thenable<T> {
const response: Response = createResponse(moduleRootPath, noServerCall);
const response: Response = createResponse(
moduleRootPath,
moduleBaseURL,
noServerCall,
);
stream.on('data', chunk => {
processBinaryChunk(response, chunk);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
);
Expand Down
17 changes: 1 addition & 16 deletions packages/shared/ReactVersion.js
Original file line number Diff line number Diff line change
@@ -1,16 +1 @@
/**
* 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.
*/

// TODO: this is special because it gets imported during build.
//
// TODO: 18.0.0 has not been released to NPM;
// It exists as a placeholder so that DevTools can support work tag changes between releases.
// When we next publish a release, update the matching TODO in backend/renderer.js
// TODO: This module is used both by the release scripts and to expose a version
// at runtime. We should instead inject the version number as part of the build
// process, and use the ReactVersions.js module as the single source of truth.
export default '18.2.0';
export default '18.3.0-PLACEHOLDER';
4 changes: 3 additions & 1 deletion scripts/shared/inlinedHostConfigs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down

0 comments on commit ebd48ac

Please sign in to comment.