Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fizz] Ensure Resumable State is Serializable #27388

Merged
merged 8 commits into from
Sep 20, 2023
612 changes: 338 additions & 274 deletions packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js

Large diffs are not rendered by default.

35 changes: 33 additions & 2 deletions packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,23 @@ export type RenderState = {
startInlineScript: PrecomputedChunk,
htmlChunks: null | Array<Chunk | PrecomputedChunk>,
headChunks: null | Array<Chunk | PrecomputedChunk>,
externalRuntimeScript: null | any,
bootstrapChunks: Array<Chunk | PrecomputedChunk>,
charsetChunks: Array<Chunk | PrecomputedChunk>,
preconnectChunks: Array<Chunk | PrecomputedChunk>,
importMapChunks: Array<Chunk | PrecomputedChunk>,
preloadChunks: Array<Chunk | PrecomputedChunk>,
hoistableChunks: Array<Chunk | PrecomputedChunk>,
preconnects: Set<any>,
fontPreloads: Set<any>,
highImagePreloads: Set<any>,
// usedImagePreloads: Set<any>,
precedences: Map<string, Map<any, any>>,
stylePrecedences: Map<string, any>,
bootstrapScripts: Set<any>,
scripts: Set<any>,
bulkPreloads: Set<any>,
preloadsMap: Map<string, any>,
boundaryResources: ?BoundaryResources,
stylesToHoist: boolean,
// This is an extra field for the legacy renderer
Expand All @@ -52,10 +64,17 @@ export type RenderState = {

export function createRenderState(
resumableState: ResumableState,
nonce: string | void,
generateStaticMarkup: boolean,
): RenderState {
const renderState = createRenderStateImpl(resumableState, nonce);
const renderState = createRenderStateImpl(
resumableState,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
);
return {
// Keep this in sync with ReactFizzConfigDOM
placeholderPrefix: renderState.placeholderPrefix,
Expand All @@ -64,11 +83,23 @@ export function createRenderState(
startInlineScript: renderState.startInlineScript,
htmlChunks: renderState.htmlChunks,
headChunks: renderState.headChunks,
externalRuntimeScript: renderState.externalRuntimeScript,
bootstrapChunks: renderState.bootstrapChunks,
charsetChunks: renderState.charsetChunks,
preconnectChunks: renderState.preconnectChunks,
importMapChunks: renderState.importMapChunks,
preloadChunks: renderState.preloadChunks,
hoistableChunks: renderState.hoistableChunks,
preconnects: renderState.preconnects,
fontPreloads: renderState.fontPreloads,
highImagePreloads: renderState.highImagePreloads,
// usedImagePreloads: renderState.usedImagePreloads,
precedences: renderState.precedences,
stylePrecedences: renderState.stylePrecedences,
bootstrapScripts: renderState.bootstrapScripts,
scripts: renderState.scripts,
bulkPreloads: renderState.bulkPreloads,
preloadsMap: renderState.preloadsMap,
boundaryResources: renderState.boundaryResources,
stylesToHoist: renderState.stylesToHoist,

Expand Down
8 changes: 4 additions & 4 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6339,7 +6339,7 @@ describe('ReactDOMFizzServer', () => {

const resumed = ReactDOMFizzServer.resumeToPipeableStream(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
);

// Create a separate stream so it doesn't close the writable. I.e. simple concat.
Expand Down Expand Up @@ -6431,7 +6431,7 @@ describe('ReactDOMFizzServer', () => {

const resumed = ReactDOMFizzServer.resumeToPipeableStream(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
{
onError(x) {
ssrErrors.push(x.message);
Expand Down Expand Up @@ -6574,7 +6574,7 @@ describe('ReactDOMFizzServer', () => {

const resumed = ReactDOMFizzServer.resumeToPipeableStream(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
{
onError(x) {
ssrErrors.push(x.message);
Expand Down Expand Up @@ -6729,7 +6729,7 @@ describe('ReactDOMFizzServer', () => {

const resumed = ReactDOMFizzServer.resumeToPipeableStream(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
{
onError(x) {
ssrErrors.push(x.message);
Expand Down
151 changes: 145 additions & 6 deletions packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ global.ReadableStream =
global.TextEncoder = require('util').TextEncoder;

let React;
let ReactDOM;
let ReactDOMFizzServer;
let ReactDOMFizzStatic;
let Suspense;
Expand All @@ -29,6 +30,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
ReactDOMFizzServer = require('react-dom/server.browser');
if (__EXPERIMENTAL__) {
ReactDOMFizzStatic = require('react-dom/static.browser');
Expand Down Expand Up @@ -481,7 +483,7 @@ describe('ReactDOMFizzStaticBrowser', () => {

const resumed = await ReactDOMFizzServer.resume(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
);

await readIntoContainer(prerendered.prelude);
Expand Down Expand Up @@ -523,7 +525,7 @@ describe('ReactDOMFizzStaticBrowser', () => {

const resumed = await ReactDOMFizzServer.resume(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
);

await readIntoContainer(prerendered.prelude);
Expand Down Expand Up @@ -562,7 +564,7 @@ describe('ReactDOMFizzStaticBrowser', () => {

const resumed = await ReactDOMFizzServer.resume(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
);

await readIntoContainer(prerendered.prelude);
Expand Down Expand Up @@ -610,7 +612,7 @@ describe('ReactDOMFizzStaticBrowser', () => {

const resumed = await ReactDOMFizzServer.resume(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
);

await readIntoContainer(prerendered.prelude);
Expand Down Expand Up @@ -651,7 +653,7 @@ describe('ReactDOMFizzStaticBrowser', () => {

const resumed = await ReactDOMFizzServer.resume(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
);

await readIntoContainer(prerendered.prelude);
Expand Down Expand Up @@ -692,7 +694,7 @@ describe('ReactDOMFizzStaticBrowser', () => {

const content = await ReactDOMFizzServer.resume(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
);

const html = await readContent(concat(prerendered.prelude, content));
Expand All @@ -701,4 +703,141 @@ describe('ReactDOMFizzStaticBrowser', () => {
expect(Array.from(html.matchAll(htmlEndTags)).length).toBe(1);
expect(Array.from(html.matchAll(bodyEndTags)).length).toBe(1);
});

// @gate enablePostpone
it('can prerender various hoistables and deduped resources', async () => {
let prerendering = true;
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return (
<>
<link rel="stylesheet" href="my-style2" precedence="low" />
<link rel="stylesheet" href="my-style1" precedence="high" />
<style precedence="high" href="my-style3">
style
</style>
<img src="my-img" />
</>
);
}

function App() {
ReactDOM.preconnect('example.com');
ReactDOM.preload('my-font', {as: 'font', type: 'font/woff2'});
ReactDOM.preload('my-style0', {as: 'style'});
// This should transfer the props in to the style that loads later.
ReactDOM.preload('my-style2', {
as: 'style',
crossOrigin: 'use-credentials',
});
return (
<div>
<Suspense fallback="Loading...">
<link rel="stylesheet" href="my-style1" precedence="high" />
<img src="my-img" />
<Postpone />
</Suspense>
<title>Hello World</title>
</div>
);
}

let calledInit = false;
jest.mock(
'init.js',
() => {
calledInit = true;
},
{virtual: true},
);

const prerendered = await ReactDOMFizzStatic.prerender(<App />, {
bootstrapScripts: ['init.js'],
});
expect(prerendered.postponed).not.toBe(null);

await readIntoContainer(prerendered.prelude);

expect(getVisibleChildren(container)).toEqual([
<link href="example.com" rel="preconnect" />,
<link
as="font"
crossorigin=""
href="my-font"
rel="preload"
type="font/woff2"
/>,
<link as="image" href="my-img" rel="preload" />,
<link data-precedence="high" href="my-style1" rel="stylesheet" />,
<link as="script" fetchpriority="low" href="init.js" rel="preload" />,
<link as="style" href="my-style0" rel="preload" />,
<link
as="style"
crossorigin="use-credentials"
href="my-style2"
rel="preload"
/>,
<title>Hello World</title>,
<div>Loading...</div>,
]);

prerendering = false;
const content = await ReactDOMFizzServer.resume(
<App />,
JSON.parse(JSON.stringify(prerendered.postponed)),
);

await readIntoContainer(content);

expect(calledInit).toBe(true);

// Dispatch load event to injected stylesheet
const link = document.querySelector(
'link[rel="stylesheet"][href="my-style2"]',
);
const event = document.createEvent('Events');
event.initEvent('load', true, true);
link.dispatchEvent(event);

// Wait for the instruction microtasks to flush.
await 0;
await 0;

expect(getVisibleChildren(container)).toEqual([
<link href="example.com" rel="preconnect" />,
<link
as="font"
crossorigin=""
href="my-font"
rel="preload"
type="font/woff2"
/>,
<link as="image" href="my-img" rel="preload" />,
<link data-precedence="high" href="my-style1" rel="stylesheet" />,
<style data-href="my-style3" data-precedence="high">
style
</style>,
<link
crossorigin="use-credentials"
data-precedence="low"
href="my-style2"
rel="stylesheet"
/>,
<link as="script" fetchpriority="low" href="init.js" rel="preload" />,
<link as="style" href="my-style0" rel="preload" />,
<link
as="style"
crossorigin="use-credentials"
href="my-style2"
rel="preload"
/>,
<title>Hello World</title>,
<div>
<img src="my-img" />
<img src="my-img" />
</div>,
]);
});
});
12 changes: 6 additions & 6 deletions packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
import {
createResumableState,
createRenderState,
resumeRenderState,
createRootFormatContext,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';

Expand Down Expand Up @@ -96,10 +97,6 @@ function renderToReadableStream(
}
const resumableState = createResumableState(
options ? options.identifierPrefix : undefined,
options ? options.nonce : undefined,
options ? options.bootstrapScriptContent : undefined,
options ? options.bootstrapScripts : undefined,
options ? options.bootstrapModules : undefined,
options ? options.unstable_externalRuntimeSrc : undefined,
);
const request = createRequest(
Expand All @@ -108,6 +105,10 @@ function renderToReadableStream(
createRenderState(
resumableState,
options ? options.nonce : undefined,
options ? options.bootstrapScriptContent : undefined,
options ? options.bootstrapScripts : undefined,
options ? options.bootstrapModules : undefined,
options ? options.unstable_externalRuntimeSrc : undefined,
options ? options.importMap : undefined,
),
createRootFormatContext(options ? options.namespaceURI : undefined),
Expand Down Expand Up @@ -177,10 +178,9 @@ function resume(
const request = resumeRequest(
children,
postponedState,
createRenderState(
resumeRenderState(
postponedState.resumableState,
options ? options.nonce : undefined,
undefined, // importMap
),
options ? options.onError : undefined,
onAllReady,
Expand Down
8 changes: 4 additions & 4 deletions packages/react-dom/src/server/ReactDOMFizzServerBun.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,6 @@ function renderToReadableStream(
}
const resumableState = createResumableState(
options ? options.identifierPrefix : undefined,
options ? options.nonce : undefined,
options ? options.bootstrapScriptContent : undefined,
options ? options.bootstrapScripts : undefined,
options ? options.bootstrapModules : undefined,
options ? options.unstable_externalRuntimeSrc : undefined,
);
const request = createRequest(
Expand All @@ -99,6 +95,10 @@ function renderToReadableStream(
createRenderState(
resumableState,
options ? options.nonce : undefined,
options ? options.bootstrapScriptContent : undefined,
options ? options.bootstrapScripts : undefined,
options ? options.bootstrapModules : undefined,
options ? options.unstable_externalRuntimeSrc : undefined,
options ? options.importMap : undefined,
),
createRootFormatContext(options ? options.namespaceURI : undefined),
Expand Down
Loading