Commit Graph

27 Commits

Author SHA1 Message Date
James Anderson 7f0b7ee9f7 test(routing): regression for /api-… page prefixes (#1542) (#1662)
* test(routing): regression for /api-… page prefixes (#1542)

Adds a focused unit test in tests/routing.test.ts that creates a
fixture with `pages/api-docs/[...slug].tsx`, `pages/api-info/index.tsx`,
and a real API route at `pages/api/hello.ts`, then asserts:

- `/api-docs/:slug+` and `/api-info` are discovered as page routes,
  not API routes (matches the Next.js fixture at
  `.nextjs-ref/test/e2e/prerender/pages/api-docs/[...slug].js`).
- The real `/api/hello` API route is still discovered correctly.
- `matchRoute` resolves `/api-docs/first` and `/api-info` against the
  page route table, pointing at the correct file paths.

The existing API-route detection already requires the trailing slash
(`startsWith("/api/")` or `=== "/api"`) in every code path, and the
`pages/` scanner only excludes the literal `"api"` directory — so the
test passes on `main`. This test pins that behaviour in place to
prevent a future regression of the substring-`api-` false positive
called out in #1542.

Refs #1542.

* style: apply biome formatter to routing.test.ts
2026-06-03 23:03:17 +01:00
Divanshu Chauhan (divkix) fdcc850638 fix(routing): serve pages/_foo.tsx when app/ also exists (#1610) 2026-05-27 22:24:02 +01:00
Nathan Nguyen 7b3fc49b92 fix(app-router): honor route group error boundaries (#1124) 2026-05-07 11:22:30 +01:00
Nathan Nguyen ebbf72cc8d feat(router): expose route graph manifest read model (#1089)
* feat(router): expose route graph manifest read model

The App Router graph now exposes root-boundary identity and a stable RouteManifest read model instead of leaving these facts implicit inside per-route arrays. Future #726 planner and compatibility work needs a semantic lookup surface for routes, layouts, pages, templates, slots, and root boundaries before it can stop reading meaning from transport keys.

This adds RootBoundaryId, graphVersion, StaticSegmentGraph maps, and fail-fast invariant checks for mismatched layout/template positions while preserving the existing routes array for current callers.

* chore: retrigger ci
2026-05-06 17:44:20 +01:00
Nathan Nguyen c1a4c8da1a fix(routing): accept any non-] chars in dynamic segment names (Next.js parity) (#1054)
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#L175

Closes #7
2026-05-05 19:21:40 +01:00
Nathan Nguyen 68c90808e0 fix(routing): discover nested parallel slot sub-routes from layout-only parents (#1051)
* fix(routing): discover nested parallel slot sub-routes from layout-only parents

Nested parallel routes failed when the parent segment had a layout but no
page.tsx. discoverSlotSubRoutes skipped any parent route without pagePath,
so slots like @parallelB/nested/page.tsx under home/layout.tsx (but no
home/page.tsx) were never discovered.

Next.js explicitly tests this shape in parallel-routes-and-interception:
app/parallel-nested/home/layout.tsx + @parallelB/default.tsx +
@parallelB/nested/page.tsx creates a valid /parallel-nested/home/nested
route.

The fix makes three targeted changes to discoverSlotSubRoutes:

1. Remove the hard pagePath requirement. Layout-only routes with parallel
   slots can still own nested slot sub-routes.

2. Compute parentPageDir from the innermost layout when pagePath is null,
   so slot ownership checks still work for layout-only routes.

3. Relax the children default.tsx requirement, but only for layout-only
   parent routes. When a parent HAS a children page, default.tsx is still
   required as a fallback for the synthetic sub-route. Layout-only parents
   do not need this fallback because the children slot was never occupied.

4. Add structural-conflict detection before creating synthetic routes. This
   prevents layout-only root routes from generating synthetic routes that
   collide with existing page routes differing only by param name (e.g.
   /shop/:id vs /shop/:name), which validateRoutePatterns rejects.

Tests ported from the Next.js parallel-routes-and-interception suite cover
both route-discovery and end-to-end rendering.

Refs: https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/parallel-routes-and-interception/parallel-routes-and-interception.test.ts

* fix(routing): correct patternsStructurallyEquivalent tree-node comparison

The initial implementation of patternsStructurallyEquivalent used a naive
dynamic-segment check (startsWith(':')) that produced two categories of
false positives:

1. Literal vs dynamic segments (e.g. 'about' vs ':id') were considered
   equivalent, incorrectly suppressing valid synthetic routes.

2. Single dynamic vs catch-all segments (e.g. ':id' vs ':slug+') were
   considered equivalent, even though they map to different validator tree
   nodes ([] vs [...]).

Replace the ad-hoc boolean logic with segmentTreeNodeType, which classifies
each segment by its Next.js validator tree node (literal, dynamic, catchAll,
optionalCatchAll). Two patterns are structurally equivalent only when every
segment maps to the same tree node type.

Also add a clarifying comment to the parentPageDir computation so the next
developer understands why the innermost layout proxies the route directory
for layout-only routes.

* fix(routing): scan synthetic routes in structural conflict check

Copilot review identified that patternsStructurallyEquivalent only scanned
the original routes array, missing conflicts between synthetic routes created
earlier in the same loop. Fixed by scanning routesByPattern.values() which
includes both original and synthetic routes.

Also adds a TODO comment documenting the known prerender limitation for
layout-only synthetic routes (skipped at build/prerender.ts:849). These
routes work in dev/SSR but won't be statically exported — this is
pre-existing behaviour for all layout-only routes.

* test(routing): cover structural conflict skip for synthetic parallel routes

Adds a unit test proving that discoverSlotSubRoutes skips synthetic routes
when an existing page route has the same structural shape but a different
param name (e.g. /shop/:id page + @feed/[name]/page.tsx slot).

Without this coverage the structural-conflict safeguard is easy to regress.

* fix(routing): guard discoverSlotSubRoutes to layout-only UI routes

Review identified that removing the old !parentRoute.pagePath guard also
let route-handler-only AppRoutes enter discoverSlotSubRoutes. Route
handlers have pagePath: null but are not layout-only UI routes. A shape
like app/api/route.ts + app/@feed/foo/page.tsx could make the /api route
handler scan the ancestor slot and materialise a synthetic UI route under
/api/foo.

Add an explicit isLayoutOnlyUiRoute check:
  !parentRoute.pagePath && !parentRoute.routePath && parentRoute.layouts.length > 0

Only page-bearing routes or layout-only UI routes can own nested parallel-
slot sub-routes. Route handlers and layout-less routes are skipped.

Also adds a regression test: a route.ts handler with an ancestor parallel
slot must not emit synthetic routes under the handler pattern.

* fix(routing): scan synthetic routes in structural conflict check + regression test

The structural-conflict guard had regressed back to scanning the original
routes array, missing conflicts against synthetic routes created earlier in
the same discoverSlotSubRoutes pass. Fixed by scanning
Array.from(routesByPattern.values()) instead.

Also adds a regression test proving that two slot sub-pages with different
param names under the same parent do not both materialise (e.g. @a/[id] and
@b/[name] under /shop — only one of /shop/:id or /shop/:name is emitted).
2026-05-05 18:30:50 +01:00
Nathan Nguyen 1e4c175ee4 fix(routing): omit empty optional catch-all params (#987)
Optional catch-all routes currently materialize the missing segment list as an empty array. Next.js leaves the param key absent when the optional catch-all capture group does not match, so pages and app routes can observe a different params shape at the zero-segment path.

The route trie treated zero remaining segments as a captured value. The fix keeps the route match while returning an empty params object for that zero-segment branch, and applies the same normalization to standalone app RSC pattern matching.

Tests cover shared trie matching, app RSC matching, app route discovery, pages route discovery, and hyphenated optional catch-all params.
2026-04-30 10:24:51 +01:00
Divanshu Chauhan (divkix) dc28af1cb6 fix: add ForbiddenBoundary/UnauthorizedBoundary for HTTP access fallback recovery (#764) (#945)
* fix: add ForbiddenBoundary/UnauthorizedBoundary for HTTP access fallback recovery (#764)

Per-layout forbidden/unauthorized boundary files (forbidden.tsx, unauthorized.tsx)
are now discovered by the router and wired into the App Router component tree via
dedicated ForbiddenBoundary and UnauthorizedBoundary client components. Previously,
only NotFoundBoundary existed, causing 403/401 errors thrown during RSC rendering
(e.g. inside Suspense boundaries) to escape into the generic error shell — this
could produce mismatched Flight/HTML data and client-side 'Connection closed' failures.

Next.js upstream: vercel/next.js#92231
  https://github.com/vercel/next.js/pull/92231

* fix: address review feedback on ForbiddenBoundary/UnauthorizedBoundary (#945)

- Export ForbiddenBoundaryInner and UnauthorizedBoundaryInner for testing
- Strengthen tests to import and call getDerivedStateFromError directly
- Use exact equality instead of startsWith for status code digest matching
- Make forbiddenPaths and unauthorizedPaths non-optional on AppRoute (matching notFoundPaths)
- Fix inaccurate ported-from attribution in test header

* fix: remove duplicate forbidden/unauthorized path iteration and update comment

Removes dead code in app-rsc-entry.ts (lines 185-190) that duplicated the existing import registration loops.

Also updates the stale nesting order comment in app-page-route-wiring.tsx to include Unauthorized and Forbidden boundaries.

* fix: align NotFoundBoundary digest matching and tests with Forbidden/Unauthorized patterns

- Use exact === instead of startsWith in NotFoundBoundary digest check
- Add forbiddenPaths/unauthorizedPaths per-layout test fixtures
- Fix property ordering: singular before plural for all boundary types

* fix: resolve rebase conflicts — align manifest code with main refactor

- Use buildAppRscManifestCode for manifest generation (matching main)
- Remove redundant || [] guards for non-optional forbiddenPaths/unauthorizedPaths
- Fix test fixture indices to match new module import count

* fix: add null guards for notFoundPaths, forbiddenPaths, unauthorizedPaths in manifest

TypeError: route.notFoundPaths is not iterable when routes lack per-layout boundary files.

CI was failing on app-router.test.ts with the error. Added null guards in both registerRouteModules() and buildRouteEntries() to handle undefined arrays.

* fix: reorder route-level boundary nesting for merge with main

Move ErrorBoundary after NotFoundBoundary/ForbiddenBoundary/
UnauthorizedBoundary to match Next.js nesting order (Error >
Unauthorized > Forbidden > NotFound). Resolves merge conflict
with bc70e1c.
2026-04-30 07:46:07 +01:00
Divanshu Chauhan (divkix) db57d45070 Shim next/root-params (#941)
* fix(app-router): shim next root params

* fix: address review feedback

* address review feedback

* test: update snapshots for root params cleanup change
2026-04-28 21:43:45 +01:00
Nathan Nguyen bee7e6b654 fix(router): reject invalid app route discovery conflicts (#928)
* fix(router): reject invalid app route discovery conflicts

App Router route discovery currently lets upward interception markers climb past the normalized root and relies on generic duplicate-pattern validation when a page and route handler share the same app route.

The scanner now validates visible interception climb depth before resolving the target, and reports an explicit page/route conflict before running generic route pattern validation.

Regression coverage captures the Next.js parity cases for root interception markers, route-group-transparent roots, two-level upward markers, and page plus route handler conflicts.

* fix(router): address app route validation review
2026-04-28 19:06:39 +01:00
Nathan Nguyen ae45ff2cf2 fix(router): ignore pre-root parallel slots for routed layouts (#927)
Routes whose first layout lives below app/ currently inherit slots discovered before that layout and clamp them to layout index 0. That incorrectly passes top-level parallel slots into a separate root layout.

Keep the pre-layout sentinel while scanning ancestors, skip those slots once the route has a real layout, and preserve the existing layout-less scanner metadata path for separate validation work. Add routing tests for the pre-root leak and the valid in-subtree slot case.
2026-04-28 11:32:38 +01:00
Nathan Nguyen faf1d9b4d6 Render intercept route layouts inside App Router slots (#872)
* Render intercept route layouts inside App Router slots

Intercepted App Router pages currently render without any layout.tsx files that live inside the intercepting route tree. This is wrong because Next.js applies those layouts before the slot layout, and nested intercept routes lose required wrappers when a modal intercept matches.

The routing pipeline assumed intercepts only needed a page module. That drops the intercept layout chain during discovery and never gives the slot override renderer enough information to rebuild the wrapper tree.

Collect intercept layout paths during route discovery, serialize them through the generated RSC entry, and apply the loaded layout modules around the intercepted page before the slot layout. Add regression coverage for discovery, code generation, and slot render order.

* Avoid repeated intercept layout scans and slot param wrapping

Intercept layout discovery rescanned the same directories for every intercepted page, and slot rendering rebuilt thenable params multiple times inside one render path. That adds avoidable work and makes the intercept wrapper path harder to read.

The implementation assumed these repeated lookups and allocations were harmless. That leaves the recursive router walk doing duplicate filesystem checks and gives slot wrappers different params wrapper identities in the same tree.

Thread the discovered intercept layout chain through the recursion, compute slot thenable params once per slot render, and keep the generated entry snapshots in sync with the added intercept layout plumbing.

* Clarify intercept layout plumbing and cover nested layout chains

The intercept layout plumbing still used mixed names in generated code, and the reverse layout wrapper loop took extra decoding to read. That makes the fix harder to review and maintain even though the behavior is correct.

The generated entry treated intercept-local layouts as a generic `layouts` field while the rest of the pipeline called them `interceptLayouts`, and the slot wrapper loop used a less direct reverse-index pattern. The tests also only covered a single intercept-local layout level.

Rename the generated intercept field to `interceptLayouts`, switch the reverse wrapper loop to the clearer classic form, and add regression coverage for nested intercept layout chains. Update the generated-entry snapshots to match.
2026-04-23 08:03:58 +01:00
Nathan Nguyen dd498bdea9 Fix sibling intercepted App Router routes (#867)
* fix app router sibling interception

* update app router entry snapshots

* preserve chained intercepted navigations

* address intercepted route review feedback

* guard probePage() against undefined PageComponent for slot-only routes

probePage() calls PageComponent(...) unconditionally. When a layout-owned
slot-only route is hit, route.page is null so PageComponent is undefined,
causing probePage() to throw TypeError on every request. probeAppPageComponent
catches it and returns null, so it does not crash, but it fires an avoidable
exception on every request to a slot-only URL and can confuse error monitoring.

probeLayoutAt() already uses the same guard pattern: if (!LayoutComp) return null.
Apply the same guard to probePage().
2026-04-21 18:23:51 +01:00
Nathan Nguyen 0dbfeff1af feat: flat keyed payload for App Router layout persistence (#750)
* 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
2026-04-10 07:49:18 +01:00
Brendan Dash a9d3455487 test: regression tests for inherited intercepting route slot deduplication (#721)
* fix: allow inherited intercepting routes to share target patterns

* fix: validate intercepting route target consistency per page

* refactor: simplify intercept target pattern collection by using Set for deduplication

* fixup: use main's inline Set dedup, remove helper export, restore .gitignore

* fix(test): pass explicit out dirs and appDir to buildApp to fix UNRESOLVED_IMPORT in RSC build

* chore: restore .gitignore to match main

---------

Co-authored-by: James <james@eli.cx>
2026-04-05 09:31:57 +00:00
Stephen Zhou c17d6941be chore: migrate to vite plus (#535)
* 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
2026-03-15 10:50:13 +00:00
Nathan Nguyen 6723edb3cd fix: use closest ancestor parallel slot instead of farthest (#545)
discoverInheritedParallelSlots iterates from root to leaf, but used
a `has()` guard that let the first (farthest) ancestor win. Remove
the guard so later (closer) ancestors overwrite earlier ones.
2026-03-15 10:01:29 +00:00
Nathan Nguyen 3d05dceeab fix: intercept routes (..) climb visible route segments, not filesystem dirs (#499)
computeInterceptTarget() used path.dirname() to climb parent directories,
but Next.js defines interception levels in terms of route segments. Route
groups like (marketing) are filesystem directories but invisible in the
URL, so path.dirname() would waste a climb on them.

Now walks backward through segments, counting only visible ones (skipping
route groups, parallel slots, and ".").

Also consolidates three copies of invisible-segment detection into a
single isInvisibleSegment() helper.
2026-03-12 13:38:21 +00:00
Nathan Nguyen af6e0ce421 fix: exclude private folders (_prefix) from App Router route discovery (#484)
* fix: exclude private folders (_prefix) from App Router route discovery

Next.js treats underscore-prefixed directories in app/ as "private
folders" that are excluded from routing. This is a recommended pattern
for colocating non-route files (e.g. _components, _lib, _utils).

vinext was only excluding @-prefixed slot directories, so files like
app/_components/page.tsx would incorrectly create routes.

Add the underscore check to all four App Router filesystem scan points:
- Main page/route discovery (scanWithExtensions exclude callback)
- Slot sub-page scanner (already had it)
- Intercepting route scanner (scanForInterceptingPages)
- Intercepting page collector (collectInterceptingPages)

* chore: format app-router.ts
2026-03-12 08:25:18 +00:00
Jared Stowell 26d5eabb92 fix: skip synthetic slot sub-routes without parent default fallback (#458)
* Fix nested parallel slot routes

* Fix nested parallel slot discovery

* Fix parallel slot routing parity
2026-03-12 07:47:25 +00:00
Jared Stowell b66fd6488c fix: Preserve encoded path delimiters in route discovery and matching (#456)
* Fix App Router decoding collision

* Fix App Router path decoding

* Address bonk review: codegen safe segment decode, static serving, nits

* fix: add codegen strict normalizer, use it at 400-returning call sites

---------

Co-authored-by: James <james@eli.cx>
2026-03-11 16:52:29 +00:00
Jared Stowell 11b27cd0a8 fix: reject conflicting dynamic route siblings during route discovery (#420)
* Validate route patterns in routers

* Validate intercepting routes
2026-03-10 21:48:36 +00:00
Jared Stowell cf0786ef34 fix: Reject non-terminal catch-all routes in pages and app routers (#401)
* Fix catch-all route overmatching

* Fix non-terminal slot routes

* Fix malformed slot route discovery

* Add validation for intercept catch-⁤

* Fix intercepting route catch-alls

* bonk

* Fix routing test route literals for typecheck

* Fix app route matcher test fixtures
2026-03-10 20:24:31 +00:00
James Anderson 764a496ce7 add oxfmt formatter (#380)
* 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
2026-03-09 14:56:14 +00:00
ask-bonk[bot] aff5d24449 fix(routing): allow hyphens in dynamic segment param names (#78)
* fix(routing): allow hyphens in dynamic segment param names

Catch-all and dynamic route params with hyphens (e.g. [[...sign-in]],
[auth-method]) caused 404 because the regex used \w+ which doesn't
match hyphens. Next.js allows hyphens in param directory names — this
is commonly used by Clerk ([[...sign-in]], [[...sign-up]]).

Changed all \w+ patterns to [\w-]+ across:
- app-router.ts (fileToAppRoute, discoverSlotSubRoutes, computeInterceptTarget)
- pages-router.ts (fileToRoute, patternToNextFormat)
- app-dev-server.ts (middleware matcher regex, config pattern matching)
- middleware.ts (pattern matching)

Closes #71

* fix(routing): allow hyphens in config matching and middleware-codegen param patterns

Addresses review feedback on PR #78: the same \w+ bug that was fixed in
routing/middleware also existed in the config pattern matching functions
(redirects, rewrites, headers) and the middleware-codegen tokenizer.

Updated all remaining param-related regex patterns to [\w-]+ across:
- index.ts: matchConfigPattern guard, tokenizer, catchAllMatch, applyHeaders
- config-matchers.ts: matchConfigPattern guard, tokenizer, catchAllMatch,
  escapeHeaderSource tokenizer
- app-dev-server.ts: __matchConfigPattern template (4 .replace() chains,
  catchAllMatch, __applyConfigHeaders)
- middleware-codegen.ts: generated matchMiddlewarePattern tokenizer

Subsumes PR #127 which fixed middleware-codegen.ts independently.

Co-authored-by: SeolJaeHyeok <milkboy2564@naver.com>

---------

Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com>
Co-authored-by: Fred K. Schott <622227+FredKSchott@users.noreply.github.com>
Co-authored-by: SeolJaeHyeok <milkboy2564@naver.com>
2026-02-26 22:15:45 -08:00
skeptrune 4349af3ecc fix: decode URL-encoded directory names and improve route precedence (#386)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 09:29:45 -06:00
Steve Faulkner 12fea722b6 Initial public release of vinext 2026-02-24 09:29:39 -06:00