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 @@ -13,6 +13,7 @@ const {
InstantValidationBoundaryContext,
PlaceValidationBoundaryBelowThisLevel,
RenderValidationBoundaryAtThisLevel,
SlotMarker,
} =
typeof window === 'undefined' && process.env.__NEXT_CACHE_COMPONENTS
? (require('../../../server/app-render/instant-validation/boundary-impl') as typeof import('../../../server/app-render/instant-validation/boundary-impl'))
Expand All @@ -22,4 +23,5 @@ export {
InstantValidationBoundaryContext,
PlaceValidationBoundaryBelowThisLevel,
RenderValidationBoundaryAtThisLevel,
SlotMarker,
}
2 changes: 1 addition & 1 deletion packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4766,7 +4766,7 @@ async function validateInstantConfigs(
)

const instantValidationState = createInstantValidationState(
payloadResult.createInstantStack
payloadResult.slotStacks
)

const validationSampleTracking =
Expand Down
78 changes: 57 additions & 21 deletions packages/next/src/server/app-render/dynamic-rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ import {
import { scheduleOnNextTick } from '../../lib/scheduler'
import { BailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr'
import { InvariantError } from '../../shared/lib/invariant-error'
import { INSTANT_VALIDATION_BOUNDARY_NAME } from './instant-validation/boundary-constants'
import {
INSTANT_VALIDATION_BOUNDARY_NAME,
INSTANT_SLOT_MARKER_PREFIX,
INSTANT_SLOT_MARKER_SUFFIX,
} from './instant-validation/boundary-constants'
import type { ValidationBoundaryTracking } from './instant-validation/boundary-tracking'
import type { InstantValidationSampleTracking } from './instant-validation/instant-samples'

Expand Down Expand Up @@ -742,6 +746,34 @@ const hasOutletRegex = new RegExp(`\\n\\s+at ${OUTLET_BOUNDARY_NAME}[\\n\\s]`)
const hasInstantValidationBoundaryRegex = new RegExp(
`\\n\\s+at ${INSTANT_VALIDATION_BOUNDARY_NAME}[\\n\\s]`
)
const slotMarkerRegex = new RegExp(
`\\n\\s+at ${INSTANT_SLOT_MARKER_PREFIX}(\\d+)${INSTANT_SLOT_MARKER_SUFFIX}[\\n\\s]`
)

/** Look up the config factory for the slot this error belongs to.
* Checks the component stack for a slot marker (__next_instant_slot_N__)
* and returns the config at that index. Falls back to index 0 (root
* config) when no slot marker is found or the slot has no config. */
function resolveInstantStack(
componentStack: string,
dynamicValidation: InstantValidationState
): (() => Error) | null {
const { slotStacks } = dynamicValidation
if (slotStacks.length > 1) {
const match = slotMarkerRegex.exec(componentStack)
if (match) {
// Slot markers are 0-indexed in the component name but
// slotStacks is 1-indexed (index 0 is the root config).
const slotIndex = parseInt(match[1], 10) + 1
const slotStack = slotStacks[slotIndex]
if (slotStack != null) {
return slotStack
}
}
}
// Fall back to root config (index 0)
return slotStacks[0] ?? null
}

export function trackAllowedDynamicAccess(
workStore: WorkStore,
Expand Down Expand Up @@ -809,11 +841,13 @@ export type InstantValidationState = {
dynamicErrors: Array<Error>
validationPreventingErrors: Array<Error>
thrownErrorsOutsideBoundary: Array<unknown>
createInstantStack: (() => Error) | null
/** Per-slot config factories. Index 0 is the root config (fallback).
* Indices 1+ correspond to slot marker components in the tree. */
slotStacks: Array<(() => Error) | null>
}

export function createInstantValidationState(
createInstantStack: (() => Error) | null
slotStacks: Array<(() => Error) | null>
): InstantValidationState {
return {
hasDynamicMetadata: false,
Expand All @@ -824,7 +858,7 @@ export function createInstantValidationState(
dynamicErrors: [],
validationPreventingErrors: [],
thrownErrorsOutsideBoundary: [],
createInstantStack,
slotStacks,
}
}

Expand All @@ -840,6 +874,14 @@ export function trackDynamicHoleInNavigation(
// We don't need to track that this is dynamic. It is only so when something else is also dynamic.
return
}
// Resolve the config stack for this specific error. If the error
// is inside a slot marker, use that slot's config. Otherwise fall
// back to the default.
const effectiveCreateInstantStack = resolveInstantStack(
componentStack,
dynamicValidation
)

if (hasMetadataRegex.test(componentStack)) {
const usageDescription =
kind === DynamicHoleKind.Runtime
Expand All @@ -849,7 +891,7 @@ export function trackDynamicHoleInNavigation(
const error = addErrorContext(
new Error(message),
componentStack,
dynamicValidation.createInstantStack
effectiveCreateInstantStack
)
dynamicValidation.dynamicMetadata = error
return
Expand All @@ -863,7 +905,7 @@ export function trackDynamicHoleInNavigation(
const error = addErrorContext(
new Error(message),
componentStack,
dynamicValidation.createInstantStack
effectiveCreateInstantStack
)
dynamicValidation.dynamicErrors.push(error)
return
Expand Down Expand Up @@ -893,7 +935,7 @@ export function trackDynamicHoleInNavigation(
const error = addErrorContext(
new Error(message),
componentStack,
dynamicValidation.createInstantStack
effectiveCreateInstantStack
)
dynamicValidation.validationPreventingErrors.push(error)
return
Expand Down Expand Up @@ -933,11 +975,8 @@ export function trackDynamicHoleInNavigation(
if (clientDynamic.syncDynamicErrorWithStack) {
// This task was the task that called the sync error.
const syncError = clientDynamic.syncDynamicErrorWithStack
if (
dynamicValidation.createInstantStack !== null &&
syncError.cause === undefined
) {
syncError.cause = dynamicValidation.createInstantStack()
if (effectiveCreateInstantStack !== null && syncError.cause === undefined) {
syncError.cause = effectiveCreateInstantStack()
}
dynamicValidation.dynamicErrors.push(syncError)
return
Expand All @@ -951,7 +990,7 @@ export function trackDynamicHoleInNavigation(
const error = addErrorContext(
new Error(message),
componentStack,
dynamicValidation.createInstantStack
effectiveCreateInstantStack
)
dynamicValidation.dynamicErrors.push(error)
return
Expand Down Expand Up @@ -1294,26 +1333,23 @@ export function getNavigationDisallowedDynamicReasons(
}

if (boundaryState.renderedIds.size < boundaryState.expectedIds.size) {
const { thrownErrorsOutsideBoundary, createInstantStack } =
dynamicValidation
const { thrownErrorsOutsideBoundary } = dynamicValidation
const rootInstantStack = dynamicValidation.slotStacks[0]
if (thrownErrorsOutsideBoundary.length === 0) {
const message = `Route "${workStore.route}": Could not validate \`unstable_instant\` because the target segment was prevented from rendering for an unknown reason.`
const error =
createInstantStack !== null ? createInstantStack() : new Error()
const error = rootInstantStack !== null ? rootInstantStack() : new Error()
error.name = 'Error'
error.message = message
return [error]
} else if (thrownErrorsOutsideBoundary.length === 1) {
const message = `Route "${workStore.route}": Could not validate \`unstable_instant\` because the target segment was prevented from rendering, likely due to the following error.`
const error =
createInstantStack !== null ? createInstantStack() : new Error()
const error = rootInstantStack !== null ? rootInstantStack() : new Error()
error.name = 'Error'
error.message = message
return [error, thrownErrorsOutsideBoundary[0] as Error]
} else {
const message = `Route "${workStore.route}": Could not validate \`unstable_instant\` because the target segment was prevented from rendering, likely due to one of the following errors.`
const error =
createInstantStack !== null ? createInstantStack() : new Error()
const error = rootInstantStack !== null ? rootInstantStack() : new Error()
error.name = 'Error'
error.message = message
return [error, ...(thrownErrorsOutsideBoundary as Error[])]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
export const INSTANT_VALIDATION_BOUNDARY_NAME =
'__next_instant_validation_boundary__'

export const INSTANT_SLOT_MARKER_PREFIX = '__next_instant_slot_'
export const INSTANT_SLOT_MARKER_SUFFIX = '__'
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,32 @@ const InstantValidationBoundary =
0
) as typeof INSTANT_VALIDATION_BOUNDARY_NAME
]

// Slot marker component for attributing validation errors to the
// correct config when a boundary spans multiple parallel slots.
// Renders a dynamically-named inner component so the slot index
// appears in the SSR component stack (__next_instant_slot_N__).
const slotMarkerCache = new Map<
string,
(props: { children: ReactNode }) => ReactNode
>()

export function SlotMarker({
name,
children,
}: {
name: string
children: ReactNode
}) {
let Marker = slotMarkerCache.get(name)
if (!Marker) {
const ns = {
[name]: function ({ children: c }: { children: ReactNode }) {
return c
},
}
Marker = ns[name]
slotMarkerCache.set(name, Marker)
}
return <Marker>{children}</Marker>
}
Loading
Loading