npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@onoxm/vite-plugin-auto-router

v0.9.7

Published

a vite plugin for automatically generating react or vue route files

Readme

@onoxm/vite-plugin-auto-router

A Vite plugin for automatically generating React or Vue route files.

English | 中文

✨ Features

  • Auto-generate route configuration, no manual maintenance needed
  • Supports both React and Vue frameworks
  • Convention-based routing, automatically mapped by directory structure
  • Supports dynamic route [id] syntax
  • home page path automatically converted to / (configurable)
  • __root__ page as root route container, wrapping all other routes (configurable)
  • Zero-config hot module replacement
  • Supports page-level configuration files
  • Virtual module mode: serves route code in-memory via virtual:onoxm-auto-router/{react|vue}, zero file-system writes
  • Auto-injects vite-env.d.ts type declarations in virtual module mode, out-of-the-box TypeScript support
  • Full IntelliSense: every plugin option ships with English JSDoc (defaults & migration notes) in the published type declarations — just hover in your IDE
  • TypeScript type safety

🚀 Installation

npm install -D @onoxm/vite-plugin-auto-router

📖 Usage Guide

Page Component Identification Rules

The plugin identifies page components based on the following rules:

React Project

  • Page components: Components with default export (export default) in the pages directory
  • Regular components: Components with named export (export const) in the pages directory

Vue Project

  • Page components:
    • Direct child components in the views directory (e.g., src/views/about.vue)
    • In nested directories, only index.vue is considered a page component (e.g., src/views/a/b/index.vue)
  • Regular components: Non-index.vue files in nested directories (e.g., src/views/a/b.vue)

React Project

Install Dependencies

npm install react-router

Configure Vite

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import autoRouter from '@onoxm/vite-plugin-auto-router/react'

export default defineConfig({
  plugins: [
    react(),
    autoRouter({
      // virtualModule will be the default in v0.10.0 and this option will be removed
      virtualModule: true
    })
  ]
})

Directory Structure

src/
├── pages/
│   ├── index.tsx
│   ├── __root__.tsx
│   ├── 404.tsx
│   └── user/
│       ├── index.tsx
│       ├── index.config.ts
│       ├── [id].tsx
│       └── [id].config.ts

Special Pages

  • home page: Path automatically converted to /, used as home route
  • __root__ page: Used as root route container, wrapping all other routes
  • 404 or notfound page: Path automatically converted to /*, used as 404 route

Page Configuration

Inherits from React Router RouteObject, with the following modifications:

  • Removed: Component, element, children
  • Added: type?: 'single' | 'wrap'

⚠️ Important Note

Configuration files must not use named exports (export const, export function, etc.). Only export default is allowed. If a configuration file contains named exports, the plugin will skip the file and display a warning in the console.

type: 'single'

When type is set to single, the page component will be generated as an independent route:

// src/pages/user/index.config.ts
import { defineConfig } from 'virtual:onoxm-auto-router/react'

export default defineConfig({
  type: 'single'
})

Generated route structure:

// virtual:onoxm-auto-router/react
import type { RouteObject } from 'react-router'
import Pages404 from './pages/404.tsx'
import Pages from './pages/index.tsx'
import PagesRoot from './pages/__root__.tsx'
import PagesUser from './pages/user/index.tsx'
import PagesUserId from './pages/user/[id]/index.tsx'

type PageConfig = Partial<
  Omit<RouteObject, 'Component' | 'element' | 'children'> & {
    type?: 'single' | 'wrap'
  }
>

export const defineConfig = (config: PageConfig) => config

export const routes: RouteObject[] = [
  {
    path: '/',
    element: <PagesRoot />,
    children: [
      {
        path: '/',
        element: <Pages />
      },
      {
        path: '/user',
        children: [
          {
            path: '',
            index: true,
            element: <PagesUser />
          },
          {
            path: ':id',
            children: [
              {
                path: '',
                index: true,
                action: async () => {},
                loader: async ({ params }) => await { params },
                element: <PagesUserId />
              }
            ]
          }
        ]
      }
    ]
  },
  {
    path: '/*',
    element: <Pages404 />
  }
]
type: 'wrap'

When type is set to wrap, the page component will act as a parent route container wrapping its child routes:

// src/pages/user/index.config.ts
import { defineConfig } from 'virtual:onoxm-auto-router/react'

export default defineConfig({
  type: 'wrap'
})

Generated route structure:

// virtual:onoxm-auto-router/react
import type { RouteObject } from 'react-router'
import Pages404 from './pages/404.tsx'
import Pages from './pages/index.tsx'
import PagesRoot from './pages/__root__.tsx'
import PagesUser from './pages/user/index.tsx'
import PagesUserId from './pages/user/[id]/index.tsx'

type PageConfig = Partial<
  Omit<RouteObject, 'Component' | 'element' | 'children'> & {
    type?: 'single' | 'wrap'
  }
>

export const defineConfig = (config: PageConfig) => config

export const routes: RouteObject[] = [
  {
    path: '/',
    element: <PagesRoot />,
    children: [
      {
        path: '/',
        element: <Pages />
      },
      {
        path: '/user',
        element: <PagesUser />,
        children: [
          {
            path: ':id',
            children: [
              {
                path: '',
                index: true,
                action: async () => {},
                loader: async ({ params }) => await { params },
                element: <PagesUserId />
              }
            ]
          }
        ]
      }
    ]
  },
  {
    path: '/*',
    element: <Pages404 />
  }
]
hydrateFallbackElement / HydrateFallback (React Router lazy mode)

When the page configuration contains hydrateFallbackElement or HydrateFallback, the plugin automatically switches the page to React Router's lazy mode:

  • No longer generates element field or synchronous import statements
  • Instead generates a lazy property that dynamically imports the component module and returns Component
  • Preserves the original hydrate field for fallback rendering during SSR hydration

In React Router v7, these are two parallel fields — you can use either or both:

| Field | Type | Accepts | | ------------------------ | --------------------- | ----------------------------- | | hydrateFallbackElement | React.ReactNode | JSX element <Loading /> | | HydrateFallback | React.ComponentType | Component reference Loading |

Usage 1: hydrateFallbackElement (JSX element)

The config file extension must be .config.tsx or .config.jsx:

// src/pages/index.config.tsx
import Loading from '../components/Loading'

export default defineConfig({
  hydrateFallbackElement: <Loading />
})

Usage 2: HydrateFallback (component reference)

// src/pages/index.config.tsx
import Loading from '../components/Loading'

export default defineConfig({
  HydrateFallback: Loading
})

Generated route structure (both usages produce the same output, differing only in the hydrate field):

// virtual:onoxm-auto-router/react
import type { RouteObject } from 'react-router'
import Loading from './components/Loading.tsx'

type PageConfig = Partial<
  Omit<RouteObject, 'Component' | 'element' | 'children' | 'lazy'> & {
    type?: 'single' | 'wrap'
  }
>

export const defineConfig = (config: PageConfig) => config

export const routes: RouteObject[] = [
  {
    path: '/',
    children: [
      {
        path: '',
        index: true,
        hydrateFallbackElement: <Loading />,  // or HydrateFallback: Loading
        lazy: () => import('./pages/index.tsx').then(m => ({ Component: m.default }))
      }
    ]
  }
]

lazy Mode Priority

| Page Config | Plugin lazy Option | Generation Mode | | ------------------------------------------------------ | -------------------- | ------------------------------------------------------------------- | | Contains hydrateFallbackElement or HydrateFallback | Any value | React Router lazy (highest priority, page-level overrides global) | | No hydrate config | true | React.lazy() + <Suspense> | | No hydrate config | false | Synchronous import + element |

Mixed Scenario

In the same project, routes with hydrate config use React Router lazy, while routes without it follow the global lazy option (React.lazy or synchronous import). The plugin intelligently determines whether to import { lazy, Suspense } from 'react' — when all routes use React Router lazy, it is not imported.

Note

  • When using hydrateFallbackElement with JSX elements, the extension must be .config.tsx or .config.jsx; when using HydrateFallback with component references, the extension can be .config.ts or .config.js
  • Imports of fallback components (e.g., Loading) are automatically collected, deduplicated, and injected into the generated route code
  • Do not use lowercase hydrateFallback (this field name does not exist in React Router v7; only HydrateFallback with capital H and hydrateFallbackElement are recognized)

Vue Project

Install Dependencies

npm install vue-router

Configure Vite

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import autoRouter from '@onoxm/vite-plugin-auto-router/vue'

export default defineConfig({
  plugins: [
    vue(),
    autoRouter({
      pagesDir: './src/views',
      configPattern: '/**/*.meta.ts',
      // virtualModule will be the default in v0.10.0 and this option will be removed
      virtualModule: true
    })
  ]
})

Directory Structure

src/
├── views/
│   ├── 404.vue
│   ├── home/
│   │   ├── index.vue
│   │   └── index.meta.ts
│   └── user/
│       ├── index.vue
│       ├── index.meta.ts
│       ├── [id].vue
│       └── [id].meta.ts

Special Pages

  • home page: Path automatically converted to /, used as home route
  • __root__ page: Used as root route container, wrapping all other routes
  • 404 or notfound page: Path automatically converted to /:pathMatch(.*)*, used as 404 route

Page Configuration

Inherits from Vue Router RouteRecordRaw, with the following modifications:

  • Removed: component, children
  • Added: type?: 'single' | 'wrap'

⚠️ Important Note

Configuration files must not use named exports (export const, export function, etc.). Only export default is allowed. If a configuration file contains named exports, the plugin will skip the file and display a warning in the console.

type: 'single'

When type is set to single, the page component will be generated as an independent route:

// src/views/user/index.meta.ts
import { defineConfig } from 'virtual:onoxm-auto-router/vue'

export default defineConfig({
  type: 'single'
})

Generated route structure:

// virtual:onoxm-auto-router/vue
import type { RouteRecordRaw } from 'vue-router'
import Views404 from './views/404.vue'
import ViewsHome from './views/home/index.vue'
import ViewsUser from './views/user/index.vue'
import ViewsUserId from './views/user/[id]/index.vue'

type PageConfig = Partial<
  Omit<RouteRecordRaw, 'component' | 'children'> & {
    type?: 'single' | 'wrap'
  }
>

export const defineConfig = (config: PageConfig) => config

export const routes: RouteRecordRaw[] = [
  {
    path: '/',
    children: [
      {
        path: '',
        name: 'home',
        component: ViewsHome
      }
    ]
  },
  {
    path: '/user',
    children: [
      {
        path: '',
        component: ViewsUser
      },
      {
        path: ':id',
        children: [
          {
            path: '',
            component: ViewsUserId
          }
        ]
      }
    ]
  },
  {
    path: '/:pathMatch(.*)*',
    children: [
      {
        path: '',
        component: Views404
      }
    ]
  }
]
type: 'wrap'

When type is set to wrap, the page component will act as a parent route container wrapping its child routes:

// src/views/user/index.meta.ts
import { defineConfig } from 'virtual:onoxm-auto-router/vue'

export default defineConfig({
  type: 'wrap'
})

Generated route structure:

// virtual:onoxm-auto-router/vue
import type { RouteRecordRaw } from 'vue-router'
import Views404 from './views/404.vue'
import ViewsHome from './views/home/index.vue'
import ViewsUser from './views/user/index.vue'
import ViewsUserId from './views/user/[id]/index.vue'

type PageConfig = Partial<
  Omit<RouteRecordRaw, 'component' | 'children'> & {
    type?: 'single' | 'wrap'
  }
>

export const defineConfig = (config: PageConfig) => config

export const routes: RouteRecordRaw[] = [
  {
    path: '/',
    children: [
      {
        path: '',
        name: 'home',
        component: ViewsHome
      }
    ]
  },
  {
    path: '/user',
    component: ViewsUser,
    children: [
      {
        path: ':id',
        children: [
          {
            path: '',
            component: ViewsUserId
          }
        ]
      }
    ]
  },
  {
    path: '/:pathMatch(.*)*',
    children: [
      {
        path: '',
        component: Views404
      }
    ]
  }
]

⚙️ Configuration Options

Plugin Configuration

| Option | Type | Default | Description | | --------------- | ------------------------------------------------ | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | framework | 'react' \| 'vue' | 'react' | Framework type | | pagesDir | string | './src/pages' | Pages directory | | routesFile | string | 'src/router/autoRouter.{tsx,jsx}' (React) / '{ts,js}' (Vue) | ⚠️ Deprecated, will be removed in v0.10.0. Generated route file path, resolved by framework and project language (ignored when virtualModule: true) | | keepHome | boolean | false | Whether to keep home page path | | keepRoot | boolean | false | Whether to keep __root__ page path | | lazy | boolean | true | Whether to enable lazy loading | | hmr | boolean | true | ⚠️ Deprecated, will be removed in v0.10.0. HMR is now enabled by default, no manual config needed | | hmrDebounceMs | number | 200 | ⚠️ Deprecated, will be removed in v0.10.0. HMR debounce delay in ms (ignored when virtualModule: true) | | configPattern | string | /**/*.config.{js,ts,jsx,tsx} | Config file format | | log | false \| 'tree' \| 'json' | false | Output route preview to console: 'tree' for readable tree structure, 'json' for formatted (pretty-printed) JSON (independent of file writing) | | dryRun | boolean | false | ⚠️ Deprecated, will be removed in v0.10.0. Only controls whether to skip file writes, does not affect console output (ignored when virtualModule: true) | | onGenerated | (filePaths: string[]) => Promise<void> \| void | undefined | ⚠️ Deprecated, will be removed in v0.10.0. Callback after routes generated (ignored when virtualModule: true) | | virtualModule | boolean | false | ⚠️ Deprecated. Will be removed in v0.10.0. Virtual module mode will become the default (and only) output format — enabled by default and no longer configurable. Please remove this option from your configuration; no replacement is needed. |

Deprecated options summary (will be removed in v0.10.0)

The following options are no longer needed due to the virtual module mechanism and will be fully removed in v0.10.0. Please remove them directly from your configuration; no replacement is needed:

| Option | Deprecated reason | Migration | | --------------- | --------------------------------------------------------------------------- | ------------------------------------------------------ | | routesFile | Virtual module doesn't write to disk, no file path needed | Remove it directly | | hmr | HMR is now enabled by default, no manual config needed | Remove it | | hmrDebounceMs | Virtual module updates via moduleGraph.invalidateModule, no debounce | Remove it | | dryRun | Virtual module already doesn't write to disk, use log for preview | Remove it, use log: 'tree' or log: 'json' instead | | onGenerated | Virtual module has no "generation complete" semantics, updates via Vite HMR | Remove it, use Vite plugin hooks to listen for updates | | virtualModule | Will become the only output format in v0.10.0, enabled by default | Remove it directly |

log Usage Example

The log option only controls whether the route tree preview is printed to the console, completely independent of file writing behavior. Supports three values:

  • false (default): no output
  • 'tree': outputs a readable route tree structure, component list, config imports, and target file path
  • 'json': outputs formatted (2-space indented) JSON (route tree with resolved component names, config imports, framework, routes file path) — readable in the console, still parseable by tooling
autoRouter({
  log: 'tree' // Print a readable route tree to the console
})

// or
autoRouter({
  log: 'json' // Print route info as JSON (pipe to a file or another tool)
})

Note

log controls printing, virtualModule controls file writing — the two are orthogonal. You can use log: 'tree' or log: 'json' alone to preview during normal route generation.

Virtual Module Mode (virtualModule)

Deprecation Notice

The virtualModule option is deprecated and will be removed in v0.10.0. Starting from v0.10.0, the plugin will always emit routes as a virtual module (virtual:onoxm-auto-router/{react|vue}). This mode will be enforced by default and no longer configurable.

Current version (0.9.x): You still need to explicitly set virtualModule: true to enable virtual module mode.

Migration advice: Start using virtualModule: true now, and remove other deprecated options (routesFile, hmr, dryRun, etc.) at the same time. Once v0.10.0 is released, you can then remove the virtualModule: true line.

Enable virtualModule: true to serve route code in-memory via the Vite virtual module virtual:onoxm-auto-router/react (or /vue) instead of writing to disk. Ideal for tree-shaking-friendly setups that prefer zero generated artifacts.

🚨 First-time Setup Required

The plugin auto-injects /// <reference types="@onoxm/vite-plugin-auto-router/virtual" /> into vite-env.d.ts during Vite startup. If your build script runs tsc before vite build (e.g., tsc -b && vite build), the first build may fail because tsc cannot resolve virtual:onoxm-auto-router/* yet.

Fix: Run vite dev or vite build once to let the plugin create vite-env.d.ts, then commit that file to your version control. All subsequent builds will work.

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import autoRouter from '@onoxm/vite-plugin-auto-router/react'

export default defineConfig({
  plugins: [
    react(),
    autoRouter({
      // virtualModule will be the default in v0.10.0 and this option will be removed
      virtualModule: true
    })
  ]
})

Import directly from the virtual module in your app code:

// src/router/index.ts
import { createBrowserRouter } from 'react-router'
import { routes } from 'virtual:onoxm-auto-router/react'

export const router = createBrowserRouter(routes)

Page config files also import defineConfig from the virtual module:

// src/pages/user/index.config.ts
import { defineConfig } from 'virtual:onoxm-auto-router/react'

export default defineConfig({
  type: 'single'
})

Build & SSR compatibility

Virtual module mode works in both vite dev and vite build, including SSR builds. The virtual module is resolved during the build phase just like regular source files. The generated route code is pure data (no browser API dependencies), so it is compatible with server-side rendering environments. However, the plugin does not provide SSR-specific entry points — for React SSR, users need to use createStaticRouter instead of createBrowserRouter in their server entry.

Options ignored in virtual module mode

The following options are ignored when virtualModule: true, and the plugin emits a warning:

  • routesFile: virtual module doesn't write to disk, no file path needed
  • hmr: HMR is now enabled by default, no manual config needed
  • hmrDebounceMs: virtual module updates via Vite's moduleGraph.invalidateModule, no debounce needed
  • dryRun: virtual module already doesn't write to disk, use log for preview instead
  • onGenerated: virtual module has no "generation complete" semantics, updates via Vite HMR

TypeScript type declaration auto-injection

The virtual module virtual:onoxm-auto-router/{react|vue} is an in-memory module with no physical file on disk, so TypeScript cannot resolve its named exports (routes, defineConfig) by default. To pass TS type checks, the module's type declarations must be added to the project's vite-env.d.ts.

The plugin package exposes the type declarations via the ./virtual subpath (@onoxm/vite-plugin-auto-router/virtual). When virtualModule: true is set and the project is a TypeScript project (i.e., tsconfig.json exists), the plugin silently auto-injects the following into vite-env.d.ts during Vite's configResolved hook (triggered by both dev and build):

/// <reference types="@onoxm/vite-plugin-auto-router/virtual" />

Injection rules:

| Scenario | Behavior | | ------------------------------------------------------------- | ------------------------------------------------------- | | src/vite-env.d.ts exists and already contains the reference | Skipped, content unchanged (idempotent) | | src/vite-env.d.ts exists but lacks the reference | Appended after a blank line | | Root vite-env.d.ts exists but no src/ one | Appended to the root file | | Both src/vite-env.d.ts and root exist | Processes the src/ one, leaves root untouched | | Neither exists | Creates src/vite-env.d.ts with the reference | | Any error | Only warns, does not throw, does not block Vite startup |

Page Configuration

| Option | Type | Default | Description | | ------ | -------------------- | ----------- | -------------------------------------------------------------------------- | | type | 'single' \| 'wrap' | 'single' | Route type | | path | string | undefined | Route path, supports [currentPath] placeholder to reference current path | | * | any | any | Inherits from router config |

path Usage Example

Use the [currentPath] placeholder to reference the current path when replacing:

// src/pages/user/index.config.ts
import { defineConfig } from 'virtual:onoxm-auto-router/react'

export default defineConfig({
  // Replace /user with /users/v2
  path: '/users/v2'
})

// Or use the placeholder to add a suffix to the current path
export default defineConfig({
  // Replace /user with /user/v2
  path: '[currentPath]/v2'
})

📄 License

MIT