create-hazejs 0.0.0-stage → 0.1.2

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.
Files changed (187) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -2
  3. package/bin/create-hazejs.mjs +2 -0
  4. package/dist/index.d.mts +1 -0
  5. package/dist/index.mjs +74 -0
  6. package/dist/scaffold.d.mts +29 -0
  7. package/dist/scaffold.mjs +76 -0
  8. package/package.json +77 -3
  9. package/templates/api/.env +4 -0
  10. package/templates/api/.env.test +1 -0
  11. package/templates/api/README.md +383 -0
  12. package/templates/api/_gitignore +9 -0
  13. package/templates/api/app/controller/AuthController.ts +45 -0
  14. package/templates/api/app/controller/GreetingController.ts +23 -0
  15. package/templates/api/app/controller/UserController.ts +38 -0
  16. package/templates/api/app/event/UserRegistered.ts +6 -0
  17. package/templates/api/app/exception/Handler.ts +13 -0
  18. package/templates/api/app/job/SendWelcomeEmail.ts +19 -0
  19. package/templates/api/app/listener/BroadcastRegistration.ts +7 -0
  20. package/templates/api/app/listener/QueueWelcomeEmail.ts +6 -0
  21. package/templates/api/app/mail/WelcomeMail.ts +17 -0
  22. package/templates/api/app/model/User.ts +11 -0
  23. package/templates/api/app/policy/UserPolicy.ts +14 -0
  24. package/templates/api/app/provider/AppProvider.ts +9 -0
  25. package/templates/api/app/provider/DatabaseProvider.ts +4 -0
  26. package/templates/api/app/response/Formatter.ts +17 -0
  27. package/templates/api/app/service/GreetingService.ts +6 -0
  28. package/templates/api/app/validate/auth.ts +20 -0
  29. package/templates/api/app/validate/greeting.ts +5 -0
  30. package/templates/api/app/validate/user.ts +12 -0
  31. package/templates/api/config/app.ts +7 -0
  32. package/templates/api/config/auth.ts +23 -0
  33. package/templates/api/config/cache.ts +23 -0
  34. package/templates/api/config/cors.ts +11 -0
  35. package/templates/api/config/database.ts +17 -0
  36. package/templates/api/config/filesystem.ts +32 -0
  37. package/templates/api/config/mail.ts +31 -0
  38. package/templates/api/config/middleware.ts +10 -0
  39. package/templates/api/config/queue.ts +21 -0
  40. package/templates/api/config/throttle.ts +12 -0
  41. package/templates/api/config/websocket.ts +12 -0
  42. package/templates/api/database/migrations/0000_create_users.sql +9 -0
  43. package/templates/api/database/migrations/0001_add_user_password.sql +1 -0
  44. package/templates/api/database/migrations/0002_add_user_role.sql +1 -0
  45. package/templates/api/database/migrations/meta/0000_snapshot.json +71 -0
  46. package/templates/api/database/migrations/meta/0001_snapshot.json +78 -0
  47. package/templates/api/database/migrations/meta/0002_snapshot.json +86 -0
  48. package/templates/api/database/migrations/meta/_journal.json +27 -0
  49. package/templates/api/database/schema/users.ts +17 -0
  50. package/templates/api/database/seeders/01-users.ts +15 -0
  51. package/templates/api/haze.config.ts +5 -0
  52. package/templates/api/package.json +38 -0
  53. package/templates/api/route/api.ts +22 -0
  54. package/templates/api/route/ws.ts +18 -0
  55. package/templates/api/schedule/app.ts +11 -0
  56. package/templates/api/tests/auth.test.ts +77 -0
  57. package/templates/api/tests/greeting.test.ts +29 -0
  58. package/templates/api/tests/user.test.ts +58 -0
  59. package/templates/api/tsconfig.json +34 -0
  60. package/templates/api/vitest.config.ts +4 -0
  61. package/templates/react/.env +8 -0
  62. package/templates/react/.env.test +1 -0
  63. package/templates/react/README.md +374 -0
  64. package/templates/react/_gitignore +9 -0
  65. package/templates/react/haze.config.ts +8 -0
  66. package/templates/react/index.html +12 -0
  67. package/templates/react/package.json +46 -0
  68. package/templates/react/server/app/controller/AuthController.ts +45 -0
  69. package/templates/react/server/app/controller/GreetingController.ts +23 -0
  70. package/templates/react/server/app/controller/UserController.ts +38 -0
  71. package/templates/react/server/app/event/UserRegistered.ts +6 -0
  72. package/templates/react/server/app/exception/Handler.ts +13 -0
  73. package/templates/react/server/app/job/SendWelcomeEmail.ts +19 -0
  74. package/templates/react/server/app/listener/BroadcastRegistration.ts +7 -0
  75. package/templates/react/server/app/listener/QueueWelcomeEmail.ts +6 -0
  76. package/templates/react/server/app/mail/WelcomeMail.ts +17 -0
  77. package/templates/react/server/app/model/User.ts +11 -0
  78. package/templates/react/server/app/policy/UserPolicy.ts +14 -0
  79. package/templates/react/server/app/provider/AppProvider.ts +9 -0
  80. package/templates/react/server/app/provider/DatabaseProvider.ts +4 -0
  81. package/templates/react/server/app/provider/SocketProvider.ts +4 -0
  82. package/templates/react/server/app/response/Formatter.ts +17 -0
  83. package/templates/react/server/app/service/GreetingService.ts +6 -0
  84. package/templates/react/server/app/socket/chat.ts +17 -0
  85. package/templates/react/server/app/validate/auth.ts +20 -0
  86. package/templates/react/server/app/validate/greeting.ts +5 -0
  87. package/templates/react/server/app/validate/user.ts +12 -0
  88. package/templates/react/server/config/app.ts +7 -0
  89. package/templates/react/server/config/auth.ts +23 -0
  90. package/templates/react/server/config/cache.ts +23 -0
  91. package/templates/react/server/config/database.ts +17 -0
  92. package/templates/react/server/config/filesystem.ts +32 -0
  93. package/templates/react/server/config/mail.ts +31 -0
  94. package/templates/react/server/config/middleware.ts +11 -0
  95. package/templates/react/server/config/queue.ts +21 -0
  96. package/templates/react/server/config/socketio.ts +13 -0
  97. package/templates/react/server/config/throttle.ts +12 -0
  98. package/templates/react/server/config/websocket.ts +12 -0
  99. package/templates/react/server/database/migrations/0000_create_users.sql +9 -0
  100. package/templates/react/server/database/migrations/0001_add_user_password.sql +1 -0
  101. package/templates/react/server/database/migrations/0002_add_user_role.sql +1 -0
  102. package/templates/react/server/database/migrations/meta/0000_snapshot.json +71 -0
  103. package/templates/react/server/database/migrations/meta/0001_snapshot.json +78 -0
  104. package/templates/react/server/database/migrations/meta/0002_snapshot.json +86 -0
  105. package/templates/react/server/database/migrations/meta/_journal.json +27 -0
  106. package/templates/react/server/database/schema/users.ts +17 -0
  107. package/templates/react/server/database/seeders/01-users.ts +15 -0
  108. package/templates/react/server/route/api.ts +24 -0
  109. package/templates/react/server/route/ws.ts +18 -0
  110. package/templates/react/server/schedule/app.ts +11 -0
  111. package/templates/react/src/App.css +22 -0
  112. package/templates/react/src/App.tsx +72 -0
  113. package/templates/react/src/api.ts +48 -0
  114. package/templates/react/src/main.tsx +9 -0
  115. package/templates/react/src/socket.ts +5 -0
  116. package/templates/react/src/vite-env.d.ts +5 -0
  117. package/templates/react/tests/auth.test.ts +77 -0
  118. package/templates/react/tests/greeting.test.ts +29 -0
  119. package/templates/react/tests/user.test.ts +58 -0
  120. package/templates/react/tsconfig.app.json +22 -0
  121. package/templates/react/tsconfig.json +7 -0
  122. package/templates/react/tsconfig.server.json +34 -0
  123. package/templates/react/vite.config.ts +18 -0
  124. package/templates/react/vitest.config.ts +8 -0
  125. package/templates/vue/.env +8 -0
  126. package/templates/vue/.env.test +1 -0
  127. package/templates/vue/README.md +374 -0
  128. package/templates/vue/_gitignore +9 -0
  129. package/templates/vue/haze.config.ts +8 -0
  130. package/templates/vue/index.html +12 -0
  131. package/templates/vue/package.json +44 -0
  132. package/templates/vue/server/app/controller/AuthController.ts +45 -0
  133. package/templates/vue/server/app/controller/GreetingController.ts +23 -0
  134. package/templates/vue/server/app/controller/UserController.ts +38 -0
  135. package/templates/vue/server/app/event/UserRegistered.ts +6 -0
  136. package/templates/vue/server/app/exception/Handler.ts +13 -0
  137. package/templates/vue/server/app/job/SendWelcomeEmail.ts +19 -0
  138. package/templates/vue/server/app/listener/BroadcastRegistration.ts +7 -0
  139. package/templates/vue/server/app/listener/QueueWelcomeEmail.ts +6 -0
  140. package/templates/vue/server/app/mail/WelcomeMail.ts +17 -0
  141. package/templates/vue/server/app/model/User.ts +11 -0
  142. package/templates/vue/server/app/policy/UserPolicy.ts +14 -0
  143. package/templates/vue/server/app/provider/AppProvider.ts +9 -0
  144. package/templates/vue/server/app/provider/DatabaseProvider.ts +4 -0
  145. package/templates/vue/server/app/provider/SocketProvider.ts +4 -0
  146. package/templates/vue/server/app/response/Formatter.ts +17 -0
  147. package/templates/vue/server/app/service/GreetingService.ts +6 -0
  148. package/templates/vue/server/app/socket/chat.ts +17 -0
  149. package/templates/vue/server/app/validate/auth.ts +20 -0
  150. package/templates/vue/server/app/validate/greeting.ts +5 -0
  151. package/templates/vue/server/app/validate/user.ts +12 -0
  152. package/templates/vue/server/config/app.ts +7 -0
  153. package/templates/vue/server/config/auth.ts +23 -0
  154. package/templates/vue/server/config/cache.ts +23 -0
  155. package/templates/vue/server/config/database.ts +17 -0
  156. package/templates/vue/server/config/filesystem.ts +32 -0
  157. package/templates/vue/server/config/mail.ts +31 -0
  158. package/templates/vue/server/config/middleware.ts +11 -0
  159. package/templates/vue/server/config/queue.ts +21 -0
  160. package/templates/vue/server/config/socketio.ts +13 -0
  161. package/templates/vue/server/config/throttle.ts +12 -0
  162. package/templates/vue/server/config/websocket.ts +12 -0
  163. package/templates/vue/server/database/migrations/0000_create_users.sql +9 -0
  164. package/templates/vue/server/database/migrations/0001_add_user_password.sql +1 -0
  165. package/templates/vue/server/database/migrations/0002_add_user_role.sql +1 -0
  166. package/templates/vue/server/database/migrations/meta/0000_snapshot.json +71 -0
  167. package/templates/vue/server/database/migrations/meta/0001_snapshot.json +78 -0
  168. package/templates/vue/server/database/migrations/meta/0002_snapshot.json +86 -0
  169. package/templates/vue/server/database/migrations/meta/_journal.json +27 -0
  170. package/templates/vue/server/database/schema/users.ts +17 -0
  171. package/templates/vue/server/database/seeders/01-users.ts +15 -0
  172. package/templates/vue/server/route/api.ts +24 -0
  173. package/templates/vue/server/route/ws.ts +18 -0
  174. package/templates/vue/server/schedule/app.ts +11 -0
  175. package/templates/vue/src/App.vue +92 -0
  176. package/templates/vue/src/api.ts +48 -0
  177. package/templates/vue/src/env.d.ts +5 -0
  178. package/templates/vue/src/main.ts +4 -0
  179. package/templates/vue/src/socket.ts +5 -0
  180. package/templates/vue/tests/auth.test.ts +77 -0
  181. package/templates/vue/tests/greeting.test.ts +29 -0
  182. package/templates/vue/tests/user.test.ts +58 -0
  183. package/templates/vue/tsconfig.app.json +22 -0
  184. package/templates/vue/tsconfig.json +7 -0
  185. package/templates/vue/tsconfig.server.json +34 -0
  186. package/templates/vue/vite.config.ts +18 -0
  187. package/templates/vue/vitest.config.ts +8 -0
@@ -0,0 +1,383 @@
1
+ # {{name}}
2
+
3
+ 基于 hazejs 的 Node 服务端项目。
4
+
5
+ ## 命令
6
+
7
+ ```bash
8
+ pnpm dev # 开发服务器,修改代码即时生效(进程不重启)
9
+ pnpm routes # 查看路由表
10
+ pnpm typegen # 生成配置键 / 路由名类型(dev 时自动执行)
11
+ pnpm test # 运行测试
12
+ pnpm build # 构建到 dist/
13
+ pnpm start # 运行构建产物(产物自带依赖,dist/ 可整体拷走)
14
+ pnpm preview # 按构建时的部署预设本地运行产物(Lambda / Vercel / Workers 等也可预览)
15
+ ```
16
+
17
+ ## 代码生成
18
+
19
+ ```bash
20
+ npx haze make:controller User --resource # app/controller/UserController.ts
21
+ npx haze make:service User # app/service/UserService.ts
22
+ npx haze make:middleware Authenticate # app/middleware/Authenticate.ts
23
+ npx haze make:validate User # app/validate/user.ts
24
+ npx haze make:test user # tests/user.test.ts
25
+ npx haze stub:publish # 复制模板到 stubs/,按团队规范定制
26
+ ```
27
+
28
+ 其余:`make:repository`、`make:provider`、`make:config`、`make:route`。名称可带子目录,如 `admin/User`。
29
+
30
+ ## 类型安全
31
+
32
+ 开发服务器会把配置键与命名路由生成到 `.haze/types/haze.d.ts`:
33
+
34
+ ```ts
35
+ config('app.name') // string
36
+ router.url('api.hello') // 路由名有补全,参数按路径检查
37
+ ```
38
+
39
+ ## 日志
40
+
41
+ - 开发环境:彩色输出,自动记录访问日志 `GET /api/hello 200 3ms`
42
+ - 生产环境:JSON 行日志(含 requestId),便于日志平台采集
43
+ - 请求内使用 `ctx.logger.info(...)`,自动带上请求 ID
44
+
45
+ ## 响应格式
46
+
47
+ - `app/response/Formatter.ts`:全局成功 / 失败响应结构(默认 `{ code, message, data }`)
48
+ - `app/interceptor/*.ts`:出站拦截器,拿到最终 Response 后可改头或替换(`haze make:interceptor`)
49
+ - 单个路由跳过:`.meta({ envelope: false })` 或 `return raw(value)`
50
+
51
+ ## 数据库
52
+
53
+ 默认使用 SQLite(`config/database.ts`,文件位于 `data/app.db`),表结构在 `database/schema/*.ts`(drizzle),模型在 `app/model/`:
54
+
55
+ ```bash
56
+ pnpm db:migrate # 执行迁移(首次运行前执行)
57
+ pnpm db:seed # 数据填充(database/seeders)
58
+ npx haze make:model Post # 生成模型 + 表结构
59
+ pnpm db:generate --name create_posts # 表结构变化后生成迁移
60
+ ```
61
+
62
+ ```ts
63
+ const user = await User.findOrFail(1) // 404:User 1 不存在
64
+ await User.where('name', 'like', '%a%').order('id', 'desc').paginate(1, 20)
65
+ await Db.table(users).where('id', 1).update({ name: 'Ada' })
66
+ await Db.transaction(async () => { /* 内部的 Db / Model 操作自动加入事务 */ })
67
+ ```
68
+
69
+ 切换到 PostgreSQL / MySQL:安装 `postgres` 或 `mysql2`,修改 `driver` 与 `url`,表结构改用 `drizzle-orm/pg-core` / `mysql-core`。
70
+
71
+ MongoDB:安装 `mongodb`,连接配置 `{ driver: 'mongodb', url: 'mongodb://…/app' }`,模型使用 `@hazejs/orm/mongo` 的 `MongoModel`(`npx haze make:model Article --dialect mongodb`),查询 API 与 SQL 一致。
72
+
73
+ ## 多应用
74
+
75
+ 在 `config/app.ts` 中声明应用,代码放在 `app/<name>/`(controller、route、config、middleware 配置、exception/Handler.ts、response/Formatter.ts):
76
+
77
+ ```ts
78
+ export default defineConfig(() => ({
79
+ name: env('APP_NAME', 'my-app'),
80
+ apps: {
81
+ admin: { prefix: '/admin' }, // /admin/** → app/admin
82
+ api: { domain: 'api.example.com' }, // 按域名分发,支持 *.example.com
83
+ },
84
+ }))
85
+ ```
86
+
87
+ - 应用配置 `app/<name>/config/*.ts` 与全局配置深合并,请求内 `config()`、`ctx.config` 读取应用配置
88
+ - 路由名自动带应用前缀:`router.url('admin.users.index')`
89
+ - 生成代码:`npx haze make:controller User --app admin`
90
+
91
+ ## 认证与安全
92
+
93
+ 模板自带 JWT 登录示例(`config/auth.ts`):
94
+
95
+ ```bash
96
+ curl -X POST localhost:3000/api/auth/register -H 'content-type: application/json' \
97
+ -d '{"name":"Ada","email":"ada@example.com","password":"correct horse"}'
98
+ curl -X POST localhost:3000/api/auth/login -H 'content-type: application/json' \
99
+ -d '{"email":"ada@example.com","password":"correct horse"}' # → { token, expiresIn, ... }
100
+ curl localhost:3000/api/auth/me -H 'authorization: Bearer <token>'
101
+ ```
102
+
103
+ - `APP_KEY`(`.env`,创建项目时已生成)用于签名 Cookie、加密 Session 与 JWT;更换:`npx haze key:generate --force`
104
+ - 路由保护:`.middleware('auth')`、`'auth:web,api'`(任一 Guard 通过)、`'guest'`;代码中 `await ctx.auth.user()`、`ctx.auth.jwt().login(user)`
105
+ - 浏览器同源场景可加 session Guard:`web: { driver: 'session', provider: 'users' }`,`ctx.auth.session('web').attempt(credentials)`
106
+ - 测试:`await t.actingAs(user)` 返回已登录的客户端
107
+ - 密码:`Hash.make(password)` / `Hash.verify(hash, password)`(scrypt)
108
+
109
+ 授权(`app/policy/*.ts` 自动注册,`npx haze make:policy Post` 生成):
110
+
111
+ ```ts
112
+ export default definePolicy(User, {
113
+ before: (actor: User | undefined) => actor?.role === 'admin' || undefined, // 管理员全部放行
114
+ update: (actor: User, user: User) => actor.id === user.id || deny('只能修改自己的资料'),
115
+ })
116
+ ```
117
+
118
+ - 控制器:`await ctx.authorize('update', user)`(未登录 401,无权限 403),`await ctx.can('delete', user)` 返回 boolean
119
+ - 路由:`.middleware('can:admin.access')`、`'can:create,Post'`、`'can:update,Post,id'`(按路由参数加载模型,实例在 `ctx.get('post')`)
120
+ - 能力名由 typegen 生成类型,拼错会在类型检查时报错;与模型无关的能力用 `defineAbilities({ 'admin.access': user => ... })`
121
+ - CSRF:使用 Session Guard 等 Cookie 认证时,在中间件中加入 `csrf`。前端回传 `XSRF-TOKEN` Cookie 的值(`X-XSRF-TOKEN` 头或表单 `_token` 字段),否则返回 419
122
+
123
+ 内置中间件(`config/middleware.ts` 中按名使用,配置文件放在 `config/` 下同名文件):
124
+
125
+ | 名字 | 用途 | 配置 |
126
+ |------|------|------|
127
+ | `cors` | 跨域,需放在 global | `cors.ts`(`defineCorsConfig`) |
128
+ | `secureHeaders` | nosniff、frame-options、referrer-policy 等安全头,生产 https 下加 HSTS | `secureHeaders.ts` |
129
+ | `compress` | br / gzip / deflate 压缩响应 | `compress.ts` |
130
+ | `throttle:60,60` / `throttle:api` | 限流(默认按用户或 IP),返回 RateLimit-* 头与 429 | `throttle.ts`(命名规则、共享存储) |
131
+ | `bodyLimit:10mb` | 调整单个路由的请求体上限(默认 `app.bodyLimit`,1mb) | — |
132
+ | `session` | 启动会话:`ctx.session.get / set / flash / regenerate` | `session.ts`(cookie / memory / 自定义存储) |
133
+ | `auth` / `guest` | 认证 | `auth.ts` |
134
+ | `can:<ability>[,Model[,param]]` | 授权 | `app/policy/*.ts` |
135
+ | `csrf` | CSRF 同步令牌(419) | `csrf.ts`(except、cookie、testing) |
136
+ | `cacheResponse:60[,user]` | 缓存 GET 响应 | `cache.ts`(responseStore) |
137
+
138
+ 文件上传:multipart 请求的 `await ctx.body()` 中文件字段为 `UploadedFile`(继承 File,可直接用 valibot 的 `v.file()`、`v.maxSize()`、`v.mimeType()` 校验),`await (await ctx.file('avatar'))?.store('avatars', 'public')` 保存到存储磁盘(见下方“文件存储与邮件”)。
139
+
140
+ ## 缓存
141
+
142
+ `config/cache.ts`(默认内存;设置 `CACHE_STORE=redis` 与 `REDIS_URL` 切换到 Redis,需要 `pnpm add ioredis`):
143
+
144
+ ```ts
145
+ import { Cache } from '@hazejs/core'
146
+
147
+ const stats = await Cache.remember('stats', '10m', () => computeStats())
148
+ await Cache.increment('views') // 原子自增
149
+ await Cache.lock('report', '30s').run(() => generateReport()) // 原子锁(拿不到时抛 LockTimeoutException)
150
+ await Cache.store('redis').get('key') // 指定存储
151
+ ```
152
+
153
+ | 驱动 | 适用 | 说明 |
154
+ |------|------|------|
155
+ | `memory` | 开发、单实例 | 进程内 |
156
+ | `redis` | 多实例 Node / Docker / Lambda | ioredis,原子自增与锁 |
157
+ | `upstash` | Vercel / Netlify / Workers | HTTP,需要 `@upstash/redis` |
158
+ | `kv` | Cloudflare Workers | 最终一致、非原子:适合缓存与 Session,不支持锁 |
159
+
160
+ - 多实例共享:`config/throttle.ts` 的 `store` 与 Session 的 `{ driver: 'cache', store: 'redis' }` 使用同一个缓存存储
161
+ - Workers 上的限流:限流规则配置 `binding: 'API_RATE_LIMITER'`,使用 Cloudflare Rate Limiting 绑定(原子、全局)
162
+ - 响应缓存:`.middleware('cacheResponse:60')` 缓存 GET 响应(`X-Cache: HIT / MISS`),`cacheResponse:60,user` 按用户区分
163
+
164
+ ## 队列与定时任务
165
+
166
+ 任务放在 `app/job/*.ts`(`npx haze make:job SendInvoice` 生成),配置在 `config/queue.ts`:
167
+
168
+ ```ts
169
+ await SendWelcomeEmail.dispatch({ userId: 1 }) // 默认 memory:在本进程消费
170
+ await SendWelcomeEmail.dispatch({ userId: 1 }, { delay: '10m' }) // 延迟
171
+ ```
172
+
173
+ - 重试:`defineJob({ tries: 3, backoff: [10, 60], timeout: '30s', failed(payload, error) { ... } })`
174
+ - 多实例:设置 `QUEUE_CONNECTION=redis`(需要 `pnpm add bullmq ioredis`),单独运行消费进程 `pnpm queue`(`haze queue:work`,构建后为 `node dist/worker.mjs queue`)
175
+ - 失败任务:`haze queue:failed` / `queue:retry [id]` / `queue:forget <id>` / `queue:flush`
176
+ - 测试:`const fake = Queue.fake()` 后 `fake.assertDispatched(SendWelcomeEmail, p => p.userId === 1)`
177
+
178
+ 定时任务在 `schedule/*.ts`:
179
+
180
+ ```ts
181
+ export default defineSchedule((s) => {
182
+ s.call('report', () => sendReport()).weekdays().at('09:00').timezone('Asia/Shanghai')
183
+ s.job(PruneTokens, { days: 30 }).daily().onOneServer() // 多实例时只执行一次
184
+ s.cron('*/5 * * * *', 'sync', () => sync()).withoutOverlapping() // 上次未结束则跳过
185
+ })
186
+ ```
187
+
188
+ | 环境 | 运行方式 |
189
+ |------|----------|
190
+ | 开发 | `pnpm schedule`(`haze schedule:work`),`haze schedule:list` 查看下次执行时间 |
191
+ | node / bun / deno / docker | `node dist/worker.mjs schedule`(常驻),或系统 cron 每分钟执行 `node dist/worker.mjs schedule:run` |
192
+ | cloudflare | 自动生成 Cron Triggers(每分钟);队列用 Cloudflare Queues(`driver: 'cloudflare'`,Worker 导出 queue()) |
193
+ | aws-lambda | 同一份产物,handler 设为 `index.schedule`,EventBridge `rate(1 minute)` 触发 |
194
+ | vercel | 自动配置 Vercel Cron 调用 `/__haze/schedule`(需要设置 `CRON_SECRET`;Hobby 计划只允许每天一次,用 `haze.config.ts` 的 `vercel.cron` 调整) |
195
+
196
+ ## 文件存储与邮件
197
+
198
+ 磁盘配置在 `config/filesystem.ts`(默认 `local` 与 `public`;`s3` 需要 `pnpm add @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner`,Workers 上用 `r2` 绑定):
199
+
200
+ ```ts
201
+ import { Storage } from '@hazejs/core'
202
+
203
+ const path = await file.store('avatars', 'public') // 上传文件,随机文件名
204
+ Storage.disk('public').url(path) // /storage/avatars/xxx.png(public 磁盘由框架直接提供)
205
+ await Storage.put('reports/2026.csv', csv)
206
+ await Storage.disk('s3').temporaryUrl('reports/2026.csv', '10m') // S3 预签名;local / r2 为 APP_KEY 签名的链接
207
+ return Storage.download('reports/2026.csv', '报表.csv') // 控制器中直接返回文件响应
208
+ ```
209
+
210
+ 邮件定义在 `app/mail/*.ts`,配置在 `config/mail.ts`(默认 `log`,只写日志):
211
+
212
+ ```ts
213
+ export default defineMail((payload: { name: string }) => ({
214
+ subject: `欢迎,${payload.name}`,
215
+ html: mailLayout({ body: html`<p>你好,${payload.name}</p>${button('开始使用', url)}` }), // 插值自动转义,纯文本自动生成
216
+ }))
217
+
218
+ await Mail.to(user.email).send(WelcomeMail, { name: user.name })
219
+ await Mail.to(user.email).queue(WelcomeMail, { name: user.name }) // 排队(任务 mail:send,失败重试)
220
+ await Mail.to(user.email).later('10m', WelcomeMail, { name: user.name })
221
+ ```
222
+
223
+ | 驱动 | 说明 |
224
+ |------|------|
225
+ | `log` / `array` | 只写日志 / 保存在内存中 |
226
+ | `smtp` | nodemailer(`pnpm add nodemailer`),不支持 Workers |
227
+ | `resend` / `postmark` / `mailgun` | HTTP 接口,可在 Workers 上运行 |
228
+ | `ses` | Amazon SES(`pnpm add @aws-sdk/client-sesv2`) |
229
+
230
+ - 本地查看邮件:运行 [Mailpit](https://mailpit.axllent.org)(`docker run -p 1025:1025 -p 8025:8025 axllent/mailpit`),设置 `MAIL_MAILER=smtp`(并 `pnpm add nodemailer`),打开 http://localhost:8025
231
+ - 附件:`attachments: [{ filename: 'a.pdf', content: bytes }]`,或 `{ filename, disk: 's3', path }` 发送时从磁盘读取
232
+ - 测试:`const mail = Mail.fake()` 后 `mail.assertSent(WelcomeMail, m => MailFake.hasRecipient(m, 'ada@example.com'))`;`const disk = Storage.fake('public')` 后 `await disk.assertExists(path)`
233
+
234
+ ## 事件与 WebSocket
235
+
236
+ 事件放在 `app/event/*.ts`,监听器在 `app/listener/*.ts`(`npx haze make:event OrderPaid` / `make:listener SendReceipt`),都会自动注册:
237
+
238
+ ```ts
239
+ export default defineEvent<{ userId: number, email: string }>() // app/event/UserRegistered.ts
240
+ export default defineListener(UserRegistered, ({ userId }) => SendWelcomeEmail.dispatch({ userId }))
241
+ export default defineListener(UserRegistered, handler, {
242
+ queue: { tries: 3 }
243
+ }) // 排队异步执行
244
+ await UserRegistered.emit({ userId: user.id, email: user.email })
245
+ ```
246
+
247
+ - 同步监听器按文件名顺序执行,返回 `false` 时不再执行后续监听器;排队监听器作为队列任务执行(payload 以 JSON 传递)
248
+ - 测试:`const events = Events.fake([UserRegistered])` 后 `events.assertEmitted(UserRegistered, p => p.userId === 1)`
249
+
250
+ WebSocket 在路由中定义(示例:`route/ws.ts`,`ws://<host>/api/ws`):
251
+
252
+ ```ts
253
+ r.ws('/api/chat', {
254
+ open(peer, ctx) { peer.subscribe('chat') },
255
+ message(peer, message) { peer.publish('chat', message.text()) },
256
+ }).middleware('auth') // 升级请求同样经过中间件
257
+
258
+ Socket.publish('users', { type: 'user.registered' }) // 在接口、任务、监听器中推送
259
+ ```
260
+
261
+ - 开发:`pnpm dev` 中直接可用,修改后新连接使用新代码;产物:node / bun / deno / docker 直接支持,`pnpm preview` 也可连接
262
+ - 多实例:设置 `WS_BROADCAST=redis`(`config/websocket.ts`),广播经 Redis 到达所有实例
263
+ - Cloudflare:自动生成 Durable Object(`HazeWebSocket`),所有连接在同一个 DO 中,`Socket.publish` 经 RPC 转发
264
+ - Vercel / Netlify / AWS Lambda:平台不支持 WebSocket(升级请求返回 426)
265
+
266
+ ## Socket.IO
267
+
268
+ 需要兼容 socket.io-client 时安装 `pnpm add @hazejs/socket.io socket.io`,并:
269
+
270
+ ```ts
271
+ // app/provider/SocketProvider.ts
272
+ export default socketIoProvider()
273
+
274
+ // app/socket/chat.ts —— 命名空间 /chat
275
+ export default defineNamespace({
276
+ middleware: [
277
+ authenticate()
278
+ ],
279
+ connection(socket, io, chat) {
280
+ socket.on('chat:send', (text, ack) => {
281
+ chat.emit('chat:message', text)
282
+ ack?.('ok')
283
+ })
284
+ },
285
+ })
286
+ ```
287
+
288
+ 与 HTTP 共用端口(`/socket.io/`),`SocketIO.to(room).emit()` 在任意位置推送;多实例在 `config/socketio.ts` 配置 `adapter: { driver: 'redis' }`(`@socket.io/redis-adapter`)。支持 Node(开发、node / docker 产物)与 Bun,Deno / Workers / Serverless 不支持。
289
+
290
+ ## 部署
291
+
292
+ ```bash
293
+ npx haze build --preset node # 默认:node dist/index.mjs(依赖已打包或追踪进产物,可整体拷走)
294
+ npx haze build --preset docker # dist/ 即构建上下文:docker build -t app dist
295
+ npx haze build --preset bun # bun dist/index.mjs
296
+ npx haze build --preset deno # deno run -A dist/server/index.mjs
297
+ npx haze build --preset vercel # .vercel/output → vercel deploy --prebuilt
298
+ npx haze build --preset netlify # .netlify/v1/functions + netlify.toml
299
+ npx haze build --preset aws-lambda # 打包 dist/lambda,handler 为 index.handler
300
+ npx haze build --preset cloudflare # dist/wrangler.json → wrangler deploy -c dist/wrangler.json
301
+ ```
302
+
303
+ 也可在 `haze.config.ts` 中设置 `preset`。构建会自动带上项目安装的数据库驱动。
304
+
305
+ 依赖处理与 Nitro 一致:能打包的依赖都打进产物,数据库驱动、原生模块等保持 external,并按实际引用的文件追踪复制到产物的 `node_modules`(node / bun / deno / aws-lambda / vercel)。可在 `haze.config.ts` 中调整:
306
+
307
+ ```ts
308
+ export default defineHazeConfig({
309
+ externals: {
310
+ traceDeps: ['sharp', 'some-pkg*'], // 保持 external 并追踪(* 表示整包复制)
311
+ inline: ['postgres'], // 强制打包
312
+ // trace: false, // node / bun 退回依赖项目的 node_modules
313
+ },
314
+ })
315
+ ```
316
+
317
+ 在 macOS / Windows 上构建 aws-lambda、vercel 产物时,如果追踪到了原生二进制,构建会给出警告:请在 Linux(CI / Docker)上构建。
318
+
319
+ `pnpm preview`(`haze preview`,全栈项目也可直接 `vite preview`)按构建时的预设运行产物本身:node / bun / deno / docker 启动产物进程,aws-lambda / vercel / netlify 在进程内调用函数,cloudflare 用 Miniflare 运行 Worker(D1 迁移自动应用到本地预览库)。
320
+
321
+ ### 生产迁移
322
+
323
+ 迁移文件打包进产物,生产环境不需要源码与 drizzle-kit:
324
+
325
+ | 预设 | 执行迁移 |
326
+ |------|----------|
327
+ | node / bun / deno | `node dist/migrate.mjs`(deno 预设为 `dist/server/migrate.mjs`;`--connection <name>` / `--all`) |
328
+ | docker | `docker run --rm <镜像> node server/migrate.mjs` |
329
+ | aws-lambda | 同一份产物,handler 设为 `index.migrate` 调用一次(事件 `{ "connection"?, "all"? }`) |
330
+ | vercel / netlify | 在构建命令前执行 `npx haze db:migrate`(使用生产库的 `DATABASE_URL`) |
331
+ | cloudflare(D1) | `npx wrangler d1 migrations apply <库名> --remote -c dist/wrangler.json` |
332
+ | cloudflare(Turso) | 在 CI 中执行 `npx haze db:migrate` |
333
+
334
+ ### Cloudflare:D1 / Turso
335
+
336
+ Workers 上可用 `d1`(绑定)与 `turso`(纯 HTTP 的 libsql 客户端)两种驱动,二者与 `sqlite` 共用同一套表结构与迁移:
337
+
338
+ ```ts
339
+ // config/database.ts —— 本地用 SQLite 文件,部署到 Workers 时切换为 D1
340
+ export default defineDatabaseConfig(() => ({
341
+ default: 'main',
342
+ connections: {
343
+ main: env('DB_DRIVER', 'sqlite') === 'd1'
344
+ ? { driver: 'd1', binding: 'DB' }
345
+ : {
346
+ driver: 'sqlite',
347
+ url: env('DATABASE_URL', 'file:./data/app.db')
348
+ },
349
+ // Turso:{ driver: 'turso', url: env('DATABASE_URL'), authToken: env('DATABASE_AUTH_TOKEN') }
350
+ },
351
+ }))
352
+ ```
353
+
354
+ ```ts
355
+ // haze.config.ts —— 合并进 dist/wrangler.json;D1 默认使用产物中的 migrations/
356
+ export default defineHazeConfig({
357
+ cloudflare: {
358
+ wrangler: {
359
+ vars: { DB_DRIVER: 'd1' },
360
+ d1_databases: [
361
+ { binding: 'DB', database_name: 'my-db', database_id: '<id>' }
362
+ ],
363
+ },
364
+ },
365
+ })
366
+ ```
367
+
368
+ D1 不支持交互式事务(`Db.transaction()` 会报错并提示),原子写入请用 `(await Db.drizzle()).batch([...])`。
369
+
370
+ ## 目录
371
+
372
+ ```
373
+ app/
374
+ controller/ 控制器:接收参数、调用服务、返回数据
375
+ service/ 业务服务(默认单例,通过 inject() 注入)
376
+ validate/ 请求校验(valibot,任何 Standard Schema 库均可)
377
+ middleware/ 中间件
378
+ exception/ 全局异常处理 Handler.ts
379
+ provider/ 服务提供者:注册绑定、启动逻辑
380
+ config/ 配置(文件名即配置键,如 config/app.ts → config('app.name'))
381
+ route/ 路由(集中定义)
382
+ tests/ 测试(@hazejs/testing)
383
+ ```
@@ -0,0 +1,9 @@
1
+ node_modules
2
+ dist
3
+ coverage
4
+ .env.local
5
+ .env.*.local
6
+ *.log
7
+ .haze
8
+ data
9
+ storage
@@ -0,0 +1,45 @@
1
+ import type { HttpContext } from '@hazejs/core'
2
+ import { Hash, Storage, UnauthorizedException, validate } from '@hazejs/core'
3
+ import UserRegistered from '~/app/event/UserRegistered'
4
+ import { User } from '~/app/model/User'
5
+ import { AvatarSchema, LoginSchema, RegisterSchema } from '~/app/validate/auth'
6
+
7
+ /**
8
+ * JWT 登录示例(config/auth.ts 的 api Guard):
9
+ * 客户端保存 login / register 返回的 token,之后请求带上 `Authorization: Bearer <token>`
10
+ */
11
+ export class AuthController {
12
+ async register(ctx: HttpContext) {
13
+ const { password, ...input } = await validate(RegisterSchema, await ctx.body())
14
+ const user = await User.create({ ...input, password: await Hash.make(password) })
15
+ // 事件:监听器投递欢迎邮件任务、推送实时消息(app/listener)
16
+ await UserRegistered.emit({ userId: user.id, email: user.email })
17
+ return ctx.created({
18
+ user,
19
+ ...await ctx.auth.jwt().login(user),
20
+ })
21
+ }
22
+
23
+ async login(ctx: HttpContext) {
24
+ const issued = await ctx.auth.jwt().attempt(await validate(LoginSchema, await ctx.body()))
25
+ if (!issued)
26
+ throw new UnauthorizedException('邮箱或密码错误')
27
+ return issued
28
+ }
29
+
30
+ /** 需要 auth 中间件:未登录时返回 401 */
31
+ me(ctx: HttpContext) {
32
+ return ctx.auth.user()
33
+ }
34
+
35
+ /** 上传头像(multipart 字段 avatar):保存到 public 磁盘,经 /storage/<path> 访问 */
36
+ async avatar(ctx: HttpContext) {
37
+ const { avatar } = await validate(AvatarSchema, await ctx.body())
38
+ const path = await (await ctx.file('avatar'))!.store('avatars', 'public')
39
+ return ctx.created({
40
+ path,
41
+ url: Storage.disk('public').url(path),
42
+ size: avatar.size,
43
+ })
44
+ }
45
+ }
@@ -0,0 +1,23 @@
1
+ import type { HttpContext } from '@hazejs/core'
2
+ import type { GreetSchema } from '~/app/validate/greeting'
3
+ import { Config, inject } from '@hazejs/core'
4
+ import { GreetingService } from '~/app/service/GreetingService'
5
+
6
+ /** 控制层:接收参数 → 调用服务 → 返回数据(框架统一包装为 { code, message, data }) */
7
+ export class GreetingController {
8
+ private greeting = inject(GreetingService)
9
+ private config = inject(Config)
10
+
11
+ index() {
12
+ return {
13
+ app: this.config.get('app.name'),
14
+ message: this.greeting.greet('hazejs'),
15
+ }
16
+ }
17
+
18
+ store(ctx: HttpContext<{ body: typeof GreetSchema }>) {
19
+ return {
20
+ message: this.greeting.greet(ctx.validated.body.name),
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,38 @@
1
+ import type { HttpContext } from '@hazejs/core'
2
+ import type { UserIdParams } from '~/app/validate/user'
3
+ import { validate } from '@hazejs/core'
4
+ import { User } from '~/app/model/User'
5
+ import { CreateUserSchema, UpdateUserSchema } from '~/app/validate/user'
6
+
7
+ type WithId = HttpContext<{ params: typeof UserIdParams }>
8
+
9
+ /** RESTful 资源控制器:路由见 route/api.ts 中的 r.resource('/users', UserController) */
10
+ export class UserController {
11
+ index(ctx: HttpContext) {
12
+ return User.order('id', 'desc').paginate(Number(ctx.query('page') ?? 1), 20)
13
+ }
14
+
15
+ show(ctx: WithId) {
16
+ return User.findOrFail(ctx.validated.params.id)
17
+ }
18
+
19
+ async store(ctx: HttpContext) {
20
+ const input = await validate(CreateUserSchema, await ctx.body())
21
+ return ctx.created(await User.create(input))
22
+ }
23
+
24
+ async update(ctx: WithId) {
25
+ const user = await User.findOrFail(ctx.validated.params.id)
26
+ // 授权:app/policy/UserPolicy.ts
27
+ await ctx.authorize('update', user)
28
+ user.fill(await validate(UpdateUserSchema, await ctx.body()))
29
+ return user.save()
30
+ }
31
+
32
+ async destroy(ctx: WithId) {
33
+ const user = await User.findOrFail(ctx.validated.params.id)
34
+ await ctx.authorize('delete', user)
35
+ await user.delete()
36
+ return ctx.noContent()
37
+ }
38
+ }
@@ -0,0 +1,6 @@
1
+ import { defineEvent } from '@hazejs/core'
2
+
3
+ /** 用户注册成功(AuthController.register 触发,监听器见 app/listener) */
4
+ const UserRegistered = defineEvent<{ userId: number, email: string }>()
5
+
6
+ export default UserRegistered
@@ -0,0 +1,13 @@
1
+ import type { HttpContext } from '@hazejs/core'
2
+ import { ExceptionHandler } from '@hazejs/core'
3
+
4
+ /**
5
+ * 全局异常处理:report() 负责记录 / 上报,render() 负责生成响应。
6
+ * 开发环境中浏览器访问出错的地址会看到带源码定位的错误页。
7
+ */
8
+ export default class Handler extends ExceptionHandler {
9
+ override report(error: unknown, ctx: HttpContext) {
10
+ // ctx.logger 已携带 requestId;可在此接入 Sentry 等上报服务
11
+ return super.report(error, ctx)
12
+ }
13
+ }
@@ -0,0 +1,19 @@
1
+ import { defineJob, Mail } from '@hazejs/core'
2
+ import WelcomeMail from '~/app/mail/WelcomeMail'
3
+ import { User } from '~/app/model/User'
4
+
5
+ /** 注册成功后投递(app/listener/QueueWelcomeEmail.ts):发送欢迎邮件,失败时按 backoff 重试 */
6
+ const SendWelcomeEmail = defineJob({
7
+ tries: 3,
8
+ backoff: [10, 60],
9
+ timeout: '30s',
10
+ async handle(payload: { userId: number }, job) {
11
+ const user = await User.find(payload.userId)
12
+ if (!user)
13
+ return
14
+ await Mail.to({ address: user.email, name: user.name }).send(WelcomeMail, { name: user.name, url: 'http://localhost:5173' })
15
+ job.logger.info(`Welcome email sent to ${user.email}`)
16
+ },
17
+ })
18
+
19
+ export default SendWelcomeEmail
@@ -0,0 +1,7 @@
1
+ import { defineListener, Socket } from '@hazejs/core'
2
+ import UserRegistered from '~/app/event/UserRegistered'
3
+
4
+ /** 实时推送:订阅了 users 的 WebSocket 连接(route/ws.ts)收到新用户注册的消息 */
5
+ export default defineListener(UserRegistered, ({ userId }) => {
6
+ Socket.publish('users', { type: 'user.registered', userId })
7
+ })
@@ -0,0 +1,6 @@
1
+ import { defineListener } from '@hazejs/core'
2
+ import UserRegistered from '~/app/event/UserRegistered'
3
+ import SendWelcomeEmail from '~/app/job/SendWelcomeEmail'
4
+
5
+ /** 注册后投递欢迎邮件任务(耗时操作放到队列,不阻塞注册响应) */
6
+ export default defineListener(UserRegistered, ({ userId }) => SendWelcomeEmail.dispatch({ userId }))
@@ -0,0 +1,17 @@
1
+ import { button, defineMail, html, mailLayout } from '@hazejs/core'
2
+
3
+ /** 欢迎邮件(app/job/SendWelcomeEmail.ts 发送):函数返回主题与 html,纯文本版本自动生成 */
4
+ export default defineMail((payload: { name: string, url: string }) => ({
5
+ subject: `欢迎加入,${payload.name}`,
6
+ html: mailLayout({
7
+ brand: '{{name}}',
8
+ preheader: '感谢注册,开始使用吧',
9
+ body: html`
10
+ <h1 style="margin:0 0 16px;font-size:22px">你好,${payload.name}</h1>
11
+ <p>感谢注册!点击下方按钮开始使用。</p>
12
+ ${button('开始使用', payload.url)}
13
+ <p style="color:#71717a">如果这不是你本人的操作,请忽略这封邮件。</p>
14
+ `,
15
+ footer: '此邮件由系统自动发送,请勿直接回复',
16
+ }),
17
+ }), { name: 'WelcomeMail' })
@@ -0,0 +1,11 @@
1
+ import { Model } from '@hazejs/orm'
2
+ import { users } from '~/database/schema/users'
3
+
4
+ /**
5
+ * 用户模型:字段与类型来自 database/schema/users.ts。
6
+ * 常用:User.findOrFail(id)、User.where('name', 'Ada').paginate(1, 20)、User.create(data)、user.save()
7
+ */
8
+ export class User extends Model.define(users) {
9
+ // 序列化(接口返回)时隐藏密码哈希
10
+ static override hidden = ['password']
11
+ }
@@ -0,0 +1,14 @@
1
+ import { definePolicy, deny } from '@hazejs/core'
2
+ import { User } from '~/app/model/User'
3
+
4
+ /**
5
+ * 用户的授权策略(app/policy 下自动注册):
6
+ * 控制器中 await ctx.authorize('update', user);路由上也可用 .middleware('can:update,User,id')
7
+ * 未登录用户默认被拒绝(authorize 返回 401),已登录但无权限返回 403 与 deny() 的消息。
8
+ */
9
+ export default definePolicy(User, {
10
+ // 管理员拥有全部权限
11
+ before: (actor: User | undefined) => actor?.role === 'admin' || undefined,
12
+ update: (actor: User, user: User) => actor.id === user.id || deny('只能修改自己的资料'),
13
+ delete: (actor: User, user: User) => actor.id === user.id || deny('只能删除自己的账号'),
14
+ })
@@ -0,0 +1,9 @@
1
+ import { defineProvider } from '@hazejs/core'
2
+ import { GreetingService } from '~/app/service/GreetingService'
3
+
4
+ /** 服务提供者:register() 中注册绑定,boot() 中执行启动逻辑 */
5
+ export default defineProvider({
6
+ register(container) {
7
+ container.singleton(GreetingService)
8
+ },
9
+ })
@@ -0,0 +1,4 @@
1
+ import { databaseProvider } from '@hazejs/orm'
2
+
3
+ // 注册数据库:读取 config/database.ts,Db / Model 自动使用该配置
4
+ export default databaseProvider()
@@ -0,0 +1,17 @@
1
+ import type { HttpContext, NormalizedError } from '@hazejs/core'
2
+ import { ResponseFormatter } from '@hazejs/core'
3
+
4
+ /**
5
+ * 统一响应格式:在这里修改即可全局调整成功 / 失败时出站数据的结构。
6
+ * 默认:成功 { code: 0, message: 'ok', data },失败 { code, message, data: null, errors? }。
7
+ * 单个路由跳过格式化:.meta({ envelope: false }) 或 return raw(value)。
8
+ */
9
+ export default class Formatter extends ResponseFormatter {
10
+ override success(data: unknown, ctx: HttpContext) {
11
+ return super.success(data, ctx)
12
+ }
13
+
14
+ override error(error: NormalizedError, ctx: HttpContext) {
15
+ return super.error(error, ctx)
16
+ }
17
+ }
@@ -0,0 +1,6 @@
1
+ /** 业务层:规则与流程,不感知 HTTP */
2
+ export class GreetingService {
3
+ greet(name: string): string {
4
+ return `Hello, ${name}!`
5
+ }
6
+ }