@voltx/server 0.4.6 → 0.4.7
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 +113 -32
- package/dist/index.cjs +278 -39
- package/dist/index.d.cts +2 -8
- package/dist/index.d.ts +2 -8
- package/dist/index.js +278 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<strong>@voltx/server</strong><br/>
|
|
3
|
-
<em>Hono-based HTTP server with file-based routing, SSR, and
|
|
3
|
+
<em>Hono-based HTTP server with file-based routing, React SSR, and Vite plugins</em>
|
|
4
4
|
</p>
|
|
5
5
|
|
|
6
6
|
<p align="center">
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
The HTTP layer of the [VoltX](https://github.com/codewithshail/voltx) framework. Built on [Hono](https://hono.dev) with file-based routing, React SSR (streaming), CORS, logging, error handling, and static file serving.
|
|
14
|
+
The HTTP layer of the [VoltX](https://github.com/codewithshail/voltx) framework. Built on [Hono](https://hono.dev) with file-based page routing, file-based API routing, React SSR (streaming), CORS, logging, error handling, and static file serving.
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
@@ -22,24 +22,125 @@ npm install @voltx/server
|
|
|
22
22
|
## Quick Start
|
|
23
23
|
|
|
24
24
|
```ts
|
|
25
|
+
// server.ts
|
|
25
26
|
import { Hono } from "hono";
|
|
26
27
|
import { registerSSR } from "@voltx/server";
|
|
28
|
+
import { registerRoutes } from "voltx/api";
|
|
27
29
|
|
|
28
30
|
const app = new Hono();
|
|
29
31
|
|
|
30
|
-
// API routes
|
|
31
|
-
app
|
|
32
|
+
// Auto-discover and mount all API routes from api/ directory
|
|
33
|
+
registerRoutes(app);
|
|
32
34
|
|
|
33
35
|
// SSR — renders React on the server, hydrates on the client
|
|
34
36
|
registerSSR(app, null, {
|
|
35
37
|
title: "My App",
|
|
36
38
|
entryServer: "src/entry-server.tsx",
|
|
37
39
|
entryClient: "src/entry-client.tsx",
|
|
40
|
+
css: "src/globals.css",
|
|
38
41
|
});
|
|
39
42
|
|
|
40
43
|
export default app;
|
|
41
44
|
```
|
|
42
45
|
|
|
46
|
+
```ts
|
|
47
|
+
// vite.config.ts
|
|
48
|
+
import { defineConfig } from "vite";
|
|
49
|
+
import devServer from "@hono/vite-dev-server";
|
|
50
|
+
import react from "@vitejs/plugin-react";
|
|
51
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
52
|
+
import { voltxRouter, voltxAPI } from "@voltx/server";
|
|
53
|
+
|
|
54
|
+
export default defineConfig({
|
|
55
|
+
plugins: [
|
|
56
|
+
react(),
|
|
57
|
+
tailwindcss(),
|
|
58
|
+
voltxRouter(), // File-based page routing → voltx/router
|
|
59
|
+
voltxAPI(), // File-based API routing → voltx/api
|
|
60
|
+
devServer({ entry: "server.ts" }),
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Vite Plugins
|
|
66
|
+
|
|
67
|
+
### `voltxRouter()` — File-Based Page Routing
|
|
68
|
+
|
|
69
|
+
Auto-discovers React components in `src/pages/` and provides the `voltx/router` virtual module.
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
src/pages/index.tsx → /
|
|
73
|
+
src/pages/about.tsx → /about
|
|
74
|
+
src/pages/blog/index.tsx → /blog
|
|
75
|
+
src/pages/blog/[slug].tsx → /blog/:slug (dynamic route)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
// Use in entry-client.tsx and entry-server.tsx
|
|
80
|
+
import { VoltxRoutes, Link, useNavigate, useParams } from "voltx/router";
|
|
81
|
+
|
|
82
|
+
// VoltxRoutes renders the matched page component
|
|
83
|
+
// Link, NavLink, useNavigate, useParams, useLocation, useSearchParams
|
|
84
|
+
// are re-exported from react-router — no separate install needed
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### Options
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
voltxRouter({
|
|
91
|
+
pagesDir: "src/pages", // default: "src/pages"
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### `voltxAPI()` — File-Based API Routing
|
|
96
|
+
|
|
97
|
+
Auto-discovers API route files in `api/` and provides the `voltx/api` virtual module.
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
api/index.ts → /api
|
|
101
|
+
api/users.ts → /api/users
|
|
102
|
+
api/users/[id].ts → /api/users/:id
|
|
103
|
+
api/[...slug].ts → /api/* (catch-all)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Each file exports named HTTP method handlers:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
// api/users.ts
|
|
110
|
+
import type { Context } from "@voltx/server";
|
|
111
|
+
|
|
112
|
+
export function GET(c: Context) {
|
|
113
|
+
return c.json([{ id: 1, name: "Alice" }]);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function POST(c: Context) {
|
|
117
|
+
const body = await c.req.json();
|
|
118
|
+
return c.json({ created: true }, 201);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Supported methods: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`.
|
|
123
|
+
|
|
124
|
+
Routes are sorted automatically: static first, dynamic (`:param`) second, catch-all (`*`) last. HMR support means new files are picked up instantly in dev mode.
|
|
125
|
+
|
|
126
|
+
#### Options
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
voltxAPI({
|
|
130
|
+
apiDir: "api", // default: "api"
|
|
131
|
+
})
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
#### Usage in server.ts
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { registerRoutes } from "voltx/api";
|
|
138
|
+
|
|
139
|
+
// Mounts all discovered API handlers on the Hono app
|
|
140
|
+
const registered = registerRoutes(app);
|
|
141
|
+
// → [{ method: "GET", path: "/api" }, { method: "POST", path: "/api/users" }, ...]
|
|
142
|
+
```
|
|
143
|
+
|
|
43
144
|
## Server-Side Rendering
|
|
44
145
|
|
|
45
146
|
`registerSSR()` provides streaming React SSR with zero config:
|
|
@@ -47,6 +148,7 @@ export default app;
|
|
|
47
148
|
- **Dev mode** — works with `@hono/vite-dev-server` for HMR
|
|
48
149
|
- **Production** — reads the Vite client manifest for hashed asset paths, serves pre-built SSR bundle
|
|
49
150
|
- **Streaming** — uses `renderToReadableStream` for fast TTFB
|
|
151
|
+
- **CSS injection** — injects your global CSS in dev mode to prevent FOUC
|
|
50
152
|
- **Public env** — injects `window.__VOLTX_ENV__` for `VITE_*` variables
|
|
51
153
|
|
|
52
154
|
```ts
|
|
@@ -56,6 +158,7 @@ registerSSR(app, viteInstance, {
|
|
|
56
158
|
title: "My App",
|
|
57
159
|
entryServer: "src/entry-server.tsx",
|
|
58
160
|
entryClient: "src/entry-client.tsx",
|
|
161
|
+
css: "src/globals.css",
|
|
59
162
|
});
|
|
60
163
|
```
|
|
61
164
|
|
|
@@ -64,38 +167,16 @@ registerSSR(app, viteInstance, {
|
|
|
64
167
|
| `title` | `string` | HTML `<title>` |
|
|
65
168
|
| `entryServer` | `string` | Path to SSR entry (exports `render()`) |
|
|
66
169
|
| `entryClient` | `string` | Path to client hydration entry |
|
|
67
|
-
|
|
68
|
-
## File-Based Routing
|
|
69
|
-
|
|
70
|
-
Drop files in `api/` and they become API endpoints:
|
|
71
|
-
|
|
72
|
-
```
|
|
73
|
-
api/index.ts → GET /api
|
|
74
|
-
api/chat.ts → POST /api/chat
|
|
75
|
-
api/users/[id].ts → GET /api/users/:id
|
|
76
|
-
api/rag/query.ts → POST /api/rag/query
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
Each file exports HTTP method handlers:
|
|
80
|
-
|
|
81
|
-
```ts
|
|
82
|
-
import type { Context } from "@voltx/server";
|
|
83
|
-
|
|
84
|
-
export async function POST(c: Context) {
|
|
85
|
-
const body = await c.req.json();
|
|
86
|
-
return c.json({ message: "Hello!" });
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function GET(c: Context) {
|
|
90
|
-
return c.json({ status: "ok" });
|
|
91
|
-
}
|
|
92
|
-
```
|
|
170
|
+
| `css` | `string` | Path to global CSS file (prevents FOUC in dev) |
|
|
93
171
|
|
|
94
172
|
## Features
|
|
95
173
|
|
|
174
|
+
- **Vite plugins** — `voltxRouter()` and `voltxAPI()` for Next.js-style file-based routing
|
|
175
|
+
- **Virtual modules** — `voltx/router` and `voltx/api` for clean imports
|
|
96
176
|
- **React SSR** — streaming server-side rendering with `registerSSR()`
|
|
97
|
-
- **
|
|
98
|
-
- **Dynamic routes** — `[param]` and `[...slug]` catch-all
|
|
177
|
+
- **Navigation** — `Link`, `NavLink`, `useNavigate`, `useParams` from `voltx/router`
|
|
178
|
+
- **Dynamic routes** — `[param]` and `[...slug]` catch-all for both pages and API
|
|
179
|
+
- **HMR** — new pages and API files are picked up instantly in dev mode
|
|
99
180
|
- **Built-in middleware** — CORS, request logging, error handling
|
|
100
181
|
- **Static file serving** — `public/` directory (favicon, robots.txt, manifest)
|
|
101
182
|
- **Full Hono access** — use any Hono middleware or plugin
|
package/dist/index.cjs
CHANGED
|
@@ -334,63 +334,302 @@ function voltxRouter(options = {}) {
|
|
|
334
334
|
},
|
|
335
335
|
load(id) {
|
|
336
336
|
if (id === RESOLVED_ID) {
|
|
337
|
-
return
|
|
338
|
-
|
|
339
|
-
|
|
337
|
+
return generateRouterModule(pagesDir);
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
handleHotUpdate({ file, server }) {
|
|
341
|
+
if (file.includes(pagesDir.replace(/\//g, "/"))) {
|
|
342
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_ID);
|
|
343
|
+
if (mod) {
|
|
344
|
+
server.moduleGraph.invalidateModule(mod);
|
|
345
|
+
server.ws.send({ type: "full-reload" });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function generateRouterModule(pagesDir) {
|
|
352
|
+
return `
|
|
353
|
+
import { createElement, Component, Suspense } from "react";
|
|
354
|
+
import { Routes, Route, Outlet } from "react-router";
|
|
355
|
+
|
|
356
|
+
// \u2500\u2500\u2500 Glob all files in pages directory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
357
|
+
const allFiles = import.meta.glob("/${pagesDir}/**/*.tsx", { eager: true });
|
|
358
|
+
|
|
359
|
+
// \u2500\u2500\u2500 Classify files by type \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
360
|
+
// Special files: layout.tsx, loading.tsx, error.tsx, not-found.tsx
|
|
361
|
+
// Page files: everything else (index.tsx, about.tsx, [slug].tsx, etc.)
|
|
362
|
+
|
|
363
|
+
const SPECIAL_FILES = new Set(["layout", "loading", "error", "not-found"]);
|
|
340
364
|
|
|
341
|
-
|
|
365
|
+
function classifyFiles() {
|
|
366
|
+
const pages = {}; // dir \u2192 [{ routePath, Component }]
|
|
367
|
+
const layouts = {}; // dir \u2192 Component
|
|
368
|
+
const loadings = {}; // dir \u2192 Component
|
|
369
|
+
const errors = {}; // dir \u2192 Component
|
|
370
|
+
let notFound = null; // global not-found component
|
|
342
371
|
|
|
343
|
-
|
|
344
|
-
const routes = [];
|
|
345
|
-
for (const [filePath, mod] of Object.entries(pages)) {
|
|
372
|
+
for (const [filePath, mod] of Object.entries(allFiles)) {
|
|
346
373
|
const Component = mod.default;
|
|
347
374
|
if (!Component) continue;
|
|
348
375
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
376
|
+
// Get relative path from pages dir: "/src/pages/blog/index.tsx" \u2192 "blog/index.tsx"
|
|
377
|
+
const rel = filePath.replace("/${pagesDir}/", "");
|
|
378
|
+
const parts = rel.replace(/\\.tsx$/, "").split("/");
|
|
379
|
+
const fileName = parts[parts.length - 1];
|
|
380
|
+
const dir = parts.length > 1 ? parts.slice(0, -1).join("/") : "";
|
|
381
|
+
|
|
382
|
+
if (fileName === "layout") {
|
|
383
|
+
layouts[dir] = Component;
|
|
384
|
+
} else if (fileName === "loading") {
|
|
385
|
+
loadings[dir] = Component;
|
|
386
|
+
} else if (fileName === "error") {
|
|
387
|
+
errors[dir] = Component;
|
|
388
|
+
} else if (fileName === "not-found") {
|
|
389
|
+
if (dir === "") notFound = Component;
|
|
390
|
+
} else {
|
|
391
|
+
// It's a page file \u2014 compute route path
|
|
392
|
+
let routePath = rel
|
|
393
|
+
.replace(/\\.tsx$/, "")
|
|
394
|
+
.replace(/(^|\\/)index$/, "$1")
|
|
395
|
+
.replace(/\\[([^\\]]+)\\]/g, ":$1")
|
|
396
|
+
.replace(/\\/$/, "");
|
|
397
|
+
|
|
398
|
+
if (!routePath) routePath = "";
|
|
399
|
+
|
|
400
|
+
if (!pages[dir]) pages[dir] = [];
|
|
401
|
+
pages[dir].push({ routePath: "/" + routePath, Component });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return { pages, layouts, loadings, errors, notFound };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const { pages, layouts, loadings, errors, notFound } = classifyFiles();
|
|
354
409
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
410
|
+
// \u2500\u2500\u2500 Error Boundary Component \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
411
|
+
class VoltxErrorBoundary extends Component {
|
|
412
|
+
constructor(props) {
|
|
413
|
+
super(props);
|
|
414
|
+
this.state = { hasError: false, error: null };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
static getDerivedStateFromError(error) {
|
|
418
|
+
return { hasError: true, error };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
render() {
|
|
422
|
+
if (this.state.hasError) {
|
|
423
|
+
const reset = () => this.setState({ hasError: false, error: null });
|
|
424
|
+
if (this.props.fallback) {
|
|
425
|
+
return createElement(this.props.fallback, {
|
|
426
|
+
error: this.state.error,
|
|
427
|
+
reset,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
return createElement("div", { style: { padding: "2rem" } },
|
|
431
|
+
createElement("h2", null, "Something went wrong"),
|
|
432
|
+
createElement("button", { onClick: reset }, "Try again")
|
|
433
|
+
);
|
|
358
434
|
}
|
|
435
|
+
return this.props.children;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// \u2500\u2500\u2500 Build nested route tree \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
440
|
+
// Directories are sorted so parents come before children.
|
|
441
|
+
// Each directory with a layout.tsx becomes a layout route.
|
|
442
|
+
// Pages within that directory become its children.
|
|
443
|
+
|
|
444
|
+
function getAllDirs() {
|
|
445
|
+
const dirs = new Set([""]);
|
|
446
|
+
for (const dir of Object.keys(pages)) dirs.add(dir);
|
|
447
|
+
for (const dir of Object.keys(layouts)) dirs.add(dir);
|
|
448
|
+
for (const dir of Object.keys(loadings)) dirs.add(dir);
|
|
449
|
+
for (const dir of Object.keys(errors)) dirs.add(dir);
|
|
450
|
+
return Array.from(dirs).sort((a, b) => {
|
|
451
|
+
if (a === "") return -1;
|
|
452
|
+
if (b === "") return 1;
|
|
453
|
+
return a.localeCompare(b);
|
|
454
|
+
});
|
|
455
|
+
}
|
|
359
456
|
|
|
360
|
-
|
|
457
|
+
function getParentDir(dir) {
|
|
458
|
+
if (dir === "") return null;
|
|
459
|
+
const idx = dir.lastIndexOf("/");
|
|
460
|
+
return idx === -1 ? "" : dir.substring(0, idx);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Find the nearest ancestor directory that has a layout
|
|
464
|
+
function findLayoutAncestor(dir) {
|
|
465
|
+
let current = getParentDir(dir);
|
|
466
|
+
while (current !== null) {
|
|
467
|
+
if (layouts[current]) return current;
|
|
468
|
+
current = getParentDir(current);
|
|
469
|
+
}
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Wrap element with loading (Suspense) and error boundary if available for a dir
|
|
474
|
+
function wrapElement(element, dir) {
|
|
475
|
+
let wrapped = element;
|
|
476
|
+
if (errors[dir]) {
|
|
477
|
+
wrapped = createElement(VoltxErrorBoundary, { fallback: errors[dir] }, wrapped);
|
|
361
478
|
}
|
|
362
|
-
|
|
479
|
+
if (loadings[dir]) {
|
|
480
|
+
wrapped = createElement(Suspense, { fallback: createElement(loadings[dir]) }, wrapped);
|
|
481
|
+
}
|
|
482
|
+
return wrapped;
|
|
363
483
|
}
|
|
364
484
|
|
|
365
|
-
|
|
485
|
+
// \u2500\u2500\u2500 Layout wrapper component \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
486
|
+
// A layout component that renders the layout with <Outlet /> for children
|
|
487
|
+
function createLayoutElement(LayoutComp, dir) {
|
|
488
|
+
// The layout component receives <Outlet /> as its children rendering point
|
|
489
|
+
// We create a wrapper that renders Layout with Outlet inside
|
|
490
|
+
function LayoutWrapper() {
|
|
491
|
+
return createElement(LayoutComp, null, createElement(Outlet));
|
|
492
|
+
}
|
|
493
|
+
LayoutWrapper.displayName = "Layout(" + (dir || "root") + ")";
|
|
494
|
+
return LayoutWrapper;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// \u2500\u2500\u2500 Generate the <Routes> tree \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
498
|
+
function buildRouteElements() {
|
|
499
|
+
const allDirs = getAllDirs();
|
|
500
|
+
|
|
501
|
+
// Build a tree structure: each dir can have child routes and child layout dirs
|
|
502
|
+
// dirChildren[dir] = array of Route elements (pages + nested layout routes)
|
|
503
|
+
const dirChildren = {};
|
|
504
|
+
for (const dir of allDirs) {
|
|
505
|
+
dirChildren[dir] = [];
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// First, add page routes to their directory
|
|
509
|
+
for (const dir of allDirs) {
|
|
510
|
+
const dirPages = pages[dir] || [];
|
|
511
|
+
for (const { routePath, Component: PageComp } of dirPages) {
|
|
512
|
+
// Compute the path relative to the directory's base
|
|
513
|
+
const dirBase = dir ? "/" + dir : "";
|
|
514
|
+
let relativePath = routePath;
|
|
515
|
+
if (dirBase && relativePath.startsWith(dirBase)) {
|
|
516
|
+
relativePath = relativePath.substring(dirBase.length);
|
|
517
|
+
}
|
|
518
|
+
if (relativePath.startsWith("/")) relativePath = relativePath.substring(1);
|
|
519
|
+
|
|
520
|
+
const isIndex = relativePath === "";
|
|
521
|
+
const routeProps = isIndex
|
|
522
|
+
? { index: true, element: createElement(PageComp), key: routePath }
|
|
523
|
+
: { path: relativePath, element: createElement(PageComp), key: routePath };
|
|
524
|
+
|
|
525
|
+
dirChildren[dir].push(createElement(Route, routeProps));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Now, nest directories bottom-up: child dirs become Route children of parent dirs
|
|
530
|
+
// Process in reverse order (deepest first)
|
|
531
|
+
const reversedDirs = allDirs.slice().reverse();
|
|
532
|
+
|
|
533
|
+
for (const dir of reversedDirs) {
|
|
534
|
+
if (dir === "") continue; // root handled last
|
|
535
|
+
|
|
536
|
+
const parentDir = getParentDir(dir);
|
|
537
|
+
if (parentDir === null) continue;
|
|
538
|
+
|
|
539
|
+
const hasLayout = !!layouts[dir];
|
|
540
|
+
const children = dirChildren[dir];
|
|
541
|
+
|
|
542
|
+
// Compute path segment for this directory relative to parent
|
|
543
|
+
const parentBase = parentDir ? parentDir + "/" : "";
|
|
544
|
+
const segment = dir.startsWith(parentBase) ? dir.substring(parentBase.length) : dir;
|
|
545
|
+
// Convert [param] to :param in segment
|
|
546
|
+
const routeSegment = segment.replace(/\\[([^\\]]+)\\]/g, ":$1");
|
|
547
|
+
|
|
548
|
+
if (hasLayout) {
|
|
549
|
+
// This dir has a layout \u2014 create a layout route with children
|
|
550
|
+
const LayoutWrapper = createLayoutElement(layouts[dir], dir);
|
|
551
|
+
let layoutElement = createElement(LayoutWrapper);
|
|
552
|
+
layoutElement = wrapElement(layoutElement, dir);
|
|
553
|
+
|
|
554
|
+
const layoutRoute = createElement(
|
|
555
|
+
Route,
|
|
556
|
+
{ path: routeSegment, element: layoutElement, key: "layout:" + dir },
|
|
557
|
+
...children
|
|
558
|
+
);
|
|
559
|
+
dirChildren[parentDir].push(layoutRoute);
|
|
560
|
+
} else if (children.length > 0) {
|
|
561
|
+
// No layout \u2014 just a path prefix grouping
|
|
562
|
+
if (routeSegment) {
|
|
563
|
+
const prefixRoute = createElement(
|
|
564
|
+
Route,
|
|
565
|
+
{ path: routeSegment, key: "prefix:" + dir },
|
|
566
|
+
...children
|
|
567
|
+
);
|
|
568
|
+
dirChildren[parentDir].push(prefixRoute);
|
|
569
|
+
} else {
|
|
570
|
+
// Empty segment \u2014 push children directly
|
|
571
|
+
dirChildren[parentDir].push(...children);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Build root routes
|
|
577
|
+
const rootChildren = dirChildren[""];
|
|
578
|
+
|
|
579
|
+
// Add not-found catch-all at the end
|
|
580
|
+
if (notFound) {
|
|
581
|
+
rootChildren.push(
|
|
582
|
+
createElement(Route, { path: "*", element: createElement(notFound), key: "not-found" })
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// If root has a layout, wrap everything in it
|
|
587
|
+
if (layouts[""]) {
|
|
588
|
+
const RootLayout = createLayoutElement(layouts[""], "");
|
|
589
|
+
let rootElement = createElement(RootLayout);
|
|
590
|
+
rootElement = wrapElement(rootElement, "");
|
|
591
|
+
|
|
592
|
+
return createElement(
|
|
593
|
+
Routes,
|
|
594
|
+
null,
|
|
595
|
+
createElement(Route, { path: "/", element: rootElement }, ...rootChildren)
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// No root layout \u2014 wrap with loading/error if present
|
|
600
|
+
let routesElement = createElement(Routes, null, ...rootChildren);
|
|
601
|
+
if (errors[""]) {
|
|
602
|
+
routesElement = createElement(VoltxErrorBoundary, { fallback: errors[""] }, routesElement);
|
|
603
|
+
}
|
|
604
|
+
if (loadings[""]) {
|
|
605
|
+
routesElement = createElement(Suspense, { fallback: createElement(loadings[""]) }, routesElement);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return routesElement;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// \u2500\u2500\u2500 Collect flat routes list (for backward compat) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
612
|
+
function collectRoutes() {
|
|
613
|
+
const result = [];
|
|
614
|
+
for (const dirPages of Object.values(pages)) {
|
|
615
|
+
for (const { routePath, Component } of dirPages) {
|
|
616
|
+
result.push({ path: routePath, Component });
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return result;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const routes = collectRoutes();
|
|
366
623
|
|
|
367
624
|
export function VoltxRoutes() {
|
|
368
|
-
return
|
|
369
|
-
Routes,
|
|
370
|
-
null,
|
|
371
|
-
routes.map(({ path, Component }) =>
|
|
372
|
-
createElement(Route, { key: path, path, element: createElement(Component) })
|
|
373
|
-
)
|
|
374
|
-
);
|
|
625
|
+
return buildRouteElements();
|
|
375
626
|
}
|
|
376
627
|
|
|
377
628
|
export { routes };
|
|
378
629
|
|
|
379
630
|
// Navigation primitives \u2014 single import source
|
|
380
|
-
export { Link, NavLink, useNavigate, useParams, useLocation, useSearchParams } from "react-router";
|
|
631
|
+
export { Link, NavLink, Outlet, useNavigate, useParams, useLocation, useSearchParams, useOutletContext } from "react-router";
|
|
381
632
|
`;
|
|
382
|
-
}
|
|
383
|
-
},
|
|
384
|
-
handleHotUpdate({ file, server }) {
|
|
385
|
-
if (file.includes(pagesDir.replace(/\//g, "/"))) {
|
|
386
|
-
const mod = server.moduleGraph.getModuleById(RESOLVED_ID);
|
|
387
|
-
if (mod) {
|
|
388
|
-
server.moduleGraph.invalidateModule(mod);
|
|
389
|
-
server.ws.send({ type: "full-reload" });
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
};
|
|
394
633
|
}
|
|
395
634
|
|
|
396
635
|
// src/vite-api-plugin.ts
|
|
@@ -613,7 +852,7 @@ ${cssLinks}
|
|
|
613
852
|
}
|
|
614
853
|
|
|
615
854
|
// src/index.ts
|
|
616
|
-
var VERSION = "0.4.
|
|
855
|
+
var VERSION = "0.4.7";
|
|
617
856
|
// Annotate the CommonJS export names for ESM import in node:
|
|
618
857
|
0 && (module.exports = {
|
|
619
858
|
Hono,
|
package/dist/index.d.cts
CHANGED
|
@@ -185,16 +185,10 @@ interface VoltxRouterOptions {
|
|
|
185
185
|
* VoltX file-based router plugin for Vite.
|
|
186
186
|
*
|
|
187
187
|
* Scans `src/pages/` and generates a virtual module that maps
|
|
188
|
-
* file paths to routes —
|
|
188
|
+
* file paths to routes with nested layout support — like Next.js App Router.
|
|
189
189
|
*
|
|
190
190
|
* Usage:
|
|
191
191
|
* import { Link, VoltxRoutes, useNavigate } from "voltx/router";
|
|
192
|
-
*
|
|
193
|
-
* Convention:
|
|
194
|
-
* src/pages/index.tsx → /
|
|
195
|
-
* src/pages/about.tsx → /about
|
|
196
|
-
* src/pages/blog/index.tsx → /blog
|
|
197
|
-
* src/pages/blog/[slug].tsx → /blog/:slug
|
|
198
192
|
*/
|
|
199
193
|
declare function voltxRouter(options?: VoltxRouterOptions): Plugin;
|
|
200
194
|
|
|
@@ -263,6 +257,6 @@ interface ViteDevServer {
|
|
|
263
257
|
*/
|
|
264
258
|
declare function registerSSR(app: Hono, vite: ViteDevServer | null, options?: SSROptions): void;
|
|
265
259
|
|
|
266
|
-
declare const VERSION = "0.4.
|
|
260
|
+
declare const VERSION = "0.4.7";
|
|
267
261
|
|
|
268
262
|
export { type CorsConfig, type HttpMethod, type MiddlewareHandler, type RouteEntry, type RouteHandler, type RouteModule, type SSROptions, type ServerConfig, type ServerInfo, VERSION, type ViteDevOptions, type VoltxAPIOptions, type VoltxRouterOptions, type VoltxServer, createCorsMiddleware, createErrorHandler, createLoggerMiddleware, createServer, createViteDevConfig, filePathToUrlPath, registerSSR, registerStaticFiles, scanAndRegisterRoutes, voltxAPI, voltxRouter };
|
package/dist/index.d.ts
CHANGED
|
@@ -185,16 +185,10 @@ interface VoltxRouterOptions {
|
|
|
185
185
|
* VoltX file-based router plugin for Vite.
|
|
186
186
|
*
|
|
187
187
|
* Scans `src/pages/` and generates a virtual module that maps
|
|
188
|
-
* file paths to routes —
|
|
188
|
+
* file paths to routes with nested layout support — like Next.js App Router.
|
|
189
189
|
*
|
|
190
190
|
* Usage:
|
|
191
191
|
* import { Link, VoltxRoutes, useNavigate } from "voltx/router";
|
|
192
|
-
*
|
|
193
|
-
* Convention:
|
|
194
|
-
* src/pages/index.tsx → /
|
|
195
|
-
* src/pages/about.tsx → /about
|
|
196
|
-
* src/pages/blog/index.tsx → /blog
|
|
197
|
-
* src/pages/blog/[slug].tsx → /blog/:slug
|
|
198
192
|
*/
|
|
199
193
|
declare function voltxRouter(options?: VoltxRouterOptions): Plugin;
|
|
200
194
|
|
|
@@ -263,6 +257,6 @@ interface ViteDevServer {
|
|
|
263
257
|
*/
|
|
264
258
|
declare function registerSSR(app: Hono, vite: ViteDevServer | null, options?: SSROptions): void;
|
|
265
259
|
|
|
266
|
-
declare const VERSION = "0.4.
|
|
260
|
+
declare const VERSION = "0.4.7";
|
|
267
261
|
|
|
268
262
|
export { type CorsConfig, type HttpMethod, type MiddlewareHandler, type RouteEntry, type RouteHandler, type RouteModule, type SSROptions, type ServerConfig, type ServerInfo, VERSION, type ViteDevOptions, type VoltxAPIOptions, type VoltxRouterOptions, type VoltxServer, createCorsMiddleware, createErrorHandler, createLoggerMiddleware, createServer, createViteDevConfig, filePathToUrlPath, registerSSR, registerStaticFiles, scanAndRegisterRoutes, voltxAPI, voltxRouter };
|
package/dist/index.js
CHANGED
|
@@ -286,63 +286,302 @@ function voltxRouter(options = {}) {
|
|
|
286
286
|
},
|
|
287
287
|
load(id) {
|
|
288
288
|
if (id === RESOLVED_ID) {
|
|
289
|
-
return
|
|
290
|
-
|
|
291
|
-
|
|
289
|
+
return generateRouterModule(pagesDir);
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
handleHotUpdate({ file, server }) {
|
|
293
|
+
if (file.includes(pagesDir.replace(/\//g, "/"))) {
|
|
294
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_ID);
|
|
295
|
+
if (mod) {
|
|
296
|
+
server.moduleGraph.invalidateModule(mod);
|
|
297
|
+
server.ws.send({ type: "full-reload" });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function generateRouterModule(pagesDir) {
|
|
304
|
+
return `
|
|
305
|
+
import { createElement, Component, Suspense } from "react";
|
|
306
|
+
import { Routes, Route, Outlet } from "react-router";
|
|
307
|
+
|
|
308
|
+
// \u2500\u2500\u2500 Glob all files in pages directory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
309
|
+
const allFiles = import.meta.glob("/${pagesDir}/**/*.tsx", { eager: true });
|
|
292
310
|
|
|
293
|
-
|
|
311
|
+
// \u2500\u2500\u2500 Classify files by type \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
312
|
+
// Special files: layout.tsx, loading.tsx, error.tsx, not-found.tsx
|
|
313
|
+
// Page files: everything else (index.tsx, about.tsx, [slug].tsx, etc.)
|
|
294
314
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
315
|
+
const SPECIAL_FILES = new Set(["layout", "loading", "error", "not-found"]);
|
|
316
|
+
|
|
317
|
+
function classifyFiles() {
|
|
318
|
+
const pages = {}; // dir \u2192 [{ routePath, Component }]
|
|
319
|
+
const layouts = {}; // dir \u2192 Component
|
|
320
|
+
const loadings = {}; // dir \u2192 Component
|
|
321
|
+
const errors = {}; // dir \u2192 Component
|
|
322
|
+
let notFound = null; // global not-found component
|
|
323
|
+
|
|
324
|
+
for (const [filePath, mod] of Object.entries(allFiles)) {
|
|
298
325
|
const Component = mod.default;
|
|
299
326
|
if (!Component) continue;
|
|
300
327
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
328
|
+
// Get relative path from pages dir: "/src/pages/blog/index.tsx" \u2192 "blog/index.tsx"
|
|
329
|
+
const rel = filePath.replace("/${pagesDir}/", "");
|
|
330
|
+
const parts = rel.replace(/\\.tsx$/, "").split("/");
|
|
331
|
+
const fileName = parts[parts.length - 1];
|
|
332
|
+
const dir = parts.length > 1 ? parts.slice(0, -1).join("/") : "";
|
|
333
|
+
|
|
334
|
+
if (fileName === "layout") {
|
|
335
|
+
layouts[dir] = Component;
|
|
336
|
+
} else if (fileName === "loading") {
|
|
337
|
+
loadings[dir] = Component;
|
|
338
|
+
} else if (fileName === "error") {
|
|
339
|
+
errors[dir] = Component;
|
|
340
|
+
} else if (fileName === "not-found") {
|
|
341
|
+
if (dir === "") notFound = Component;
|
|
342
|
+
} else {
|
|
343
|
+
// It's a page file \u2014 compute route path
|
|
344
|
+
let routePath = rel
|
|
345
|
+
.replace(/\\.tsx$/, "")
|
|
346
|
+
.replace(/(^|\\/)index$/, "$1")
|
|
347
|
+
.replace(/\\[([^\\]]+)\\]/g, ":$1")
|
|
348
|
+
.replace(/\\/$/, "");
|
|
349
|
+
|
|
350
|
+
if (!routePath) routePath = "";
|
|
351
|
+
|
|
352
|
+
if (!pages[dir]) pages[dir] = [];
|
|
353
|
+
pages[dir].push({ routePath: "/" + routePath, Component });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return { pages, layouts, loadings, errors, notFound };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const { pages, layouts, loadings, errors, notFound } = classifyFiles();
|
|
361
|
+
|
|
362
|
+
// \u2500\u2500\u2500 Error Boundary Component \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
363
|
+
class VoltxErrorBoundary extends Component {
|
|
364
|
+
constructor(props) {
|
|
365
|
+
super(props);
|
|
366
|
+
this.state = { hasError: false, error: null };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
static getDerivedStateFromError(error) {
|
|
370
|
+
return { hasError: true, error };
|
|
371
|
+
}
|
|
306
372
|
|
|
307
|
-
|
|
308
|
-
if (
|
|
309
|
-
|
|
373
|
+
render() {
|
|
374
|
+
if (this.state.hasError) {
|
|
375
|
+
const reset = () => this.setState({ hasError: false, error: null });
|
|
376
|
+
if (this.props.fallback) {
|
|
377
|
+
return createElement(this.props.fallback, {
|
|
378
|
+
error: this.state.error,
|
|
379
|
+
reset,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
return createElement("div", { style: { padding: "2rem" } },
|
|
383
|
+
createElement("h2", null, "Something went wrong"),
|
|
384
|
+
createElement("button", { onClick: reset }, "Try again")
|
|
385
|
+
);
|
|
310
386
|
}
|
|
387
|
+
return this.props.children;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// \u2500\u2500\u2500 Build nested route tree \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
392
|
+
// Directories are sorted so parents come before children.
|
|
393
|
+
// Each directory with a layout.tsx becomes a layout route.
|
|
394
|
+
// Pages within that directory become its children.
|
|
395
|
+
|
|
396
|
+
function getAllDirs() {
|
|
397
|
+
const dirs = new Set([""]);
|
|
398
|
+
for (const dir of Object.keys(pages)) dirs.add(dir);
|
|
399
|
+
for (const dir of Object.keys(layouts)) dirs.add(dir);
|
|
400
|
+
for (const dir of Object.keys(loadings)) dirs.add(dir);
|
|
401
|
+
for (const dir of Object.keys(errors)) dirs.add(dir);
|
|
402
|
+
return Array.from(dirs).sort((a, b) => {
|
|
403
|
+
if (a === "") return -1;
|
|
404
|
+
if (b === "") return 1;
|
|
405
|
+
return a.localeCompare(b);
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function getParentDir(dir) {
|
|
410
|
+
if (dir === "") return null;
|
|
411
|
+
const idx = dir.lastIndexOf("/");
|
|
412
|
+
return idx === -1 ? "" : dir.substring(0, idx);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Find the nearest ancestor directory that has a layout
|
|
416
|
+
function findLayoutAncestor(dir) {
|
|
417
|
+
let current = getParentDir(dir);
|
|
418
|
+
while (current !== null) {
|
|
419
|
+
if (layouts[current]) return current;
|
|
420
|
+
current = getParentDir(current);
|
|
421
|
+
}
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
311
424
|
|
|
312
|
-
|
|
425
|
+
// Wrap element with loading (Suspense) and error boundary if available for a dir
|
|
426
|
+
function wrapElement(element, dir) {
|
|
427
|
+
let wrapped = element;
|
|
428
|
+
if (errors[dir]) {
|
|
429
|
+
wrapped = createElement(VoltxErrorBoundary, { fallback: errors[dir] }, wrapped);
|
|
313
430
|
}
|
|
314
|
-
|
|
431
|
+
if (loadings[dir]) {
|
|
432
|
+
wrapped = createElement(Suspense, { fallback: createElement(loadings[dir]) }, wrapped);
|
|
433
|
+
}
|
|
434
|
+
return wrapped;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// \u2500\u2500\u2500 Layout wrapper component \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
438
|
+
// A layout component that renders the layout with <Outlet /> for children
|
|
439
|
+
function createLayoutElement(LayoutComp, dir) {
|
|
440
|
+
// The layout component receives <Outlet /> as its children rendering point
|
|
441
|
+
// We create a wrapper that renders Layout with Outlet inside
|
|
442
|
+
function LayoutWrapper() {
|
|
443
|
+
return createElement(LayoutComp, null, createElement(Outlet));
|
|
444
|
+
}
|
|
445
|
+
LayoutWrapper.displayName = "Layout(" + (dir || "root") + ")";
|
|
446
|
+
return LayoutWrapper;
|
|
315
447
|
}
|
|
316
448
|
|
|
317
|
-
|
|
449
|
+
// \u2500\u2500\u2500 Generate the <Routes> tree \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
450
|
+
function buildRouteElements() {
|
|
451
|
+
const allDirs = getAllDirs();
|
|
452
|
+
|
|
453
|
+
// Build a tree structure: each dir can have child routes and child layout dirs
|
|
454
|
+
// dirChildren[dir] = array of Route elements (pages + nested layout routes)
|
|
455
|
+
const dirChildren = {};
|
|
456
|
+
for (const dir of allDirs) {
|
|
457
|
+
dirChildren[dir] = [];
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// First, add page routes to their directory
|
|
461
|
+
for (const dir of allDirs) {
|
|
462
|
+
const dirPages = pages[dir] || [];
|
|
463
|
+
for (const { routePath, Component: PageComp } of dirPages) {
|
|
464
|
+
// Compute the path relative to the directory's base
|
|
465
|
+
const dirBase = dir ? "/" + dir : "";
|
|
466
|
+
let relativePath = routePath;
|
|
467
|
+
if (dirBase && relativePath.startsWith(dirBase)) {
|
|
468
|
+
relativePath = relativePath.substring(dirBase.length);
|
|
469
|
+
}
|
|
470
|
+
if (relativePath.startsWith("/")) relativePath = relativePath.substring(1);
|
|
471
|
+
|
|
472
|
+
const isIndex = relativePath === "";
|
|
473
|
+
const routeProps = isIndex
|
|
474
|
+
? { index: true, element: createElement(PageComp), key: routePath }
|
|
475
|
+
: { path: relativePath, element: createElement(PageComp), key: routePath };
|
|
476
|
+
|
|
477
|
+
dirChildren[dir].push(createElement(Route, routeProps));
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Now, nest directories bottom-up: child dirs become Route children of parent dirs
|
|
482
|
+
// Process in reverse order (deepest first)
|
|
483
|
+
const reversedDirs = allDirs.slice().reverse();
|
|
484
|
+
|
|
485
|
+
for (const dir of reversedDirs) {
|
|
486
|
+
if (dir === "") continue; // root handled last
|
|
487
|
+
|
|
488
|
+
const parentDir = getParentDir(dir);
|
|
489
|
+
if (parentDir === null) continue;
|
|
490
|
+
|
|
491
|
+
const hasLayout = !!layouts[dir];
|
|
492
|
+
const children = dirChildren[dir];
|
|
493
|
+
|
|
494
|
+
// Compute path segment for this directory relative to parent
|
|
495
|
+
const parentBase = parentDir ? parentDir + "/" : "";
|
|
496
|
+
const segment = dir.startsWith(parentBase) ? dir.substring(parentBase.length) : dir;
|
|
497
|
+
// Convert [param] to :param in segment
|
|
498
|
+
const routeSegment = segment.replace(/\\[([^\\]]+)\\]/g, ":$1");
|
|
499
|
+
|
|
500
|
+
if (hasLayout) {
|
|
501
|
+
// This dir has a layout \u2014 create a layout route with children
|
|
502
|
+
const LayoutWrapper = createLayoutElement(layouts[dir], dir);
|
|
503
|
+
let layoutElement = createElement(LayoutWrapper);
|
|
504
|
+
layoutElement = wrapElement(layoutElement, dir);
|
|
505
|
+
|
|
506
|
+
const layoutRoute = createElement(
|
|
507
|
+
Route,
|
|
508
|
+
{ path: routeSegment, element: layoutElement, key: "layout:" + dir },
|
|
509
|
+
...children
|
|
510
|
+
);
|
|
511
|
+
dirChildren[parentDir].push(layoutRoute);
|
|
512
|
+
} else if (children.length > 0) {
|
|
513
|
+
// No layout \u2014 just a path prefix grouping
|
|
514
|
+
if (routeSegment) {
|
|
515
|
+
const prefixRoute = createElement(
|
|
516
|
+
Route,
|
|
517
|
+
{ path: routeSegment, key: "prefix:" + dir },
|
|
518
|
+
...children
|
|
519
|
+
);
|
|
520
|
+
dirChildren[parentDir].push(prefixRoute);
|
|
521
|
+
} else {
|
|
522
|
+
// Empty segment \u2014 push children directly
|
|
523
|
+
dirChildren[parentDir].push(...children);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Build root routes
|
|
529
|
+
const rootChildren = dirChildren[""];
|
|
530
|
+
|
|
531
|
+
// Add not-found catch-all at the end
|
|
532
|
+
if (notFound) {
|
|
533
|
+
rootChildren.push(
|
|
534
|
+
createElement(Route, { path: "*", element: createElement(notFound), key: "not-found" })
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// If root has a layout, wrap everything in it
|
|
539
|
+
if (layouts[""]) {
|
|
540
|
+
const RootLayout = createLayoutElement(layouts[""], "");
|
|
541
|
+
let rootElement = createElement(RootLayout);
|
|
542
|
+
rootElement = wrapElement(rootElement, "");
|
|
543
|
+
|
|
544
|
+
return createElement(
|
|
545
|
+
Routes,
|
|
546
|
+
null,
|
|
547
|
+
createElement(Route, { path: "/", element: rootElement }, ...rootChildren)
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// No root layout \u2014 wrap with loading/error if present
|
|
552
|
+
let routesElement = createElement(Routes, null, ...rootChildren);
|
|
553
|
+
if (errors[""]) {
|
|
554
|
+
routesElement = createElement(VoltxErrorBoundary, { fallback: errors[""] }, routesElement);
|
|
555
|
+
}
|
|
556
|
+
if (loadings[""]) {
|
|
557
|
+
routesElement = createElement(Suspense, { fallback: createElement(loadings[""]) }, routesElement);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return routesElement;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// \u2500\u2500\u2500 Collect flat routes list (for backward compat) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
564
|
+
function collectRoutes() {
|
|
565
|
+
const result = [];
|
|
566
|
+
for (const dirPages of Object.values(pages)) {
|
|
567
|
+
for (const { routePath, Component } of dirPages) {
|
|
568
|
+
result.push({ path: routePath, Component });
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return result;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const routes = collectRoutes();
|
|
318
575
|
|
|
319
576
|
export function VoltxRoutes() {
|
|
320
|
-
return
|
|
321
|
-
Routes,
|
|
322
|
-
null,
|
|
323
|
-
routes.map(({ path, Component }) =>
|
|
324
|
-
createElement(Route, { key: path, path, element: createElement(Component) })
|
|
325
|
-
)
|
|
326
|
-
);
|
|
577
|
+
return buildRouteElements();
|
|
327
578
|
}
|
|
328
579
|
|
|
329
580
|
export { routes };
|
|
330
581
|
|
|
331
582
|
// Navigation primitives \u2014 single import source
|
|
332
|
-
export { Link, NavLink, useNavigate, useParams, useLocation, useSearchParams } from "react-router";
|
|
583
|
+
export { Link, NavLink, Outlet, useNavigate, useParams, useLocation, useSearchParams, useOutletContext } from "react-router";
|
|
333
584
|
`;
|
|
334
|
-
}
|
|
335
|
-
},
|
|
336
|
-
handleHotUpdate({ file, server }) {
|
|
337
|
-
if (file.includes(pagesDir.replace(/\//g, "/"))) {
|
|
338
|
-
const mod = server.moduleGraph.getModuleById(RESOLVED_ID);
|
|
339
|
-
if (mod) {
|
|
340
|
-
server.moduleGraph.invalidateModule(mod);
|
|
341
|
-
server.ws.send({ type: "full-reload" });
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
};
|
|
346
585
|
}
|
|
347
586
|
|
|
348
587
|
// src/vite-api-plugin.ts
|
|
@@ -565,7 +804,7 @@ ${cssLinks}
|
|
|
565
804
|
}
|
|
566
805
|
|
|
567
806
|
// src/index.ts
|
|
568
|
-
var VERSION = "0.4.
|
|
807
|
+
var VERSION = "0.4.7";
|
|
569
808
|
export {
|
|
570
809
|
Hono2 as Hono,
|
|
571
810
|
VERSION,
|
package/package.json
CHANGED