@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 +21 -0
- package/README.en.md +253 -0
- package/README.md +254 -0
- package/dist/hono-ssr.d.ts +22 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +131 -0
- package/dist/route.d.ts +7 -0
- package/dist/route.js +17 -0
- package/dist/types-hATUid1L.d.ts +91 -0
- package/dist/vite.d.ts +7 -0
- package/dist/vite.js +214 -0
- package/package.json +71 -0
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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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 };
|
package/dist/route.d.ts
ADDED
|
@@ -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
|
+
}
|