Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface NextAdapter {
ctx: {
phase: PHASE_TYPE
nextVersion: string
projectDir: string
}
) => Promise<NextConfigComplete> | NextConfigComplete
onBuildComplete?: (ctx: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Called for any CLI command that loads the `next.config.js` file to allow modific
- `config`: The complete Next.js configuration object
- `context.phase`: The current build phase (see [phases](/docs/app/api-reference/config/next-config-js#phase))
- `context.nextVersion`: Version of Next.js being used
- `context.projectDir`: Absolute path to the Next.js project directory

**Returns:** The modified configuration object (can be async)

Expand Down
2 changes: 2 additions & 0 deletions packages/next/.storybook/decorators/use-overlay-reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ACTION_BUILDING_INDICATOR_HIDE,
ACTION_BUILDING_INDICATOR_SHOW,
ACTION_CACHE_INDICATOR,
ACTION_INSTANT_ERRORS_CLEAR,
ACTION_INSTANT_NAVS_TOGGLE,
ACTION_INSTANT_NAVS_RESET,
ACTION_DEBUG_INFO,
Expand Down Expand Up @@ -88,6 +89,7 @@ export function useStorybookOverlayReducer(initialState?: OverlayState) {
case ACTION_RENDERING_INDICATOR_HIDE:
case ACTION_RENDERING_INDICATOR_SHOW:
case ACTION_CACHE_INDICATOR:
case ACTION_INSTANT_ERRORS_CLEAR:
case ACTION_INSTANT_NAVS_TOGGLE:
case ACTION_INSTANT_NAVS_RESET:
case ACTION_STATIC_INDICATOR:
Expand Down
13 changes: 9 additions & 4 deletions packages/next/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -1244,8 +1244,13 @@
"1243": "This \"use cache\" has a dynamic cache life that was propagated to its parent.",
"1244": "A \"use cache\" with short \\`expire\\` (under 5 minutes) is nested inside another \"use cache\" that has no explicit \\`cacheLife\\`, which is not allowed during prerendering. Add \\`cacheLife()\\` to the outer \"use cache\" to choose whether it should be prerendered (with longer \\`expire\\`) or remain dynamic (with short \\`expire\\`). Read more: https://nextjs.org/docs/messages/nested-use-cache-no-explicit-cachelife",
"1245": "A \"use cache\" with zero \\`revalidate\\` is nested inside another \"use cache\" that has no explicit \\`cacheLife\\`, which is not allowed during prerendering. Add \\`cacheLife()\\` to the outer \"use cache\" to choose whether it should be prerendered (with non-zero \\`revalidate\\`) or remain dynamic (with zero \\`revalidate\\`). Read more: https://nextjs.org/docs/messages/nested-use-cache-no-explicit-cachelife",
"1246": "Could not validate instant UI because an expected segment was not rendered.",
"1247": "\\`experimental.cssChunking: \"graph\"\\` is only supported with Turbopack. Please remove the option or run Next.js with Turbopack in %s.",
"1248": "\\`experimental.cssChunking: \"strict\"\\` is only supported with webpack. Please remove the option or run Next.js with webpack in %s.",
"1249": "\\`experimental.cssChunking: false\\` is only supported with webpack. Please remove the option or run Next.js with webpack in %s."
"1246": "Route \"%s\": Next.js encountered uncached data during the initial render or a navigation.\\n\\n\\`fetch(...)\\` or \\`connection()\\` accessed under \\`<Suspense>\\` prevents the route from being prerendered or the navigation from being instant, leading to a slower user experience.\\n\\nWays to fix this:\\n - Cache the data access with \\`\"use cache\"\\`\\n - Provide a placeholder with \\`<Suspense fallback={...}>\\` around the data access\\n - Set \\`export const instant = false\\` to allow a blocking route\\n\\nLearn more: https://nextjs.org/docs/messages/blocking-route",
"1247": "Route \"%s\": Next.js encountered runtime data during the initial render or a navigation.\\n\\n\\`cookies()\\`, \\`headers()\\`, \\`params\\`, or \\`searchParams\\` accessed under \\`<Suspense>\\` prevents the route from being prerendered or the navigation from being instant, leading to a slower user experience.\\n\\nWays to fix this:\\n - Use \\`generateStaticParams\\` to make route params static\\n - Provide a placeholder with \\`<Suspense fallback={...}>\\` around the data access\\n - Set \\`export const instant = false\\` to allow a blocking route\\n\\nLearn more: https://nextjs.org/docs/messages/blocking-route",
"1248": "Could not validate instant UI because an expected segment was not rendered.",
"1249": "Route \"%s\": Next.js encountered uncached data during the initial render or a navigation.\\n\\n\\`fetch(...)\\` or \\`connection()\\` accessed outside of \\`<Suspense>\\` prevents the route from being prerendered or the navigation from being instant, leading to a slower user experience.\\n\\nWays to fix this:\\n - Cache the data access with \\`\"use cache\"\\`\\n - Provide a placeholder with \\`<Suspense fallback={...}>\\` around the data access\\n - Set \\`export const instant = false\\` to allow a blocking route\\n\\nLearn more: https://nextjs.org/docs/messages/blocking-route",
"1250": "Route \"%s\": Next.js encountered runtime data during the initial render or a navigation.\\n\\n\\`cookies()\\`, \\`headers()\\`, \\`params\\`, or \\`searchParams\\` accessed outside of \\`<Suspense>\\` prevents the route from being prerendered or the navigation from being instant, leading to a slower user experience.\\n\\nWays to fix this:\\n - Use \\`generateStaticParams\\` to make route params static\\n - Provide a placeholder with \\`<Suspense fallback={...}>\\` around the data access\\n - Set \\`export const instant = false\\` to allow a blocking route\\n\\nLearn more: https://nextjs.org/docs/messages/blocking-route",
"1251": "Route \"%s\": Next.js encountered runtime data during the initial render or a navigation.\\n\\n\\`cookies()\\`, \\`headers()\\`, \\`params\\`, or \\`searchParams\\` accessed outside of \\`<Suspense>\\` prevents the route from being prerendered or the navigation from being instant, leading to a slower user experience.\\n\\nWays to fix this:\\n - Provide a placeholder with \\`<Suspense fallback={...}>\\` around the data access\\n - Use \\`generateStaticParams\\` to make route params static\\n - Set \\`export const instant = false\\` to allow a blocking route\\n\\nLearn more: https://nextjs.org/docs/messages/blocking-route",
"1252": "\\`experimental.cssChunking: \"graph\"\\` is only supported with Turbopack. Please remove the option or run Next.js with Turbopack in %s.",
"1253": "\\`experimental.cssChunking: \"strict\"\\` is only supported with webpack. Please remove the option or run Next.js with webpack in %s.",
"1254": "\\`experimental.cssChunking: false\\` is only supported with webpack. Please remove the option or run Next.js with webpack in %s."
}
4 changes: 4 additions & 0 deletions packages/next/src/build/adapter/build-complete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,10 @@ export interface NextAdapter {
* nextVersion is the current version of Next.js being used
*/
nextVersion: string
/**
* projectDir is the absolute directory the Next.js application is in
*/
projectDir: string
}
) => Promise<NextConfigComplete> | NextConfigComplete
onBuildComplete?: (ctx: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ export type GuidanceKind =
| 'sync-io'
| 'sync-io-client'

export type GuidanceVariant = 'runtime' | 'navigation'
export type GuidanceVariant = 'runtime' | 'dynamic'

export const DOCS_URLS: Record<GuidanceKind, string> = {
'blocking-route': 'https://nextjs.org/docs/messages/blocking-route',
Expand Down Expand Up @@ -562,6 +562,9 @@ export const EXPLANATIONS: Record<GuidanceKind, string> = {
'This value would be evaluated during the prerender and fixed at build time, instead of recomputed on each visit.',
}

export const BLOCKING_ROUTE_NAVIGATION_EXPLANATION =
'This prevents the navigation from being instant, leading to a slower user experience.'

const syncCardsByCause: Record<string, FixCard[]> = {
'Math.random()': syncMathCards,
'Date.now()': syncDateCards,
Expand Down Expand Up @@ -601,7 +604,7 @@ export function getCards(
): FixCard[] {
switch (kind) {
case 'blocking-route':
return variant === 'navigation' ? dynamicCards : runtimeCards
return variant === 'dynamic' ? dynamicCards : runtimeCards
case 'metadata':
return variant === 'runtime' ? metadataRuntimeCards : metadataDynamicCards
case 'viewport':
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {
createDynamicBodyError,
createDynamicBodyErrorInNavigation,
createDynamicMetadataError,
createDynamicOrRuntimeBodyError,
createDynamicOrRuntimeMetadataError,
createDynamicOrRuntimeViewportError,
createDynamicViewportError,
createRuntimeBodyError,
createRuntimeBodyErrorInNavigation,
createRuntimeMetadataError,
createRuntimeViewportError,
} from '../../../server/app-render/blocking-route-messages'
Expand All @@ -24,14 +26,6 @@ import {

const ROUTE = '/example'

// Every detection helper in errors.tsx walks the user-facing error message
// produced by the server-side factories in `blocking-route-messages.ts` and
// `sync-io-messages.ts`. These tests guard the contract between the two
// modules: if a factory's wording shifts in a way the detector can't
// recognize, classification silently falls back to "not an instant error"
// and the overlay shows the wrong UI. Three regressions in this exact spot
// during the redesign motivated this test.

describe('isRuntimeVariant', () => {
it('returns true for runtime body factory output', () => {
expect(isRuntimeVariant(createRuntimeBodyError(ROUTE).message)).toBe(true)
Expand Down Expand Up @@ -128,24 +122,48 @@ describe('isSyncIOClientError', () => {
})

describe('getBlockingRouteErrorDetails', () => {
it('classifies createRuntimeBodyError as blocking-route + runtime', () => {
it('classifies createRuntimeBodyError as blocking-route + runtime (SSR-only)', () => {
expect(getBlockingRouteErrorDetails(createRuntimeBodyError(ROUTE))).toEqual(
{ type: 'blocking-route', variant: 'runtime' }
{ type: 'blocking-route', variant: 'runtime', inNavigation: false }
)
})

it('classifies createDynamicBodyError as blocking-route + navigation', () => {
it('classifies createDynamicBodyError as blocking-route + dynamic (SSR-only)', () => {
expect(getBlockingRouteErrorDetails(createDynamicBodyError(ROUTE))).toEqual(
{ type: 'blocking-route', variant: 'navigation' }
{ type: 'blocking-route', variant: 'dynamic', inNavigation: false }
)
})

it('classifies createDynamicOrRuntimeBodyError as blocking-route + navigation', () => {
it('classifies createRuntimeBodyErrorInNavigation as blocking-route + runtime + inNavigation', () => {
expect(
getBlockingRouteErrorDetails(createRuntimeBodyErrorInNavigation(ROUTE))
).toEqual({
type: 'blocking-route',
variant: 'runtime',
inNavigation: true,
})
})

it('classifies createDynamicBodyErrorInNavigation as blocking-route + dynamic + inNavigation', () => {
expect(
getBlockingRouteErrorDetails(createDynamicBodyErrorInNavigation(ROUTE))
).toEqual({
type: 'blocking-route',
variant: 'dynamic',
inNavigation: true,
})
})

it('classifies createDynamicOrRuntimeBodyError as blocking-route + dynamic (SSR-only)', () => {
// The "either" factory has no clear runtime signal — falls into the
// navigation branch by `isRuntimeVariant`. Documents current behavior.
// dynamic branch by `isRuntimeVariant`. Documents current behavior.
expect(
getBlockingRouteErrorDetails(createDynamicOrRuntimeBodyError(ROUTE))
).toEqual({ type: 'blocking-route', variant: 'navigation' })
).toEqual({
type: 'blocking-route',
variant: 'dynamic',
inNavigation: false,
})
})

it('classifies createRuntimeMetadataError as dynamic-metadata + runtime', () => {
Expand All @@ -154,16 +172,16 @@ describe('getBlockingRouteErrorDetails', () => {
).toEqual({ type: 'dynamic-metadata', variant: 'runtime' })
})

it('classifies createDynamicMetadataError as dynamic-metadata + navigation', () => {
it('classifies createDynamicMetadataError as dynamic-metadata + dynamic', () => {
expect(
getBlockingRouteErrorDetails(createDynamicMetadataError(ROUTE))
).toEqual({ type: 'dynamic-metadata', variant: 'navigation' })
).toEqual({ type: 'dynamic-metadata', variant: 'dynamic' })
})

it('classifies createDynamicOrRuntimeMetadataError as dynamic-metadata + navigation', () => {
it('classifies createDynamicOrRuntimeMetadataError as dynamic-metadata + dynamic', () => {
expect(
getBlockingRouteErrorDetails(createDynamicOrRuntimeMetadataError(ROUTE))
).toEqual({ type: 'dynamic-metadata', variant: 'navigation' })
).toEqual({ type: 'dynamic-metadata', variant: 'dynamic' })
})

it('classifies createRuntimeViewportError as dynamic-viewport + runtime', () => {
Expand All @@ -172,16 +190,16 @@ describe('getBlockingRouteErrorDetails', () => {
).toEqual({ type: 'dynamic-viewport', variant: 'runtime' })
})

it('classifies createDynamicViewportError as dynamic-viewport + navigation', () => {
it('classifies createDynamicViewportError as dynamic-viewport + dynamic', () => {
expect(
getBlockingRouteErrorDetails(createDynamicViewportError(ROUTE))
).toEqual({ type: 'dynamic-viewport', variant: 'navigation' })
).toEqual({ type: 'dynamic-viewport', variant: 'dynamic' })
})

it('classifies createDynamicOrRuntimeViewportError as dynamic-viewport + navigation', () => {
it('classifies createDynamicOrRuntimeViewportError as dynamic-viewport + dynamic', () => {
expect(
getBlockingRouteErrorDetails(createDynamicOrRuntimeViewportError(ROUTE))
).toEqual({ type: 'dynamic-viewport', variant: 'navigation' })
).toEqual({ type: 'dynamic-viewport', variant: 'dynamic' })
})

it.each<[SyncIOApiType, string, string]>([
Expand Down
48 changes: 39 additions & 9 deletions packages/next/src/next-devtools/dev-overlay/container/errors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
type GuidanceKind,
type GuidanceVariant,
} from '../components/instant/instant-guidance'
import { BLOCKING_ROUTE_NAVIGATION_EXPLANATION } from '../components/instant/instant-guidance-data'
import { CodeFrame } from '../components/code-frame/code-frame'
import { ErrorOverlayCallStack } from '../components/errors/error-overlay-call-stack/error-overlay-call-stack'
import { ErrorCause } from './runtime-error/error-cause'
Expand Down Expand Up @@ -129,17 +130,18 @@ type HydrationErrorDetails = {

type BlockingRouteErrorDetails = {
type: 'blocking-route'
variant: 'navigation' | 'runtime'
variant: 'dynamic' | 'runtime'
inNavigation: boolean
}

type DynamicMetadataErrorDetails = {
type: 'dynamic-metadata'
variant: 'navigation' | 'runtime'
variant: 'dynamic' | 'runtime'
}

type DynamicViewportErrorDetails = {
type: 'dynamic-viewport'
variant: 'navigation' | 'runtime'
variant: 'dynamic' | 'runtime'
}

type SyncIOErrorDetails = {
Expand Down Expand Up @@ -320,16 +322,31 @@ export function isSyncIOClientError(message: string): boolean {
return match !== null && match[2] === '-client'
}

// Detects errors emitted during navigation-phase instant validation: body
// errors from `createRuntimeBodyErrorInNavigation` /
// `createDynamicBodyErrorInNavigation` (SSR factories instead say "during
// the initial render"), and validation errors from
// `trackDynamicHoleInNavigation` / `getNavigationDisallowedDynamicReasons`.
export function isBlockingRouteInNavError(message: string): boolean {
return (
message.includes('or a navigation') ||
message.includes('Could not validate `unstable_instant`') ||
message.includes('Could not validate instant UI')
)
}

export function getBlockingRouteErrorDetails(
error: Error
): null | ErrorDetails {
const message = error.message
const inNavigation = isBlockingRouteInNavError(message)

const isBlockingPageLoadError = message.includes('/blocking-route')
if (isBlockingPageLoadError) {
return {
type: 'blocking-route',
variant: isRuntimeVariant(message) ? 'runtime' : 'navigation',
variant: isRuntimeVariant(message) ? 'runtime' : 'dynamic',
inNavigation,
}
}

Expand All @@ -339,7 +356,7 @@ export function getBlockingRouteErrorDetails(
if (isDynamicMetadataError) {
return {
type: 'dynamic-metadata',
variant: isRuntimeVariant(message) ? 'runtime' : 'navigation',
variant: isRuntimeVariant(message) ? 'runtime' : 'dynamic',
}
}

Expand All @@ -349,7 +366,7 @@ export function getBlockingRouteErrorDetails(
if (isBlockingViewportError) {
return {
type: 'dynamic-viewport',
variant: isRuntimeVariant(message) ? 'runtime' : 'navigation',
variant: isRuntimeVariant(message) ? 'runtime' : 'dynamic',
}
}

Expand Down Expand Up @@ -540,10 +557,23 @@ Next.js version: ${props.versionInfo.installed} (${process.env.__NEXT_BUNDLER})\
errorType={errorType}
errorMessage={
errorDetails.variant === 'runtime'
? 'Next.js encountered runtime data during the initial render.'
: 'Next.js encountered uncached data during the initial render.'
? errorDetails.inNavigation
? 'Next.js encountered runtime data during a navigation.'
: 'Next.js encountered runtime data during the initial render.'
: errorDetails.inNavigation
? 'Next.js encountered uncached data during a navigation.'
: 'Next.js encountered uncached data during the initial render.'
}
headerChildren={
<InstantHeaderExplanation
kind="blocking-route"
explanation={
errorDetails.inNavigation
? BLOCKING_ROUTE_NAVIGATION_EXPLANATION
: undefined
}
/>
}
headerChildren={<InstantHeaderExplanation kind="blocking-route" />}
onClose={isServerError ? undefined : onClose}
debugInfo={debugInfo}
error={error}
Expand Down
25 changes: 24 additions & 1 deletion packages/next/src/next-devtools/dev-overlay/dev-overlay.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createContext, useContext, useRef, useState } from 'react'
import { createContext, useContext, useEffect, useRef, useState } from 'react'
import { ShadowPortal } from './components/shadow-portal'
import { ComponentStyles } from './styles/component-styles'
import { ErrorOverlay } from './components/errors/error-overlay/error-overlay'
Expand All @@ -9,6 +9,7 @@ import { DevToolsIndicator } from './components/devtools-indicator/devtools-indi
import { PanelRouter } from './menu/panel-router'
import { PanelRouterContext, type PanelStateKind } from './menu/context'
import { useDevOverlayContext } from '../dev-overlay.browser'
import { ACTION_INSTANT_ERRORS_CLEAR, type DispatcherEvent } from './shared'

export const RenderErrorContext = createContext<{
runtimeErrors: ReadyRuntimeError[]
Expand All @@ -17,6 +18,26 @@ export const RenderErrorContext = createContext<{

export const useRenderErrorContext = () => useContext(RenderErrorContext)

// Dispatches `ACTION_INSTANT_ERRORS_CLEAR` whenever the page changes to a
// new non-empty value. The first non-empty value is recorded as a baseline
// (the route the user landed on) and does not trigger a clear.
function useClearInstantErrorsOnNav(
page: string,
dispatch: (action: DispatcherEvent) => void
) {
const baselinePageRef = useRef<string | null>(null)
useEffect(() => {
if (page === '') return
if (baselinePageRef.current === null) {
baselinePageRef.current = page
return
}
if (page === baselinePageRef.current) return
baselinePageRef.current = page
dispatch({ type: ACTION_INSTANT_ERRORS_CLEAR, currentPath: page })
}, [page, dispatch])
}

export function DevOverlay() {
const [selectedIndex, setSelectedIndex] = useState(-1)
const { state, dispatch, getSquashedHydrationErrorDetails } =
Expand All @@ -25,6 +46,8 @@ export function DevOverlay() {
state.instantNavs ? 'instant-navs' : null
)

useClearInstantErrorsOnNav(state.page, dispatch)

const triggerRef = useRef<HTMLButtonElement>(null)
return (
<ShadowPortal>
Expand Down
Loading
Loading