diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 089e9964ce970..9eeb120082991 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -296,6 +296,8 @@ export function createResponseState( const integrity = typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity; + preloadBootstrapModule(resources, src, nonce, integrity); + bootstrapChunks.push( startModuleSrc, stringToChunk(escapeTextForBrowser(src)), @@ -1977,7 +1979,7 @@ function pushLink( } } pushLinkImpl(resource.chunks, resource.props); - resources.usedStylesheets.add(resource); + resources.usedStylesheets.set(key, resource); return pushLinkImpl(target, props); } else { // This stylesheet refers to a Resource and we create a new one if necessary @@ -4249,8 +4251,7 @@ export function writePreamble( // Flush unblocked stylesheets by precedence resources.precedences.forEach(flushAllStylesInPreamble, destination); - resources.usedStylesheets.forEach(resource => { - const key = getResourceKey(resource.props.as, resource.props.href); + resources.usedStylesheets.forEach((resource, key) => { if (resources.stylesMap.has(key)) { // The underlying stylesheet is represented both as a used stylesheet // (a regular component we will attempt to preload) and as a StylesheetResource. @@ -4345,8 +4346,7 @@ export function writeHoistables( // but we want to kick off preloading as soon as possible resources.precedences.forEach(preloadLateStyles, destination); - resources.usedStylesheets.forEach(resource => { - const key = getResourceKey(resource.props.as, resource.props.href); + resources.usedStylesheets.forEach((resource, key) => { if (resources.stylesMap.has(key)) { // The underlying stylesheet is represented both as a used stylesheet // (a regular component we will attempt to preload) and as a StylesheetResource. @@ -4861,12 +4861,18 @@ type PreconnectProps = { }; type PreconnectResource = TResource<'preconnect', null>; -type PreloadProps = { +type PreloadAsProps = { rel: 'preload', as: string, href: string, [string]: mixed, }; +type PreloadModuleProps = { + rel: 'modulepreload', + href: string, + [string]: mixed, +}; +type PreloadProps = PreloadAsProps | PreloadModuleProps; type PreloadResource = TResource<'preload', PreloadProps>; type StylesheetProps = { @@ -4911,7 +4917,7 @@ export type Resources = { // usedImagePreloads: Set, precedences: Map>, stylePrecedences: Map, - usedStylesheets: Set, + usedStylesheets: Map, scripts: Set, usedScripts: Set, explicitStylesheetPreloads: Set, @@ -4939,7 +4945,7 @@ export function createResources(): Resources { // usedImagePreloads: new Set(), precedences: new Map(), stylePrecedences: new Map(), - usedStylesheets: new Set(), + usedStylesheets: new Map(), scripts: new Set(), usedScripts: new Set(), explicitStylesheetPreloads: new Set(), @@ -5512,6 +5518,46 @@ function preloadBootstrapScript( pushLinkImpl(resource.chunks, props); } +// This function is only safe to call at Request start time since it assumes +// that each module 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 preloadBootstrapModule( + 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 module with src "%s" to not have been preloaded already. please file an issue', + src, + ); + } + } + const props: PreloadModuleProps = { + rel: 'modulepreload', + href: src, + nonce, + integrity, + }; + const resource: PreloadResource = { + type: 'preload', + chunks: [], + state: NoState, + props, + }; + resources.preloadsMap.set(key, resource); + resources.explicitScriptPreloads.add(resource); + pushLinkImpl(resource.chunks, props); + return; +} + function internalPreinitScript( resources: Resources, src: string, diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index e16beba0513ad..d2478d4cccd1f 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -600,7 +600,10 @@ describe('ReactDOMFizzServer', () => { 'init.js', {src: 'init2.js', integrity: 'init2hash'}, ], - bootstrapModules: ['init.mjs'], + bootstrapModules: [ + 'init.mjs', + {src: 'init2.mjs', integrity: 'init2hash'}, + ], }, ); pipe(writable); @@ -615,16 +618,23 @@ describe('ReactDOMFizzServer', () => { nonce={CSPnonce} integrity="init2hash" />, + , + ,
Loading...
, ]); - // check that there are 4 scripts with a matching nonce: - // The runtime script, an inline bootstrap script, and two src scripts + // check that there are 6 scripts with a matching nonce: + // The runtime script, an inline bootstrap script, two bootstrap scripts and two bootstrap modules expect( Array.from(container.getElementsByTagName('script')).filter( node => node.getAttribute('nonce') === CSPnonce, ).length, - ).toEqual(5); + ).toEqual(6); await act(() => { resolve({default: Text}); @@ -638,6 +648,13 @@ describe('ReactDOMFizzServer', () => { nonce={CSPnonce} integrity="init2hash" />, + , + ,
Hello
, ]); } finally { @@ -3783,6 +3800,9 @@ describe('ReactDOMFizzServer', () => { + + +
hello world
diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js index 6571cddcc6981..b19ef8d667e2c 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 03ce2f5b8468b..59ae61f1cdcd7 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 7bda83ff2cd3b..682bf6dd2a03b 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 f121a7c289c0e..911d11b538c2c 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-server-dom-fb/src/__tests__/ReactDOMServerFB-test.internal.js b/packages/react-server-dom-fb/src/__tests__/ReactDOMServerFB-test.internal.js index 667db5c4443c2..8e6ff121cc952 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
"`, ); });