@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 homepage 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.tstype 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.vueis considered a page component (e.g.,src/views/a/b/index.vue)
- Direct child components in the views directory (e.g.,
- Regular components: Non-
index.vuefiles in nested directories (e.g.,src/views/a/b.vue)
React Project
Install Dependencies
npm install react-routerConfigure 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.tsSpecial Pages
homepage: Path automatically converted to/, used as home route__root__page: Used as root route container, wrapping all other routes404ornotfoundpage: 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.). Onlyexport defaultis 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
elementfield or synchronousimportstatements - Instead generates a
lazyproperty that dynamically imports the component module and returnsComponent - 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
hydrateFallbackElementwith JSX elements, the extension must be.config.tsxor.config.jsx; when usingHydrateFallbackwith component references, the extension can be.config.tsor.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; onlyHydrateFallbackwith capital H andhydrateFallbackElementare recognized)
Vue Project
Install Dependencies
npm install vue-routerConfigure 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.tsSpecial Pages
homepage: Path automatically converted to/, used as home route__root__page: Used as root route container, wrapping all other routes404ornotfoundpage: 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.). Onlyexport defaultis 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 viamoduleGraph.invalidateModule, no debounce | Remove it | |dryRun| Virtual module already doesn't write to disk, uselogfor preview | Remove it, uselog: 'tree'orlog: '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
logcontrols printing,virtualModulecontrols file writing — the two are orthogonal. You can uselog: 'tree'orlog: 'json'alone to preview during normal route generation.
Virtual Module Mode (virtualModule)
Deprecation Notice
The
virtualModuleoption 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: trueto enable virtual module mode.Migration advice: Start using
virtualModule: truenow, and remove other deprecated options (routesFile,hmr,dryRun, etc.) at the same time. Once v0.10.0 is released, you can then remove thevirtualModule: trueline.
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" />intovite-env.d.tsduring Vite startup. If your build script runstscbeforevite build(e.g.,tsc -b && vite build), the first build may fail becausetsccannot resolvevirtual:onoxm-auto-router/*yet.Fix: Run
vite devorvite buildonce to let the plugin createvite-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 neededhmr: HMR is now enabled by default, no manual config neededhmrDebounceMs: virtual module updates via Vite'smoduleGraph.invalidateModule, no debounce neededdryRun: virtual module already doesn't write to disk, uselogfor preview insteadonGenerated: 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
