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 14, 2023
1 parent cbd1f41 commit 856057e
Show file tree
Hide file tree
Showing 16 changed files with 126 additions and 36 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
25 changes: 18 additions & 7 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,20 +63,29 @@ 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, {
const {pipe} = renderToPipeableStream(React.createElement(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'],
Expand All @@ -89,6 +99,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
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 @@ -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 @@ -5228,6 +5228,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';

0 comments on commit 856057e

Please sign in to comment.