From d7e653bed3bcd2e71b7a3ba381dea8dd267e1f9c Mon Sep 17 00:00:00 2001 From: Josh Story Date: Mon, 1 May 2023 11:03:26 -0700 Subject: [PATCH] bootstrap scripts now preload --- .../src/server/ReactFizzConfigDOM.js | 43 +++++++++++++++++++ .../src/server/ReactFizzConfigDOMLegacy.js | 3 ++ .../src/__tests__/ReactDOMFizzServer-test.js | 37 +++++++++++++--- .../ReactDOMFizzServerBrowser-test.js | 4 +- .../__tests__/ReactDOMFizzServerNode-test.js | 2 +- .../ReactDOMFizzStaticBrowser-test.js | 2 +- .../__tests__/ReactDOMFizzStaticNode-test.js | 2 +- .../src/server/ReactDOMFizzServerBrowser.js | 4 ++ .../src/server/ReactDOMFizzServerBun.js | 4 ++ .../src/server/ReactDOMFizzServerEdge.js | 4 ++ .../src/server/ReactDOMFizzServerNode.js | 4 ++ .../src/server/ReactDOMFizzStaticBrowser.js | 4 ++ .../src/server/ReactDOMFizzStaticEdge.js | 4 ++ .../src/server/ReactDOMFizzStaticNode.js | 5 ++- .../src/server/ReactDOMLegacyServerImpl.js | 4 ++ .../server/ReactDOMLegacyServerNodeStream.js | 9 +++- .../src/ReactDOMServerFB.js | 4 ++ .../ReactDOMServerFB-test.internal.js | 2 +- packages/react-server/src/ReactFizzServer.js | 10 +++-- 19 files changed, 135 insertions(+), 16 deletions(-) diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 5314564242ed9..089e9964ce970 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -201,6 +201,7 @@ export type ExternalRuntimeScript = { // if passed externalRuntimeConfig and the enableFizzExternalRuntime feature flag // is set, the server will send instructions via data attributes (instead of inline scripts) export function createResponseState( + resources: Resources, identifierPrefix: string | void, nonce: string | void, bootstrapScriptContent: string | void, @@ -266,6 +267,8 @@ export function createResponseState( const integrity = typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity; + preloadBootstrapScript(resources, src, nonce, integrity); + bootstrapChunks.push( startScriptSrc, stringToChunk(escapeTextForBrowser(src)), @@ -5469,6 +5472,46 @@ function preinit(href: string, options: PreinitOptions): void { } } +// This function is only safe to call at Request start time since it assumes +// that each script has not already been preloaded. If we find a need to preload +// scripts at any other point in time we will need to check whether the preload +// already exists and not assume it +function preloadBootstrapScript( + resources: Resources, + src: string, + nonce: ?string, + integrity: ?string, +): void { + const key = getResourceKey('script', src); + if (__DEV__) { + if (resources.preloadsMap.has(key)) { + // This is coded as a React error because it should be impossible for a userspace preload to preempt this call + // If a userspace preload can preempt it then this assumption is broken and we need to reconsider this strategy + // rather than instruct the user to not preload their bootstrap scripts themselves + console.error( + 'Internal React Error: React expected bootstrap script with src "%s" to not have been preloaded already. please file an issue', + src, + ); + } + } + const props: PreloadProps = { + rel: 'preload', + href: src, + as: 'script', + nonce, + integrity, + }; + const resource: PreloadResource = { + type: 'preload', + chunks: [], + state: NoState, + props, + }; + resources.preloadsMap.set(key, resource); + resources.explicitScriptPreloads.add(resource); + pushLinkImpl(resource.chunks, props); +} + function internalPreinitScript( resources: Resources, src: string, diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js index 5e1be3c91b94c..d8f7094b50068 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js @@ -8,6 +8,7 @@ */ import type { + Resources, BootstrapScriptDescriptor, ExternalRuntimeScript, FormatContext, @@ -63,11 +64,13 @@ export type ResponseState = { }; export function createResponseState( + resources: Resources, generateStaticMarkup: boolean, identifierPrefix: string | void, externalRuntimeConfig: string | BootstrapScriptDescriptor | void, ): ResponseState { const responseState = createResponseStateImpl( + resources, identifierPrefix, undefined, undefined, diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 2bf48eae39b18..e16beba0513ad 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -596,14 +596,27 @@ describe('ReactDOMFizzServer', () => { { nonce: 'R4nd0m', bootstrapScriptContent: 'function noop(){}', - bootstrapScripts: ['init.js'], + bootstrapScripts: [ + 'init.js', + {src: 'init2.js', integrity: 'init2hash'}, + ], bootstrapModules: ['init.mjs'], }, ); pipe(writable); }); - expect(getVisibleChildren(container)).toEqual(
Loading...
); + expect(getVisibleChildren(container)).toEqual([ + , + , +
Loading...
, + ]); // check that there are 4 scripts with a matching nonce: // The runtime script, an inline bootstrap script, and two src scripts @@ -611,12 +624,22 @@ describe('ReactDOMFizzServer', () => { Array.from(container.getElementsByTagName('script')).filter( node => node.getAttribute('nonce') === CSPnonce, ).length, - ).toEqual(4); + ).toEqual(5); await act(() => { resolve({default: Text}); }); - expect(getVisibleChildren(container)).toEqual(
Hello
); + expect(getVisibleChildren(container)).toEqual([ + , + , +
Hello
, + ]); } finally { CSPnonce = null; } @@ -3756,7 +3779,11 @@ describe('ReactDOMFizzServer', () => { expect(getVisibleChildren(document)).toEqual( - + + + + +
hello world
diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js index c6dbae0d0c403..6571cddcc6981 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js @@ -84,7 +84,7 @@ describe('ReactDOMFizzServerBrowser', () => { ); const result = await readResult(stream); expect(result).toMatchInlineSnapshot( - `"
hello world
"`, + `"
hello world
"`, ); }); @@ -500,7 +500,7 @@ describe('ReactDOMFizzServerBrowser', () => { ); const result = await readResult(stream); expect(result).toMatchInlineSnapshot( - `"
hello world
"`, + `"
hello world
"`, ); }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js index 11c7043e2b9de..03ce2f5b8468b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js @@ -98,7 +98,7 @@ describe('ReactDOMFizzServerNode', () => { pipe(writable); jest.runAllTimers(); expect(output.result).toMatchInlineSnapshot( - `"
hello world
"`, + `"
hello world
"`, ); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js index dc15ddc58aabf..7bda83ff2cd3b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js @@ -84,7 +84,7 @@ describe('ReactDOMFizzStaticBrowser', () => { }); const prelude = await readContent(result.prelude); expect(prelude).toMatchInlineSnapshot( - `"
hello world
"`, + `"
hello world
"`, ); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStaticNode-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStaticNode-test.js index c17d08fb033a6..f121a7c289c0e 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzStaticNode-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzStaticNode-test.js @@ -86,7 +86,7 @@ describe('ReactDOMFizzStaticNode', () => { ); const prelude = await readContent(result.prelude); expect(prelude).toMatchInlineSnapshot( - `"
hello world
"`, + `"
hello world
"`, ); }); diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js index 2c4080b61234d..6bda14046fadb 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js @@ -20,6 +20,7 @@ import { } from 'react-server/src/ReactFizzServer'; import { + createResources, createResponseState, createRootFormatContext, } from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; @@ -79,9 +80,12 @@ function renderToReadableStream( allReady.catch(() => {}); reject(error); } + const resources = createResources(); const request = createRequest( children, + resources, createResponseState( + resources, options ? options.identifierPrefix : undefined, options ? options.nonce : undefined, options ? options.bootstrapScriptContent : undefined, diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBun.js b/packages/react-dom/src/server/ReactDOMFizzServerBun.js index 686c839296ff7..73dc8cfc3b0f8 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerBun.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerBun.js @@ -20,6 +20,7 @@ import { } from 'react-server/src/ReactFizzServer'; import { + createResources, createResponseState, createRootFormatContext, } from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; @@ -80,9 +81,12 @@ function renderToReadableStream( allReady.catch(() => {}); reject(error); } + const resources = createResources(); const request = createRequest( children, + resources, createResponseState( + resources, options ? options.identifierPrefix : undefined, options ? options.nonce : undefined, options ? options.bootstrapScriptContent : undefined, diff --git a/packages/react-dom/src/server/ReactDOMFizzServerEdge.js b/packages/react-dom/src/server/ReactDOMFizzServerEdge.js index 2c4080b61234d..6bda14046fadb 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerEdge.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerEdge.js @@ -20,6 +20,7 @@ import { } from 'react-server/src/ReactFizzServer'; import { + createResources, createResponseState, createRootFormatContext, } from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; @@ -79,9 +80,12 @@ function renderToReadableStream( allReady.catch(() => {}); reject(error); } + const resources = createResources(); const request = createRequest( children, + resources, createResponseState( + resources, options ? options.identifierPrefix : undefined, options ? options.nonce : undefined, options ? options.bootstrapScriptContent : undefined, diff --git a/packages/react-dom/src/server/ReactDOMFizzServerNode.js b/packages/react-dom/src/server/ReactDOMFizzServerNode.js index 566f181feda91..e1512dea07132 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerNode.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerNode.js @@ -23,6 +23,7 @@ import { } from 'react-server/src/ReactFizzServer'; import { + createResources, createResponseState, createRootFormatContext, } from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; @@ -59,9 +60,12 @@ type PipeableStream = { }; function createRequestImpl(children: ReactNodeList, options: void | Options) { + const resources = createResources(); return createRequest( children, + resources, createResponseState( + resources, options ? options.identifierPrefix : undefined, options ? options.nonce : undefined, options ? options.bootstrapScriptContent : undefined, diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js b/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js index b568e1bec4a89..5c9b637649591 100644 --- a/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js +++ b/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js @@ -20,6 +20,7 @@ import { } from 'react-server/src/ReactFizzServer'; import { + createResources, createResponseState, createRootFormatContext, } from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; @@ -64,9 +65,12 @@ function prerender( }; resolve(result); } + const resources = createResources(); const request = createRequest( children, + resources, createResponseState( + resources, options ? options.identifierPrefix : undefined, undefined, options ? options.bootstrapScriptContent : undefined, diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js b/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js index b568e1bec4a89..5c9b637649591 100644 --- a/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js +++ b/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js @@ -20,6 +20,7 @@ import { } from 'react-server/src/ReactFizzServer'; import { + createResources, createResponseState, createRootFormatContext, } from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; @@ -64,9 +65,12 @@ function prerender( }; resolve(result); } + const resources = createResources(); const request = createRequest( children, + resources, createResponseState( + resources, options ? options.identifierPrefix : undefined, undefined, options ? options.bootstrapScriptContent : undefined, diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticNode.js b/packages/react-dom/src/server/ReactDOMFizzStaticNode.js index f9291cb5648db..156b947d8d751 100644 --- a/packages/react-dom/src/server/ReactDOMFizzStaticNode.js +++ b/packages/react-dom/src/server/ReactDOMFizzStaticNode.js @@ -22,6 +22,7 @@ import { } from 'react-server/src/ReactFizzServer'; import { + createResources, createResponseState, createRootFormatContext, } from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; @@ -78,10 +79,12 @@ function prerenderToNodeStreams( }; resolve(result); } - + const resources = createResources(); const request = createRequest( children, + resources, createResponseState( + resources, options ? options.identifierPrefix : undefined, undefined, options ? options.bootstrapScriptContent : undefined, diff --git a/packages/react-dom/src/server/ReactDOMLegacyServerImpl.js b/packages/react-dom/src/server/ReactDOMLegacyServerImpl.js index fe6e8f9a54b4d..719879107be2d 100644 --- a/packages/react-dom/src/server/ReactDOMLegacyServerImpl.js +++ b/packages/react-dom/src/server/ReactDOMLegacyServerImpl.js @@ -20,6 +20,7 @@ import { } from 'react-server/src/ReactFizzServer'; import { + createResources, createResponseState, createRootFormatContext, } from 'react-dom-bindings/src/server/ReactFizzConfigDOMLegacy'; @@ -61,9 +62,12 @@ function renderToStringImpl( function onShellReady() { readyToStream = true; } + const resources = createResources(); const request = createRequest( children, + resources, createResponseState( + resources, generateStaticMarkup, options ? options.identifierPrefix : undefined, unstable_externalRuntimeSrc, diff --git a/packages/react-dom/src/server/ReactDOMLegacyServerNodeStream.js b/packages/react-dom/src/server/ReactDOMLegacyServerNodeStream.js index 8b493b0bda3ef..4dc848b2962ab 100644 --- a/packages/react-dom/src/server/ReactDOMLegacyServerNodeStream.js +++ b/packages/react-dom/src/server/ReactDOMLegacyServerNodeStream.js @@ -19,6 +19,7 @@ import { } from 'react-server/src/ReactFizzServer'; import { + createResources, createResponseState, createRootFormatContext, } from 'react-dom-bindings/src/server/ReactFizzConfigDOMLegacy'; @@ -70,9 +71,15 @@ function renderToNodeStreamImpl( startFlowing(request, destination); } const destination = new ReactMarkupReadableStream(); + const resources = createResources(); const request = createRequest( children, - createResponseState(false, options ? options.identifierPrefix : undefined), + resources, + createResponseState( + resources, + false, + options ? options.identifierPrefix : undefined, + ), createRootFormatContext(), Infinity, onError, diff --git a/packages/react-server-dom-fb/src/ReactDOMServerFB.js b/packages/react-server-dom-fb/src/ReactDOMServerFB.js index 5944cb00bee7d..bdf4dcedaee0b 100644 --- a/packages/react-server-dom-fb/src/ReactDOMServerFB.js +++ b/packages/react-server-dom-fb/src/ReactDOMServerFB.js @@ -23,6 +23,7 @@ import { } from 'react-server/src/ReactFizzServer'; import { + createResources, createResponseState, createRootFormatContext, } from 'react-server/src/ReactFizzConfig'; @@ -49,9 +50,12 @@ function renderToStream(children: ReactNodeList, options: Options): Stream { fatal: false, error: null, }; + const resources = createResources(); const request = createRequest( children, + resources, createResponseState( + resources, options ? options.identifierPrefix : undefined, undefined, options ? options.bootstrapScriptContent : undefined, diff --git a/packages/react-server-dom-fb/src/__tests__/ReactDOMServerFB-test.internal.js b/packages/react-server-dom-fb/src/__tests__/ReactDOMServerFB-test.internal.js index d3267ebdb5db0..667db5c4443c2 100644 --- a/packages/react-server-dom-fb/src/__tests__/ReactDOMServerFB-test.internal.js +++ b/packages/react-server-dom-fb/src/__tests__/ReactDOMServerFB-test.internal.js @@ -59,7 +59,7 @@ describe('ReactDOMServerFB', () => { }); const result = readResult(stream); expect(result).toMatchInlineSnapshot( - `"
hello world
"`, + `"
hello world
"`, ); }); diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index fc56c8d7c8583..68dbe68f88c90 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -72,7 +72,6 @@ import { writePostamble, hoistResources, setCurrentlyRenderingBoundaryResourcesTarget, - createResources, createBoundaryResources, prepareHostDispatcher, supportsRequestStorage, @@ -270,6 +269,7 @@ function noop(): void {} export function createRequest( children: ReactNodeList, + resources: Resources, responseState: ResponseState, rootFormatContext: FormatContext, progressiveChunkSize: void | number, @@ -282,7 +282,6 @@ export function createRequest( prepareHostDispatcher(); const pingedTasks: Array = []; const abortSet: Set = new Set(); - const resources: Resources = createResources(); const request: Request = { destination: null, flushScheduled: false, @@ -2343,7 +2342,12 @@ function flushCompletedQueues( // We haven't flushed the root yet so we don't need to check any other branches further down return; } - } else if (enableFloat) { + } else if (request.pendingRootTasks > 0) { + // We have not yet flushed the root segment so we early return + return; + } + + if (enableFloat) { writeHoistables(destination, request.resources, request.responseState); }