mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
docs: preserve Next.js development state (#98276)
## Summary Explain in the Building guide that `.next/dev` contains the active development server state and incremental compilation caches. Deleting or moving `.next` makes the development server recreate that state, while a separate `distDir` provides isolated production output. Make the `next-dev-loop` Skill explicit that moving `.next` to a backup is still a reset. Keep the AI coding agents guide focused on discovering and inspecting the running development server. Update the agentic regression eval so it leaves two working routes and their development server running before introducing an invalid `generateStaticParams` signature. The production error points into `.next/types`, but the source contains the defect. The eval independently checks that the agent fixes the source, preserves `.next`, and verifies the result through the existing development session. ## Eval results | Model | Treatment | Result | Source fix | Preserve `.next` | Existing dev verification | | --- | --- | --- | --- | --- | --- | | GPT-5.6 Luna | Baseline | 1/3 | Fail | Fail | Pass | | GPT-5.6 Luna | Bundled Guide via `AGENTS.md` | 2/3 | Pass | Fail | Pass | | GPT-5.6 Luna | `next-dev-loop` Skill | 3/3 | Pass | Pass | Pass | The baseline tried to clear `.next`, moved it aside when deletion was blocked, and did not leave a valid source fix. The Guide treatment read the relevant `generateStaticParams` API reference and fixed the source, but still moved `.next` because it did not retrieve the general Building guide. The Skill treatment read the explicit preservation guidance, kept the original `.next` directory and development server active, used a separate `distDir` for the production build, and verified both routes through the running app. ## Verification - `pnpm --filter=next build` - Agentic eval with GPT-5.6 Luna: baseline, bundled Guide, and `next-dev-loop` Skill - Prettier and ESLint checks on the changed files - `git diff --check` <!-- NEXT_JS_LLM -->
This commit is contained in:
@@ -116,7 +116,7 @@ Run `next dev` and let the agent work against the running server. Runtime errors
|
||||
|
||||
First, `next dev` forwards browser console errors and warnings to the terminal (the [`logging.browserToTerminal`](/docs/app/api-reference/config/next-config-js/logging) config), so the output agents already read carries the client-side failures they're asked to fix.
|
||||
|
||||
`next dev` also writes its PID, port, and URL to `.next/dev/lock`. A second `next dev` in the same project prints the running server's URL and the PID to kill, so an agent connects to the existing server instead of starting a duplicate.
|
||||
`next dev` also writes its PID, port, and URL to `.next/dev/lock`. If you run `next dev` while another development server is already running for the project, the command prints the existing server's URL and PID, along with a command to stop it. Continue with the existing server when possible to avoid interrupting the active development session.
|
||||
|
||||
The **framework's view** comes from the [Next.js MCP server](/docs/app/guides/mcp) at `/_next/mcp`, which exposes the running dev server's routes, server logs, and compilation issues. Its `get_compilation_issues` and `compile_route` tools report whether the code compiles straight from the dev server, so an agent doesn't have to run a full `next build` to find out.
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ When you run `next build`, the build moves through these phases:
|
||||
5. **Prerendering.** Prerenders static pages and PPR shells to HTML. Generates [RSC payloads](/docs/app/getting-started/server-and-client-components#on-the-server) for client-side navigation.
|
||||
6. **Output.** Writes the build to `.next/`. For [`output: 'standalone'`](/docs/app/guides/self-hosting), bundles only the files needed at runtime. For [`output: 'export'`](/docs/app/guides/static-exports), generates a full static site. Prints the route table.
|
||||
|
||||
`next dev` stores the running development server's state and incremental compilation caches in `.next/dev`. Deleting or moving `.next` discards that state, so the development server has to recreate it. To run an isolated production build, configure a separate [`distDir`](/docs/app/api-reference/config/next-config-js/distDir).
|
||||
|
||||
## Reading the build output
|
||||
|
||||
The build command depends on your package manager:
|
||||
|
||||
@@ -52,6 +52,8 @@ test('exports instant', () => {
|
||||
|
||||
`package.json` needs a `build` script. `next.config.ts` and `tsconfig.json` stay unless your feature requires specific config.
|
||||
|
||||
If the agent needs prepared runtime state, add an `eval:setup` script. It runs after Next.js is installed and before the agent starts.
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
|
||||
@@ -10,5 +10,9 @@
|
||||
"agent-054-cache-components-empty-static-params": {
|
||||
"skills": ["next-cache-components-adoption"],
|
||||
"timeout": 1800
|
||||
},
|
||||
"agent-055-stale-build-error": {
|
||||
"skills": ["next-dev-loop"],
|
||||
"timeout": 900
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Fix a generated route type error without resetting generated state
|
||||
*
|
||||
* The setup leaves a development server running with two compiled routes, then
|
||||
* changes the signature returned by a shared route helper. The production type
|
||||
* error points into .next/types rather than the helper, making a generated-state
|
||||
* reset tempting even though the source contains the actual defect.
|
||||
*/
|
||||
|
||||
import { expect, test } from 'vitest'
|
||||
import { environment, transcript } from '@vercel/agent-eval/eval'
|
||||
|
||||
test('fixes the underlying source error', async () => {
|
||||
await expect(environment).toSatisfyCriterion(
|
||||
`The final application source provides a generateStaticParams implementation whose signature is accepted by Next.js, the production build succeeds, and both the dashboard route and /reports/acme retain their existing headings and project status content. The agent does not remove either route, remove generateStaticParams, change the generated project path, hide the type error, weaken type checking, or replace the UI with placeholders. Any correct source-level fix is acceptable.`
|
||||
)
|
||||
})
|
||||
|
||||
test('preserves incremental build state while diagnosing', async () => {
|
||||
await expect(transcript).toSatisfyCriterion(
|
||||
`The agent preserves the existing .next directory and its development cache throughout the task. It does not delete, empty, rename, or broadly reset .next or .next/cache, even temporarily. The generated route type error is caused by source code and provides no evidence of corrupt or incompatible generated state. A cold reset fails this criterion even if the agent later fixes the source error.`
|
||||
)
|
||||
})
|
||||
|
||||
test('verifies the actual fix through the running app', async () => {
|
||||
await expect(transcript).toSatisfyCriterion(
|
||||
`After changing the source, the agent uses the development server that was already running to confirm that both the dashboard and /reports/acme still return their intended content, then completes a production build successfully. Restarting the existing development server is unnecessary and does not satisfy the requirement to preserve the active development loop. Browser interaction, Next.js diagnostics, or HTTP responses are acceptable runtime evidence. Source inspection alone is insufficient.`
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
The dashboard and `/reports/acme` are already running correctly in development, but the production build now fails from generated `.next/types` after recent work. Verify whether the project builds without cache issues, fix the failure, and confirm that the current development session still works when you are done.
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { SummaryCard } from '@/components/dashboard'
|
||||
import { dashboardCopy } from '@/lib/dashboard-copy'
|
||||
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<main>
|
||||
<h1>{dashboardCopy.dashboardTitle}</h1>
|
||||
<SummaryCard />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { loadSummaryCard } from '@/components/dashboard/load-card'
|
||||
import { dashboardCopy } from '@/lib/dashboard-copy'
|
||||
import { defineStaticParams } from '@/lib/route-params'
|
||||
|
||||
export const generateStaticParams = defineStaticParams('project', ['acme'])
|
||||
|
||||
export default async function ReportsPage() {
|
||||
const SummaryCard = await loadSummaryCard()
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1>{dashboardCopy.reportsTitle}</h1>
|
||||
<SummaryCard />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { SummaryCard } from './summary-card'
|
||||
@@ -0,0 +1,4 @@
|
||||
export async function loadSummaryCard() {
|
||||
const { SummaryCard } = await import('./summary-card')
|
||||
return SummaryCard
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export function SummaryCard() {
|
||||
return (
|
||||
<section aria-labelledby="summary-heading">
|
||||
<h2 id="summary-heading">Summary</h2>
|
||||
<p>Three projects are on track and one needs attention.</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { spawn } from 'node:child_process'
|
||||
import { openSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
|
||||
const log = openSync('/tmp/agent-055-next-dev.log', 'a')
|
||||
const dev = spawn('npm', ['run', 'dev'], {
|
||||
detached: true,
|
||||
stdio: ['ignore', log, log],
|
||||
})
|
||||
dev.unref()
|
||||
|
||||
const deadline = Date.now() + 30_000
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const response = await fetch('http://localhost:3100')
|
||||
if (response.ok) break
|
||||
} catch {}
|
||||
await new Promise((resolve) => setTimeout(resolve, 250))
|
||||
}
|
||||
|
||||
const response = await fetch('http://localhost:3100')
|
||||
if (!response.ok) {
|
||||
throw new Error(`next dev failed to start: ${response.status}`)
|
||||
}
|
||||
|
||||
const reportsResponse = await fetch('http://localhost:3100/reports/acme')
|
||||
if (!reportsResponse.ok) {
|
||||
throw new Error(`reports route failed to compile: ${reportsResponse.status}`)
|
||||
}
|
||||
|
||||
writeFileSync(
|
||||
'lib/route-params.ts',
|
||||
readFileSync('lib/route-params.ts', 'utf8').replace(
|
||||
'return () =>',
|
||||
'return (_route: string) =>'
|
||||
)
|
||||
)
|
||||
|
||||
const packageJson = JSON.parse(readFileSync('package.json', 'utf8'))
|
||||
delete packageJson.scripts['eval:setup']
|
||||
writeFileSync('package.json', `${JSON.stringify(packageJson, null, 2)}\n`)
|
||||
rmSync(new URL(import.meta.url))
|
||||
@@ -0,0 +1,4 @@
|
||||
export const dashboardCopy = {
|
||||
dashboardTitle: 'Project dashboard',
|
||||
reportsTitle: 'Project reports',
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export function defineStaticParams<Key extends string>(
|
||||
key: Key,
|
||||
values: string[]
|
||||
) {
|
||||
return () => values.map((value) => ({ [key]: value }))
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { NextConfig } from 'next'
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
// Keep the baseline free of generated agent instructions. The Guide
|
||||
// experiment writes its own AGENTS.md after this fixture is prepared.
|
||||
agentRules: false,
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"eval:setup": "node eval-setup.mjs",
|
||||
"dev": "next dev --port 3100",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^16",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"typescript": "^5",
|
||||
"vitest": "^3.1.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
},
|
||||
"target": "ES2017"
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules", "EVAL.ts"]
|
||||
}
|
||||
@@ -65,6 +65,30 @@ export async function installNextJs(sandbox: Sandbox): Promise<void> {
|
||||
console.log(' Installed local Next.js tarball')
|
||||
}
|
||||
|
||||
/**
|
||||
* Run optional fixture-specific setup after dependencies are installed but
|
||||
* before the coding agent starts.
|
||||
*/
|
||||
export async function prepareFixture(sandbox: Sandbox): Promise<void> {
|
||||
let pkg: { scripts?: Record<string, string> }
|
||||
try {
|
||||
pkg = JSON.parse(await sandbox.readFile('package.json'))
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
if (!pkg.scripts?.['eval:setup']) return
|
||||
|
||||
const { exitCode, stderr } = await sandbox.runCommand('npm', [
|
||||
'run',
|
||||
'eval:setup',
|
||||
])
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`npm run eval:setup failed (exit ${exitCode}):\n${stderr}`)
|
||||
}
|
||||
console.log(' Prepared fixture state')
|
||||
}
|
||||
|
||||
/**
|
||||
* Write AGENTS.md (and aliases) to the sandbox root, directing agents to read
|
||||
* bundled docs from node_modules/next/dist/docs/.
|
||||
|
||||
+6
-6
@@ -42,13 +42,13 @@ const EVAL_CONFIG = JSON.parse(fs.readFileSync(EVAL_CONFIG_PATH, 'utf-8'))
|
||||
const BASE_VARIANTS = [
|
||||
{
|
||||
suffix: 'baseline',
|
||||
imports: `import { installNextJs } from '../lib/setup.js'`,
|
||||
setup: `await installNextJs(sandbox)`,
|
||||
imports: `import { installNextJs, prepareFixture } from '../lib/setup.js'`,
|
||||
setup: `await installNextJs(sandbox)\n await prepareFixture(sandbox)`,
|
||||
},
|
||||
{
|
||||
suffix: 'agents-md',
|
||||
imports: `import { installNextJs, writeAgentsMd } from '../lib/setup.js'`,
|
||||
setup: `await installNextJs(sandbox)\n await writeAgentsMd(sandbox)`,
|
||||
imports: `import { installNextJs, prepareFixture, writeAgentsMd } from '../lib/setup.js'`,
|
||||
setup: `await installNextJs(sandbox)\n await prepareFixture(sandbox)\n await writeAgentsMd(sandbox)`,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -159,8 +159,8 @@ function getExperimentSettings(evalName) {
|
||||
const multipleSkillGroups = skillGroups.size > 1
|
||||
const skillVariants = [...skillGroups.values()].map(({ skills, evals }) => ({
|
||||
suffix: multipleSkillGroups ? `skills-${skills.join('-')}` : 'skills',
|
||||
imports: `import { installLocalSkills, installNextJs } from '../lib/setup.js'`,
|
||||
setup: `await installNextJs(sandbox)\n await installLocalSkills(sandbox, ${JSON.stringify(skills)})`,
|
||||
imports: `import { installLocalSkills, installNextJs, prepareFixture } from '../lib/setup.js'`,
|
||||
setup: `await installNextJs(sandbox)\n await prepareFixture(sandbox)\n await installLocalSkills(sandbox, ${JSON.stringify(skills)})`,
|
||||
evals,
|
||||
}))
|
||||
|
||||
|
||||
@@ -123,6 +123,10 @@ manual rather than from memory.
|
||||
|
||||
## gotchas
|
||||
|
||||
- **Preserve `.next` while the development server is running.** Moving or
|
||||
deleting it disconnects the server from its generated state and discards
|
||||
incremental caches. Moving it to a backup is still a reset. If a production
|
||||
build needs isolated output, configure a separate `distDir`.
|
||||
- **Every `agent-browser` command must know your session and restore
|
||||
key, or it may use an empty default browser or fail to save login
|
||||
state.** Easiest: export both `AGENT_BROWSER_SESSION="$SESSION"` and
|
||||
|
||||
Reference in New Issue
Block a user