diff --git a/fixtures/flight/__tests__/__e2e__/smoke.test.js b/fixtures/flight/__tests__/__e2e__/smoke.test.js index 267bd109081a2..f2bcbbd111c84 100644 --- a/fixtures/flight/__tests__/__e2e__/smoke.test.js +++ b/fixtures/flight/__tests__/__e2e__/smoke.test.js @@ -16,6 +16,13 @@ test('smoke test', async ({page}) => { await expect(page.getByTestId('promise-as-a-child-test')).toHaveText( 'Promise as a child hydrates without errors: deferred text' ); + await expect(page.getByTestId('prerendered')).not.toBeAttached(); + + await expect(consoleErrors).toEqual([]); + await expect(pageErrors).toEqual([]); + + await page.goto('/prerender'); + await expect(page.getByTestId('prerendered')).toBeAttached(); await expect(consoleErrors).toEqual([]); await expect(pageErrors).toEqual([]); diff --git a/fixtures/flight/server/global.js b/fixtures/flight/server/global.js index e4ae3a62916a3..8133bf7912199 100644 --- a/fixtures/flight/server/global.js +++ b/fixtures/flight/server/global.js @@ -86,7 +86,7 @@ function request(options, body) { }); } -app.all('/', async function (req, res, next) { +async function renderApp(req, res, next) { // Proxy the request to the regional server. const proxiedHeaders = { 'X-Forwarded-Host': req.hostname, @@ -102,12 +102,14 @@ app.all('/', async function (req, res, next) { proxiedHeaders['Content-type'] = req.get('Content-type'); } + const requestsPrerender = req.path === '/prerender'; + const promiseForData = request( { host: '127.0.0.1', port: 3001, method: req.method, - path: '/', + path: requestsPrerender ? '/?prerender=1' : '/', headers: proxiedHeaders, }, req @@ -210,7 +212,10 @@ app.all('/', async function (req, res, next) { res.end(); } } -}); +} + +app.all('/', renderApp); +app.all('/prerender', renderApp); if (process.env.NODE_ENV === 'development') { app.use(express.static('public')); diff --git a/fixtures/flight/server/region.js b/fixtures/flight/server/region.js index bc4ba05ddf3b4..daf619741a1ea 100644 --- a/fixtures/flight/server/region.js +++ b/fixtures/flight/server/region.js @@ -105,8 +105,67 @@ async function renderApp(res, returnValue, formState) { pipe(res); } +async function prerenderApp(res, returnValue, formState) { + const {prerenderToNodeStream} = await import( + 'react-server-dom-webpack/static' + ); + // const m = require('../src/App.js'); + const m = await import('../src/App.js'); + + let moduleMap; + let mainCSSChunks; + if (process.env.NODE_ENV === 'development') { + // Read the module map from the HMR server in development. + moduleMap = await ( + await fetch('http://localhost:3000/react-client-manifest.json') + ).json(); + mainCSSChunks = ( + await ( + await fetch('http://localhost:3000/entrypoint-manifest.json') + ).json() + ).main.css; + } else { + // Read the module map from the static build in production. + moduleMap = JSON.parse( + await readFile( + path.resolve(__dirname, `../build/react-client-manifest.json`), + 'utf8' + ) + ); + mainCSSChunks = JSON.parse( + await readFile( + path.resolve(__dirname, `../build/entrypoint-manifest.json`), + 'utf8' + ) + ).main.css; + } + const App = m.default.default || m.default; + const root = React.createElement( + React.Fragment, + null, + // Prepend the App's tree with stylesheets required for this entrypoint. + mainCSSChunks.map(filename => + React.createElement('link', { + rel: 'stylesheet', + href: filename, + precedence: 'default', + key: filename, + }) + ), + React.createElement(App, {prerender: true}) + ); + // For client-invoked server actions we refresh the tree and return a return value. + const payload = {root, returnValue, formState}; + const {prelude} = await prerenderToNodeStream(payload, moduleMap); + prelude.pipe(res); +} + app.get('/', async function (req, res) { - await renderApp(res, null, null); + if ('prerender' in req.query) { + await prerenderApp(res, null, null); + } else { + await renderApp(res, null, null); + } }); app.post('/', bodyParser.text(), async function (req, res) { diff --git a/fixtures/flight/src/App.js b/fixtures/flight/src/App.js index 027056c515021..08987750eb210 100644 --- a/fixtures/flight/src/App.js +++ b/fixtures/flight/src/App.js @@ -23,7 +23,7 @@ const promisedText = new Promise(resolve => setTimeout(() => resolve('deferred text'), 100) ); -export default async function App() { +export default async function App({prerender}) { const res = await fetch('http://localhost:3001/todos'); const todos = await res.json(); return ( @@ -35,6 +35,11 @@ export default async function App() { + {prerender ? ( + + ) : ( + + )}

{getServerState()}

diff --git a/packages/react-server-dom-esm/npm/static.js b/packages/react-server-dom-esm/npm/static.js new file mode 100644 index 0000000000000..13a632e641179 --- /dev/null +++ b/packages/react-server-dom-esm/npm/static.js @@ -0,0 +1,6 @@ +'use strict'; + +throw new Error( + 'The React Server Writer cannot be used outside a react-server environment. ' + + 'You must configure Node.js using the `--conditions react-server` flag.' +); diff --git a/packages/react-server-dom-esm/npm/static.node.js b/packages/react-server-dom-esm/npm/static.node.js new file mode 100644 index 0000000000000..ff0b9b2a42f2f --- /dev/null +++ b/packages/react-server-dom-esm/npm/static.node.js @@ -0,0 +1,12 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-esm-server.node.production.js'); +} else { + s = require('./cjs/react-server-dom-esm-server.node.development.js'); +} + +if (s.prerenderToNodeStream) { + exports.prerenderToNodeStream = s.prerenderToNodeStream; +} diff --git a/packages/react-server-dom-esm/package.json b/packages/react-server-dom-esm/package.json index bd9e9c394962e..a1f8f17f45014 100644 --- a/packages/react-server-dom-esm/package.json +++ b/packages/react-server-dom-esm/package.json @@ -17,6 +17,8 @@ "client.node.js", "server.js", "server.node.js", + "static.js", + "static.node.js", "cjs/", "esm/" ], @@ -33,6 +35,11 @@ "default": "./server.js" }, "./server.node": "./server.node.js", + "./static": { + "react-server": "./static.node.js", + "default": "./static.js" + }, + "./static.node": "./static.node.js", "./node-loader": "./esm/react-server-dom-esm-node-loader.production.js", "./src/*": "./src/*.js", "./package.json": "./package.json" diff --git a/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js index c724d89c2b435..bb65ef4b659a7 100644 --- a/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js @@ -18,6 +18,8 @@ import type {Busboy} from 'busboy'; import type {Writable} from 'stream'; import type {Thenable} from 'shared/ReactTypes'; +import {Readable} from 'stream'; + import { createRequest, startWork, @@ -123,6 +125,80 @@ function renderToPipeableStream( }, }; } +function createFakeWritable(readable: any): Writable { + // The current host config expects a Writable so we create + // a fake writable for now to push into the Readable. + return ({ + write(chunk) { + return readable.push(chunk); + }, + end() { + readable.push(null); + }, + destroy(error) { + readable.destroy(error); + }, + }: any); +} + +type PrerenderOptions = { + environmentName?: string | (() => string), + filterStackFrame?: (url: string, functionName: string) => boolean, + onError?: (error: mixed) => void, + onPostpone?: (reason: string) => void, + identifierPrefix?: string, + temporaryReferences?: TemporaryReferenceSet, + signal?: AbortSignal, +}; + +type StaticResult = { + prelude: Readable, +}; + +function prerenderToNodeStream( + model: ReactClientValue, + moduleBasePath: ClientManifest, + options?: PrerenderOptions, +): Promise { + return new Promise((resolve, reject) => { + const onFatalError = reject; + function onAllReady() { + const readable: Readable = new Readable({ + read() { + startFlowing(request, writable); + }, + }); + const writable = createFakeWritable(readable); + resolve({prelude: readable}); + } + + const request = createRequest( + model, + moduleBasePath, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.onPostpone : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + onAllReady, + onFatalError, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort(request, (signal: any).reason); + } else { + const listener = () => { + abort(request, (signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + startWork(request); + }); +} function decodeReplyFromBusboy( busboyStream: Busboy, @@ -207,6 +283,7 @@ function decodeReply( export { renderToPipeableStream, + prerenderToNodeStream, decodeReplyFromBusboy, decodeReply, decodeAction, diff --git a/packages/react-server-dom-esm/src/server/react-flight-dom-server.node.js b/packages/react-server-dom-esm/src/server/react-flight-dom-server.node.js index d14d2b8ed362a..f24946fcae8bb 100644 --- a/packages/react-server-dom-esm/src/server/react-flight-dom-server.node.js +++ b/packages/react-server-dom-esm/src/server/react-flight-dom-server.node.js @@ -9,6 +9,7 @@ export { renderToPipeableStream, + prerenderToNodeStream, decodeReplyFromBusboy, decodeReply, decodeAction, diff --git a/packages/react-server-dom-esm/src/server/react-flight-dom-server.node.stable.js b/packages/react-server-dom-esm/src/server/react-flight-dom-server.node.stable.js new file mode 100644 index 0000000000000..d14d2b8ed362a --- /dev/null +++ b/packages/react-server-dom-esm/src/server/react-flight-dom-server.node.stable.js @@ -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. + * + * @flow + */ + +export { + renderToPipeableStream, + decodeReplyFromBusboy, + decodeReply, + decodeAction, + decodeFormState, + registerServerReference, + registerClientReference, + createTemporaryReferenceSet, +} from './ReactFlightDOMServerNode'; diff --git a/packages/react-server-dom-esm/static.js b/packages/react-server-dom-esm/static.js new file mode 100644 index 0000000000000..83d8b8a017ff2 --- /dev/null +++ b/packages/react-server-dom-esm/static.js @@ -0,0 +1,13 @@ +/** + * 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 + */ + +throw new Error( + 'The React Server cannot be used outside a react-server environment. ' + + 'You must configure Node.js using the `--conditions react-server` flag.', +); diff --git a/packages/react-server-dom-esm/static.node.js b/packages/react-server-dom-esm/static.node.js new file mode 100644 index 0000000000000..d15eddc6f9b0e --- /dev/null +++ b/packages/react-server-dom-esm/static.node.js @@ -0,0 +1,10 @@ +/** + * 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 {prerenderToNodeStream} from './src/server/react-flight-dom-server.node'; diff --git a/packages/react-server-dom-turbopack/npm/static.browser.js b/packages/react-server-dom-turbopack/npm/static.browser.js new file mode 100644 index 0000000000000..edc104a459383 --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/static.browser.js @@ -0,0 +1,12 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-turbopack-server.browser.production.js'); +} else { + s = require('./cjs/react-server-dom-turbopack-server.browser.development.js'); +} + +if (s.prerender) { + exports.prerender = s.prerender; +} diff --git a/packages/react-server-dom-turbopack/npm/static.edge.js b/packages/react-server-dom-turbopack/npm/static.edge.js new file mode 100644 index 0000000000000..c074f8ffe7ee4 --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/static.edge.js @@ -0,0 +1,12 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-turbopack-server.edge.production.js'); +} else { + s = require('./cjs/react-server-dom-turbopack-server.edge.development.js'); +} + +if (s.prerender) { + exports.prerender = s.prerender; +} diff --git a/packages/react-server-dom-turbopack/npm/static.js b/packages/react-server-dom-turbopack/npm/static.js new file mode 100644 index 0000000000000..13a632e641179 --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/static.js @@ -0,0 +1,6 @@ +'use strict'; + +throw new Error( + 'The React Server Writer cannot be used outside a react-server environment. ' + + 'You must configure Node.js using the `--conditions react-server` flag.' +); diff --git a/packages/react-server-dom-turbopack/npm/static.node.js b/packages/react-server-dom-turbopack/npm/static.node.js new file mode 100644 index 0000000000000..84083a965189b --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/static.node.js @@ -0,0 +1,12 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-turbopack-server.node.production.js'); +} else { + s = require('./cjs/react-server-dom-turbopack-server.node.development.js'); +} + +if (s.prerenderToNodeStream) { + exports.prerenderToNodeStream = s.prerenderToNodeStream; +} diff --git a/packages/react-server-dom-turbopack/npm/static.node.unbundled.js b/packages/react-server-dom-turbopack/npm/static.node.unbundled.js new file mode 100644 index 0000000000000..e77863bf36a60 --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/static.node.unbundled.js @@ -0,0 +1,12 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-turbopack-server.node.unbundled.production.js'); +} else { + s = require('./cjs/react-server-dom-turbopack-server.node.unbundled.development.js'); +} + +if (s.prerenderToNodeStream) { + exports.prerenderToNodeStream = s.prerenderToNodeStream; +} diff --git a/packages/react-server-dom-turbopack/package.json b/packages/react-server-dom-turbopack/package.json index 93e694b3a3e1b..93cd7d37a04ae 100644 --- a/packages/react-server-dom-turbopack/package.json +++ b/packages/react-server-dom-turbopack/package.json @@ -22,6 +22,11 @@ "server.edge.js", "server.node.js", "server.node.unbundled.js", + "static.js", + "static.browser.js", + "static.edge.js", + "static.node.js", + "static.node.unbundled.js", "node-register.js", "cjs/", "esm/" @@ -63,6 +68,24 @@ "./server.edge": "./server.edge.js", "./server.node": "./server.node.js", "./server.node.unbundled": "./server.node.unbundled.js", + "./static": { + "react-server": { + "workerd": "./static.edge.js", + "deno": "./static.browser.js", + "node": { + "turbopack": "./static.node.js", + "webpack": "./static.node.js", + "default": "./static.node.unbundled.js" + }, + "edge-light": "./static.edge.js", + "browser": "./static.browser.js" + }, + "default": "./static.js" + }, + "./static.browser": "./static.browser.js", + "./static.edge": "./static.edge.js", + "./static.node": "./static.node.js", + "./static.node.unbundled": "./static.node.unbundled.js", "./node-loader": "./esm/react-server-dom-turbopack-node-loader.production.js", "./node-register": "./node-register.js", "./src/*": "./src/*.js", diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js index 58a87992d6c6e..4e5d5171efb19 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js @@ -100,6 +100,64 @@ function renderToReadableStream( return stream; } +type StaticResult = { + prelude: ReadableStream, +}; + +function prerender( + model: ReactClientValue, + turbopackMap: ClientManifest, + options?: Options, +): Promise { + return new Promise((resolve, reject) => { + const onFatalError = reject; + function onAllReady() { + const stream = new ReadableStream( + { + type: 'bytes', + start: (controller): ?Promise => { + startWork(request); + }, + pull: (controller): ?Promise => { + startFlowing(request, controller); + }, + cancel: (reason): ?Promise => { + stopFlowing(request); + abort(request, reason); + }, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + resolve({prelude: stream}); + } + const request = createRequest( + model, + turbopackMap, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.onPostpone : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + onAllReady, + onFatalError, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort(request, (signal: any).reason); + } else { + const listener = () => { + abort(request, (signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + }); +} + function decodeReply( body: string | FormData, turbopackMap: ServerManifest, @@ -121,4 +179,10 @@ function decodeReply( return root; } -export {renderToReadableStream, decodeReply, decodeAction, decodeFormState}; +export { + renderToReadableStream, + prerender, + decodeReply, + decodeAction, + decodeFormState, +}; diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js index 58a87992d6c6e..4e5d5171efb19 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js @@ -100,6 +100,64 @@ function renderToReadableStream( return stream; } +type StaticResult = { + prelude: ReadableStream, +}; + +function prerender( + model: ReactClientValue, + turbopackMap: ClientManifest, + options?: Options, +): Promise { + return new Promise((resolve, reject) => { + const onFatalError = reject; + function onAllReady() { + const stream = new ReadableStream( + { + type: 'bytes', + start: (controller): ?Promise => { + startWork(request); + }, + pull: (controller): ?Promise => { + startFlowing(request, controller); + }, + cancel: (reason): ?Promise => { + stopFlowing(request); + abort(request, reason); + }, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + resolve({prelude: stream}); + } + const request = createRequest( + model, + turbopackMap, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.onPostpone : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + onAllReady, + onFatalError, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort(request, (signal: any).reason); + } else { + const listener = () => { + abort(request, (signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + }); +} + function decodeReply( body: string | FormData, turbopackMap: ServerManifest, @@ -121,4 +179,10 @@ function decodeReply( return root; } -export {renderToReadableStream, decodeReply, decodeAction, decodeFormState}; +export { + renderToReadableStream, + prerender, + decodeReply, + decodeAction, + decodeFormState, +}; diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js index d76bcb5759b0e..e484d4b7e77d5 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js @@ -18,6 +18,8 @@ import type {Busboy} from 'busboy'; import type {Writable} from 'stream'; import type {Thenable} from 'shared/ReactTypes'; +import {Readable} from 'stream'; + import { createRequest, startWork, @@ -125,6 +127,81 @@ function renderToPipeableStream( }; } +function createFakeWritable(readable: any): Writable { + // The current host config expects a Writable so we create + // a fake writable for now to push into the Readable. + return ({ + write(chunk) { + return readable.push(chunk); + }, + end() { + readable.push(null); + }, + destroy(error) { + readable.destroy(error); + }, + }: any); +} + +type PrerenderOptions = { + environmentName?: string | (() => string), + filterStackFrame?: (url: string, functionName: string) => boolean, + onError?: (error: mixed) => void, + onPostpone?: (reason: string) => void, + identifierPrefix?: string, + temporaryReferences?: TemporaryReferenceSet, + signal?: AbortSignal, +}; + +type StaticResult = { + prelude: Readable, +}; + +function prerenderToNodeStream( + model: ReactClientValue, + turbopackMap: ClientManifest, + options?: PrerenderOptions, +): Promise { + return new Promise((resolve, reject) => { + const onFatalError = reject; + function onAllReady() { + const readable: Readable = new Readable({ + read() { + startFlowing(request, writable); + }, + }); + const writable = createFakeWritable(readable); + resolve({prelude: readable}); + } + + const request = createRequest( + model, + turbopackMap, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.onPostpone : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + onAllReady, + onFatalError, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort(request, (signal: any).reason); + } else { + const listener = () => { + abort(request, (signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + startWork(request); + }); +} + function decodeReplyFromBusboy( busboyStream: Busboy, turbopackMap: ServerManifest, @@ -208,6 +285,7 @@ function decodeReply( export { renderToPipeableStream, + prerenderToNodeStream, decodeReplyFromBusboy, decodeReply, decodeAction, diff --git a/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.browser.js b/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.browser.js index 0100b65554aec..d8373ec551bc0 100644 --- a/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.browser.js +++ b/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.browser.js @@ -9,6 +9,7 @@ export { renderToReadableStream, + prerender, decodeReply, decodeAction, decodeFormState, diff --git a/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.browser.stable.js b/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.browser.stable.js new file mode 100644 index 0000000000000..0100b65554aec --- /dev/null +++ b/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.browser.stable.js @@ -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. + * + * @flow + */ + +export { + renderToReadableStream, + decodeReply, + decodeAction, + decodeFormState, + registerServerReference, + registerClientReference, + createClientModuleProxy, + createTemporaryReferenceSet, +} from './ReactFlightDOMServerBrowser'; diff --git a/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.edge.js b/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.edge.js index eb887b73a8ae8..9521ba6b68841 100644 --- a/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.edge.js +++ b/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.edge.js @@ -9,6 +9,7 @@ export { renderToReadableStream, + prerender, decodeReply, decodeAction, decodeFormState, diff --git a/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.edge.stable.js b/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.edge.stable.js new file mode 100644 index 0000000000000..eb887b73a8ae8 --- /dev/null +++ b/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.edge.stable.js @@ -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. + * + * @flow + */ + +export { + renderToReadableStream, + decodeReply, + decodeAction, + decodeFormState, + registerServerReference, + registerClientReference, + createClientModuleProxy, + createTemporaryReferenceSet, +} from './ReactFlightDOMServerEdge'; diff --git a/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.js b/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.js index 0d159704067ea..badc2ed50b691 100644 --- a/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.js +++ b/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.js @@ -9,6 +9,7 @@ export { renderToPipeableStream, + prerenderToNodeStream, decodeReplyFromBusboy, decodeReply, decodeAction, diff --git a/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.stable.js b/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.stable.js new file mode 100644 index 0000000000000..0d159704067ea --- /dev/null +++ b/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.stable.js @@ -0,0 +1,20 @@ +/** + * 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 { + renderToPipeableStream, + decodeReplyFromBusboy, + decodeReply, + decodeAction, + decodeFormState, + registerServerReference, + registerClientReference, + createClientModuleProxy, + createTemporaryReferenceSet, +} from './ReactFlightDOMServerNode'; diff --git a/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.unbundled.js b/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.unbundled.js index 0d159704067ea..badc2ed50b691 100644 --- a/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.unbundled.js +++ b/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.unbundled.js @@ -9,6 +9,7 @@ export { renderToPipeableStream, + prerenderToNodeStream, decodeReplyFromBusboy, decodeReply, decodeAction, diff --git a/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.unbundled.stable.js b/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.unbundled.stable.js new file mode 100644 index 0000000000000..0d159704067ea --- /dev/null +++ b/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.unbundled.stable.js @@ -0,0 +1,20 @@ +/** + * 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 { + renderToPipeableStream, + decodeReplyFromBusboy, + decodeReply, + decodeAction, + decodeFormState, + registerServerReference, + registerClientReference, + createClientModuleProxy, + createTemporaryReferenceSet, +} from './ReactFlightDOMServerNode'; diff --git a/packages/react-server-dom-turbopack/static.browser.js b/packages/react-server-dom-turbopack/static.browser.js new file mode 100644 index 0000000000000..2589789163206 --- /dev/null +++ b/packages/react-server-dom-turbopack/static.browser.js @@ -0,0 +1,10 @@ +/** + * 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 {prerender} from './src/server/react-flight-dom-server.browser'; diff --git a/packages/react-server-dom-turbopack/static.edge.js b/packages/react-server-dom-turbopack/static.edge.js new file mode 100644 index 0000000000000..a39d54c73f579 --- /dev/null +++ b/packages/react-server-dom-turbopack/static.edge.js @@ -0,0 +1,10 @@ +/** + * 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 {prerender} from './src/server/react-flight-dom-server.edge'; diff --git a/packages/react-server-dom-turbopack/static.js b/packages/react-server-dom-turbopack/static.js new file mode 100644 index 0000000000000..83d8b8a017ff2 --- /dev/null +++ b/packages/react-server-dom-turbopack/static.js @@ -0,0 +1,13 @@ +/** + * 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 + */ + +throw new Error( + 'The React Server cannot be used outside a react-server environment. ' + + 'You must configure Node.js using the `--conditions react-server` flag.', +); diff --git a/packages/react-server-dom-turbopack/static.node.js b/packages/react-server-dom-turbopack/static.node.js new file mode 100644 index 0000000000000..d15eddc6f9b0e --- /dev/null +++ b/packages/react-server-dom-turbopack/static.node.js @@ -0,0 +1,10 @@ +/** + * 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 {prerenderToNodeStream} from './src/server/react-flight-dom-server.node'; diff --git a/packages/react-server-dom-turbopack/static.node.unbundled.js b/packages/react-server-dom-turbopack/static.node.unbundled.js new file mode 100644 index 0000000000000..b2134459afc7a --- /dev/null +++ b/packages/react-server-dom-turbopack/static.node.unbundled.js @@ -0,0 +1,10 @@ +/** + * 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 {prerenderToNodeStream} from './src/server/react-flight-dom-server.node.unbundled'; diff --git a/packages/react-server-dom-webpack/npm/static.browser.js b/packages/react-server-dom-webpack/npm/static.browser.js new file mode 100644 index 0000000000000..7d514abd6bf71 --- /dev/null +++ b/packages/react-server-dom-webpack/npm/static.browser.js @@ -0,0 +1,12 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-webpack-server.browser.production.js'); +} else { + s = require('./cjs/react-server-dom-webpack-server.browser.development.js'); +} + +if (s.prerender) { + exports.prerender = s.prerender; +} diff --git a/packages/react-server-dom-webpack/npm/static.edge.js b/packages/react-server-dom-webpack/npm/static.edge.js new file mode 100644 index 0000000000000..a4ae48f55eb1b --- /dev/null +++ b/packages/react-server-dom-webpack/npm/static.edge.js @@ -0,0 +1,12 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-webpack-server.edge.production.js'); +} else { + s = require('./cjs/react-server-dom-webpack-server.edge.development.js'); +} + +if (s.prerender) { + exports.prerender = s.prerender; +} diff --git a/packages/react-server-dom-webpack/npm/static.js b/packages/react-server-dom-webpack/npm/static.js new file mode 100644 index 0000000000000..13a632e641179 --- /dev/null +++ b/packages/react-server-dom-webpack/npm/static.js @@ -0,0 +1,6 @@ +'use strict'; + +throw new Error( + 'The React Server Writer cannot be used outside a react-server environment. ' + + 'You must configure Node.js using the `--conditions react-server` flag.' +); diff --git a/packages/react-server-dom-webpack/npm/static.node.js b/packages/react-server-dom-webpack/npm/static.node.js new file mode 100644 index 0000000000000..dbc4179d3e788 --- /dev/null +++ b/packages/react-server-dom-webpack/npm/static.node.js @@ -0,0 +1,12 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-webpack-server.node.production.js'); +} else { + s = require('./cjs/react-server-dom-webpack-server.node.development.js'); +} + +if (s.prerenderToNodeStream) { + exports.prerenderToNodeStream = s.prerenderToNodeStream; +} diff --git a/packages/react-server-dom-webpack/npm/static.node.unbundled.js b/packages/react-server-dom-webpack/npm/static.node.unbundled.js new file mode 100644 index 0000000000000..73c8a3b86e9c7 --- /dev/null +++ b/packages/react-server-dom-webpack/npm/static.node.unbundled.js @@ -0,0 +1,12 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-webpack-server.node.unbundled.production.js'); +} else { + s = require('./cjs/react-server-dom-webpack-server.node.unbundled.development.js'); +} + +if (s.prerenderToNodeStream) { + exports.prerenderToNodeStream = s.prerenderToNodeStream; +} diff --git a/packages/react-server-dom-webpack/package.json b/packages/react-server-dom-webpack/package.json index b8e2ccf92e3e3..7a1fe29d4d4a9 100644 --- a/packages/react-server-dom-webpack/package.json +++ b/packages/react-server-dom-webpack/package.json @@ -23,6 +23,11 @@ "server.edge.js", "server.node.js", "server.node.unbundled.js", + "static.js", + "static.browser.js", + "static.edge.js", + "static.node.js", + "static.node.unbundled.js", "node-register.js", "cjs/", "esm/" @@ -63,6 +68,23 @@ "./server.edge": "./server.edge.js", "./server.node": "./server.node.js", "./server.node.unbundled": "./server.node.unbundled.js", + "./static": { + "react-server": { + "workerd": "./static.edge.js", + "deno": "./static.browser.js", + "node": { + "webpack": "./static.node.js", + "default": "./static.node.unbundled.js" + }, + "edge-light": "./static.edge.js", + "browser": "./static.browser.js" + }, + "default": "./static.js" + }, + "./static.browser": "./static.browser.js", + "./static.edge": "./static.edge.js", + "./static.node": "./static.node.js", + "./static.node.unbundled": "./static.node.unbundled.js", "./node-loader": "./esm/react-server-dom-webpack-node-loader.production.js", "./node-register": "./node-register.js", "./src/*": "./src/*.js", diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index 718ddf6c5716c..faaf8aef01b0d 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -10,6 +10,7 @@ 'use strict'; import {patchSetImmediate} from '../../../../scripts/jest/patchSetImmediate'; +import {Readable} from 'stream'; // Polyfills for test environment global.ReadableStream = @@ -28,6 +29,7 @@ let React; let FlightReactDOM; let ReactDOMClient; let ReactServerDOMServer; +let ReactServerDOMStaticServer; let ReactServerDOMClient; let ReactDOMFizzServer; let ReactDOMStaticServer; @@ -59,12 +61,20 @@ describe('ReactFlightDOM', () => { jest.mock('react-server-dom-webpack/server', () => require('react-server-dom-webpack/server.node.unbundled'), ); + if (__EXPERIMENTAL__) { + jest.mock('react-server-dom-webpack/static', () => + require('react-server-dom-webpack/static.node.unbundled'), + ); + } const WebpackMock = require('./utils/WebpackMock'); clientExports = WebpackMock.clientExports; clientModuleError = WebpackMock.clientModuleError; webpackMap = WebpackMock.webpackMap; ReactServerDOMServer = require('react-server-dom-webpack/server'); + if (__EXPERIMENTAL__) { + ReactServerDOMStaticServer = require('react-server-dom-webpack/static'); + } // This reset is to load modules for the SSR/Browser scope. jest.unmock('react-server-dom-webpack/server'); @@ -2650,4 +2660,66 @@ describe('ReactFlightDOM', () => {
, ); }); + + // @gate experimental + it('can prerender', async () => { + let resolveGreeting; + const greetingPromise = new Promise(resolve => { + resolveGreeting = resolve; + }); + + function App() { + return ( +
+ +
+ ); + } + + async function Greeting() { + await greetingPromise; + return 'hello world'; + } + + const {pendingResult} = await serverAct(async () => { + // destructure trick to avoid the act scope from awaiting the returned value + return { + pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream( + , + webpackMap, + ), + }; + }); + + resolveGreeting(); + const {prelude} = await pendingResult; + + const response = ReactServerDOMClient.createFromReadableStream( + Readable.toWeb(prelude), + ); + + const {writable: fizzWritable, readable: fizzReadable} = getTestStream(); + + function ClientApp() { + return use(response); + } + + const shellErrors = []; + await serverAct(async () => { + ReactDOMFizzServer.renderToPipeableStream( + React.createElement(ClientApp), + { + onShellError(error) { + shellErrors.push(error.message); + }, + }, + ).pipe(fizzWritable); + }); + + expect(shellErrors).toEqual([]); + + const container = document.createElement('div'); + await readInto(container, fizzReadable); + expect(getMeaningfulChildren(container)).toEqual(
hello world
); + }); }); diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js index 6a6f2936f846b..daf05b3283229 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js @@ -100,6 +100,64 @@ function renderToReadableStream( return stream; } +type StaticResult = { + prelude: ReadableStream, +}; + +function prerender( + model: ReactClientValue, + webpackMap: ClientManifest, + options?: Options, +): Promise { + return new Promise((resolve, reject) => { + const onFatalError = reject; + function onAllReady() { + const stream = new ReadableStream( + { + type: 'bytes', + start: (controller): ?Promise => { + startWork(request); + }, + pull: (controller): ?Promise => { + startFlowing(request, controller); + }, + cancel: (reason): ?Promise => { + stopFlowing(request); + abort(request, reason); + }, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + resolve({prelude: stream}); + } + const request = createRequest( + model, + webpackMap, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.onPostpone : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + onAllReady, + onFatalError, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort(request, (signal: any).reason); + } else { + const listener = () => { + abort(request, (signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + }); +} + function decodeReply( body: string | FormData, webpackMap: ServerManifest, @@ -121,4 +179,10 @@ function decodeReply( return root; } -export {renderToReadableStream, decodeReply, decodeAction, decodeFormState}; +export { + renderToReadableStream, + prerender, + decodeReply, + decodeAction, + decodeFormState, +}; diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js index 6a6f2936f846b..daf05b3283229 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js @@ -100,6 +100,64 @@ function renderToReadableStream( return stream; } +type StaticResult = { + prelude: ReadableStream, +}; + +function prerender( + model: ReactClientValue, + webpackMap: ClientManifest, + options?: Options, +): Promise { + return new Promise((resolve, reject) => { + const onFatalError = reject; + function onAllReady() { + const stream = new ReadableStream( + { + type: 'bytes', + start: (controller): ?Promise => { + startWork(request); + }, + pull: (controller): ?Promise => { + startFlowing(request, controller); + }, + cancel: (reason): ?Promise => { + stopFlowing(request); + abort(request, reason); + }, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + resolve({prelude: stream}); + } + const request = createRequest( + model, + webpackMap, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.onPostpone : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + onAllReady, + onFatalError, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort(request, (signal: any).reason); + } else { + const listener = () => { + abort(request, (signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + }); +} + function decodeReply( body: string | FormData, webpackMap: ServerManifest, @@ -121,4 +179,10 @@ function decodeReply( return root; } -export {renderToReadableStream, decodeReply, decodeAction, decodeFormState}; +export { + renderToReadableStream, + prerender, + decodeReply, + decodeAction, + decodeFormState, +}; diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js index 73479bdf3ef04..1506259476703 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js @@ -18,6 +18,8 @@ import type {Busboy} from 'busboy'; import type {Writable} from 'stream'; import type {Thenable} from 'shared/ReactTypes'; +import {Readable} from 'stream'; + import { createRequest, startWork, @@ -125,6 +127,81 @@ function renderToPipeableStream( }; } +function createFakeWritable(readable: any): Writable { + // The current host config expects a Writable so we create + // a fake writable for now to push into the Readable. + return ({ + write(chunk) { + return readable.push(chunk); + }, + end() { + readable.push(null); + }, + destroy(error) { + readable.destroy(error); + }, + }: any); +} + +type PrerenderOptions = { + environmentName?: string | (() => string), + filterStackFrame?: (url: string, functionName: string) => boolean, + onError?: (error: mixed) => void, + onPostpone?: (reason: string) => void, + identifierPrefix?: string, + temporaryReferences?: TemporaryReferenceSet, + signal?: AbortSignal, +}; + +type StaticResult = { + prelude: Readable, +}; + +function prerenderToNodeStream( + model: ReactClientValue, + webpackMap: ClientManifest, + options?: PrerenderOptions, +): Promise { + return new Promise((resolve, reject) => { + const onFatalError = reject; + function onAllReady() { + const readable: Readable = new Readable({ + read() { + startFlowing(request, writable); + }, + }); + const writable = createFakeWritable(readable); + resolve({prelude: readable}); + } + + const request = createRequest( + model, + webpackMap, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.onPostpone : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + onAllReady, + onFatalError, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort(request, (signal: any).reason); + } else { + const listener = () => { + abort(request, (signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + startWork(request); + }); +} + function decodeReplyFromBusboy( busboyStream: Busboy, webpackMap: ServerManifest, @@ -208,6 +285,7 @@ function decodeReply( export { renderToPipeableStream, + prerenderToNodeStream, decodeReplyFromBusboy, decodeReply, decodeAction, diff --git a/packages/react-server-dom-webpack/src/server/react-flight-dom-server.browser.js b/packages/react-server-dom-webpack/src/server/react-flight-dom-server.browser.js index 0100b65554aec..d8373ec551bc0 100644 --- a/packages/react-server-dom-webpack/src/server/react-flight-dom-server.browser.js +++ b/packages/react-server-dom-webpack/src/server/react-flight-dom-server.browser.js @@ -9,6 +9,7 @@ export { renderToReadableStream, + prerender, decodeReply, decodeAction, decodeFormState, diff --git a/packages/react-server-dom-webpack/src/server/react-flight-dom-server.browser.stable.js b/packages/react-server-dom-webpack/src/server/react-flight-dom-server.browser.stable.js new file mode 100644 index 0000000000000..0100b65554aec --- /dev/null +++ b/packages/react-server-dom-webpack/src/server/react-flight-dom-server.browser.stable.js @@ -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. + * + * @flow + */ + +export { + renderToReadableStream, + decodeReply, + decodeAction, + decodeFormState, + registerServerReference, + registerClientReference, + createClientModuleProxy, + createTemporaryReferenceSet, +} from './ReactFlightDOMServerBrowser'; diff --git a/packages/react-server-dom-webpack/src/server/react-flight-dom-server.edge.js b/packages/react-server-dom-webpack/src/server/react-flight-dom-server.edge.js index eb887b73a8ae8..9521ba6b68841 100644 --- a/packages/react-server-dom-webpack/src/server/react-flight-dom-server.edge.js +++ b/packages/react-server-dom-webpack/src/server/react-flight-dom-server.edge.js @@ -9,6 +9,7 @@ export { renderToReadableStream, + prerender, decodeReply, decodeAction, decodeFormState, diff --git a/packages/react-server-dom-webpack/src/server/react-flight-dom-server.edge.stable.js b/packages/react-server-dom-webpack/src/server/react-flight-dom-server.edge.stable.js new file mode 100644 index 0000000000000..eb887b73a8ae8 --- /dev/null +++ b/packages/react-server-dom-webpack/src/server/react-flight-dom-server.edge.stable.js @@ -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. + * + * @flow + */ + +export { + renderToReadableStream, + decodeReply, + decodeAction, + decodeFormState, + registerServerReference, + registerClientReference, + createClientModuleProxy, + createTemporaryReferenceSet, +} from './ReactFlightDOMServerEdge'; diff --git a/packages/react-server-dom-webpack/src/server/react-flight-dom-server.node.js b/packages/react-server-dom-webpack/src/server/react-flight-dom-server.node.js index 0d159704067ea..badc2ed50b691 100644 --- a/packages/react-server-dom-webpack/src/server/react-flight-dom-server.node.js +++ b/packages/react-server-dom-webpack/src/server/react-flight-dom-server.node.js @@ -9,6 +9,7 @@ export { renderToPipeableStream, + prerenderToNodeStream, decodeReplyFromBusboy, decodeReply, decodeAction, diff --git a/packages/react-server-dom-webpack/src/server/react-flight-dom-server.node.stable.js b/packages/react-server-dom-webpack/src/server/react-flight-dom-server.node.stable.js new file mode 100644 index 0000000000000..0d159704067ea --- /dev/null +++ b/packages/react-server-dom-webpack/src/server/react-flight-dom-server.node.stable.js @@ -0,0 +1,20 @@ +/** + * 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 { + renderToPipeableStream, + decodeReplyFromBusboy, + decodeReply, + decodeAction, + decodeFormState, + registerServerReference, + registerClientReference, + createClientModuleProxy, + createTemporaryReferenceSet, +} from './ReactFlightDOMServerNode'; diff --git a/packages/react-server-dom-webpack/src/server/react-flight-dom-server.node.unbundled.js b/packages/react-server-dom-webpack/src/server/react-flight-dom-server.node.unbundled.js index 0d159704067ea..badc2ed50b691 100644 --- a/packages/react-server-dom-webpack/src/server/react-flight-dom-server.node.unbundled.js +++ b/packages/react-server-dom-webpack/src/server/react-flight-dom-server.node.unbundled.js @@ -9,6 +9,7 @@ export { renderToPipeableStream, + prerenderToNodeStream, decodeReplyFromBusboy, decodeReply, decodeAction, diff --git a/packages/react-server-dom-webpack/src/server/react-flight-dom-server.node.unbundled.stable.js b/packages/react-server-dom-webpack/src/server/react-flight-dom-server.node.unbundled.stable.js new file mode 100644 index 0000000000000..0d159704067ea --- /dev/null +++ b/packages/react-server-dom-webpack/src/server/react-flight-dom-server.node.unbundled.stable.js @@ -0,0 +1,20 @@ +/** + * 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 { + renderToPipeableStream, + decodeReplyFromBusboy, + decodeReply, + decodeAction, + decodeFormState, + registerServerReference, + registerClientReference, + createClientModuleProxy, + createTemporaryReferenceSet, +} from './ReactFlightDOMServerNode'; diff --git a/packages/react-server-dom-webpack/static.browser.js b/packages/react-server-dom-webpack/static.browser.js new file mode 100644 index 0000000000000..2589789163206 --- /dev/null +++ b/packages/react-server-dom-webpack/static.browser.js @@ -0,0 +1,10 @@ +/** + * 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 {prerender} from './src/server/react-flight-dom-server.browser'; diff --git a/packages/react-server-dom-webpack/static.edge.js b/packages/react-server-dom-webpack/static.edge.js new file mode 100644 index 0000000000000..a39d54c73f579 --- /dev/null +++ b/packages/react-server-dom-webpack/static.edge.js @@ -0,0 +1,10 @@ +/** + * 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 {prerender} from './src/server/react-flight-dom-server.edge'; diff --git a/packages/react-server-dom-webpack/static.js b/packages/react-server-dom-webpack/static.js new file mode 100644 index 0000000000000..83d8b8a017ff2 --- /dev/null +++ b/packages/react-server-dom-webpack/static.js @@ -0,0 +1,13 @@ +/** + * 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 + */ + +throw new Error( + 'The React Server cannot be used outside a react-server environment. ' + + 'You must configure Node.js using the `--conditions react-server` flag.', +); diff --git a/packages/react-server-dom-webpack/static.node.js b/packages/react-server-dom-webpack/static.node.js new file mode 100644 index 0000000000000..d15eddc6f9b0e --- /dev/null +++ b/packages/react-server-dom-webpack/static.node.js @@ -0,0 +1,10 @@ +/** + * 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 {prerenderToNodeStream} from './src/server/react-flight-dom-server.node'; diff --git a/packages/react-server-dom-webpack/static.node.unbundled.js b/packages/react-server-dom-webpack/static.node.unbundled.js new file mode 100644 index 0000000000000..b2134459afc7a --- /dev/null +++ b/packages/react-server-dom-webpack/static.node.unbundled.js @@ -0,0 +1,10 @@ +/** + * 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 {prerenderToNodeStream} from './src/server/react-flight-dom-server.node.unbundled'; diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 6c9536d95acf7..5920cd0a2fbaf 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -376,6 +376,8 @@ export type Request = { taintCleanupQueue: Array, onError: (error: mixed) => ?string, onPostpone: (reason: string) => void, + onAllReady: () => void, + onFatalError: mixed => void, // DEV-only environmentName: () => string, filterStackFrame: (url: string, functionName: string) => boolean, @@ -435,6 +437,8 @@ function RequestInstance( temporaryReferences: void | TemporaryReferenceSet, environmentName: void | string | (() => string), // DEV-only filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only + onAllReady: void | (() => void), + onFatalError: void | ((error: mixed) => void), ) { if ( ReactSharedInternals.A !== null && @@ -486,6 +490,8 @@ function RequestInstance( this.onError = onError === undefined ? defaultErrorHandler : onError; this.onPostpone = onPostpone === undefined ? defaultPostponeHandler : onPostpone; + this.onAllReady = onAllReady === undefined ? noop : onAllReady; + this.onFatalError = onFatalError === undefined ? noop : onFatalError; if (__DEV__) { this.environmentName = @@ -513,6 +519,8 @@ function RequestInstance( pingedTasks.push(rootTask); } +function noop(): void {} + export function createRequest( model: ReactClientValue, bundlerConfig: ClientManifest, @@ -522,6 +530,8 @@ export function createRequest( temporaryReferences: void | TemporaryReferenceSet, environmentName: void | string | (() => string), // DEV-only filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only + onAllReady: void | (() => void), + onFatalError: void | (() => void), ): Request { // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors return new RequestInstance( @@ -533,6 +543,8 @@ export function createRequest( temporaryReferences, environmentName, filterStackFrame, + onAllReady, + onFatalError, ); } @@ -2886,6 +2898,8 @@ function logRecoverableError( } function fatalError(request: Request, error: mixed): void { + const onFatalError = request.onFatalError; + onFatalError(error); if (enableTaint) { cleanupTaintQueue(request); } @@ -3752,6 +3766,11 @@ function performWork(request: Request): void { logRecoverableError(request, error, null); fatalError(request, error); } finally { + if (request.abortableTasks.size === 0) { + // we're done rendering + const onAllReady = request.onAllReady; + onAllReady(); + } ReactSharedInternals.H = prevDispatcher; resetHooksForRequest(); currentRequest = prevRequest; diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index 4a07e036530a2..be5706c927c7c 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -43,6 +43,8 @@ module.exports = [ 'react-server-dom-webpack/client.node.unbundled', 'react-server-dom-webpack/server', 'react-server-dom-webpack/server.node.unbundled', + 'react-server-dom-webpack/static', + 'react-server-dom-webpack/static.node.unbundled', 'react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js', // react-server-dom-webpack/client.node 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerNode.js', 'react-server-dom-webpack/src/server/react-flight-dom-server.node.unbundled', @@ -82,6 +84,8 @@ module.exports = [ 'react-server-dom-webpack/client.node', 'react-server-dom-webpack/server', 'react-server-dom-webpack/server.node', + 'react-server-dom-webpack/static', + 'react-server-dom-webpack/static.node', 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpack.js', 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackServer.js', 'react-server-dom-webpack/src/server/react-flight-dom-server.node', @@ -123,6 +127,8 @@ module.exports = [ 'react-server-dom-turbopack/client.node.unbundled', 'react-server-dom-turbopack/server', 'react-server-dom-turbopack/server.node.unbundled', + 'react-server-dom-turbopack/static', + 'react-server-dom-turbopack/static.node.unbundled', 'react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js', // react-server-dom-turbopack/client.node.unbundled 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerNode.js', 'react-server-dom-turbopack/src/server/react-flight-dom-server.node.unbundled', @@ -164,6 +170,8 @@ module.exports = [ 'react-server-dom-turbopack/client.node', 'react-server-dom-turbopack/server', 'react-server-dom-turbopack/server.node', + 'react-server-dom-turbopack/static', + 'react-server-dom-turbopack/static.node', 'react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js', // react-server-dom-turbopack/client.node 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopack.js', 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopackServer.js', @@ -238,6 +246,7 @@ module.exports = [ 'react-server-dom-webpack/client', 'react-server-dom-webpack/client.browser', 'react-server-dom-webpack/server.browser', + 'react-server-dom-webpack/static.browser', 'react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js', // react-server-dom-webpack/client.browser 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpack.js', 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackBrowser.js', @@ -299,6 +308,7 @@ module.exports = [ 'react-server-dom-turbopack/client', 'react-server-dom-turbopack/client.browser', 'react-server-dom-turbopack/server.browser', + 'react-server-dom-turbopack/static.browser', 'react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js', // react-server-dom-turbopack/client.browser 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopack.js', 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopackBrowser.js', @@ -339,6 +349,7 @@ module.exports = [ 'react-server-dom-webpack', 'react-server-dom-webpack/client.edge', 'react-server-dom-webpack/server.edge', + 'react-server-dom-webpack/static.edge', 'react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js', // react-server-dom-webpack/client.edge 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpack.js', 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackServer.js', @@ -378,6 +389,7 @@ module.exports = [ 'react-server-dom-turbopack', 'react-server-dom-turbopack/client.edge', 'react-server-dom-turbopack/server.edge', + 'react-server-dom-turbopack/static.edge', 'react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js', // react-server-dom-turbopack/client.edge 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopack.js', 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopackServer.js', @@ -419,6 +431,8 @@ module.exports = [ 'react-server-dom-esm/client.node', 'react-server-dom-esm/server', 'react-server-dom-esm/server.node', + 'react-server-dom-esm/static', + 'react-server-dom-esm/static.node', 'react-server-dom-esm/src/client/ReactFlightDOMClientNode.js', // react-server-dom-esm/client.node 'react-server-dom-esm/src/server/react-flight-dom-server.node', 'react-server-dom-esm/src/server/ReactFlightDOMServerNode.js', // react-server-dom-esm/src/server/react-flight-dom-server.node