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
3 changes: 2 additions & 1 deletion docs/01-app/01-getting-started/04-linking-and-navigating.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ related:
links:
- app/api-reference/components/link
- app/api-reference/file-conventions/loading
- app/guides/optimizing-prefetching
- app/guides/prefetching
---

Expand Down Expand Up @@ -95,7 +96,7 @@ By skipping or partially prefetching dynamic routes, Next.js avoids unnecessary
height="748"
/>

> **Good to know:** See the [Prefetching guide](/docs/app/guides/prefetching) for the full behavior, including how to control prefetching per link and how it changes when you adopt [Partial Prefetching](/docs/app/guides/adopting-partial-prefetching).
> **Good to know:** See the [Prefetching guide](/docs/app/guides/prefetching) for the full behavior, including how to control prefetching per link and how it changes when you adopt [Partial Prefetching](/docs/app/guides/adopting-partial-prefetching). To include URL-dependent content in a link's prefetch, see [Optimizing prefetching](/docs/app/guides/optimizing-prefetching).

To improve the navigation experience to dynamic routes, you can use [streaming](#streaming).

Expand Down
96 changes: 35 additions & 61 deletions evals/evals/agent-035-connection-dynamic/EVAL.ts
Original file line number Diff line number Diff line change
@@ -1,76 +1,50 @@
/**
* connection() for Dynamic Rendering
*
* Tests whether the agent uses connection() from next/server to force dynamic
* rendering instead of the deprecated unstable_noStore().
*
* Tricky because agents use unstable_noStore(), force-dynamic segment config,
* or unrelated Dynamic APIs instead of the stable connection() function.
*/
/** Cache Components: synchronous request-time I/O in shared UI. */

import { expect, test } from 'vitest'
import { readFileSync, existsSync, readdirSync } from 'fs'
import { existsSync, readFileSync, readdirSync, statSync } from 'fs'
import { join } from 'path'

function readAppFiles(): string {
const appDir = join(process.cwd(), 'app')
if (!existsSync(appDir)) return ''
const entries = readdirSync(appDir, { recursive: true }) as string[]
const files = entries.filter((f) => f.endsWith('.tsx') || f.endsWith('.ts'))
return files.map((f) => readFileSync(join(appDir, f), 'utf-8')).join('\n')
import { environment } from '@vercel/agent-eval/eval'

const IGNORE_DIRS = new Set(['.git', '.next', 'node_modules', 'dist', 'build'])

function readSource(dir: string): string[] {
if (!existsSync(dir)) return []
return readdirSync(dir).flatMap((entry) => {
if (IGNORE_DIRS.has(entry)) return []
const path = join(dir, entry)
if (statSync(path).isDirectory()) return readSource(path)
if (entry === 'EVAL.ts' || !/\.(ts|tsx|js|jsx)$/.test(entry)) return []
return readFileSync(path, 'utf8')
})
}

test('Component imports connection from next/server', () => {
const content = readAppFiles()
const source = readSource(process.cwd()).join('\n')
const config = readFileSync(join(process.cwd(), 'next.config.ts'), 'utf8')

// Should import connection from next/server
expect(content).toMatch(/import.*connection.*from\s+['"]next\/server['"]/)
test('enables Cache Components without opt-outs', () => {
expect(config).toMatch(/cacheComponents\s*:\s*true/)
expect(source).not.toMatch(/export\s+(?:const|var|let)\s+instant\s*=\s*false/)
})

test('Component uses await connection()', () => {
const content = readAppFiles()

// connection() returns a Promise and must be awaited
expect(content).toMatch(/await\s+connection\s*\(\s*\)/)
test('uses an explicit synchronous I/O boundary', () => {
expect(source).toMatch(/\b(?:io|connection)\s*\(/)
expect(source).toMatch(/<Suspense[\s>]/)
})

test('Component is async', () => {
const content = readAppFiles()

// Component must be async to use await
expect(content).toMatch(/async\s+function|export\s+default\s+async/)
test('keeps the operations frame useful while metadata loads', async () => {
await expect(environment).toSatisfyCriterion(
`The stable Operations navigation and page frame must remain outside the Suspense boundary used for request metadata. Accept equivalent component and file organization.`
)
})

test('Component uses Date for timestamp', () => {
const content = readAppFiles()

// Should use Date for generating timestamp
expect(content).toMatch(/new\s+Date\s*\(|Date\.now\s*\(/)
test('keeps request metadata fresh', async () => {
await expect(environment).toSatisfyCriterion(
`The request identifier, current time, and sampling value must remain request-specific. Render them in a focused async child that awaits io() or connection() before calling randomUUID, new Date or Date.now, and Math.random. Prefer io() unless the implementation specifically needs to wait for a real user request. Accept equivalent component and file organization.`
)
})

test('Does NOT use unstable_noStore (deprecated)', () => {
const content = readAppFiles()

// Should NOT use deprecated unstable_noStore
expect(content).not.toMatch(/unstable_noStore/)
})

test('Does NOT use force-dynamic segment config as primary approach', () => {
const content = readAppFiles()

// Should use connection() as the primary approach instead of segment config
const hasForceDynamic =
content.includes("dynamic = 'force-dynamic'") ||
content.includes('dynamic = "force-dynamic"')
const hasConnection = /connection\s*\(\s*\)/.test(content)

// Enforce that connection() is the primary dynamic approach
// If force-dynamic is used, it cannot be the only dynamic mechanism
// When connection() is available/used, force-dynamic should not be redundantly added
if (hasForceDynamic && !hasConnection) {
// force-dynamic is being used as the sole dynamic mechanism (wrong)
expect.fail(
'force-dynamic segment config should not be the primary approach - use connection() instead'
)
}
test('does not reuse request metadata', async () => {
await expect(environment).toSatisfyCriterion(
`The request identifier, current time, and sampling value must not be cached, evaluated at module scope, or replaced with static values. Reject any solution that can reuse those values across requests. Accept equivalent component and file organization.`
)
})
2 changes: 1 addition & 1 deletion evals/evals/agent-035-connection-dynamic/PROMPT.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Create a server component that displays the current server timestamp on each request. The timestamp should be different on every page load (not prerendered at build time). Use Next.js's recommended approach for opting out of static prerendering when you need dynamic data without using cookies/headers.
Enable Cache Components for this operations console. The navigation must stay immediately useful, while the request identifier, current time, and sampling value must still be generated per request. Finish with a production build and do not leave routes opted out.
20 changes: 8 additions & 12 deletions evals/evals/agent-035-connection-dynamic/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import type { Metadata } from 'next'
import type { ReactNode } from 'react'
import { RequestMetadata } from '@/components/request-metadata'

export const metadata: Metadata = {
title: 'Random Number',
description: 'Display a random number on each request',
}

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
<body>
<nav>Operations</nav>
<RequestMetadata />
{children}
</body>
</html>
)
}
6 changes: 3 additions & 3 deletions evals/evals/agent-035-connection-dynamic/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export default function Home() {
export default function HomePage() {
return (
<main>
<h1>Random Number Generator</h1>
<p>Please implement a random number that changes on each request.</p>
<h1>Deployment health</h1>
<p>All systems operational</p>
</main>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { randomUUID } from 'node:crypto'

export function RequestMetadata() {
const generatedAt = new Date().toISOString()
const sample = Math.random()
const requestId = randomUUID()

return (
<aside>
Request {requestId} generated at {generatedAt} (sample {sample})
</aside>
)
}
2 changes: 1 addition & 1 deletion evals/evals/agent-035-connection-dynamic/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@
"target": "ES2017"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"exclude": ["node_modules", "EVAL.ts"]
}
80 changes: 28 additions & 52 deletions evals/evals/agent-040-instant/EVAL.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,43 @@
/**
* Instant Navigations
* Instant Validation
*
* Verifies that the agent enables instant navigation on the product route.
*
* The eval harness runs `next build` as a script — with `cacheComponents: true`,
* the build validates that the caching structure produces an instant static shell.
* This test checks the agent actually exported `instant` (the build would
* pass without it, since Suspense alone satisfies the build on a static route).
* Starts with a non-instant navigation and verifies that the agent follows the
* framework's default validation feedback to produce a meaningful Cache
* Components shell: the stable title is available immediately while live
* inventory streams behind a Suspense boundary.
*/

import { expect, test } from 'vitest'
import { readFileSync, existsSync, readdirSync, statSync } from 'fs'
import { join } from 'path'

type SourceFile = { path: string; content: string }

const IGNORE_DIRS = new Set([
'.git',
'.next',
'node_modules',
'dist',
'build',
'coverage',
])
import { environment } from '@vercel/agent-eval/eval'

const IGNORE_FILES = new Set(['EVAL.ts', 'PROMPT.md'])
test('keeps the product title in the static shell', async () => {
await expect(environment).toSatisfyCriterion(
`The product title "Premium Widget" must render in the product page's static shell. It must remain outside the Suspense boundary and async work used for the live inventory, so the title is available before the inventory resolves.

function readSourceFiles(dir: string): SourceFile[] {
if (!existsSync(dir)) return []

const files: SourceFile[] = []
for (const entry of readdirSync(dir)) {
if (IGNORE_DIRS.has(entry)) continue

const fullPath = join(dir, entry)
const stats = statSync(fullPath)
Accept equivalent component and file organization. Reject solutions where the page awaits the inventory before returning or places the title behind the inventory boundary.`
)
})

if (stats.isDirectory()) {
files.push(...readSourceFiles(fullPath))
continue
}
test('streams inventory behind a meaningful fallback', async () => {
await expect(environment).toSatisfyCriterion(
`The inventory count and price must render from an async child beneath a Suspense boundary with a visible, meaningful fallback. The product page must be able to return its title and fallback without waiting for getInventory().

if (IGNORE_FILES.has(entry)) continue
Accept equivalent component and file organization. Reject a null or empty fallback, or a solution where the page awaits inventory before returning.`
)
})

if (/\.(ts|tsx|js|jsx)$/.test(entry)) {
files.push({
path: fullPath,
content: readFileSync(fullPath, 'utf-8'),
})
}
}
test('keeps inventory live and uncached', async () => {
await expect(environment).toSatisfyCriterion(
`The randomized inventory must remain request-time data. Preserve the runtime boundary in getInventory(), such as connection(), and do not cache the inventory or replace its count and price with static values.

return files
}
Accept equivalent component and file organization. Reject any solution that makes the randomized inventory reusable across requests.`
)
})

const sourceFiles = readSourceFiles(process.cwd())
test('keeps default instant validation enabled', async () => {
await expect(environment).toSatisfyCriterion(
`The solution must keep the framework's default instant validation enabled and solve the static-shell problem directly. It must not add instant = false, disable Cache Components, or use Partial Prefetching as a substitute for a valid product shell.

test('instant exported from the product route', () => {
const productPage = sourceFiles.find(
(f) => f.path.includes('product') && /page\.(tsx?|jsx?)$/.test(f.path)
Accept equivalent component and file organization.`
)

expect(productPage).toBeDefined()
expect(productPage!.content).toMatch(/export\s+(const|var|let)\s+instant\b/)
expect(productPage!.content).toMatch(/prefetch\s*:\s*['"]static['"]/)
})
2 changes: 1 addition & 1 deletion evals/evals/agent-040-instant/PROMPT.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Navigating from home to the product page is slow. The title should appear immediately.
Navigating from home to the product page is slow. Make the product title appear immediately while the live inventory loads.
2 changes: 1 addition & 1 deletion evals/evals/agent-040-instant/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@
"target": "ES2017"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"exclude": ["node_modules", "EVAL.ts"]
}
55 changes: 38 additions & 17 deletions evals/evals/agent-042-enable-ppr/EVAL.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,52 @@
/**
* Enable PPR
* Cache Components: route config semantics
*
* Tests whether the agent knows that partial pre-rendering is enabled via
* `cacheComponents: true` in next.config.ts, NOT the old
* `experimental: { ppr: true }` flag.
*
* Tricky because most training data and older docs reference the experimental
* flag. The current way to enable PPR in Next.js 16 is `cacheComponents: true`.
* Verifies that adoption translates legacy configuration instead of deleting
* behavior or silencing validation.
*/

import { expect, test } from 'vitest'
import { readFileSync } from 'fs'
import { existsSync, readFileSync, readdirSync, statSync } from 'fs'
import { join } from 'path'
import { environment } from '@vercel/agent-eval/eval'

const IGNORE_DIRS = new Set(['.git', '.next', 'node_modules', 'dist', 'build'])

function readSource(dir: string): string[] {
if (!existsSync(dir)) return []
return readdirSync(dir).flatMap((entry) => {
if (IGNORE_DIRS.has(entry)) return []
const path = join(dir, entry)
if (statSync(path).isDirectory()) return readSource(path)
if (entry === 'EVAL.ts' || !/\.(ts|tsx|js|jsx)$/.test(entry)) return []
return readFileSync(path, 'utf8')
})
}

test('Enables PPR via cacheComponents in next.config', () => {
const config = readFileSync(join(process.cwd(), 'next.config.ts'), 'utf-8')
const source = readSource(process.cwd()).join('\n')
const config = readFileSync(join(process.cwd(), 'next.config.ts'), 'utf8')

test('enables the current Cache Components config only', () => {
expect(config).toMatch(/cacheComponents\s*:\s*true/)
expect(config).not.toMatch(/dynamicIO\s*:/)
expect(config).not.toMatch(/useCache\s*:/)
})

test('Does not use the old experimental.ppr flag', () => {
const config = readFileSync(join(process.cwd(), 'next.config.ts'), 'utf-8')
test('removes route opt-outs and incompatible segment config', () => {
expect(source).not.toMatch(/export\s+(?:const|var|let)\s+instant\s*=\s*false/)
expect(source).not.toMatch(
/export\s+(?:const|var|let)\s+(?:dynamic|revalidate|fetchCache)\s*=/
)
})

// Strip comments to avoid false positives from explanatory comments
const stripped = config
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/\/\/.*$/gm, '')
test('preserves the hourly newsroom feed', async () => {
await expect(environment).toSatisfyCriterion(
`The newsroom feed must remain cached with an explicit approximately-hourly cache lifetime. A focused data-level cache is valid, and a page-level cache is also valid because the Newsroom page contains only the feed and stable framing with the same lifetime. Reject solutions that remove or materially change the feed's hourly refresh behavior.`
)
})

expect(stripped).not.toMatch(/ppr\s*:\s*true/)
test('keeps editor preview request-specific', async () => {
await expect(environment).toSatisfyCriterion(
`The editor preview must still read its cookie at request time outside every public cache. Place the request-specific preview below Suspense or equivalent meaningful loading UI while leaving useful stable preview framing in the static shell. Reject solutions that cache the cookie value, hide the entire preview page behind one fallback, or remove the request-specific behavior.`
)
})
2 changes: 1 addition & 1 deletion evals/evals/agent-042-enable-ppr/PROMPT.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Enable partial pre-rendering for this app.
Move this publishing app to Cache Components in one branch. Keep the newsroom feed on its hourly refresh policy and keep editor preview request-specific. Finish with a production build without leaving routes opted out.
18 changes: 0 additions & 18 deletions evals/evals/agent-042-enable-ppr/app/ProductList.tsx

This file was deleted.

Loading
Loading