@follow-app/vite-plugin-route-builder
v0.1.1
Published
## Overview
Readme
Vite Route Builder Plugin - Technical Documentation
Overview
The Vite Route Builder plugin is a build-time code generation tool that creates React Router route configurations from a file system-based routing structure, similar to Next.js App Router. It transforms a pages/ directory structure into optimized route objects with lazy loading and proper component imports.
Core Architecture
Plugin Flow
- Build-time Generation: The plugin runs during Vite's build process to generate static route configurations
- File System Scanning: Uses glob patterns to discover page and layout files
- Route Tree Building: Leverages the existing
route-builder.tslogic to create route hierarchy - Code Generation: Produces a TypeScript file with lazy-loaded route objects
Key Components
vite-plugin-route-builder.ts: Main Vite plugin implementationroute-builder.ts: Core routing logic for transforming file paths to route objectsgenerated-routes.ts: Auto-generated output file containing route configurations
File System Routing Conventions
Directory Structure
pages/
├── (main)/ # Route group (doesn't affect URL path)
│ ├── layout.tsx # Layout component for grouped routes
│ ├── index.tsx # / route
│ └── discover.tsx # /discover route
├── settings/
│ ├── layout.tsx # Layout for /settings/*
│ ├── index.tsx # /settings route
│ └── profile.tsx # /settings/profile route
├── (settings)/ # Another route group
│ ├── layout.tsx # Layout component
│ └── general.tsx # /general route
└── [...404].tsx # Catch-all routeRoute Mapping Rules
- Index Routes:
index.tsxfiles create routes for their parent directory path - Named Routes: File names become route paths (e.g.,
profile.tsx→/profile) - Grouped Routes: Directories with parentheses
(name)group routes without affecting URL paths - Dynamic Routes:
[param].tsxcreates dynamic parameter routes - Catch-all Routes:
[...name].tsxcreates catch-all routes - Layouts:
layout.tsxfiles provide wrapper components for child routes - Sync Loading: Files with
.sync.tsxextension are loaded synchronously instead of lazy loading
Synchronous vs Asynchronous Loading
The plugin supports two loading strategies:
- Asynchronous Loading (default): Files with
.tsxextension are lazy-loaded for optimal performance - Synchronous Loading: Files with
.sync.tsxextension are imported directly and loaded synchronously
pages/
├── home.tsx # Lazy loaded: const lazy1 = () => import("./pages/home")
├── critical.sync.tsx # Sync loaded: import SyncComponent1 from "./pages/critical"
└── settings/
├── layout.sync.tsx # Sync loaded layout
└── profile.tsx # Lazy loaded pageWhen to Use Sync Loading
Use .sync.tsx extension for:
- Critical above-the-fold components
- Small, lightweight components that don't benefit from code splitting
- Components that need to be immediately available (no loading state)
- Layout components that are always needed
Implementation Details
1. Route Discovery Process
// Plugin discovers files using glob patterns
const pageFiles = glob.sync("./src/pages/**/*.{ts,tsx}", {
ignore: ["**/*.d.ts", "**/*.test.*", "**/*.spec.*"],
})2. Route Tree Generation
The plugin uses the existing route-builder.ts logic:
import { buildRoute } from "../route-builder"
// Transform file paths to route objects
const routes = buildRoute(pageFiles)3. Path Resolution Strategy
The plugin implements sophisticated path matching to connect route objects with their corresponding files:
function findFileForRoute(route: RouteObject, pageFiles: string[]): string | null {
const routePath = route.handle?.fs as string
if (!routePath) return null
// Strategy 1: Direct file match (fs.tsx)
let targetFile = `./src/pages/${routePath}.tsx`
if (pageFiles.includes(targetFile)) return targetFile
// Strategy 2: Layout file for grouped routes (fs/layout.tsx)
targetFile = `./src/pages/${routePath}/layout.tsx`
if (pageFiles.includes(targetFile)) return targetFile
// Strategy 3: Index file (fs/index.tsx)
targetFile = `./src/pages/${routePath}/index.tsx`
if (pageFiles.includes(targetFile)) return targetFile
// Strategy 4: Handle paths ending with '/' (index pages)
if (routePath.endsWith("/")) {
const cleanPath = routePath.slice(0, -1)
targetFile = `./src/pages/${cleanPath}/index.tsx`
if (pageFiles.includes(targetFile)) return targetFile
}
return null
}4. Loading Strategy Implementation
The plugin generates different import strategies based on file extensions:
Asynchronous Loading (.tsx files)
// Generate lazy imports for .tsx files
const lazyComponents = new Map<string, string>()
let lazyCounter = 1
function collectLazyFunctions(route: RouteObject, pageFiles: string[]) {
const file = findFileForRoute(route, pageFiles)
if (file && !file.endsWith(".sync.tsx") && !lazyComponents.has(file)) {
const relativePath = path.relative(
path.dirname("./src/generated-routes.ts"),
file.replace("./src/", "./src/"),
)
const varName = `LazyComponent${lazyCounter++}`
lazyComponents.set(file, varName)
return `const ${varName} = () => import("${relativePath}")`
}
}Synchronous Loading (.sync.tsx files)
// Generate direct imports for .sync.tsx files
const syncComponents = new Map<string, string>()
let syncCounter = 1
function collectSyncImports(route: RouteObject, pageFiles: string[]) {
const file = findFileForRoute(route, pageFiles)
if (file && file.endsWith(".sync.tsx") && !syncComponents.has(file)) {
const relativePath = path.relative(
path.dirname("./src/generated-routes.ts"),
file.replace("./src/", "./src/"),
)
const varName = `SyncComponent${syncCounter++}`
syncComponents.set(file, varName)
return `import ${varName} from "${relativePath}"`
}
}Route Object Assignment
Routes use different properties based on loading strategy:
function assignComponentToRoute(route: RouteObject, file: string) {
if (file.endsWith(".sync.tsx")) {
// Sync components use Component property
route.Component = syncComponents.get(file)
} else {
// Async components use lazy property
route.lazy = lazyComponents.get(file)
}
}5. Code Generation
The final step generates a complete TypeScript file:
const output = `
// Do not edit manually
/* eslint-disable */
// @ts-nocheck
import type { RouteObject } from "react-router"
import { lazy } from "react"
${lazyImports.join("\n")}
export const routes: RouteObject[] = ${serializedRoutes}
`Route Object Structure
Generated Route Format
interface RouteObject {
path?: string
index?: boolean
children?: RouteObject[]
element?: React.ComponentType
// Internal properties removed in final output
handle?: { fs: string } // Removed during serialization
}Example Generated Output
// Synchronous imports (loaded immediately)
import SyncComponent1 from "./pages/(main)/layout"
import SyncComponent2 from "./pages/critical-page"
// Asynchronous imports (lazy loaded)
const LazyComponent1 = () => import("./pages/(main)/index")
const LazyComponent2 = () => import("./pages/settings/layout")
const LazyComponent3 = () => import("./pages/settings/profile")
export const routes: RouteObject[] = [
{
path: "/",
Component: SyncComponent1, // Sync loaded layout
children: [
{
index: true,
lazy: LazyComponent1, // Lazy loaded page
},
{
path: "critical",
Component: SyncComponent2, // Sync loaded critical page
},
],
},
{
path: "/settings",
lazy: LazyComponent2, // Lazy loaded layout
children: [
{
path: "profile",
lazy: LazyComponent3, // Lazy loaded page
},
],
},
]Problem Solving Approach
Common Issues and Solutions
Path Mismatch: Routes generated without corresponding lazy functions
- Solution: Enhanced file matching with multiple strategies and path normalization
Incorrect Import Paths: Absolute paths causing import failures
- Solution: Proper relative path calculation using
path.relative()
- Solution: Proper relative path calculation using
Unused Lazy Variables: Too many lazy imports for non-existent files
- Solution: Only generate lazy imports for routes with actual file matches
Index Route Handling: Paths ending with
/causing mapping issues- Solution: Special handling for index pages and path cleaning
Debugging Strategy
The plugin includes comprehensive logging for troubleshooting:
console.log("📁 Page files found:", pageFiles.length)
console.log("🎯 Routes with lazy functions:", routesWithLazy)
console.log("📝 Generated lazy imports:", lazyComponents.size)Performance Considerations
Build-time Optimization
- Static Generation: All routing logic runs at build time, not runtime
- Lazy Loading: Components are loaded on-demand, reducing initial bundle size
- Tree Shaking: Unused routes and components are eliminated during bundling
Memory Efficiency
- File Caching: Plugin processes files once and caches results
- Selective Import: Only imports files that are actually used in routes
Configuration Options
The plugin accepts the following configuration options:
RouteBuilderPluginOptions
interface RouteBuilderPluginOptions {
/** Page files glob pattern */
pagePattern?: string
/** Output path for generated routes */
outputPath?: string
/** Whether to enable in dev mode */
enableInDev?: boolean
/** Custom file to route path transformation logic */
transformPath?: (path: string) => string
/** Whether to disable logging */
debug?: boolean
/** Custom order for segment groups in route tree. Array of group names (without parentheses). Default: filesystem order */
segmentGroupOrder?: string[]
}Option Descriptions
pagePattern(default:"./pages/**/*.{tsx,sync.tsx}"): Glob pattern for discovering page filesoutputPath(default:"./src/generated-routes.ts"): Output path for the generated route configurationenableInDev(default:true): Whether to enable route generation in development modetransformPath: Custom function to transform file paths before route generationdebug(default:false): Enable detailed logging for troubleshootingsegmentGroupOrder(default:[]): Custom ordering for route segment groups
Segment Group Ordering
By default, route groups (directories with parentheses like (main), (external)) are ordered alphabetically based on the filesystem. The segmentGroupOrder option allows you to specify a custom order:
// Example: Place (main) routes before (external) routes
routeBuilder({
segmentGroupOrder: ["main", "external"], // Without parentheses
})
// Or with parentheses (both formats supported)
routeBuilder({
segmentGroupOrder: ["(main)", "(external)"], // With parentheses
})Default Behavior (Filesystem Order)
pages/
├── (admin)/ # Third
├── (external)/ # First (alphabetically)
├── (main)/ # Second
└── (settings)/ # FourthWith Custom Order
// Configuration
segmentGroupOrder: ['main', 'admin']
// Result:
pages/
├── (main)/ # First (specified in order)
├── (admin)/ # Second (specified in order)
├── (external)/ # Third (not specified, filesystem order)
└── (settings)/ # Fourth (not specified, filesystem order)Usage Examples
// Basic usage with default options
import routeBuilder from "./vite-plugin-route-builder"
export default defineConfig({
plugins: [routeBuilder()],
})
// Advanced configuration with parentheses format
export default defineConfig({
plugins: [
routeBuilder({
pagePattern: "./src/pages/**/*.{tsx,sync.tsx}",
outputPath: "./src/router/generated-routes.ts",
enableInDev: true,
debug: true,
segmentGroupOrder: ["(main)", "(dashboard)", "(settings)", "(external)"],
transformPath: (path) => path.replace(/\.sync\.tsx$/, ".tsx"),
}),
],
})
// Mixed format is also supported
export default defineConfig({
plugins: [
routeBuilder({
segmentGroupOrder: ["(main)", "dashboard", "settings", "(external)"],
}),
],
})Integration Points
Vite Integration
// vite.config.ts
import { defineConfig } from "vite"
import routeBuilder from "./plugins/vite/vite-plugin-route-builder"
export default defineConfig({
plugins: [
routeBuilder(), // Add the route builder plugin
// ... other plugins
],
})React Router Integration
// App.tsx
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import { routes } from './generated-routes'
const router = createBrowserRouter(routes)
export default function App() {
return <RouterProvider router={router} />
}Future Enhancements
Potential Improvements
- Watch Mode: Real-time route regeneration during development
- TypeScript Validation: Compile-time route validation
- Route Metadata: Support for route-level metadata and guards
- Custom Conventions: Configurable file naming conventions
- Nested Layouts: Support for multiple layout levels
Extensibility
The plugin architecture allows for easy extension:
- Custom file processors for different route types
- Pluggable path resolution strategies
- Configurable code generation templates
- Integration with other meta-frameworks
Conclusion
The Vite Route Builder plugin successfully transforms file system-based routing into optimized React Router configurations. By leveraging build-time generation, it provides excellent performance while maintaining developer ergonomics similar to Next.js App Router. The robust path matching and lazy loading implementation ensures reliable route generation for complex application structures.
License
2025 © Innei, Released under the MIT License.
Personal Website · GitHub @Innei
