The test suite has accumulated a bunch of patterns for disabling tests
that are known to fail under some configuration: `it.skip`, `if
(isNextDev) { test('skipped in dev mode', () => {}); return }`, whole
describes toggled off by checking `process.env.__NEXT_CACHE_COMPONENTS`.
These all have the same flaw: nothing tells you when the thing you
skipped starts working. The test stays disabled forever, and the
workaround it was guarding rots along with it.
React solves this with the `@gate` pragma, and this PR ports it to the
Next.js e2e harness:
```ts
// Blocked on the optimization that marks a route as fully static when
// no dynamic params are referenced in Server Components.
// @gate !cacheComponents
it('navigates to a page with a lazily-generated static param', async () => {
// body unchanged
})
```
The test still runs. If the condition is false and the test fails, the
failure is absorbed and the suite stays green. If it _passes_, the suite
fails: the gate is stale, delete it. So instead of a skip that hides a
fixed bug indefinitely, you get a CI failure the day the fix lands.
When the condition is static, the inversion is Jest's own `test.failing`
under the hood. A lazy condition isn't known until the fixture's
resolved config is read inside the body, so those tests invert at
runtime instead.
`// @force-gate <condition>` skips for real — for tests that can't even
be attempted (prefetching is disabled in dev, deploy has no local build
output, the fixture won't build under the condition), and for tests of a
new API, where the disabled state can only throw and running it proves
nothing:
```ts
// Prefetching is disabled in dev, so this suite has nothing to test.
// @force-gate prefetching
describe('segment cache prefetch scheduling', () => {
// ...
})
```
There's no staleness check in that case, so this is a judgment call:
prefer `@gate` when the off state fails for a meaningful reason — the
flag changes behavior that already exists — and `@force-gate` when the
body can only throw because the API doesn't exist. A static condition
(mode, bundler) resolves at collection time into a normal Jest skip. A
lazy condition resolves at runtime, and when a lazy force-gate on a
describe is false, we skip the fixture build entirely — that's what
makes it usable for suites whose fixtures are build-incompatible with
the condition. (One caveat: Jest has no way to skip a test that's
already running, so these report as passing with a warning in the log,
not as skipped.)
Conditions live in a hand-written registry. I considered deriving the
lazy ones from the config schema automatically, but a gate is a claim
about which dimension of the test matrix explains a failure, and I'd
rather each of those claims be spelled out with a description.
Referencing an undeclared name fails the suite at collection time, so a
typo can't silently disable a gate.
The important design decision for lazy conditions is that they read the
fixture's _resolved_ config, never `process.env`. The env var isn't the
truth: `__NEXT_CACHE_COMPONENTS=true` only applies when the fixture
doesn't set `cacheComponents` itself, and config resolution implies
flags the fixture never mentions (`cacheComponents: true` alone turns on
`experimental.ppr`). Resolution happens in a child process, because
in-process `loadConfig` would leak the fixture's `.env` files into the
Jest worker. Suites with no lazy gate never pay for any of this.
The condition expression is parsed using a small grammar (also ported
from the React repo). An expression that doesn't parse fails the suite:
```ts
// @gate mode === 'start' && !cacheComponents
// @gate !(turbopack || rspack)
```
There's also a runtime version, mirroring React's `gate(flags =>
flags.enableFoo)`, for tests that run under both states but assert
differently (and for `it.each`, where the pragma can't attach):
```ts
import { gate } from 'next-test-utils'
it('renders the fallback', async () => {
if (await gate((conditions) => conditions.cacheComponents)) {
// PPR: the fallback is part of the static shell
} else {
// fully dynamic: the fallback streams in
}
})
```
It also accepts the pragma expression language as a string: `await
gate('cacheComponents && !dev')`.
Docs are in `test/lib/gate/README.md`; `test/unit/gate/` covers the
transform, the expression language, and the runtime.
30 KiB
Next.js Development Guide
Note:
CLAUDE.mdis a symlink toAGENTS.md. They are the same file.
Codebase structure
Monorepo Overview
This is a pnpm monorepo containing the Next.js framework and related packages.
next.js/
├── packages/ # Published npm packages
├── turbopack/ # Turbopack bundler (Rust) - git subtree
├── crates/ # Rust crates for Next.js SWC bindings
├── test/ # All test suites
├── examples/ # Example Next.js applications
├── docs/ # Documentation
└── scripts/ # Build and maintenance scripts
Core Package: packages/next
The main Next.js framework lives in packages/next/. This is what gets published as the next npm package.
Source code is in packages/next/src/.
Key entry points:
- Dev server:
src/cli/next-dev.ts→src/server/dev/next-dev-server.ts - Production server:
src/cli/next-start.ts→src/server/next-server.ts - Build:
src/cli/next-build.ts→src/build/index.ts
Compiled output goes to packages/next/dist/ (mirrors src/ structure).
Other Important Packages
packages/create-next-app/- Thecreate-next-appCLI toolpackages/next-swc/- Native Rust bindings (SWC transforms)packages/eslint-plugin-next/- ESLint rules for Next.jspackages/font/-next/fontimplementationpackages/third-parties/- Third-party script integrations
README files
Before editing or creating files in any subdirectory (e.g., packages/*, crates/*), read all README.md files in the directory path from the repo root up to and including the target file's directory. This helps identify any local patterns, conventions, and documentation.
Example: Before editing turbopack/crates/turbopack-ecmascript-runtime/js/src/nodejs/runtime/runtime-base.ts, read:
turbopack/README.md(if exists)turbopack/crates/README.md(if exists)turbopack/crates/turbopack-ecmascript-runtime/README.md(if exists)turbopack/crates/turbopack-ecmascript-runtime/js/README.md(if exists - closest to target file)
Build Commands
# Build the Next.js package
pnpm --filter=next build
# Build all JS code
pnpm build
# Build all JS and Rust code
pnpm build-all
# Run specific task
pnpm --filter=next exec taskr <task>
Fast Local Development
For iterative development, default to watch mode plus the explicit test script that matches the mode and bundler being verified.
Default agent rule: If you are changing Next.js source or integration tests, start pnpm --filter=next dev in a separate terminal session before making edits (unless it is already running). If you skip this, explicitly state why (for example: docs-only, read-only investigation, or CI-only analysis).
1. Start watch build in background:
# Auto-rebuilds on file changes (~1-2s per change vs ~60s full build)
# Keep this running while you iterate on code
pnpm --filter=next dev
2. Run focused tests with the matching mode script:
# Development mode with Turbopack
pnpm test-dev-turbo test/path/to/test.ts
# Development mode with Webpack
pnpm test-dev-webpack test/path/to/test.ts
# Production build+start with Turbopack
pnpm test-start-turbo test/path/to/test.ts
# Production build+start with Webpack
pnpm test-start-webpack test/path/to/test.ts
3. When done, kill the background watch process (if you started it).
For type errors only: Use pnpm --filter=next types (~10s) instead of pnpm --filter=next build (~60s).
After the workspace is bootstrapped, prefer pnpm --filter=next build when edits are limited to core Next.js files. Use full pnpm build-all for branch switches/bootstrap, before CI push, or when changes span multiple packages.
Always run a full bootstrap build after switching branches:
git checkout <branch>
pnpm build-all # Sets up outputs for dependent packages (Turborepo dedupes if unchanged)
Bundler Selection
Turbopack is the default bundler for both next dev and next build. To force webpack:
next build --webpack # Production build with webpack
next dev --webpack # Dev server with webpack
There is no --no-turbopack flag.
Testing
# Run specific test file (development mode with Turbopack)
pnpm test-dev-turbo test/path/to/test.test.ts
# Run tests matching pattern
pnpm test-dev-turbo -t "pattern"
# Run development tests
pnpm test-dev-turbo test/development/
Test commands by mode:
pnpm test-dev-turbo- Development mode with Turbopack (default)pnpm test-dev-webpack- Development mode with Webpackpnpm test-start-turbo- Production build+start with Turbopackpnpm test-start-webpack- Production build+start with Webpack
Other test commands:
pnpm test-unit- Run unit tests only (fast, no browser)pnpm new-test- Generate a new test file from template (interactive)
Generate tests non-interactively (for AI agents):
Generating tests using pnpm new-test is mandatory.
# Use --args for non-interactive mode. It is a `turbo gen` flag, so pass it
# directly, without a `--` separator.
# Format: pnpm new-test --args <appDir> <name> <type>
# appDir: true/false (is this for app directory?)
# name: test name (e.g. "my-feature")
# type: e2e | production | development | unit
pnpm new-test --args true my-feature e2e
Analyzing test output efficiently:
Never re-run the same test suite with different grep filters. Capture output once to a file, then read from it:
# Run once, save everything
HEADLESS=true pnpm test-dev-turbo test/path/to/test.ts > /tmp/test-output.log 2>&1
# Then analyze without re-running
grep "●" /tmp/test-output.log # Failed test names
grep -A5 "Error:" /tmp/test-output.log # Error details
tail -5 /tmp/test-output.log # Summary
Writing Tests
Test writing expectations:
-
Use
pnpm new-testto generate new test suites - it creates proper structure with fixture files -
Use
retry()fromnext-test-utilsinstead ofsetTimeoutfor waiting// Good - use retry() for polling/waiting import { retry } from 'next-test-utils' await retry(async () => { const text = await browser.elementByCss('p').text() expect(text).toBe('expected value') }) // Bad - don't use setTimeout for waiting await new Promise((resolve) => setTimeout(resolve, 1000)) -
Do NOT use
check()- it is deprecated. Useretry()+expect()instead// Deprecated - don't use check() await check(() => browser.elementByCss('p').text(), /expected/) // Good - use retry() with expect() await retry(async () => { const text = await browser.elementByCss('p').text() expect(text).toMatch(/expected/) }) -
Prefer real fixture directories over inline
filesobjects// Good - use a real directory with fixture files const { next } = nextTestSetup({ files: __dirname, // points to directory containing test fixtures }) // Avoid - inline file definitions are harder to maintain const { next } = nextTestSetup({ files: { 'app/page.tsx': `export default function Page() { ... }`, }, })
Linting and Types
pnpm lint # Full lint (types, prettier, eslint, ast-grep)
pnpm lint-fix # Auto-fix lint issues
pnpm prettier-fix # Fix formatting only
pnpm types # TypeScript type checking
Type-check with the repo's own commands. pnpm typescript runs tsc --noEmit against the root tsconfig.json, which includes scripts/**/*.js and loads this repo's type augmentations. A hand-rolled tsconfig pointed at a single file misses those augmentations and will report clean while CI fails. For example NodeJS.ProcessEnv is declared in packages/next/types/global.d.ts with NODE_ENV required, so a plain Record<string, string> is not a valid env for an execa call.
Prefer a Throwaway Worktree
Prefer a throwaway git worktree over changing the user's checkout. Switching their branch, or leaving a failed rebase behind, interrupts whatever they had open. It is also the right call for anything untrusted, such as a contributor's branch, because their files and any half-finished state stay outside the working copy.
git worktree add /tmp/scratch-work <branch> # or --detach <commit>
# ... work in /tmp/scratch-work ...
git worktree remove --force /tmp/scratch-work
Always remove the worktree when finished, and prefer removing it in a cleanup path that also runs on failure.
A fresh worktree has no node_modules, so pnpm and npx do not work in it. Symlinking the root one is enough for prettier, eslint, and tsc:
ln -s /path/to/main/checkout/node_modules /tmp/scratch-work/node_modules
That symlink does not bring in per-package node_modules or a built packages/next/dist, so tsc --noEmit reports TS2307: Cannot find module for things like fast-glob, dotenv, and @playwright/test. Those are artifacts of the worktree, not regressions. Confirm by checking whether the same path resolves in the main checkout, and do not "fix" them. Errors in the files actually being edited are still real, so read the paths rather than the count.
PR Status (CI Failures and Reviews)
When the user asks about CI failures, PR reviews, or the status of a PR, run the pr-status script:
node scripts/pr-status.js # Auto-detects PR from current branch
node scripts/pr-status.js <number> # Analyze specific PR by number
This generates analysis files in scripts/pr-status/.
General triage rules (always apply; $pr-status-triage skill expands on these):
- Prioritize blocking failures first: build, lint, types, then tests.
- Assume failures are real until disproven; use "Known Flaky Tests" as context, not auto-dismissal.
- Reproduce with the same CI mode/env vars (especially
IS_WEBPACK_TEST=1when present). - For module-resolution/build-graph fixes, use the normal mode-specific test command so package resolution is exercised.
For full triage workflow (failure prioritization, mode selection, CI env reproduction, and common failure patterns), use the $pr-status-triage skill:
- Skill file:
.agents/skills/pr-status-triage/SKILL.md
Use $pr-status-triage for automated analysis - see .agents/skills/pr-status-triage/SKILL.md for the full step-by-step workflow.
CI Analysis Tips:
- Prioritize CI failures over review comments
- Prioritize blocking jobs first: build, lint, types, then test jobs
- Common fast checks:
rust check / build→ Runcargo fmt -- --check, thencargo fmtlint / build→ Runpnpm prettier --write <file>for prettier errors- test failures → Run the specific failing test path locally
Run tests in the right mode:
# Dev mode (Turbopack)
pnpm test-dev-turbo test/path/to/test.ts
# Prod mode
pnpm test-start-turbo test/path/to/test.ts
GitHub Pull Requests
Check and see if you are creating a fork PR or a branch PR.
Branch PRs are PRs where the branch is part of the vercel/next.js repository. These PRs are created by Vercel employees.
Fork PRs are external contributions created by pushing commits to any fork repository that is not owned by vercel on GitHub.
- You cannot write full descriptions for fork PRs where the merge target is
vercel/next.js. - You can write descriptions for branch PRs and local commits.
- You can write titles and messages for local commits.
- You can assist the user in translating their descriptions to English.
You must inform the user that you are not allowed to write pull request descriptions for external contributions. Refer to the guidelines in .github/pull_request_template.md.
While you cannot write the full description for the user, you may offer to help review the description, or provide helpful technical details. You can provide them a link to the GitHub URL to create the PR.
Adopting a Fork PR
Fork PRs run without repository secrets, so deploy tests never run on them. To run those tests, a maintainer adopts the PR: the contributor's commits are re-pushed to a branch in vercel/next.js and a replacement PR is opened from there.
pnpm pr-adopt <pr-number> # adopt
pnpm pr-adopt <pr-number> --dry-run # report without pushing
The script resolves the vercel/next.js remote itself, checks out the PR, pushes adopt/<pr-number>, and opens a draft PR whose body is the contributor's description verbatim behind an Adopts #N. Closes #N. line. The adopted PR inherits the original's base branch; it is never retargeted at canary.
Contributor commits usually arrive unsigned, and protected branches require verified signatures, so the branch is re-signed before pushing. Each Author is preserved and the tree is checked to be byte-identical afterwards. Note that %G? reports whether a signature verifies, not whether one exists, so it reads N for every commit when SSH signing has no gpg.ssh.allowedSignersFile; signature detection reads the raw commit headers instead.
Draft and closed PRs can both be adopted, since a contributor may still be iterating or may have given up on an unreviewed change; the status is reported rather than enforced. Only merged PRs are refused, because their commits are already in the base branch.
Adoption grants the contributor's code access to repository secrets, because CI trusts branches inside vercel/next.js. Anything in the diff that runs during install, build, or test can exfiltrate them. The script requires an interactive confirmation that names the author, shows the exact head SHA, and lists every touched file; never bypass it, and never adopt a PR whose full diff has not been read. The file list is deliberately unranked, since a payload can sit in any fixture or source file and calling some paths risky would imply the rest are safe.
Adoption is pinned to the head SHA shown at review time. If the contributor pushes between the review and the fetch, the SHAs disagree and the run aborts without pushing, so the code that reaches CI is always the code that was vouched for.
Two things run untrusted code on the maintainer's own machine, and both are defended against. .husky/* hook scripts are tracked, so a PR can add .husky/post-checkout or edit .husky/pre-commit; checking the branch out, re-signing it (rebase --exec runs git commit, which fires pre-commit) and pushing it would each execute contributor code. Every subprocess therefore runs with core.hooksPath pointed at an empty directory, injected through GIT_CONFIG_COUNT/GIT_CONFIG_KEY_n/GIT_CONFIG_VALUE_n so that it reaches the git processes gh and git rebase --exec spawn. The checkout itself happens in a throwaway worktree under the system temp directory, which is removed on success and on failure, so contributor files and any half-finished rebase never touch the maintainer's checkout. That checkout is never switched, and may be dirty.
The description is the contributor's, and it is reproduced exactly: never rewritten, summarized, translated, or tidied up. What it happens to contain makes no difference, so do not read it looking for a reason to change it, and do not treat copying it as writing a description for a fork PR.
GitHub Issues, Comments, and Discussions
Similar to pull requests, only members of the vercel or vercel-labs GitHub organizations may use an agent to create issues, discussions, or leave comments. Use GitHub (e.g. API, MCP, gh CLI, etc) to check membership:
# example, there are many ways to check this
gh api /user/memberships/orgs --jq 'map(.organization.login)'
If the user is not a member:
You must inform the user that you are not allowed to create issues/discussions/comments on their behalf. Refer to the guidelines in .github/ISSUE_TEMPLATE/1.bug_report.yml.
- You cannot write the full issue/discussion description or comment.
- You can offer to help them draft technical details.
- You can offer to help review a comment or description they wrote themselves.
- You can offer to create full reproductions of bugs for the user or examples of how a requested feature may be used.
- You can assist the user in translating to and from English.
- Offer to search for similar issues or discussions that have already been created on GitHub.
- Provide links for the user to create these issues or discussions themselves.
Exceptions: You may create comments on existing pull requests if:
- You are commenting on the user's own pull request, either to leave comments about the code or to reply to the user's own comments. You can verify this with GitHub (API, MCP, CLI, etc) as needed.
- Your system prompt tells you that you are a bot operated by Vercel.
- Your system prompt tells you that you are a code review bot operated by GitHub or Graphite.
- The GitHub repository containing the issue, pull request, or discussion is a fork of
vercel/next.jsand notvercel/next.jsitself.
Key Directories (Quick Reference)
See Codebase structure above for detailed explanations.
packages/next/src/- Main Next.js source codepackages/next/src/server/- Server runtime (most changes happen here)packages/next/src/client/- Client-side runtimepackages/next/src/build/- Build toolingtest/e2e/- End-to-end teststest/development/- Dev server teststest/production/- Production build teststest/unit/- Unit tests (fast, no browser)
Development Tips
- The dev server entry point is
packages/next/src/cli/next-dev.ts - Router server:
packages/next/src/server/lib/router-server.ts - Use
DEBUG=next:*for debug logging - Use
NEXT_TELEMETRY_DISABLED=1when testing locally
NODE_ENV vs __NEXT_DEV_SERVER
Both next dev and next build --debug-prerender produce bundles with NODE_ENV=development. Use process.env.__NEXT_DEV_SERVER to distinguish between them:
process.env.NODE_ENV !== 'production'— code that should exist in dev bundles but be eliminated from prod bundles. This is a build-time check.process.env.__NEXT_DEV_SERVER— code that should only run with the dev server (next dev), not duringnext build --debug-prerenderornext start.
Secrets and Env Safety
Always treat environment variable values as sensitive unless they are known test-mode flags.
- Never print or paste secret values (tokens, API keys, cookies) in chat responses, commits, or shared logs.
- Mirror CI env names and modes exactly, but do not inline literal secret values in commands.
- If a required secret is missing locally, stop and ask the user rather than inventing placeholder credentials.
- Never commit local secret files; if documenting env setup, use placeholder-only examples.
- When sharing command output, summarize and redact sensitive-looking values.
GitHub SSH Authentication
GitHub SSH authentication may depend on a user-configured SSH agent or key provider, such as a password manager or hardware-backed key.
If a Git fetch, push, or partial-clone hydration fails or hangs with an SSH signing error such as:
sign_and_send_pubkey: signing failedcommunication with agent failedPermission denied (publickey)
stop immediately and ask the user to ensure their SSH agent or key provider is available and unlocked. Do not switch remotes to HTTPS, mutate remote URLs, retry repeatedly, or attempt another authentication workaround unless the user explicitly requests it.
Before a force-push or stack rebase that may hydrate partial-clone objects, prefer a lightweight SSH preflight. If it fails due to the SSH agent or key provider, ask the user to make it available or unlock it before continuing.
Specialized Skills
Use skills for conditional, deep workflows. Keep baseline iteration/build/test policy in this file.
$pr-status-triage- CI failure and PR review triage withscripts/pr-status.js$create-pr- branch, commit, push, and draft PR creation workflow$deploy-release-test- validate a commit preview package and trigger the fulltest_e2e_deploy_release.ymlsuite$backport-pr- cherry-pick merged PRs fromcanaryto release branches$flags- feature-flag wiring across config/schema/define-env/runtime env$dce-edge- DCE-saferequire()patterns and edge/runtime constraints$react-vendoring-entry-base.tsboundaries and vendored React type/runtime rules$react-sync- build a local React checkout and sync it into Next.js for testing$runtime-debug- runtime-bundle/module-resolution regression reproduction and verification$next-rspack- @next/rspack-core and @next/rspack-binding maintenance (rspack/ directory)$gate-tests-@gate/@force-gatetest directives: replacingit.skip/fake-green skips, conditions, variant-shard fixtures$authoring-skills- how to create and maintain skills in.agents/skills/
Context-Efficient Workflows
Reading large files (>500 lines, e.g. app-render.tsx):
- Grep first to find relevant line numbers, then read targeted ranges with
offset/limit - Never re-read the same section of a file without code changes in between
- For generated files (
dist/,node_modules/,.next/): search only, don't read
Build & test output:
- Capture to file once, then analyze: e.g.
pnpm build 2>&1 | tee /tmp/build.log - Don't re-run the same test command without code changes; re-analyze saved output instead
Batch edits before building:
- Group related edits across files, then run one build, not build-per-edit
- Use
pnpm --filter=next types(~10s) to check type errors without full rebuild
External API calls (gh, curl):
- Save response to variable or file:
JOBS=$(gh api ...) && echo "$JOBS" | jq '...' - Don't re-fetch the same API data to analyze from different angles
Commit and PR Style
- Do NOT add "Generated with Claude Code" or co-author footers to commits or PRs
- Keep commit messages concise and descriptive
- PR descriptions should focus on what changed and why
- Do NOT mark PRs as "ready for review" (
gh pr ready) - leave PRs in draft mode and let the user decide when to mark them ready
Task Decomposition and Verification
- Split work into smaller, individually verifiable tasks. Before starting, break the overall goal into incremental steps where each step produces a result that can be checked independently.
- Verify each task before moving on to the next. After completing a step, confirm it works correctly (e.g., run relevant tests, check types, build, or manually inspect output). Do not proceed to the next task until the current one is verified.
- Choose the right verification method for each change. This may include running unit tests, integration tests, type checking, linting, building the project, or inspecting runtime behavior depending on what was changed.
- When unclear how to verify a change, ask the user. If there is no obvious test or verification method for a particular change, ask the user how they would like it verified before moving on.
Pre-validate before committing to avoid slow lint-staged failures (~2 min each):
# Run exactly what the pre-commit hook runs on your changed files:
pnpm prettier --with-node-modules --ignore-path .prettierignore --write <files>
npx eslint --config eslint.config.mjs --fix <files>
Rebuilding Before Running Tests
When running Next.js integration tests, you must rebuild if source files have changed:
- First run after branch switch/bootstrap (or if unsure)? →
pnpm build-all - Edited only core Next.js files (
packages/next/**) after bootstrap? →pnpm --filter=next build - Edited Next.js code or Turbopack (Rust)? →
pnpm build-all
Development Anti-Patterns
For runtime internals, use focused skills:
- Feature-flag plumbing and runtime bundle wiring:
$flags(.agents/skills/flags/SKILL.md) - DCE and edge/runtime constraints:
$dce-edge(.agents/skills/dce-edge/SKILL.md) - React vendoring and
entry-base.tsboundaries:$react-vendoring(.agents/skills/react-vendoring/SKILL.md) - Debugging and verification workflow:
$runtime-debug(.agents/skills/runtime-debug/SKILL.md)
Keep these high-frequency guardrails in mind:
- Reproduce module resolution and bundling issues with the normal mode-specific test command so package resolution is exercised.
- Validate edge bundling regressions with
pnpm test-start-webpack test/e2e/app-dir/app/standalone.test.ts - Use
__NEXT_SHOW_IGNORE_LISTED=truewhen you need full internal stack traces
Core runtime/bundling rules (always apply; skills above expand on these with verification steps and examples):
- New flags: add type in
config-shared.ts, schema inconfig-schema.ts, anddefine-env.tswhen used in user-bundled code. - If a flag is consumed in pre-compiled runtime internals, also wire runtime env values (
next-server.ts/export/worker.tsas needed). define-env.tsaffects user bundling; it does not control pre-compiled runtime bundle internals.- Keep
require()behind compile-timeif/elsebranches for DCE (avoid early-return/throw patterns). - In edge builds, force feature flags that gate Node-only imports to
falseindefine-env.ts. react-server-dom-webpack/*imports must stay inentry-base.ts; consume via component module exports elsewhere.
Test Gotchas
- Cache components enables PPR by default: When
__NEXT_CACHE_COMPONENTS=true, most app-dir pages use PPR implicitly. Dedicatedppr-full/andppr/test suites are mostlydescribe.skip(migrating to cache components). To test PPR codepaths, run normal app-dir e2e tests with__NEXT_CACHE_COMPONENTS=truerather than looking for explicit PPR test suites. - Quick smoke testing with toy apps: For fast feedback, generate a minimal test fixture with
pnpm new-test --args true <name> e2e, then run the dev server directly withnode packages/next/dist/bin/next dev --port <port>andcurl --max-time 10. This avoids the overhead of the full test harness and gives immediate feedback on hangs/crashes. - Mode-specific tests need
skipStart: true+ manualnext.start()inbeforeAllafter mode check - Don't rely on exact log messages - filter by content patterns, find sequences not positions
- Snapshot tests vary by env flags: Tests with inline snapshots can produce different output depending on env flags. When updating snapshots, always run the test with the exact env flags the CI job uses (check
.github/workflows/build_and_test.ymlafterBuild:sections). Turbopack resolvesreact-dom/server.edge(no Node APIs likerenderToPipeableStream), while webpack resolves the.nodebuild (has them). app-page.tsis a build template compiled by the user's bundler: Anyrequire()in this file is traced by webpack/turbopack atnext buildtime. You cannot require internal modules with relative paths because they won't be resolvable from the user's project. Instead, export new helpers fromentry-base.tsand access them viaentryBase.*in the template.- Reproducing CI failures locally: Always match the exact CI env vars (check
pr-statusoutput for "Job Environment Variables"). Key differences such asIS_WEBPACK_TEST=1can change bundler selection and snapshot output, so use the CI command and mode when verifying module resolution fixes. - Showing full stack traces: Set
__NEXT_SHOW_IGNORE_LISTED=trueto disable the ignore-list filtering in dev server error output. By default, Next.js collapses internal frames toat ignore-listed frames, which hides useful context when debugging framework internals. Defined inpackages/next/src/server/patch-error-inspect.ts. - Router act tests must use LinkAccordion to control prefetches: Always use
LinkAccordionto control when prefetches happen insideactscopes. Never usebrowser.back()to return to a page where accordion links are already visible — BFCache restores state and triggers uncontrolled re-prefetches. See$router-actfor full patterns.
Rust/Cargo
- cargo fmt uses ASCII order (uppercase before lowercase) - just run
cargo fmt - Internal compiler error (ICE)? Delete incremental compilation artifacts and retry. Remove
*/incrementaldirectories from your cargo target directory (defaulttarget/, or checkCARGO_TARGET_DIRenv var) - Avoid adding new
super::imports except in inlinemodblocks (e.g.mod tests { ... }) — prefercrate::-rooted paths. This makes imports consistent and easier to grep for.
Node.js Source Maps
findSourceMap()needs--enable-source-mapsflag or returns undefined- Source map paths vary (webpack:
./src/, tsc:src/) - try multiple formats process.cwd()in stack trace formatting produces different paths in tests vs production
Stale Native Binary
If Turbopack produces unexpected errors after switching branches or pulling, check if packages/next-swc/native/*.node is stale. Delete it and run pnpm install to get the npm-published binary instead of a locally-built one.
Documentation Code Blocks
- When adding
highlight={...}attributes to code blocks, carefully count the actual line numbers within the code block - Account for empty lines, import statements, and type imports that shift line numbers
- Highlights should point to the actual relevant code, not unrelated lines like
return (or framework boilerplate - Double-check highlights by counting lines from 1 within each code block
Server Security: Internal Header Filtering
Next.js strips internal headers from incoming requests via filterInternalHeaders() in packages/next/src/server/lib/server-ipc/utils.ts. This runs at the entry point in packages/next/src/server/lib/router-server.ts before any server code executes. Only headers listed in the INTERNAL_HEADERS array are stripped.
When reviewing PRs: if new code reads a request header that is not a standard HTTP header (like content-type, accept, user-agent, host, authorization, cookie, etc.), flag it for security review. The header may be forgeable by an external attacker if it is not in the INTERNAL_HEADERS filter list in packages/next/src/server/lib/server-ipc/utils.ts.