next-vite-router 1.0.8 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,287 +1,279 @@
1
- # Next Vite Router
2
-
3
- Next Vite Router is an npm package that provides Next.js-style, file-based routing for React applications built with Vite. It generates React Router route objects from a filesystem layout (pages, layouts, not-found), and it exposes a Vite plugin that injects a virtual module to generate routes at build/dev time.
4
-
5
- ## Installation
6
-
7
- Install with your package manager of choice:
8
-
9
- ```next-vite-router/README.md#L1-4
10
- pnpm add next-vite-router
11
- # or
12
- npm install next-vite-router
13
- # or
14
- yarn add next-vite-router
15
- ```
16
-
17
- The package has peer dependencies: React, React DOM and React Router DOM. Make sure your project already includes compatible versions (React 18+ is recommended).
18
-
19
- ## CLI / Getting Started
20
-
21
- The quickest way to start a new project with `next-vite-router` is to use the interactive CLI:
22
-
23
- ```next-vite-router/README.md#L5-10
24
- npx next-vite-router init
25
- # or
26
- pnpm dlx next-vite-router init
27
- # or
28
- yarn dlx next-vite-router init
29
- # or
30
- bunx next-vite-router init
31
- ```
32
-
33
- The CLI will prompt you for:
34
-
35
- 1. **Project name** — name of the folder to create
36
- 2. **Template** — which React Vite variant to use:
37
- - `react-ts` (React + TypeScript)
38
- - `react-swc-ts` (React + SWC + TypeScript)
39
- - `react-compiler-ts` (React Compiler + TypeScript)
40
- 3. **Src folder** — whether to keep source files in `src/` (recommended) or place them at the project root
41
- 4. **Tailwind CSS** — whether to add and configure Tailwind CSS (with `@tailwindcss/vite`)
42
-
43
- ### What the CLI does
44
-
45
- The CLI automates the entire setup:
46
-
47
- - Scaffolds a new Vite + React project using `create-vite`
48
- - Installs `next-vite-router` and `react-router-dom@latest`
49
- - Configures the Vite plugin in `vite.config.ts`
50
- - Sets up path aliases (`@/*`) in `tsconfig.json` and Vite config
51
- - Creates TypeScript declarations for the virtual module (`virtual:next-vite-router`)
52
- - Generates example pages (`app/page.tsx`, `app/layout.tsx`, `app/example/page.tsx`)
53
- - Optionally configures Tailwind CSS with `@tailwindcss/vite` and creates `global.css`
54
-
55
- After the CLI finishes:
56
-
57
- ```next-vite-router/README.md#L11-12
58
- cd <your-project-name>
59
- pnpm dev
60
- ```
61
-
62
- Your app is ready with file-based routing!
63
-
64
- ## Quick Start — Vite plugin
65
-
66
- The recommended and easiest way to use this library in a Vite app is via the Vite plugin. The plugin exposes a virtual module `virtual:next-vite-router` that provides a `generateRoutes()` function and re-exports `useRoutes` from `react-router-dom` for convenience.
67
-
68
- Example `vite.config.ts` usage:
69
-
70
- ```next-vite-router/README.md#L13-18
71
- import { defineConfig } from 'vite';
72
- import react from '@vitejs/plugin-react';
73
- import { nextViteRouter } from 'next-vite-router';
74
-
75
- export default defineConfig({
76
- plugins: [
77
- react(),
78
- nextViteRouter({ pagesDir: 'src/pages' })
79
- ]
80
- });
81
- ```
82
-
83
- Example usage inside your app. Important: call `generateRoutes()` once at module level (not inside a component) to avoid infinite re-renders.
84
-
85
- File: `src/App.tsx`
86
-
87
- ```next-vite-router/README.md#L19-24
88
- import { useRoutes } from 'react-router-dom';
89
- import { generateRoutes } from 'virtual:next-vite-router';
90
-
91
- const routes = generateRoutes();
92
-
93
- export default function App() {
94
- const element = useRoutes(routes);
95
- return element;
96
- }
97
- ```
98
-
99
- File: `src/main.tsx`
100
-
101
- ```next-vite-router/README.md#L25-33
102
- import { StrictMode } from 'react';
103
- import { createRoot } from 'react-dom/client';
104
- import { BrowserRouter } from 'react-router-dom';
105
- import App from './App';
106
-
107
- createRoot(document.getElementById('root')!).render(
108
- <StrictMode>
109
- <BrowserRouter>
110
- <App />
111
- </BrowserRouter>
112
- </StrictMode>
113
- );
114
- ```
115
-
116
- Key points:
117
-
118
- - Call `generateRoutes()` once at module level, not inside the component (prevents infinite re-renders).
119
- - Wrap your app with `BrowserRouter` at the entry point (`main.tsx`), not inside `App`.
120
- - `pagesDir` (default: `src/app`) is the directory where you organize your route files (`page.tsx`, `layout.tsx`, `not-found.tsx`). It has nothing to do with your `App.tsx` component.
121
- - The virtual module is `virtual:next-vite-router` (the plugin resolves it internally).
122
-
123
- Example file structure:
124
-
125
- ```next-vite-router/README.md#L34-42
126
- src/
127
- ├── App.tsx ← Your main app component
128
- ├── main.tsx ← Entry point (with BrowserRouter)
129
- └── app/ ← pagesDir (routes are organized here)
130
- ├── page.tsx ← Home page
131
- ├── layout.tsx ← Root layout
132
- └── dashboard/
133
- ├── page.tsx ← /dashboard page
134
- └── layout.tsx ← Dashboard layout
135
- ```
136
-
137
- TypeScript note:
138
-
139
- If you're using TypeScript in a consuming project, add a declaration file so the virtual module is recognized by the compiler. Create a file such as `src/virtual.d.ts` (or any `*.d.ts` included by your `tsconfig.json`) with the following content:
140
-
141
- ```next-vite-router/README.md#L43-47
142
- declare module 'virtual:next-vite-router' {
143
- import type { RouteObject } from 'react-router-dom';
144
-
145
- export const generateRoutes: () => RouteObject[];
146
- export { useRoutes } from 'react-router-dom';
147
- }
148
- ```
149
-
150
- This ensures TypeScript knows the shape of the virtual module provided by the Vite plugin and avoids `Cannot find module 'virtual:next-vite-router'` errors in editors and builds.
151
-
152
- ## Programmatic route generation
153
-
154
- If you prefer to generate routes manually (for custom setups), the package exports helpers you can use with `import.meta.glob`.
155
-
156
- Example:
157
-
158
- ```next-vite-router/README.md#L48-54
159
- import { createRouteGenerator } from 'next-vite-router';
160
-
161
- // in a Vite environment
162
- const pages = import.meta.glob('../../pages/**/page.tsx');
163
- const layouts = import.meta.glob('../../pages/**/layout.tsx', { eager: true });
164
- const notFounds = import.meta.glob('../../pages/**/not-found.tsx');
165
-
166
- const generateRoutes = createRouteGenerator(pages, layouts, notFounds);
167
- const routes = generateRoutes();
168
- ```
169
-
170
- Use this when you need a fully custom glob layout or want to call the generator at runtime.
171
-
172
- ## Route folder conventions: grouping vs dynamic segments
173
-
174
- This library follows Next.js-style conventions for naming folders that affect routing. Two common patterns are:
175
-
176
- - Parentheses for grouping routes without affecting the URL (route groups).
177
- - Square brackets for dynamic route segments (dynamic params in the URL).
178
-
179
- Concrete examples:
180
-
181
- ```next-vite-router/README.md#L55-62
182
- - Structure:
183
- - `src/app/(components)/header/page.tsx`
184
- - `src/app/blog/[id]/page.tsx`
185
-
186
- - Behavior:
187
- - `header/page.tsx` will be included as a page, but the `(components)` folder will NOT appear in the generated URL. The page will be reachable at `/header` (or depending on nesting, at `/<parent>/header`).
188
- - `blog/[id]/page.tsx` will produce a route like `/blog/:id` where `:id` is a dynamic parameter.
189
- ```
190
-
191
- Notes and additional patterns:
192
-
193
- - Route groups (folders named with parentheses, e.g. `(admin)` or `(components)`) are useful to organize files, share layouts, or split code visually without adding path segments. Anything inside a group is treated as if the grouping folder does not exist in the URL.
194
- - Dynamic segments use square brackets (e.g. `[slug]`, `[id]`). They map to route parameters and are available to your components via React Router's `useParams()` hook.
195
- - Catch-all segments are commonly written as `[...slug]` and typically map to a wildcard or splat parameter. Optional catch-all segments are written `[[...slug]]` in Next.js — if you use or expect these conventions, confirm the behavior in your app (this library mirrors Next.js-style patterns, but check your generated routes when using more advanced patterns).
196
-
197
- Examples of expected mappings:
198
-
199
- ```next-vite-router/README.md#L63-74
200
- src/app/
201
- ├── blog/
202
- │ ├── page.tsx ← /blog
203
- │ └── [id]/page.tsx ← /blog/:id
204
- └── (components)/
205
- └── header/
206
- └── page.tsx ← /header (the `(components)` folder is not part of the path)
207
- ```
208
-
209
- If you ever need to verify the final route objects, call `generateRoutes()` (the virtual module) and inspect the returned `RouteObject[]` to confirm path strings and param names.
210
-
211
- ## API Reference
212
-
213
- Exports from the package (see `src/index.ts`):
214
-
215
- - `createRouteGenerator(pages, layouts, notFounds)` — returns a function that builds `RouteObject[]` using your globs.
216
- - `buildRouteTree` — builds the intermediate route tree from page/layout/not-found globs.
217
- - `treeToRoutes` — converts the route tree to React Router routes.
218
- - `registerMiddleware`, `clearMiddlewares` — middleware helpers to manage registered middlewares.
219
- - `configureRouter` — configure router-level options.
220
- - `nextViteRouter(options)` — Vite plugin factory. Options:
221
- - `pagesDir?: string` (default: `src/app`)
222
- - `pageFile?: string` (default: `page.tsx`)
223
- - `layoutFile?: string` (default: `layout.tsx`)
224
- - `notFoundFile?: string` (default: `not-found.tsx`)
225
- - Types exported: `RouterConfig`, `RouteNode`, `RouteMiddleware`, `NextViteRouterPluginOptions`.
226
-
227
- ### `configureRouter(config: RouterConfig)`
228
-
229
- Use `configureRouter` to set global router options for runtime behavior. The function merges the provided `config` with any existing configuration.
230
-
231
- Common `RouterConfig` fields:
232
-
233
- - `loadingFallback?: React.ComponentType` — component shown while lazy-loaded page components load.
234
- - `pagesDir?: string` — path used by the generator to resolve pages (useful if you need to override defaults at runtime).
235
- - `enableLazyLoading?: boolean` — when true, pages are wrapped to load lazily.
236
-
237
- Example:
238
-
239
- ```next-vite-router/README.md#L75-80
240
- import { configureRouter } from 'next-vite-router';
241
-
242
- configureRouter({
243
- loadingFallback: () => <div>Loading...</div>,
244
- enableLazyLoading: true,
245
- });
246
- ```
247
-
248
- Call `configureRouter` during app bootstrap before calling `generateRoutes()`.
249
-
250
- ### `registerMiddleware(pattern: RegExp, middleware: RouteMiddleware)`
251
-
252
- `registerMiddleware` lets you register middleware functions that will be applied to route elements whose path matches the provided `pattern` (a `RegExp`). Middlewares receive the route element (`ReactElement`) and should return a new element (for example wrapping it with providers, guards, or layout components).
253
-
254
- Important details:
255
-
256
- - Middlewares are stored in insertion order and applied sequentially.
257
- - To remove all registered middlewares, use `clearMiddlewares()`.
258
- - The internal helper `applyMiddlewares(path, element)` is used when constructing the final route element.
259
-
260
- Example:
261
-
262
- ```next-vite-router/README.md#L81-86
263
- import { registerMiddleware } from 'next-vite-router';
264
-
265
- registerMiddleware(/^\/dashboard/, (element) => (
266
- <AuthGuard>{element}</AuthGuard>
267
- ));
268
- ```
269
-
270
- ## Configuration options (plugin)
271
-
272
- The plugin takes an options object. Defaults are:
273
-
274
- ```next-vite-router/README.md#L87-92
275
- {
276
- pagesDir: 'src/app',
277
- pageFile: 'page.tsx',
278
- layoutFile: 'layout.tsx',
279
- notFoundFile: 'not-found.tsx',
280
- }
281
- ```
282
-
283
- Pass a custom `pagesDir` if your project uses `src/pages` or another convention.
284
-
285
- ## License
286
-
287
- MIT
1
+ # Next Vite Router
2
+
3
+ File-based routing for React + Vite.
4
+
5
+ Define routes with folders and let the package generate the
6
+ [React Router](https://reactrouter.com/) `RouteObject[]`. It does not replace
7
+ React Router, navigation, history, loaders, or application state.
8
+
9
+ ## Start here
10
+
11
+ ```bash
12
+ npm create vite@latest my-app -- --template react-ts
13
+ cd my-app
14
+ npm install next-vite-router react-router-dom
15
+ ```
16
+
17
+ Add the plugin to `vite.config.ts`:
18
+
19
+ ```ts
20
+ import { defineConfig } from "vite";
21
+ import react from "@vitejs/plugin-react";
22
+ import { nextViteRouter } from "next-vite-router/plugin";
23
+
24
+ export default defineConfig({
25
+ plugins: [react(), nextViteRouter()],
26
+ });
27
+ ```
28
+
29
+ Create your route tree:
30
+
31
+ ```text
32
+ src/app/
33
+ ├── page.tsx
34
+ ├── about/
35
+ │ └── page.tsx
36
+ └── users/
37
+ ├── page.tsx
38
+ └── [id]/
39
+ └── page.tsx
40
+ ```
41
+
42
+ This generates:
43
+
44
+ ```text
45
+ /
46
+ /about
47
+ /users
48
+ /users/:id
49
+ ```
50
+
51
+ Use the generated router inside a normal React Router provider:
52
+
53
+ ```tsx
54
+ // src/App.tsx
55
+ import { Router } from "virtual:next-vite-router";
56
+
57
+ export default function App() {
58
+ return <Router />;
59
+ }
60
+ ```
61
+
62
+ ```tsx
63
+ // src/main.tsx
64
+ import { StrictMode } from "react";
65
+ import { createRoot } from "react-dom/client";
66
+ import { BrowserRouter } from "react-router-dom";
67
+ import App from "./App";
68
+
69
+ createRoot(document.getElementById("root")!).render(
70
+ <StrictMode>
71
+ <BrowserRouter>
72
+ <App />
73
+ </BrowserRouter>
74
+ </StrictMode>,
75
+ );
76
+ ```
77
+
78
+ That is the complete setup. Run:
79
+
80
+ ```bash
81
+ npm run dev
82
+ ```
83
+
84
+ ## Use React Router directly
85
+
86
+ `Router` is only a convenience wrapper around
87
+ `useRoutes(generateRoutes())`. You can keep using React Router APIs:
88
+
89
+ ```tsx
90
+ import {
91
+ Link,
92
+ generateRoutes,
93
+ useRoutes,
94
+ } from "virtual:next-vite-router";
95
+ import { useNavigate, useParams } from "react-router-dom";
96
+
97
+ const routes = generateRoutes();
98
+
99
+ export default function App() {
100
+ const element = useRoutes(routes);
101
+ return (
102
+ <>
103
+ <Link to="/users">Users</Link>
104
+ {element}
105
+ </>
106
+ );
107
+ }
108
+ ```
109
+
110
+ Use `useNavigate`, `useParams`, loaders, actions, and other React Router APIs
111
+ from `react-router-dom`. The package intentionally does not create competing
112
+ navigation or data-loading APIs.
113
+
114
+ ## File conventions
115
+
116
+ | File or folder | Result |
117
+ | --- | --- |
118
+ | `page.tsx` | Route page |
119
+ | `layout.tsx` | Nested layout rendered with React Router's `Outlet` |
120
+ | `not-found.tsx` | `*` route for that subtree |
121
+ | `[id]` | Dynamic segment `:id` |
122
+ | `[...slug]` | Catch-all `*` segment |
123
+ | `[[...slug]]` | Optional catch-all `*` segment |
124
+ | `(admin)` | Route group omitted from the URL |
125
+
126
+ Layouts are the recommended way to add route-scoped navigation, providers, or
127
+ authorization:
128
+
129
+ ```text
130
+ src/app/
131
+ ├── layout.tsx
132
+ └── dashboard/
133
+ ├── layout.tsx
134
+ └── page.tsx
135
+ ```
136
+
137
+ ```tsx
138
+ import { Outlet } from "react-router-dom";
139
+
140
+ export default function DashboardLayout() {
141
+ return (
142
+ <AuthGuard>
143
+ <Outlet />
144
+ </AuthGuard>
145
+ );
146
+ }
147
+ ```
148
+
149
+ ## Type-safe paths
150
+
151
+ The Vite plugin generates `virtual-next-vite-router.d.ts` automatically:
152
+
153
+ ```tsx
154
+ import type {
155
+ RouteParams,
156
+ RoutePath,
157
+ } from "virtual:next-vite-router";
158
+
159
+ const path = "/users/:id" satisfies RoutePath;
160
+ const params: RouteParams<typeof path> = { id: "123" };
161
+ ```
162
+
163
+ Invalid paths and missing dynamic parameters are reported by TypeScript.
164
+ No manual declaration for the virtual module is required.
165
+
166
+ ## Lazy loading
167
+
168
+ Pages and `not-found.tsx` files use Vite's non-eager `import.meta.glob`, so
169
+ Vite can emit separate chunks for pages. Layouts are eager because they wrap
170
+ the generated route tree. Lazy loading is enabled by default:
171
+
172
+ ```tsx
173
+ import { configureRouter } from "next-vite-router";
174
+
175
+ configureRouter({
176
+ loadingFallback: () => <div>Loading route...</div>,
177
+ });
178
+ ```
179
+
180
+ `enableLazyLoading: false` is available for compatibility and testing. It does
181
+ not turn Vite's asynchronous imports into synchronous imports.
182
+
183
+ ## Optional middleware
184
+
185
+ Prefer layouts for route-scoped behavior. `registerMiddleware` is an optional
186
+ escape hatch for cross-cutting wrappers:
187
+
188
+ ```tsx
189
+ import { registerMiddleware } from "next-vite-router";
190
+
191
+ registerMiddleware(/^\/dashboard/, (element) => (
192
+ <AuthGuard>{element}</AuthGuard>
193
+ ));
194
+ ```
195
+
196
+ Middleware wraps generated route elements; it does not create routes or
197
+ replace React Router.
198
+
199
+ ## CLI
200
+
201
+ For an interactive setup:
202
+
203
+ ```bash
204
+ npx create-next-vite-router init
205
+ ```
206
+
207
+ The CLI creates a Vite React project, installs the package and
208
+ `react-router-dom@^7.0.0`, configures the plugin, and adds an example route
209
+ tree.
210
+
211
+ ## Compatibility
212
+
213
+ | Package | Supported versions |
214
+ | --- | --- |
215
+ | React / React DOM | `>=18 <20` |
216
+ | React Router DOM | `>=6 <8` |
217
+ | Vite | `>=5 <9` |
218
+
219
+ The compatibility workflow tests React 18 and 19 with React Router 6 and 7
220
+ across Vite 5, 6, 7, and 8.
221
+
222
+ ## Advanced API
223
+
224
+ For custom glob layouts, the package exports:
225
+
226
+ ```ts
227
+ import {
228
+ buildRouteTree,
229
+ createRouteGenerator,
230
+ treeToRoutes,
231
+ } from "next-vite-router";
232
+ ```
233
+
234
+ The normal pipeline is:
235
+
236
+ ```text
237
+ filesystem -> buildRouteTree() -> treeToRoutes() -> RouteObject[] -> React Router
238
+ ```
239
+
240
+ ## Examples
241
+
242
+ The repository includes a runnable showcase in [`examples/`](./examples/):
243
+
244
+ - `basic`
245
+ - `layouts`
246
+ - `dynamic-routes`
247
+ - `middleware`
248
+ - `typed-routes`
249
+
250
+ Run it from the examples directory:
251
+
252
+ ```bash
253
+ cd examples
254
+ pnpm install
255
+ pnpm dev
256
+ ```
257
+
258
+ The app opens a small navigation menu so each example can be tested
259
+ immediately. The `predev` script builds the local package automatically.
260
+
261
+ ## Development
262
+
263
+ From this repository:
264
+
265
+ ```bash
266
+ pnpm install
267
+ pnpm run build
268
+ pnpm run test:generator
269
+ pnpm run test:routes
270
+ pnpm run test:compatibility
271
+ pnpm exec tsc -p test/fixture/tsconfig.json --noEmit
272
+ pnpm run test:local
273
+ ```
274
+
275
+ The route matrix lives in [`test/routes.test.js`](./test/routes.test.js).
276
+
277
+ ## License
278
+
279
+ MIT
package/dist/cli.cjs CHANGED
@@ -47,7 +47,7 @@ function detectPackageManager() {
47
47
  case "pnpm":
48
48
  return {
49
49
  name: "pnpm",
50
- createProjectCommand: (projectName, template) => `pnpm create vite@latest ${projectName} --template ${template} --no-rolldown`,
50
+ createProjectCommand: (projectName, template) => `pnpm create vite@latest ${projectName} --template ${template} --no-interactive`,
51
51
  addDepsCommand: (deps) => `pnpm add ${deps.join(" ")}`,
52
52
  addDevDepsCommand: (deps) => `pnpm add -D ${deps.join(" ")}`,
53
53
  installCommand: "pnpm install"
@@ -55,7 +55,7 @@ function detectPackageManager() {
55
55
  case "bun":
56
56
  return {
57
57
  name: "bun",
58
- createProjectCommand: (projectName, template) => `bun create vite ${projectName} --template=${template} --no-rolldown`,
58
+ createProjectCommand: (projectName, template) => `bun create vite ${projectName} --template=${template} --no-interactive`,
59
59
  addDepsCommand: (deps) => `bun add ${deps.join(" ")}`,
60
60
  addDevDepsCommand: (deps) => `bun add -d ${deps.join(" ")}`,
61
61
  installCommand: "bun install"
@@ -63,7 +63,7 @@ function detectPackageManager() {
63
63
  case "yarn":
64
64
  return {
65
65
  name: "yarn",
66
- createProjectCommand: (projectName, template) => `yarn create vite ${projectName} --template=${template} --no-rolldown`,
66
+ createProjectCommand: (projectName, template) => `yarn create vite ${projectName} --template=${template} --no-interactive`,
67
67
  addDepsCommand: (deps) => `yarn add ${deps.join(" ")}`,
68
68
  addDevDepsCommand: (deps) => `yarn add -D ${deps.join(" ")}`,
69
69
  installCommand: "yarn install"
@@ -72,7 +72,7 @@ function detectPackageManager() {
72
72
  default:
73
73
  return {
74
74
  name: "npm",
75
- createProjectCommand: (projectName, template) => `npm create vite@latest ${projectName} -- --template ${template} --no-rolldown`,
75
+ createProjectCommand: (projectName, template) => `npm create vite@latest ${projectName} -- --template ${template} --no-interactive`,
76
76
  addDepsCommand: (deps) => `npm install ${deps.join(" ")}`,
77
77
  addDevDepsCommand: (deps) => `npm install -D ${deps.join(" ")}`,
78
78
  installCommand: "npm install"
@@ -126,10 +126,17 @@ const logger = {
126
126
  }
127
127
  };
128
128
  function getViteConfigPath(projectDir) {
129
- const tsPath = path.join(projectDir, "vite.config.ts");
130
- const jsPath = path.join(projectDir, "vite.config.js");
131
- if (fs.existsSync(tsPath)) return tsPath;
132
- if (fs.existsSync(jsPath)) return jsPath;
129
+ for (const fileName of [
130
+ "vite.config.ts",
131
+ "vite.config.mts",
132
+ "vite.config.cts",
133
+ "vite.config.js",
134
+ "vite.config.mjs",
135
+ "vite.config.cjs"
136
+ ]) {
137
+ const configPath = path.join(projectDir, fileName);
138
+ if (fs.existsSync(configPath)) return configPath;
139
+ }
133
140
  return null;
134
141
  }
135
142
  function readViteConfig(projectDir) {
@@ -200,10 +207,18 @@ function createVirtualDTS(projectDir, useSrc) {
200
207
  }
201
208
  const filePath = path.join(targetDir, "virtual-next-vite-router.d.ts");
202
209
  const content = `declare module 'virtual:next-vite-router' {
210
+ import type { ReactElement } from 'react';
203
211
  import type { RouteObject } from 'react-router-dom';
204
212
 
213
+ export type RoutePath = string;
214
+ export type RoutePathParams = Record<string, Record<string, string>>;
215
+ export type RouteParams<P extends RoutePath> = P extends keyof RoutePathParams
216
+ ? RoutePathParams[P]
217
+ : never;
218
+
205
219
  export const generateRoutes: () => RouteObject[];
206
- export { useRoutes } from 'react-router-dom';
220
+ export function Router(): ReactElement | null;
221
+ export { Link, useRoutes } from 'react-router-dom';
207
222
  }
208
223
  `;
209
224
  fs.writeFileSync(filePath, content, "utf-8");
@@ -217,19 +232,12 @@ function createMainTSX(projectDir, useSrc) {
217
232
  const content = `import { StrictMode } from 'react';
218
233
  import { createRoot } from 'react-dom/client';
219
234
  import { BrowserRouter } from "react-router-dom";
220
- import { generateRoutes, useRoutes } from 'virtual:next-vite-router';
221
-
222
- const routes = generateRoutes();
223
-
224
- export default function App() {
225
- const element = useRoutes(routes);
226
- return element;
227
- }
235
+ import { Router } from 'virtual:next-vite-router';
228
236
 
229
237
  createRoot(document.getElementById('root')!).render(
230
238
  <StrictMode>
231
239
  <BrowserRouter>
232
- <App />
240
+ <Router />
233
241
  </BrowserRouter>
234
242
  </StrictMode>,
235
243
  );
@@ -455,12 +463,11 @@ const init = new commander.Command().name("init").description(
455
463
  name: "template",
456
464
  message: "Select a template:",
457
465
  choices: [
458
- { title: "React + TypeScript", value: "react-ts" },
466
+ { title: "React + TypeScript (Recommended)", value: "react-ts" },
459
467
  {
460
468
  title: "React + TypeScript (React Compiler)",
461
469
  value: "react-compiler-ts"
462
- },
463
- { title: "React + SWC (Recommended)", value: "react-swc-ts" }
470
+ }
464
471
  ],
465
472
  initial: 0
466
473
  },
@@ -504,13 +511,10 @@ const init = new commander.Command().name("init").description(
504
511
  const useTailwind = tailwindResponse.useTailwind;
505
512
  const createCommand = pm.createProjectCommand(projectName, variant);
506
513
  logger.info(`Creating project "${projectName}" using ${pm.name}...`);
507
- try {
508
- child_process.execSync(createCommand, {
509
- stdio: ["pipe", "inherit", "inherit"],
510
- input: "n\n"
511
- });
512
- } catch (error) {
513
- }
514
+ child_process.execSync(createCommand, {
515
+ stdio: ["pipe", "inherit", "inherit"],
516
+ input: "n\n"
517
+ });
514
518
  logger.info("Installing dependencies...");
515
519
  child_process.execSync(pm.installCommand, { stdio: "inherit", cwd: projectName });
516
520
  logger.info(
@@ -518,7 +522,7 @@ const init = new commander.Command().name("init").description(
518
522
  );
519
523
  const addDepsCmd = pm.addDepsCommand([
520
524
  "next-vite-router",
521
- "react-router-dom@latest"
525
+ "react-router-dom@^7.0.0"
522
526
  ]);
523
527
  child_process.execSync(addDepsCmd, { stdio: "inherit", cwd: projectName });
524
528
  logger.info("Configuring next-vite-router in the project...");
@@ -531,7 +535,7 @@ const init = new commander.Command().name("init").description(
531
535
  Project "${projectName}" is ready!
532
536
  `);
533
537
  });
534
- const version = "1.0.8";
538
+ const version = "1.1.1";
535
539
  const pkg = {
536
540
  version
537
541
  };
package/dist/index.js CHANGED
@@ -1,86 +1,84 @@
1
- import i, { Suspense as g, lazy as z } from "react";
2
- import { nextViteRouter as D } from "./plugin.js";
3
- function f(e) {
4
- return /^\([^)]+\)$/.test(e) ? "" : /^\[\.\.\.(.+)\]$/.test(e) ? "*" : /^\[([^\]]+)\]$/.test(e) ? e.replace(/^\[([^\]]+)\]$/, ":$1") : e;
5
- }
6
- function F(e) {
7
- return e.replace("../../app/", "").replace(/\/?(page|layout|not-found)\.tsx$/, "");
8
- }
9
- let m = {};
10
- function k(e) {
11
- m = { ...m, ...e };
1
+ import { e as C, n as f } from "./index.plugin--ecyiAem.js";
2
+ import { a as G } from "./index.plugin--ecyiAem.js";
3
+ import i, { Suspense as y, lazy as F } from "react";
4
+ const L = {
5
+ enableLazyLoading: !0
6
+ };
7
+ let d = { ...L };
8
+ function x(e) {
9
+ d = { ...d, ...e };
12
10
  }
13
- function C() {
14
- return m;
11
+ function $() {
12
+ return d;
15
13
  }
16
- let d = [];
17
- function S(e, a) {
18
- d.push({ pattern: e, middleware: a });
14
+ let E = [];
15
+ function T(e, r) {
16
+ E.push({ pattern: e, middleware: r });
19
17
  }
20
- function T() {
21
- d = [];
18
+ function j() {
19
+ E = [];
22
20
  }
23
- function E(e, a) {
24
- let r = a;
25
- for (const n of d)
26
- n.pattern.test(e) && (r = n.middleware(r));
27
- return r;
21
+ function z(e, r) {
22
+ let n = r;
23
+ for (const t of E)
24
+ t.pattern.test(e) && (n = t.middleware(n));
25
+ return n;
28
26
  }
29
- const x = () => i.createElement("div"), y = /* @__PURE__ */ new Map();
30
- function R(e, a, r) {
31
- const n = C();
32
- if (n.enableLazyLoading === !1) {
33
- const u = i.lazy(e), c = i.createElement(
34
- g,
27
+ const M = () => i.createElement("div"), b = /* @__PURE__ */ new Map();
28
+ function R(e, r, n) {
29
+ const t = $();
30
+ if (t.enableLazyLoading === !1) {
31
+ const c = i.lazy(e), u = i.createElement(
32
+ y,
35
33
  { fallback: null },
36
34
  // No loading fallback when disabled
37
- i.createElement(u)
35
+ i.createElement(c)
38
36
  );
39
- return r ? E(r, c) : c;
37
+ return n ? z(n, u) : u;
40
38
  }
41
- let t = y.get(a);
42
- t || (t = z(e), y.set(a, t));
43
- const l = n.loadingFallback || x, o = i.createElement(
44
- g,
45
- { fallback: i.createElement(l) },
46
- i.createElement(t)
39
+ let l = b.get(r);
40
+ l || (l = F(e), b.set(r, l));
41
+ const o = t.loadingFallback || M, a = i.createElement(
42
+ y,
43
+ { fallback: i.createElement(o) },
44
+ i.createElement(l)
47
45
  );
48
- return r ? E(r, o) : o;
46
+ return n ? z(n, a) : a;
49
47
  }
50
- function p(e, a, r, n) {
51
- const t = F(a), l = t ? t.split("/") : [];
52
- let o = e, u = "";
53
- l.forEach((c, w) => {
54
- u = u ? `${u}/${c}` : c, o.children.has(c) || o.children.set(c, {
55
- segment: c,
56
- fullPath: u,
48
+ function p(e, r, n, t) {
49
+ const l = C(r), o = l ? l.split("/") : [];
50
+ let a = e, c = "";
51
+ o.forEach((u, h) => {
52
+ c = c ? `${c}/${u}` : u, a.children.has(u) || a.children.set(u, {
53
+ segment: u,
54
+ fullPath: c,
57
55
  children: /* @__PURE__ */ new Map()
58
- }), o = o.children.get(c), w === l.length - 1 && (o[n] = r);
59
- }), l.length === 0 && (e[n] = r);
56
+ }), a = a.children.get(u), h === o.length - 1 && (a[t] = n);
57
+ }), o.length === 0 && (e[t] = n);
60
58
  }
61
- function L(e, a, r) {
62
- const n = {
59
+ function S(e, r, n) {
60
+ const t = {
63
61
  segment: "",
64
62
  fullPath: "",
65
63
  children: /* @__PURE__ */ new Map()
66
64
  };
67
- return Object.entries(a).forEach(([t, l]) => {
68
- p(n, t, l.default, "layout");
69
- }), Object.entries(e).forEach(([t, l]) => {
70
- p(n, t, l, "page");
71
- }), Object.entries(r).forEach(([t, l]) => {
72
- p(n, t, l, "notFound");
73
- }), n;
65
+ return Object.entries(r).forEach(([l, o]) => {
66
+ p(t, l, o.default, "layout");
67
+ }), Object.entries(e).forEach(([l, o]) => {
68
+ p(t, l, o, "page");
69
+ }), Object.entries(n).forEach(([l, o]) => {
70
+ p(t, l, o, "notFound");
71
+ }), t;
74
72
  }
75
- function h(e, a, r = !1) {
76
- const n = R(
73
+ function m(e, r, n = !1) {
74
+ const t = R(
77
75
  async () => ({ default: (await e.page()).default }),
78
- `page:${e.fullPath || a}`,
79
- a
76
+ `page:${e.fullPath || r}`,
77
+ r
80
78
  );
81
- return r ? { index: !0, element: n } : { path: f(e.segment), element: n };
79
+ return n ? { index: !0, element: t } : { path: f(e.segment), element: t };
82
80
  }
83
- function $(e) {
81
+ function g(e) {
84
82
  return {
85
83
  path: "*",
86
84
  element: R(
@@ -89,62 +87,70 @@ function $(e) {
89
87
  )
90
88
  };
91
89
  }
92
- function s(e, a) {
93
- const r = [];
94
- return e.children.forEach((n, t) => {
95
- const l = f(t), o = l ? a ? `${a}/${l}` : `/${l}` : a;
96
- r.push(...b(n, !1, o));
97
- }), r;
98
- }
99
- function b(e, a = !1, r = "") {
90
+ function s(e, r) {
100
91
  const n = [];
101
- if (a && e.layout) {
102
- const t = [];
103
- return e.page && t.push(h(e, "/", !0)), t.push(...s(e, "")), e.notFound && t.push($(e)), n.push({
92
+ return [...e.children.entries()].sort(([t], [l]) => t.localeCompare(l)).forEach(([t, l]) => {
93
+ const o = f(t), a = o ? r ? `${r}/${o}` : `/${o}` : r;
94
+ n.push(...w(l, !1, a));
95
+ }), n;
96
+ }
97
+ function w(e, r = !1, n = "") {
98
+ const t = [];
99
+ if (r && e.layout) {
100
+ const a = [];
101
+ return e.page && a.push(m(e, "/", !0)), a.push(...s(e, "")), e.notFound && a.push(g(e)), t.push({
104
102
  path: "/",
105
103
  element: i.createElement(e.layout),
106
- children: t
107
- }), n;
104
+ children: a
105
+ }), t;
108
106
  }
109
107
  if (e.layout) {
110
- const t = f(e.segment);
111
- if (!t)
112
- return s(e, r);
113
- const l = r + "/" + t, o = [];
114
- return e.page && o.push(h(e, l, !0)), o.push(...s(e, l)), e.notFound && o.push($(e)), n.push({
115
- path: t,
108
+ const a = f(e.segment);
109
+ if (!a)
110
+ return s(e, n);
111
+ const c = n + "/" + a, u = [];
112
+ return e.page && u.push(m(e, c, !0)), u.push(...s(e, c)), e.notFound && u.push(g(e)), t.push({
113
+ path: a,
116
114
  element: i.createElement(e.layout),
117
- children: o.length > 0 ? o : void 0
118
- }), n;
115
+ children: u.length > 0 ? u : void 0
116
+ }), t;
119
117
  }
120
118
  if (e.page) {
121
- const t = f(e.segment);
122
- if (!t)
123
- return s(e, r);
124
- const l = r + "/" + t, o = s(e, l), u = h(e, l, !1);
125
- return n.push({
126
- path: u.path,
127
- element: u.element,
128
- children: o.length > 0 ? o : void 0
129
- }), n;
119
+ const a = f(e.segment);
120
+ if (!a)
121
+ return r ? (t.push({
122
+ path: "/",
123
+ element: m(e, "/", !0).element,
124
+ children: s(e, n).length > 0 ? s(e, n) : void 0
125
+ }), e.notFound && t.push(g(e)), t) : s(e, n);
126
+ const c = n + "/" + a, u = s(e, c), h = m(e, c, !1);
127
+ return t.push({
128
+ path: h.path,
129
+ element: h.element,
130
+ children: u.length > 0 ? u : void 0
131
+ }), t;
130
132
  }
131
- return s(e, r);
133
+ const l = f(e.segment), o = s(e, n);
134
+ return l ? [{
135
+ path: l,
136
+ children: o.length > 0 ? o : void 0
137
+ }] : o;
132
138
  }
133
- function j(e, a, r) {
139
+ function O(e, r, n) {
134
140
  return () => {
135
- const n = L(e, a, r);
136
- return b(n, !0);
141
+ const t = S(e, r, n);
142
+ return w(t, !0);
137
143
  };
138
144
  }
139
145
  export {
140
- L as buildRouteTree,
141
- T as clearMiddlewares,
142
- k as configureRouter,
146
+ S as buildRouteTree,
147
+ j as clearMiddlewares,
148
+ x as configureRouter,
143
149
  R as createLazyElement,
144
- j as createRouteGenerator,
145
- F as extractRoutePath,
146
- D as nextViteRouter,
150
+ O as createRouteGenerator,
151
+ C as extractRoutePath,
152
+ G as nextViteRouter,
147
153
  f as normalizeSegment,
148
- S as registerMiddleware,
149
- b as treeToRoutes
154
+ T as registerMiddleware,
155
+ w as treeToRoutes
150
156
  };
@@ -0,0 +1,132 @@
1
+ import p from "node:fs";
2
+ import i from "node:path";
3
+ function y(e) {
4
+ return /^\([^)]+\)$/.test(e) ? "" : /^\[\[\.\.\.(.+)\]\]$/.test(e) || /^\[\.\.\.(.+)\]$/.test(e) ? "*" : /^\[([^\]]+)\]$/.test(e) ? e.replace(/^\[([^\]]+)\]$/, ":$1") : e;
5
+ }
6
+ function b(e) {
7
+ return e.replace(/\\/g, "/").replace("../../app/", "").replace(/\/?(page|layout|not-found)\.tsx$/, "");
8
+ }
9
+ function R(e) {
10
+ const t = e.split(i.sep).filter(Boolean).map(y).filter(Boolean);
11
+ return t.length > 0 ? `/${t.join("/")}` : "/";
12
+ }
13
+ function h(e, t) {
14
+ const s = /* @__PURE__ */ new Set();
15
+ function n(a) {
16
+ for (const u of p.readdirSync(a, { withFileTypes: !0 })) {
17
+ const r = i.join(a, u.name);
18
+ u.isDirectory() ? n(r) : u.isFile() && u.name === t && s.add(R(i.relative(e, a)));
19
+ }
20
+ }
21
+ return p.existsSync(e) && n(e), [...s].sort();
22
+ }
23
+ function x(e) {
24
+ const t = e.match(/^\/(?:\[\.\.\.|:)([^/\]]+)\]?$/);
25
+ return t == null ? void 0 : t[1];
26
+ }
27
+ function $(e) {
28
+ const t = e.map((n) => ` | "${n}"`).join(`
29
+ `), s = e.map((n) => {
30
+ const a = n.split("/").map((r) => x(`/${r}`)).filter((r) => !!r), u = a.length > 0 ? a.map((r) => ` ${r}: string;`).join(`
31
+ `) : " [key: string]: never;";
32
+ return ` "${n}": {
33
+ ${u}
34
+ };`;
35
+ }).join(`
36
+ `);
37
+ return `declare module 'virtual:next-vite-router' {
38
+ import type { ReactElement } from 'react';
39
+ import type { RouteObject } from 'react-router-dom';
40
+
41
+ export type RoutePath =
42
+ ${t || ' | "/"'};
43
+
44
+ export type RouteParams<P extends RoutePath> = P extends keyof RoutePathParams
45
+ ? RoutePathParams[P]
46
+ : never;
47
+
48
+ export interface RoutePathParams {
49
+ ${s}
50
+ }
51
+
52
+ export const generateRoutes: () => RouteObject[];
53
+ export function Router(): ReactElement | null;
54
+ export { Link, useRoutes } from 'react-router-dom';
55
+ }
56
+ `;
57
+ }
58
+ function g(e = {}) {
59
+ const {
60
+ pagesDir: t = "src/app",
61
+ pageFile: s = "page.tsx",
62
+ layoutFile: n = "layout.tsx",
63
+ notFoundFile: a = "not-found.tsx"
64
+ } = e, u = "virtual:next-vite-router", r = "\0" + u;
65
+ let l = "", m = "";
66
+ function f() {
67
+ const o = $(h(l, s));
68
+ p.writeFileSync(m, o, "utf-8");
69
+ }
70
+ return {
71
+ name: "next-vite-router",
72
+ configResolved(o) {
73
+ l = i.resolve(o.root, t), m = i.join(i.dirname(l), "virtual-next-vite-router.d.ts"), f();
74
+ },
75
+ resolveId(o) {
76
+ if (o === u)
77
+ return r;
78
+ },
79
+ load(o) {
80
+ if (o === r)
81
+ return `
82
+ import { buildRouteTree, treeToRoutes, normalizeSegment, createLazyElement } from 'next-vite-router';
83
+ import { Link, useRoutes } from 'react-router-dom';
84
+
85
+ const pagesGlob = import.meta.glob('/${t}/**/${s}');
86
+ const layoutsGlob = import.meta.glob('/${t}/**/${n}', { eager: true });
87
+ const notFoundsGlob = import.meta.glob('/${t}/**/${a}');
88
+
89
+ const transformPath = (path) => path.replace('/${t}/', '');
90
+
91
+ const pages = Object.fromEntries(
92
+ Object.entries(pagesGlob).map(([key, value]) => [transformPath(key), value])
93
+ );
94
+ const layouts = Object.fromEntries(
95
+ Object.entries(layoutsGlob).map(([key, value]) => [transformPath(key), value])
96
+ );
97
+ const notFounds = Object.fromEntries(
98
+ Object.entries(notFoundsGlob).map(([key, value]) => [transformPath(key), value])
99
+ );
100
+
101
+ export function generateRoutes() {
102
+ const tree = buildRouteTree(pages, layouts, notFounds);
103
+ const routes = treeToRoutes(tree, true);
104
+ return routes;
105
+ }
106
+
107
+ export function Router() {
108
+ return useRoutes(generateRoutes());
109
+ }
110
+
111
+ export { Link, useRoutes };
112
+ `;
113
+ },
114
+ handleHotUpdate({ file: o, server: c }) {
115
+ if (o.includes(t) && (o.endsWith(s) || o.endsWith(n) || o.endsWith(a))) {
116
+ f();
117
+ const d = c.moduleGraph.getModuleById(
118
+ r
119
+ );
120
+ d && (c.moduleGraph.invalidateModule(d), c.ws.send({
121
+ type: "full-reload",
122
+ path: "*"
123
+ }));
124
+ }
125
+ }
126
+ };
127
+ }
128
+ export {
129
+ g as a,
130
+ b as e,
131
+ y as n
132
+ };
package/dist/plugin.js CHANGED
@@ -1,59 +1,6 @@
1
- function d(i = {}) {
2
- const {
3
- pagesDir: t = "src/app",
4
- pageFile: n = "page.tsx",
5
- layoutFile: a = "layout.tsx",
6
- notFoundFile: u = "not-found.tsx"
7
- } = i, s = "virtual:next-vite-router", o = "\0" + s;
8
- return {
9
- name: "next-vite-router",
10
- resolveId(e) {
11
- if (e === s)
12
- return o;
13
- },
14
- load(e) {
15
- if (e === o)
16
- return `
17
- import { buildRouteTree, treeToRoutes, normalizeSegment, createLazyElement } from 'next-vite-router';
18
-
19
- const pagesGlob = import.meta.glob('/${t}/**/${n}');
20
- const layoutsGlob = import.meta.glob('/${t}/**/${a}', { eager: true });
21
- const notFoundsGlob = import.meta.glob('/${t}/**/${u}');
22
-
23
- const transformPath = (path) => path.replace('/${t}/', '');
24
-
25
- const pages = Object.fromEntries(
26
- Object.entries(pagesGlob).map(([key, value]) => [transformPath(key), value])
27
- );
28
- const layouts = Object.fromEntries(
29
- Object.entries(layoutsGlob).map(([key, value]) => [transformPath(key), value])
30
- );
31
- const notFounds = Object.fromEntries(
32
- Object.entries(notFoundsGlob).map(([key, value]) => [transformPath(key), value])
33
- );
34
-
35
- export function generateRoutes() {
36
- const tree = buildRouteTree(pages, layouts, notFounds);
37
- const routes = treeToRoutes(tree, true);
38
- return routes;
39
- }
40
-
41
- export { useRoutes } from 'react-router-dom';
42
- `;
43
- },
44
- handleHotUpdate({ file: e, server: r }) {
45
- if (e.includes(t) && (e.endsWith(n) || e.endsWith(a) || e.endsWith(u))) {
46
- const l = r.moduleGraph.getModuleById(
47
- o
48
- );
49
- l && (r.moduleGraph.invalidateModule(l), r.ws.send({
50
- type: "full-reload",
51
- path: "*"
52
- }));
53
- }
54
- }
55
- };
56
- }
1
+ import "node:fs";
2
+ import "node:path";
3
+ import { a as i } from "./index.plugin--ecyiAem.js";
57
4
  export {
58
- d as nextViteRouter
5
+ i as nextViteRouter
59
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-vite-router",
3
- "version": "1.0.8",
3
+ "version": "1.1.1",
4
4
  "description": "Next.js-like file-based routing for React + Vite",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -40,10 +40,10 @@
40
40
  "homepage": "https://github.com/Rlzee/next-vite-router#readme",
41
41
  "license": "MIT",
42
42
  "peerDependencies": {
43
- "react": "^18.0.0",
44
- "react-dom": "^18.0.0",
45
- "react-router-dom": "^6.0.0",
46
- "vite": "^5.0.0"
43
+ "react": ">=18 <20",
44
+ "react-dom": ">=18 <20",
45
+ "react-router-dom": ">=6 <8",
46
+ "vite": ">=5 <9"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/node": "^24.10.1",
@@ -63,6 +63,9 @@
63
63
  "dev": "vite build --watch",
64
64
  "build": "vite build && vite build -c vite.config.cli.ts",
65
65
  "test:local": "node test/test-package.js",
66
+ "test:routes": "node test/test-route-types.js",
67
+ "test:generator": "node --test test/routes.test.js",
68
+ "test:compatibility": "node test/test-compatibility.js",
66
69
  "preview": "pnpm pack --dry-run",
67
70
  "cli": "cd test && node ../dist/cli.cjs"
68
71
  }