@voltx/server 0.3.0 → 0.3.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 +50 -33
- package/dist/index.cjs +144 -6
- package/dist/index.d.cts +62 -8
- package/dist/index.d.ts +62 -8
- package/dist/index.js +142 -6
- package/package.json +12 -2
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 and SSE streaming</em>
|
|
3
|
+
<em>Hono-based HTTP server with file-based routing, SSR, and SSE streaming</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 (
|
|
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.
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
@@ -22,34 +22,63 @@ npm install @voltx/server
|
|
|
22
22
|
## Quick Start
|
|
23
23
|
|
|
24
24
|
```ts
|
|
25
|
-
import {
|
|
25
|
+
import { Hono } from "hono";
|
|
26
|
+
import { registerSSR } from "@voltx/server";
|
|
26
27
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
const app = new Hono();
|
|
29
|
+
|
|
30
|
+
// API routes
|
|
31
|
+
app.get("/api", (c) => c.json({ status: "ok" }));
|
|
32
|
+
|
|
33
|
+
// SSR — renders React on the server, hydrates on the client
|
|
34
|
+
registerSSR(app, null, {
|
|
35
|
+
title: "My App",
|
|
36
|
+
entryServer: "src/entry-server.tsx",
|
|
37
|
+
entryClient: "src/entry-client.tsx",
|
|
32
38
|
});
|
|
33
39
|
|
|
34
|
-
|
|
35
|
-
// ⚡ VoltX server running at http://localhost:3000
|
|
40
|
+
export default app;
|
|
36
41
|
```
|
|
37
42
|
|
|
43
|
+
## Server-Side Rendering
|
|
44
|
+
|
|
45
|
+
`registerSSR()` provides streaming React SSR with zero config:
|
|
46
|
+
|
|
47
|
+
- **Dev mode** — works with `@hono/vite-dev-server` for HMR
|
|
48
|
+
- **Production** — reads the Vite client manifest for hashed asset paths, serves pre-built SSR bundle
|
|
49
|
+
- **Streaming** — uses `renderToReadableStream` for fast TTFB
|
|
50
|
+
- **Public env** — injects `window.__VOLTX_ENV__` for `VITE_*` variables
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { registerSSR } from "@voltx/server";
|
|
54
|
+
|
|
55
|
+
registerSSR(app, viteInstance, {
|
|
56
|
+
title: "My App",
|
|
57
|
+
entryServer: "src/entry-server.tsx",
|
|
58
|
+
entryClient: "src/entry-client.tsx",
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
| Option | Type | Description |
|
|
63
|
+
|--------|------|-------------|
|
|
64
|
+
| `title` | `string` | HTML `<title>` |
|
|
65
|
+
| `entryServer` | `string` | Path to SSR entry (exports `render()`) |
|
|
66
|
+
| `entryClient` | `string` | Path to client hydration entry |
|
|
67
|
+
|
|
38
68
|
## File-Based Routing
|
|
39
69
|
|
|
40
|
-
Drop files in `
|
|
70
|
+
Drop files in `api/` and they become API endpoints:
|
|
41
71
|
|
|
42
72
|
```
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
47
77
|
```
|
|
48
78
|
|
|
49
79
|
Each file exports HTTP method handlers:
|
|
50
80
|
|
|
51
81
|
```ts
|
|
52
|
-
// src/routes/api/chat.ts
|
|
53
82
|
import type { Context } from "@voltx/server";
|
|
54
83
|
|
|
55
84
|
export async function POST(c: Context) {
|
|
@@ -64,24 +93,12 @@ export function GET(c: Context) {
|
|
|
64
93
|
|
|
65
94
|
## Features
|
|
66
95
|
|
|
67
|
-
- **
|
|
68
|
-
- **
|
|
96
|
+
- **React SSR** — streaming server-side rendering with `registerSSR()`
|
|
97
|
+
- **File-based routing** — Next.js-style `api/` directory
|
|
98
|
+
- **Dynamic routes** — `[param]` and `[...slug]` catch-all
|
|
69
99
|
- **Built-in middleware** — CORS, request logging, error handling
|
|
70
|
-
- **Static file serving** —
|
|
71
|
-
- **
|
|
72
|
-
- **Graceful shutdown** — Clean server stop with `server.stop()`
|
|
73
|
-
- **Full Hono access** — `server.app` gives you the raw Hono instance
|
|
74
|
-
|
|
75
|
-
## Configuration
|
|
76
|
-
|
|
77
|
-
| Option | Type | Default | Description |
|
|
78
|
-
|--------|------|---------|-------------|
|
|
79
|
-
| `port` | `number` | `3000` | Server port |
|
|
80
|
-
| `hostname` | `string` | `"0.0.0.0"` | Bind address |
|
|
81
|
-
| `routesDir` | `string` | `"src/routes"` | Routes directory |
|
|
82
|
-
| `staticDir` | `string` | `"public"` | Static files directory |
|
|
83
|
-
| `cors` | `boolean \| object` | `true` | CORS configuration |
|
|
84
|
-
| `logger` | `boolean` | `true` (dev) | Request logging |
|
|
100
|
+
- **Static file serving** — `public/` directory (favicon, robots.txt, manifest)
|
|
101
|
+
- **Full Hono access** — use any Hono middleware or plugin
|
|
85
102
|
|
|
86
103
|
## Part of VoltX
|
|
87
104
|
|
package/dist/index.cjs
CHANGED
|
@@ -26,7 +26,9 @@ __export(index_exports, {
|
|
|
26
26
|
createErrorHandler: () => createErrorHandler,
|
|
27
27
|
createLoggerMiddleware: () => createLoggerMiddleware,
|
|
28
28
|
createServer: () => createServer,
|
|
29
|
+
createViteDevConfig: () => createViteDevConfig,
|
|
29
30
|
filePathToUrlPath: () => filePathToUrlPath,
|
|
31
|
+
registerSSR: () => registerSSR,
|
|
30
32
|
registerStaticFiles: () => registerStaticFiles,
|
|
31
33
|
scanAndRegisterRoutes: () => scanAndRegisterRoutes
|
|
32
34
|
});
|
|
@@ -161,13 +163,16 @@ function filePathToUrlPath(filePath, routesDir) {
|
|
|
161
163
|
const ext = (0, import_node_path.extname)(rel);
|
|
162
164
|
rel = rel.slice(0, -ext.length);
|
|
163
165
|
rel = rel.replace(/\\/g, "/");
|
|
164
|
-
if (rel === "index")
|
|
165
|
-
if (rel.endsWith("/index")) {
|
|
166
|
+
if (rel === "index") rel = "";
|
|
167
|
+
else if (rel.endsWith("/index")) {
|
|
166
168
|
rel = rel.slice(0, -"/index".length);
|
|
167
169
|
}
|
|
168
170
|
rel = rel.replace(/\[([^\]\.]+)\]/g, ":$1");
|
|
169
171
|
rel = rel.replace(/\[\.\.\.([^\]]+)\]/g, "*");
|
|
170
|
-
|
|
172
|
+
const dirBasename = routesDir.replace(/\\/g, "/").split("/").pop() ?? "";
|
|
173
|
+
const prefix = dirBasename && dirBasename !== "routes" && dirBasename !== "src" ? `/${dirBasename}` : "";
|
|
174
|
+
if (!rel) return prefix || "/";
|
|
175
|
+
return `${prefix}/${rel}`;
|
|
171
176
|
}
|
|
172
177
|
async function importRouteModule(filePath) {
|
|
173
178
|
try {
|
|
@@ -190,7 +195,7 @@ function createServer(config = {}) {
|
|
|
190
195
|
const {
|
|
191
196
|
port = Number(process.env.PORT) || 3e3,
|
|
192
197
|
hostname = "0.0.0.0",
|
|
193
|
-
routesDir = "
|
|
198
|
+
routesDir = "api",
|
|
194
199
|
staticDir = "public",
|
|
195
200
|
cors = true,
|
|
196
201
|
logger: enableLogger = process.env.NODE_ENV !== "production",
|
|
@@ -246,10 +251,10 @@ function createServer(config = {}) {
|
|
|
246
251
|
},
|
|
247
252
|
async stop() {
|
|
248
253
|
if (httpServer) {
|
|
249
|
-
await new Promise((
|
|
254
|
+
await new Promise((resolve3, reject) => {
|
|
250
255
|
httpServer.close((err) => {
|
|
251
256
|
if (err) reject(err);
|
|
252
|
-
else
|
|
257
|
+
else resolve3();
|
|
253
258
|
});
|
|
254
259
|
});
|
|
255
260
|
httpServer = null;
|
|
@@ -271,6 +276,137 @@ function createServer(config = {}) {
|
|
|
271
276
|
|
|
272
277
|
// src/index.ts
|
|
273
278
|
var import_hono2 = require("hono");
|
|
279
|
+
|
|
280
|
+
// src/vite.ts
|
|
281
|
+
function createViteDevConfig(options = {}) {
|
|
282
|
+
const {
|
|
283
|
+
root = process.cwd(),
|
|
284
|
+
entry = "src/index.ts"
|
|
285
|
+
} = options;
|
|
286
|
+
return {
|
|
287
|
+
root,
|
|
288
|
+
server: {
|
|
289
|
+
// Let Hono handle the port — Vite runs in middleware mode via the plugin
|
|
290
|
+
hmr: true
|
|
291
|
+
},
|
|
292
|
+
plugins: [],
|
|
293
|
+
// Plugins are added dynamically when starting
|
|
294
|
+
// Externalize Node.js packages for SSR
|
|
295
|
+
ssr: {
|
|
296
|
+
external: ["react", "react-dom"]
|
|
297
|
+
},
|
|
298
|
+
entry
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/ssr.ts
|
|
303
|
+
var import_node_path3 = require("path");
|
|
304
|
+
var import_node_fs = require("fs");
|
|
305
|
+
function getPublicEnv() {
|
|
306
|
+
const publicEnv = {};
|
|
307
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
308
|
+
if (key.startsWith("VOLTX_PUBLIC_") && value !== void 0) {
|
|
309
|
+
publicEnv[key] = value;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return publicEnv;
|
|
313
|
+
}
|
|
314
|
+
function registerSSR(app, vite, options = {}) {
|
|
315
|
+
const entryServer = options.entryServer ?? "src/entry-server.tsx";
|
|
316
|
+
const entryClient = options.entryClient ?? "src/entry-client.tsx";
|
|
317
|
+
const title = options.title ?? "VoltX App";
|
|
318
|
+
let manifest = null;
|
|
319
|
+
app.get("*", async (c) => {
|
|
320
|
+
const url = new URL(c.req.url, "http://localhost").pathname;
|
|
321
|
+
if (url.startsWith("/api/") || url.startsWith("/assets/") || url.startsWith("/@") || url.startsWith("/node_modules/") || url.includes(".")) {
|
|
322
|
+
return c.notFound();
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
let render;
|
|
326
|
+
if (vite) {
|
|
327
|
+
const mod = await vite.ssrLoadModule(entryServer);
|
|
328
|
+
render = mod.render;
|
|
329
|
+
} else if (process.env.NODE_ENV === "production") {
|
|
330
|
+
const ssrBundlePath = (0, import_node_path3.resolve)(process.cwd(), "dist/server/entry-server.js");
|
|
331
|
+
const mod = await import(ssrBundlePath);
|
|
332
|
+
render = mod.render;
|
|
333
|
+
} else {
|
|
334
|
+
const mod = await import(
|
|
335
|
+
/* @vite-ignore */
|
|
336
|
+
(0, import_node_path3.resolve)(process.cwd(), entryServer)
|
|
337
|
+
);
|
|
338
|
+
render = mod.render;
|
|
339
|
+
}
|
|
340
|
+
const appStream = await render(url);
|
|
341
|
+
const publicEnv = getPublicEnv();
|
|
342
|
+
let clientScript;
|
|
343
|
+
let cssLinks = "";
|
|
344
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
345
|
+
if (!isProd) {
|
|
346
|
+
clientScript = `/${entryClient}`;
|
|
347
|
+
} else {
|
|
348
|
+
if (!manifest) {
|
|
349
|
+
const manifestPath = (0, import_node_path3.resolve)(process.cwd(), "dist/client/.vite/manifest.json");
|
|
350
|
+
if ((0, import_node_fs.existsSync)(manifestPath)) {
|
|
351
|
+
manifest = JSON.parse((0, import_node_fs.readFileSync)(manifestPath, "utf-8"));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const entry = manifest?.[entryClient];
|
|
355
|
+
clientScript = entry ? `/${entry.file}` : "/assets/entry-client.js";
|
|
356
|
+
if (entry?.css) {
|
|
357
|
+
cssLinks = entry.css.map((css) => ` <link rel="stylesheet" href="/${css}" />`).join("\n");
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const htmlHead = `<!DOCTYPE html>
|
|
361
|
+
<html lang="en">
|
|
362
|
+
<head>
|
|
363
|
+
<meta charset="UTF-8" />
|
|
364
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
365
|
+
<title>${title}</title>
|
|
366
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
367
|
+
<link rel="manifest" href="/site.webmanifest" />
|
|
368
|
+
<meta name="theme-color" content="#0a0a0a" />
|
|
369
|
+
${!isProd ? ' <script type="module" src="/@vite/client"></script>' : ""}
|
|
370
|
+
${cssLinks}
|
|
371
|
+
<script>window.__VOLTX_ENV__ = ${JSON.stringify(publicEnv)}</script>
|
|
372
|
+
</head>
|
|
373
|
+
<body>
|
|
374
|
+
<div id="root">`;
|
|
375
|
+
const htmlTail = `</div>
|
|
376
|
+
<script type="module" src="${clientScript}"></script>
|
|
377
|
+
</body>
|
|
378
|
+
</html>`;
|
|
379
|
+
const { readable, writable } = new TransformStream();
|
|
380
|
+
const writer = writable.getWriter();
|
|
381
|
+
const encoder = new TextEncoder();
|
|
382
|
+
(async () => {
|
|
383
|
+
try {
|
|
384
|
+
await writer.write(encoder.encode(htmlHead));
|
|
385
|
+
const reader = appStream.getReader();
|
|
386
|
+
while (true) {
|
|
387
|
+
const { done, value } = await reader.read();
|
|
388
|
+
if (done) break;
|
|
389
|
+
await writer.write(value);
|
|
390
|
+
}
|
|
391
|
+
await writer.write(encoder.encode(htmlTail));
|
|
392
|
+
} catch (err) {
|
|
393
|
+
console.error("[voltx] SSR stream error:", err);
|
|
394
|
+
} finally {
|
|
395
|
+
await writer.close();
|
|
396
|
+
}
|
|
397
|
+
})();
|
|
398
|
+
return new Response(readable, {
|
|
399
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
400
|
+
});
|
|
401
|
+
} catch (err) {
|
|
402
|
+
if (vite) vite.ssrFixStacktrace(err);
|
|
403
|
+
console.error("[voltx] SSR render error:", err);
|
|
404
|
+
return c.text("Internal Server Error", 500);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// src/index.ts
|
|
274
410
|
var VERSION = "0.3.0";
|
|
275
411
|
// Annotate the CommonJS export names for ESM import in node:
|
|
276
412
|
0 && (module.exports = {
|
|
@@ -280,7 +416,9 @@ var VERSION = "0.3.0";
|
|
|
280
416
|
createErrorHandler,
|
|
281
417
|
createLoggerMiddleware,
|
|
282
418
|
createServer,
|
|
419
|
+
createViteDevConfig,
|
|
283
420
|
filePathToUrlPath,
|
|
421
|
+
registerSSR,
|
|
284
422
|
registerStaticFiles,
|
|
285
423
|
scanAndRegisterRoutes
|
|
286
424
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -7,7 +7,7 @@ interface ServerConfig {
|
|
|
7
7
|
port?: number;
|
|
8
8
|
/** Hostname to bind to (default: "0.0.0.0") */
|
|
9
9
|
hostname?: string;
|
|
10
|
-
/** Directory to scan for file-based routes (default: "
|
|
10
|
+
/** Directory to scan for file-based routes (default: "api") */
|
|
11
11
|
routesDir?: string;
|
|
12
12
|
/** Directory for static files (default: "public") */
|
|
13
13
|
staticDir?: string;
|
|
@@ -90,7 +90,7 @@ interface VoltxServer {
|
|
|
90
90
|
*
|
|
91
91
|
* const server = createServer({
|
|
92
92
|
* port: 3000,
|
|
93
|
-
* routesDir: "
|
|
93
|
+
* routesDir: "api",
|
|
94
94
|
* cors: true,
|
|
95
95
|
* logger: true,
|
|
96
96
|
* });
|
|
@@ -111,11 +111,11 @@ declare function scanAndRegisterRoutes(app: Hono, routesDir: string): Promise<Ro
|
|
|
111
111
|
/**
|
|
112
112
|
* Convert a file path to a URL path.
|
|
113
113
|
*
|
|
114
|
-
* Examples:
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
114
|
+
* Examples (routesDir = "api"):
|
|
115
|
+
* api/index.ts → /api
|
|
116
|
+
* api/chat.ts → /api/chat
|
|
117
|
+
* api/users/[id].ts → /api/users/:id
|
|
118
|
+
* api/[...slug].ts → /api/*
|
|
119
119
|
*/
|
|
120
120
|
declare function filePathToUrlPath(filePath: string, routesDir: string): string;
|
|
121
121
|
|
|
@@ -145,6 +145,60 @@ declare function createLoggerMiddleware(): hono.MiddlewareHandler;
|
|
|
145
145
|
*/
|
|
146
146
|
declare function createErrorHandler(customHandler?: (err: Error, c: Context) => Response | Promise<Response>): (err: Error, c: Context) => Promise<Response>;
|
|
147
147
|
|
|
148
|
+
interface ViteDevOptions {
|
|
149
|
+
/** Project root directory (default: process.cwd()) */
|
|
150
|
+
root?: string;
|
|
151
|
+
/** Hono server entry file (default: src/index.ts) */
|
|
152
|
+
entry?: string;
|
|
153
|
+
/** Frontend entry for client-side hydration */
|
|
154
|
+
entryClient?: string;
|
|
155
|
+
/** Frontend entry for SSR rendering */
|
|
156
|
+
entryServer?: string;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Create a Vite config for full-stack development with Hono.
|
|
160
|
+
*
|
|
161
|
+
* Uses @hono/vite-dev-server to embed Vite inside Hono.
|
|
162
|
+
* The Hono app handles API routes, Vite handles frontend assets + HMR.
|
|
163
|
+
*
|
|
164
|
+
* This function returns a Vite config object that can be written to
|
|
165
|
+
* a temporary vite.config.ts or passed to Vite's createServer API.
|
|
166
|
+
*/
|
|
167
|
+
declare function createViteDevConfig(options?: ViteDevOptions): {
|
|
168
|
+
root: string;
|
|
169
|
+
server: {
|
|
170
|
+
hmr: boolean;
|
|
171
|
+
};
|
|
172
|
+
plugins: never[];
|
|
173
|
+
ssr: {
|
|
174
|
+
external: string[];
|
|
175
|
+
};
|
|
176
|
+
entry: string;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
interface SSROptions {
|
|
180
|
+
/** Path to entry-server module (default: src/entry-server.tsx) */
|
|
181
|
+
entryServer?: string;
|
|
182
|
+
/** Path to entry-client module (default: src/entry-client.tsx) */
|
|
183
|
+
entryClient?: string;
|
|
184
|
+
/** App title (default: "VoltX App") */
|
|
185
|
+
title?: string;
|
|
186
|
+
}
|
|
187
|
+
/** Vite dev server shape — minimal interface to avoid hard dep on vite */
|
|
188
|
+
interface ViteDevServer {
|
|
189
|
+
ssrLoadModule(url: string): Promise<Record<string, unknown>>;
|
|
190
|
+
ssrFixStacktrace(e: Error): void;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Register SSR catch-all handler on a Hono app.
|
|
194
|
+
* Must be registered AFTER API routes — it catches all non-API GET requests
|
|
195
|
+
* and renders the React app server-side with streaming.
|
|
196
|
+
*
|
|
197
|
+
* In dev: loads entry-server via Vite's ssrLoadModule (HMR-aware).
|
|
198
|
+
* In prod: loads pre-built SSR bundle + reads Vite manifest for asset paths.
|
|
199
|
+
*/
|
|
200
|
+
declare function registerSSR(app: Hono, vite: ViteDevServer | null, options?: SSROptions): void;
|
|
201
|
+
|
|
148
202
|
declare const VERSION = "0.3.0";
|
|
149
203
|
|
|
150
|
-
export { type CorsConfig, type HttpMethod, type MiddlewareHandler, type RouteEntry, type RouteHandler, type RouteModule, type ServerConfig, type ServerInfo, VERSION, type VoltxServer, createCorsMiddleware, createErrorHandler, createLoggerMiddleware, createServer, filePathToUrlPath, registerStaticFiles, scanAndRegisterRoutes };
|
|
204
|
+
export { type CorsConfig, type HttpMethod, type MiddlewareHandler, type RouteEntry, type RouteHandler, type RouteModule, type SSROptions, type ServerConfig, type ServerInfo, VERSION, type ViteDevOptions, type VoltxServer, createCorsMiddleware, createErrorHandler, createLoggerMiddleware, createServer, createViteDevConfig, filePathToUrlPath, registerSSR, registerStaticFiles, scanAndRegisterRoutes };
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ interface ServerConfig {
|
|
|
7
7
|
port?: number;
|
|
8
8
|
/** Hostname to bind to (default: "0.0.0.0") */
|
|
9
9
|
hostname?: string;
|
|
10
|
-
/** Directory to scan for file-based routes (default: "
|
|
10
|
+
/** Directory to scan for file-based routes (default: "api") */
|
|
11
11
|
routesDir?: string;
|
|
12
12
|
/** Directory for static files (default: "public") */
|
|
13
13
|
staticDir?: string;
|
|
@@ -90,7 +90,7 @@ interface VoltxServer {
|
|
|
90
90
|
*
|
|
91
91
|
* const server = createServer({
|
|
92
92
|
* port: 3000,
|
|
93
|
-
* routesDir: "
|
|
93
|
+
* routesDir: "api",
|
|
94
94
|
* cors: true,
|
|
95
95
|
* logger: true,
|
|
96
96
|
* });
|
|
@@ -111,11 +111,11 @@ declare function scanAndRegisterRoutes(app: Hono, routesDir: string): Promise<Ro
|
|
|
111
111
|
/**
|
|
112
112
|
* Convert a file path to a URL path.
|
|
113
113
|
*
|
|
114
|
-
* Examples:
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
114
|
+
* Examples (routesDir = "api"):
|
|
115
|
+
* api/index.ts → /api
|
|
116
|
+
* api/chat.ts → /api/chat
|
|
117
|
+
* api/users/[id].ts → /api/users/:id
|
|
118
|
+
* api/[...slug].ts → /api/*
|
|
119
119
|
*/
|
|
120
120
|
declare function filePathToUrlPath(filePath: string, routesDir: string): string;
|
|
121
121
|
|
|
@@ -145,6 +145,60 @@ declare function createLoggerMiddleware(): hono.MiddlewareHandler;
|
|
|
145
145
|
*/
|
|
146
146
|
declare function createErrorHandler(customHandler?: (err: Error, c: Context) => Response | Promise<Response>): (err: Error, c: Context) => Promise<Response>;
|
|
147
147
|
|
|
148
|
+
interface ViteDevOptions {
|
|
149
|
+
/** Project root directory (default: process.cwd()) */
|
|
150
|
+
root?: string;
|
|
151
|
+
/** Hono server entry file (default: src/index.ts) */
|
|
152
|
+
entry?: string;
|
|
153
|
+
/** Frontend entry for client-side hydration */
|
|
154
|
+
entryClient?: string;
|
|
155
|
+
/** Frontend entry for SSR rendering */
|
|
156
|
+
entryServer?: string;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Create a Vite config for full-stack development with Hono.
|
|
160
|
+
*
|
|
161
|
+
* Uses @hono/vite-dev-server to embed Vite inside Hono.
|
|
162
|
+
* The Hono app handles API routes, Vite handles frontend assets + HMR.
|
|
163
|
+
*
|
|
164
|
+
* This function returns a Vite config object that can be written to
|
|
165
|
+
* a temporary vite.config.ts or passed to Vite's createServer API.
|
|
166
|
+
*/
|
|
167
|
+
declare function createViteDevConfig(options?: ViteDevOptions): {
|
|
168
|
+
root: string;
|
|
169
|
+
server: {
|
|
170
|
+
hmr: boolean;
|
|
171
|
+
};
|
|
172
|
+
plugins: never[];
|
|
173
|
+
ssr: {
|
|
174
|
+
external: string[];
|
|
175
|
+
};
|
|
176
|
+
entry: string;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
interface SSROptions {
|
|
180
|
+
/** Path to entry-server module (default: src/entry-server.tsx) */
|
|
181
|
+
entryServer?: string;
|
|
182
|
+
/** Path to entry-client module (default: src/entry-client.tsx) */
|
|
183
|
+
entryClient?: string;
|
|
184
|
+
/** App title (default: "VoltX App") */
|
|
185
|
+
title?: string;
|
|
186
|
+
}
|
|
187
|
+
/** Vite dev server shape — minimal interface to avoid hard dep on vite */
|
|
188
|
+
interface ViteDevServer {
|
|
189
|
+
ssrLoadModule(url: string): Promise<Record<string, unknown>>;
|
|
190
|
+
ssrFixStacktrace(e: Error): void;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Register SSR catch-all handler on a Hono app.
|
|
194
|
+
* Must be registered AFTER API routes — it catches all non-API GET requests
|
|
195
|
+
* and renders the React app server-side with streaming.
|
|
196
|
+
*
|
|
197
|
+
* In dev: loads entry-server via Vite's ssrLoadModule (HMR-aware).
|
|
198
|
+
* In prod: loads pre-built SSR bundle + reads Vite manifest for asset paths.
|
|
199
|
+
*/
|
|
200
|
+
declare function registerSSR(app: Hono, vite: ViteDevServer | null, options?: SSROptions): void;
|
|
201
|
+
|
|
148
202
|
declare const VERSION = "0.3.0";
|
|
149
203
|
|
|
150
|
-
export { type CorsConfig, type HttpMethod, type MiddlewareHandler, type RouteEntry, type RouteHandler, type RouteModule, type ServerConfig, type ServerInfo, VERSION, type VoltxServer, createCorsMiddleware, createErrorHandler, createLoggerMiddleware, createServer, filePathToUrlPath, registerStaticFiles, scanAndRegisterRoutes };
|
|
204
|
+
export { type CorsConfig, type HttpMethod, type MiddlewareHandler, type RouteEntry, type RouteHandler, type RouteModule, type SSROptions, type ServerConfig, type ServerInfo, VERSION, type ViteDevOptions, type VoltxServer, createCorsMiddleware, createErrorHandler, createLoggerMiddleware, createServer, createViteDevConfig, filePathToUrlPath, registerSSR, registerStaticFiles, scanAndRegisterRoutes };
|
package/dist/index.js
CHANGED
|
@@ -127,13 +127,16 @@ function filePathToUrlPath(filePath, routesDir) {
|
|
|
127
127
|
const ext = extname(rel);
|
|
128
128
|
rel = rel.slice(0, -ext.length);
|
|
129
129
|
rel = rel.replace(/\\/g, "/");
|
|
130
|
-
if (rel === "index")
|
|
131
|
-
if (rel.endsWith("/index")) {
|
|
130
|
+
if (rel === "index") rel = "";
|
|
131
|
+
else if (rel.endsWith("/index")) {
|
|
132
132
|
rel = rel.slice(0, -"/index".length);
|
|
133
133
|
}
|
|
134
134
|
rel = rel.replace(/\[([^\]\.]+)\]/g, ":$1");
|
|
135
135
|
rel = rel.replace(/\[\.\.\.([^\]]+)\]/g, "*");
|
|
136
|
-
|
|
136
|
+
const dirBasename = routesDir.replace(/\\/g, "/").split("/").pop() ?? "";
|
|
137
|
+
const prefix = dirBasename && dirBasename !== "routes" && dirBasename !== "src" ? `/${dirBasename}` : "";
|
|
138
|
+
if (!rel) return prefix || "/";
|
|
139
|
+
return `${prefix}/${rel}`;
|
|
137
140
|
}
|
|
138
141
|
async function importRouteModule(filePath) {
|
|
139
142
|
try {
|
|
@@ -156,7 +159,7 @@ function createServer(config = {}) {
|
|
|
156
159
|
const {
|
|
157
160
|
port = Number(process.env.PORT) || 3e3,
|
|
158
161
|
hostname = "0.0.0.0",
|
|
159
|
-
routesDir = "
|
|
162
|
+
routesDir = "api",
|
|
160
163
|
staticDir = "public",
|
|
161
164
|
cors = true,
|
|
162
165
|
logger: enableLogger = process.env.NODE_ENV !== "production",
|
|
@@ -212,10 +215,10 @@ function createServer(config = {}) {
|
|
|
212
215
|
},
|
|
213
216
|
async stop() {
|
|
214
217
|
if (httpServer) {
|
|
215
|
-
await new Promise((
|
|
218
|
+
await new Promise((resolve3, reject) => {
|
|
216
219
|
httpServer.close((err) => {
|
|
217
220
|
if (err) reject(err);
|
|
218
|
-
else
|
|
221
|
+
else resolve3();
|
|
219
222
|
});
|
|
220
223
|
});
|
|
221
224
|
httpServer = null;
|
|
@@ -237,6 +240,137 @@ function createServer(config = {}) {
|
|
|
237
240
|
|
|
238
241
|
// src/index.ts
|
|
239
242
|
import { Hono as Hono2 } from "hono";
|
|
243
|
+
|
|
244
|
+
// src/vite.ts
|
|
245
|
+
function createViteDevConfig(options = {}) {
|
|
246
|
+
const {
|
|
247
|
+
root = process.cwd(),
|
|
248
|
+
entry = "src/index.ts"
|
|
249
|
+
} = options;
|
|
250
|
+
return {
|
|
251
|
+
root,
|
|
252
|
+
server: {
|
|
253
|
+
// Let Hono handle the port — Vite runs in middleware mode via the plugin
|
|
254
|
+
hmr: true
|
|
255
|
+
},
|
|
256
|
+
plugins: [],
|
|
257
|
+
// Plugins are added dynamically when starting
|
|
258
|
+
// Externalize Node.js packages for SSR
|
|
259
|
+
ssr: {
|
|
260
|
+
external: ["react", "react-dom"]
|
|
261
|
+
},
|
|
262
|
+
entry
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/ssr.ts
|
|
267
|
+
import { resolve as resolve2 } from "path";
|
|
268
|
+
import { readFileSync, existsSync } from "fs";
|
|
269
|
+
function getPublicEnv() {
|
|
270
|
+
const publicEnv = {};
|
|
271
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
272
|
+
if (key.startsWith("VOLTX_PUBLIC_") && value !== void 0) {
|
|
273
|
+
publicEnv[key] = value;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return publicEnv;
|
|
277
|
+
}
|
|
278
|
+
function registerSSR(app, vite, options = {}) {
|
|
279
|
+
const entryServer = options.entryServer ?? "src/entry-server.tsx";
|
|
280
|
+
const entryClient = options.entryClient ?? "src/entry-client.tsx";
|
|
281
|
+
const title = options.title ?? "VoltX App";
|
|
282
|
+
let manifest = null;
|
|
283
|
+
app.get("*", async (c) => {
|
|
284
|
+
const url = new URL(c.req.url, "http://localhost").pathname;
|
|
285
|
+
if (url.startsWith("/api/") || url.startsWith("/assets/") || url.startsWith("/@") || url.startsWith("/node_modules/") || url.includes(".")) {
|
|
286
|
+
return c.notFound();
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
let render;
|
|
290
|
+
if (vite) {
|
|
291
|
+
const mod = await vite.ssrLoadModule(entryServer);
|
|
292
|
+
render = mod.render;
|
|
293
|
+
} else if (process.env.NODE_ENV === "production") {
|
|
294
|
+
const ssrBundlePath = resolve2(process.cwd(), "dist/server/entry-server.js");
|
|
295
|
+
const mod = await import(ssrBundlePath);
|
|
296
|
+
render = mod.render;
|
|
297
|
+
} else {
|
|
298
|
+
const mod = await import(
|
|
299
|
+
/* @vite-ignore */
|
|
300
|
+
resolve2(process.cwd(), entryServer)
|
|
301
|
+
);
|
|
302
|
+
render = mod.render;
|
|
303
|
+
}
|
|
304
|
+
const appStream = await render(url);
|
|
305
|
+
const publicEnv = getPublicEnv();
|
|
306
|
+
let clientScript;
|
|
307
|
+
let cssLinks = "";
|
|
308
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
309
|
+
if (!isProd) {
|
|
310
|
+
clientScript = `/${entryClient}`;
|
|
311
|
+
} else {
|
|
312
|
+
if (!manifest) {
|
|
313
|
+
const manifestPath = resolve2(process.cwd(), "dist/client/.vite/manifest.json");
|
|
314
|
+
if (existsSync(manifestPath)) {
|
|
315
|
+
manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const entry = manifest?.[entryClient];
|
|
319
|
+
clientScript = entry ? `/${entry.file}` : "/assets/entry-client.js";
|
|
320
|
+
if (entry?.css) {
|
|
321
|
+
cssLinks = entry.css.map((css) => ` <link rel="stylesheet" href="/${css}" />`).join("\n");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const htmlHead = `<!DOCTYPE html>
|
|
325
|
+
<html lang="en">
|
|
326
|
+
<head>
|
|
327
|
+
<meta charset="UTF-8" />
|
|
328
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
329
|
+
<title>${title}</title>
|
|
330
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
331
|
+
<link rel="manifest" href="/site.webmanifest" />
|
|
332
|
+
<meta name="theme-color" content="#0a0a0a" />
|
|
333
|
+
${!isProd ? ' <script type="module" src="/@vite/client"></script>' : ""}
|
|
334
|
+
${cssLinks}
|
|
335
|
+
<script>window.__VOLTX_ENV__ = ${JSON.stringify(publicEnv)}</script>
|
|
336
|
+
</head>
|
|
337
|
+
<body>
|
|
338
|
+
<div id="root">`;
|
|
339
|
+
const htmlTail = `</div>
|
|
340
|
+
<script type="module" src="${clientScript}"></script>
|
|
341
|
+
</body>
|
|
342
|
+
</html>`;
|
|
343
|
+
const { readable, writable } = new TransformStream();
|
|
344
|
+
const writer = writable.getWriter();
|
|
345
|
+
const encoder = new TextEncoder();
|
|
346
|
+
(async () => {
|
|
347
|
+
try {
|
|
348
|
+
await writer.write(encoder.encode(htmlHead));
|
|
349
|
+
const reader = appStream.getReader();
|
|
350
|
+
while (true) {
|
|
351
|
+
const { done, value } = await reader.read();
|
|
352
|
+
if (done) break;
|
|
353
|
+
await writer.write(value);
|
|
354
|
+
}
|
|
355
|
+
await writer.write(encoder.encode(htmlTail));
|
|
356
|
+
} catch (err) {
|
|
357
|
+
console.error("[voltx] SSR stream error:", err);
|
|
358
|
+
} finally {
|
|
359
|
+
await writer.close();
|
|
360
|
+
}
|
|
361
|
+
})();
|
|
362
|
+
return new Response(readable, {
|
|
363
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
364
|
+
});
|
|
365
|
+
} catch (err) {
|
|
366
|
+
if (vite) vite.ssrFixStacktrace(err);
|
|
367
|
+
console.error("[voltx] SSR render error:", err);
|
|
368
|
+
return c.text("Internal Server Error", 500);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/index.ts
|
|
240
374
|
var VERSION = "0.3.0";
|
|
241
375
|
export {
|
|
242
376
|
Hono2 as Hono,
|
|
@@ -245,7 +379,9 @@ export {
|
|
|
245
379
|
createErrorHandler,
|
|
246
380
|
createLoggerMiddleware,
|
|
247
381
|
createServer,
|
|
382
|
+
createViteDevConfig,
|
|
248
383
|
filePathToUrlPath,
|
|
384
|
+
registerSSR,
|
|
249
385
|
registerStaticFiles,
|
|
250
386
|
scanAndRegisterRoutes
|
|
251
387
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltx/server",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "VoltX Server — Hono-based HTTP server with file-based routing, SSE streaming, and static file serving",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -23,9 +23,19 @@
|
|
|
23
23
|
"hono": "^4.7.0",
|
|
24
24
|
"@hono/node-server": "^1.14.0"
|
|
25
25
|
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"react": ">=18.0.0",
|
|
28
|
+
"react-dom": ">=18.0.0"
|
|
29
|
+
},
|
|
30
|
+
"peerDependenciesMeta": {
|
|
31
|
+
"react": { "optional": true },
|
|
32
|
+
"react-dom": { "optional": true }
|
|
33
|
+
},
|
|
26
34
|
"devDependencies": {
|
|
27
35
|
"tsup": "^8.0.0",
|
|
28
|
-
"typescript": "^5.7.0"
|
|
36
|
+
"typescript": "^5.7.0",
|
|
37
|
+
"@types/react": "^19.0.0",
|
|
38
|
+
"@types/react-dom": "^19.0.0"
|
|
29
39
|
},
|
|
30
40
|
"keywords": ["voltx", "server", "hono", "http", "routing", "file-based", "sse", "streaming", "middleware"],
|
|
31
41
|
"license": "MIT",
|