diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts
index fcc3c896d694..b2d0ef7e82c2 100644
--- a/packages/next/build/webpack-config.ts
+++ b/packages/next/build/webpack-config.ts
@@ -571,17 +571,29 @@ export default async function getBaseWebpackConfig(
.replace(/\\/g, '/'),
...(config.experimental.appDir
? {
- [CLIENT_STATIC_FILES_RUNTIME_MAIN_ROOT]:
- `./` +
- path
- .relative(
- dir,
- path.join(
- NEXT_PROJECT_ROOT_DIST_CLIENT,
- dev ? 'app-next-dev.js' : 'app-next.js'
+ [CLIENT_STATIC_FILES_RUNTIME_MAIN_ROOT]: dev
+ ? [
+ require.resolve(
+ `next/dist/compiled/@next/react-refresh-utils/dist/runtime`
+ ),
+ `./` +
+ path
+ .relative(
+ dir,
+ path.join(
+ NEXT_PROJECT_ROOT_DIST_CLIENT,
+ 'app-next-dev.js'
+ )
+ )
+ .replace(/\\/g, '/'),
+ ]
+ : `./` +
+ path
+ .relative(
+ dir,
+ path.join(NEXT_PROJECT_ROOT_DIST_CLIENT, 'app-next.js')
)
- )
- .replace(/\\/g, '/'),
+ .replace(/\\/g, '/'),
}
: {}),
} as ClientEntries)
diff --git a/packages/next/build/webpack/loaders/next-app-loader.ts b/packages/next/build/webpack/loaders/next-app-loader.ts
index 82333a9e5a0a..3b8833630c5a 100644
--- a/packages/next/build/webpack/loaders/next-app-loader.ts
+++ b/packages/next/build/webpack/loaders/next-app-loader.ts
@@ -140,6 +140,12 @@ const nextAppLoader: webpack.LoaderDefinitionFunction<{
export const AppRouter = require('next/dist/client/components/app-router.client.js').default
export const LayoutRouter = require('next/dist/client/components/layout-router.client.js').default
+ export const HotReloader = ${
+ // Disable HotReloader component in production
+ this.mode === 'development'
+ ? `require('next/dist/client/components/hot-reloader.client.js').default`
+ : 'null'
+ }
export const hooksClientContext = require('next/dist/client/components/hooks-client-context.js')
export const __next_app_webpack_require__ = __webpack_require__
diff --git a/packages/next/client/app-dev/error-overlay/hot-dev-client.js b/packages/next/client/app-dev/error-overlay/hot-dev-client.js
deleted file mode 100644
index ab646f298d61..000000000000
--- a/packages/next/client/app-dev/error-overlay/hot-dev-client.js
+++ /dev/null
@@ -1,466 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2013-present, Facebook, Inc.
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-// This file is a modified version of the Create React App HMR dev client that
-// can be found here:
-// https://github.com/facebook/create-react-app/blob/v3.4.1/packages/react-dev-utils/webpackHotDevClient.js
-
-import {
- register,
- onBuildError,
- onBuildOk,
- onRefresh,
-} from 'next/dist/compiled/@next/react-dev-overlay/dist/client'
-import stripAnsi from 'next/dist/compiled/strip-ansi'
-import {
- addMessageListener,
- sendMessage,
-} from '../../dev/error-overlay/websocket'
-import formatWebpackMessages from './format-webpack-messages'
-
-// This alternative WebpackDevServer combines the functionality of:
-// https://github.com/webpack/webpack-dev-server/blob/webpack-1/client/index.js
-// https://github.com/webpack/webpack/blob/webpack-1/hot/dev-server.js
-
-// It only supports their simplest configuration (hot updates on same server).
-// It makes some opinionated choices on top, like adding a syntax error overlay
-// that looks similar to our console output. The error overlay is inspired by:
-// https://github.com/glenjamin/webpack-hot-middleware
-
-window.__nextDevClientId = Math.round(Math.random() * 100 + Date.now())
-
-let hadRuntimeError = false
-export default function connect() {
- register()
-
- // Taken from on-demand-entries-client.js
- // TODO: check 404 case
- setInterval(() => {
- sendMessage(
- JSON.stringify({
- event: 'ping',
- // TODO: fix case for dynamic parameters, this will be resolved wrong currently.
- page: window.location.pathname,
- appDirRoute: true,
- })
- )
- }, 2500)
-
- addMessageListener((event) => {
- console.log(event.data)
- if (
- event.data.indexOf('action') === -1 &&
- // TODO: clean this up for consistency
- event.data.indexOf('pong') === -1
- ) {
- return
- }
-
- try {
- processMessage(event)
- } catch (ex) {
- console.warn('Invalid HMR message: ' + event.data + '\n', ex)
- }
- })
-
- return {
- onUnrecoverableError() {
- hadRuntimeError = true
- },
- }
-}
-
-// Remember some state related to hot module replacement.
-var isFirstCompilation = true
-var mostRecentCompilationHash = null
-var hasCompileErrors = false
-
-function clearOutdatedErrors() {
- // Clean up outdated compile errors, if any.
- if (typeof console !== 'undefined' && typeof console.clear === 'function') {
- if (hasCompileErrors) {
- console.clear()
- }
- }
-}
-
-// Successful compilation.
-function handleSuccess() {
- clearOutdatedErrors()
-
- const isHotUpdate =
- !isFirstCompilation ||
- ((!window.__NEXT_DATA__ || window.__NEXT_DATA__.page !== '/_error') &&
- isUpdateAvailable())
- isFirstCompilation = false
- hasCompileErrors = false
-
- // Attempt to apply hot updates or reload.
- if (isHotUpdate) {
- tryApplyUpdates(function onSuccessfulHotUpdate(hasUpdates) {
- // Only dismiss it when we're sure it's a hot update.
- // Otherwise it would flicker right before the reload.
- onFastRefresh(hasUpdates)
- })
- }
-}
-
-// Compilation with warnings (e.g. ESLint).
-function handleWarnings(warnings) {
- clearOutdatedErrors()
-
- const isHotUpdate = !isFirstCompilation
- isFirstCompilation = false
- hasCompileErrors = false
-
- function printWarnings() {
- // Print warnings to the console.
- const formatted = formatWebpackMessages({
- warnings: warnings,
- errors: [],
- })
-
- if (typeof console !== 'undefined' && typeof console.warn === 'function') {
- for (let i = 0; i < formatted.warnings.length; i++) {
- if (i === 5) {
- console.warn(
- 'There were more warnings in other files.\n' +
- 'You can find a complete log in the terminal.'
- )
- break
- }
- console.warn(stripAnsi(formatted.warnings[i]))
- }
- }
- }
-
- printWarnings()
-
- // Attempt to apply hot updates or reload.
- if (isHotUpdate) {
- tryApplyUpdates(function onSuccessfulHotUpdate(hasUpdates) {
- // Only dismiss it when we're sure it's a hot update.
- // Otherwise it would flicker right before the reload.
- onFastRefresh(hasUpdates)
- })
- }
-}
-
-// Compilation with errors (e.g. syntax error or missing modules).
-function handleErrors(errors) {
- clearOutdatedErrors()
-
- isFirstCompilation = false
- hasCompileErrors = true
-
- // "Massage" webpack messages.
- var formatted = formatWebpackMessages({
- errors: errors,
- warnings: [],
- })
-
- // Only show the first error.
- onBuildError(formatted.errors[0])
-
- // Also log them to the console.
- if (typeof console !== 'undefined' && typeof console.error === 'function') {
- for (var i = 0; i < formatted.errors.length; i++) {
- console.error(stripAnsi(formatted.errors[i]))
- }
- }
-
- // Do not attempt to reload now.
- // We will reload on next success instead.
- if (process.env.__NEXT_TEST_MODE) {
- if (self.__NEXT_HMR_CB) {
- self.__NEXT_HMR_CB(formatted.errors[0])
- self.__NEXT_HMR_CB = null
- }
- }
-}
-
-let startLatency = undefined
-
-function onFastRefresh(hasUpdates) {
- onBuildOk()
- if (hasUpdates) {
- onRefresh()
- }
-
- if (startLatency) {
- const endLatency = Date.now()
- const latency = endLatency - startLatency
- console.log(`[Fast Refresh] done in ${latency}ms`)
- sendMessage(
- JSON.stringify({
- event: 'client-hmr-latency',
- id: window.__nextDevClientId,
- startTime: startLatency,
- endTime: endLatency,
- })
- )
- if (self.__NEXT_HMR_LATENCY_CB) {
- self.__NEXT_HMR_LATENCY_CB(latency)
- }
- }
-}
-
-// There is a newer version of the code available.
-function handleAvailableHash(hash) {
- // Update last known compilation hash.
- mostRecentCompilationHash = hash
-}
-
-// Handle messages from the server.
-function processMessage(e) {
- const obj = JSON.parse(e.data)
- if (obj.event && obj.event === 'pong') obj.action = 'pong'
-
- switch (obj.action) {
- case 'building': {
- startLatency = Date.now()
- console.log('[Fast Refresh] rebuilding')
- break
- }
- case 'built':
- case 'sync': {
- if (obj.hash) {
- handleAvailableHash(obj.hash)
- }
-
- const { errors, warnings } = obj
- const hasErrors = Boolean(errors && errors.length)
- if (hasErrors) {
- sendMessage(
- JSON.stringify({
- event: 'client-error',
- errorCount: errors.length,
- clientId: window.__nextDevClientId,
- })
- )
- return handleErrors(errors)
- }
-
- const hasWarnings = Boolean(warnings && warnings.length)
- if (hasWarnings) {
- sendMessage(
- JSON.stringify({
- event: 'client-warning',
- warningCount: warnings.length,
- clientId: window.__nextDevClientId,
- })
- )
- return handleWarnings(warnings)
- }
-
- sendMessage(
- JSON.stringify({
- event: 'client-success',
- clientId: window.__nextDevClientId,
- })
- )
- return handleSuccess()
- }
- case 'reloadPage': {
- sendMessage(
- JSON.stringify({
- event: 'client-reload-page',
- clientId: window.__nextDevClientId,
- })
- )
- return window.location.reload()
- }
- case 'removedPage': {
- const [page] = obj.data
- if (page === window.next.router.pathname) {
- sendMessage(
- JSON.stringify({
- event: 'client-removed-page',
- clientId: window.__nextDevClientId,
- page,
- })
- )
- return window.location.reload()
- }
- return
- }
- case 'addedPage': {
- const [page] = obj.data
- if (
- page === window.next.router.pathname &&
- typeof window.next.router.components[page] === 'undefined'
- ) {
- sendMessage(
- JSON.stringify({
- event: 'client-added-page',
- clientId: window.__nextDevClientId,
- page,
- })
- )
- return window.location.reload()
- }
- return
- }
- case 'pong': {
- const { invalid } = obj
- if (invalid) {
- // Payload can be invalid even if the page does exist.
- // So, we check if it can be created.
- fetch(location.href, {
- credentials: 'same-origin',
- }).then((pageRes) => {
- if (pageRes.status === 200) {
- // Page exists now, reload
- location.reload()
- } else {
- // TODO: fix this
- // Page doesn't exist
- // if (
- // self.__NEXT_DATA__.page === Router.pathname &&
- // Router.pathname !== '/_error'
- // ) {
- // // We are still on the page,
- // // reload to show 404 error page
- // location.reload()
- // }
- }
- })
- }
- return
- }
- default: {
- throw new Error('Unexpected action ' + obj.action)
- }
- }
-}
-
-// Is there a newer version of this code available?
-function isUpdateAvailable() {
- /* globals __webpack_hash__ */
- // __webpack_hash__ is the hash of the current compilation.
- // It's a global variable injected by Webpack.
- return mostRecentCompilationHash !== __webpack_hash__
-}
-
-// Webpack disallows updates in other states.
-function canApplyUpdates() {
- return module.hot.status() === 'idle'
-}
-function afterApplyUpdates(fn) {
- if (canApplyUpdates()) {
- fn()
- } else {
- function handler(status) {
- if (status === 'idle') {
- module.hot.removeStatusHandler(handler)
- fn()
- }
- }
- module.hot.addStatusHandler(handler)
- }
-}
-
-// Attempt to update code on the fly, fall back to a hard reload.
-function tryApplyUpdates(onHotUpdateSuccess) {
- if (!module.hot) {
- // HotModuleReplacementPlugin is not in Webpack configuration.
- console.error('HotModuleReplacementPlugin is not in Webpack configuration.')
- // window.location.reload();
- return
- }
-
- if (!isUpdateAvailable() || !canApplyUpdates()) {
- onBuildOk()
- return
- }
-
- function handleApplyUpdates(err, updatedModules) {
- if (err || hadRuntimeError || !updatedModules) {
- if (err) {
- console.warn(
- '[Fast Refresh] performing full reload\n\n' +
- "Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree.\n" +
- 'You might have a file which exports a React component but also exports a value that is imported by a non-React component file.\n' +
- 'Consider migrating the non-React component export to a separate file and importing it into both files.\n\n' +
- 'It is also possible the parent component of the component you edited is a class component, which disables Fast Refresh.\n' +
- 'Fast Refresh requires at least one parent function component in your React tree.'
- )
- } else if (hadRuntimeError) {
- console.warn(
- '[Fast Refresh] performing full reload because your application had an unrecoverable error'
- )
- }
- performFullReload(err)
- return
- }
-
- const hasUpdates = Boolean(updatedModules.length)
- if (typeof onHotUpdateSuccess === 'function') {
- // Maybe we want to do something.
- onHotUpdateSuccess(hasUpdates)
- }
-
- if (isUpdateAvailable()) {
- // While we were updating, there was a new update! Do it again.
- tryApplyUpdates(hasUpdates ? onBuildOk : onHotUpdateSuccess)
- } else {
- onBuildOk()
- if (process.env.__NEXT_TEST_MODE) {
- afterApplyUpdates(() => {
- if (self.__NEXT_HMR_CB) {
- self.__NEXT_HMR_CB()
- self.__NEXT_HMR_CB = null
- }
- })
- }
- }
- }
-
- // https://webpack.js.org/api/hot-module-replacement/#check
- module.hot.check(/* autoApply */ true).then(
- (updatedModules) => {
- handleApplyUpdates(null, updatedModules)
- },
- (err) => {
- handleApplyUpdates(err, null)
- }
- )
-}
-
-function performFullReload(err) {
- const stackTrace =
- err &&
- ((err.stack && err.stack.split('\n').slice(0, 5).join('\n')) ||
- err.message ||
- err + '')
-
- sendMessage(
- JSON.stringify({
- event: 'client-full-reload',
- stackTrace,
- })
- )
-
- window.location.reload()
-}
diff --git a/packages/next/client/app-index.tsx b/packages/next/client/app-index.tsx
index e545e546c138..13ba7f0bed17 100644
--- a/packages/next/client/app-index.tsx
+++ b/packages/next/client/app-index.tsx
@@ -159,7 +159,7 @@ function useInitialServerResponse(cacheKey: string) {
return newResponse
}
-const ServerRoot = ({ cacheKey }: { cacheKey: string }) => {
+function ServerRoot({ cacheKey }: { cacheKey: string }) {
React.useEffect(() => {
rscCache.delete(cacheKey)
})
@@ -168,6 +168,19 @@ const ServerRoot = ({ cacheKey }: { cacheKey: string }) => {
return root
}
+function ErrorOverlay({
+ children,
+}: React.PropsWithChildren<{}>): React.ReactElement {
+ if (process.env.NODE_ENV === 'production') {
+ return <>{children}>
+ } else {
+ const {
+ ReactDevOverlay,
+ } = require('next/dist/compiled/@next/react-dev-overlay/dist/client')
+ return {children}
+ }
+}
+
function Root({ children }: React.PropsWithChildren<{}>): React.ReactElement {
if (process.env.__NEXT_TEST_MODE) {
// eslint-disable-next-line react-hooks/rules-of-hooks
@@ -183,17 +196,19 @@ function Root({ children }: React.PropsWithChildren<{}>): React.ReactElement {
return children as React.ReactElement
}
-const RSCComponent = () => {
+function RSCComponent() {
const cacheKey = getCacheKey()
return
}
export function hydrate() {
renderReactElement(appElement!, () => (
-
-
-
-
-
+
+
+
+
+
+
+
))
}
diff --git a/packages/next/client/app-next-dev.js b/packages/next/client/app-next-dev.js
index 395b66030754..842c553d801e 100644
--- a/packages/next/client/app-next-dev.js
+++ b/packages/next/client/app-next-dev.js
@@ -1,8 +1,4 @@
import { hydrate, version } from './app-index'
-import {
- connectHMR /*, addMessageListener */,
-} from './dev/error-overlay/websocket'
-import connect from './app-dev/error-overlay/hot-dev-client'
// TODO: implement FOUC guard
@@ -13,10 +9,6 @@ window.next = {
appDir: true,
}
-// TODO: implement assetPrefix support
-connectHMR({ assetPrefix: '', path: '/_next/webpack-hmr', log: true })
-connect()
-
hydrate()
// TODO: build indicator
diff --git a/packages/next/client/components/app-router.client.tsx b/packages/next/client/components/app-router.client.tsx
index edd452e5bab3..5bca0a94c4ae 100644
--- a/packages/next/client/components/app-router.client.tsx
+++ b/packages/next/client/components/app-router.client.tsx
@@ -50,10 +50,12 @@ export default function AppRouter({
initialTree,
initialCanonicalUrl,
children,
+ hotReloader,
}: {
initialTree: FlightRouterState
initialCanonicalUrl: string
children: React.ReactNode
+ hotReloader?: React.ReactNode
}) {
const [{ tree, cache, pushRef, canonicalUrl }, dispatch] = React.useReducer<
typeof reducer
@@ -238,6 +240,7 @@ export default function AppRouter({
}}
>
{children}
+ {hotReloader}
diff --git a/packages/next/client/components/hot-reloader.client.tsx b/packages/next/client/components/hot-reloader.client.tsx
new file mode 100644
index 000000000000..090dd05b3995
--- /dev/null
+++ b/packages/next/client/components/hot-reloader.client.tsx
@@ -0,0 +1,454 @@
+import { useCallback, useContext, useEffect, useRef } from 'react'
+import { FullAppTreeContext } from '../../shared/lib/app-router-context'
+import {
+ register,
+ onBuildError,
+ onBuildOk,
+ onRefresh,
+} from 'next/dist/compiled/@next/react-dev-overlay/dist/client'
+import stripAnsi from 'next/dist/compiled/strip-ansi'
+import formatWebpackMessages from '../dev/error-overlay/format-webpack-messages'
+
+function getSocketProtocol(assetPrefix: string): string {
+ let protocol = window.location.protocol
+
+ try {
+ // assetPrefix is a url
+ protocol = new URL(assetPrefix).protocol
+ } catch (_) {}
+
+ return protocol === 'http:' ? 'ws' : 'wss'
+}
+
+// const TIMEOUT = 5000
+
+// TODO: add actual type
+type PongEvent = any
+
+let mostRecentCompilationHash: any = null
+let __nextDevClientId = Math.round(Math.random() * 100 + Date.now())
+let hadRuntimeError = false
+
+// let startLatency = undefined
+
+function onFastRefresh(hasUpdates: boolean) {
+ onBuildOk()
+ if (hasUpdates) {
+ onRefresh()
+ }
+
+ // if (startLatency) {
+ // const endLatency = Date.now()
+ // const latency = endLatency - startLatency
+ // console.log(`[Fast Refresh] done in ${latency}ms`)
+ // sendMessage(
+ // JSON.stringify({
+ // event: 'client-hmr-latency',
+ // id: __nextDevClientId,
+ // startTime: startLatency,
+ // endTime: endLatency,
+ // })
+ // )
+ // // if (self.__NEXT_HMR_LATENCY_CB) {
+ // // self.__NEXT_HMR_LATENCY_CB(latency)
+ // // }
+ // }
+}
+
+// There is a newer version of the code available.
+function handleAvailableHash(hash: string) {
+ // Update last known compilation hash.
+ mostRecentCompilationHash = hash
+}
+
+// Is there a newer version of this code available?
+function isUpdateAvailable() {
+ /* globals __webpack_hash__ */
+ // __webpack_hash__ is the hash of the current compilation.
+ // It's a global variable injected by Webpack.
+ // @ts-expect-error __webpack_hash__ exists
+ return mostRecentCompilationHash !== __webpack_hash__
+}
+
+// Webpack disallows updates in other states.
+function canApplyUpdates() {
+ // @ts-expect-error module.hot exists
+ return module.hot.status() === 'idle'
+}
+// function afterApplyUpdates(fn: any) {
+// if (canApplyUpdates()) {
+// fn()
+// } else {
+// function handler(status: any) {
+// if (status === 'idle') {
+// // @ts-expect-error module.hot exists
+// module.hot.removeStatusHandler(handler)
+// fn()
+// }
+// }
+// // @ts-expect-error module.hot exists
+// module.hot.addStatusHandler(handler)
+// }
+// }
+
+// Attempt to update code on the fly, fall back to a hard reload.
+function tryApplyUpdates(onHotUpdateSuccess: any, sendMessage: any) {
+ // @ts-expect-error module.hot exists
+ if (!module.hot) {
+ // HotModuleReplacementPlugin is not in Webpack configuration.
+ console.error('HotModuleReplacementPlugin is not in Webpack configuration.')
+ // window.location.reload();
+ return
+ }
+
+ if (!isUpdateAvailable() || !canApplyUpdates()) {
+ onBuildOk()
+ return
+ }
+
+ function handleApplyUpdates(err: any, updatedModules: any) {
+ if (err || hadRuntimeError || !updatedModules) {
+ if (err) {
+ console.warn(
+ '[Fast Refresh] performing full reload\n\n' +
+ "Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree.\n" +
+ 'You might have a file which exports a React component but also exports a value that is imported by a non-React component file.\n' +
+ 'Consider migrating the non-React component export to a separate file and importing it into both files.\n\n' +
+ 'It is also possible the parent component of the component you edited is a class component, which disables Fast Refresh.\n' +
+ 'Fast Refresh requires at least one parent function component in your React tree.'
+ )
+ } else if (hadRuntimeError) {
+ console.warn(
+ '[Fast Refresh] performing full reload because your application had an unrecoverable error'
+ )
+ }
+ performFullReload(err, sendMessage)
+ return
+ }
+
+ const hasUpdates = Boolean(updatedModules.length)
+ if (typeof onHotUpdateSuccess === 'function') {
+ // Maybe we want to do something.
+ onHotUpdateSuccess(hasUpdates)
+ }
+
+ if (isUpdateAvailable()) {
+ // While we were updating, there was a new update! Do it again.
+ tryApplyUpdates(hasUpdates ? onBuildOk : onHotUpdateSuccess, sendMessage)
+ } else {
+ onBuildOk()
+ // if (process.env.__NEXT_TEST_MODE) {
+ // afterApplyUpdates(() => {
+ // if (self.__NEXT_HMR_CB) {
+ // self.__NEXT_HMR_CB()
+ // self.__NEXT_HMR_CB = null
+ // }
+ // })
+ // }
+ }
+ }
+
+ // https://webpack.js.org/api/hot-module-replacement/#check
+ // @ts-expect-error module.hot exists
+ module.hot.check(/* autoApply */ true).then(
+ (updatedModules: any) => {
+ handleApplyUpdates(null, updatedModules)
+ },
+ (err: any) => {
+ handleApplyUpdates(err, null)
+ }
+ )
+}
+
+function performFullReload(err: any, sendMessage: any) {
+ const stackTrace =
+ err &&
+ ((err.stack && err.stack.split('\n').slice(0, 5).join('\n')) ||
+ err.message ||
+ err + '')
+
+ sendMessage(
+ JSON.stringify({
+ event: 'client-full-reload',
+ stackTrace,
+ })
+ )
+
+ window.location.reload()
+}
+
+function processMessage(e: any, sendMessage: any) {
+ const obj = JSON.parse(e.data)
+
+ switch (obj.action) {
+ case 'building': {
+ // startLatency = Date.now()
+ console.log('[Fast Refresh] rebuilding')
+ break
+ }
+ case 'built':
+ case 'sync': {
+ if (obj.hash) {
+ handleAvailableHash(obj.hash)
+ }
+
+ const { errors, warnings } = obj
+ const hasErrors = Boolean(errors && errors.length)
+ // Compilation with errors (e.g. syntax error or missing modules).
+ if (hasErrors) {
+ sendMessage(
+ JSON.stringify({
+ event: 'client-error',
+ errorCount: errors.length,
+ clientId: __nextDevClientId,
+ })
+ )
+
+ // "Massage" webpack messages.
+ var formatted = formatWebpackMessages({
+ errors: errors,
+ warnings: [],
+ })
+
+ // Only show the first error.
+ onBuildError(formatted.errors[0])
+
+ // Also log them to the console.
+ for (let i = 0; i < formatted.errors.length; i++) {
+ console.error(stripAnsi(formatted.errors[i]))
+ }
+
+ // Do not attempt to reload now.
+ // We will reload on next success instead.
+ // if (process.env.__NEXT_TEST_MODE) {
+ // if (self.__NEXT_HMR_CB) {
+ // self.__NEXT_HMR_CB(formatted.errors[0])
+ // self.__NEXT_HMR_CB = null
+ // }
+ // }
+ return
+ }
+
+ const hasWarnings = Boolean(warnings && warnings.length)
+ if (hasWarnings) {
+ sendMessage(
+ JSON.stringify({
+ event: 'client-warning',
+ warningCount: warnings.length,
+ clientId: __nextDevClientId,
+ })
+ )
+
+ // Compilation with warnings (e.g. ESLint).
+ const isHotUpdate = obj.action !== 'sync'
+
+ // Print warnings to the console.
+ const formattedMessages = formatWebpackMessages({
+ warnings: warnings,
+ errors: [],
+ })
+
+ for (let i = 0; i < formattedMessages.warnings.length; i++) {
+ if (i === 5) {
+ console.warn(
+ 'There were more warnings in other files.\n' +
+ 'You can find a complete log in the terminal.'
+ )
+ break
+ }
+ console.warn(stripAnsi(formattedMessages.warnings[i]))
+ }
+
+ // Attempt to apply hot updates or reload.
+ if (isHotUpdate) {
+ tryApplyUpdates(function onSuccessfulHotUpdate(hasUpdates: any) {
+ // Only dismiss it when we're sure it's a hot update.
+ // Otherwise it would flicker right before the reload.
+ onFastRefresh(hasUpdates)
+ }, sendMessage)
+ }
+ return
+ }
+
+ sendMessage(
+ JSON.stringify({
+ event: 'client-success',
+ clientId: __nextDevClientId,
+ })
+ )
+
+ const isHotUpdate =
+ obj.action !== 'sync' ||
+ ((!window.__NEXT_DATA__ || window.__NEXT_DATA__.page !== '/_error') &&
+ isUpdateAvailable())
+
+ // Attempt to apply hot updates or reload.
+ if (isHotUpdate) {
+ tryApplyUpdates(function onSuccessfulHotUpdate(hasUpdates: any) {
+ // Only dismiss it when we're sure it's a hot update.
+ // Otherwise it would flicker right before the reload.
+ onFastRefresh(hasUpdates)
+ }, sendMessage)
+ }
+ return
+ }
+ case 'reloadPage': {
+ sendMessage(
+ JSON.stringify({
+ event: 'client-reload-page',
+ clientId: __nextDevClientId,
+ })
+ )
+ return window.location.reload()
+ }
+ case 'removedPage': {
+ // const [page] = obj.data
+ // if (page === window.next.router.pathname) {
+ // sendMessage(
+ // JSON.stringify({
+ // event: 'client-removed-page',
+ // clientId: window.__nextDevClientId,
+ // page,
+ // })
+ // )
+ // return window.location.reload()
+ // }
+ return
+ }
+ case 'addedPage': {
+ // const [page] = obj.data
+ // if (
+ // page === window.next.router.pathname &&
+ // typeof window.next.router.components[page] === 'undefined'
+ // ) {
+ // sendMessage(
+ // JSON.stringify({
+ // event: 'client-added-page',
+ // clientId: window.__nextDevClientId,
+ // page,
+ // })
+ // )
+ // return window.location.reload()
+ // }
+ return
+ }
+ case 'pong': {
+ const { invalid } = obj
+ if (invalid) {
+ // Payload can be invalid even if the page does exist.
+ // So, we check if it can be created.
+ fetch(location.href, {
+ credentials: 'same-origin',
+ }).then((pageRes) => {
+ if (pageRes.status === 200) {
+ // Page exists now, reload
+ location.reload()
+ } else {
+ // TODO: fix this
+ // Page doesn't exist
+ // if (
+ // self.__NEXT_DATA__.page === Router.pathname &&
+ // Router.pathname !== '/_error'
+ // ) {
+ // // We are still on the page,
+ // // reload to show 404 error page
+ // location.reload()
+ // }
+ }
+ })
+ }
+ return
+ }
+ default: {
+ throw new Error('Unexpected action ' + obj.action)
+ }
+ }
+}
+
+export default function HotReload({ assetPrefix }: { assetPrefix: string }) {
+ const { tree } = useContext(FullAppTreeContext)
+
+ const webSocketRef = useRef()
+ const sendMessage = useCallback((data) => {
+ const socket = webSocketRef.current
+ if (!socket || socket.readyState !== socket.OPEN) return
+ return socket.send(data)
+ }, [])
+
+ useEffect(() => {
+ register()
+ }, [])
+
+ useEffect(() => {
+ if (webSocketRef.current) {
+ return
+ }
+
+ const { hostname, port } = window.location
+ const protocol = getSocketProtocol(assetPrefix || '')
+ const normalizedAssetPrefix = assetPrefix.replace(/^\/+/, '')
+
+ let url = `${protocol}://${hostname}:${port}${
+ normalizedAssetPrefix ? `/${normalizedAssetPrefix}` : ''
+ }`
+
+ if (normalizedAssetPrefix.startsWith('http')) {
+ url = `${protocol}://${normalizedAssetPrefix.split('://')[1]}`
+ }
+
+ webSocketRef.current = new window.WebSocket(`${url}/_next/webpack-hmr`)
+ }, [assetPrefix])
+ useEffect(() => {
+ // Taken from on-demand-entries-client.js
+ // TODO: check 404 case
+ const interval = setInterval(() => {
+ sendMessage(
+ JSON.stringify({
+ event: 'ping',
+ // TODO: fix case for dynamic parameters, this will be resolved wrong currently.
+ tree,
+ appDirRoute: true,
+ })
+ )
+ }, 2500)
+ return () => clearInterval(interval)
+ }, [tree, sendMessage])
+ useEffect(() => {
+ const handler = (event: MessageEvent) => {
+ if (
+ event.data.indexOf('action') === -1 &&
+ // TODO: clean this up for consistency
+ event.data.indexOf('pong') === -1
+ ) {
+ return
+ }
+
+ try {
+ processMessage(event, sendMessage)
+ } catch (ex) {
+ console.warn('Invalid HMR message: ' + event.data + '\n', ex)
+ }
+ }
+
+ if (webSocketRef.current) {
+ webSocketRef.current.addEventListener('message', handler)
+ }
+
+ return () =>
+ webSocketRef.current &&
+ webSocketRef.current.removeEventListener('message', handler)
+ }, [sendMessage])
+ // useEffect(() => {
+ // const interval = setInterval(function () {
+ // if (
+ // lastActivityRef.current &&
+ // Date.now() - lastActivityRef.current > TIMEOUT
+ // ) {
+ // handleDisconnect()
+ // }
+ // }, 2500)
+
+ // return () => clearInterval(interval)
+ // })
+ return null
+}
diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx
index a7843dd646d2..c9b8c571eed2 100644
--- a/packages/next/server/app-render.tsx
+++ b/packages/next/server/app-render.tsx
@@ -377,6 +377,9 @@ export async function renderToHTML(
const LayoutRouter =
ComponentMod.LayoutRouter as typeof import('../client/components/layout-router.client').default
+ const HotReloader = ComponentMod.HotReloader as
+ | typeof import('../client/components/hot-reloader.client').default
+ | null
const headers = req.headers
// @ts-expect-error TODO: fix type of req
@@ -746,7 +749,7 @@ export async function renderToHTML(
const search = stringifyQuery(query)
// TODO: validate req.url as it gets passed to render.
- const initialCanonicalUrl = req.url
+ const initialCanonicalUrl = req.url!
// TODO: change tree to accommodate this
// /blog/[...slug]/page.js -> /blog/hello-world/b/c/d -> ['children', 'blog', 'children', ['slug', 'hello-world/b/c/d']]
@@ -760,7 +763,8 @@ export async function renderToHTML(
firstItem: true,
})
- const AppRouter = ComponentMod.AppRouter
+ const AppRouter =
+ ComponentMod.AppRouter as typeof import('../client/components/app-router.client').default
const {
QueryContext,
PathnameContext,
@@ -774,6 +778,7 @@ export async function renderToHTML(
{/* */}
}
initialCanonicalUrl={initialCanonicalUrl}
initialTree={initialTree}
>
diff --git a/packages/next/server/dev/on-demand-entry-handler.ts b/packages/next/server/dev/on-demand-entry-handler.ts
index 35ca2ad02c95..41451552f661 100644
--- a/packages/next/server/dev/on-demand-entry-handler.ts
+++ b/packages/next/server/dev/on-demand-entry-handler.ts
@@ -15,6 +15,56 @@ import { serverComponentRegex } from '../../build/webpack/loaders/utils'
import { getPageStaticInfo } from '../../build/analysis/get-page-static-info'
import { isMiddlewareFile, isMiddlewareFilename } from '../../build/utils'
import { PageNotFoundError } from '../../shared/lib/utils'
+import { FlightRouterState } from '../app-render'
+
+function treePathToEntrypoint(
+ segmentPath: string[],
+ parentPath?: string
+): string {
+ const [parallelRouteKey, segment] = segmentPath
+
+ const path =
+ (parentPath ? parentPath + '/' : '') +
+ (parallelRouteKey !== 'children' ? parallelRouteKey + '/' : '') +
+ (segment === '' ? 'page' : segment)
+
+ // Last segment
+ if (segmentPath.length === 2) {
+ return path
+ }
+
+ const childSegmentPath = segmentPath.slice(2)
+ return treePathToEntrypoint(childSegmentPath, path)
+}
+
+function getEntrypointsFromTree(
+ tree: FlightRouterState,
+ isFirst: boolean,
+ parentPath: string[] = []
+) {
+ const [segment, parallelRoutes] = tree
+
+ const currentSegment = Array.isArray(segment) ? segment[0] : segment
+
+ const currentPath = [...parentPath, currentSegment]
+
+ if (!isFirst && currentSegment === '') {
+ // TODO get rid of '' at the start of tree
+ return [treePathToEntrypoint(currentPath.slice(1))]
+ }
+
+ return Object.keys(parallelRoutes).reduce(
+ (paths: string[], key: string): string[] => {
+ const childTree = parallelRoutes[key]
+ const childPages = getEntrypointsFromTree(childTree, false, [
+ ...currentPath,
+ key,
+ ])
+ return [...paths, ...childPages]
+ },
+ []
+ )
+}
export const ADDED = Symbol('added')
export const BUILDING = Symbol('building')
@@ -161,11 +211,13 @@ export function onDemandEntryHandler({
)
}, pingIntervalTime + 1000).unref()
- function handlePing(pg: string, appDirRoute?: true) {
- if (appDirRoute) {
- const page = normalizePathSep(pg)
- // TODO: fix this
- const pageKey = `server${page}/page`
+ function handleAppDirPing(
+ tree: FlightRouterState
+ ): { success: true } | { invalid: true } {
+ const pages = getEntrypointsFromTree(tree, true)
+
+ for (const page of pages) {
+ const pageKey = `server/${page}`
const entryInfo = entries[pageKey]
// If there's no entry, it may have been invalidated and needs to be re-built.
@@ -175,22 +227,26 @@ export function onDemandEntryHandler({
}
// We don't need to maintain active state of anything other than BUILT entries
- if (entryInfo.status !== BUILT) return
+ if (entryInfo.status !== BUILT) continue
// If there's an entryInfo
if (!lastServerAccessPagesForAppDir.includes(pageKey)) {
lastServerAccessPagesForAppDir.unshift(pageKey)
// Maintain the buffer max length
+ // TODO: verify that the current pageKey is not at the end of the array as multiple entrypoints can exist
if (lastServerAccessPagesForAppDir.length > pagesBufferLength) {
lastServerAccessPagesForAppDir.pop()
}
}
entryInfo.lastActiveTime = Date.now()
entryInfo.dispose = false
- return { success: true }
}
+ return { success: true }
+ }
+
+ function handlePing(pg: string) {
const page = normalizePathSep(pg)
const pageKey = `client${page}`
const entryInfo = entries[pageKey]
@@ -306,11 +362,13 @@ export function onDemandEntryHandler({
)
if (parsedData.event === 'ping') {
- const result = handlePing(parsedData.page, parsedData.appDirRoute)
+ const result = parsedData.appDirRoute
+ ? handleAppDirPing(parsedData.tree)
+ : handlePing(parsedData.page)
client.send(
JSON.stringify({
...result,
- event: 'pong',
+ [parsedData.appDirRoute ? 'action' : 'event']: 'pong',
})
)
}
@@ -363,7 +421,7 @@ function disposeInactiveEntries(
})
}
-// Make sure only one invalidation happens at a time
+// Make sure only one invalidation happens at a timeā«
// Otherwise, webpack hash gets changed and it'll force the client to reload.
class Invalidator {
private multiCompiler: webpack.MultiCompiler
diff --git a/packages/react-dev-overlay/src/internal/ReactDevOverlay.tsx b/packages/react-dev-overlay/src/internal/ReactDevOverlay.tsx
index 1d2131021195..fe2fe056ca2c 100644
--- a/packages/react-dev-overlay/src/internal/ReactDevOverlay.tsx
+++ b/packages/react-dev-overlay/src/internal/ReactDevOverlay.tsx
@@ -47,9 +47,11 @@ type ErrorType = 'runtime' | 'build'
const ReactDevOverlay: React.FunctionComponent = function ReactDevOverlay({
children,
preventDisplay,
+ globalOverlay,
}: {
children?: React.ReactNode
preventDisplay?: ErrorType[]
+ globalOverlay?: boolean
}) {
const [state, dispatch] = React.useReducer<
React.Reducer
@@ -84,7 +86,7 @@ const ReactDevOverlay: React.FunctionComponent = function ReactDevOverlay({
{children ?? null}
{isMounted ? (
-
+
diff --git a/packages/react-dev-overlay/src/internal/components/ShadowPortal.tsx b/packages/react-dev-overlay/src/internal/components/ShadowPortal.tsx
index 61b7391c58ad..e3a418a11443 100644
--- a/packages/react-dev-overlay/src/internal/components/ShadowPortal.tsx
+++ b/packages/react-dev-overlay/src/internal/components/ShadowPortal.tsx
@@ -3,10 +3,12 @@ import { createPortal } from 'react-dom'
export type ShadowPortalProps = {
children: React.ReactNode
+ globalOverlay?: boolean
}
export const ShadowPortal: React.FC = function Portal({
children,
+ globalOverlay,
}) {
let mountNode = React.useRef(null)
let portalNode = React.useRef(null)
@@ -14,7 +16,9 @@ export const ShadowPortal: React.FC = function Portal({
let [, forceUpdate] = React.useState<{} | undefined>()
React.useLayoutEffect(() => {
- const ownerDocument = mountNode.current!.ownerDocument!
+ const ownerDocument = globalOverlay
+ ? document
+ : mountNode.current!.ownerDocument!
portalNode.current = ownerDocument.createElement('nextjs-portal')
shadowNode.current = portalNode.current.attachShadow({ mode: 'open' })
ownerDocument.body.appendChild(portalNode.current)
@@ -28,7 +32,7 @@ export const ShadowPortal: React.FC = function Portal({
return shadowNode.current ? (
createPortal(children, shadowNode.current as any)
- ) : (
+ ) : globalOverlay ? null : (
)
}