Skip to content

feat(nuxt): migrate to unrouting for nuxt file system -> route generation#34316

Merged
danielroe merged 16 commits intomainfrom
feat/unrouting
Feb 24, 2026
Merged

feat(nuxt): migrate to unrouting for nuxt file system -> route generation#34316
danielroe merged 16 commits intomainfrom
feat/unrouting

Conversation

@danielroe
Copy link
Copy Markdown
Member

@danielroe danielroe commented Feb 11, 2026

🔗 Linked issue

resolves #23521

📚 Description

this migrates nuxt to use unrouting, which has a slightly slower initial start (6ms -> 8ms for a large app) but is much more performant for dev server changes (28x faster if you are not adding/removing pages, and ~15% faster even if you are) 🔥

it's also more solid (it's not sensitive to page order) and uses a trie for constructing routes.

@bolt-new-by-stackblitz
Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Feb 11, 2026

Open in StackBlitz

@nuxt/kit

npm i https://pkg.pr.new/@nuxt/kit@34316

@nuxt/nitro-server

npm i https://pkg.pr.new/@nuxt/nitro-server@34316

nuxt

npm i https://pkg.pr.new/nuxt@34316

@nuxt/rspack-builder

npm i https://pkg.pr.new/@nuxt/rspack-builder@34316

@nuxt/schema

npm i https://pkg.pr.new/@nuxt/schema@34316

@nuxt/vite-builder

npm i https://pkg.pr.new/@nuxt/vite-builder@34316

@nuxt/webpack-builder

npm i https://pkg.pr.new/@nuxt/webpack-builder@34316

commit: 3548ed6

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Feb 11, 2026

Merging this PR will degrade performance by 36.29%

⚡ 3 improved benchmarks
❌ 2 regressed benchmarks
✅ 13 untouched benchmarks
🆕 2 new benchmarks
⏩ 3 skipped benchmarks1

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
cold start (initial build + emit) - large (202 files) 6 ms 9.2 ms -34.42%
emit (no fs change) - large (202 files) 6,033.3 µs 212.6 µs ×28
add file + emit - medium (52 files) 3.5 ms 3 ms +17.6%
cold start (initial build + emit) - medium (52 files) 1.8 ms 2.8 ms -36.29%
🆕 emit (no fs change) - small (10 files) N/A 44.7 µs N/A
🆕 cold start (initial build + emit) - small (10 files) N/A 1.7 ms N/A
emit (no fs change) - medium (52 files) 1,293.6 µs 80.6 µs ×16

Comparing feat/unrouting (3548ed6) with main (6abf310)

Open in CodSpeed

Footnotes

  1. 3 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

- Add PagesContext with persistent route tree for dev-mode incremental updates
- Use unrouting's addFile/removeFile instead of full tree rebuild on file add/unlink
- Add picomatch for compiled glob pattern matching on watcher events
- Extract augmentAndResolve for shared downstream pipeline
- Remove redundant uniqueBy deduplication (handled by unrouting tree)
- Add branch-agnostic dev server lifecycle benchmark
@socket-security
Copy link
Copy Markdown

socket-security bot commented Feb 12, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​types/​picomatch@​4.0.21001007482100

View full report

@danielroe danielroe marked this pull request as ready for review February 13, 2026 00:29
@danielroe danielroe changed the title feat(nuxt): try migrating to unrouting feat(nuxt): migrate to unrouting for nuxt file system -> route generation Feb 13, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3548ed6 and ce2999a.

⛔ Files ignored due to path filters (2)
  • packages/nuxt/package.json is excluded by !**/package.json
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !pnpm-lock.yaml
📒 Files selected for processing (1)
  • packages/nuxt/src/pages/utils.ts

Walkthrough

Introduces a PagesContext-driven pipeline for Nuxt pages with persistent route-tree tracking and incremental updates in dev mode. Adds PagesContext, createPagesContext, augmentAndResolve, toNuxtPages, and picomatch-based page pattern matching. Integrates handleRouteRules and wires incremental watcher logic in the pages module to mutate the PagesContext on page file add/remove and emit updated pages, with a fallback to full rebuilds when incremental updates are not applicable or fail. Tests updated to use the new PagesContext API and revised generateRoutesFromFiles signature.

Possibly related PRs

  • nuxt/nuxt PR 30684: Modifies packages/nuxt/src/pages/module.ts and the timing of resolvePagesRoutes and builder watch/template update logic—closely overlaps the incremental dev-time wiring in this change.
  • nuxt/nuxt PR 30088: Changes pages routing pipeline and extractRouteRules usage/signature, directly related to this PR’s handleRouteRules integration and route-rule handling.
  • nuxt/nuxt PR 32897: Centralises inline route-rule handling on NuxtPage objects and Nitro routeRules, overlapping the new route-rule extraction/application flow added here.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Issue #23521 requests exporting page utility functions, but the PR migrates to unrouting instead, which fundamentally replaces the utility functions (e.g., parseSegment, getRoutePath) rather than exporting them. The PR replaces the legacy utilities with new APIs (createPagesContext, augmentAndResolve) but does not explicitly address the original request to export parseSegment and getRoutePath for third-party reuse.
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: migrating Nuxt's file system to route generation to use the unrouting library.
Description check ✅ Passed The description is related to the changeset, explaining the migration to unrouting and its performance benefits, and references the linked issue.
Out of Scope Changes check ✅ Passed The PR focuses on migrating to unrouting for route generation. Test file updates and benchmark changes align with this migration objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/unrouting

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/nuxt/src/pages/utils.ts`:
- Around line 89-110: The mode-detection in toNuxtPages incorrectly prioritises
'server' when both modes are present; update the logic in the toNuxtPages
function so that you only set page.mode when route.modes exists and contains
exactly one entry: if route.modes has length === 1 and that single value is
'server' set page.mode = 'server'; if length === 1 and the value is 'client' set
page.mode = 'client'; otherwise (both present, empty or undefined) leave
page.mode undefined to keep the page isomorphic. Use the existing route.modes
and page.mode symbols to locate and change the conditional.
🧹 Nitpick comments (4)
packages/nuxt/test/pages-dev.bench.ts (2)

50-50: Consider using PagesContextOptions directly instead of a local type alias.

ContextOptions duplicates a subset of PagesContextOptions from utils.ts. Using the existing type would reduce duplication and stay in sync with any future additions.

♻️ Suggested change
-type ContextOptions = { roots: string[] }
+import type { PagesContextOptions } from '../src/pages/utils.ts'

Then replace ContextOptions with PagesContextOptions in the createSimulator signature.


63-66: Unnecessary alias for createPagesContext.

createCtx is assigned createPagesContext but never reassigned or swapped — it just adds indirection.

♻️ Suggested change
-function createSimulator (filePaths: string[], opts: ContextOptions): DevSimulator {
-  const createCtx = createPagesContext
-  let ctx = createCtx(opts)
+function createSimulator (filePaths: string[], opts: ContextOptions): DevSimulator {
+  let ctx = createPagesContext(opts)
   const files = filePaths.map(p => ({ path: p }))

And update line 70:

-      ctx = createCtx(opts)
+      ctx = createPagesContext(opts)
packages/nuxt/src/pages/module.ts (1)

110-121: Consider consolidating resolvePagesRoutes and augmentAndResolvePages wrappers.

Both wrappers add handleRouteRules after calling the underlying utility. If handleRouteRules were folded into augmentAndResolve in utils.ts, these two separate wrappers could be simplified. That said, keeping route-rule handling in the module layer (rather than utils) is a reasonable separation of concerns — this is just a thought for future consideration.

packages/nuxt/src/pages/utils.ts (1)

89-96: toNuxtPages is not exported — verify this is intentional.

The function declaration at line 89 uses function toNuxtPages(...) without export. The AI summary mentions it as a new export, and the module import line (module.ts line 19) does not import it, so internal-only usage is likely fine. However, if external consumers (e.g. module authors) need to convert unrouting output, you may want to export it in the future.

Comment on lines 89 to 110
function toNuxtPages (routes: VueRoute[]): NuxtPage[] {
return routes.map((route) => {
const page: NuxtPage = {
name: route.name,
path: route.path,
file: route.file,
children: route.children?.length ? toNuxtPages(route.children) : [],
}

if (route.modes?.includes('server')) {
page.mode = 'server'
} else if (route.modes?.includes('client')) {
page.mode = 'client'
}

if (route.meta?.groups) {
page.meta = { ...page.meta, groups: route.meta.groups }
}

return page
})
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Search for VueRoute type definition
rg "type VueRoute|interface VueRoute" --type ts --type tsx -A 10

Repository: nuxt/nuxt

Length of output: 80


🏁 Script executed:

# Check for mode-related tests or examples
rg "modes.*server.*client|mode.*server" packages/nuxt/src/pages --type ts --type tsx -B 2 -A 2

Repository: nuxt/nuxt

Length of output: 80


🏁 Script executed:

# Examine the utils.ts file more broadly to understand context
cat -n packages/nuxt/src/pages/utils.ts | head -120

Repository: nuxt/nuxt

Length of output: 5318


🏁 Script executed:

# Search for unrouting imports and usage
rg "unrouting|VueRoute" packages/nuxt/src/pages --type ts --type tsx | head -20

Repository: nuxt/nuxt

Length of output: 80


🏁 Script executed:

# Search for tests related to toNuxtPages or mode handling
fd ".*\.test\.|.*\.spec\." packages/nuxt/src/pages --type f

Repository: nuxt/nuxt

Length of output: 35


🏁 Script executed:

# Look for any test files mentioning modes or toNuxtPages
rg "toNuxtPages|modes.*server.*client" packages/nuxt/src/pages --type ts -B 3 -A 3

Repository: nuxt/nuxt

Length of output: 1905


🌐 Web query:

unrouting npm package VueRoute modes property documentation

💡 Result:

In unrouting, “modes” are file-suffix variants (e.g. page.client.vue, page.server.vue) that the parser/tree builder can detect, and that the Vue Router emitter can surface on the emitted route objects.

  • Configure detection: pass modes: string[] to buildTree(...) (it extends ParsePathOptions) to tell unrouting which suffixes to treat as modes (example: ['client', 'server']). When present, detected modes are recorded. [1]
  • Low-level parse output: parsePath(...) returns ParsedPath objects whose meta can include modes?: string[]. [2]
  • Vue Router emit output (VueRoute): toVueRouter4(...) returns VueRoute[], where each VueRoute may include modes?: string[] described as “Mode variants” (only present when mode files exist). [2]

References
[1] unjs/unrouting README — buildTree options include modes (mode suffixes to detect). (github.com)
[2] unjs/unrouting README — VueRoute interface includes modes?: string[]; ParsedPath.meta.modes?: string[]. (github.com)

Citations:


🏁 Script executed:

# Find NuxtPage type definition
rg "interface NuxtPage|type NuxtPage" --type ts -A 15

Repository: nuxt/nuxt

Length of output: 3720


🏁 Script executed:

# Search for any tests or examples using .client.vue or .server.vue
rg "\.client\.vue|\.server\.vue" packages/nuxt --type ts -B 2 -A 2 | head -40

Repository: nuxt/nuxt

Length of output: 2392


🏁 Script executed:

# Look for any documentation or comments about mode property
rg "mode.*server|mode.*client" packages/nuxt/src/pages --type ts -B 2 -A 2

Repository: nuxt/nuxt

Length of output: 3480


🌐 Web query:

unrouting VueRoute modes when both client and server variants exist documentation

💡 Result:

When you have both a client (browser) and server (SSR/Node) router, Vue Router’s docs recommend using different “history” implementations per environment:

  • Client (browser): use HTML5 history for clean URLs: createWebHistory() (or hash if you can’t configure the server). (router.vuejs.org)
  • Server (SSR/Node): use memory/abstract history because there is no real browser URL/history:
    • Vue Router v4: createMemoryHistory() is “perfect for Node environment and SSR”. (router.vuejs.org)
    • Vue Router v3: router “mode” becomes abstract in non-browser environments (Node). (v3.router.vuejs.org)

Key implication (the “unrouting/404 on refresh” issue):

  • If you use HTML5 history on the client, you must configure the server to return your app entry (e.g., index.html) for unknown paths, otherwise direct loads like /user/123 will 404. (router.vuejs.org)

Nuxt note (if that’s what you mean by client/server variants):

  • Nuxt 3’s router.options.hashMode is specifically for SPA mode, and SSR is not supported with hash mode. (dev.nuxt.com)
  • Nuxt 2 documents that changing router mode is not recommended due to SSR. (v2.nuxt.com)

Citations:


🏁 Script executed:

# Search for test files related to pages routes and modes
fd ".*\.test\.|.*\.spec\." packages/nuxt/src/pages | head -20

Repository: nuxt/nuxt

Length of output: 35


🏁 Script executed:

# Look for any existing tests for toNuxtPages function
rg "toNuxtPages" packages/nuxt --type ts -B 5 -A 10

Repository: nuxt/nuxt

Length of output: 2510


🏁 Script executed:

# Check if there are any discussions or examples of .server.vue + .client.vue together
rg "\.server\.vue.*\.client\.vue|\.client\.vue.*\.server\.vue" packages/nuxt --type ts

Repository: nuxt/nuxt

Length of output: 35


🏁 Script executed:

# Search for any test fixtures with dual-mode pages
fd "page\.(server|client)\.vue" packages/nuxt/test --type f

Repository: nuxt/nuxt

Length of output: 35


🏁 Script executed:

# Search for unrouting documentation or issues discussing dual-mode handling
rg "modes.*server.*client|server\.vue.*client\.vue" packages/nuxt --type ts -B 3 -A 3

Repository: nuxt/nuxt

Length of output: 605


🏁 Script executed:

# Check the schema definition more carefully for NuxtPage.mode
cat -n packages/schema/src/types/hooks.ts | sed -n '1,150p'

Repository: nuxt/nuxt

Length of output: 5893


🌐 Web query:

unrouting component islands dual mode page both server and client variants

💡 Result:

In unjs/unrouting, “dual mode” (both server and client variants for the same route) is handled via mode variant support: you can create multiple files that map to the same URL by adding a configurable mode suffix such as .client and .server (e.g., page.client.vue, page.server.vue). During tree-building you enable detection via the modes option (e.g., modes: ['client', 'server']). When emitting to Vue Router 4, the resulting route record can include a modes?: string[] field indicating which variants exist for that route. [1]

This feature is about filesystem routing + emitting route definitions/metadata (including “named view” support like index@sidebar.vue), not a full “islands” rendering system by itself. [1]

Sources:
[1] unjs/unrouting README (mode variants, modes option, Vue Router emit shape) (github.com)

Citations:


Fix mode-detection logic to handle dual-mode pages correctly.

When route.modes contains both 'server' and 'client' (indicating both .server.vue and .client.vue variants exist), the route should remain isomorphic without a mode set, not be forced to mode: 'server'. The current logic at lines 98–102 prioritises 'server' unconditionally, which incorrectly converts dual-mode pages into server-only islands.

Update the mode assignment to:

  • modes contains only 'server' → set mode: 'server'
  • modes contains only 'client' → set mode: 'client'
  • modes contains both or is empty/undefined → leave undefined (isomorphic)
🤖 Prompt for AI Agents
In `@packages/nuxt/src/pages/utils.ts` around lines 89 - 110, The mode-detection
in toNuxtPages incorrectly prioritises 'server' when both modes are present;
update the logic in the toNuxtPages function so that you only set page.mode when
route.modes exists and contains exactly one entry: if route.modes has length ===
1 and that single value is 'server' set page.mode = 'server'; if length === 1
and the value is 'client' set page.mode = 'client'; otherwise (both present,
empty or undefined) leave page.mode undefined to keep the page isomorphic. Use
the existing route.modes and page.mode symbols to locate and change the
conditional.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/nuxt/test/pages.test.ts (1)

70-78: ⚠️ Potential issue | 🟡 Minor

Test files lack explicit priority values, creating inconsistency with production code.

In resolvePagesRoutes (utils.ts, lines 97–101), each layer directory receives an explicit priority based on its index. However, the test at lines 70–78 maps files as { path: file.path } without setting priority, so all files have undefined priority.

The "should correctly merge nested routes" test case (line 697) does pass with correct output (confirmed by snapshots), but this works despite the omission rather than by explicit intent. Adding explicit priority values would improve consistency with production code and make layer ordering more transparent.

Suggested refinement: Derive priority explicitly from the roots array index:

Suggested fix
-  const files = test.files.map(file => ({ path: file.path }))
-
-  result = generateRoutesFromFiles(files, { roots: ['pages/', 'layer/pages/'] }).map((route, index) => {
+  const roots = ['pages/', 'layer/pages/']
+  const files = test.files.map(file => {
+    const rootIndex = roots.findIndex(root => file.path.startsWith(root))
+    return {
+      path: file.path,
+      priority: rootIndex >= 0 ? rootIndex : 0,
+    }
+  })
+
+  result = generateRoutesFromFiles(files, { roots }).map((route, index) => {
🧹 Nitpick comments (1)
packages/nuxt/src/pages/utils.ts (1)

63-66: Type difference between toVueRouter4 return type and declared emit return type: ensure conversion from VueRoute[] to NuxtPage[] is intentional.

toVueRouter4(tree, emitOptions) returns VueRoute[] from the unrouting package, where VueRoute has modes?: string[]. However, the emit() method's interface declaration specifies return type NuxtPage[], where NuxtPage has mode?: 'client' | 'server' | 'all'. Although TypeScript's structural typing permits this due to interface compatibility, the semantic difference between modes and mode indicates a conversion should occur. Verify that the emitOptions.attrs: { mode: modes } configuration properly converts the modes array into the singular mode property for each route before returning, or add an explicit comment clarifying why the type difference is acceptable here.

@danielroe danielroe enabled auto-merge February 24, 2026 12:19
@danielroe danielroe added this pull request to the merge queue Feb 24, 2026
Merged via the queue into main with commit c22957b Feb 24, 2026
18 of 21 checks passed
@danielroe danielroe deleted the feat/unrouting branch February 24, 2026 12:43
@github-actions github-actions bot mentioned this pull request Feb 24, 2026
This was referenced Mar 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Export page utility functions

1 participant