Skip to content

Instantly share code, notes, and snippets.

@composite
Last active June 23, 2025 01:04
Show Gist options
  • Save composite/13499e1503d6f8bc1e686536fb0c0f64 to your computer and use it in GitHub Desktop.
Save composite/13499e1503d6f8bc1e686536fb0c0f64 to your computer and use it in GitHub Desktop.
A Vite plugin that directory and flat based router for React Router (Next.js style)

Vite Data Route plugin for ReactRouter v7

A powerful file-based routing plugin for React Router v7 that enables automatic route generation based on your file system structure, with full support for layouts, loaders, actions, and error boundaries.

Requirements

  • react-router 7.0.0 or higher
  • vite 6.0.0 or higher
  • glob package (dev dependency)

Installation

npm install -D glob
# or
pnpm add -D glob
# or  
yarn add -D glob

Usage

vite.config.ts

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import reactRoutePlugin from './vite-react-routing-plugin';

export default defineConfig({
  plugins: [
    reactRoutePlugin({ root: './src/app/pages' }), 
    react()
  ]
});

Client-side Integration

// src/app/index.tsx
import { createBrowserRouter, RouterProvider } from "react-router";
import React from "react";
import ReactDOM from "react-dom/client";
import { routes } from 'virtual:react-routing-plugin/client';

const router = createBrowserRouter(routes);
const root = document.getElementById("root");

ReactDOM.createRoot(root).render(
  <RouterProvider router={router} />
);

Server-side Integration (Optional)

// src/server/index.tsx
import { renderToString } from "react-dom/server";
import {
  createStaticHandler,
  createStaticRouter,
  StaticRouterProvider,
} from "react-router";
import { routes } from 'virtual:react-routing-plugin/server';

const { query, dataRoutes } = createStaticHandler(routes);

export async function handler(request: Request) {
  const context = await query(request);
  const router = createStaticRouter(dataRoutes, context);
  
  return new Response(renderToString(
    <StaticRouterProvider router={router} context={context} />
  ), { 
    status: context.statusCode,
    headers: { 'Content-Type': 'text/html' }
  });
}

TypeScript Support

Add the type definitions to your project:

// types/vite-react-routing-plugin.d.ts
declare module 'virtual:react-routing-plugin/client' {
  import { RouteObject } from 'react-router';
  export const routes: RouteObject[];
}

declare module 'virtual:react-routing-plugin/server' {
  import { RouteObject } from 'react-router';
  export const routes: RouteObject[];
}

API Reference

reactRoutePlugin

function reactRoutePlugin(opts?: DataRoutePluginOptions): Plugin[]

Options

  • root?: string - Base directory for file scanning. Default: './src/app/pages'
  • clientScan?: ScanOptions | false - Client-side virtual module scanning options. Set to false to disable or return minimal route structure for path checking only.
  • serverScan?: ScanOptions | false - Server-side virtual module scanning options. Set to false to disable or return minimal route structure for path checking only.
  • lazy?: boolean - Whether to use lazy imports for page components and routing modules. Default: true. Set to false for SSR/SSG optimization using static imports.

ScanOptions

interface ScanOptions {
  page?: boolean;      // Scan page components (default: true)
  route?: boolean;     // Scan route modules (default: true) 
  layout?: boolean;    // Scan layout components (default: true)
  error?: boolean;     // Scan error components (default: true)
  fallback?: boolean;  // Scan fallback components (default: true)
}

File-based Routing Rules

Required Files

At least one of these files must exist in a directory to define a route:

  • page.{js,jsx,ts,tsx} - Page component that exports a React component as default. Can optionally export loader and action functions.
  • route.{js,ts} - Route module that can export loader and action functions for both client and server.

Optional Files

  • layout.{js,jsx,ts,tsx} - Layout component that must export a React component as default and include <Outlet /> for child route rendering.
  • error.{js,jsx,ts,tsx} - Error boundary component for handling route errors.
  • fallback.{js,jsx,ts,tsx} - Fallback component for hydration fallback (HydrateFallback).

Layout Inheritance Rules

Important: Pages inherit layouts based on their actual file directory path, not the routing path.

  • Current directory layout takes priority
  • If no layout exists, searches parent directories (nearest first)
  • Flat routing also follows actual file path-based inheritance
  • Layout components must include <Outlet /> to render child routes

Loader and Action Priority

  • Client-side: If both page.js and route.js exist, only route.js loader/action functions are used
  • Server-side: Only route.js loader/action functions are executed; page.js functions are ignored

Routing Patterns

Static Routes

Standard directory/file names create static routes:

FILESYSTEM                     URL
====================           ======
pages/page.js                  /
pages/about/page.js            /about  
pages/jobs/page.js             /jobs
pages/$$email/page.js          /$email

Dynamic Routes

Use $ prefix for dynamic segments:

FILESYSTEM                         URL
========================           =======================
pages/movie/$id/page.js            /movie/1, /movie/2, ...
pages/actor/$gender/$name/page.js  /actor/male/john, /actor/female/jane

Optional Dynamic Routes

Use $ suffix for optional segments:

FILESYSTEM                         URL
========================           =======================
pages/theatre/$id$/page.js         /theatre/1, /theatre/2, /theatre
pages/seats/$id/get$/page.js       /seats/1, /seats/1/get, /seats/2/get

Group Routes

Use parentheses to group routes without adding URL segments:

FILESYSTEM                             URL
================================       ==================
pages/(front)/page.js                  /
pages/(front)/about/page.js            /about
pages/admin-panel/page.js              /admin-panel

Flat Routes

Use dots (.) in directory names to create flat routing structure:

FILESYSTEM                         URL
====================               ======
pages/about.ours/page.js           /about/ours
pages/movie.$id/page.js            /movie/1, /movie/2
pages/actor.$gender.$name/page.js  /actor/male/john, /actor/female/jane

Catch-all Routes

Use $ as the segment name:

FILESYSTEM                     URL
========================       =======================
pages/address/$/page.js        /address/seoul, /address/kyeonggi/anyang

Access the catch-all parameter with params['*'] in your components.

Route Priority

Routes are resolved in the following priority order:

  1. Static routes - Exact string matches
  2. Dynamic routes - Routes with :param segments
  3. Flat routes - Routes defined with dot notation
  4. Group routes - Routes with parentheses grouping
  5. Folder catch-all routes - $/page.js
  6. Flat catch-all routes - address.$/page.js

Generated Route Structure Example

Given this file structure:

src/app/pages/
├── (main)/
│   ├── layout.tsx
│   ├── page.tsx
│   └── system/
│       ├── member/
│       │   ├── page.tsx
│       │   └── new/
│       │       └── page.tsx
│       └── role/
│           └── page.tsx
└── auth/
    └── login/
        └── page.tsx

The plugin generates:

export const routes = [
  {
    path: "/",
    Component: Layout0,
    children: [
      {
        index: true,
        lazy: () => import("/src/app/pages/(main)/page.tsx")
          .then(m => ({ Component: m.default, loader: m.loader, action: m.action }))
      },
      {
        path: "system/member",
        lazy: () => import("/src/app/pages/(main)/system/member/page.tsx")
          .then(m => ({ Component: m.default, loader: m.loader, action: m.action }))
      },
      {
        path: "system/member/new", 
        lazy: () => import("/src/app/pages/(main)/system/member/new/page.tsx")
          .then(m => ({ Component: m.default, loader: m.loader, action: m.action }))
      },
      {
        path: "system/role",
        lazy: () => import("/src/app/pages/(main)/system/role/page.tsx")
          .then(m => ({ Component: m.default, loader: m.loader, action: m.action }))
      }
    ]
  },
  {
    path: "/auth/login",
    lazy: () => import("/src/app/pages/auth/login/page.tsx")
      .then(m => ({ Component: m.default, loader: m.loader, action: m.action }))
  }
];

Error Handling

The plugin throws these error types during build time and development:

  • InvalidRouteError - Invalid route configuration (e.g., flat routes with grouping)
  • DuplicateRouteError - Conflicting route paths

When errors occur, the plugin provides fallback empty routes and logs detailed error information to keep the development server running.

Hot Module Replacement

The plugin supports HMR with intelligent update strategies:

  • File content changes: Attempts partial route reconstruction, falls back to full reconstruction if needed
  • File add/delete/rename: Full route reconstruction
  • Debounced updates: Multiple rapid changes are batched with a 100ms debounce

Performance Features

  • Map-based lookups: O(1) file and directory resolution
  • Single-pass scanning: Files are processed once per update
  • Efficient hierarchy building: Bottom-up directory tree construction
  • Lazy loading support: Code splitting with dynamic imports by default
  • Eager loading option: Static imports for SSR/SSG optimization

Migration from Other Routing Solutions

From Manual React Router Configuration

  1. Move your route components to the pages directory following the file conventions
  2. Extract loaders and actions to separate route.js files or keep them in page.js files
  3. Convert layout components to use <Outlet /> instead of manual routing
  4. Replace manual createBrowserRouter calls with the virtual module import

From Next.js App Router

  • app/page.tsxpages/page.tsx
  • app/layout.tsxpages/layout.tsx
  • app/loading.tsxpages/fallback.tsx
  • app/error.tsxpages/error.tsx
  • app/[slug]/page.tsxpages/$slug/page.tsx

Troubleshooting

Routes not updating in development

  • Check that files are within the configured root directory
  • Verify file extensions match the supported patterns
  • Check browser console for route generation errors

Layout not applying to child routes

  • Ensure layout components include <Outlet />
  • Verify the layout is in the correct directory relative to the page
  • Check that layout scanning is enabled in options

TypeScript errors with virtual modules

  • Add the provided type definitions to your project
  • Ensure TypeScript can resolve the virtual module paths
  • Check that vite/client types are included in your tsconfig.json

Advanced Configuration

Custom File Patterns

The plugin currently supports these file patterns:

  • **/route.{js,ts}
  • **/{page,layout,error,fallback}.{js,jsx,ts,tsx}

Environment-specific Builds

Use different scan options for different environments:

export default defineConfig(({ mode }) => ({
  plugins: [
    reactRoutePlugin({
      lazy: mode === 'development', // Eager loading in production
      clientScan: mode === 'development' ? undefined : { layout: false }, // Disable layouts in production client
    }),
    react()
  ]
}));

This plugin provides a powerful, flexible file-based routing solution that scales from simple static sites to complex applications with nested layouts and dynamic routes.

import type { Plugin } from 'vite';
import * as path from 'path';
import { glob } from 'glob';
export interface ScanOptions {
page?: boolean;
route?: boolean;
layout?: boolean;
error?: boolean;
fallback?: boolean;
}
export interface DataRoutePluginOptions {
root?: string;
clientScan?: ScanOptions | false;
serverScan?: ScanOptions | false;
lazy?: boolean;
}
interface RouteObjectTemplate {
path?: string;
index?: boolean;
children?: RouteObjectTemplate[];
componentImport?: string;
loaderImport?: string;
actionImport?: string;
layoutImport?: string;
errorBoundaryImport?: string;
hydrateFallbackImport?: string;
isLazy?: boolean;
filePath?: string;
dir?: string;
}
interface RouteFile {
type: 'page' | 'route' | 'layout' | 'error' | 'fallback';
filePath: string;
routePath: string;
isFlat: boolean;
dir: string;
}
interface ImportInfo {
importName: string;
filePath: string;
}
class InvalidRouteError extends Error {
constructor(message: string, filePath: string) {
super(`${message} at ${filePath}`);
this.name = 'InvalidRouteError';
}
}
class DuplicateRouteError extends Error {
constructor(message: string, paths: string[]) {
super(`${message}: ${paths.join(', ')}`);
this.name = 'DuplicateRouteError';
}
}
const dirSplitRx = /[/.]/;
export default function reactRoutePlugin(
opts: DataRoutePluginOptions = {}
): Plugin[] {
const options = {
root: opts.root || './src/app/pages',
clientScan:
opts.clientScan !== false
? {
page: true,
route: true,
layout: true,
error: true,
fallback: true,
...opts.clientScan,
}
: (false as const),
serverScan:
opts.serverScan !== false
? {
page: true,
route: true,
layout: true,
error: true,
fallback: true,
...opts.serverScan,
}
: (false as const),
lazy: opts.lazy !== false,
};
let routeFiles: RouteFile[] = [];
let clientCode: string = '';
let serverCode: string = '';
function isFlatRouting(filePath: string): boolean {
const segments = path.dirname(filePath).split('/').filter(Boolean);
return segments.some((segment) => {
const trimmed = segment.replace(/^\.+|\.+$/g, '');
return trimmed.includes('.');
});
}
function scanFiles(): RouteFile[] {
const rootPath = path.resolve(options.root);
const files: RouteFile[] = [];
const patterns = [
'**/route.{js,ts}',
'**/{page,layout,error,fallback}.{js,jsx,ts,tsx}',
];
patterns.forEach((pattern) => {
const foundFiles = glob.sync(pattern, { cwd: rootPath });
foundFiles.forEach((file) => {
const fullPath = path.join(rootPath, file);
const parsed = path.parse(file);
const type = parsed.name as RouteFile['type'];
const dir = path.dirname(fullPath);
const isFlat = isFlatRouting(file);
if (isFlat && file.includes('(')) {
throw new InvalidRouteError(
'Group routing not allowed in flat routing',
fullPath
);
}
const routePath = generateRoutePath(file, isFlat);
files.push({
type,
filePath: fullPath,
routePath,
isFlat,
dir,
});
});
});
return files;
}
function generateRoutePath(filePath: string, isFlat: boolean): string {
const parsed = path.parse(filePath);
if (isFlat) {
const segments = parsed.name.split('.');
return '/' + segments.map(convertSegment).filter(Boolean).join('/');
} else {
const segments = path.dirname(filePath).split('/').filter(Boolean);
return '/' + segments.map(convertSegment).filter(Boolean).join('/');
}
}
function convertSegment(segment: string): string {
if (segment.startsWith('(') && segment.endsWith(')')) {
return '';
}
if (segment.includes('$$')) {
return segment.replace(/\$\$/g, '$');
}
if (segment.startsWith('$') && segment.endsWith('$')) {
const param = segment.slice(1, -1);
if (!param) return '';
return `:${param}?`;
}
if (segment.startsWith('$')) {
return `:${segment.slice(1)}`;
}
if (segment === '$') {
return '*';
}
return segment;
}
function validateRoutes(files: RouteFile[]): void {
const routePaths = new Map<string, RouteFile[]>();
files.forEach((file) => {
if (file.type === 'page' || file.type === 'route') {
const existing = routePaths.get(file.routePath) || [];
existing.push(file);
routePaths.set(file.routePath, existing);
}
});
routePaths.forEach((routeFiles, routePath) => {
if (routeFiles.length > 1) {
const types = routeFiles.map((f) => f.type);
const hasPage = types.includes('page');
const hasRoute = types.includes('route');
if (hasPage && hasRoute && routeFiles.length === 2) {
return;
}
throw new DuplicateRouteError(
`Duplicate route ${routePath}`,
routeFiles.map((f) => f.filePath)
);
}
});
}
function findNearestFile(
dir: string,
fileMap: Map<string, RouteFile>
): RouteFile | undefined {
let currentDir = dir;
while (currentDir !== path.dirname(currentDir)) {
if (fileMap.has(currentDir)) {
return fileMap.get(currentDir);
}
currentDir = path.dirname(currentDir);
}
return undefined;
}
function buildRouteTree(
files: RouteFile[],
isServer: boolean,
scanOptions: ScanOptions | false
): RouteObjectTemplate[] {
if (scanOptions === false) {
return files
.filter((f) => f.type === 'page' || f.type === 'route')
.map((f) => ({ path: f.routePath }));
}
const rootPath = path.resolve(options.root);
// 1. 파일별 맵 생성
const layoutMap = new Map<string, RouteFile>();
const errorMap = new Map<string, RouteFile>();
const fallbackMap = new Map<string, RouteFile>();
const pageRouteFiles = files.filter(
(f) => f.type === 'page' || f.type === 'route'
);
files.forEach((file) => {
if (file.type === 'layout' && scanOptions.layout) {
layoutMap.set(file.dir, file);
} else if (file.type === 'error' && scanOptions.error) {
errorMap.set(file.dir, file);
} else if (file.type === 'fallback' && scanOptions.fallback) {
fallbackMap.set(file.dir, file);
}
});
// 2. 디렉토리별 그룹핑 (실제 파일 디렉토리 기준)
const dirMap = new Map<
string,
{
routes: RouteFile[];
layout?: RouteFile;
children: Map<string, any>;
}
>();
// 모든 디렉토리 초기화
const allDirs = new Set<string>();
files.forEach((file) => {
let currentDir = file.dir;
while (
currentDir !== path.dirname(currentDir) &&
currentDir.startsWith(rootPath)
) {
allDirs.add(currentDir);
currentDir = path.dirname(currentDir);
}
});
allDirs.forEach((dir) => {
dirMap.set(dir, { routes: [], children: new Map() });
});
// 라우트 파일들을 디렉토리에 배치
pageRouteFiles.forEach((file) => {
if (
(file.type === 'page' && scanOptions.page) ||
(file.type === 'route' && scanOptions.route)
) {
const entry = dirMap.get(file.dir);
if (entry) {
entry.routes.push(file);
}
}
});
// 레이아웃 연결
dirMap.forEach((entry, dir) => {
const layout = layoutMap.get(dir);
if (layout) {
entry.layout = layout;
}
});
// 3. 디렉토리 계층 구조 생성 (bottom-up)
const sortedDirs = [...allDirs].sort(
(a, b) => b.split(dirSplitRx).length - a.split(dirSplitRx).length
); // 깊은 것부터
sortedDirs.forEach((dir) => {
const parentDir = path.dirname(dir);
if (parentDir !== dir && dirMap.has(parentDir)) {
const parent = dirMap.get(parentDir)!;
const current = dirMap.get(dir)!;
parent.children.set(dir, current);
}
});
// 4. RouteObject 생성
function createRouteObject(routeFile: RouteFile): RouteObjectTemplate {
const routeObj: RouteObjectTemplate = {
path: routeFile.routePath,
isLazy: options.lazy,
filePath: routeFile.filePath,
dir: routeFile.dir,
};
if (routeFile.type === 'page') {
routeObj.componentImport = routeFile.filePath;
if (!isServer) {
// Check if there's a route file in the same directory
const hasRouteFile = pageRouteFiles.some(
(f) =>
f.type === 'route' &&
f.dir === routeFile.dir &&
f.routePath === routeFile.routePath
);
if (!hasRouteFile) {
routeObj.loaderImport = routeFile.filePath;
routeObj.actionImport = routeFile.filePath;
}
}
}
if (routeFile.type === 'route') {
routeObj.loaderImport = routeFile.filePath;
routeObj.actionImport = routeFile.filePath;
}
// Add inherited error/fallback
const nearestError = findNearestFile(routeFile.dir, errorMap);
const nearestFallback = findNearestFile(routeFile.dir, fallbackMap);
if (nearestError) routeObj.errorBoundaryImport = nearestError.filePath;
if (nearestFallback)
routeObj.hydrateFallbackImport = nearestFallback.filePath;
return routeObj;
}
function createLayoutObject(layoutFile: RouteFile): RouteObjectTemplate {
const layoutObj: RouteObjectTemplate = {
path: layoutFile.routePath,
layoutImport: layoutFile.filePath,
isLazy: options.lazy,
children: [],
filePath: layoutFile.filePath,
dir: layoutFile.dir,
};
// Add inherited error/fallback for layout
const nearestError = findNearestFile(layoutFile.dir, errorMap);
const nearestFallback = findNearestFile(layoutFile.dir, fallbackMap);
if (nearestError) layoutObj.errorBoundaryImport = nearestError.filePath;
if (nearestFallback)
layoutObj.hydrateFallbackImport = nearestFallback.filePath;
return layoutObj;
}
// 5. 최상위 디렉토리부터 트리 구성
function buildTreeFromDir(dir: string): RouteObjectTemplate[] {
const entry = dirMap.get(dir);
if (!entry) return [];
const result: RouteObjectTemplate[] = [];
// 현재 디렉토리의 레이아웃이 있으면 레이아웃을 만들고 자식들을 추가
if (entry.layout) {
const layoutObj = createLayoutObject(entry.layout);
// 같은 디렉토리의 페이지들을 index route로 추가
entry.routes.forEach((route) => {
const routeObj = createRouteObject(route);
routeObj.index = true;
delete routeObj.path;
layoutObj.children!.push(routeObj);
});
// 자식 디렉토리들 처리
entry.children.forEach((_, childDir) => {
const childRoutes = buildTreeFromDir(childDir);
childRoutes.forEach((childRoute) => {
// 자식 라우트의 경로를 상대 경로로 변경
if (childRoute.path && layoutObj.path) {
const relativePath = childRoute.path.replace(
layoutObj.path === '/' ? '/' : layoutObj.path + '/',
''
);
if (relativePath !== childRoute.path) {
childRoute.path = relativePath;
}
}
layoutObj.children!.push(childRoute);
});
});
result.push(layoutObj);
} else {
// 레이아웃이 없으면 페이지들을 직접 추가
entry.routes.forEach((route) => {
result.push(createRouteObject(route));
});
// 자식 디렉토리들도 처리
entry.children.forEach((_, childDir) => {
result.push(...buildTreeFromDir(childDir));
});
}
return result;
}
// 루트부터 시작해서 트리 구성
return buildTreeFromDir(rootPath);
}
function generateRouteCode(routes: RouteObjectTemplate[]): string {
const importMap = new Map<string, ImportInfo>(); // 파일경로 -> ImportInfo 매핑
let importCounter = 0;
// 고급 import 중복 제거 로직
function getImportReference(
filePath: string,
type: 'default' | 'loader' | 'action' | 'error' | 'fallback'
): string {
if (!importMap.has(filePath)) {
let prefix = 'Component';
if (filePath.includes('layout.')) prefix = 'Layout';
else if (filePath.includes('error.')) prefix = 'ErrorBoundary';
else if (filePath.includes('fallback.')) prefix = 'HydrateFallback';
else if (filePath.includes('route.')) prefix = 'Route';
else if (filePath.includes('page.')) prefix = 'Page';
importMap.set(filePath, {
importName: `${prefix}${importCounter++}`,
filePath,
});
}
const importInfo = importMap.get(filePath)!;
// namespace import 방식으로 접근
switch (type) {
case 'loader':
return `${importInfo.importName}.loader`;
case 'action':
return `${importInfo.importName}.action`;
default:
return `${importInfo.importName}.default`;
}
}
function generateImports(): string[] {
return [...importMap.values()].map(
({ importName, filePath }) =>
`import * as ${importName} from ${JSON.stringify(filePath)};`
);
}
function processRoute(route: RouteObjectTemplate): string {
const parts: string[] = [];
if (route.path) {
parts.push(`path: ${JSON.stringify(route.path)}`);
}
if (route.index) {
parts.push('index: true');
}
// Layout 처리
if (route.layoutImport) {
const importName = getImportReference(route.layoutImport, 'default');
parts.push(`Component: ${importName}`);
}
// Page Component 처리
else if (route.componentImport) {
if (route.isLazy) {
// Lazy 모드에서는 기존 방식 유지
const lazyParts: string[] = [];
lazyParts.push(`Component: m.default`);
if (route.loaderImport) lazyParts.push(`loader: m.loader`);
if (route.actionImport) lazyParts.push(`action: m.action`);
parts.push(
`lazy: () => import(${JSON.stringify(route.componentImport)}).then(m => ({ ${lazyParts.join(', ')} }))`
);
} else {
// Eager 모드에서 최적화된 import 사용
const componentName = getImportReference(
route.componentImport,
'default'
);
parts.push(`Component: ${componentName}`);
if (route.loaderImport === route.componentImport) {
// 같은 파일에서 loader 가져오기
const loaderName = getImportReference(route.loaderImport, 'loader');
parts.push(`loader: ${loaderName}`);
} else if (route.loaderImport) {
// 다른 파일에서 loader 가져오기
const loaderName = getImportReference(route.loaderImport, 'loader');
parts.push(`loader: ${loaderName}`);
}
if (route.actionImport === route.componentImport) {
// 같은 파일에서 action 가져오기
const actionName = getImportReference(route.actionImport, 'action');
parts.push(`action: ${actionName}`);
} else if (route.actionImport) {
// 다른 파일에서 action 가져오기
const actionName = getImportReference(route.actionImport, 'action');
parts.push(`action: ${actionName}`);
}
}
}
// Route-only (component 없이 loader/action만)
else if (route.loaderImport || route.actionImport) {
if (route.isLazy) {
const lazyParts: string[] = [];
if (route.loaderImport) lazyParts.push(`loader: m.loader`);
if (route.actionImport) lazyParts.push(`action: m.action`);
const importPath = route.loaderImport || route.actionImport;
parts.push(
`lazy: () => import(${JSON.stringify(importPath)}).then(m => ({ ${lazyParts.join(', ')} }))`
);
} else {
if (route.loaderImport) {
const loaderName = getImportReference(route.loaderImport, 'loader');
parts.push(`loader: ${loaderName}`);
}
if (route.actionImport) {
const actionName = getImportReference(route.actionImport, 'action');
parts.push(`action: ${actionName}`);
}
}
}
// Error/Fallback 처리 (항상 eager)
if (route.errorBoundaryImport) {
const importName = getImportReference(
route.errorBoundaryImport,
'error'
);
parts.push(`ErrorBoundary: ${importName}`);
}
if (route.hydrateFallbackImport) {
const importName = getImportReference(
route.hydrateFallbackImport,
'fallback'
);
parts.push(`HydrateFallback: ${importName}`);
}
if (route.children && route.children.length > 0) {
const childrenCode = route.children.map(processRoute).join(',\n ');
parts.push(`children: [\n ${childrenCode}\n ]`);
}
return ` {\n ${parts.join(',\n ')}\n }`;
}
// Route 처리
const routeObjects = routes.map(processRoute);
// 최적화된 import 구문 생성
const importStatements = generateImports();
const importsCode =
importStatements.length > 0 ? importStatements.join('\n') + '\n' : '';
const routesCode = `[\n${routeObjects.join(',\n')}\n]`;
return `${importsCode}export const routes = ${routesCode};`;
}
function generateFallbackCode(): string {
return 'export const routes = [];';
}
function updateRoutes(): void {
try {
routeFiles = scanFiles();
validateRoutes(routeFiles);
if (options.clientScan || options.clientScan === false) {
const clientRoutes = buildRouteTree(
routeFiles,
false,
options.clientScan
);
clientCode = generateRouteCode(clientRoutes);
}
if (options.serverScan || options.serverScan === false) {
const serverRoutes = buildRouteTree(
routeFiles,
true,
options.serverScan
);
serverCode = generateRouteCode(serverRoutes);
}
} catch (error) {
console.error('Route generation failed:', error);
clientCode = generateFallbackCode();
serverCode = generateFallbackCode();
}
}
updateRoutes();
return [
{
name: 'react-routing-plugin',
configureServer(server) {
const rootPath = path.resolve(options.root);
server.watcher.add(rootPath);
const debounceEvent = Object.assign(
(file: string) => {
if (!file.startsWith(rootPath)) return;
if (debounceEvent.timer) {
clearTimeout(debounceEvent.timer);
}
debounceEvent.timer = setTimeout(() => {
console.log(
`[${new Date().toLocaleString('sv')}]`,
'react-routing-plugin: Updating routes...'
);
updateRoutes();
const moduleClient = server.moduleGraph.getModuleById(
'virtual:react-routing-plugin/client'
);
const moduleServer = server.moduleGraph.getModuleById(
'virtual:react-routing-plugin/server'
);
if (moduleClient) {
void server.reloadModule(moduleClient);
}
if (moduleServer) {
void server.reloadModule(moduleServer);
}
debounceEvent.timer = null;
}, 100);
},
{ timer: null as NodeJS.Timeout | null }
);
server.watcher.on('change', debounceEvent);
},
resolveId(id) {
if (id === 'virtual:react-routing-plugin/client') {
return id;
}
if (id === 'virtual:react-routing-plugin/server') {
return id;
}
},
load(id) {
if (id === 'virtual:react-routing-plugin/client') {
return clientCode;
}
if (id === 'virtual:react-routing-plugin/server') {
return serverCode;
}
},
buildStart() {
updateRoutes();
},
},
];
}
declare module 'virtual:react-routing-plugin/client' {
import { RouteObject } from 'react-router';
export const routes: RouteObject[];
}
declare module 'virtual:react-routing-plugin/server' {
import { RouteObject } from 'react-router';
export const routes: RouteObject[];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment