* perf(routing): precompute route precedence before sorting
`compareRoutes` re-parsed every pattern on each comparison, so a sort
called `routePrecedence` ~2·log n times per route instead of once. Replace
it with `sortRoutes`, which scores each pattern once into a Map and sorts
by (score, pattern.localeCompare). The localeCompare tiebreaker already
guarantees a total order, so ordering is byte-identical; this just removes
the redundant re-parsing on every route scan (dev re-scans on file change,
plus the pages/api and app-graph builds).
* perf(app-router): cache convention-file probes per route-graph scan
`buildAppRouteGraph` walks the appDir→leaf chain separately for every
route (layouts, templates, errors, boundaries, slots), each step probing
up to 4 extensions per convention file via `existsSync`. Shared ancestor
directories — the `app/` root above all — were re-stat'd once per
descendant route, so a scan cost roughly routes × depth × conventions ×
extensions syscalls.
Memoize `findFile` through a `WeakMap<ValidFileMatcher, Map>` keyed by a
per-scan matcher clone created in `buildAppRouteGraph`. Results are
immutable within one scan, so no invalidation is needed: the cache lives
exactly as long as the scan and is GC'd afterward. The fresh per-scan key
also keeps overlapping scans isolated, and the `null` not-found outcome is
cached too. Probe results are identical to the previous direct `existsSync`
calls.
* perf(deploy): single tree walk for detection and reuse parsed package.json
`analyzeProject` walked the app/ tree twice — once in `detectISR` and again
in `detectMDX` — and `detectNativeModules` re-read and re-parsed
package.json that `detectProject` had already parsed into `allDeps`.
Merge the two walkers into one `scanTreeForDetection(dir, { isr, mdx })`
that evaluates both predicates per entry with each flag short-circuiting
independently, extract the next.config MDX check into `detectMDXFromConfig`,
and add `resolveProjectDir` for the shared root/`src` precedence. Pass the
already-merged `allDeps` into `detectNativeModules`. Detection semantics are
unchanged: same files visited, same patterns tested, same booleans out.
Next.js PARAMETER_PATTERN accepts any non-] characters inside brackets:
[repo:name], [variant.id], [user@domain]
vinext was restricting param names to [\w-]+, silently treating segments
with dots, colons, or @ as literal URLs. This fix aligns the regexes with
Next.js:
- app-route-graph.ts: ^\[([^\]]+)\]$ (was [\w-]+)
- pages-router.ts: same
- route-validation.ts: patternToNextFormat now backtracks over +/* suffixes
- shims/router.ts: extractRouteParamNames now accepts dotted/colon names
Also corrects a misleading comment in routePrecedence: dynamic routes CAN
score negative, but the trie matcher (route-trie.ts) always checks static
children before dynamic children at each node, so purely-static routes still
win at request time.
Next.js reference:
https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/get-dynamic-param.ts#L175Closes#7
* Extract app page route wiring helpers
* Add slot client primitives
* Fix app page error boundary serialization
* Fix client error boundary pathname reset
* Document Next.js error boundary verification
* Implement flat App Router payload for layout persistence
* fix: address review findings in flat payload implementation
- Fix stale closure on readBrowserRouterState by using a useRef updated
synchronously during render instead of a closure captured in
useLayoutEffect. External callers (navigate, server actions, HMR) now
always read the current router state.
- Restore GlobalErrorBoundary wrapping that was dropped when switching
from buildPageElement to buildAppPageElements. Apps with
app/global-error.tsx now get their global error boundary back.
- Add exhaustive default case to routerReducer so new action types
produce a compile error and a runtime throw instead of silent undefined.
- Remove dead code: createRouteNodeSnapshot, AppRouteNodeSnapshot,
AppRouteNodeValue were defined but never imported.
- Remove deprecated buildAppPageRouteElement and its test — no
production callers remain after the flat payload cutover.
- Short-circuit normalizeAppElements when no slot keys need rewriting
to avoid unnecessary allocation on every payload.
- Align test data in error boundary RSC payload test (matchedParams
slug: "post" -> "missing" to match requestUrl /posts/missing).
* fix: normalize flat payload after use(), not before
createFromReadableStream() returns a React thenable whose .then()
returns undefined (not a Promise). Chaining .then(normalizeAppElements)
broke SSR by assigning undefined to flightRoot.
Fix: call use() on the raw thenable, then normalize synchronously
after resolution. Also widen renderAppPageLifecycle element type to
accept flat map payloads.
* fix: produce flat RSC payload on all rendering paths
The SSR entry always expects a flat Record<string, ReactNode> with
__route and __rootLayout metadata from the RSC stream. Three paths
were still producing bare ReactNode payloads:
1. renderAppPageBoundaryElementResponse only created the flat map for
isRscRequest=true, but HTML requests also flow through RSC→SSR
2. buildPageElements "no default export" early return
3. Server action "Page not found" fallback
All three now produce the flat keyed element map, fixing 17 test
failures across 404/not-found, forbidden/unauthorized, error boundary,
production build, rewrite, and encoded-slash paths.
* test: update unit tests for flat RSC payload on all paths
- Update renderElementToStream mock to extract the route element from
the flat map before rendering to HTML (mirrors real SSR entry flow)
- Update entry template snapshots for the buildPageElements changes
* fix: wrap Flight thenable in Promise.resolve() before chaining .then()
createFromReadableStream() returns a React Flight thenable whose
.then() returns undefined instead of a new Promise. The browser
entry's normalizeAppElementsPromise chained .then() on this raw
thenable, producing undefined — which crashed use() during hydration
with "An unsupported type was passed to use(): undefined".
Wrapping in Promise.resolve() first converts the Flight thenable
into a real Promise, making .then() chains work correctly.
The same fix was already applied to the SSR entry in 5395efc but
was missed in the browser entry.
* fix: eliminate Promise from ElementsContext to fix React 19 hydration
React 19.2.4's use(Promise) during hydration triggers "async Client
Component" because native Promises lack React's internal .status
property (set only by Flight thenables). When use() encounters a
Promise without .status, it suspends — which React interprets as the
component being async, causing a fatal error.
Fix: store resolved AppElements directly in ElementsContext and
router state instead of Promise<AppElements>. The navigation async
flow (createPendingNavigationCommit) awaits the Promise before
dispatching, so React state never holds a Promise.
- ElementsContext: Promise<AppElements> → AppElements
- AppRouterState.elements: Promise<AppElements> → AppElements
- mergeElementsPromise → mergeElements (sync object spread)
- Slot: useContext only, no use(Promise)
- SSR entry: pass resolved elements to context
- dispatchBrowserTree: simplified, no async error handler
Also fix flaky instrumentation E2E test that read the last error
entry instead of finding by path.
* test: update slot and browser state tests for resolved ElementsContext
- Remove Promise wrappers from ElementsContext test values
- mergeElementsPromise → mergeElements (sync)
- Replace Suspense streaming test with direct render test
- Remove unused createDeferred helper and Suspense import
- Update browser state test assertions (no longer async)
* ci: retrigger
* fix: address code review findings (P1-P3)
P1a: mergeElements preserves previous slot content when the new payload
marks a parallel slot as unmatched. On soft navigation, unmatched slots
keep their previous subtree instead of triggering notFound().
P1b: renderNavigationPayload now receives navId and checks for
superseded navigations after its await. Stale payloads are discarded
instead of being dispatched into the React tree.
P2: The catch block in renderNavigationPayload only calls
commitClientNavigationState() when activateNavigationSnapshot() was
actually reached, preventing counter underflow.
P3: The no-default-export fallback in buildPageElements now derives
the root layout tree path from route.layoutTreePositions and
route.routeSegments instead of hardcoding "/".
* fix: avoid serializing app render dependency wrappers
* Fix flat payload dependency barriers
* Fix template-only route wrappers
* chore: trigger CI review
* fix: skip Slot wrapping for layout entries without a default export
When a layout entry exists but has no default export, the element is
never written to the flat map. The unconditional Slot wrapping would
return null (id not in elements), silently dropping the entire route
subtree below that layout level.
Guard the Slot wrapping with a check for the layout component. When
absent, pass layoutChildren through directly — preserving the
LayoutSegmentProvider and error/not-found boundaries while skipping
the Slot indirection.
* fix: restore merged app router entry behavior
* fix: address app router review regressions
* Fix app-page-request intercept tests
* Address PR 2c review follow-ups
* Refactor same-url payload commits
* Clarify PR 2c browser invariants
* Update App Router entry snapshots
* Tighten App Router review follow-ups
* Align App Router entry helper usage
* Tighten browser commit invariants
* fix(app-router): preserve scoped parallel slot identity
* test(app-router): align slot identity assertions
* fix(app-router): keep boundary root layout metadata unknown
* docs(app-router): refresh stale review comments
* chore: migrate to vite plus
* Disable typeAware and typeCheck
* Update CI
* Fix CI
* Fix test
* Clean
* Run test with vp
* Try revert
* react: false In test
* Fix test
* Revert "Try revert"
This reverts commit 009da10473.
* Update
* Update
* Try revert ci changes
* revert
* Run vp migrate
* Disable typeAware and typeCheck for now
* Better resolve for test
* Use vp dev instead of vite
* Update expect
* Fix NormalizeManifestModuleId
* Try increase timeout
* Update to use vp
* Try new check
* Bring back npx vp
* Migrate CI
* Make next-intl resolvable
* Update
* Update
* Update
* Fix app router route-group conflicts
* Review app router slot discovery
* fix: address review feedback from bonk
- Fix routeSegments bug: store raw filesystem segments (with route groups)
instead of normalizeVisibleRouteSegments output, so useSelectedLayoutSegments()
sees the correct segment list at runtime for synthetic slot sub-routes
- Eliminate redundant convertSegmentsToRouteParts call: store the pre-computed
result in subPathMap alongside rawSegments, reuse it in the merge loop instead
of re-deriving from already-stripped segments
- Make ownerDir required (non-optional) in ParallelSlot: it is always set by
discoverParallelSlots; remove the dead ?? '' fallback in slotKey
- Replace O(n) routes.find() with O(1) Map lookup: build routesByPattern before
the outer loop and keep it updated as synthetic routes are added
- Add comment to normalizeVisibleRouteSegments explaining the intentional difference
from convertSegmentsToRouteParts (dynamic segments not converted) and that both
functions must stay in sync
- Remove spurious g flag from normalizeRoutePattern regex: $ anchors to end of
string so there can only ever be one match
* fix: remove now-unused normalizeVisibleRouteSegments, add doc comment to convertSegmentsToRouteParts
---------
Co-authored-by: James <james@eli.cx>
* add oxfmt formatter: config, scripts, CI, editor setup, docs
* rebuild lockfile
* fix: add Format to required checks list, remove dead ignore pattern
* run fmt
* add format to agents.md again