diff --git a/packages/next/src/client/app-index.tsx b/packages/next/src/client/app-index.tsx
index abdf69f321702..f1c1065048b3d 100644
--- a/packages/next/src/client/app-index.tsx
+++ b/packages/next/src/client/app-index.tsx
@@ -260,7 +260,7 @@ export function hydrate(
const { createWebSocket } =
require('./dev/hot-reloader/app/web-socket') as typeof import('./dev/hot-reloader/app/web-socket')
- staticIndicatorState = { pathname: null, appIsrManifest: {} }
+ staticIndicatorState = { pathname: null, appIsrManifest: null }
webSocket = createWebSocket(assetPrefix, staticIndicatorState)
}
diff --git a/packages/next/src/client/dev/hot-reloader/app/hot-reloader-app.tsx b/packages/next/src/client/dev/hot-reloader/app/hot-reloader-app.tsx
index 4030e4666db26..02051f1cee907 100644
--- a/packages/next/src/client/dev/hot-reloader/app/hot-reloader-app.tsx
+++ b/packages/next/src/client/dev/hot-reloader/app/hot-reloader-app.tsx
@@ -41,7 +41,7 @@ import { getOrCreateDebugChannelReadableWriterPair } from '../../debug-channel'
export interface StaticIndicatorState {
pathname: string | null
- appIsrManifest: Record
+ appIsrManifest: Record | null
}
let mostRecentCompilationHash: any = null
@@ -261,18 +261,18 @@ export function processMessage(
if (process.env.__NEXT_DEV_INDICATOR) {
staticIndicatorState.appIsrManifest = message.data
- // handle initial status on receiving manifest
- // navigation is handled in useEffect for pathname changes
- // as we'll receive the updated manifest before usePathname
- // triggers for new value
- if (
- staticIndicatorState.pathname &&
- staticIndicatorState.pathname in message.data
- ) {
- dispatcher.onStaticIndicator(true)
- } else {
- dispatcher.onStaticIndicator(false)
- }
+ // Handle the initial static indicator status on receiving the ISR
+ // manifest. Navigation is handled in an effect inside HotReload for
+ // pathname changes as we'll receive the updated manifest before
+ // usePathname triggers for a new value.
+
+ const isStatic = staticIndicatorState.pathname
+ ? message.data[staticIndicatorState.pathname]
+ : undefined
+
+ dispatcher.onStaticIndicator(
+ isStatic === undefined ? 'pending' : isStatic ? 'static' : 'dynamic'
+ )
}
break
}
@@ -542,10 +542,14 @@ export default function HotReload({
staticIndicatorState.pathname = pathname
- if (pathname && pathname in staticIndicatorState.appIsrManifest) {
- dispatcher.onStaticIndicator(true)
- } else {
- dispatcher.onStaticIndicator(false)
+ if (staticIndicatorState.appIsrManifest) {
+ const isStatic = pathname
+ ? staticIndicatorState.appIsrManifest[pathname]
+ : undefined
+
+ dispatcher.onStaticIndicator(
+ isStatic === undefined ? 'pending' : isStatic ? 'static' : 'dynamic'
+ )
}
}, [pathname, staticIndicatorState])
}
diff --git a/packages/next/src/client/dev/hot-reloader/pages/hot-reloader-pages.ts b/packages/next/src/client/dev/hot-reloader/pages/hot-reloader-pages.ts
index c22424f52ff94..de6a175acebb9 100644
--- a/packages/next/src/client/dev/hot-reloader/pages/hot-reloader-pages.ts
+++ b/packages/next/src/client/dev/hot-reloader/pages/hot-reloader-pages.ts
@@ -265,10 +265,10 @@ export function handleStaticIndicator() {
appComponent?.getInitialProps !== appComponent?.origGetInitialProps
const isPageStatic =
- window.location.pathname in isrManifest ||
+ isrManifest[window.location.pathname] ||
(!isDynamicPage && !hasAppGetInitialProps)
- dispatcher.onStaticIndicator(isPageStatic)
+ dispatcher.onStaticIndicator(isPageStatic ? 'static' : 'dynamic')
}
}
diff --git a/packages/next/src/next-devtools/dev-overlay.browser.tsx b/packages/next/src/next-devtools/dev-overlay.browser.tsx
index 98a848cd7f83e..854508455fd3c 100644
--- a/packages/next/src/next-devtools/dev-overlay.browser.tsx
+++ b/packages/next/src/next-devtools/dev-overlay.browser.tsx
@@ -55,7 +55,7 @@ export interface Dispatcher {
onDebugInfo(debugInfo: DebugInfo): void
onBeforeRefresh(): void
onRefresh(): void
- onStaticIndicator(status: boolean): void
+ onStaticIndicator(status: 'pending' | 'static' | 'dynamic' | 'disabled'): void
onDevIndicator(devIndicator: DevIndicatorServerState): void
onDevToolsConfig(config: DevToolsConfig): void
onUnhandledError(reason: Error): void
@@ -147,9 +147,14 @@ export const dispatcher: Dispatcher = {
dispatch({ type: ACTION_VERSION_INFO, versionInfo })
}
),
- onStaticIndicator: createQueuable((dispatch: Dispatch, status: boolean) => {
- dispatch({ type: ACTION_STATIC_INDICATOR, staticIndicator: status })
- }),
+ onStaticIndicator: createQueuable(
+ (
+ dispatch: Dispatch,
+ status: 'pending' | 'static' | 'dynamic' | 'disabled'
+ ) => {
+ dispatch({ type: ACTION_STATIC_INDICATOR, staticIndicator: status })
+ }
+ ),
onDebugInfo: createQueuable((dispatch: Dispatch, debugInfo: DebugInfo) => {
dispatch({ type: ACTION_DEBUG_INFO, debugInfo })
}),
diff --git a/packages/next/src/next-devtools/dev-overlay/icons/loading-icon.tsx b/packages/next/src/next-devtools/dev-overlay/icons/loading-icon.tsx
new file mode 100644
index 0000000000000..2f24c85f94bd3
--- /dev/null
+++ b/packages/next/src/next-devtools/dev-overlay/icons/loading-icon.tsx
@@ -0,0 +1,31 @@
+export function LoadingIcon() {
+ return (
+
+ )
+}
diff --git a/packages/next/src/next-devtools/dev-overlay/menu/panel-router.tsx b/packages/next/src/next-devtools/dev-overlay/menu/panel-router.tsx
index c4dc69fe78b9e..6463277eb8234 100644
--- a/packages/next/src/next-devtools/dev-overlay/menu/panel-router.tsx
+++ b/packages/next/src/next-devtools/dev-overlay/menu/panel-router.tsx
@@ -24,6 +24,7 @@ import {
ACTION_ERROR_OVERLAY_OPEN,
} from '../shared'
import GearIcon from '../icons/gear-icon'
+import { LoadingIcon } from '../icons/loading-icon'
import { UserPreferencesBody } from '../components/errors/dev-tools-indicator/dev-tools-info/user-preferences'
import { useShortcuts } from '../hooks/use-shortcuts'
import { useUpdateAllPanelPositions } from '../components/devtools-indicator/devtools-indicator'
@@ -60,17 +61,24 @@ const MenuPanel = () => {
}
},
},
- {
- title: `Current route is ${state.staticIndicator ? 'static' : 'dynamic'}.`,
- label: 'Route',
- value: state.staticIndicator ? 'Static' : 'Dynamic',
- onClick: () => setPanel('route-type'),
- attributes: {
- 'data-nextjs-route-type': state.staticIndicator
- ? 'static'
- : 'dynamic',
- },
- },
+ state.staticIndicator === 'disabled'
+ ? undefined
+ : state.staticIndicator === 'pending'
+ ? {
+ title: 'Loading...',
+ label: 'Route',
+ value: ,
+ }
+ : {
+ title: `Current route is ${state.staticIndicator}.`,
+ label: 'Route',
+ value:
+ state.staticIndicator === 'static' ? 'Static' : 'Dynamic',
+ onClick: () => setPanel('route-type'),
+ attributes: {
+ 'data-nextjs-route-type': state.staticIndicator,
+ },
+ },
!!process.env.TURBOPACK
? {
title: 'Turbopack is enabled.',
@@ -166,39 +174,39 @@ export const PanelRouter = () => {
-
-
- }
- >
-
-
-
+
}
- />
-
-
-
+ >
+
+
+
+
+
+
+ )}
{isAppRouter && (
diff --git a/packages/next/src/next-devtools/dev-overlay/shared.ts b/packages/next/src/next-devtools/dev-overlay/shared.ts
index af92595fa64fd..ca43ae3fd9bc0 100644
--- a/packages/next/src/next-devtools/dev-overlay/shared.ts
+++ b/packages/next/src/next-devtools/dev-overlay/shared.ts
@@ -49,7 +49,7 @@ export interface OverlayState {
readonly notFound: boolean
readonly buildingIndicator: boolean
readonly renderingIndicator: boolean
- readonly staticIndicator: boolean
+ readonly staticIndicator: 'pending' | 'static' | 'dynamic' | 'disabled'
readonly showIndicator: boolean
readonly disableDevIndicator: boolean
readonly debugInfo: DebugInfo
@@ -112,7 +112,7 @@ export const ACTION_DEVTOOL_UPDATE_ROUTE_STATE =
interface StaticIndicatorAction {
type: typeof ACTION_STATIC_INDICATOR
- staticIndicator: boolean
+ staticIndicator: 'pending' | 'static' | 'dynamic' | 'disabled'
}
interface BuildOkAction {
@@ -263,7 +263,7 @@ export const INITIAL_OVERLAY_STATE: Omit<
errors: [],
notFound: false,
renderingIndicator: false,
- staticIndicator: false,
+ staticIndicator: 'disabled',
/*
This is set to `true` when we can reliably know
whether the indicator is in disabled state or not.
diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx
index bc7decf5004de..6e0540d597681 100644
--- a/packages/next/src/server/app-render/app-render.tsx
+++ b/packages/next/src/server/app-render/app-render.tsx
@@ -1502,6 +1502,7 @@ async function renderToHTMLOrFlightImpl(
serverActions,
assetPrefix = '',
enableTainting,
+ experimental,
} = renderOpts
// We need to expose the bundled `require` API globally for
@@ -1566,10 +1567,18 @@ async function renderToHTMLOrFlightImpl(
globalThis.__next_chunk_load__ = __next_chunk_load__
}
- if (process.env.NODE_ENV === 'development') {
- // reset isr status at start of request
+ if (
+ process.env.NODE_ENV === 'development' &&
+ renderOpts.setIsrStatus &&
+ !experimental.cacheComponents
+ ) {
+ // Reset the ISR status at start of request.
const { pathname } = new URL(req.url || '/', 'http://n')
- renderOpts.setIsrStatus?.(pathname, false)
+ renderOpts.setIsrStatus(
+ pathname,
+ // Only pages using the Node runtime can use ISR, Edge is always dynamic.
+ process.env.NEXT_RUNTIME === 'edge' ? false : undefined
+ )
}
if (
@@ -1844,19 +1853,19 @@ async function renderToHTMLOrFlightImpl(
if (
process.env.NODE_ENV === 'development' &&
renderOpts.setIsrStatus &&
+ !experimental.cacheComponents &&
+ // Only pages using the Node runtime can use ISR, so we only need to
+ // update the status for those.
// The type check here ensures that `req` is correctly typed, and the
// environment variable check provides dead code elimination.
process.env.NEXT_RUNTIME !== 'edge' &&
- isNodeNextRequest(req) &&
- !isDevWarmupRequest
+ isNodeNextRequest(req)
) {
const setIsrStatus = renderOpts.setIsrStatus
req.originalRequest.on('end', () => {
- if (!requestStore.usedDynamic && !workStore.forceDynamic) {
- // only node can be ISR so we only need to update the status here
- const { pathname } = new URL(req.url || '/', 'http://n')
- setIsrStatus(pathname, true)
- }
+ const { pathname } = new URL(req.url || '/', 'http://n')
+ const isStatic = !requestStore.usedDynamic && !workStore.forceDynamic
+ setIsrStatus(pathname, isStatic)
})
}
diff --git a/packages/next/src/server/app-render/types.ts b/packages/next/src/server/app-render/types.ts
index 1f6132f9e6f85..217760cb650c3 100644
--- a/packages/next/src/server/app-render/types.ts
+++ b/packages/next/src/server/app-render/types.ts
@@ -93,7 +93,7 @@ export interface RenderOptsPartial {
}
isOnDemandRevalidate?: boolean
isPossibleServerAction?: boolean
- setIsrStatus?: (key: string, value: boolean) => void
+ setIsrStatus?: (key: string, value: boolean | undefined) => void
setReactDebugChannel?: (
debugChannel: { readable: ReadableStream },
htmlRequestId: string,
diff --git a/packages/next/src/server/dev/hot-middleware.ts b/packages/next/src/server/dev/hot-middleware.ts
index b5b0095fa95d9..eed2d8df90b9c 100644
--- a/packages/next/src/server/dev/hot-middleware.ts
+++ b/packages/next/src/server/dev/hot-middleware.ts
@@ -30,6 +30,7 @@ import type { HmrMessageSentToBrowser } from './hot-reloader-types'
import { HMR_MESSAGE_SENT_TO_BROWSER } from './hot-reloader-types'
import { devIndicatorServerState } from './dev-indicator-server-state'
import { createBinaryHmrMessageData } from './messages'
+import type { NextConfigComplete } from '../config-shared'
function isMiddlewareStats(stats: webpack.Stats) {
for (const key of stats.compilation.entrypoints.keys()) {
@@ -70,31 +71,21 @@ function getStatsForSyncEvent(
}
export class WebpackHotMiddleware {
- private clients = new Set()
+ private clientsWithoutRequestId = new Set()
private clientsByRequestId: Map = new Map()
-
- clientLatestStats: { ts: number; stats: webpack.Stats } | null
- middlewareLatestStats: { ts: number; stats: webpack.Stats } | null
- serverLatestStats: { ts: number; stats: webpack.Stats } | null
- closed: boolean
- versionInfo: VersionInfo
- devtoolsFrontendUrl: string | undefined
- devToolsConfig: DevToolsConfig
+ private closed = false
+ private clientLatestStats: { ts: number; stats: webpack.Stats } | null = null
+ private middlewareLatestStats: { ts: number; stats: webpack.Stats } | null =
+ null
+ private serverLatestStats: { ts: number; stats: webpack.Stats } | null = null
constructor(
compilers: webpack.Compiler[],
- versionInfo: VersionInfo,
- devtoolsFrontendUrl: string | undefined,
- devToolsConfig: DevToolsConfig
+ private versionInfo: VersionInfo,
+ private devtoolsFrontendUrl: string | undefined,
+ private config: NextConfigComplete,
+ private devToolsConfig: DevToolsConfig
) {
- this.clientLatestStats = null
- this.middlewareLatestStats = null
- this.serverLatestStats = null
- this.closed = false
- this.versionInfo = versionInfo
- this.devtoolsFrontendUrl = devtoolsFrontendUrl
- this.devToolsConfig = devToolsConfig || ({} as DevToolsConfig)
-
compilers[0].hooks.invalid.tap(
'webpack-hot-middleware',
this.onClientInvalid
@@ -175,17 +166,17 @@ export class WebpackHotMiddleware {
onHMR = (client: ws, requestId: string | null) => {
if (this.closed) return
- this.clients.add(client)
-
if (requestId) {
this.clientsByRequestId.set(requestId, client)
+ } else {
+ this.clientsWithoutRequestId.add(client)
}
client.addEventListener('close', () => {
- this.clients.delete(client)
-
if (requestId) {
this.clientsByRequestId.delete(requestId)
+ } else {
+ this.clientsWithoutRequestId.delete(client)
}
})
@@ -259,7 +250,32 @@ export class WebpackHotMiddleware {
return
}
- for (const wsClient of this.clients) {
+ for (const wsClient of [
+ ...this.clientsWithoutRequestId,
+ ...this.clientsByRequestId.values(),
+ ]) {
+ this.publishToClient(wsClient, message)
+ }
+ }
+
+ publishToLegacyClients = (message: HmrMessageSentToBrowser) => {
+ if (this.closed) {
+ return
+ }
+
+ // Clients with a request ID are inferred App Router clients. If Cache
+ // Components is not enabled, we consider those legacy clients. Pages
+ // Router clients are also considered legacy clients. TODO: Maybe mark
+ // clients as App Router / Pages Router clients explicitly, instead of
+ // inferring it from the presence of a request ID.
+
+ if (!this.config.experimental.cacheComponents) {
+ for (const wsClient of this.clientsByRequestId.values()) {
+ this.publishToClient(wsClient, message)
+ }
+ }
+
+ for (const wsClient of this.clientsWithoutRequestId) {
this.publishToClient(wsClient, message)
}
}
@@ -273,28 +289,31 @@ export class WebpackHotMiddleware {
// https://github.com/webpack/tapable/issues/32#issuecomment-350644466
this.closed = true
- for (const wsClient of this.clients) {
+ for (const wsClient of [
+ ...this.clientsWithoutRequestId,
+ ...this.clientsByRequestId.values(),
+ ]) {
// it's okay to not cleanly close these websocket connections, this is dev
wsClient.terminate()
}
- this.clients.clear()
+ this.clientsWithoutRequestId.clear()
this.clientsByRequestId.clear()
}
deleteClient = (client: ws, requestId: string | null) => {
- this.clients.delete(client)
-
if (requestId) {
this.clientsByRequestId.delete(requestId)
+ } else {
+ this.clientsWithoutRequestId.delete(client)
}
}
hasClients = () => {
- return this.clients.size > 0
+ return this.clientsWithoutRequestId.size + this.clientsByRequestId.size > 0
}
getClientCount = () => {
- return this.clients.size
+ return this.clientsWithoutRequestId.size + this.clientsByRequestId.size
}
}
diff --git a/packages/next/src/server/dev/hot-reloader-turbopack.ts b/packages/next/src/server/dev/hot-reloader-turbopack.ts
index 6a4eec631fa42..0b93c6217a294 100644
--- a/packages/next/src/server/dev/hot-reloader-turbopack.ts
+++ b/packages/next/src/server/dev/hot-reloader-turbopack.ts
@@ -433,7 +433,7 @@ export async function createHotReloaderTurbopack(
let hmrEventHappened = false
let hmrHash = 0
- const clients = new Set()
+ const clientsWithoutRequestId = new Set()
const clientsByRequestId = new Map()
const clientStates = new WeakMap()
@@ -457,7 +457,10 @@ export async function createHotReloaderTurbopack(
}
}
- for (const client of clients) {
+ for (const client of [
+ ...clientsWithoutRequestId,
+ ...clientsByRequestId.values(),
+ ]) {
const state = clientStates.get(client)
if (!state) {
continue
@@ -490,7 +493,10 @@ export async function createHotReloaderTurbopack(
const sendEnqueuedMessagesDebounce = debounce(sendEnqueuedMessages, 2)
const sendHmr: SendHmr = (id: string, message: HmrMessageSentToBrowser) => {
- for (const client of clients) {
+ for (const client of [
+ ...clientsWithoutRequestId,
+ ...clientsByRequestId.values(),
+ ]) {
clientStates.get(client)?.messages.set(id, message)
}
@@ -505,7 +511,10 @@ export async function createHotReloaderTurbopack(
payload.diagnostics = []
payload.issues = []
- for (const client of clients) {
+ for (const client of [
+ ...clientsWithoutRequestId,
+ ...clientsByRequestId.values(),
+ ]) {
clientStates.get(client)?.turbopackUpdates.push(payload)
}
@@ -667,7 +676,7 @@ export async function createHotReloaderTurbopack(
dev: {
assetMapper,
changeSubscriptions,
- clients,
+ clients: [...clientsWithoutRequestId, ...clientsByRequestId.values()],
clientStates,
serverFields,
@@ -762,7 +771,8 @@ export async function createHotReloaderTurbopack(
projectPath,
distDir,
sendHmrMessage: (message) => hotReloader.send(message),
- getActiveConnectionCount: () => clients.size,
+ getActiveConnectionCount: () =>
+ clientsWithoutRequestId.size + clientsByRequestId.size,
getDevServerUrl: () => process.env.__NEXT_PRIVATE_ORIGIN,
}),
]
@@ -856,18 +866,26 @@ export async function createHotReloaderTurbopack(
// TODO: Figure out if socket type can match the NextJsHotReloaderInterface
onHMR(req, socket: Socket, head, onUpgrade) {
wsServer.handleUpgrade(req, socket, head, (client) => {
- onUpgrade(client)
const clientIssues: EntryIssuesMap = new Map()
const subscriptions: Map> = new Map()
- clients.add(client)
-
const requestId = req.url
? new URL(req.url, 'http://n').searchParams.get('id')
: null
+ // Clients with a request ID are inferred App Router clients. If Cache
+ // Components is not enabled, we consider those legacy clients. Pages
+ // Router clients are also considered legacy clients. TODO: Maybe mark
+ // clients as App Router / Pages Router clients explicitly, instead of
+ // inferring it from the presence of a request ID.
if (requestId) {
clientsByRequestId.set(requestId, client)
+ onUpgrade(client, {
+ isLegacyClient: !nextConfig.experimental.cacheComponents,
+ })
+ } else {
+ clientsWithoutRequestId.add(client)
+ onUpgrade(client, { isLegacyClient: true })
}
clientStates.set(client, {
@@ -883,11 +901,12 @@ export async function createHotReloaderTurbopack(
subscription.return?.()
}
clientStates.delete(client)
- clients.delete(client)
if (requestId) {
clientsByRequestId.delete(requestId)
deleteReactDebugChannel(requestId)
+ } else {
+ clientsWithoutRequestId.delete(client)
}
})
@@ -1066,7 +1085,30 @@ export async function createHotReloaderTurbopack(
send(action) {
const payload = JSON.stringify(action)
- for (const client of clients) {
+ for (const client of [
+ ...clientsWithoutRequestId,
+ ...clientsByRequestId.values(),
+ ]) {
+ client.send(payload)
+ }
+ },
+
+ sendToLegacyClients(action) {
+ const payload = JSON.stringify(action)
+
+ // Clients with a request ID are inferred App Router clients. If Cache
+ // Components is not enabled, we consider those legacy clients. Pages
+ // Router clients are also considered legacy clients. TODO: Maybe mark
+ // clients as App Router / Pages Router clients explicitly, instead of
+ // inferring it from the presence of a request ID.
+
+ if (!nextConfig.experimental.cacheComponents) {
+ for (const client of clientsByRequestId.values()) {
+ client.send(payload)
+ }
+ }
+
+ for (const client of clientsWithoutRequestId) {
client.send(payload)
}
},
@@ -1314,11 +1356,14 @@ export async function createHotReloaderTurbopack(
})
},
close() {
- for (const wsClient of clients) {
+ for (const wsClient of [
+ ...clientsWithoutRequestId,
+ ...clientsByRequestId.values(),
+ ]) {
// it's okay to not cleanly close these websocket connections, this is dev
wsClient.terminate()
}
- clients.clear()
+ clientsWithoutRequestId.clear()
clientsByRequestId.clear()
},
}
@@ -1370,7 +1415,10 @@ export async function createHotReloaderTurbopack(
const errors = new Map()
addErrors(errors, currentEntryIssues)
- for (const client of clients) {
+ for (const client of [
+ ...clientsWithoutRequestId,
+ ...clientsByRequestId.values(),
+ ]) {
const state = clientStates.get(client)
if (!state) {
continue
diff --git a/packages/next/src/server/dev/hot-reloader-types.ts b/packages/next/src/server/dev/hot-reloader-types.ts
index 9de7d17e4ac3e..c04d8d899ea04 100644
--- a/packages/next/src/server/dev/hot-reloader-types.ts
+++ b/packages/next/src/server/dev/hot-reloader-types.ts
@@ -135,7 +135,7 @@ export interface TurbopackConnectedMessage {
export interface AppIsrManifestMessage {
type: HMR_MESSAGE_SENT_TO_BROWSER.ISR_MANIFEST
- data: Record
+ data: Record
}
export interface DevToolsConfigMessage {
@@ -213,6 +213,11 @@ export interface NextJsHotReloaderInterface {
clearHmrServerError(): void
start(): Promise
send(action: HmrMessageSentToBrowser): void
+ /**
+ * Send the given action only to legacy clients, i.e. Pages Router clients,
+ * and App Router clients that don't have Cache Components enabled.
+ */
+ sendToLegacyClients(action: HmrMessageSentToBrowser): void
setReactDebugChannel(
debugChannel: ReactDebugChannelForBrowser,
htmlRequestId: string,
@@ -223,7 +228,10 @@ export interface NextJsHotReloaderInterface {
req: IncomingMessage,
_socket: Duplex,
head: Buffer,
- onUpgrade: (client: { send(data: string): void }) => void
+ onUpgrade: (
+ client: { send(data: string): void },
+ context: { isLegacyClient: boolean }
+ ) => void
): void
invalidate({
reloadAfterInvalidation,
diff --git a/packages/next/src/server/dev/hot-reloader-webpack.ts b/packages/next/src/server/dev/hot-reloader-webpack.ts
index 600342fd5af78..75a8ce084d36b 100644
--- a/packages/next/src/server/dev/hot-reloader-webpack.ts
+++ b/packages/next/src/server/dev/hot-reloader-webpack.ts
@@ -424,7 +424,10 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface {
req: IncomingMessage,
_socket: Duplex,
head: Buffer,
- callback: (client: ws.WebSocket) => void
+ callback: (
+ client: ws.WebSocket,
+ context: { isLegacyClient: boolean }
+ ) => void
) {
wsServer.handleUpgrade(req, req.socket, head, (client) => {
const requestId = req.url
@@ -437,7 +440,16 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface {
this.webpackHotMiddleware.onHMR(client, requestId)
this.onDemandEntries?.onHMR(client, () => this.hmrServerError)
- callback(client)
+
+ // Clients with a request ID are inferred App Router clients. If Cache
+ // Components is not enabled, we consider those legacy clients. Pages
+ // Router clients are also considered legacy clients. TODO: Maybe mark
+ // clients as App Router / Pages Router clients explicitly, instead of
+ // inferring it from the presence of a request ID.
+ const isLegacyClient =
+ !requestId || !this.config.experimental.cacheComponents
+
+ callback(client, { isLegacyClient })
client.addEventListener('message', async ({ data }) => {
data = typeof data !== 'string' ? data.toString() : data
@@ -1562,6 +1574,7 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface {
this.multiCompiler.compilers,
this.versionInfo,
this.devtoolsFrontendUrl,
+ this.config,
initialDevToolsConfig
)
@@ -1704,6 +1717,10 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface {
this.webpackHotMiddleware!.publishToClient(client, message)
}
+ public sendToLegacyClients(message: HmrMessageSentToBrowser): void {
+ this.webpackHotMiddleware!.publishToLegacyClients(message)
+ }
+
public setReactDebugChannel(
debugChannel: ReactDebugChannelForBrowser,
htmlRequestId: string,
diff --git a/packages/next/src/server/dev/turbopack-utils.ts b/packages/next/src/server/dev/turbopack-utils.ts
index 5cc1a0eadeaee..e0ec8857e83b9 100644
--- a/packages/next/src/server/dev/turbopack-utils.ts
+++ b/packages/next/src/server/dev/turbopack-utils.ts
@@ -565,7 +565,7 @@ type HandleEntrypointsHooks = {
type HandleEntrypointsDevOpts = {
assetMapper: AssetMapper
changeSubscriptions: ChangeSubscriptions
- clients: Set
+ clients: Array
clientStates: ClientStateMap
serverFields: ServerFields
diff --git a/packages/next/src/server/lib/dev-bundler-service.ts b/packages/next/src/server/lib/dev-bundler-service.ts
index 44cd3889963bf..722db07182698 100644
--- a/packages/next/src/server/lib/dev-bundler-service.ts
+++ b/packages/next/src/server/lib/dev-bundler-service.ts
@@ -12,7 +12,7 @@ import type { ReactDebugChannelForBrowser } from '../dev/debug-channel'
* bundler while in development.
*/
export class DevBundlerService {
- public appIsrManifestInner: InstanceType>
+ public appIsrManifestInner: InstanceType>
constructor(
private readonly bundler: DevBundler,
@@ -24,7 +24,7 @@ export class DevBundlerService {
function length() {
return 16
}
- ) as any
+ )
}
public ensurePage: typeof this.bundler.hotReloader.ensurePage = async (
@@ -86,7 +86,7 @@ export class DevBundlerService {
}
public get appIsrManifest() {
- const serializableManifest: Record = {}
+ const serializableManifest: Record = {}
for (const [key, value] of this.appIsrManifestInner) {
serializableManifest[key] = value
@@ -95,13 +95,20 @@ export class DevBundlerService {
return serializableManifest
}
- public setIsrStatus(key: string, value: boolean) {
- if (value === false) {
+ public setIsrStatus(key: string, value: boolean | undefined) {
+ if (value === undefined) {
this.appIsrManifestInner.remove(key)
} else {
this.appIsrManifestInner.set(key, value)
}
- this.bundler?.hotReloader?.send({
+
+ // Only send the ISR manifest to legacy clients, i.e. Pages Router clients,
+ // or App Router clients that have Cache Components disabled. The ISR
+ // manifest is only used to inform the static indicator, which currently
+ // does not provide useful information if Cache Components is enabled due to
+ // its binary nature (i.e. it does not support showing info for partially
+ // static pages).
+ this.bundler?.hotReloader?.sendToLegacyClients({
type: HMR_MESSAGE_SENT_TO_BROWSER.ISR_MANIFEST,
data: this.appIsrManifest,
})
diff --git a/packages/next/src/server/lib/router-server.ts b/packages/next/src/server/lib/router-server.ts
index 1672cbd50360e..a0e30a9c81a51 100644
--- a/packages/next/src/server/lib/router-server.ts
+++ b/packages/next/src/server/lib/router-server.ts
@@ -801,13 +801,22 @@ export async function initialize(opts: {
req,
socket,
head,
- (client) => {
- client.send(
- JSON.stringify({
- type: HMR_MESSAGE_SENT_TO_BROWSER.ISR_MANIFEST,
- data: devBundlerService?.appIsrManifest || {},
- } satisfies AppIsrManifestMessage)
- )
+ (client, { isLegacyClient }) => {
+ if (isLegacyClient) {
+ // Only send the ISR manifest to legacy clients, i.e. Pages
+ // Router clients, or App Router clients that have Cache
+ // Components disabled. The ISR manifest is only used to inform
+ // the static indicator, which currently does not provide useful
+ // information if Cache Components is enabled due to its binary
+ // nature (i.e. it does not support showing info for partially
+ // static pages).
+ client.send(
+ JSON.stringify({
+ type: HMR_MESSAGE_SENT_TO_BROWSER.ISR_MANIFEST,
+ data: devBundlerService?.appIsrManifest || {},
+ } satisfies AppIsrManifestMessage)
+ )
+ }
}
)
}
diff --git a/packages/next/src/server/lib/router-utils/router-server-context.ts b/packages/next/src/server/lib/router-utils/router-server-context.ts
index 568be2acae75f..8d73e4219406c 100644
--- a/packages/next/src/server/lib/router-utils/router-server-context.ts
+++ b/packages/next/src/server/lib/router-utils/router-server-context.ts
@@ -39,7 +39,7 @@ export type RouterServerContext = Record<
// allow dev server to log with original stack
logErrorWithOriginalStack?: (err: unknown, type: string) => void
// allow setting ISR status in dev
- setIsrStatus?: (key: string, value: boolean) => void
+ setIsrStatus?: (key: string, value: boolean | undefined) => void
setReactDebugChannel?: (
debugChannel: { readable: ReadableStream },
htmlRequestId: string,
diff --git a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts
index 0df31c045d5a2..2c400090edae2 100644
--- a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts
+++ b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts
@@ -127,7 +127,7 @@ export type ServerFields = {
interceptionRoutes?: ReturnType<
typeof import('./filesystem').buildCustomRoute
>[]
- setIsrStatus?: (key: string, value: boolean) => void
+ setIsrStatus?: (key: string, value: boolean | undefined) => void
resetFetch?: () => void
}
diff --git a/packages/next/src/server/render.tsx b/packages/next/src/server/render.tsx
index 4e6fee912ffb4..f8c4bb204650e 100644
--- a/packages/next/src/server/render.tsx
+++ b/packages/next/src/server/render.tsx
@@ -258,7 +258,7 @@ export type RenderOptsPartial = {
assetQueryString?: string
resolvedUrl?: string
resolvedAsPath?: string
- setIsrStatus?: (key: string, value: boolean) => void
+ setIsrStatus?: (key: string, value: boolean | undefined) => void
clientReferenceManifest?: DeepReadonly
nextFontManifest?: DeepReadonly
distDir?: string
diff --git a/test/development/app-dir/dev-indicator/route-type.test.ts b/test/development/app-dir/dev-indicator/route-type.test.ts
index f4685e22c6c60..11126e4840a3d 100644
--- a/test/development/app-dir/dev-indicator/route-type.test.ts
+++ b/test/development/app-dir/dev-indicator/route-type.test.ts
@@ -1,5 +1,5 @@
import { nextTestSetup } from 'e2e-utils'
-import { getRouteTypeFromDevToolsIndicator, retry } from 'next-test-utils'
+import { assertStaticIndicator, retry } from 'next-test-utils'
describe('app dir dev indicator - route type', () => {
const { next } = nextTestSetup({
@@ -10,7 +10,7 @@ describe('app dir dev indicator - route type', () => {
const browser = await next.browser('/')
await retry(async () => {
- expect(await getRouteTypeFromDevToolsIndicator(browser)).toBe('Static')
+ await assertStaticIndicator(browser, 'Static')
})
})
@@ -25,7 +25,7 @@ describe('app dir dev indicator - route type', () => {
try {
await retry(async () => {
- expect(await getRouteTypeFromDevToolsIndicator(browser)).toBe('Dynamic')
+ await assertStaticIndicator(browser, 'Dynamic')
})
} finally {
await next.patchFile('app/page.tsx', origContent)
@@ -44,7 +44,7 @@ describe('app dir dev indicator - route type', () => {
try {
await retry(async () => {
- expect(await getRouteTypeFromDevToolsIndicator(browser)).toBe('Dynamic')
+ await assertStaticIndicator(browser, 'Dynamic')
})
} finally {
await next.patchFile('app/page.tsx', origContent)
@@ -56,6 +56,6 @@ describe('app dir dev indicator - route type', () => {
await browser.waitForElementByCss('#ready')
- expect(await getRouteTypeFromDevToolsIndicator(browser)).toBe('Dynamic')
+ await assertStaticIndicator(browser, 'Dynamic')
})
})
diff --git a/test/development/dev-indicator/app/app/static-indicator/dynamic/page.tsx b/test/development/dev-indicator/app/app/static-indicator/dynamic/page.tsx
new file mode 100644
index 0000000000000..ef6849bce549b
--- /dev/null
+++ b/test/development/dev-indicator/app/app/static-indicator/dynamic/page.tsx
@@ -0,0 +1,9 @@
+import { connection } from 'next/server'
+import { setTimeout } from 'timers/promises'
+
+export default async function Page() {
+ await connection()
+ await setTimeout(100)
+
+ return This is a dynamic app router page.
+}
diff --git a/test/development/dev-indicator/app/app/static-indicator/static/page.tsx b/test/development/dev-indicator/app/app/static-indicator/static/page.tsx
new file mode 100644
index 0000000000000..58bb180559156
--- /dev/null
+++ b/test/development/dev-indicator/app/app/static-indicator/static/page.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return This is a static app router page.
+}
diff --git a/test/development/dev-indicator/app/layout.js b/test/development/dev-indicator/app/layout.js
deleted file mode 100644
index 4ee00a218505a..0000000000000
--- a/test/development/dev-indicator/app/layout.js
+++ /dev/null
@@ -1,7 +0,0 @@
-export default function RootLayout({ children }) {
- return (
-
- {children}
-
- )
-}
diff --git a/test/development/dev-indicator/app/layout.tsx b/test/development/dev-indicator/app/layout.tsx
new file mode 100644
index 0000000000000..5f632238aabd0
--- /dev/null
+++ b/test/development/dev-indicator/app/layout.tsx
@@ -0,0 +1,15 @@
+import { Suspense } from 'react'
+import { Nav } from '../components/nav'
+
+export default function Root({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+ Loading...
}>{children}
+
+
+
+ )
+}
diff --git a/test/development/dev-indicator/components/nav.tsx b/test/development/dev-indicator/components/nav.tsx
new file mode 100644
index 0000000000000..718616586c6d4
--- /dev/null
+++ b/test/development/dev-indicator/components/nav.tsx
@@ -0,0 +1,26 @@
+import Link from 'next/link'
+
+export function Nav() {
+ return (
+
+ )
+}
diff --git a/test/development/dev-indicator/dev-indicator.test.ts b/test/development/dev-indicator/dev-indicator.test.ts
index 60c9a85befea2..8a00f9a08cc36 100644
--- a/test/development/dev-indicator/dev-indicator.test.ts
+++ b/test/development/dev-indicator/dev-indicator.test.ts
@@ -1,5 +1,8 @@
import { nextTestSetup } from 'e2e-utils'
-import { getRouteTypeFromDevToolsIndicator, retry } from 'next-test-utils'
+import { assertStaticIndicator, retry } from 'next-test-utils'
+
+const withCacheComponents =
+ process.env.__NEXT_EXPERIMENTAL_CACHE_COMPONENTS === 'true'
describe('dev indicator - route type', () => {
const { next } = nextTestSetup({
@@ -8,111 +11,209 @@ describe('dev indicator - route type', () => {
describe('getServerSideProps', () => {
it('should update when going from dynamic -> static', async () => {
- const browser = await next.browser('/gssp')
+ const browser = await next.browser('/pages/gssp')
await retry(async () => {
- expect(await getRouteTypeFromDevToolsIndicator(browser)).toBe('Dynamic')
+ await assertStaticIndicator(browser, 'Dynamic')
})
// validate static -> dynamic updates
- await browser.elementByCss("[href='/']").click()
+ await browser.elementByCss("[href='/pages']").click()
await retry(async () => {
- expect(await getRouteTypeFromDevToolsIndicator(browser)).toBe('Static')
+ await assertStaticIndicator(browser, 'Static')
})
})
it('should update when going from static -> dynamic', async () => {
- const browser = await next.browser('/')
+ const browser = await next.browser('/pages')
await retry(async () => {
- expect(await getRouteTypeFromDevToolsIndicator(browser)).toBe('Static')
+ await assertStaticIndicator(browser, 'Static')
})
// validate static -> dynamic updates
- await browser.elementByCss("[href='/gssp']").click()
+ await browser.elementByCss("[href='/pages/gssp']").click()
await retry(async () => {
- expect(await getRouteTypeFromDevToolsIndicator(browser)).toBe('Dynamic')
+ await assertStaticIndicator(browser, 'Dynamic')
})
})
it('should be marked dynamic on first load', async () => {
- const browser = await next.browser('/gssp')
+ const browser = await next.browser('/pages/gssp')
await retry(async () => {
- expect(await getRouteTypeFromDevToolsIndicator(browser)).toBe('Dynamic')
+ await assertStaticIndicator(browser, 'Dynamic')
})
})
})
describe('getInitialProps', () => {
it('should be marked dynamic on first load', async () => {
- const browser = await next.browser('/gip')
+ const browser = await next.browser('/pages/gip')
await retry(async () => {
- expect(await getRouteTypeFromDevToolsIndicator(browser)).toBe('Dynamic')
+ await assertStaticIndicator(browser, 'Dynamic')
})
})
it('should update when going from dynamic -> static', async () => {
- const browser = await next.browser('/gip')
+ const browser = await next.browser('/pages/gip')
await retry(async () => {
- expect(await getRouteTypeFromDevToolsIndicator(browser)).toBe('Dynamic')
+ await assertStaticIndicator(browser, 'Dynamic')
})
- await browser.elementByCss("[href='/']").click()
+ await browser.elementByCss("[href='/pages']").click()
await retry(async () => {
- expect(await getRouteTypeFromDevToolsIndicator(browser)).toBe('Static')
+ await assertStaticIndicator(browser, 'Static')
})
})
it('should update when going from static -> dynamic', async () => {
- const browser = await next.browser('/')
+ const browser = await next.browser('/pages')
await retry(async () => {
- expect(await getRouteTypeFromDevToolsIndicator(browser)).toBe('Static')
+ await assertStaticIndicator(browser, 'Static')
})
- await browser.elementByCss("[href='/gip']").click()
+ await browser.elementByCss("[href='/pages/gip']").click()
await retry(async () => {
- expect(await getRouteTypeFromDevToolsIndicator(browser)).toBe('Dynamic')
+ await assertStaticIndicator(browser, 'Dynamic')
})
})
})
describe('getStaticPaths', () => {
it('should be marked static on first load', async () => {
- const browser = await next.browser('/pregenerated')
+ const browser = await next.browser('/pages/pregenerated')
await retry(async () => {
- expect(await getRouteTypeFromDevToolsIndicator(browser)).toBe('Static')
+ await assertStaticIndicator(browser, 'Static')
})
})
it('should update when going from dynamic -> static', async () => {
- const browser = await next.browser('/gssp')
+ const browser = await next.browser('/pages/gssp')
await retry(async () => {
- expect(await getRouteTypeFromDevToolsIndicator(browser)).toBe('Dynamic')
+ await assertStaticIndicator(browser, 'Dynamic')
})
- await browser.elementByCss("[href='/pregenerated']").click()
+ await browser.elementByCss("[href='/pages/pregenerated']").click()
await retry(async () => {
- expect(await getRouteTypeFromDevToolsIndicator(browser)).toBe('Static')
+ await assertStaticIndicator(browser, 'Static')
})
})
})
it('should have route type as static by default for static page', async () => {
- const browser = await next.browser('/')
+ const browser = await next.browser('/pages')
await retry(async () => {
- expect(await getRouteTypeFromDevToolsIndicator(browser)).toBe('Static')
+ await assertStaticIndicator(browser, 'Static')
+ })
+ })
+
+ describe('with App Router', () => {
+ describe('when loading a dynamic page', () => {
+ if (withCacheComponents) {
+ describe('with Cache Components enabled', () => {
+ it('should not show a static indicator', async () => {
+ const browser = await next.browser('/app/static-indicator/dynamic')
+ await assertStaticIndicator(browser, undefined)
+ })
+
+ it('should still show a static indicator when navigating to a Pages Router page', async () => {
+ const browser = await next.browser('/app/static-indicator/dynamic')
+ await assertStaticIndicator(browser, undefined)
+
+ await browser.elementByCss("[href='/pages']").click()
+
+ await retry(async () => {
+ await assertStaticIndicator(browser, 'Static')
+ })
+ })
+ })
+ } else {
+ describe('with Cache Components disabled', () => {
+ it('should be marked dynamic on first load', async () => {
+ const browser = await next.browser('/app/static-indicator/dynamic')
+
+ await retry(async () => {
+ await assertStaticIndicator(browser, 'Dynamic')
+ })
+ })
+
+ it('should update when going from dynamic -> static', async () => {
+ const browser = await next.browser('/app/static-indicator/dynamic')
+
+ await retry(async () => {
+ await assertStaticIndicator(browser, 'Dynamic')
+ })
+
+ await browser
+ .elementByCss("[href='/app/static-indicator/static']")
+ .click()
+
+ await retry(async () => {
+ await assertStaticIndicator(browser, 'Static')
+ })
+ })
+ })
+ }
+ })
+
+ describe('when loading a static page', () => {
+ if (withCacheComponents) {
+ describe('with Cache Components enabled', () => {
+ it('should not show a static indicator', async () => {
+ const browser = await next.browser('/app/static-indicator/static')
+ await assertStaticIndicator(browser, undefined)
+ })
+
+ it('should still show a static indicator when navigating to a Pages Router page', async () => {
+ const browser = await next.browser('/app/static-indicator/static')
+ await assertStaticIndicator(browser, undefined)
+
+ await browser.elementByCss("[href='/pages/gssp']").click()
+
+ await retry(async () => {
+ await assertStaticIndicator(browser, 'Dynamic')
+ })
+ })
+ })
+ } else {
+ describe('with Cache Components disabled', () => {
+ it('should be marked static on first load', async () => {
+ const browser = await next.browser('/app/static-indicator/static')
+
+ await retry(async () => {
+ await assertStaticIndicator(browser, 'Static')
+ })
+ })
+
+ it('should update when going from static -> dynamic', async () => {
+ const browser = await next.browser('/app/static-indicator/static')
+
+ await retry(async () => {
+ await assertStaticIndicator(browser, 'Static')
+ })
+
+ await browser
+ .elementByCss("[href='/app/static-indicator/dynamic']")
+ .click()
+
+ await retry(async () => {
+ await assertStaticIndicator(browser, 'Dynamic')
+ })
+ })
+ })
+ }
})
})
})
diff --git a/test/development/dev-indicator/pages/_app.tsx b/test/development/dev-indicator/pages/_app.tsx
new file mode 100644
index 0000000000000..d79c1cd7ac8da
--- /dev/null
+++ b/test/development/dev-indicator/pages/_app.tsx
@@ -0,0 +1,13 @@
+import type { AppProps } from 'next/app'
+import { Nav } from '../components/nav'
+
+export default function MyApp({ Component, pageProps }: AppProps) {
+ return (
+ <>
+
+
+
+
+ >
+ )
+}
diff --git a/test/development/dev-indicator/pages/gssp.tsx b/test/development/dev-indicator/pages/gssp.tsx
deleted file mode 100644
index 20de16ace781f..0000000000000
--- a/test/development/dev-indicator/pages/gssp.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import Link from 'next/link'
-
-export default function Page() {
- return (
-
- hello world to /pregenerated{' '}
- to /
-
- )
-}
-
-export async function getServerSideProps() {
- return {
- props: {
- static: false,
- },
- }
-}
diff --git a/test/development/dev-indicator/pages/index.tsx b/test/development/dev-indicator/pages/index.tsx
deleted file mode 100644
index ef1ba7d9a400a..0000000000000
--- a/test/development/dev-indicator/pages/index.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import Link from 'next/link'
-
-export default function Page() {
- return (
-
- hello world to /gssp{' '}
- to /pregenerated
- to /gip
-
- )
-}
diff --git a/test/development/dev-indicator/pages/[slug]/index.tsx b/test/development/dev-indicator/pages/pages/[slug]/index.tsx
similarity index 71%
rename from test/development/dev-indicator/pages/[slug]/index.tsx
rename to test/development/dev-indicator/pages/pages/[slug]/index.tsx
index d6c5fa700a742..bd385cd4a4fdc 100644
--- a/test/development/dev-indicator/pages/[slug]/index.tsx
+++ b/test/development/dev-indicator/pages/pages/[slug]/index.tsx
@@ -1,12 +1,5 @@
-import Link from 'next/link'
-
export default function Page({ slug }: { slug: string }) {
- return (
-
- hello world {slug} to /gssp
- to /
-
- )
+ return hello world {slug}
}
export const getStaticPaths = async () => {
diff --git a/test/development/dev-indicator/pages/gip.tsx b/test/development/dev-indicator/pages/pages/gip.tsx
similarity index 50%
rename from test/development/dev-indicator/pages/gip.tsx
rename to test/development/dev-indicator/pages/pages/gip.tsx
index 3d85c0d860c9d..f991b46c3261e 100644
--- a/test/development/dev-indicator/pages/gip.tsx
+++ b/test/development/dev-indicator/pages/pages/gip.tsx
@@ -1,11 +1,5 @@
-import Link from 'next/link'
-
export default function Page() {
- return (
-
- hello world to /
-
- )
+ return hello world
}
Page.getInitialProps = async () => {
diff --git a/test/development/dev-indicator/pages/pages/gssp.tsx b/test/development/dev-indicator/pages/pages/gssp.tsx
new file mode 100644
index 0000000000000..d38a06eab2529
--- /dev/null
+++ b/test/development/dev-indicator/pages/pages/gssp.tsx
@@ -0,0 +1,11 @@
+export default function Page() {
+ return hello world
+}
+
+export async function getServerSideProps() {
+ return {
+ props: {
+ static: false,
+ },
+ }
+}
diff --git a/test/development/dev-indicator/pages/pages/index.tsx b/test/development/dev-indicator/pages/pages/index.tsx
new file mode 100644
index 0000000000000..ff7159d9149fe
--- /dev/null
+++ b/test/development/dev-indicator/pages/pages/index.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return hello world
+}
diff --git a/test/e2e/app-dir/cache-components-dynamic-imports/cache-components-dynamic-imports.test.ts b/test/e2e/app-dir/cache-components-dynamic-imports/cache-components-dynamic-imports.test.ts
index 8a0005c0aba44..5bdca48d054d5 100644
--- a/test/e2e/app-dir/cache-components-dynamic-imports/cache-components-dynamic-imports.test.ts
+++ b/test/e2e/app-dir/cache-components-dynamic-imports/cache-components-dynamic-imports.test.ts
@@ -1,10 +1,6 @@
import * as path from 'path'
import { nextTestSetup } from 'e2e-utils'
-import {
- assertNoRedbox,
- getRouteTypeFromDevToolsIndicator,
- retry,
-} from 'next-test-utils'
+import { assertNoRedbox, retry } from 'next-test-utils'
describe('async imports in cacheComponents', () => {
const { next, isNextStart, isNextDev } = nextTestSetup({
@@ -65,8 +61,6 @@ describe('async imports in cacheComponents', () => {
expect(await browser.elementByCss('body').text()).toBe('hello')
if (isNextDev) {
await retry(async () => {
- // the page should be static
- expect(await getRouteTypeFromDevToolsIndicator(browser)).toBe('Static')
// we shouldn't get any errors from `spawnDynamicValidationInDev`
await assertNoRedbox(browser)
})
diff --git a/test/lib/next-test-utils.ts b/test/lib/next-test-utils.ts
index 740ea159e1f31..2c0271d440781 100644
--- a/test/lib/next-test-utils.ts
+++ b/test/lib/next-test-utils.ts
@@ -1053,35 +1053,37 @@ export async function assertNoDevToolsIndicator(browser: Playwright) {
}
}
-export async function getRouteTypeFromDevToolsIndicator(
- browser: Playwright
-): Promise<'Static' | 'Dynamic'> {
+export async function assertStaticIndicator(
+ browser: Playwright,
+ expectedRouteType: 'Static' | 'Dynamic' | undefined
+): Promise {
await openDevToolsIndicatorPopover(browser)
- return browser.eval(() => {
+ const routeType = await browser.eval(() => {
const portal = [].slice
.call(document.querySelectorAll('nextjs-portal'))
.find((p) => p.shadowRoot.querySelector('[data-nextjs-toast]'))
- const root = portal?.shadowRoot
-
- // 'Route\nStatic' || 'Route\nDynamic'
- const routeTypeText = root?.querySelector(
- '[data-nextjs-route-type]'
- )?.innerText
-
- if (!routeTypeText) {
- throw new Error('No Route Type Text Found')
- }
+ return (
+ portal?.shadowRoot
+ // 'Route\nStatic' || 'Route\nDynamic'
+ ?.querySelector('[data-nextjs-route-type]')
+ ?.innerText.split('\n')
+ .pop()
+ )
+ })
- // 'Static' || 'Dynamic'
- const routeType = routeTypeText.split('\n').pop()
- if (routeType !== 'Static' && routeType !== 'Dynamic') {
- throw new Error(`Invalid Route Type: ${routeType}`)
+ if (routeType !== expectedRouteType) {
+ if (expectedRouteType) {
+ throw new Error(
+ `Expected static indicator with route type ${expectedRouteType}, found ${routeType} instead.`
+ )
+ } else {
+ throw new Error(
+ `Expected no static indicator, found ${routeType} instead.`
+ )
}
-
- return routeType as 'Static' | 'Dynamic'
- })
+ }
}
export function getRedboxHeader(browser: Playwright): Promise {