JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 3511
  • Score
    100M100P100Q101302F

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

    npm version npm downloads bundle Codecov License JSDocs

    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/removeFile for dev server HMR)
    • Pluggable route name generation
    • Route ordering by segment priority (static > dynamic > optional > catchall)
    • Named view support (@viewName convention)
    • Mode variant support (.client, .server, configurable)
    • Duplicate route name detection
    • Emit to framework routers

    Install

    # npm
    npm install unrouting
    
    # pnpm
    pnpm install unrouting

    Usage

    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
    ): void

    removeFile(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): boolean

    toVueRouter4(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[]): string
    import { 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
    ): void

    isPageNode(node)

    Check if a node has files attached (page node vs structural node).

    function isPageNode(node: RouteNode): boolean

    How 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.