From cdb8a1d19d0c0d43a72c3f0fe739b04da247c360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 19 Oct 2021 22:36:10 -0400 Subject: [PATCH] [Fizz] Add option to inject bootstrapping script tags after the shell is injected (#22594) * Add option to inject bootstrap scripts These are emitted right after the shell as flushed. * Update ssr fixtures to use bootstrapScripts instead of manual script tag * Add option to FB renderer too --- fixtures/ssr/server/render.js | 1 + fixtures/ssr/src/components/Chrome.js | 1 - fixtures/ssr/yarn.lock | 10 +- fixtures/ssr2/package.json | 4 +- fixtures/ssr2/server/render.js | 1 + fixtures/ssr2/src/Html.js | 1 - fixtures/ssr2/yarn.lock | 5277 +++++++++++++++++ .../src/__tests__/ReactDOMFizzServer-test.js | 31 +- .../ReactDOMFizzServerBrowser-test.js | 16 + .../__tests__/ReactDOMFizzServerNode-test.js | 18 + .../src/server/ReactDOMFizzServerBrowser.js | 6 + .../src/server/ReactDOMFizzServerNode.js | 6 + .../src/server/ReactDOMServerFormatConfig.js | 48 + .../ReactDOMServerLegacyFormatConfig.js | 3 + .../server/ReactNativeServerFormatConfig.js | 7 + .../src/ReactNoopServer.js | 7 + .../src/ReactDOMServerFB.js | 11 +- .../ReactDOMServerFB-test.internal.js | 15 + packages/react-server/src/ReactFizzServer.js | 2 + .../forks/ReactServerFormatConfig.custom.js | 1 + 20 files changed, 5445 insertions(+), 21 deletions(-) create mode 100644 fixtures/ssr2/yarn.lock 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;