App Router dynamic-route params were being returned in reversed key
order (deepest segment first) because the recursive trie matcher
mutated result.params on the way back up the call stack.
Fix by collecting param entries into a shared stack during forward
traversal, then materializing the params object from that stack at
the terminal node. This preserves declaration order for Object.keys().
- Shared buildParams helper extracted to routing/utils.ts to unify
code between route-trie.ts and app-optimistic-routing.ts.
- Added tests verifying key order for [category]/[id], [a]/[b]/[c],
mixed static+dynamic, and backtracking-to-catch-all scenarios.
Fixes#1537
* fix(routing): decode matched params with decodeURIComponent
Vinext's segment-safe normalization preserves encoded delimiters (%2F) for
correct route matching, but the captured param values were left undecoded.
This caused dynamic params to return "a%2Fb" instead of "a/b", diverging
from Next.js which decodes at the route-matcher level.
Add decodeURIComponent pass in trieMatch and matchRoutePattern — the two
route matching entry points — so all downstream consumers (pages, route
handlers, metadata, useParams) receive decoded param values.
Mirrors Next.js: packages/next/src/shared/lib/router/utils/route-matcher.ts L25-27
* test(app-rsc-route-matching): update param decode assertion to match new behavior
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.
* 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
* perf: replace O(n) linear route matching with radix trie
Every request triggered 3-6 linear scans over the full route list, calling
matchPattern() for every route until a match. For apps with many routes
this means thousands of comparisons per request.
This replaces the linear scan with a trie-based O(depth) lookup in all
4 codebases that share the matching logic:
- pages-router.ts (scanner module, dev time)
- app-router.ts (scanner module, dev time)
- app-rsc-entry.ts (generated entry, runtime)
- pages-server-entry.ts (generated entry, runtime)
The trie enforces correct precedence at each node via traversal order:
static > dynamic > catch-all > optional catch-all. Recursive DFS with
backtracking ensures dead-end branches fall through to alternatives.
matchPattern is retained only in app-rsc-entry.ts for the intercept
lookup (typically 0-5 entries, not worth trie-ifying).
* test: update entry-templates snapshot and assertion for trie-based matching
The server entry no longer inlines matchPattern — it imports the trie
module instead. Update the explicit assertion to check for buildRouteTrie
and trieMatch, and regenerate all entry template snapshots.
* fix: address review comments on trie-based route matching
- Correct "radix trie" → "trie (prefix tree)" in doc comment (no path compression)
- Remove unused `routes` parameter from `matchRoute` in app-rsc-entry
- Replace stream-of-consciousness test comment with concise explanation
* fix: update matchRoute regex in test to match new 1-arg signature
The test was asserting the old `matchRoute(url, routes)` signature in
the generated code. Updated to match the new `matchRoute(url)` signature
after the unused `routes` parameter was removed.