From 143d3e1b89d7f64d607bbfc844d1324b39ed93dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Fri, 25 Apr 2025 11:52:28 -0400 Subject: [PATCH 1/2] [Fizz] Emit link rel="expect" to block render before the shell has fully loaded (#33016) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The semantics of React is that anything outside of Suspense boundaries in a transition doesn't display until it has fully unsuspended. With SSR streaming the intention is to preserve that. We explicitly don't want to support the mode of document streaming normally supported by the browser where it can paint content as tags stream in since that leads to content popping in and thrashing in unpredictable ways. This should instead be modeled explictly by nested Suspense boundaries or something like SuspenseList. After the first shell any nested Suspense boundaries are only revealed, by script, once they're fully streamed in to the next boundary. So this is already the case there. However, for the initial shell we have been at the mercy of browser heuristics for how long it decides to stream before the first paint. Chromium now has [an API explicitly for this use case](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API/Using#stabilizing_page_state_to_make_cross-document_transitions_consistent) that lets us model the semantics that we want. This is always important but especially so with MPA View Transitions. After this a simple document looks like this: ```html

hello world

... ``` The `rel="expect"` tag indicates that we want to wait to paint until we have streamed far enough to be able to paint the id `"«R»"` which indicates the shell. Ideally this `id` would be assigned to the root most HTML element in the body. However, this is tricky in our implementation because there can be multiple and we can render them out of order. So instead, we assign the id to the first bootstrap script if there is one since these are always added to the end of the shell. If there isn't a bootstrap script then we emit an empty `` instead as a marker. Since we currently put as much as possible in the shell if it's loaded by the time we render, this can have some negative effects for very large documents. We should instead apply the heuristic where very large Suspense boundaries get outlined outside the shell even if they're immediately available. This means that even prerenders can end up with script tags. We only emit the `rel="expect"` if you're rendering a whole document. I.e. if you rendered either a `` or `` tag. If you're rendering a partial document, then we don't really know where the streaming parts are anyway and can't provide such guarantees. This does apply whether you're streaming or not because we still want to block rendering until the end, but in practice any serialized state that needs hydrate should still be embedded after the completion id. --- fixtures/ssr/server/render.js | 36 ++++- fixtures/ssr/src/components/Chrome.js | 1 + .../src/server/ReactFizzConfigDOM.js | 136 +++++++++++++++--- .../src/__tests__/ReactDOMFizzServer-test.js | 11 +- .../ReactDOMFizzServerBrowser-test.js | 8 +- .../__tests__/ReactDOMFizzServerEdge-test.js | 2 +- .../__tests__/ReactDOMFizzServerNode-test.js | 4 +- .../src/__tests__/ReactDOMFizzStatic-test.js | 5 +- .../ReactDOMFizzStaticBrowser-test.js | 20 ++- .../__tests__/ReactDOMFizzStaticNode-test.js | 4 +- .../src/__tests__/ReactDOMFloat-test.js | 9 +- .../src/__tests__/ReactDOMLegacyFloat-test.js | 3 +- .../ReactDOMSingletonComponents-test.js | 5 +- .../src/__tests__/ReactRenderDocument-test.js | 34 +++-- .../react-dom/src/test-utils/FizzTestUtils.js | 5 +- .../react-markup/src/ReactFizzConfigMarkup.js | 32 ++++- .../ReactDOMServerFB-test.internal.js | 2 +- .../src/__tests__/ReactFlightDOM-test.js | 13 +- .../__tests__/ReactFlightDOMBrowser-test.js | 4 +- packages/react-server/src/ReactFizzServer.js | 6 +- 20 files changed, 274 insertions(+), 66 deletions(-) diff --git a/fixtures/ssr/server/render.js b/fixtures/ssr/server/render.js index a4fe698858ab1..e20b9a35dc502 100644 --- a/fixtures/ssr/server/render.js +++ b/fixtures/ssr/server/render.js @@ -1,5 +1,6 @@ import React from 'react'; import {renderToPipeableStream} from 'react-dom/server'; +import {Writable} from 'stream'; import App from '../src/components/App'; @@ -14,11 +15,41 @@ if (process.env.NODE_ENV === 'development') { assets = require('../build/asset-manifest.json'); } +class ThrottledWritable extends Writable { + constructor(destination) { + super(); + this.destination = destination; + this.delay = 150; + } + + _write(chunk, encoding, callback) { + let o = 0; + const write = () => { + this.destination.write(chunk.slice(o, o + 100), encoding, x => { + o += 100; + if (o < chunk.length) { + setTimeout(write, this.delay); + } else { + callback(x); + } + }); + }; + setTimeout(write, this.delay); + } + + _final(callback) { + setTimeout(() => { + this.destination.end(callback); + }, this.delay); + } +} + export default function render(url, res) { res.socket.on('error', error => { // Log fatal errors console.error('Fatal', error); }); + console.log('hello'); let didError = false; const {pipe, abort} = renderToPipeableStream(, { bootstrapScripts: [assets['main.js']], @@ -26,7 +57,10 @@ export default function render(url, res) { // If something errored before we started streaming, we set the error code appropriately. res.statusCode = didError ? 500 : 200; res.setHeader('Content-type', 'text/html'); - pipe(res); + // To test the actual chunks taking time to load over the network, we throttle + // the stream a bit. + const throttledResponse = new ThrottledWritable(res); + pipe(throttledResponse); }, onShellError(x) { // Something errored before we could complete the shell so we emit an alternative shell. diff --git a/fixtures/ssr/src/components/Chrome.js b/fixtures/ssr/src/components/Chrome.js index 5cf81a877f7e3..984c726a02652 100644 --- a/fixtures/ssr/src/components/Chrome.js +++ b/fixtures/ssr/src/components/Chrome.js @@ -37,6 +37,7 @@ export default class Chrome extends Component { +

This should appear in the first paint.

'); const startScriptSrc = stringToPrecomputedChunk(''); +const scriptNonce = stringToPrecomputedChunk(' nonce="'); +const scriptIntegirty = stringToPrecomputedChunk(' integrity="'); +const scriptCrossOrigin = stringToPrecomputedChunk(' crossorigin="'); +const endAsyncScript = stringToPrecomputedChunk(' async="">'); /** * This escaping function is designed to work with with inline scripts where the entire @@ -367,7 +368,7 @@ export function createRenderState( nonce === undefined ? startInlineScript : stringToPrecomputedChunk( - '', + '' + + '', ); }); @@ -4189,7 +4190,7 @@ describe('ReactDOMFizzServer', () => { renderOptions.unstable_externalRuntimeSrc, ).map(n => n.outerHTML), ).toEqual([ - '', + '', '', '', '', @@ -4276,7 +4277,7 @@ describe('ReactDOMFizzServer', () => { renderOptions.unstable_externalRuntimeSrc, ).map(n => n.outerHTML), ).toEqual([ - '', + '', '', '', '', @@ -4512,7 +4513,7 @@ describe('ReactDOMFizzServer', () => { // the html should be as-is expect(document.documentElement.innerHTML).toEqual( - '

hello world!

', + '

hello world!

', ); }); @@ -6492,7 +6493,7 @@ describe('ReactDOMFizzServer', () => { }); expect(document.documentElement.outerHTML).toEqual( - '', + '', ); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js index 4022f227a8abe..f5b01d2462403 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js @@ -85,7 +85,7 @@ describe('ReactDOMFizzServerBrowser', () => { ); const result = await readResult(stream); expect(result).toMatchInlineSnapshot( - `"hello world"`, + `"hello world"`, ); }); @@ -99,7 +99,7 @@ describe('ReactDOMFizzServerBrowser', () => { ); const result = await readResult(stream); expect(result).toMatchInlineSnapshot( - `"
hello world
"`, + `"
hello world
"`, ); }); @@ -529,7 +529,7 @@ describe('ReactDOMFizzServerBrowser', () => { const result = await readResult(stream); expect(result).toEqual( - 'foobar', + 'foobar', ); }); @@ -547,7 +547,7 @@ describe('ReactDOMFizzServerBrowser', () => { expect(result).toMatchInlineSnapshot( // TODO: remove interpolation because it prevents snapshot updates. // eslint-disable-next-line jest/no-interpolation-in-snapshots - `"
hello world
"`, + `"
hello world
"`, ); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServerEdge-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServerEdge-test.js index c442f1813836c..1eefe1a4082e6 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServerEdge-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServerEdge-test.js @@ -72,7 +72,7 @@ describe('ReactDOMFizzServerEdge', () => { }); expect(result).toMatchInlineSnapshot( - `"
hello
"`, + `"
hello
"`, ); }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js index e97b4a29a7497..2704c243eba48 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js @@ -79,7 +79,7 @@ describe('ReactDOMFizzServerNode', () => { }); // with Float, we emit empty heads if they are elided when rendering expect(output.result).toMatchInlineSnapshot( - `"hello world"`, + `"hello world"`, ); }); @@ -97,7 +97,7 @@ describe('ReactDOMFizzServerNode', () => { pipe(writable); }); expect(output.result).toMatchInlineSnapshot( - `"
hello world
"`, + `"
hello world
"`, ); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js index 96e6538cd2196..de6e21b557a1d 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js @@ -106,7 +106,10 @@ describe('ReactDOMFizzStatic', () => { node.tagName !== 'TEMPLATE' && node.tagName !== 'template' && !node.hasAttribute('hidden') && - !node.hasAttribute('aria-hidden') + !node.hasAttribute('aria-hidden') && + // Ignore the render blocking expect + (node.getAttribute('rel') !== 'expect' || + node.getAttribute('blocking') !== 'render') ) { const props = {}; const attributes = node.attributes; diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js index f973a5ed4d6e0..7eecb16cf82f6 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js @@ -187,7 +187,7 @@ describe('ReactDOMFizzStaticBrowser', () => { ); const prelude = await readContent(result.prelude); expect(prelude).toMatchInlineSnapshot( - `"hello world"`, + `"hello world"`, ); }); @@ -201,7 +201,7 @@ describe('ReactDOMFizzStaticBrowser', () => { ); const prelude = await readContent(result.prelude); expect(prelude).toMatchInlineSnapshot( - `"
hello world
"`, + `"
hello world
"`, ); }); @@ -1428,7 +1428,8 @@ describe('ReactDOMFizzStaticBrowser', () => { expect(await readContent(content)).toBe( '' + '' + - 'Hello', + '' + + 'Hello', ); }); @@ -1474,7 +1475,8 @@ describe('ReactDOMFizzStaticBrowser', () => { expect(await readContent(content)).toBe( '' + '' + - 'Hello', + '' + + 'Hello', ); }); @@ -1525,7 +1527,8 @@ describe('ReactDOMFizzStaticBrowser', () => { expect(await readContent(content)).toBe( '' + '' + - '
Hello
', + '' + + '
Hello
', ); }); @@ -1607,7 +1610,8 @@ describe('ReactDOMFizzStaticBrowser', () => { let result = decoder.decode(value, {stream: true}); expect(result).toBe( - 'hello', + '' + + 'hello', ); await 1; @@ -1631,7 +1635,9 @@ describe('ReactDOMFizzStaticBrowser', () => { const slice = result.slice(0, instructionIndex + '$RC'.length); expect(slice).toBe( - 'hello"`, + `"
hello world
"`, ); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index 7404cec64a00c..5328a4ac9e055 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -250,7 +250,10 @@ describe('ReactDOMFloat', () => { node.tagName !== 'TEMPLATE' && node.tagName !== 'template' && !node.hasAttribute('hidden') && - !node.hasAttribute('aria-hidden')) + !node.hasAttribute('aria-hidden') && + // Ignore the render blocking expect + (node.getAttribute('rel') !== 'expect' || + node.getAttribute('blocking') !== 'render')) ) { const props = {}; const attributes = node.attributes; @@ -690,7 +693,9 @@ describe('ReactDOMFloat', () => { pipe(writable); }); expect(chunks).toEqual([ - 'foobar', + '' + + 'foo' + + 'bar', '', ]); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMLegacyFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMLegacyFloat-test.js index 52c9746abdb4f..f2cabafc9f575 100644 --- a/packages/react-dom/src/__tests__/ReactDOMLegacyFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMLegacyFloat-test.js @@ -34,7 +34,8 @@ describe('ReactDOMFloat', () => { ); expect(result).toEqual( - 'title', + '' + + 'title', ); }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js b/packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js index 84db05bc779db..d887972e92ca1 100644 --- a/packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js @@ -104,7 +104,10 @@ describe('ReactDOM HostSingleton', () => { el.tagName !== 'TEMPLATE' && el.tagName !== 'template' && !el.hasAttribute('hidden') && - !el.hasAttribute('aria-hidden')) || + !el.hasAttribute('aria-hidden') && + // Ignore the render blocking expect + (node.getAttribute('rel') !== 'expect' || + node.getAttribute('blocking') !== 'render')) || el.hasAttribute('data-meaningful') ) { const props = {}; diff --git a/packages/react-dom/src/__tests__/ReactRenderDocument-test.js b/packages/react-dom/src/__tests__/ReactRenderDocument-test.js index 9522a920bc291..2b54bc90090e4 100644 --- a/packages/react-dom/src/__tests__/ReactRenderDocument-test.js +++ b/packages/react-dom/src/__tests__/ReactRenderDocument-test.js @@ -77,12 +77,16 @@ describe('rendering React components at document', () => { await act(() => { root = ReactDOMClient.hydrateRoot(testDocument, ); }); - expect(testDocument.body.innerHTML).toBe('Hello world'); + expect(testDocument.body.innerHTML).toBe( + 'Hello world' + '', + ); await act(() => { root.render(); }); - expect(testDocument.body.innerHTML).toBe('Hello moon'); + expect(testDocument.body.innerHTML).toBe( + 'Hello moon' + '', + ); expect(body === testDocument.body).toBe(true); }); @@ -107,7 +111,9 @@ describe('rendering React components at document', () => { await act(() => { root = ReactDOMClient.hydrateRoot(testDocument, ); }); - expect(testDocument.body.innerHTML).toBe('Hello world'); + expect(testDocument.body.innerHTML).toBe( + 'Hello world' + '', + ); const originalDocEl = testDocument.documentElement; const originalHead = testDocument.head; @@ -118,8 +124,10 @@ describe('rendering React components at document', () => { expect(testDocument.firstChild).toBe(originalDocEl); expect(testDocument.head).toBe(originalHead); expect(testDocument.body).toBe(originalBody); - expect(originalBody.firstChild).toEqual(null); - expect(originalHead.firstChild).toEqual(null); + expect(originalBody.innerHTML).toBe(''); + expect(originalHead.innerHTML).toBe( + '', + ); }); it('should not be able to switch root constructors', async () => { @@ -157,13 +165,17 @@ describe('rendering React components at document', () => { root = ReactDOMClient.hydrateRoot(testDocument, ); }); - expect(testDocument.body.innerHTML).toBe('Hello world'); + expect(testDocument.body.innerHTML).toBe( + 'Hello world' + '', + ); await act(() => { root.render(); }); - expect(testDocument.body.innerHTML).toBe('Goodbye world'); + expect(testDocument.body.innerHTML).toBe( + '' + 'Goodbye world', + ); }); it('should be able to mount into document', async () => { @@ -192,7 +204,9 @@ describe('rendering React components at document', () => { ); }); - expect(testDocument.body.innerHTML).toBe('Hello world'); + expect(testDocument.body.innerHTML).toBe( + 'Hello world' + '', + ); }); it('cannot render over an existing text child at the root', async () => { @@ -325,7 +339,9 @@ describe('rendering React components at document', () => { : [], ); expect(testDocument.body.innerHTML).toBe( - favorSafetyOverHydrationPerf ? 'Hello world' : 'Goodbye world', + favorSafetyOverHydrationPerf + ? 'Hello world' + : 'Goodbye world', ); }); diff --git a/packages/react-dom/src/test-utils/FizzTestUtils.js b/packages/react-dom/src/test-utils/FizzTestUtils.js index 537c64a889a7d..12c768e1a0008 100644 --- a/packages/react-dom/src/test-utils/FizzTestUtils.js +++ b/packages/react-dom/src/test-utils/FizzTestUtils.js @@ -150,7 +150,10 @@ function getVisibleChildren(element: Element): React$Node { node.tagName !== 'TEMPLATE' && node.tagName !== 'template' && !node.hasAttribute('hidden') && - !node.hasAttribute('aria-hidden') + !node.hasAttribute('aria-hidden') && + // Ignore the render blocking expect + (node.getAttribute('rel') !== 'expect' || + node.getAttribute('blocking') !== 'render') ) { const props: any = {}; const attributes = node.attributes; diff --git a/packages/react-markup/src/ReactFizzConfigMarkup.js b/packages/react-markup/src/ReactFizzConfigMarkup.js index 3d08ed1ee64a2..444952dc58502 100644 --- a/packages/react-markup/src/ReactFizzConfigMarkup.js +++ b/packages/react-markup/src/ReactFizzConfigMarkup.js @@ -17,7 +17,10 @@ import type { FormatContext, } from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; -import {pushStartInstance as pushStartInstanceImpl} from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; +import { + pushStartInstance as pushStartInstanceImpl, + writePreambleStart as writePreambleStartImpl, +} from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; import type { Destination, @@ -62,13 +65,11 @@ export { writeEndPendingSuspenseBoundary, writeHoistablesForBoundary, writePlaceholder, - writeCompletedRoot, createRootFormatContext, createRenderState, createResumableState, createPreambleState, createHoistableState, - writePreambleStart, writePreambleEnd, writeHoistables, writePostamble, @@ -203,5 +204,30 @@ export function writeEndClientRenderedSuspenseBoundary( return true; } +export function writePreambleStart( + destination: Destination, + resumableState: ResumableState, + renderState: RenderState, + willFlushAllSegments: boolean, + skipExpect?: boolean, // Used as an override by ReactFizzConfigMarkup +): void { + return writePreambleStartImpl( + destination, + resumableState, + renderState, + willFlushAllSegments, + true, // skipExpect + ); +} + +export function writeCompletedRoot( + destination: Destination, + resumableState: ResumableState, + renderState: RenderState, +): boolean { + // Markup doesn't have any bootstrap scripts nor shell completions. + return true; +} + export type TransitionStatus = FormStatus; export const NotPendingTransition: TransitionStatus = NotPending; 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 35b41cbd230d0..6d022ceb26c17 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-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index 0b16b3b32114d..80562624eb173 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -193,7 +193,10 @@ describe('ReactFlightDOM', () => { node.tagName !== 'TEMPLATE' && node.tagName !== 'template' && !node.hasAttribute('hidden') && - !node.hasAttribute('aria-hidden')) + !node.hasAttribute('aria-hidden') && + // Ignore the render blocking expect + (node.getAttribute('rel') !== 'expect' || + node.getAttribute('blocking') !== 'render')) ) { const props = {}; const attributes = node.attributes; @@ -1917,11 +1920,15 @@ describe('ReactFlightDOM', () => { expect(content1).toEqual( '' + - '

hello world

', + '' + + '' + + '

hello world

', ); expect(content2).toEqual( '' + - '

hello world

', + '' + + '' + + '

hello world

', ); }); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index f3fa444fc1528..4313c379b70bd 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -1899,8 +1899,8 @@ describe('ReactFlightDOMBrowser', () => { } expect(content).toEqual( - '' + - '

hello world

', + '' + + '

hello world

', ); }); diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 487751c6be385..52d677ad1be40 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -5157,7 +5157,11 @@ function flushCompletedQueues( ); flushSegment(request, destination, completedRootSegment, null); request.completedRootSegment = null; - writeCompletedRoot(destination, request.renderState); + writeCompletedRoot( + destination, + request.resumableState, + request.renderState, + ); } writeHoistables(destination, request.resumableState, request.renderState); From 0c28a09eefaa0e70a313644fd8e455c8ab7ba3eb Mon Sep 17 00:00:00 2001 From: mofeiZ <34200447+mofeiZ@users.noreply.github.com> Date: Fri, 25 Apr 2025 14:26:59 -0400 Subject: [PATCH 2/2] [ci] Reduce non-deterministic builds for eslint-plugin-react-hooks (#33026) See https://github.com/rollup/plugins/issues/1425 Currently, `@babel/helper-string-parser/lib/index.js` is either emitted as a wrapped esmodule or inline depending on the ordering of async functions in `rollup/commonjs`. Specifically, `@babel/types/lib/definitions/core.js` is cyclic (i.e. transitively depends upon itself), but sometimes `@babel/helper-string-parser/lib/index.js` is emitted before this is realized. A relatively straightforward patch is to wrap all modules (see https://github.com/rollup/plugins/issues/1425#issuecomment-1465626736). This only regresses `eslint-plugin-react-hooks` bundle size by ~1.8% and is safer (see https://github.com/rollup/plugins/blob/master/packages/commonjs/README.md#strictrequires) > The default value of true will wrap all CommonJS files in functions which are executed when they are required for the first time, preserving NodeJS semantics. This is the safest setting and should be used if the generated code does not work correctly with "auto". Note that strictRequires: true can have a small impact on the size and performance of generated code, but less so if the code is minified. (note that we're on an earlier version of `@rollup/commonjs` which does not default to `strictRequires: true`) --- scripts/rollup/build.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/rollup/build.js b/scripts/rollup/build.js index 5cf0518c41833..d745eaed4c04d 100644 --- a/scripts/rollup/build.js +++ b/scripts/rollup/build.js @@ -393,7 +393,8 @@ function getPlugins( }; }, }, - bundle.tsconfig != null ? commonjs() : false, + // See https://github.com/rollup/plugins/issues/1425 + bundle.tsconfig != null ? commonjs({strictRequires: true}) : false, // Shim any modules that need forking in this environment. useForks(forks), // Ensure we don't try to bundle any fbjs modules.