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);
}