Skip to content

Commit

Permalink
[Fizz] preload bootstrapModules (#26754)
Browse files Browse the repository at this point in the history
stacked on #26753 

Adds support for preloading bootstrapModules. We don't yet support
modules in Float's public interface but this implementation should be
compatible with what we do when we add it.
  • Loading branch information
gnoff committed May 31, 2023
1 parent b864ad4 commit ae31d2e
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 18 deletions.
62 changes: 54 additions & 8 deletions packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
Expand Up @@ -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)),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -4911,7 +4917,7 @@ export type Resources = {
// usedImagePreloads: Set<PreloadResource>,
precedences: Map<string, Set<StyleResource>>,
stylePrecedences: Map<string, StyleTagResource>,
usedStylesheets: Set<PreloadResource>,
usedStylesheets: Map<string, PreloadResource>,
scripts: Set<ScriptResource>,
usedScripts: Set<PreloadResource>,
explicitStylesheetPreloads: Set<PreloadResource>,
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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,
Expand Down
28 changes: 24 additions & 4 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Expand Up @@ -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);
Expand All @@ -615,16 +618,23 @@ describe('ReactDOMFizzServer', () => {
nonce={CSPnonce}
integrity="init2hash"
/>,
<link rel="modulepreload" href="init.mjs" nonce={CSPnonce} />,
<link
rel="modulepreload"
href="init2.mjs"
nonce={CSPnonce}
integrity="init2hash"
/>,
<div>Loading...</div>,
]);

// 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});
Expand All @@ -638,6 +648,13 @@ describe('ReactDOMFizzServer', () => {
nonce={CSPnonce}
integrity="init2hash"
/>,
<link rel="modulepreload" href="init.mjs" nonce={CSPnonce} />,
<link
rel="modulepreload"
href="init2.mjs"
nonce={CSPnonce}
integrity="init2hash"
/>,
<div>Hello</div>,
]);
} finally {
Expand Down Expand Up @@ -3783,6 +3800,9 @@ describe('ReactDOMFizzServer', () => {
<link rel="preload" href="foo" as="script" />
<link rel="preload" href="bar" as="script" />
<link rel="preload" href="baz" as="script" integrity="qux" />
<link rel="modulepreload" href="quux" />
<link rel="modulepreload" href="corge" />
<link rel="modulepreload" href="grault" integrity="garply" />
</head>
<body>
<div>hello world</div>
Expand Down
Expand Up @@ -84,7 +84,7 @@ describe('ReactDOMFizzServerBrowser', () => {
);
const result = await readResult(stream);
expect(result).toMatchInlineSnapshot(
`"<link rel="preload" href="init.js" as="script"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
`"<link rel="preload" href="init.js" as="script"/><link rel="modulepreload" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
);
});

Expand Down Expand Up @@ -500,7 +500,7 @@ describe('ReactDOMFizzServerBrowser', () => {
);
const result = await readResult(stream);
expect(result).toMatchInlineSnapshot(
`"<link rel="preload" href="init.js" as="script" nonce="R4nd0m"/><div>hello world</div><script nonce="${nonce}">INIT();</script><script src="init.js" nonce="${nonce}" async=""></script><script type="module" src="init.mjs" nonce="${nonce}" async=""></script>"`,
`"<link rel="preload" href="init.js" as="script" nonce="R4nd0m"/><link rel="modulepreload" href="init.mjs" nonce="R4nd0m"/><div>hello world</div><script nonce="${nonce}">INIT();</script><script src="init.js" nonce="${nonce}" async=""></script><script type="module" src="init.mjs" nonce="${nonce}" async=""></script>"`,
);
});
});
Expand Up @@ -98,7 +98,7 @@ describe('ReactDOMFizzServerNode', () => {
pipe(writable);
jest.runAllTimers();
expect(output.result).toMatchInlineSnapshot(
`"<link rel="preload" href="init.js" as="script"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
`"<link rel="preload" href="init.js" as="script"/><link rel="modulepreload" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
);
});

Expand Down
Expand Up @@ -84,7 +84,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
});
const prelude = await readContent(result.prelude);
expect(prelude).toMatchInlineSnapshot(
`"<link rel="preload" href="init.js" as="script"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
`"<link rel="preload" href="init.js" as="script"/><link rel="modulepreload" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
);
});

Expand Down
Expand Up @@ -86,7 +86,7 @@ describe('ReactDOMFizzStaticNode', () => {
);
const prelude = await readContent(result.prelude);
expect(prelude).toMatchInlineSnapshot(
`"<link rel="preload" href="init.js" as="script"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
`"<link rel="preload" href="init.js" as="script"/><link rel="modulepreload" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
);
});

Expand Down
Expand Up @@ -59,7 +59,7 @@ describe('ReactDOMServerFB', () => {
});
const result = readResult(stream);
expect(result).toMatchInlineSnapshot(
`"<link rel="preload" href="init.js" as="script"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
`"<link rel="preload" href="init.js" as="script"/><link rel="modulepreload" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
);
});

Expand Down

0 comments on commit ae31d2e

Please sign in to comment.