From 734e292c10a9ac806b6b5b131110ae27d3c1a323 Mon Sep 17 00:00:00 2001 From: eps1lon Date: Thu, 11 Jul 2024 18:15:16 +0200 Subject: [PATCH] Wait for pending Webpack Hot Updates before evaluating JS from RSC responses The Webpack runtime always tries to be minimal. Since all pages share the same chunk, the prod runtime supports all pages. However, in dev, the webpack runtime only supports the current page. If we navigate to a new page, the Webpack runtime may need more functionality. Previously, we eagerly evaluated the RSC payload before any Hot Update was applied. This could lead to runtime errors. For RSC payloads specifically, we just fell back to an MPA navigation. We could continue to rely on this fallback. It may be disorienting though since we flash the error toast. We would also need to adjust this logic since `createFromFetch` no longer throws in these cases in later React version and instead lets the nearest Error Boundary handle these cases. --- .../app/hot-reloader-client.tsx | 33 ++++++++++++++++--- .../router-reducer/fetch-server-response.ts | 9 +++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/packages/next/src/client/components/react-dev-overlay/app/hot-reloader-client.tsx b/packages/next/src/client/components/react-dev-overlay/app/hot-reloader-client.tsx index 39be81f513406..c742fed14230d 100644 --- a/packages/next/src/client/components/react-dev-overlay/app/hot-reloader-client.tsx +++ b/packages/next/src/client/components/react-dev-overlay/app/hot-reloader-client.tsx @@ -47,17 +47,35 @@ let __nextDevClientId = Math.round(Math.random() * 100 + Date.now()) let reloading = false let startLatency: number | null = null -function onBeforeFastRefresh(dispatcher: Dispatcher, hasUpdates: boolean) { +let pendingHotUpdateWebpack = Promise.resolve() +let resolvePendingHotUpdateWebpack: () => void = () => {} +function setPendingHotUpdateWebpack() { + pendingHotUpdateWebpack = new Promise((resolve) => { + resolvePendingHotUpdateWebpack = () => { + resolve() + } + }) +} + +export function waitForWebpackRuntimeHotUpdate() { + return pendingHotUpdateWebpack +} + +function handleBeforeHotUpdateWebpack( + dispatcher: Dispatcher, + hasUpdates: boolean +) { if (hasUpdates) { dispatcher.onBeforeRefresh() } } -function onFastRefresh( +function handleSuccessfulHotUpdateWebpack( dispatcher: Dispatcher, sendMessage: (message: string) => void, updatedModules: ReadonlyArray ) { + resolvePendingHotUpdateWebpack() dispatcher.onBuildOk() reportHmrLatency(sendMessage, updatedModules) @@ -281,12 +299,16 @@ function processMessage( } else { tryApplyUpdates( function onBeforeHotUpdate(hasUpdates: boolean) { - onBeforeFastRefresh(dispatcher, hasUpdates) + handleBeforeHotUpdateWebpack(dispatcher, hasUpdates) }, function onSuccessfulHotUpdate(webpackUpdatedModules: string[]) { // Only dismiss it when we're sure it's a hot update. // Otherwise it would flicker right before the reload. - onFastRefresh(dispatcher, sendMessage, webpackUpdatedModules) + handleSuccessfulHotUpdateWebpack( + dispatcher, + sendMessage, + webpackUpdatedModules + ) }, sendMessage, dispatcher @@ -320,6 +342,9 @@ function processMessage( } case HMR_ACTIONS_SENT_TO_BROWSER.BUILDING: { startLatency = Date.now() + if (!process.env.TURBOPACK) { + setPendingHotUpdateWebpack() + } console.log('[Fast Refresh] rebuilding') break } diff --git a/packages/next/src/client/components/router-reducer/fetch-server-response.ts b/packages/next/src/client/components/router-reducer/fetch-server-response.ts index f51cc73155677..d53af9d632edf 100644 --- a/packages/next/src/client/components/router-reducer/fetch-server-response.ts +++ b/packages/next/src/client/components/router-reducer/fetch-server-response.ts @@ -29,6 +29,7 @@ import { urlToUrlWithoutFlightMarker } from '../app-router' import { callServer } from '../../app-call-server' import { PrefetchKind } from './router-reducer-types' import { hexHash } from '../../../shared/lib/hash' +import { waitForWebpackRuntimeHotUpdate } from '../react-dev-overlay/app/hot-reloader-client' export type FetchServerResponseResult = [ flightData: FlightData, @@ -152,6 +153,14 @@ export async function fetchServerResponse( return doMpaNavigation(responseUrl.toString()) } + // We may navigate to a page that requires a different Webpack runtime. + // In prod, every page will have the same Webpack runtime. + // In dev, the Webpack runtime is minimal for each page. + // We need to ensure the Webpack runtime is updated before executing client-side JS of the new page. + if (process.env.NODE_ENV !== 'production') { + await waitForWebpackRuntimeHotUpdate() + } + // Handle the `fetch` readable stream that can be unwrapped by `React.use`. const [buildId, flightData]: NextFlightResponse = await createFromFetch( Promise.resolve(res),