Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
@@ -0,0 +1,11 @@
'use client'

import { PathnameReader } from './pathname-reader'

export function InnerWrapper() {
return (
<p>
Current path: <PathnameReader />
</p>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use client'

import { InnerWrapper } from './inner-wrapper'

export function MiddleWrapper() {
return (
<div>
<InnerWrapper />
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use client'

import { MiddleWrapper } from './middle-wrapper'

export function OuterWrapper() {
return (
<section>
<MiddleWrapper />
</section>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Instant } from 'next'
import { OuterWrapper } from './outer-wrapper'

export const unstable_instant: Instant = {
level: 'experimental-error',
unstable_samples: [
{
params: {
one: '123',
// two: <missing>
},
},
],
}

export default function Page() {
return (
<main>
<p>
usePathname() called from a Client Component nested three wrapper
components deep on a route with dynamic params where not all params are
provided in the sample. The build-time validation error should still
point at the actual call site in the user's source, not at a generic{' '}
<code>{'<unknown>'}</code> frame.
</p>
<OuterWrapper />
</main>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use client'

import { usePathname } from 'next/navigation'

export function PathnameReader() {
const pathname = usePathname()
return <span data-testid="pathname">{pathname}</span>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Instant } from 'next'
import { PathnameReader } from './pathname-reader'

export const unstable_instant: Instant = {
level: 'experimental-error',
unstable_samples: [
{
params: {
one: '123',
// two: <missing>
},
},
],
}

export default function Page() {
return (
<main>
<p>
usePathname() called directly at the top of a Client Component body (no{' '}
<code>ensureThrows()</code> wrapper) on a route with dynamic params
where not all params are provided in the sample. The build-time
validation error should point at the call site in the user's source.
</p>
<PathnameReader />
</main>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use client'

import { usePathname } from 'next/navigation'

export function PathnameReader() {
const pathname = usePathname()
return <span data-testid="pathname">{pathname}</span>
}
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,82 @@ describe('instant-validation-build', () => {
expect(result.cliOutput).not.toContain('AssertionError')
expect(result.exitCode).toBe(1)
})

it('error - usePathname() at the top of a Client Component body (no ensureThrows wrapper)', async () => {
const result = await prerender(
'/(default)/pathname/invalid-use-pathname-missing-params-no-wrapper/[one]/[two]'
)
// Snapshot the full validation error so any change to the stack frame
// attribution (good or bad) shows up clearly. Today's behavior: the file,
// line, and column are correct, but the function name is reported as
// `<unknown>` instead of `PathnameReader`. When the framework starts
// recovering the real function name from sourcemaps, update this snapshot.
expect(extractBuildValidationError(result.cliOutput))
.toMatchInlineSnapshot(`
"Error: Route "/pathname/invalid-use-pathname-missing-params-no-wrapper/[one]/[two]" called usePathname() but param "two" is not defined in the \`unstable_samples\` of \`unstable_instant\`. usePathname() requires all route params to be provided.
at <unknown> (app/(default)/pathname/invalid-use-pathname-missing-params-no-wrapper/[one]/[two]/pathname-reader.tsx:6:20)
Comment on lines +827 to +828
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't really tell us stack attribution is working because it's just the sync error from misuing samples. The codeframe is the one we finally want. But it's broken at the moment.

4 |
5 | export function PathnameReader() {
> 6 | const pathname = usePathname()
| ^
7 | return <span data-testid="pathname">{pathname}</span>
8 | }
9 | {
digest: 'INSTANT_VALIDATION_ERROR'
}
Build-time instant validation failed for route "/pathname/invalid-use-pathname-missing-params-no-wrapper/[one]/[two]".
To get a more detailed stack trace and pinpoint the issue, try one of the following:
- Start the app in development mode by running \`next dev\`, then open "/pathname/invalid-use-pathname-missing-params-no-wrapper/[one]/[two]" in your browser to investigate the error.
- Rerun the production build with \`next build --debug-prerender\` to generate better stack traces.
Stopping prerender due to instant validation errors."
`)
expect(result.cliOutput).not.toContain('AssertionError')
expect(result.exitCode).toBe(1)
})

it('error - usePathname() called from a Client Component nested three wrappers deep', async () => {
const result = await prerender(
'/(default)/pathname/invalid-use-pathname-deep-component/[one]/[two]'
)
// Even when the offending hook call is buried under multiple Client
// Component wrappers, the build-time validation error should identify
// the actual call site inside `pathname-reader.tsx` (not a wrapper file).
// Today's behavior: file/line/column are correct, but the function name
// in the stack frame is an SWC-minified single letter instead of
// `PathnameReader`. The minified name is not deterministic across
// module-graph orderings, so normalize it to `<minified>` before
// snapshotting.
const error = extractBuildValidationError(result.cliOutput).replace(
/at [a-z] \(app\/\(default\)/g,
'at <minified> (app/(default)'
)
expect(error).toMatchInlineSnapshot(`
"Error: Route "/pathname/invalid-use-pathname-deep-component/[one]/[two]" called usePathname() but param "two" is not defined in the \`unstable_samples\` of \`unstable_instant\`. usePathname() requires all route params to be provided.
at <minified> (app/(default)/pathname/invalid-use-pathname-deep-component/[one]/[two]/pathname-reader.tsx:6:20)
4 |
5 | export function PathnameReader() {
> 6 | const pathname = usePathname()
| ^
7 | return <span data-testid="pathname">{pathname}</span>
8 | }
9 | {
digest: 'INSTANT_VALIDATION_ERROR'
}
Build-time instant validation failed for route "/pathname/invalid-use-pathname-deep-component/[one]/[two]".
To get a more detailed stack trace and pinpoint the issue, try one of the following:
- Start the app in development mode by running \`next dev\`, then open "/pathname/invalid-use-pathname-deep-component/[one]/[two]" in your browser to investigate the error.
- Rerun the production build with \`next build --debug-prerender\` to generate better stack traces.
Stopping prerender due to instant validation errors."
`)
// Wrapper components should not appear in the call chain — `usePathname`
// is called directly inside `PathnameReader`, not inside its wrappers.
const rawError = extractBuildValidationError(result.cliOutput)
expect(rawError).not.toContain('outer-wrapper.tsx')
expect(rawError).not.toContain('middle-wrapper.tsx')
expect(rawError).not.toContain('inner-wrapper.tsx')
expect(result.cliOutput).not.toContain('AssertionError')
expect(result.exitCode).toBe(1)
})
})

describe('root params', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { PathnameLabel } from './pathname-label'

// No `unstable_instant` config at all — the [slug] param becomes a fallback
// route param during validation, which means usePathname() suspends. This is
// the natural user shape and matches the test-app's repro at
// `88-client-use-pathname/[slug]/page.tsx`.

export default function Page() {
return (
<main>
<PathnameLabel />
</main>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use client'

import { usePathname } from 'next/navigation'

export function PathnameLabel() {
const pathname = usePathname()
return <span data-testid="pathname-label">{pathname}</span>
}
3 changes: 3 additions & 0 deletions test/e2e/app-dir/instant-validation/app/default/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export default async function Page() {
<li>
<DebugLinks href="/default/static/valid-blocking-inside-static" />
</li>
<li>
<DebugLinks href="/default/invalid-use-pathname-no-samples/123" />
</li>
</ul>
</main>
)
Expand Down
44 changes: 44 additions & 0 deletions test/e2e/app-dir/instant-validation/instant-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1893,6 +1893,50 @@ describe('instant validation', () => {
expectNoBuildValidationErrors(result)
}
})

it('invalid - usePathname() in a client component on a route with a fallback param', async () => {
if (!isNextDev) {
// Build-mode coverage lives in
// test/e2e/app-dir/instant-validation-build/instant-validation-build.test.ts
// under the `pathname` describe block. This test asserts the dev
// overlay's frame attribution.
return
}
if (isClientNav) {
// The bug we're capturing only manifests on initial load, where the
// dev validation runs server-side and the React stack is what gets
// surfaced. Skip the client-nav case so the inline snapshot below
// doesn't have to capture two different shapes.
return
}
// The fixture lives under `default/` (no root Suspense above body) so
// the validation hole isn't swallowed before the Insight can fire.
// `[slug]` is a fallback route param with no `unstable_samples`, so
// `usePathname()` suspends during dev validation — mirroring the
// test-app's repro at `88-client-use-pathname/[slug]`.
const browser = await navigateTo(
'/default/invalid-use-pathname-no-samples/123'
)
// Snapshot the dev redbox shape so the bug surface is visible:
// today the overlay reports the parent page.tsx's render JSX as the
// source, not the `usePathname()` call inside pathname-label.tsx,
// and routes the user toward Cache / Stream / Block cards even
// though `usePathname` is URL data, not cacheable data.
await expect(browser).toDisplayCollapsedRedbox(`
{
"code": "E1265",
"description": "Next.js encountered uncached data during prerendering.",
"environmentLabel": "Server",
"label": "Blocking Route",
"source": "app/default/invalid-use-pathname-no-samples/[slug]/page.tsx (11:7) @ Page
> 11 | <PathnameLabel />
| ^",
"stack": [
"Page app/default/invalid-use-pathname-no-samples/[slug]/page.tsx (11:7)",
],
}
`)
})
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should include a dev assertion to ensure there is a proper stack.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like this? d7285c6

})

describe('client errors', () => {
Expand Down
Loading