diff --git a/fixtures/ssr/server/render.js b/fixtures/ssr/server/render.js index e0fc50f3f9538..9857a8a83dcfa 100644 --- a/fixtures/ssr/server/render.js +++ b/fixtures/ssr/server/render.js @@ -21,6 +21,7 @@ export default function render(url, res) { }); let didError = false; const {pipe, abort} = renderToPipeableStream(, { + bootstrapScripts: [assets['main.js']], onCompleteShell() { // If something errored before we started streaming, we set the error code appropriately. res.statusCode = didError ? 500 : 200; diff --git a/fixtures/ssr/src/components/Chrome.js b/fixtures/ssr/src/components/Chrome.js index c895663710560..23056dab92a4a 100644 --- a/fixtures/ssr/src/components/Chrome.js +++ b/fixtures/ssr/src/components/Chrome.js @@ -46,7 +46,6 @@ export default class Chrome extends Component { __html: `assetManifest = ${JSON.stringify(assets)};`, }} /> - "`, + ); + }); + // @gate experimental it('emits all HTML as one unit if we wait until the end to start', async () => { let hasLoaded = false; diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js index 7fa1c208dffc9..bd0ca112a272b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js @@ -82,6 +82,24 @@ describe('ReactDOMFizzServer', () => { ); }); + // @gate experimental + it('should emit bootstrap script src at the end', () => { + const {writable, output} = getTestWritable(); + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( +
hello world
, + { + bootstrapScriptContent: 'INIT();', + bootstrapScripts: ['init.js'], + bootstrapModules: ['init.mjs'], + }, + ); + pipe(writable); + jest.runAllTimers(); + expect(output.result).toMatchInlineSnapshot( + `"
hello world
"`, + ); + }); + // @gate experimental it('should start writing after pipe', () => { const {writable, output} = getTestWritable(); diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js index 2865ef46b2574..907f0823cffe9 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js @@ -27,6 +27,9 @@ type Options = {| identifierPrefix?: string, namespaceURI?: string, nonce?: string, + bootstrapScriptContent?: string, + bootstrapScripts?: Array, + bootstrapModules?: Array, progressiveChunkSize?: number, signal?: AbortSignal, onCompleteShell?: () => void, @@ -43,6 +46,9 @@ function renderToReadableStream( createResponseState( options ? options.identifierPrefix : undefined, options ? options.nonce : undefined, + options ? options.bootstrapScriptContent : undefined, + options ? options.bootstrapScripts : undefined, + options ? options.bootstrapModules : undefined, ), createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, diff --git a/packages/react-dom/src/server/ReactDOMFizzServerNode.js b/packages/react-dom/src/server/ReactDOMFizzServerNode.js index fe532a32c3b50..33a7083bb95eb 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerNode.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerNode.js @@ -32,6 +32,9 @@ type Options = {| identifierPrefix?: string, namespaceURI?: string, nonce?: string, + bootstrapScriptContent?: string, + bootstrapScripts?: Array, + bootstrapModules?: Array, progressiveChunkSize?: number, onCompleteShell?: () => void, onCompleteAll?: () => void, @@ -51,6 +54,9 @@ function createRequestImpl(children: ReactNodeList, options: void | Options) { createResponseState( options ? options.identifierPrefix : undefined, options ? options.nonce : undefined, + options ? options.bootstrapScriptContent : undefined, + options ? options.bootstrapScripts : undefined, + options ? options.bootstrapModules : undefined, ), createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js index 67d90f8513452..1f5a7a65bed59 100644 --- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js @@ -59,6 +59,7 @@ export const isPrimaryRenderer = true; // Per response, global state that is not contextual to the rendering subtree. export type ResponseState = { + bootstrapChunks: Array, startInlineScript: PrecomputedChunk, placeholderPrefix: PrecomputedChunk, segmentPrefix: PrecomputedChunk, @@ -73,11 +74,19 @@ export type ResponseState = { }; const startInlineScript = stringToPrecomputedChunk(''); + +const startScriptSrc = stringToPrecomputedChunk(''); // Allows us to keep track of what we've already written so we can refer back to it. export function createResponseState( identifierPrefix: string | void, nonce: string | void, + bootstrapScriptContent: string | void, + bootstrapScripts: Array | void, + bootstrapModules: Array | void, ): ResponseState { const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix; const inlineScriptWithNonce = @@ -86,7 +95,34 @@ export function createResponseState( : stringToPrecomputedChunk( '"`, + ); + }); + it('emits all HTML as one unit if we wait until the end to start', async () => { let hasLoaded = false; let resolve; diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 4ab2f06d518db..6692b92648643 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -36,6 +36,7 @@ import { closeWithError, } from './ReactServerStreamConfig'; import { + writeCompletedRoot, writePlaceholder, writeStartCompletedSuspenseBoundary, writeStartPendingSuspenseBoundary, @@ -1779,6 +1780,7 @@ function flushCompletedQueues( if (completedRootSegment !== null && request.pendingRootTasks === 0) { flushSegment(request, destination, completedRootSegment); request.completedRootSegment = null; + writeCompletedRoot(destination, request.responseState); } // We emit client rendering instructions for already emitted boundaries first. diff --git a/packages/react-server/src/forks/ReactServerFormatConfig.custom.js b/packages/react-server/src/forks/ReactServerFormatConfig.custom.js index d816198c64180..8cfe59ce1628c 100644 --- a/packages/react-server/src/forks/ReactServerFormatConfig.custom.js +++ b/packages/react-server/src/forks/ReactServerFormatConfig.custom.js @@ -44,6 +44,7 @@ export const pushStartCompletedSuspenseBoundary = $$$hostConfig.pushStartCompletedSuspenseBoundary; export const pushEndCompletedSuspenseBoundary = $$$hostConfig.pushEndCompletedSuspenseBoundary; +export const writeCompletedRoot = $$$hostConfig.writeCompletedRoot; export const writePlaceholder = $$$hostConfig.writePlaceholder; export const writeStartCompletedSuspenseBoundary = $$$hostConfig.writeStartCompletedSuspenseBoundary;