Files
Tobias Koppers e953bdaaaa Fix dev route discovery startup race (#97920)
### What?

Fix a dev-server startup race that could temporarily publish an empty
Pages Router route set when Watchpack's first filesystem aggregation
arrived before its route-directory scan completed.

The dev bundler now snapshots the initial `pages/` and `app/` files
before starting the watcher. During startup it ignores Watchpack
aggregations that are missing an existing snapshot page file, so it does
not publish route definitions or errors from a partial filesystem view.
Empty route directories are detected from the same snapshot rather than
from a separate callback that could race or mistake a read error for an
empty directory.

The `next-head` e2e fixture now delays Watchpack's initial `pages/` scan
in development mode, deterministically covering the failing ordering
while preserving every existing head-tag assertion and non-dev test
mode.

### Why?

PR #97905's build-and-test run 32911367895, job 98006962799 (`test cache
components dev (6/6) / build`) exposed this as a flaky
`test/e2e/next-head/index.test.ts` failure. In the raw job log around
lines 3135–3305, retry 0 reported `Ready in 420ms` and then served `/`
as a 404 four times. The returned document contained the `_error` page
title (`404: This page could not be found`) and `pages__error` chunks,
so four head-tag assertions failed downstream. Retry 1 passed about nine
seconds later.

The first Watchpack aggregation could run route discovery before
`pages/index.js` appeared in `getTimeInfoEntries()`. That published no
page route definitions and released the request handler, causing `/` to
fall through to `_error` until a later aggregation repaired the route
table. This change waits to publish any initial route state until
Watchpack has a complete view, rather than adding request retries,
sleeps, or broader timeouts.

### How?

- Snapshot route directories once before watcher startup, tolerating a
directory removed during boot so Watchpack can report the removal
normally.
- Ignore incomplete startup aggregations until Watchpack observes every
snapshot page that still exists, with a 30-second safety deadline so an
unwatchable file cannot stall startup.
- Drive the empty-directory startup path from the snapshot, removing the
error-swallowing `fs.readdir` race.
- Delay the first Watchpack `pages/` scan in a separate dev-only
route-discovery case and assert that the injection fired. The focused
test reproduced `GET / 404` before the fix and returns `GET / 200` with
the fix.
- Preserve the original five `next/head` test cases unchanged; validate
the app/pages conflict path separately.

### Verification

- Exact CI environment: `__NEXT_CACHE_COMPONENTS=true
__NEXT_EXPERIMENTAL_CACHED_NAVIGATIONS=true
__NEXT_EXPERIMENTAL_SERVER_COMPONENTS_HMR_CANCELLATION=true
NEXT_EXTERNAL_TESTS_FILTERS=test/cache-components-tests-manifest.json
NEXT_TEST_MODE=dev IS_TURBOPACK_TEST=1 TURBOPACK_DEV=1 pnpm
test-dev-turbo test/e2e/next-head/index.test.ts` — 5/5 passed
- `IS_WEBPACK_TEST=1 NEXT_TEST_MODE=dev pnpm test-dev-webpack
test/e2e/next-head/index.test.ts` — 5/5 passed
- `pnpm --filter=next types`
- `pnpm --filter=next build`
- Prettier, ESLint, and `git diff --check`

<!-- NEXT_JS_LLM -->


<!-- fleet 4ac891b6-2d24-4103-95bf-9daf04359e3f -->

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-09-09 01:34:27 +02:00
..