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
2 changes: 2 additions & 0 deletions .agents/skills/authoring-skills/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ description: >
Covers the supported spec fields, description writing, naming conventions,
and the relationship between always-loaded AGENTS.md and on-demand skills.
user-invocable: false
metadata:
internal: true
---

# Authoring Skills
Expand Down
2 changes: 2 additions & 0 deletions .agents/skills/dce-edge/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ description: >
Covers if/else branching for webpack DCE, TypeScript definite assignment,
the NEXT_RUNTIME vs real feature flag distinction, and forcing flags false
for edge in define-env.ts.
metadata:
internal: true
---

# DCE + Edge
Expand Down
2 changes: 2 additions & 0 deletions .agents/skills/flags/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ description: >
next-server.ts, export/worker.ts, or module.compiled.js. Covers type
declaration, zod schema, build-time injection, runtime env plumbing,
and the decision between runtime env-var branching vs separate bundle variants.
metadata:
internal: true
---

# Feature Flags
Expand Down
1 change: 1 addition & 0 deletions .agents/skills/gh-stack/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ description: >
metadata:
author: github
version: '0.0.2'
internal: true
---

# gh-stack
Expand Down
2 changes: 2 additions & 0 deletions .agents/skills/pr-status-triage/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ description: >
Covers blocker-first prioritization (build > lint > types > tests),
CI env var matching for local reproduction, and the Known Flaky Tests
distinction.
metadata:
internal: true
---

# PR Status Triage
Expand Down
2 changes: 2 additions & 0 deletions .agents/skills/react-vendoring/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ description: >
React channels, type declarations, Turbopack remap to
react-server-dom-turbopack, ComponentMod access patterns, and ESLint
suppression for guarded requires.
metadata:
internal: true
---

# React Vendoring
Expand Down
2 changes: 2 additions & 0 deletions .agents/skills/router-act/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ description: >
Covers the act API, fixture patterns, prefetch control via LinkAccordion,
fake clocks, and avoiding flaky testing patterns.
user-invocable: false
metadata:
internal: true
---

# Router Act Testing
Expand Down
2 changes: 2 additions & 0 deletions .agents/skills/runtime-debug/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ description: >
traces, or runtime bundle selection (module.compiled.js). Covers CI env
mirroring, full stack traces via __NEXT_SHOW_IGNORE_LISTED, route trace
inspection, and webpack stats diffing.
metadata:
internal: true
---

# Runtime Debug
Expand Down
2 changes: 2 additions & 0 deletions .agents/skills/update-docs/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
name: update-docs
description: This skill should be used when the user asks to "update documentation for my changes", "check docs for this PR", "what docs need updating", "sync docs with code", "scaffold docs for this feature", "document this feature", "review docs completeness", "add docs for this change", "what documentation is affected", "docs impact", or mentions "docs/", "docs/01-app", "docs/02-pages", "MDX", "documentation update", "API reference", ".mdx files". Provides guided workflow for updating Next.js documentation based on code changes.
metadata:
internal: true
---

# Next.js Documentation Updater
Expand Down
2 changes: 2 additions & 0 deletions .agents/skills/v8-jit/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ description: >
megamorphic deopt, closure allocation, array packing, and profiling with
--trace-opt / --trace-deopt.
user-invocable: false
metadata:
internal: true
---

# V8 JIT Optimization
Expand Down
2 changes: 2 additions & 0 deletions .agents/skills/write-api-reference/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ description: |
**Output type:** A markdown (.mdx) API reference page with YAML frontmatter, usage example, reference section, behavior notes, and examples.
agent: Plan
context: fork
metadata:
internal: true
---

# Writing API Reference Pages
Expand Down
2 changes: 2 additions & 0 deletions .agents/skills/write-guide/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ description: |
**Output type:** A markdown guide with YAML frontmatter, introduction, 2-4 progressive steps, and next steps section.
agent: Plan
context: fork
metadata:
internal: true
---

# Writing Guides
Expand Down
237 changes: 237 additions & 0 deletions packages/next/src/next-devtools/dev-overlay/container/errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import {
createDynamicBodyError,
createDynamicMetadataError,
createDynamicOrRuntimeBodyError,
createDynamicOrRuntimeMetadataError,
createDynamicOrRuntimeViewportError,
createDynamicViewportError,
createRuntimeBodyError,
createRuntimeMetadataError,
createRuntimeViewportError,
} from '../../../server/app-render/blocking-route-messages'
import {
createSyncIOClientError,
createSyncIOError,
createSyncIORuntimeError,
type SyncIOApiType,
} from '../../../server/app-render/sync-io-messages'
import {
getBlockingRouteErrorDetails,
isRuntimeVariant,
isSyncIOClientError,
isSyncIOError,
} from './errors'

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)
})

it('returns false for dynamic body factory output', () => {
expect(isRuntimeVariant(createDynamicBodyError(ROUTE).message)).toBe(false)
})

it('returns true for runtime metadata factory output', () => {
expect(isRuntimeVariant(createRuntimeMetadataError(ROUTE).message)).toBe(
true
)
})

it('returns false for dynamic metadata factory output', () => {
expect(isRuntimeVariant(createDynamicMetadataError(ROUTE).message)).toBe(
false
)
})

it('returns true for runtime viewport factory output', () => {
expect(isRuntimeVariant(createRuntimeViewportError(ROUTE).message)).toBe(
true
)
})

it('returns false for dynamic viewport factory output', () => {
expect(isRuntimeVariant(createDynamicViewportError(ROUTE).message)).toBe(
false
)
})
})

describe('isSyncIOError', () => {
it.each<[SyncIOApiType]>([['time'], ['random'], ['crypto']])(
'returns true for createSyncIOError(%s)',
(type) => {
const message = createSyncIOError(ROUTE, 'expr', type).message
expect(isSyncIOError(message)).toBe(true)
}
)

it.each<[SyncIOApiType]>([['time'], ['random'], ['crypto']])(
'returns true for createSyncIORuntimeError(%s)',
(type) => {
const message = createSyncIORuntimeError(ROUTE, 'expr', type).message
expect(isSyncIOError(message)).toBe(true)
}
)

it.each<[SyncIOApiType]>([['time'], ['random'], ['crypto']])(
'returns true for createSyncIOClientError(%s)',
(type) => {
const message = createSyncIOClientError(ROUTE, 'expr', type).message
expect(isSyncIOError(message)).toBe(true)
}
)

it('returns false for non sync-IO factory output', () => {
expect(isSyncIOError(createRuntimeBodyError(ROUTE).message)).toBe(false)
expect(isSyncIOError(createDynamicMetadataError(ROUTE).message)).toBe(false)
})

it('returns false for an unrelated error message', () => {
expect(isSyncIOError('Random unrelated error text')).toBe(false)
})
})

describe('isSyncIOClientError', () => {
it.each<[SyncIOApiType]>([['time'], ['random'], ['crypto']])(
'returns true for createSyncIOClientError(%s)',
(type) => {
const message = createSyncIOClientError(ROUTE, 'expr', type).message
expect(isSyncIOClientError(message)).toBe(true)
}
)

it.each<[SyncIOApiType]>([['time'], ['random'], ['crypto']])(
'returns false for createSyncIOError(%s)',
(type) => {
const message = createSyncIOError(ROUTE, 'expr', type).message
expect(isSyncIOClientError(message)).toBe(false)
}
)

it.each<[SyncIOApiType]>([['time'], ['random'], ['crypto']])(
'returns false for createSyncIORuntimeError(%s)',
(type) => {
const message = createSyncIORuntimeError(ROUTE, 'expr', type).message
expect(isSyncIOClientError(message)).toBe(false)
}
)
})

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

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

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

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

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

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

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

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

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

it.each<[SyncIOApiType, string, string]>([
['time', 'Date.now()', 'Date.now()'],
['random', 'Math.random()', 'Math.random()'],
['crypto', 'crypto.randomUUID()', 'crypto.randomUUID()'],
])(
'classifies createSyncIOError(%s) as sync-io + cause %s',
(type, expression, expectedCause) => {
expect(
getBlockingRouteErrorDetails(createSyncIOError(ROUTE, expression, type))
).toEqual({ type: 'sync-io', cause: expectedCause })
}
)

it.each<[SyncIOApiType, string, string]>([
['time', 'Date.now()', 'Date.now()'],
['random', 'Math.random()', 'Math.random()'],
['crypto', 'crypto.randomUUID()', 'crypto.randomUUID()'],
])(
'classifies createSyncIOClientError(%s) as sync-io-client + cause %s',
(type, expression, expectedCause) => {
expect(
getBlockingRouteErrorDetails(
createSyncIOClientError(ROUTE, expression, type)
)
).toEqual({ type: 'sync-io-client', cause: expectedCause })
}
)

// The time-type factory always appends `elapsedTimeBullet` text containing
// `Date.now()` regardless of which API the user actually called. If
// SYNC_IO_APIS is ordered wrong, `Date.now()` will match the bullet text
// and shadow the real cause.
it.each<[string, string]>([
['Date.now()', 'Date.now()'],
['new Date()', 'new Date()'],
['Date()', 'Date()'],
])(
'preserves cause %s against the `Date.now()` mention in the time bullet',
(expression, expectedCause) => {
const error = createSyncIOError(ROUTE, expression, 'time')
expect(getBlockingRouteErrorDetails(error)).toEqual({
type: 'sync-io',
cause: expectedCause,
})
}
)

it('returns null for an unrelated error', () => {
expect(getBlockingRouteErrorDetails(new Error('regular bug'))).toBe(null)
})
})
16 changes: 10 additions & 6 deletions packages/next/src/next-devtools/dev-overlay/container/errors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ function InstantRuntimeError({
)
}

function isRuntimeVariant(message: string): boolean {
export function isRuntimeVariant(message: string): boolean {
// Discriminates between `createRuntimeBodyError` and `createDynamicBodyError`
return (
message.includes('encountered runtime data') &&
Expand All @@ -280,10 +280,12 @@ function isRuntimeVariant(message: string): boolean {
const SYNC_IO_APIS = [
// Math
'Math.random()',
// Date/Time — `new Date()` before `Date()` to avoid substring false positive
'Date.now()',
// Date/Time — `new Date()` before `Date()` (substring false positive) and
// both before `Date.now()` (the `elapsedTimeBullet` text always contains
// `Date.now()` regardless of which API the user actually called).
'new Date()',
'Date()',
'Date.now()',
// Node Crypto — longer strings first to avoid substring false positives
"require('node:crypto').generateKeyPairSync(...)",
"require('node:crypto').generateKeySync(...)",
Expand All @@ -303,16 +305,18 @@ const SYNC_IO_DOCS_PATTERN =
// Discriminate sync IO errors via the docs URL embedded in the user-facing
// message by `createSyncIOError`, `createSyncIORuntimeError`, and
// `createSyncIOClientError`.
function isSyncIOError(message: string): boolean {
export function isSyncIOError(message: string): boolean {
return SYNC_IO_DOCS_PATTERN.test(message)
}

function isSyncIOClientError(message: string): boolean {
export function isSyncIOClientError(message: string): boolean {
const match = SYNC_IO_DOCS_PATTERN.exec(message)
return match !== null && match[2] === '-client'
}

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

const isBlockingPageLoadError = message.includes('/blocking-route')
Expand Down
Loading
Loading