@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 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 (Next.js-style), CORS, logging, error handling, and static file serving out of the box.
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 { createServer } from "@voltx/server";
25
+ import { Hono } from "hono";
26
+ import { registerSSR } from "@voltx/server";
26
27
 
27
- const server = createServer({
28
- port: 3000,
29
- routesDir: "src/routes",
30
- cors: true,
31
- logger: true,
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
- await server.start();
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 `src/routes/` and they become API endpoints automatically:
70
+ Drop files in `api/` and they become API endpoints:
41
71
 
42
72
  ```
43
- src/routes/index.ts → GET /
44
- src/routes/api/chat.ts → POST /api/chat
45
- src/routes/api/users/[id].ts → GET /api/users/:id
46
- src/routes/api/[...slug].ts → /api/* (catch-all)
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
- - **File-based routing** — Next.js-style, zero config
68
- - **Dynamic routes** — `[param]` → `:param`, `[...slug]` → catch-all
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** — Serves from `public/` directory
71
- - **Per-route middleware** — Export `middleware` from any route file
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") return "/";
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
- return "/" + rel;
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 = "src/routes",
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((resolve2, reject) => {
254
+ await new Promise((resolve3, reject) => {
250
255
  httpServer.close((err) => {
251
256
  if (err) reject(err);
252
- else resolve2();
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: "src/routes") */
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: "src/routes",
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
- * routes/index.ts → /
116
- * routes/api/chat.ts → /api/chat
117
- * routes/api/users/[id].ts → /api/users/:id
118
- * routes/api/[...slug].ts → /api/*
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: "src/routes") */
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: "src/routes",
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
- * routes/index.ts → /
116
- * routes/api/chat.ts → /api/chat
117
- * routes/api/users/[id].ts → /api/users/:id
118
- * routes/api/[...slug].ts → /api/*
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") return "/";
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
- return "/" + rel;
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 = "src/routes",
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((resolve2, reject) => {
218
+ await new Promise((resolve3, reject) => {
216
219
  httpServer.close((err) => {
217
220
  if (err) reject(err);
218
- else resolve2();
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.0",
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",