@soybeanjs/hono-ssr 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SoybeanJS Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md ADDED
@@ -0,0 +1,253 @@
1
+ # @soybeanjs/hono-ssr
2
+
3
+ <p align="center">
4
+ <strong>Hono SSR Vite Plugin — Batteries-included Full-stack SSR</strong>
5
+ </p>
6
+
7
+ <p align="center">
8
+ File-based Routing · Type-safe · Multi-platform · Developer Experience First
9
+ </p>
10
+
11
+ ---
12
+
13
+ [中文](./README.md) | English
14
+
15
+ ## Overview
16
+
17
+ `@soybeanjs/hono-ssr` is a Vite SSR plugin for [Hono](https://hono.dev), delivering a batteries-included full-stack development experience. It integrates **file-based routing**, **client & server builds**, **SSR manifest management**, and **multi-platform deployment adapters** — so you can focus on building your application, not configuring build tools.
18
+
19
+ ## Features
20
+
21
+ - 🗂️ **File-based Routing** — Auto-scan `server/api/` and `server/routes/` for API endpoints
22
+ - 🔒 **Type-safe Route Definitions** — Full type inference and validation via `createDefineRoute()`
23
+ - 🏗️ **Dual Build Pipeline** — Handles client and server bundles with automatic manifest generation
24
+ - 🎯 **Virtual Module Manifest** — `virtual:hono-ssr-manifest` inlines the asset manifest at build time, with automatic dev/prod switching
25
+ - ☁️ **Multi-platform** — Cloudflare Workers / Pages, Node.js, Bun, Deno, Vercel, Netlify
26
+ - 🔥 **HMR Dev Server** — Powered by `@hono/vite-dev-server` with Cloudflare bindings simulation
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ pnpm add @soybeanjs/hono-ssr
32
+ ```
33
+
34
+ Peer dependencies:
35
+
36
+ ```bash
37
+ pnpm add hono vite
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ### 1. Configure the Vite Plugin
43
+
44
+ ```ts
45
+ // vite.config.ts
46
+ import { defineConfig } from 'vite';
47
+ import { HonoSSR } from '@soybeanjs/hono-ssr/vite';
48
+
49
+ export default defineConfig({
50
+ plugins: [
51
+ HonoSSR({
52
+ serverEntry: 'server/app.ts',
53
+ clientEntry: 'app/entry-client.ts',
54
+ buildType: 'cloudflare-workers'
55
+ })
56
+ ]
57
+ });
58
+ ```
59
+
60
+ ### 2. Server Entry
61
+
62
+ ```ts
63
+ // server/app.ts
64
+ import { Hono } from 'hono';
65
+ import { setupFileRoutes } from '@soybeanjs/hono-ssr';
66
+ import { resolveManifest } from 'virtual:hono-ssr-manifest';
67
+
68
+ const app = new Hono();
69
+
70
+ // Register file-based routes
71
+ setupFileRoutes({
72
+ prefix: '/api',
73
+ onRouteRegister: route => {
74
+ app.on(route.method, route.path, ...route.handlers);
75
+ }
76
+ });
77
+
78
+ // Register file-based routes with auth middleware example
79
+ setupFileRoutes({
80
+ prefix: '/api',
81
+ onRouteRegister: route => {
82
+ if (route.meta?.requiresAuth) {
83
+ app.on(route.method, route.path, authMiddleware, ...route.handlers);
84
+ } else {
85
+ app.on(route.method, route.path, ...route.handlers);
86
+ }
87
+ }
88
+ });
89
+
90
+ // SSR rendering
91
+ app.get('*', async c => {
92
+ const { scripts, styles } = resolveManifest();
93
+ return c.html(`
94
+ <!DOCTYPE html>
95
+ <html>
96
+ <head>${styles}</head>
97
+ <body>
98
+ <div id="app"></div>
99
+ ${scripts}
100
+ </body>
101
+ </html>
102
+ `);
103
+ });
104
+
105
+ export default app;
106
+ ```
107
+
108
+ ### 3. Define API Routes
109
+
110
+ ```ts
111
+ // server/api/users.ts
112
+ import { createDefineRoute } from '@soybeanjs/hono-ssr/route';
113
+ import { z } from 'zod';
114
+
115
+ const defineRoute = createDefineRoute<{ Bindings: { DB: D1Database } }>();
116
+
117
+ export const GET = defineRoute({
118
+ handlers: [
119
+ async c => {
120
+ const users = await c.env.DB.prepare('SELECT * FROM users').all();
121
+ return c.json(users.results);
122
+ }
123
+ ]
124
+ });
125
+
126
+ export const POST = defineRoute({
127
+ handlers: [
128
+ zValidator('json', z.object({ name: z.string() })),
129
+ async c => {
130
+ const { name } = c.req.valid('json');
131
+ return c.json({ id: 1, name }, 201);
132
+ }
133
+ ]
134
+ });
135
+ ```
136
+
137
+ ### 4. Client Entry
138
+
139
+ ```ts
140
+ // app/entry-client.ts
141
+ import { createApp } from './main';
142
+
143
+ const app = createApp();
144
+ app.mount('#app');
145
+ ```
146
+
147
+ ## Export Paths
148
+
149
+ | Path | Description |
150
+ | --------------------------- | ---------------------------------------------------------------------------- |
151
+ | `@soybeanjs/hono-ssr` | Main entry: `setupFileRoutes`, `getFilesRoutes`, type definitions |
152
+ | `@soybeanjs/hono-ssr/route` | Route utilities: `createDefineRoute` (**must use this path in route files**) |
153
+ | `@soybeanjs/hono-ssr/vite` | Vite plugin: `HonoSSR` |
154
+ | `@soybeanjs/hono-ssr/types` | TypeScript type declarations |
155
+
156
+ > **Important**: `createDefineRoute` must be imported from `@soybeanjs/hono-ssr/route`, **not** from the main entry. Importing from the main entry causes a circular dependency that leads to the SSR error `Cannot access '__vite_ssr_import_1__' before initialization`.
157
+
158
+ ## API Reference
159
+
160
+ ### `HonoSSR(options)`
161
+
162
+ Vite plugin factory.
163
+
164
+ ```ts
165
+ interface HonoSSRPluginOptions<T extends HonoSSRBuildType = HonoSSRBuildType> {
166
+ /** Server entry file @default 'server/app.ts' */
167
+ serverEntry?: string;
168
+ /** Client entry file @default 'app/entry-client.ts' */
169
+ clientEntry?: string;
170
+ /** File-based routing options */
171
+ fileRoute?: HonoSSRFileRouteOptions;
172
+ /** @hono/vite-dev-server options */
173
+ devServer?: DevServerOptions;
174
+ /** Dev server exclude patterns @default [/^\/app\/.+/] */
175
+ devServerExclude?: (string | RegExp)[];
176
+ /** Deployment target */
177
+ buildType?: 'cloudflare-workers' | 'cloudflare-pages' | 'node' | 'bun' | 'deno' | 'vercel' | 'netlify-functions';
178
+ /** Build options forwarded to @hono/vite-build */
179
+ buildOptions?: NodeBuildOptions | BunBuildOptions | CloudflareWorkersBuildOptions | ...;
180
+ /** Cloudflare Platform Proxy options (simulate bindings in dev) */
181
+ platformProxyOptions?: GetPlatformProxyOptions;
182
+ }
183
+ ```
184
+
185
+ ### `setupFileRoutes(options?, onRouteRegister?)`
186
+
187
+ Scans file-based routes and returns route records.
188
+
189
+ ```ts
190
+ function setupFileRoutes<Meta = RouteMeta>(
191
+ options?: SetupFileRoutesOptions,
192
+ onRouteRegister?: (route: RouteRecord<Meta>) => void
193
+ ): RouteRecord<Meta>[];
194
+ ```
195
+
196
+ ### `createDefineRoute<Env, Meta>()`
197
+
198
+ Creates a type-safe route definer.
199
+
200
+ ```ts
201
+ const defineRoute = createDefineRoute<{ Bindings: Env }>();
202
+
203
+ // Supports 1–7 middleware/handler functions
204
+ export const GET = defineRoute({
205
+ handlers: [middleware1, middleware2, handler],
206
+ meta: { description: 'Get user list' }
207
+ });
208
+ ```
209
+
210
+ ### Virtual Modules
211
+
212
+ | Module ID | Exports | Description |
213
+ | --------------------------- | ------------------------------ | --------------------------------------------------------------------------------- |
214
+ | `virtual:hono-file-routes` | `scannedRouteModules` | File-based route scan results |
215
+ | `virtual:hono-ssr-manifest` | `resolveManifest()`, `default` | SSR asset manifest — returns client entry path in dev, hashed build paths in prod |
216
+
217
+ ## Recommended Project Structure
218
+
219
+ ```
220
+ project/
221
+ ├── app/ # Client code
222
+ │ ├── entry-client.ts # Client entry
223
+ │ ├── main.ts # App factory
224
+ │ └── App.vue # Root component
225
+ ├── server/ # Server code
226
+ │ ├── app.ts # Hono server entry
227
+ │ ├── api/ # API routes (auto-scanned)
228
+ │ │ ├── users.ts
229
+ │ │ └── posts.ts
230
+ │ └── routes/ # Page routes (auto-scanned)
231
+ │ └── index.tsx
232
+ ├── vite.config.ts
233
+ ├── tsconfig.json
234
+ └── wrangler.toml # Cloudflare config
235
+ ```
236
+
237
+ ## Development
238
+
239
+ ```bash
240
+ # Build
241
+ pnpm build
242
+
243
+ # Type check
244
+ pnpm typecheck
245
+
246
+ # Lint & format
247
+ pnpm lint
248
+ pnpm fmt
249
+ ```
250
+
251
+ ## License
252
+
253
+ [MIT](./LICENSE)
package/README.md ADDED
@@ -0,0 +1,254 @@
1
+ # @soybeanjs/hono-ssr
2
+
3
+ <p align="center">
4
+ <strong>Hono SSR Vite 插件 — 开箱即用的全栈 SSR 方案</strong>
5
+ </p>
6
+
7
+ <p align="center">
8
+ 文件路由 · 类型安全 · 多平台部署 · 开发体验优先
9
+ </p>
10
+
11
+ ---
12
+
13
+ 中文 | [English](./README.en.md)
14
+
15
+ ## 简介
16
+
17
+ `@soybeanjs/hono-ssr` 是一个为 [Hono](https://hono.dev) 框架设计的 Vite SSR 插件,提供开箱即用的全栈开发体验。它整合了 **文件路由**、**客户端构建**、**SSR Manifest 管理**、**多平台部署适配** 等能力,让你可以专注于业务逻辑而非构建配置。
18
+
19
+ ### 特性
20
+
21
+ - 🗂️ **文件路由** — 基于文件系统的 API 路由,自动扫描 `server/api/` 和 `server/routes/` 目录
22
+ - 🔒 **类型安全路由定义** — 通过 `createDefineRoute()` 获得完整的类型推断和校验
23
+ - 🏗️ **客户端/服务端双构建** — 自动处理 client bundle 和 server bundle,生成 manifest.json
24
+ - 🎯 **虚拟模块 Manifest** — `virtual:hono-ssr-manifest` 在构建时内联资源清单,开发/生产环境自动切换
25
+ - ☁️ **多平台支持** — Cloudflare Workers / Pages、Node.js、Bun、Deno、Vercel、Netlify
26
+ - 🔥 **HMR 开发服务器** — 基于 `@hono/vite-dev-server`,支持 Cloudflare 绑定模拟
27
+
28
+ ### 安装
29
+
30
+ ```bash
31
+ pnpm add @soybeanjs/hono-ssr
32
+ ```
33
+
34
+ 需要安装 peer dependencies:
35
+
36
+ ```bash
37
+ pnpm add hono vite
38
+ ```
39
+
40
+ ### 快速开始
41
+
42
+ #### 1. 配置 Vite 插件
43
+
44
+ ```ts
45
+ // vite.config.ts
46
+ import { defineConfig } from 'vite';
47
+ import { HonoSSR } from '@soybeanjs/hono-ssr/vite';
48
+
49
+ export default defineConfig({
50
+ plugins: [
51
+ HonoSSR({
52
+ serverEntry: 'server/app.ts', // 服务端入口(默认)
53
+ clientEntry: 'app/entry-client.ts', // 客户端入口(默认)
54
+ buildType: 'cloudflare-workers' // 部署目标
55
+ })
56
+ ]
57
+ });
58
+ ```
59
+
60
+ #### 2. 编写服务端入口
61
+
62
+ ```ts
63
+ // server/app.ts
64
+ import { Hono } from 'hono';
65
+ import { setupFileRoutes } from '@soybeanjs/hono-ssr';
66
+ import { resolveManifest } from 'virtual:hono-ssr-manifest';
67
+
68
+ const app = new Hono();
69
+
70
+ // 注册文件路由
71
+ setupFileRoutes({
72
+ prefix: '/api',
73
+ onRouteRegister: route => {
74
+ app.on(route.method, route.path, ...route.handlers);
75
+ }
76
+ });
77
+
78
+ // 根据路由元数据进行文件路由注册
79
+ setupFileRoutes({
80
+ prefix: '/api',
81
+ onRouteRegister: route => {
82
+ if (route.meta?.requiresAuth) {
83
+ app.on(route.method, route.path, authMiddleware, ...route.handlers);
84
+ } else {
85
+ app.on(route.method, route.path, ...route.handlers);
86
+ }
87
+ }
88
+ });
89
+
90
+ // SSR 渲染
91
+ app.get('*', async c => {
92
+ const { scripts, styles } = resolveManifest();
93
+ return c.html(`
94
+ <!DOCTYPE html>
95
+ <html>
96
+ <head>${styles}</head>
97
+ <body>
98
+ <div id="app"></div>
99
+ ${scripts}
100
+ </body>
101
+ </html>
102
+ `);
103
+ });
104
+
105
+ export default app;
106
+ ```
107
+
108
+ #### 3. 创建 API 路由
109
+
110
+ ```ts
111
+ // server/api/users.ts
112
+ import { createDefineRoute } from '@soybeanjs/hono-ssr/route';
113
+ import { z } from 'zod';
114
+
115
+ const defineRoute = createDefineRoute<{ Bindings: { DB: D1Database } }>();
116
+
117
+ export const GET = defineRoute({
118
+ handlers: [
119
+ async c => {
120
+ const users = await c.env.DB.prepare('SELECT * FROM users').all();
121
+ return c.json(users.results);
122
+ }
123
+ ]
124
+ });
125
+
126
+ export const POST = defineRoute({
127
+ handlers: [
128
+ zValidator('json', z.object({ name: z.string() })),
129
+ async c => {
130
+ const { name } = c.req.valid('json');
131
+ // ... 创建用户
132
+ return c.json({ id: 1, name }, 201);
133
+ }
134
+ ]
135
+ });
136
+ ```
137
+
138
+ #### 4. 创建客户端入口
139
+
140
+ ```ts
141
+ // app/entry-client.ts
142
+ import { createApp } from './main';
143
+
144
+ const app = createApp();
145
+ app.mount('#app');
146
+ ```
147
+
148
+ ### 导出路径
149
+
150
+ | 路径 | 说明 |
151
+ | --------------------------- | ------------------------------------------------------------- |
152
+ | `@soybeanjs/hono-ssr` | 主入口:`setupFileRoutes`、`getFilesRoutes`、类型定义 |
153
+ | `@soybeanjs/hono-ssr/route` | 路由工具:`createDefineRoute`(**路由文件中必须使用此路径**) |
154
+ | `@soybeanjs/hono-ssr/vite` | Vite 插件:`HonoSSR` |
155
+ | `@soybeanjs/hono-ssr/types` | TypeScript 类型声明 |
156
+
157
+ > **注意**:`createDefineRoute` 必须从 `@soybeanjs/hono-ssr/route` 导入,**不要**从主入口导入,否则会产生循环依赖导致 SSR 报错 `Cannot access '__vite_ssr_import_1__' before initialization`。
158
+
159
+ ### API 参考
160
+
161
+ #### `HonoSSR(options)`
162
+
163
+ Vite 插件工厂函数。
164
+
165
+ ```ts
166
+ interface HonoSSRPluginOptions<T extends HonoSSRBuildType = HonoSSRBuildType> {
167
+ /** 服务端入口文件 @default 'server/app.ts' */
168
+ serverEntry?: string;
169
+ /** 客户端入口文件 @default 'app/entry-client.ts' */
170
+ clientEntry?: string;
171
+ /** 文件路由配置 */
172
+ fileRoute?: HonoSSRFileRouteOptions;
173
+ /** @hono/vite-dev-server 的配置 */
174
+ devServer?: DevServerOptions;
175
+ /** Dev Server 排除模式 @default [/^\/app\/.+/] */
176
+ devServerExclude?: (string | RegExp)[];
177
+ /** 部署目标 */
178
+ buildType?: 'cloudflare-workers' | 'cloudflare-pages' | 'node' | 'bun' | 'deno' | 'vercel' | 'netlify-functions';
179
+ /** 传递给 @hono/vite-build 的构建配置 */
180
+ buildOptions?: NodeBuildOptions | BunBuildOptions | CloudflareWorkersBuildOptions | ...;
181
+ /** Cloudflare Platform Proxy 配置(开发模式模拟绑定) */
182
+ platformProxyOptions?: GetPlatformProxyOptions;
183
+ }
184
+ ```
185
+
186
+ #### `setupFileRoutes(options?, onRouteRegister?)`
187
+
188
+ 扫描并返回文件路由记录。
189
+
190
+ ```ts
191
+ function setupFileRoutes<Meta = RouteMeta>(
192
+ options?: SetupFileRoutesOptions,
193
+ onRouteRegister?: (route: RouteRecord<Meta>) => void
194
+ ): RouteRecord<Meta>[];
195
+ ```
196
+
197
+ #### `createDefineRoute<Env, Meta>()`
198
+
199
+ 创建类型安全的路由定义器。
200
+
201
+ ```ts
202
+ const defineRoute = createDefineRoute<{ Bindings: Env }>();
203
+
204
+ // 支持 1–7 个中间件处理函数
205
+ export const GET = defineRoute({
206
+ handlers: [middleware1, middleware2, handler],
207
+ meta: { description: 'Get user list' }
208
+ });
209
+ ```
210
+
211
+ #### 虚拟模块
212
+
213
+ | 模块 ID | 导出 | 说明 |
214
+ | --------------------------- | ------------------------------ | --------------------------------------------------------- |
215
+ | `virtual:hono-file-routes` | `scannedRouteModules` | 文件路由扫描结果 |
216
+ | `virtual:hono-ssr-manifest` | `resolveManifest()`, `default` | SSR 资源清单,dev 返回 client 入口,prod 返回构建产物路径 |
217
+
218
+ ### 项目结构建议
219
+
220
+ ```
221
+ project/
222
+ ├── app/ # 客户端代码
223
+ │ ├── entry-client.ts # 客户端入口
224
+ │ ├── main.ts # 应用工厂
225
+ │ └── App.vue # 根组件
226
+ ├── server/ # 服务端代码
227
+ │ ├── app.ts # Hono 服务端入口
228
+ │ ├── api/ # API 路由(自动扫描)
229
+ │ │ ├── users.ts
230
+ │ │ └── posts.ts
231
+ │ └── routes/ # 页面路由(自动扫描)
232
+ │ └── index.tsx
233
+ ├── vite.config.ts
234
+ ├── tsconfig.json
235
+ └── wrangler.toml # Cloudflare 配置
236
+ ```
237
+
238
+ ### 开发
239
+
240
+ ```bash
241
+ # 构建
242
+ pnpm build
243
+
244
+ # 类型检查
245
+ pnpm typecheck
246
+
247
+ # 代码检查与格式化
248
+ pnpm lint
249
+ pnpm fmt
250
+ ```
251
+
252
+ ### License
253
+
254
+ [MIT](./LICENSE)
@@ -0,0 +1,22 @@
1
+ //#region src/hono-ssr.d.ts
2
+ declare module 'virtual:hono-file-routes' {
3
+ export interface ScannedRouteModule {
4
+ source: string;
5
+ scanDir: string;
6
+ module: Record<string, unknown>;
7
+ }
8
+ export const scannedRouteModules: ScannedRouteModule[];
9
+ }
10
+ declare module 'virtual:hono-ssr-manifest' {
11
+ interface ManifestEntry {
12
+ file: string;
13
+ css?: string[];
14
+ isEntry?: boolean;
15
+ }
16
+ export function resolveManifest(): {
17
+ scripts: string;
18
+ styles: string;
19
+ };
20
+ const manifest: Record<string, ManifestEntry>;
21
+ export default manifest;
22
+ }
@@ -0,0 +1,39 @@
1
+ import { MiddlewareHandler } from "hono";
2
+
3
+ //#region src/index.d.ts
4
+ declare const HTTP_METHODS: readonly ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD", "ALL"];
5
+ type HttpMethod = (typeof HTTP_METHODS)[number];
6
+ type RouteHandlers = [MiddlewareHandler, ...MiddlewareHandler[]];
7
+ interface RouteMeta {
8
+ [key: string]: any;
9
+ }
10
+ interface RouteDefinition<Meta = RouteMeta> {
11
+ handlers: RouteHandlers;
12
+ meta?: Meta;
13
+ }
14
+ interface RouteRecord<Meta = RouteMeta> {
15
+ method: HttpMethod;
16
+ path: string;
17
+ source: string;
18
+ group?: string[];
19
+ handlers: RouteHandlers;
20
+ meta?: Meta;
21
+ }
22
+ interface SetupFileRoutesOptions<Meta = RouteMeta> {
23
+ /**
24
+ * prefix specifies the prefix for the file-based routes. It is useful when you want to group the file-based routes under a specific path.
25
+ *
26
+ * @default '/api'
27
+ */
28
+ prefix?: string;
29
+ onRouteRegister?: (route: RouteRecord<Meta>) => void;
30
+ }
31
+ /**
32
+ * setupFileRoutes sets up the file-based routes for Hono SSR. It scans the specified directories for route files and returns the route records.
33
+ * @param options
34
+ * @param onRouteRegister
35
+ */
36
+ declare function setupFileRoutes<Meta = RouteMeta>(options?: SetupFileRoutesOptions<Meta>): RouteRecord<Meta>[];
37
+ declare function getFilesRoutes<Meta = RouteMeta>($prefix?: string): RouteRecord<Meta>[];
38
+ //#endregion
39
+ export { RouteDefinition, RouteHandlers, RouteMeta, RouteRecord, SetupFileRoutesOptions, getFilesRoutes, setupFileRoutes };
package/dist/index.js ADDED
@@ -0,0 +1,131 @@
1
+ import { scannedRouteModules } from "virtual:hono-file-routes";
2
+
3
+ //#region src/index.ts
4
+ const HTTP_METHODS = [
5
+ "GET",
6
+ "POST",
7
+ "PUT",
8
+ "PATCH",
9
+ "DELETE",
10
+ "OPTIONS",
11
+ "HEAD",
12
+ "ALL"
13
+ ];
14
+ /**
15
+ * setupFileRoutes sets up the file-based routes for Hono SSR. It scans the specified directories for route files and returns the route records.
16
+ * @param options
17
+ * @param onRouteRegister
18
+ */
19
+ function setupFileRoutes(options = {}) {
20
+ const routes = getFilesRoutes(options.prefix);
21
+ if (options?.onRouteRegister) for (const route of routes) options.onRouteRegister(route);
22
+ return routes;
23
+ }
24
+ function getFilesRoutes($prefix = "/api") {
25
+ const routes = [];
26
+ const prefix = normalizeRoutePrefix($prefix);
27
+ for (const scannedRoute of scannedRouteModules) {
28
+ const { path, group } = normalizeRoutePath(scannedRoute.source, scannedRoute.scanDir, prefix);
29
+ for (const method of HTTP_METHODS) {
30
+ if (!Object.prototype.hasOwnProperty.call(scannedRoute.module, method)) continue;
31
+ const route = scannedRoute.module[method];
32
+ const { handlers, meta } = resolveRouteDefinition(route, {
33
+ method,
34
+ source: scannedRoute.source
35
+ });
36
+ routes.push({
37
+ method,
38
+ path,
39
+ source: scannedRoute.source,
40
+ group,
41
+ handlers,
42
+ meta
43
+ });
44
+ }
45
+ }
46
+ assertNoRouteConflicts(routes);
47
+ return sortRoutes(routes);
48
+ }
49
+ function sortRoutes(routes) {
50
+ return routes.sort((a, b) => {
51
+ if (a.path === b.path) return 0;
52
+ const aSegments = a.path.split("/").filter(Boolean);
53
+ const bSegments = b.path.split("/").filter(Boolean);
54
+ for (let i = 0; i < Math.min(aSegments.length, bSegments.length); i++) {
55
+ const aSegment = aSegments[i];
56
+ const bSegment = bSegments[i];
57
+ const aRank = getSegmentRank(aSegment);
58
+ const bRank = getSegmentRank(bSegment);
59
+ if (aRank !== bRank) return aRank - bRank;
60
+ if (aSegment !== bSegment) return aSegment.localeCompare(bSegment);
61
+ }
62
+ return aSegments.length - bSegments.length;
63
+ });
64
+ }
65
+ function getSegmentRank(segment) {
66
+ if (segment === "*") return 2;
67
+ if (segment.startsWith(":")) return 1;
68
+ return 0;
69
+ }
70
+ function isRouteDefinition(obj) {
71
+ return obj && typeof obj === "object" && isRouteHandlers(obj.handlers);
72
+ }
73
+ function isRouteHandlers(obj) {
74
+ return Array.isArray(obj) && obj.length > 0;
75
+ }
76
+ function resolveRouteDefinition(exported, context) {
77
+ if (isRouteDefinition(exported)) return exported;
78
+ if (isRouteHandlers(exported)) return { handlers: exported };
79
+ throw new TypeError(`Invalid route export "${context.method}" in "${context.source}". Expected MiddlewareHandler[] or RouteDefinition.`);
80
+ }
81
+ function assertNoRouteConflicts(routes) {
82
+ const routeMap = /* @__PURE__ */ new Map();
83
+ for (const route of routes) {
84
+ const routeKey = `${route.method} ${route.path}`;
85
+ const existingRoute = routeMap.get(routeKey);
86
+ if (existingRoute) throw new Error(`Conflicting file routes for "${route.method} ${route.path}": "${existingRoute.source}" and "${route.source}".`);
87
+ routeMap.set(routeKey, route);
88
+ }
89
+ }
90
+ function normalizeRoutePath(source, scanDir, prefix) {
91
+ const rawSegments = getRelativeSource(source, scanDir).replace(/\.[^.]+$/, "").split("/").filter(Boolean);
92
+ const group = [];
93
+ const pathSegments = rawSegments.slice(0, -1).flatMap((segment) => {
94
+ const matchedGroup = segment.match(/^\(([^/()]+)\)$/);
95
+ if (!matchedGroup) return [segment];
96
+ group.push(matchedGroup[1]);
97
+ return [];
98
+ });
99
+ const fileSegment = rawSegments.at(-1);
100
+ if (fileSegment) pathSegments.push(fileSegment);
101
+ if (pathSegments[pathSegments.length - 1] === "index") pathSegments.pop();
102
+ const normalizedPath = pathSegments.map(normalizeRouteSegment);
103
+ return {
104
+ path: joinRoutePath(prefix, normalizedPath.length ? `/${normalizedPath.join("/")}` : "/"),
105
+ group: group.length ? group : void 0
106
+ };
107
+ }
108
+ function getRelativeSource(source, scanDir) {
109
+ const prefix = `${scanDir}/`;
110
+ if (source.startsWith(prefix)) return source.slice(prefix.length);
111
+ if (source === scanDir) return "";
112
+ throw new Error(`Scanned route source "${source}" does not match scan dir "${scanDir}".`);
113
+ }
114
+ function normalizeRouteSegment(segment) {
115
+ if (/^\[\[\.\.\..+\]\]$/.test(segment)) return "*";
116
+ if (/^\[\.\.\..+\]$/.test(segment)) return "*";
117
+ return segment.replace(/^\[(.+)\]$/, ":$1");
118
+ }
119
+ function normalizeRoutePrefix(prefix) {
120
+ if (!prefix || prefix === "/") return "";
121
+ const normalizedPrefix = prefix.startsWith("/") ? prefix : `/${prefix}`;
122
+ return normalizedPrefix.endsWith("/") ? normalizedPrefix.slice(0, -1) : normalizedPrefix;
123
+ }
124
+ function joinRoutePath(prefix, path) {
125
+ if (!prefix) return path;
126
+ if (path === "/") return prefix;
127
+ return `${prefix}${path}`;
128
+ }
129
+
130
+ //#endregion
131
+ export { getFilesRoutes, setupFileRoutes };
@@ -0,0 +1,7 @@
1
+ import { t as DefineRouteInterface } from "./types-hATUid1L.js";
2
+ import { Env } from "hono/types";
3
+
4
+ //#region src/route.d.ts
5
+ declare function createDefineRoute<E extends Env, Meta extends Record<string, any>>(): DefineRouteInterface<E, string, Meta>;
6
+ //#endregion
7
+ export { createDefineRoute };
package/dist/route.js ADDED
@@ -0,0 +1,17 @@
1
+ import { createFactory } from "hono/factory";
2
+
3
+ //#region src/route.ts
4
+ function createDefineRoute() {
5
+ const factory = createFactory();
6
+ const defineRoute = ({ handlers, meta }) => {
7
+ const $handlers = Array.isArray(handlers) ? handlers : [handlers];
8
+ return {
9
+ handlers: factory.createHandlers(...$handlers),
10
+ meta
11
+ };
12
+ };
13
+ return defineRoute;
14
+ }
15
+
16
+ //#endregion
17
+ export { createDefineRoute };
@@ -0,0 +1,91 @@
1
+ import { DevServerOptions } from "@hono/vite-dev-server";
2
+ import { Env, H, HandlerResponse, Input, IntersectNonAnyTypes } from "hono/types";
3
+ import { BunBuildOptions } from "@hono/vite-build/bun";
4
+ import { CloudflarePagesBuildOptions } from "@hono/vite-build/cloudflare-pages";
5
+ import { CloudflareWorkersBuildOptions } from "@hono/vite-build/cloudflare-workers";
6
+ import { DenoBuildOptions } from "@hono/vite-build/deno";
7
+ import { NetlifyFunctionsBuildOptions } from "@hono/vite-build/netlify-functions";
8
+ import { NodeBuildOptions } from "@hono/vite-build/node";
9
+ import { VercelBuildOptions } from "@hono/vite-build/vercel";
10
+ import { GetPlatformProxyOptions } from "wrangler";
11
+
12
+ //#region src/types.d.ts
13
+ /**
14
+ * HonoSSRPluginOptions defines the options for the Hono SSR plugin.
15
+ */
16
+ interface HonoSSRPluginOptions<T extends HonoSSRBuildType = HonoSSRBuildType> {
17
+ /**
18
+ * @default 'server/app.ts'
19
+ */
20
+ serverEntry?: string;
21
+ /**
22
+ * @default 'app/entry-client.ts'
23
+ */
24
+ clientEntry?: string;
25
+ fileRoute?: HonoSSRFileRouteOptions;
26
+ devServer?: DevServerOptions;
27
+ /**
28
+ * @default "[/^\/app\/.+/]"
29
+ */
30
+ devServerExclude?: (string | RegExp)[];
31
+ /**
32
+ * adapter: 'cloudflare' | 'node' | 'bun'
33
+ *
34
+ * @default true
35
+ */
36
+ enableDevServerAdapter?: boolean;
37
+ buildType?: T;
38
+ buildOptions?: HonoSSRBuildRecord[T];
39
+ /**
40
+ * platformProxyOptions is the options for the platform proxy in development mode.
41
+ *
42
+ * It is used to proxy the requests to the platform when using the dev server adapter.
43
+ */
44
+ platformProxyOptions?: GetPlatformProxyOptions;
45
+ }
46
+ type HonoSSRBuildRecord = {
47
+ node?: NodeBuildOptions;
48
+ bun?: BunBuildOptions;
49
+ deno?: DenoBuildOptions;
50
+ 'cloudflare-workers'?: CloudflareWorkersBuildOptions;
51
+ 'cloudflare-pages'?: CloudflarePagesBuildOptions;
52
+ vercel?: VercelBuildOptions;
53
+ 'netlify-functions'?: NetlifyFunctionsBuildOptions;
54
+ };
55
+ type HonoSSRBuildType = keyof HonoSSRBuildRecord;
56
+ /**
57
+ * HonoSSRFileRouteOptions defines the options for file-based routing api in Hono SSR.
58
+ */
59
+ interface HonoSSRFileRouteOptions {
60
+ /**
61
+ * scanDirs specifies the directories to scan for route api files.
62
+ *
63
+ * @default "['server/api', 'server/routes']"
64
+ */
65
+ scanDirs?: string[];
66
+ /**
67
+ * ignore specifies the patterns to ignore when scanning for route api files.
68
+ *
69
+ * @default `['**‍/*.test.*', '**‍/*.spec.*', '**‍/__tests__/**‍']`
70
+ */
71
+ ignore?: string[];
72
+ }
73
+ interface RouteDefinition<T, S extends Record<string, any> = Record<string, any>> {
74
+ handlers: T;
75
+ meta?: S;
76
+ }
77
+ interface DefineRouteInterface<E extends Env, P extends string, S extends Record<string, any> = Record<string, any>> {
78
+ <I extends Input = {}, R extends HandlerResponse<any> = any, E2 extends Env = E>(definition: RouteDefinition<H<E2, P, I, R>, S>): RouteDefinition<[H<E2, P, I, R>], S>;
79
+ <I extends Input = {}, R extends HandlerResponse<any> = any, E2 extends Env = E>(definition: RouteDefinition<[H<E2, P, I, R>]>): RouteDefinition<[H<E2, P, I, R>]>;
80
+ <I extends Input = {}, I2 extends Input = I, R extends HandlerResponse<any> = any, R2 extends HandlerResponse<any> = any, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>>(definition: RouteDefinition<[H<E2, P, I, R>, H<E3, P, I2, R2>], S>): RouteDefinition<[H<E2, P, I, R>, H<E3, P, I2, R2>], S>;
81
+ <I extends Input = {}, I2 extends Input = I, I3 extends Input = I & I2, R extends HandlerResponse<any> = any, R2 extends HandlerResponse<any> = any, R3 extends HandlerResponse<any> = any, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>>(definition: RouteDefinition<[H<E2, P, I, R>, H<E3, P, I2, R2>, H<E4, P, I3, R3>], S>): RouteDefinition<[H<E2, P, I, R>, H<E3, P, I2, R2>, H<E4, P, I3, R3>], S>;
82
+ <I extends Input = {}, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, R extends HandlerResponse<any> = any, R2 extends HandlerResponse<any> = any, R3 extends HandlerResponse<any> = any, R4 extends HandlerResponse<any> = any, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>>(definition: RouteDefinition<[H<E2, P, I, R>, H<E3, P, I2, R2>, H<E4, P, I3, R3>, H<E5, P, I4, R4>], S>): RouteDefinition<[H<E2, P, I, R>, H<E3, P, I2, R2>, H<E4, P, I3, R3>, H<E5, P, I4, R4>], S>;
83
+ <I extends Input = {}, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, R extends HandlerResponse<any> = any, R2 extends HandlerResponse<any> = any, R3 extends HandlerResponse<any> = any, R4 extends HandlerResponse<any> = any, R5 extends HandlerResponse<any> = any, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>>(definition: RouteDefinition<[H<E2, P, I, R>, H<E3, P, I2, R2>, H<E4, P, I3, R3>, H<E5, P, I4, R4>, H<E6, P, I5, R5>], S>): RouteDefinition<[H<E2, P, I, R>, H<E3, P, I2, R2>, H<E4, P, I3, R3>, H<E5, P, I4, R4>, H<E6, P, I5, R5>], S>;
84
+ <I extends Input = {}, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, R extends HandlerResponse<any> = any, R2 extends HandlerResponse<any> = any, R3 extends HandlerResponse<any> = any, R4 extends HandlerResponse<any> = any, R5 extends HandlerResponse<any> = any, R6 extends HandlerResponse<any> = any, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>>(definition: RouteDefinition<[H<E2, P, I, R>, H<E3, P, I2, R2>, H<E4, P, I3, R3>, H<E5, P, I4, R4>, H<E6, P, I5, R5>, H<E7, P, I6, R6>], S>): RouteDefinition<[H<E2, P, I, R>, H<E3, P, I2, R2>, H<E4, P, I3, R3>, H<E5, P, I4, R4>, H<E6, P, I5, R5>, H<E7, P, I6, R6>], S>;
85
+ <I extends Input = {}, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, R extends HandlerResponse<any> = any, R2 extends HandlerResponse<any> = any, R3 extends HandlerResponse<any> = any, R4 extends HandlerResponse<any> = any, R5 extends HandlerResponse<any> = any, R6 extends HandlerResponse<any> = any, R7 extends HandlerResponse<any> = any, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>>(definition: RouteDefinition<[H<E2, P, I, R>, H<E3, P, I2, R2>, H<E4, P, I3, R3>, H<E5, P, I4, R4>, H<E6, P, I5, R5>, H<E7, P, I6, R6>, H<E8, P, I7, R7>], S>): RouteDefinition<[H<E2, P, I, R>, H<E3, P, I2, R2>, H<E4, P, I3, R3>, H<E5, P, I4, R4>, H<E6, P, I5, R5>, H<E7, P, I6, R6>, H<E8, P, I7, R7>], S>;
86
+ <I extends Input = {}, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, R extends HandlerResponse<any> = any, R2 extends HandlerResponse<any> = any, R3 extends HandlerResponse<any> = any, R4 extends HandlerResponse<any> = any, R5 extends HandlerResponse<any> = any, R6 extends HandlerResponse<any> = any, R7 extends HandlerResponse<any> = any, R8 extends HandlerResponse<any> = any, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>>(definition: RouteDefinition<[H<E2, P, I, R>, H<E3, P, I2, R2>, H<E4, P, I3, R3>, H<E5, P, I4, R4>, H<E6, P, I5, R5>, H<E7, P, I6, R6>, H<E8, P, I7, R7>, H<E9, P, I8, R8>], S>): RouteDefinition<[H<E2, P, I, R>, H<E3, P, I2, R2>, H<E4, P, I3, R3>, H<E5, P, I4, R4>, H<E6, P, I5, R5>, H<E7, P, I6, R6>, H<E8, P, I7, R7>, H<E9, P, I8, R8>], S>;
87
+ <I extends Input = {}, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, R extends HandlerResponse<any> = any, R2 extends HandlerResponse<any> = any, R3 extends HandlerResponse<any> = any, R4 extends HandlerResponse<any> = any, R5 extends HandlerResponse<any> = any, R6 extends HandlerResponse<any> = any, R7 extends HandlerResponse<any> = any, R8 extends HandlerResponse<any> = any, R9 extends HandlerResponse<any> = any, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>>(definition: RouteDefinition<[H<E2, P, I, R>, H<E3, P, I2, R2>, H<E4, P, I3, R3>, H<E5, P, I4, R4>, H<E6, P, I5, R5>, H<E7, P, I6, R6>, H<E8, P, I7, R7>, H<E9, P, I8, R8>, H<E10, P, I9, R9>], S>): RouteDefinition<[H<E2, P, I, R>, H<E3, P, I2, R2>, H<E4, P, I3, R3>, H<E5, P, I4, R4>, H<E6, P, I5, R5>, H<E7, P, I6, R6>, H<E8, P, I7, R7>, H<E9, P, I8, R8>, H<E10, P, I9, R9>], S>;
88
+ <I extends Input = {}, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, I10 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8 & I9, R extends HandlerResponse<any> = any, R2 extends HandlerResponse<any> = any, R3 extends HandlerResponse<any> = any, R4 extends HandlerResponse<any> = any, R5 extends HandlerResponse<any> = any, R6 extends HandlerResponse<any> = any, R7 extends HandlerResponse<any> = any, R8 extends HandlerResponse<any> = any, R9 extends HandlerResponse<any> = any, R10 extends HandlerResponse<any> = any, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, E11 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9, E10]>>(definition: RouteDefinition<[H<E2, P, I, R>, H<E3, P, I2, R2>, H<E4, P, I3, R3>, H<E5, P, I4, R4>, H<E6, P, I5, R5>, H<E7, P, I6, R6>, H<E8, P, I7, R7>, H<E9, P, I8, R8>, H<E10, P, I9, R9>, H<E11, P, I10, R10>], S>): RouteDefinition<[H<E2, P, I, R>, H<E3, P, I2, R2>, H<E4, P, I3, R3>, H<E5, P, I4, R4>, H<E6, P, I5, R5>, H<E7, P, I6, R6>, H<E8, P, I7, R7>, H<E9, P, I8, R8>, H<E10, P, I9, R9>, H<E11, P, I10, R10>], S>;
89
+ }
90
+ //#endregion
91
+ export { HonoSSRBuildType as n, HonoSSRPluginOptions as r, DefineRouteInterface as t };
package/dist/vite.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { n as HonoSSRBuildType, r as HonoSSRPluginOptions } from "./types-hATUid1L.js";
2
+ import { Plugin } from "vite";
3
+
4
+ //#region src/vite.d.ts
5
+ declare function HonoSSR<T extends HonoSSRBuildType = HonoSSRBuildType>(options: HonoSSRPluginOptions<T>): Promise<Plugin<any>[]>;
6
+ //#endregion
7
+ export { HonoSSR };
package/dist/vite.js ADDED
@@ -0,0 +1,214 @@
1
+ import DevServer, { defaultOptions } from "@hono/vite-dev-server";
2
+ import { normalizePath } from "vite";
3
+ import { readFile } from "node:fs/promises";
4
+ import path, { extname, relative } from "node:path";
5
+ import { glob } from "tinyglobby";
6
+
7
+ //#region src/shared.ts
8
+ async function interopDefault(m) {
9
+ const resolved = await m;
10
+ return resolved.default || resolved;
11
+ }
12
+ function normalizeDirs(dirs) {
13
+ return [...new Set(dirs.map((dir) => trimSlashes(normalizePath(dir))).filter(Boolean))];
14
+ }
15
+ function trimSlashes(value) {
16
+ return value.replace(/^\/+|\/+$/g, "");
17
+ }
18
+
19
+ //#endregion
20
+ //#region src/plugins/client.ts
21
+ const VIRTUAL_MANIFEST_ID = "virtual:hono-ssr-manifest";
22
+ const RESOLVED_VIRTUAL_MANIFEST_ID = `\0${VIRTUAL_MANIFEST_ID}`;
23
+ function ClientBuild(clientEntry) {
24
+ return {
25
+ name: "hono-ssr:client-build",
26
+ apply: (_config, { command, mode }) => {
27
+ if (command === "build" && mode === "client") return true;
28
+ return false;
29
+ },
30
+ config: () => {
31
+ return { build: {
32
+ rolldownOptions: { input: clientEntry },
33
+ manifest: true
34
+ } };
35
+ }
36
+ };
37
+ }
38
+ /**
39
+ * 虚拟模块插件:将 client build 产物 manifest.json 通过 virtual:hono-ssr-manifest 暴露给 SSR 端。
40
+ * 消费方直接 `import { resolveManifest } from 'virtual:hono-ssr-manifest'` 即可。
41
+ * 插件在 load 时根据 config.command 判断 dev/build,直接生成对应代码,
42
+ * 无需 import.meta.env.DEV 或全局变量。
43
+ */
44
+ function ManifestPlugin(clientEntry) {
45
+ let config;
46
+ return {
47
+ name: "hono-ssr:manifest",
48
+ configResolved(resolvedConfig) {
49
+ config = resolvedConfig;
50
+ },
51
+ resolveId(id) {
52
+ if (id === VIRTUAL_MANIFEST_ID) return RESOLVED_VIRTUAL_MANIFEST_ID;
53
+ return null;
54
+ },
55
+ async load(id) {
56
+ if (id !== RESOLVED_VIRTUAL_MANIFEST_ID) return null;
57
+ if (config.command === "serve") return [
58
+ `export function resolveManifest() {`,
59
+ ` return {`,
60
+ ` scripts: '<script type="module" src="${clientEntry.startsWith("/") ? clientEntry : `/${clientEntry}`}"><\/script>',`,
61
+ ` styles: ''`,
62
+ ` };`,
63
+ `}`,
64
+ "",
65
+ `export default {};`
66
+ ].join("\n");
67
+ const manifestPath = path.resolve(config.root, "dist", ".vite", "manifest.json");
68
+ let manifestJson = "{}";
69
+ try {
70
+ manifestJson = await readFile(manifestPath, "utf-8");
71
+ JSON.parse(manifestJson);
72
+ } catch {}
73
+ return [
74
+ `const __manifest__ = ${manifestJson};`,
75
+ "",
76
+ `export function resolveManifest() {`,
77
+ ` var manifest = __manifest__;`,
78
+ ` if (!manifest) {`,
79
+ ` return { scripts: '', styles: '' };`,
80
+ ` }`,
81
+ "",
82
+ ` var entries = Object.values(manifest);`,
83
+ ` var entry = entries.find(function(m) { return m.isEntry; });`,
84
+ ` var scripts = entry ? '<script type="module" crossorigin src="/' + entry.file + '"><\/script>' : '';`,
85
+ ` var styles = entry && entry.css ? entry.css.map(function(css) { return '<link rel="stylesheet" crossorigin href="/' + css + '">'; }).join('\\n ') : '';`,
86
+ "",
87
+ ` return { scripts: scripts, styles: styles };`,
88
+ `}`,
89
+ "",
90
+ `export default __manifest__;`
91
+ ].join("\n");
92
+ }
93
+ };
94
+ }
95
+
96
+ //#endregion
97
+ //#region src/plugins/file-route.ts
98
+ const DEFAULT_FILE_ROUTE_SCAN_DIRS = ["server/api", "server/routes"];
99
+ const DEFAULT_IGNORE = [
100
+ "**/*.test.*",
101
+ "**/*.spec.*",
102
+ "**/__tests__/**"
103
+ ];
104
+ const VIRTUAL_FILE_ROUTES_MODULE_ID = "virtual:hono-file-routes";
105
+ const RESOLVED_VIRTUAL_FILE_ROUTES_MODULE_ID = `\0${VIRTUAL_FILE_ROUTES_MODULE_ID}`;
106
+ const ROUTE_FILE_EXTENSIONS = new Set([
107
+ ".js",
108
+ ".mjs",
109
+ ".cjs",
110
+ ".ts",
111
+ ".mts",
112
+ ".cts",
113
+ ".tsx",
114
+ ".jsx"
115
+ ]);
116
+ const ROUTE_FILE_GLOB = "**/*.{js,mjs,cjs,ts,mts,cts,tsx,jsx}";
117
+ function FileRoutesPlugin(options) {
118
+ const { ignore = [] } = options || {};
119
+ const scanDirs = normalizeDirs(options?.scanDirs ?? [...DEFAULT_FILE_ROUTE_SCAN_DIRS]);
120
+ let config;
121
+ return {
122
+ name: "hono-ssr:file-routes",
123
+ configResolved(resolvedConfig) {
124
+ config = resolvedConfig;
125
+ },
126
+ resolveId(id) {
127
+ if (id === VIRTUAL_FILE_ROUTES_MODULE_ID) return RESOLVED_VIRTUAL_FILE_ROUTES_MODULE_ID;
128
+ return null;
129
+ },
130
+ async load(id) {
131
+ if (id !== RESOLVED_VIRTUAL_FILE_ROUTES_MODULE_ID) return null;
132
+ return generateRouteModule(await scanRouteModules(config.root, scanDirs, ignore));
133
+ },
134
+ handleHotUpdate(ctx) {
135
+ if (!isScannedRouteFile(ctx.file, config.root, scanDirs)) return;
136
+ invalidateRouteModule(ctx.server);
137
+ }
138
+ };
139
+ }
140
+ async function scanRouteModules(root, scanDirs, ignore) {
141
+ const sources = await glob(scanDirs.map((scanDir) => `${scanDir}/${ROUTE_FILE_GLOB}`), {
142
+ cwd: root,
143
+ ignore: [...DEFAULT_IGNORE, ...ignore],
144
+ onlyFiles: true
145
+ });
146
+ return [...new Set(sources.map((source) => normalizePath(source)))].sort((a, b) => a.localeCompare(b)).map((source) => ({
147
+ source,
148
+ scanDir: resolveScanDir(source, scanDirs)
149
+ }));
150
+ }
151
+ function resolveScanDir(source, scanDirs) {
152
+ const matchedScanDirs = scanDirs.filter((scanDir) => source === scanDir || source.startsWith(`${scanDir}/`));
153
+ if (!matchedScanDirs.length) throw new Error(`Scanned route source "${source}" does not match any configured scan dir.`);
154
+ return matchedScanDirs.sort((a, b) => b.length - a.length)[0];
155
+ }
156
+ function generateRouteModule(files) {
157
+ if (!files.length) return "export const scannedRouteModules = [];\n";
158
+ return `${files.map((file, index) => `import * as routeModule${index} from ${JSON.stringify(`/${file.source}`)};`).join("\n")}\n\nexport const scannedRouteModules = [\n${files.map((file, index) => ` { source: ${JSON.stringify(file.source)}, scanDir: ${JSON.stringify(file.scanDir)}, module: routeModule${index} }`).join(",\n")}\n];\n`;
159
+ }
160
+ function hasRouteFileExtension(file) {
161
+ return ROUTE_FILE_EXTENSIONS.has(extname(file));
162
+ }
163
+ function isScannedRouteFile(file, root, scanDirs) {
164
+ if (!hasRouteFileExtension(file)) return false;
165
+ const relativeFile = normalizePath(relative(root, file));
166
+ return scanDirs.some((scanDir) => relativeFile === scanDir || relativeFile.startsWith(`${scanDir}/`));
167
+ }
168
+ function invalidateRouteModule(server) {
169
+ const module = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_FILE_ROUTES_MODULE_ID);
170
+ if (module) server.moduleGraph.invalidateModule(module);
171
+ }
172
+
173
+ //#endregion
174
+ //#region src/vite.ts
175
+ async function HonoSSR(options) {
176
+ const { serverEntry = "server/app.ts", clientEntry = "app/entry-client.ts", fileRoute, devServer, devServerExclude = [/^\/app\/.+/], buildType, buildOptions, platformProxyOptions = {} } = options;
177
+ const HonoBuild = buildType ? await interopDefault(import(`@hono/vite-build/${buildType}`)) : void 0;
178
+ let HonoAdapter;
179
+ if (buildType === "cloudflare-workers" || buildType === "cloudflare-pages") {
180
+ const cfAdapter = await interopDefault(import("@hono/vite-dev-server/cloudflare"));
181
+ HonoAdapter = () => cfAdapter({ proxy: platformProxyOptions });
182
+ } else if (buildType === "bun") HonoAdapter = await interopDefault(import("@hono/vite-dev-server/bun"));
183
+ else if (buildType === "node") HonoAdapter = await interopDefault(import("@hono/vite-dev-server/node"));
184
+ let honoAdapterPromise;
185
+ function getHonoAdapter() {
186
+ honoAdapterPromise ??= HonoAdapter?.();
187
+ return honoAdapterPromise;
188
+ }
189
+ const plugins = [
190
+ FileRoutesPlugin(fileRoute),
191
+ ManifestPlugin(clientEntry),
192
+ DevServer({
193
+ ...defaultOptions,
194
+ entry: serverEntry,
195
+ injectClientScript: false,
196
+ adapter: getHonoAdapter,
197
+ ...devServer,
198
+ exclude: [
199
+ ...defaultOptions.exclude,
200
+ ...devServerExclude ?? [],
201
+ ...devServer?.exclude ?? []
202
+ ]
203
+ }),
204
+ ClientBuild(clientEntry)
205
+ ];
206
+ if (HonoBuild) plugins.push(HonoBuild({
207
+ entry: serverEntry,
208
+ ...buildOptions
209
+ }));
210
+ return plugins;
211
+ }
212
+
213
+ //#endregion
214
+ export { HonoSSR };
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@soybeanjs/hono-ssr",
3
+ "version": "0.0.1",
4
+ "description": "A Vite SSR plugin for Hono framework, providing file-based routing, client build, SSR manifest management, and multi-platform deployment adaptation.",
5
+ "homepage": "https://github.com/soybeanjs/hono-ssr",
6
+ "bugs": {
7
+ "url": "https://github.com/soybeanjs/hono-ssr/issues"
8
+ },
9
+ "license": "MIT",
10
+ "author": {
11
+ "name": "Soybean",
12
+ "email": "soybeanjs@outlook.com",
13
+ "url": "https://github.com/soybeanjs"
14
+ },
15
+ "repository": {
16
+ "url": "https://github.com/soybeanjs/hono-ssr.git"
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "type": "module",
22
+ "main": "./dist/index.js",
23
+ "module": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js",
29
+ "require": "./dist/index.js"
30
+ },
31
+ "./route": {
32
+ "types": "./dist/route.d.ts",
33
+ "import": "./dist/route.js",
34
+ "require": "./dist/route.js"
35
+ },
36
+ "./vite": {
37
+ "types": "./dist/vite.d.ts",
38
+ "import": "./dist/vite.js",
39
+ "require": "./dist/vite.js"
40
+ },
41
+ "./types": {
42
+ "types": "./dist/hono-ssr.d.ts"
43
+ }
44
+ },
45
+ "publishConfig": {
46
+ "registry": "https://registry.npmjs.org/"
47
+ },
48
+ "devDependencies": {
49
+ "@hono/vite-build": "^1.11.1",
50
+ "@hono/vite-dev-server": "^0.26.0",
51
+ "@soybeanjs/cli": "^1.7.2",
52
+ "@soybeanjs/oxc-config": "^0.2.3",
53
+ "@types/node": "^25.9.2",
54
+ "hono": "^4.12.24",
55
+ "tinyglobby": "^0.2.17",
56
+ "typescript": "^6.0.3",
57
+ "vite": "npm:@voidzero-dev/vite-plus-core@^0.1.24",
58
+ "vite-plus": "^0.1.24",
59
+ "wrangler": "^4.98.0"
60
+ },
61
+ "scripts": {
62
+ "build": "vp pack",
63
+ "commit": "soy git-commit",
64
+ "fmt": "vp fmt",
65
+ "lint": "vp lint --fix",
66
+ "publish-pkg": "pnpm publish --access public",
67
+ "release": "soy release",
68
+ "typecheck": "tsc --noEmit --skipLibCheck",
69
+ "upkg": "soy ncu"
70
+ }
71
+ }