@samitouri / QOS-React-2 / commits / 7468903294

[Static][Fizz] bootstrap scripts should only emit once (#27674)

I introduced a bug in a recent change to how bootstrap scripts are handled. Rather than clearing out the bootstrap script state from ResumableState on completion of the prerender I did it during the flushing phase which comes later after the postponed state has likely been serialized. We should freeze these objects in dev so this is not possible to do easily in test (nor in actual code in real systems). This fixes the bug by eliminating the bootstrap config during getPostponedState which is before the state can be serialized.

Josh Story committed Nov 8, 2023 at 17:51 UTC 746890329452cbec8685eb3466847c5f17d9dc77
6 files changed +92 -12
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+9 -7
@@ -690,6 +690,13 @@ export function resetResumableState(
690 resumableState.moduleScriptResources = {};
691 }
692
693 +export function completeResumableState(resumableState: ResumableState): void {
694 + // This function is called when we have completed a prerender and there is a shell.
695 + resumableState.bootstrapScriptContent = undefined;
696 + resumableState.bootstrapScripts = undefined;
697 + resumableState.bootstrapModules = undefined;
698 +}
699 +
700 // Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion
701 // modes. We only include the variants as they matter for the sake of our purposes.
702 // We don't actually provide the namespace therefore we use constants instead of the string.
@@ -3723,11 +3730,7 @@ export function pushEndInstance(
3730 function writeBootstrap(
3731 destination: Destination,
3732 renderState: RenderState,
3726 - resumableState: ResumableState,
3733 ): boolean {
3728 - resumableState.bootstrapScriptContent = undefined;
3729 - resumableState.bootstrapScripts = undefined;
3730 - resumableState.bootstrapModules = undefined;
3734 const bootstrapChunks = renderState.bootstrapChunks;
3735 let i = 0;
3736 for (; i < bootstrapChunks.length - 1; i++) {
@@ -3744,9 +3747,8 @@ function writeBootstrap(
3747 export function writeCompletedRoot(
3748 destination: Destination,
3749 renderState: RenderState,
3747 - resumableState: ResumableState,
3750 ): boolean {
3749 - return writeBootstrap(destination, renderState, resumableState);
3751 + return writeBootstrap(destination, renderState);
3752 }
3753
3754 // Structural Nodes
@@ -4211,7 +4213,7 @@ export function writeCompletedBoundaryInstruction(
4213 } else {
4214 writeMore = writeChunkAndReturn(destination, completeBoundaryDataEnd);
4215 }
4214 - return writeBootstrap(destination, renderState, resumableState) && writeMore;
4216 + return writeBootstrap(destination, renderState) && writeMore;
4217 }
4218
4219 const clientRenderScript1Full = stringToPrecomputedChunk(
packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js
+1
@@ -171,6 +171,7 @@ export {
171 setCurrentlyRenderingBoundaryResourcesTarget,
172 prepareHostDispatcher,
173 resetResumableState,
174 + completeResumableState,
175 emitEarlyPreloads,
176 } from './ReactFizzConfigDOM';
177
packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
+76
@@ -1419,4 +1419,80 @@ describe('ReactDOMFizzStaticBrowser', () => {
1419 '<!DOCTYPE html><html><head></head><body>hello<!--$?--><template id="B:1"></template><!--/$--><div hidden id="S:1">world<!-- --></div><script>$RC',
1420 );
1421 });
1422 +
1423 + // @gate enablePostpone
1424 + it('does not bootstrap again in a resume if it bootstraps', async () => {
1425 + let prerendering = true;
1426 +
1427 + function Postpone() {
1428 + if (prerendering) {
1429 + React.unstable_postpone();
1430 + }
1431 + return null;
1432 + }
1433 +
1434 + function App() {
1435 + return (
1436 + <html>
1437 + <body>
1438 + <Suspense fallback="loading...">
1439 + <Postpone />
1440 + hello
1441 + </Suspense>
1442 + </body>
1443 + </html>
1444 + );
1445 + }
1446 +
1447 + let inits = 0;
1448 + jest.mock(
1449 + 'init.js',
1450 + () => {
1451 + inits++;
1452 + },
1453 + {virtual: true},
1454 + );
1455 +
1456 + const prerendered = await ReactDOMFizzStatic.prerender(<App />, {
1457 + bootstrapScripts: ['init.js'],
1458 + });
1459 +
1460 + const postponedSerializedState = JSON.stringify(prerendered.postponed);
1461 +
1462 + expect(prerendered.postponed).not.toBe(null);
1463 +
1464 + await readIntoContainer(prerendered.prelude);
1465 +
1466 + expect(getVisibleChildren(container)).toEqual([
1467 + <link rel="preload" href="init.js" fetchpriority="low" as="script" />,
1468 + 'loading...',
1469 + ]);
1470 +
1471 + expect(inits).toBe(1);
1472 +
1473 + jest.resetModules();
1474 + jest.mock(
1475 + 'init.js',
1476 + () => {
1477 + inits++;
1478 + },
1479 + {virtual: true},
1480 + );
1481 +
1482 + prerendering = false;
1483 +
1484 + const content = await ReactDOMFizzServer.resume(
1485 + <App />,
1486 + JSON.parse(postponedSerializedState),
1487 + );
1488 +
1489 + await readIntoContainer(content);
1490 +
1491 + expect(inits).toBe(1);
1492 +
1493 + expect(getVisibleChildren(container)).toEqual([
1494 + <link rel="preload" href="init.js" fetchpriority="low" as="script" />,
1495 + 'hello',
1496 + ]);
1497 + });
1498 });
packages/react-noop-renderer/src/ReactNoopServer.js
+1
@@ -95,6 +95,7 @@ const ReactNoopServer = ReactFizzServer({
95 },
96
97 resetResumableState(): void {},
98 + completeResumableState(): void {},
99
100 pushTextInstance(
101 target: Array<Uint8Array>,
packages/react-server/src/ReactFizzServer.js
+4 -5
@@ -77,6 +77,7 @@ import {
77 pushFormStateMarkerIsMatching,
78 pushFormStateMarkerIsNotMatching,
79 resetResumableState,
80 + completeResumableState,
81 emitEarlyPreloads,
82 } from './ReactFizzConfig';
83 import {
@@ -3968,11 +3969,7 @@ function flushCompletedQueues(
3969
3970 flushSegment(request, destination, completedRootSegment);
3971 request.completedRootSegment = null;
3971 - writeCompletedRoot(
3972 - destination,
3973 - request.renderState,
3974 - request.resumableState,
3975 - );
3972 + writeCompletedRoot(destination, request.renderState);
3973 } else {
3974 // We haven't flushed the root yet so we don't need to check any other branches further down
3975 return;
@@ -4283,6 +4280,8 @@ export function getPostponedState(request: Request): null | PostponedState {
4280 ) {
4281 // We postponed the root so we didn't flush anything.
4282 resetResumableState(request.resumableState, request.renderState);
4283 + } else {
4284 + completeResumableState(request.resumableState);
4285 }
4286 return {
4287 nextSegmentId: request.nextSegmentId,
packages/react-server/src/forks/ReactFizzConfig.custom.js
+1
@@ -41,6 +41,7 @@ export const supportsRequestStorage = false;
41 export const requestStorage: AsyncLocalStorage<Request> = (null: any);
42
43 export const resetResumableState = $$$config.resetResumableState;
44 +export const completeResumableState = $$$config.completeResumableState;
45 export const getChildFormatContext = $$$config.getChildFormatContext;
46 export const makeId = $$$config.makeId;
47 export const pushTextInstance = $$$config.pushTextInstance;