Package Exports
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (unrouting) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
📍 unrouting
Universal filesystem routing
unrouting parses file paths into a route tree and emits route definitions for any framework router. It handles nested routes, dynamic params, catchalls, optional segments, route groups, layer merging, and more – as a standalone, framework-agnostic library.
Status
In active development. The core pipeline (parse, tree, emit) is functional and used by Nuxt.
- Generic route parsing covering major filesystem routing patterns
- Route tree with nesting, layer merging, group transparency
- Layer priority (multiple roots with configurable file precedence)
- Incremental tree updates (
addFile/removeFilefor dev server HMR) - Pluggable route name generation
- Route ordering by segment priority (static > dynamic > optional > catchall)
- Named view support (
@viewNameconvention) - Mode variant support (
.client,.server, configurable) - Duplicate route name detection
- Emit to framework routers
-
vue-router(nested routes, names, files, children, meta, components, modes) - rou3/Nitro
- RegExp patterns
- SolidStart
- SvelteKit
-
Install
# npm
npm install unrouting
# pnpm
pnpm install unroutingUsage
The library has a three-phase pipeline: parse file paths into tokens, build a route tree, and emit to a target format. For most use cases you only need two function calls.
Quick start
import { buildTree, toVueRouter4 } from 'unrouting'
const tree = buildTree([
'pages/index.vue',
'pages/about.vue',
'pages/users/[id].vue',
'pages/users.vue',
'pages/[...slug].vue',
], { roots: ['pages/'] })
const routes = toVueRouter4(tree)
// [
// { name: 'index', path: '/', file: 'pages/index.vue', children: [] },
// { name: 'about', path: '/about', file: 'pages/about.vue', children: [] },
// { name: 'users', path: '/users', file: 'pages/users.vue', children: [
// { name: 'users-id', path: ':id()', file: 'pages/users/[id].vue', children: [] },
// ]},
// { name: 'slug', path: '/:slug(.*)*', file: 'pages/[...slug].vue', children: [] },
// ]Nuxt-like usage with layers
import { buildTree, toVueRouter4 } from 'unrouting'
// Files from app + layer directories with priority
const tree = buildTree([
{ path: 'pages/index.vue', priority: 0 }, // app layer (wins on collision)
{ path: 'pages/dashboard.vue', priority: 0 },
{ path: 'pages/dashboard/settings.vue', priority: 0 },
{ path: 'layer/pages/dashboard/analytics.vue', priority: 1 }, // extending layer
{ path: 'layer/pages/index.vue', priority: 1 }, // overridden by app layer
], {
roots: ['pages/', 'layer/pages/'],
extensions: ['.vue'],
modes: ['client', 'server'],
warn: msg => console.warn(msg),
})
const routes = toVueRouter4(tree, {
onDuplicateRouteName: (name, file, existingFile) => {
console.warn(`Duplicate route name "${name}": ${file} and ${existingFile}`)
},
})Emitting to different formats
All emitters accept a RouteTree:
import { buildTree, toRegExp, toRou3, toVueRouter4 } from 'unrouting'
const tree = buildTree(['users/[id]/posts/[slug].vue'])
// Vue Router 4 – nested routes with names, files, children
const vueRoutes = toVueRouter4(tree)
// [{ name: 'users-id-posts-slug', path: '/users/:id()/posts/:slug()', file: '...', children: [] }]
// rou3/Nitro – flat route patterns
const rou3Routes = toRou3(tree)
// [{ path: '/users/:id/posts/:slug', file: '...' }]
// RegExp – matcher patterns with named groups
const regexpRoutes = toRegExp(tree)
// [{ pattern: /^\/users\/(?<id>[^/]+)\/posts\/(?<slug>[^/]+)\/?$/, keys: ['id', 'slug'], file: '...' }]Incremental updates (dev server)
The route tree is mutable. Instead of rebuilding everything when a file changes, use addFile and removeFile to update the tree in place – avoiding the cost of re-parsing all files and reconstructing the tree from scratch on every change.
import { addFile, buildTree, removeFile, toVueRouter4 } from 'unrouting'
const opts = { roots: ['pages/'], extensions: ['.vue'] }
// Build once at startup
const tree = buildTree(initialFiles, opts)
let routes = toVueRouter4(tree)
// On file add/remove (e.g., from a watcher callback)
addFile(tree, 'pages/new-page.vue', opts)
routes = toVueRouter4(tree)
removeFile(tree, 'pages/old-page.vue')
routes = toVueRouter4(tree)
// Rename = remove + add
removeFile(tree, 'pages/old-name.vue')
addFile(tree, 'pages/new-name.vue', opts)
routes = toVueRouter4(tree)addFile supports the same InputFile format as buildTree for layer priority:
addFile(tree, { path: 'layer/pages/about.vue', priority: 1 }, opts)Standalone parsing and segment conversion
If you don't need the full tree pipeline – e.g., you already have resolved routes and only need to convert individual path segments or strings to Vue Router syntax – you can use the parse + convert functions directly:
import { parsePath, parseSegment, toVueRouterPath, toVueRouterSegment } from 'unrouting'
// Parse a full file path
const [result] = parsePath(['users/[id]/profile.vue'])
// {
// file: 'users/[id]/profile.vue',
// segments: [
// [{ type: 'static', value: 'users' }],
// [{ type: 'dynamic', value: 'id' }],
// [{ type: 'static', value: 'profile' }],
// ],
// }
// Convert parsed segments to a Vue Router path
toVueRouterPath(result.segments) // => '/users/:id()/profile'
// Parse and convert a single segment (e.g., i18n per-locale route path)
const tokens = parseSegment('[...slug]')
// [{ type: 'catchall', value: 'slug' }]
toVueRouterSegment(tokens) // => ':slug(.*)*'Supported patterns
| Pattern | Example | Description |
|---|---|---|
| Static | about.vue |
Static route segment |
| Index | index.vue |
Index page (maps to /) |
| Dynamic | [slug].vue |
Required parameter |
| Optional | [[slug]].vue |
Optional parameter |
| Catchall | [...slug].vue |
Catch-all (zero or more segments) |
| Repeatable | [slug]+.vue |
One or more segments |
| Optional repeatable | [[slug]]+.vue |
Zero or more segments |
| Group | (admin)/dashboard.vue |
Route group (transparent to path, stored in meta) |
| Mixed | prefix-[slug]-suffix.vue |
Static and dynamic in one segment |
| Nested | parent.vue + parent/child.vue |
Parent layout with child routes |
| Named views | index@sidebar.vue |
Vue Router named view slots |
| Modes | page.client.vue |
Mode variants (configurable suffixes) |
API
buildTree(input, options?)
Build a route tree from file paths. Accepts raw strings, InputFile[] (with priority), or pre-parsed ParsedPath[].
function buildTree(
input: string[] | InputFile[] | ParsedPath[],
options?: BuildTreeOptions
): RouteTree
interface InputFile {
path: string
/** Lower number = higher priority. Default: 0 */
priority?: number
}Options (extends ParsePathOptions):
| Option | Type | Description |
|---|---|---|
roots |
string[] |
Root paths to strip (e.g., ['pages/', 'layer/pages/']) |
extensions |
string[] |
File extensions to strip (default: strip all) |
modes |
string[] |
Mode suffixes to detect (e.g., ['client', 'server']) |
warn |
(msg: string) => void |
Warning callback for invalid characters in dynamic params |
duplicateStrategy |
'first-wins' | 'last-wins' | 'error' |
How to handle duplicate paths (default: 'first-wins') |
When files from different layers collide at the same tree position, the file with the lowest priority number wins regardless of insertion order.
addFile(tree, filePath, options?)
Add a single file to an existing route tree in place. Parses the file and inserts it, avoiding a full rebuild. Accepts a plain string or InputFile with priority.
function addFile(
tree: RouteTree,
filePath: string | InputFile,
options?: BuildTreeOptions
): voidremoveFile(tree, filePath)
Remove a file from an existing route tree by its original file path. Prunes empty structural nodes left behind. Returns true if the file was found and removed.
function removeFile(tree: RouteTree, filePath: string): booleantoVueRouter4(tree, options?)
Emit Vue Router 4 route definitions from a tree. Handles nested routes, names, index promotion, structural collapse, groups, catchall optimisation, route ordering, named views, and mode variants.
function toVueRouter4(tree: RouteTree, options?: VueRouterEmitOptions): VueRoute[]
interface VueRoute {
name?: string
path: string
file?: string
/** Named view components. Only present when multiple views exist. */
components?: Record<string, string>
/** Mode variants. Only present when mode files exist. */
modes?: string[]
children: VueRoute[]
meta?: Record<string, unknown>
}
interface VueRouterEmitOptions {
/** Custom name generator. Receives raw `/`-separated name, returns final name. */
getRouteName?: (rawName: string) => string
/** Called when two routes produce the same name. */
onDuplicateRouteName?: (name: string, file: string, existingFile: string) => void
}Routes are sorted by segment priority within each level: static segments first, then dynamic, optional, and catchall last.
toRou3(tree)
Emit rou3/Nitro route patterns from a tree.
function toRou3(tree: RouteTree): Rou3Route[]
interface Rou3Route {
path: string
file: string
}toRegExp(tree)
Emit RegExp matchers from a tree.
function toRegExp(tree: RouteTree): RegExpRoute[]
interface RegExpRoute {
pattern: RegExp
keys: string[]
file: string
}toVueRouterSegment(tokens, options?)
Convert a single parsed segment (an array of tokens returned by parseSegment) into a Vue Router 4 path segment string. Useful for modules that already have resolved routes and only need segment-level path conversion (e.g., @nuxtjs/i18n converting per-locale custom paths).
function toVueRouterSegment(
tokens: ParsedPathSegmentToken[],
options?: ToVueRouterSegmentOptions
): string
interface ToVueRouterSegmentOptions {
/**
* Whether non-index segments follow this one.
* When true, catchall uses ([^/]*)*; when false (default), uses (.*)*
*/
hasSucceeding?: boolean
}import { parseSegment, toVueRouterSegment } from 'unrouting'
toVueRouterSegment(parseSegment('[id]')) // => ':id()'
toVueRouterSegment(parseSegment('[[opt]]')) // => ':opt?'
toVueRouterSegment(parseSegment('[...slug]')) // => ':slug(.*)*'
toVueRouterSegment(parseSegment('prefix-[slug]')) // => 'prefix-:slug()'
// i18n use case – parse a custom locale path segment
const tokens = parseSegment('[foo]_[bar]:[...buz]_buz_[[qux]]')
const path = `/${toVueRouterSegment(tokens)}`
// => '/:foo()_:bar()\::buz(.*)*_buz_:qux?'toVueRouterPath(segments)
Convert an array of parsed path segments into a full Vue Router 4 path string. Automatically determines hasSucceeding per segment so that mid-path catchalls use the restrictive ([^/]*)* pattern and terminal catchalls use (.*)*.
function toVueRouterPath(segments: ParsedPathSegment[]): stringimport { parsePath, toVueRouterPath } from 'unrouting'
toVueRouterPath(parsePath(['users/[id]/posts.vue'])[0].segments)
// => '/users/:id()/posts'
toVueRouterPath(parsePath(['[...slug]/suffix.vue'])[0].segments)
// => '/:slug([^/]*)*/suffix' (mid-path catchall auto-detected)
toVueRouterPath(parsePath(['prefix/[...slug].vue'])[0].segments)
// => '/prefix/:slug(.*)*' (terminal catchall)parsePath(filePaths, options?)
Parse file paths into segments. Standalone – does not build a tree.
function parsePath(filePaths: string[], options?: ParsePathOptions): ParsedPath[]
interface ParsedPath {
file: string
segments: ParsedPathSegment[]
meta?: { modes?: string[], name?: string }
}compileParsePath(options?)
Pre-compile parsing options into a reusable function. Useful in hot paths (e.g., dev server file watchers) where parsePath would otherwise reconstruct the same regexes on every call.
function compileParsePath(options?: ParsePathOptions): CompiledParsePath
interface CompiledParsePath {
(filePaths: string[]): ParsedPath[]
}Returns a callable with the same signature as parsePath (minus the options argument). The regexes for root stripping, extension matching, and mode detection are built once at compile time.
import { addFile, buildTree, compileParsePath, toVueRouter4 } from 'unrouting'
const opts = { roots: ['pages/'], modes: ['client', 'server'] }
// Compile once at startup
const parse = compileParsePath(opts)
const tree = buildTree(initialFiles, opts)
// In a file watcher callback — no regex re-compilation
addFile(tree, 'pages/new-page.vue', parse)
const routes = toVueRouter4(tree)The compiled function can be passed directly to addFile as the options argument:
// These are equivalent, but the compiled version avoids re-building regexes:
addFile(tree, file, parse) // pre-compiled (fast)
addFile(tree, file, opts) // raw options (re-compiles each call)parseSegment(segment, absolutePath?, warn?)
Parse a single filesystem segment into typed tokens. Useful for modules that need to parse custom paths (e.g., i18n locale-specific routes).
function parseSegment(
segment: string,
absolutePath?: string,
warn?: (message: string) => void
): ParsedPathSegmentToken[]
// Token types: 'static' | 'dynamic' | 'optional' | 'catchall' |
// 'repeatable' | 'optional-repeatable' | 'group'walkTree(tree, visitor)
Walk all nodes depth-first.
function walkTree(
tree: RouteTree,
visitor: (node: RouteNode, depth: number, parent: RouteNode | null) => void
): voidisPageNode(node)
Check if a node has files attached (page node vs structural node).
function isPageNode(node: RouteNode): booleanHow nesting works
The tree distinguishes between page nodes (have files) and structural nodes (directory-only, no files):
- Page nodes create nesting boundaries – children get relative paths
- Structural nodes collapse – their path segment is prepended to descendants
parent.vue + parent/child.vue
→ { path: '/parent', children: [{ path: 'child' }] }
parent/child.vue (no parent.vue)
→ { path: '/parent/child' } (structural 'parent' collapses)index.vue promotes a structural directory into a page node:
users/index.vue + users/[id].vue
→ { path: '/users', file: 'users/index.vue', children: [{ path: ':id()' }] }Route groups (name) are transparent – they don't affect paths or nesting, but are stored in meta.groups.
Development
- Clone this repository
- Enable Corepack using
corepack enable - Install dependencies using
pnpm install - Run interactive tests using
pnpm dev
License
Made with ❤️
Published under MIT License.