Split out Edge and Node implementations of the Flight Client (#26187)

This splits out the Edge and Node implementations of Flight Client into
their own implementations. The Node implementation now takes a Node
Stream as input.

I removed the bundler config from the Browser variant because you're
never supposed to use that in the browser since it's only for SSR.
Similarly, it's required on the server. This also enables generating a
SSR manifest from the Webpack plugin. This is necessary for SSR so that
you can reverse look up what a client module is called on the server.

I also removed the option to pass a callServer from the server. We might
want to add it back in the future but basically, we don't recommend
calling Server Functions from render for initial render because if that
happened client-side it would be a client-side waterfall. If it's never
called in initial render, then it also shouldn't ever happen during SSR.
This might be considered too restrictive.

~This also compiles the unbundled packages as ESM. This isn't strictly
necessary because we only need access to dynamic import to load the
modules but we don't have any other build options that leave
`import(...)` intact, and seems appropriate that this would also be an
ESM module.~ Went with `import(...)` in CJS instead.
This commit is contained in:
Sebastian Markbåge 2023-02-21 13:18:24 -05:00 committed by GitHub
parent 70b0bbda76
commit 60144a04da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 657 additions and 124 deletions

View File

@ -23,8 +23,17 @@ async function babelLoad(url, context, defaultLoad) {
const result = await defaultLoad(url, context, defaultLoad);
if (result.format === 'module') {
const opt = Object.assign({filename: url}, babelOptions);
const {code} = await babel.transformAsync(result.source, opt);
return {source: code, format: 'module'};
const newResult = await babel.transformAsync(result.source, opt);
if (!newResult) {
if (typeof result.source === 'string') {
return result;
}
return {
source: Buffer.from(result.source).toString('utf8'),
format: 'module',
};
}
return {source: newResult.code, format: 'module'};
}
return defaultLoad(url, context, defaultLoad);
}
@ -39,8 +48,16 @@ async function babelTransformSource(source, context, defaultTransformSource) {
const {format} = context;
if (format === 'module') {
const opt = Object.assign({filename: context.url}, babelOptions);
const {code} = await babel.transformAsync(source, opt);
return {source: code};
const newResult = await babel.transformAsync(source, opt);
if (!newResult) {
if (typeof source === 'string') {
return {source};
}
return {
source: Buffer.from(source).toString('utf8'),
};
}
return {source: newResult.code};
}
return defaultTransformSource(source, context, defaultTransformSource);
}

View File

@ -1,11 +1,13 @@
'use strict';
const {renderToPipeableStream} = require('react-server-dom-webpack/server');
const {readFile} = require('fs').promises;
const {resolve} = require('path');
const React = require('react');
module.exports = async function (req, res) {
const {renderToPipeableStream} = await import(
'react-server-dom-webpack/server'
);
switch (req.method) {
case 'POST': {
const serverReference = JSON.parse(req.get('rsc-action'));

View File

@ -0,0 +1,34 @@
/**
* 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 {TextDecoder} from 'util';
export type StringDecoder = TextDecoder;
export const supportsBinaryStreams = true;
export function createStringDecoder(): StringDecoder {
return new TextDecoder();
}
const decoderOptions = {stream: true};
export function readPartialStringChunk(
decoder: StringDecoder,
buffer: Uint8Array,
): string {
return decoder.decode(buffer, decoderOptions);
}
export function readFinalStringChunk(
decoder: StringDecoder,
buffer: Uint8Array,
): string {
return decoder.decode(buffer);
}

View File

@ -7,6 +7,6 @@
* @flow
*/
export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
export * from 'react-client/src/ReactFlightClientHostConfigNode';
export * from 'react-client/src/ReactFlightClientHostConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';

View File

@ -7,6 +7,6 @@
* @flow
*/
export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
export * from 'react-client/src/ReactFlightClientHostConfigNode';
export * from 'react-client/src/ReactFlightClientHostConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';
export * from 'react-server-dom-webpack/src/ReactFlightClientNodeBundlerConfig';

View File

@ -7,4 +7,4 @@
* @flow
*/
export * from './src/ReactFlightDOMClient';
export * from './src/ReactFlightDOMClientBrowser';

View File

@ -7,4 +7,4 @@
* @flow
*/
export * from './src/ReactFlightDOMClient';
export * from './src/ReactFlightDOMClientEdge';

View File

@ -7,4 +7,4 @@
* @flow
*/
export * from './src/ReactFlightDOMClient';
export * from './src/ReactFlightDOMClientNode';

View File

@ -7,4 +7,4 @@
* @flow
*/
export * from './src/ReactFlightDOMClient';
export * from './src/ReactFlightDOMClientNode';

View File

@ -64,7 +64,7 @@
"./server.edge": "./server.edge.js",
"./server.node": "./server.node.js",
"./server.node.unbundled": "./server.node.unbundled.js",
"./node-loader": "./esm/react-server-dom-webpack-node-loader.js",
"./node-loader": "./esm/react-server-dom-webpack-node-loader.production.min.js",
"./node-register": "./node-register.js",
"./src/*": "./src/*",
"./package.json": "./package.json"

View File

@ -0,0 +1,98 @@
/**
* 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 type {
Thenable,
FulfilledThenable,
RejectedThenable,
} from 'shared/ReactTypes';
export type WebpackSSRMap = {
[clientId: string]: {
[clientExportName: string]: ClientReference<any>,
},
};
export type BundlerConfig = WebpackSSRMap;
export opaque type ClientReferenceMetadata = {
id: string,
chunks: Array<string>,
name: string,
async: boolean,
};
// eslint-disable-next-line no-unused-vars
export opaque type ClientReference<T> = {
specifier: string,
name: string,
};
export function resolveClientReference<T>(
bundlerConfig: BundlerConfig,
metadata: ClientReferenceMetadata,
): ClientReference<T> {
const resolvedModuleData = bundlerConfig[metadata.id][metadata.name];
return resolvedModuleData;
}
const asyncModuleCache: Map<string, Thenable<any>> = new Map();
export function preloadModule<T>(
metadata: ClientReference<T>,
): null | Thenable<any> {
const existingPromise = asyncModuleCache.get(metadata.specifier);
if (existingPromise) {
if (existingPromise.status === 'fulfilled') {
return null;
}
return existingPromise;
} else {
// $FlowFixMe[unsupported-syntax]
const modulePromise: Thenable<T> = import(metadata.specifier);
modulePromise.then(
value => {
const fulfilledThenable: FulfilledThenable<mixed> =
(modulePromise: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = value;
},
reason => {
const rejectedThenable: RejectedThenable<mixed> = (modulePromise: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = reason;
},
);
asyncModuleCache.set(metadata.specifier, modulePromise);
return modulePromise;
}
}
export function requireModule<T>(metadata: ClientReference<T>): T {
let moduleExports;
// We assume that preloadModule has been called before, which
// should have added something to the module cache.
const promise: any = asyncModuleCache.get(metadata.specifier);
if (promise.status === 'fulfilled') {
moduleExports = promise.value;
} else {
throw promise.reason;
}
if (metadata.name === '*') {
// This is a placeholder value that represents that the caller imported this
// as a CommonJS module as is.
return moduleExports;
}
if (metadata.name === '') {
// This is a placeholder value that represents that the caller accessed the
// default property of this if it was an ESM interop module.
return moduleExports.default;
}
return moduleExports[metadata.name];
}

View File

@ -11,8 +11,6 @@ import type {Thenable} from 'shared/ReactTypes.js';
import type {Response as FlightResponse} from 'react-client/src/ReactFlightClientStream';
import type {BundlerConfig} from './ReactFlightClientWebpackBundlerConfig';
import {
createResponse,
getRoot,
@ -28,10 +26,16 @@ type CallServerCallback = <A, T>(
) => Promise<T>;
export type Options = {
moduleMap?: BundlerConfig,
callServer?: CallServerCallback,
};
function createResponseFromOptions(options: void | Options) {
return createResponse(
null,
options && options.callServer ? options.callServer : undefined,
);
}
function startReadingFromStream(
response: FlightResponse,
stream: ReadableStream,
@ -63,10 +67,7 @@ function createFromReadableStream<T>(
stream: ReadableStream,
options?: Options,
): Thenable<T> {
const response: FlightResponse = createResponse(
options && options.moduleMap ? options.moduleMap : null,
options && options.callServer ? options.callServer : undefined,
);
const response: FlightResponse = createResponseFromOptions(options);
startReadingFromStream(response, stream);
return getRoot(response);
}
@ -75,10 +76,7 @@ function createFromFetch<T>(
promiseForResponse: Promise<Response>,
options?: Options,
): Thenable<T> {
const response: FlightResponse = createResponse(
options && options.moduleMap ? options.moduleMap : null,
options && options.callServer ? options.callServer : undefined,
);
const response: FlightResponse = createResponseFromOptions(options);
promiseForResponse.then(
function (r) {
startReadingFromStream(response, (r.body: any));
@ -94,10 +92,7 @@ function createFromXHR<T>(
request: XMLHttpRequest,
options?: Options,
): Thenable<T> {
const response: FlightResponse = createResponse(
options && options.moduleMap ? options.moduleMap : null,
options && options.callServer ? options.callServer : undefined,
);
const response: FlightResponse = createResponseFromOptions(options);
let processedLength = 0;
function progress(e: ProgressEvent): void {
const chunk = request.responseText;

View File

@ -0,0 +1,95 @@
/**
* 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 type {Thenable} from 'shared/ReactTypes.js';
import type {Response as FlightResponse} from 'react-client/src/ReactFlightClientStream';
import type {BundlerConfig} from './ReactFlightClientWebpackBundlerConfig';
import {
createResponse,
getRoot,
reportGlobalError,
processBinaryChunk,
close,
} from 'react-client/src/ReactFlightClientStream';
function noServerCall() {
throw new Error(
'Server Functions cannot be called during initial render. ' +
'This would create a fetch waterfall. Try to use a Server Component ' +
'to pass data to Client Components instead.',
);
}
export type Options = {
moduleMap?: BundlerConfig,
};
function createResponseFromOptions(options: void | Options) {
return createResponse(
options && options.moduleMap ? options.moduleMap : null,
noServerCall,
);
}
function startReadingFromStream(
response: FlightResponse,
stream: ReadableStream,
): void {
const reader = stream.getReader();
function progress({
done,
value,
}: {
done: boolean,
value: ?any,
...
}): void | Promise<void> {
if (done) {
close(response);
return;
}
const buffer: Uint8Array = (value: any);
processBinaryChunk(response, buffer);
return reader.read().then(progress).catch(error);
}
function error(e: any) {
reportGlobalError(response, e);
}
reader.read().then(progress).catch(error);
}
function createFromReadableStream<T>(
stream: ReadableStream,
options?: Options,
): Thenable<T> {
const response: FlightResponse = createResponseFromOptions(options);
startReadingFromStream(response, stream);
return getRoot(response);
}
function createFromFetch<T>(
promiseForResponse: Promise<Response>,
options?: Options,
): Thenable<T> {
const response: FlightResponse = createResponseFromOptions(options);
promiseForResponse.then(
function (r) {
startReadingFromStream(response, (r.body: any));
},
function (e) {
reportGlobalError(response, e);
},
);
return getRoot(response);
}
export {createFromFetch, createFromReadableStream};

View File

@ -0,0 +1,54 @@
/**
* 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 type {Thenable} from 'shared/ReactTypes.js';
import type {Response} from 'react-client/src/ReactFlightClientStream';
import type {BundlerConfig} from 'react-client/src/ReactFlightClientHostConfig';
import type {Readable} from 'stream';
import {
createResponse,
getRoot,
reportGlobalError,
processBinaryChunk,
close,
} from 'react-client/src/ReactFlightClientStream';
import {processStringChunk} from '../../react-client/src/ReactFlightClientStream';
function noServerCall() {
throw new Error(
'Server Functions cannot be called during initial render. ' +
'This would create a fetch waterfall. Try to use a Server Component ' +
'to pass data to Client Components instead.',
);
}
function createFromNodeStream<T>(
stream: Readable,
moduleMap: $NonMaybeType<BundlerConfig>,
): Thenable<T> {
const response: Response = createResponse(moduleMap, noServerCall);
stream.on('data', chunk => {
if (typeof chunk === 'string') {
processStringChunk(response, chunk, 0);
} else {
processBinaryChunk(response, chunk);
}
});
stream.on('error', error => {
reportGlobalError(response, error);
});
stream.on('end', () => close(response));
return getRoot(response);
}
export {createFromNodeStream};

View File

@ -55,7 +55,8 @@ type Options = {
isServer: boolean,
clientReferences?: ClientReferencePath | $ReadOnlyArray<ClientReferencePath>,
chunkName?: string,
manifestFilename?: string,
clientManifestFilename?: string,
ssrManifestFilename?: string,
};
const PLUGIN_NAME = 'React Server Plugin';
@ -63,7 +64,8 @@ const PLUGIN_NAME = 'React Server Plugin';
export default class ReactFlightWebpackPlugin {
clientReferences: $ReadOnlyArray<ClientReferencePath>;
chunkName: string;
manifestFilename: string;
clientManifestFilename: string;
ssrManifestFilename: string;
constructor(options: Options) {
if (!options || typeof options.isServer !== 'boolean') {
@ -99,8 +101,10 @@ export default class ReactFlightWebpackPlugin {
} else {
this.chunkName = 'client[index]';
}
this.manifestFilename =
options.manifestFilename || 'react-client-manifest.json';
this.clientManifestFilename =
options.clientManifestFilename || 'react-client-manifest.json';
this.ssrManifestFilename =
options.ssrManifestFilename || 'react-ssr-manifest.json';
}
apply(compiler: any) {
@ -209,15 +213,20 @@ export default class ReactFlightWebpackPlugin {
if (clientFileNameFound === false) {
compilation.warnings.push(
new WebpackError(
`Client runtime at ${clientImportName} was not found. React Server Components module map file ${_this.manifestFilename} was not created.`,
`Client runtime at ${clientImportName} was not found. React Server Components module map file ${_this.clientManifestFilename} was not created.`,
),
);
return;
}
const json: {
const clientManifest: {
[string]: {
[string]: {chunks: $FlowFixMe, id: $FlowFixMe, name: string},
[string]: {chunks: $FlowFixMe, id: string, name: string},
},
} = {};
const ssrManifest: {
[string]: {
[string]: {specifier: string, name: string},
},
} = {};
compilation.chunkGroups.forEach(function (chunkGroup) {
@ -239,9 +248,16 @@ export default class ReactFlightWebpackPlugin {
.getExportsInfo(module)
.getProvidedExports();
const moduleExports: {
const clientExports: {
[string]: {chunks: $FlowFixMe, id: $FlowFixMe, name: string},
} = {};
const ssrExports: {
[string]: {specifier: string, name: string},
} = {};
ssrManifest[id] = ssrExports;
['', '*']
.concat(
Array.isArray(moduleProvidedExports)
@ -249,16 +265,21 @@ export default class ReactFlightWebpackPlugin {
: [],
)
.forEach(function (name) {
moduleExports[name] = {
clientExports[name] = {
id,
chunks: chunkIds,
name: name,
};
ssrExports[name] = {
specifier: module.resource,
name: name,
};
});
const href = pathToFileURL(module.resource).href;
if (href !== undefined) {
json[href] = moduleExports;
clientManifest[href] = clientExports;
ssrManifest[href] = ssrExports;
}
}
@ -280,10 +301,15 @@ export default class ReactFlightWebpackPlugin {
});
});
const output = JSON.stringify(json, null, 2);
const clientOutput = JSON.stringify(clientManifest, null, 2);
compilation.emitAsset(
_this.manifestFilename,
new sources.RawSource(output, false),
_this.clientManifestFilename,
new sources.RawSource(clientOutput, false),
);
const ssrOutput = JSON.stringify(ssrManifest, null, 2);
compilation.emitAsset(
_this.ssrManifestFilename,
new sources.RawSource(ssrOutput, false),
);
},
);

View File

@ -18,12 +18,10 @@ global.TextDecoder = require('util').TextDecoder;
let clientExports;
let serverExports;
let webpackMap;
let webpackModules;
let webpackServerMap;
let act;
let React;
let ReactDOMClient;
let ReactDOMServer;
let ReactServerDOMWriter;
let ReactServerDOMReader;
let Suspense;
@ -37,29 +35,15 @@ describe('ReactFlightDOMBrowser', () => {
clientExports = WebpackMock.clientExports;
serverExports = WebpackMock.serverExports;
webpackMap = WebpackMock.webpackMap;
webpackModules = WebpackMock.webpackModules;
webpackServerMap = WebpackMock.webpackServerMap;
React = require('react');
ReactDOMClient = require('react-dom/client');
ReactDOMServer = require('react-dom/server.browser');
ReactServerDOMWriter = require('react-server-dom-webpack/server.browser');
ReactServerDOMReader = require('react-server-dom-webpack/client');
Suspense = React.Suspense;
use = React.use;
});
async function readResult(stream) {
const reader = stream.getReader();
let result = '';
while (true) {
const {done, value} = await reader.read();
if (done) {
return result;
}
result += Buffer.from(value).toString('utf8');
}
}
function makeDelayedText(Model) {
let error, _resolve, _reject;
let promise = new Promise((resolve, reject) => {
@ -466,52 +450,6 @@ describe('ReactFlightDOMBrowser', () => {
expect(isDone).toBeTruthy();
});
// @gate enableUseHook
it('should allow an alternative module mapping to be used for SSR', async () => {
function ClientComponent() {
return <span>Client Component</span>;
}
// The Client build may not have the same IDs as the Server bundles for the same
// component.
const ClientComponentOnTheClient = clientExports(ClientComponent);
const ClientComponentOnTheServer = clientExports(ClientComponent);
// In the SSR bundle this module won't exist. We simulate this by deleting it.
const clientId = webpackMap[ClientComponentOnTheClient.filepath]['*'].id;
delete webpackModules[clientId];
// Instead, we have to provide a translation from the client meta data to the SSR
// meta data.
const ssrMetadata = webpackMap[ClientComponentOnTheServer.filepath]['*'];
const translationMap = {
[clientId]: {
'*': ssrMetadata,
},
};
function App() {
return <ClientComponentOnTheClient />;
}
const stream = ReactServerDOMWriter.renderToReadableStream(
<App />,
webpackMap,
);
const response = ReactServerDOMReader.createFromReadableStream(stream, {
moduleMap: translationMap,
});
function ClientRoot() {
return use(response);
}
const ssrStream = await ReactDOMServer.renderToReadableStream(
<ClientRoot />,
);
const result = await readResult(ssrStream);
expect(result).toEqual('<span>Client Component</span>');
});
// @gate enableUseHook
it('should be able to complete after aborting and throw the reason client-side', async () => {
const reportedErrors = [];

View File

@ -0,0 +1,98 @@
/**
* 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.
*
* @emails react-core
*/
'use strict';
// Polyfills for test environment
global.ReadableStream =
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;
let clientExports;
let webpackMap;
let webpackModules;
let React;
let ReactDOMServer;
let ReactServerDOMWriter;
let ReactServerDOMReader;
let use;
describe('ReactFlightDOMEdge', () => {
beforeEach(() => {
jest.resetModules();
const WebpackMock = require('./utils/WebpackMock');
clientExports = WebpackMock.clientExports;
webpackMap = WebpackMock.webpackMap;
webpackModules = WebpackMock.webpackModules;
React = require('react');
ReactDOMServer = require('react-dom/server.edge');
ReactServerDOMWriter = require('react-server-dom-webpack/server.edge');
ReactServerDOMReader = require('react-server-dom-webpack/client.edge');
use = React.use;
});
async function readResult(stream) {
const reader = stream.getReader();
let result = '';
while (true) {
const {done, value} = await reader.read();
if (done) {
return result;
}
result += Buffer.from(value).toString('utf8');
}
}
// @gate enableUseHook
it('should allow an alternative module mapping to be used for SSR', async () => {
function ClientComponent() {
return <span>Client Component</span>;
}
// The Client build may not have the same IDs as the Server bundles for the same
// component.
const ClientComponentOnTheClient = clientExports(ClientComponent);
const ClientComponentOnTheServer = clientExports(ClientComponent);
// In the SSR bundle this module won't exist. We simulate this by deleting it.
const clientId = webpackMap[ClientComponentOnTheClient.filepath]['*'].id;
delete webpackModules[clientId];
// Instead, we have to provide a translation from the client meta data to the SSR
// meta data.
const ssrMetadata = webpackMap[ClientComponentOnTheServer.filepath]['*'];
const translationMap = {
[clientId]: {
'*': ssrMetadata,
},
};
function App() {
return <ClientComponentOnTheClient />;
}
const stream = ReactServerDOMWriter.renderToReadableStream(
<App />,
webpackMap,
);
const response = ReactServerDOMReader.createFromReadableStream(stream, {
moduleMap: translationMap,
});
function ClientRoot() {
return use(response);
}
const ssrStream = await ReactDOMServer.renderToReadableStream(
<ClientRoot />,
);
const result = await readResult(ssrStream);
expect(result).toEqual('<span>Client Component</span>');
});
});

View File

@ -0,0 +1,108 @@
/**
* 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.
*
* @emails react-core
*/
'use strict';
// Don't wait before processing work on the server.
// TODO: we can replace this with FlightServer.act().
global.setImmediate = cb => cb();
let clientExports;
let webpackMap;
let webpackModules;
let React;
let ReactDOMServer;
let ReactServerDOMWriter;
let ReactServerDOMReader;
let Stream;
let use;
describe('ReactFlightDOMNode', () => {
beforeEach(() => {
jest.resetModules();
const WebpackMock = require('./utils/WebpackMock');
clientExports = WebpackMock.clientExports;
webpackMap = WebpackMock.webpackMap;
webpackModules = WebpackMock.webpackModules;
React = require('react');
ReactDOMServer = require('react-dom/server.node');
ReactServerDOMWriter = require('react-server-dom-webpack/server.node');
ReactServerDOMReader = require('react-server-dom-webpack/client.node');
Stream = require('stream');
use = React.use;
});
function readResult(stream) {
return new Promise((resolve, reject) => {
let buffer = '';
const writable = new Stream.PassThrough();
writable.setEncoding('utf8');
writable.on('data', chunk => {
buffer += chunk;
});
writable.on('error', error => {
reject(error);
});
writable.on('end', () => {
resolve(buffer);
});
stream.pipe(writable);
});
}
// @gate enableUseHook
it('should allow an alternative module mapping to be used for SSR', async () => {
function ClientComponent() {
return <span>Client Component</span>;
}
// The Client build may not have the same IDs as the Server bundles for the same
// component.
const ClientComponentOnTheClient = clientExports(ClientComponent);
const ClientComponentOnTheServer = clientExports(ClientComponent);
// In the SSR bundle this module won't exist. We simulate this by deleting it.
const clientId = webpackMap[ClientComponentOnTheClient.filepath]['*'].id;
delete webpackModules[clientId];
// Instead, we have to provide a translation from the client meta data to the SSR
// meta data.
const ssrMetadata = webpackMap[ClientComponentOnTheServer.filepath]['*'];
const translationMap = {
[clientId]: {
'*': ssrMetadata,
},
};
function App() {
return <ClientComponentOnTheClient />;
}
const stream = ReactServerDOMWriter.renderToPipeableStream(
<App />,
webpackMap,
);
const readable = new Stream.PassThrough();
const response = ReactServerDOMReader.createFromNodeStream(
readable,
translationMap,
);
stream.pipe(readable);
function ClientRoot() {
return use(response);
}
const ssrStream = await ReactDOMServer.renderToPipeableStream(
<ClientRoot />,
);
const result = await readResult(ssrStream);
expect(result).toEqual('<span>Client Component</span>');
});
});

View File

@ -141,6 +141,23 @@ declare module 'util' {
declare function deprecate(f: Function, string: string): Function;
declare function promisify(f: Function): Function;
declare function callbackify(f: Function): Function;
declare class TextDecoder {
constructor(
encoding?: string,
options?: {
fatal?: boolean,
ignoreBOM?: boolean,
...
},
): void;
decode(
input?: ArrayBuffer | DataView | $TypedArray,
options?: {stream?: boolean, ...},
): string;
encoding: string;
fatal: boolean;
ignoreBOM: boolean;
}
declare class TextEncoder {
constructor(encoding?: string): TextEncoder;
encode(buffer: string): Uint8Array;

View File

@ -19,6 +19,7 @@ const Sync = require('./sync');
const sizes = require('./plugins/sizes-plugin');
const useForks = require('./plugins/use-forks-plugin');
const stripUnusedImports = require('./plugins/strip-unused-imports');
const dynamicImports = require('./plugins/dynamic-imports');
const Packaging = require('./packaging');
const {asyncRimRaf} = require('./utils');
const codeFrame = require('@babel/code-frame');
@ -45,7 +46,8 @@ process.on('unhandledRejection', err => {
const {
NODE_ES2015,
NODE_ESM,
ESM_DEV,
ESM_PROD,
UMD_DEV,
UMD_PROD,
UMD_PROFILING,
@ -216,7 +218,8 @@ function getFormat(bundleType) {
case RN_FB_PROD:
case RN_FB_PROFILING:
return `cjs`;
case NODE_ESM:
case ESM_DEV:
case ESM_PROD:
return `es`;
case BROWSER_SCRIPT:
return `iife`;
@ -226,8 +229,8 @@ function getFormat(bundleType) {
function isProductionBundleType(bundleType) {
switch (bundleType) {
case NODE_ES2015:
case NODE_ESM:
return true;
case ESM_DEV:
case UMD_DEV:
case NODE_DEV:
case BUN_DEV:
@ -235,6 +238,7 @@ function isProductionBundleType(bundleType) {
case RN_OSS_DEV:
case RN_FB_DEV:
return false;
case ESM_PROD:
case UMD_PROD:
case NODE_PROD:
case BUN_PROD:
@ -256,7 +260,6 @@ function isProductionBundleType(bundleType) {
function isProfilingBundleType(bundleType) {
switch (bundleType) {
case NODE_ES2015:
case NODE_ESM:
case FB_WWW_DEV:
case FB_WWW_PROD:
case NODE_DEV:
@ -267,6 +270,8 @@ function isProfilingBundleType(bundleType) {
case RN_FB_PROD:
case RN_OSS_DEV:
case RN_OSS_PROD:
case ESM_DEV:
case ESM_PROD:
case UMD_DEV:
case UMD_PROD:
case BROWSER_SCRIPT:
@ -328,6 +333,8 @@ function getPlugins(
bundleType === RN_FB_PROFILING;
const shouldStayReadable = isFBWWWBundle || isRNBundle || forcePrettyOutput;
return [
// Keep dynamic imports as externals
dynamicImports(),
{
name: 'rollup-plugin-flow-remove-types',
transform(code) {
@ -385,7 +392,7 @@ function getPlugins(
// Apply dead code elimination and/or minification.
// closure doesn't yet support leaving ESM imports intact
isProduction &&
bundleType !== NODE_ESM &&
bundleType !== ESM_PROD &&
closure({
compilation_level: 'SIMPLE',
language_in: 'ECMASCRIPT_2020',
@ -396,7 +403,9 @@ function getPlugins(
? 'ECMASCRIPT5'
: 'ECMASCRIPT5_STRICT',
emit_use_strict:
bundleType !== BROWSER_SCRIPT && bundleType !== NODE_ESM,
bundleType !== BROWSER_SCRIPT &&
bundleType !== ESM_PROD &&
bundleType !== ESM_DEV,
env: 'CUSTOM',
warning_level: 'QUIET',
apply_input_source_maps: false,
@ -404,6 +413,7 @@ function getPlugins(
process_common_js_modules: false,
rewrite_polyfills: false,
inject_libraries: false,
allow_dynamic_import: true,
// Don't let it create global variables in the browser.
// https://github.com/facebook/react/issues/10909
@ -740,7 +750,8 @@ async function buildEverything() {
for (const bundle of Bundles.bundles) {
bundles.push(
[bundle, NODE_ES2015],
[bundle, NODE_ESM],
[bundle, ESM_DEV],
[bundle, ESM_PROD],
[bundle, UMD_DEV],
[bundle, UMD_PROD],
[bundle, UMD_PROFILING],

View File

@ -9,7 +9,8 @@ const __EXPERIMENTAL__ =
const bundleTypes = {
NODE_ES2015: 'NODE_ES2015',
NODE_ESM: 'NODE_ESM',
ESM_DEV: 'ESM_DEV',
ESM_PROD: 'ESM_PROD',
UMD_DEV: 'UMD_DEV',
UMD_PROD: 'UMD_PROD',
UMD_PROFILING: 'UMD_PROFILING',
@ -32,7 +33,8 @@ const bundleTypes = {
const {
NODE_ES2015,
NODE_ESM,
ESM_DEV,
ESM_PROD,
UMD_DEV,
UMD_PROD,
UMD_PROFILING,
@ -393,7 +395,7 @@ const bundles = [
global: 'ReactServerDOMClient',
minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: false,
externals: ['react'],
externals: ['react', 'util'],
},
{
bundleTypes: [NODE_DEV, NODE_PROD],
@ -402,7 +404,7 @@ const bundles = [
global: 'ReactServerDOMClient',
minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: false,
externals: ['react'],
externals: ['react', 'util'],
},
{
bundleTypes: [NODE_DEV, NODE_PROD],
@ -427,7 +429,7 @@ const bundles = [
/******* React Server DOM Webpack Node.js Loader *******/
{
bundleTypes: [NODE_ESM],
bundleTypes: [ESM_PROD],
moduleType: RENDERER_UTILS,
entry: 'react-server-dom-webpack/node-loader',
global: 'ReactServerWebpackNodeLoader',
@ -1025,12 +1027,14 @@ function getFilename(bundle, bundleType) {
switch (bundleType) {
case NODE_ES2015:
return `${name}.js`;
case NODE_ESM:
return `${name}.js`;
case BUN_DEV:
return `${name}.development.js`;
case BUN_PROD:
return `${name}.production.min.js`;
case ESM_DEV:
return `${name}.development.js`;
case ESM_PROD:
return `${name}.production.min.js`;
case UMD_DEV:
return `${name}.development.js`;
case UMD_PROD:

View File

@ -17,7 +17,8 @@ const {
const {
NODE_ES2015,
NODE_ESM,
ESM_DEV,
ESM_PROD,
UMD_DEV,
UMD_PROD,
UMD_PROFILING,
@ -49,7 +50,8 @@ function getBundleOutputPath(bundle, bundleType, filename, packageName) {
switch (bundleType) {
case NODE_ES2015:
return `build/node_modules/${packageName}/cjs/${filename}`;
case NODE_ESM:
case ESM_DEV:
case ESM_PROD:
return `build/node_modules/${packageName}/esm/${filename}`;
case BUN_DEV:
case BUN_PROD:

View File

@ -0,0 +1,19 @@
/**
* 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.
*/
'use strict';
module.exports = function dynamicImports() {
return {
name: 'scripts/rollup/plugins/dynamic-imports',
renderDynamicImport({targetModuleId}) {
if (targetModuleId === null) {
return {left: 'import(', right: ')'};
}
return null;
},
};
};

View File

@ -53,7 +53,7 @@ module.exports = {
IS_REACT_ACT_ENVIRONMENT: 'readonly',
},
parserOptions: {
ecmaVersion: 5,
ecmaVersion: 2020,
sourceType: 'script',
},
rules: {

View File

@ -52,7 +52,7 @@ module.exports = {
IS_REACT_ACT_ENVIRONMENT: 'readonly',
},
parserOptions: {
ecmaVersion: 2017,
ecmaVersion: 2020,
sourceType: 'module',
},
rules: {

View File

@ -6,7 +6,8 @@ const {bundleTypes, moduleTypes} = require('./bundles');
const {
NODE_ES2015,
NODE_ESM,
ESM_DEV,
ESM_PROD,
UMD_DEV,
UMD_PROD,
UMD_PROFILING,
@ -66,8 +67,20 @@ ${license}
${source}`;
},
/***************** NODE_ESM *****************/
[NODE_ESM](source, globalName, filename, moduleType) {
/***************** ESM_DEV *****************/
[ESM_DEV](source, globalName, filename, moduleType) {
return `/**
* @license React
* ${filename}
*
${license}
*/
${source}`;
},
/***************** ESM_PROD *****************/
[ESM_PROD](source, globalName, filename, moduleType) {
return `/**
* @license React
* ${filename}

View File

@ -83,6 +83,7 @@ module.exports = [
'react-server-dom-webpack/client',
'react-server-dom-webpack/client.browser',
'react-server-dom-webpack/server.browser',
'react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js', // react-server-dom-webpack/client.browser
'react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js', // react-server-dom-webpack/server.browser
'react-client/src/ReactFlightClientStream.js', // We can only type check this in streaming configurations.
'react-devtools',
@ -114,6 +115,7 @@ module.exports = [
'react-server-dom-webpack',
'react-server-dom-webpack/client.edge',
'react-server-dom-webpack/server.edge',
'react-server-dom-webpack/src/ReactFlightDOMClientEdge.js', // react-server-dom-webpack/client.edge
'react-server-dom-webpack/src/ReactFlightDOMServerEdge.js', // react-server-dom-webpack/server.edge
'react-client/src/ReactFlightClientStream.js', // We can only type check this in streaming configurations.
'react-devtools',