-
Notifications
You must be signed in to change notification settings - Fork 29.8k
Wait for pending Webpack Hot Updates before evaluating JS from RSC responses #67673
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
Changes from 1 commit
6b52a18
36f7d76
6d38386
c9be020
58a3ff7
fe3296d
ccf31b9
0467756
3438909
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
…sponses 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.
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<string> | ||
| ) { | ||
| resolvePendingHotUpdateWebpack() | ||
| dispatcher.onBuildOk() | ||
| reportHmrLatency(sendMessage, updatedModules) | ||
|
|
||
|
|
@@ -159,6 +177,7 @@ function tryApplyUpdates( | |
| dispatcher: Dispatcher | ||
| ) { | ||
| if (!isUpdateAvailable() || !canApplyUpdates()) { | ||
| resolvePendingHotUpdateWebpack() | ||
| dispatcher.onBuildOk() | ||
| reportHmrLatency(sendMessage, []) | ||
| return | ||
|
|
@@ -281,12 +300,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 +343,9 @@ function processMessage( | |
| } | ||
| case HMR_ACTIONS_SENT_TO_BROWSER.BUILDING: { | ||
| startLatency = Date.now() | ||
| if (!process.env.TURBOPACK) { | ||
| setPendingHotUpdateWebpack() | ||
| } | ||
|
Comment on lines
344
to
+348
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The downside here is that we always get a hot update on client-side navigations as far as I can tell. Even if the runtime doesn't get new functionality, it does get updates to "use strict";
/*
* ATTENTION: An "eval-source-map" devtool has been used.
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
self["webpackHotUpdate_N_E"]("webpack",{},
/******/ function(__webpack_require__) { // webpackRuntimeModules
/******/ /* webpack/runtime/getFullHash */
/******/ (() => {
/******/ __webpack_require__.h = () => ("d60fe58ebbed83dc")
/******/ })();
/******/
/******/ }
); |
||
| console.log('[Fast Refresh] rebuilding') | ||
| break | ||
| } | ||
|
|
@@ -426,6 +452,7 @@ function processMessage( | |
| reloading = true | ||
| return window.location.reload() | ||
| } | ||
| resolvePendingHotUpdateWebpack() | ||
| startTransition(() => { | ||
| router.hmrRefresh() | ||
| dispatcher.onRefresh() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,6 +29,7 @@ import { | |
| 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 interface FetchServerResponseOptions { | ||
| readonly flightRouterState: FlightRouterState | ||
|
|
@@ -180,6 +181,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') { | ||
eps1lon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| await waitForWebpackRuntimeHotUpdate() | ||
| } | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess we can safely ignore prefetches here @ztanner?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. right, in dev this function won't see any prefetches
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is however called with
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Which wording are you referring to?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "prefetchKind" being defined even though we're not prefetching. In my head I'm mixing this in with prefetches in the browser e.g. maybe this is just initially consuing because I haven't looked into
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I agree it's a bit confusing. Part of what's confusing is that prefetches are tightly coupled to navigations. A navigation cannot exist without a prefetch (it's what the reducer acts upon when changing the router state), so that's why we have the concept of a "temporary" prefetch. I don't think you're mixing the concepts though,
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The
|
||
|
|
||
| // Handle the `fetch` readable stream that can be unwrapped by `React.use`. | ||
| const response: NavigationFlightResponse = await createFromFetch( | ||
| Promise.resolve(res), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
drive-by rename. We only call this in Webpack implying there's no Fast Refresh in Turbopack. Fast Refresh is also a React feature that's enabled by HMR so it is a bit confusing to invoke that name at all.