@lightfish/cli 0.0.4 → 0.0.6

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 (30) hide show
  1. package/dist/create-lightfish.js +126 -60
  2. package/dist/lightfish.js +527 -63
  3. package/dist/templates/react-vite/.agents/skills/lightfish-server/SKILL.md +447 -0
  4. package/dist/templates/react-vite/.vscode/settings.json +42 -0
  5. package/dist/templates/react-vite/AGENTS.md +254 -0
  6. package/dist/templates/react-vite/README.md +150 -0
  7. package/dist/templates/react-vite/eslint.config.js +25 -0
  8. package/dist/templates/react-vite/index.html +13 -0
  9. package/dist/templates/react-vite/lightfish-server.config.js +6 -0
  10. package/dist/templates/react-vite/package.json +41 -0
  11. package/dist/templates/react-vite/pnpm-lock.yaml +3649 -0
  12. package/dist/templates/react-vite/public/favicon.svg +1 -0
  13. package/dist/templates/react-vite/public/icons.svg +24 -0
  14. package/dist/templates/react-vite/server/router/hello/index.ts +20 -0
  15. package/dist/templates/react-vite/server/schema/index.ts +18 -0
  16. package/dist/templates/react-vite/src/assets/hero.png +0 -0
  17. package/dist/templates/react-vite/src/assets/react.svg +1 -0
  18. package/dist/templates/react-vite/src/assets/vite.svg +1 -0
  19. package/dist/templates/react-vite/src/main.css +1 -0
  20. package/dist/templates/react-vite/src/main.tsx +43 -0
  21. package/dist/templates/react-vite/src/pages/example/index.tsx +26 -0
  22. package/dist/templates/react-vite/src/pages/example/layout.tsx +31 -0
  23. package/dist/templates/react-vite/src/pages/example/model.ts +20 -0
  24. package/dist/templates/react-vite/src/pages/index.tsx +17 -0
  25. package/dist/templates/react-vite/src/pages/layout.tsx +15 -0
  26. package/dist/templates/react-vite/tsconfig.app.json +30 -0
  27. package/dist/templates/react-vite/tsconfig.json +7 -0
  28. package/dist/templates/react-vite/tsconfig.node.json +24 -0
  29. package/dist/templates/react-vite/vite.config.ts +61 -0
  30. package/package.json +6 -2
@@ -0,0 +1,447 @@
1
+ ---
2
+ name: lightfish-server
3
+ description: lightfish-server 全栈开发指南,涵盖 Schema 定义、后端路由开发、前端 apiRequest 调用,以及从数据库到接口的完整工作流
4
+ ---
5
+
6
+ # lightfish-server 全栈开发指南
7
+
8
+ ## 项目结构速览
9
+
10
+ ```
11
+ 项目根/
12
+ ├── src/ # 前端应用
13
+ ├── server/ # lightfish-server 后端
14
+ │ ├── schema/index.ts # Drizzle ORM 数据库 Schema
15
+ │ ├── router/ # 约定式路由目录
16
+ │ │ └── <module>/index.ts # 路由入口
17
+ │ └── migrations/ # 数据库迁移文件(自动生成,勿手动编辑)
18
+ ├── lightfish-server.config.js # 后端配置
19
+ └── vite.config.ts # 前端构建配置
20
+ ```
21
+
22
+ `lightfish-server.config.js` 示例:
23
+
24
+ ```js
25
+ import { defineConfig } from "@lightfish/server";
26
+
27
+ export default defineConfig({
28
+ port: 3000,
29
+ apiDir: "./server/router",
30
+ appName: "lightfish-front-app-test2", // 数据库 schema 前缀 & 路由前缀
31
+ env: "dev",
32
+ });
33
+ ```
34
+
35
+ ---
36
+
37
+ ## 一、Schema 定义
38
+
39
+ ### 位置与命名空间
40
+
41
+ Schema 入口为 `server/schema/index.ts`。使用 `pgSchema(appName)` 创建数据库命名空间,避免多应用之间的表名冲突:
42
+
43
+ ```ts
44
+ import {
45
+ pgSchema,
46
+ integer,
47
+ varchar,
48
+ boolean,
49
+ timestamp,
50
+ } from "drizzle-orm/pg-core";
51
+
52
+ const appSchema = pgSchema("lightfish-front-app-test2"); // 与 config 中 appName 一致
53
+
54
+ export const usersTable = appSchema.table("users", {
55
+ id: integer().primaryKey().generatedAlwaysAsIdentity(),
56
+ email: varchar({ length: 255 }).notNull().unique(),
57
+ displayName: varchar({ length: 255 }).notNull(),
58
+ phone: varchar({ length: 50 }),
59
+ remark: varchar({ length: 500 }),
60
+ active: boolean().notNull().default(true),
61
+ createdAt: timestamp().notNull().defaultNow(),
62
+ updatedAt: timestamp().notNull().defaultNow(),
63
+ });
64
+ ```
65
+
66
+ ### 常用列类型
67
+
68
+ | Drizzle 类型 | 对应 PG 类型 | 示列 |
69
+ | ------------------------ | ---------------- | --------- |
70
+ | `integer()` | INTEGER | 自增主键 |
71
+ | `varchar({ length: N })` | VARCHAR(N) | 字符串 |
72
+ | `boolean()` | BOOLEAN | 布尔值 |
73
+ | `timestamp()` | TIMESTAMP | 时间戳 |
74
+ | `jsonb()` | JSONB | JSON 对象 |
75
+ | `text()` | TEXT | 长文本 |
76
+ | `doublePrecision()` | DOUBLE PRECISION | 浮点数 |
77
+
78
+ ### 约束速查
79
+
80
+ - `.primaryKey()` — 主键
81
+ - `.notNull()` — 非空
82
+ - `.unique()` — 唯一约束
83
+ - `.default(value)` — 默认值
84
+ - `.defaultNow()` — 默认当前时间
85
+ - `.generatedAlwaysAsIdentity()` — 自增(适用于 integer 主键)
86
+
87
+ ---
88
+
89
+ ## 二、后端路由开发
90
+
91
+ ### 2.1 约定式路由
92
+
93
+ `server/router/` 下的文件系统目录结构自动映射为 URL 路径。
94
+
95
+ **规则**:
96
+
97
+ - 只有 `index.ts` / `index.js` 被识别为路由文件
98
+ - 目录名即 URL 路径名(区分大小写)
99
+ - `[paramName]` 格式的目录名表示动态参数
100
+
101
+ ```
102
+ server/router/
103
+ ├── index.ts → /
104
+ ├── users/
105
+ │ ├── index.ts → /users
106
+ │ └── [id]/
107
+ │ └── index.ts → /users/:id
108
+ └── auth/
109
+ ├── login/
110
+ │ └── index.ts → /auth/login
111
+ └── register/
112
+ └── index.ts → /auth/register
113
+ ```
114
+
115
+ ### 2.2 Handler 基本形态
116
+
117
+ #### PostgreSQL(通过框架注入)
118
+
119
+ 框架默认注入 PG 数据库实例到 `c.get('db')`:
120
+
121
+ ```ts
122
+ import type { ContextWithDb } from "@lightfish/server";
123
+
124
+ // 方法约束(可选)
125
+ export const method = "POST";
126
+
127
+ type SomeBody = {
128
+ // 请求体类型
129
+ };
130
+
131
+ export default async function handler(c: ContextWithDb) {
132
+ // 1. 获取数据库
133
+ const db = c.get("db");
134
+ if (!db) {
135
+ throw new Error("Database not configured");
136
+ }
137
+
138
+ // 2. 读取请求体
139
+ const body = await c.req.json<SomeBody>();
140
+
141
+ // 3. 读取动态路由参数
142
+ const { id } = c.get("params");
143
+
144
+ // 4. 业务逻辑...
145
+
146
+ // 5. 直接返回业务数据(框架自动包 { success, data, code })
147
+ return { id: 1, name: "Alice" };
148
+ }
149
+ ```
150
+
151
+ #### MySQL(通过 @lightfish/server/pool)
152
+
153
+ MySQL 连接由 handler 自行通过 `@lightfish/server/pool` 创建,框架不自动注入:
154
+
155
+ ```ts
156
+ import { createMySQLDb } from "@lightfish/server/pool";
157
+
158
+ const db = createMySQLDb({
159
+ connectionString: "mysql://user:pass@host:3306/dbname",
160
+ max: 10,
161
+ });
162
+
163
+ export default async function handler() {
164
+ const products = await db.execute("SELECT * FROM products");
165
+ return products[0];
166
+ }
167
+ ```
168
+
169
+ ### 2.3 HTTP 方法约束
170
+
171
+ 通过 `export const method` 声明:
172
+
173
+ ```ts
174
+ export const method = "GET";
175
+
176
+ export const method = ["GET", "POST"]; // 允许多种方法
177
+
178
+ // 不声明则所有方法均可访问
179
+ ```
180
+
181
+ ### 2.4 请求体读取
182
+
183
+ ```ts
184
+ const body = await c.req.json<T>();
185
+ ```
186
+
187
+ ### 2.5 数据库操作
188
+
189
+ 使用 `c.get('db')` 获取 Drizzle ORM 实例,配合 schema 中定义的表进行操作:
190
+
191
+ ```ts
192
+ import { eq, and, like, desc } from "drizzle-orm";
193
+ import { usersTable } from "../../../schema"; // 相对路径 import
194
+
195
+ // 查询
196
+ const [user] = await db
197
+ .select()
198
+ .from(usersTable)
199
+ .where(eq(usersTable.email, email))
200
+ .limit(1);
201
+
202
+ // 插入
203
+ const [newUser] = await db
204
+ .insert(usersTable)
205
+ .values({ email, displayName, password })
206
+ .returning({ id: usersTable.id, email: usersTable.email });
207
+
208
+ // 更新
209
+ const [updated] = await db
210
+ .update(usersTable)
211
+ .set({ displayName: newName })
212
+ .where(eq(usersTable.id, id))
213
+ .returning();
214
+
215
+ // 删除
216
+ await db.delete(usersTable).where(eq(usersTable.id, id));
217
+ ```
218
+
219
+ ### 2.6 动态路由参数
220
+
221
+ 目录 `[id]` 对应路径参数 `:id`,通过 `c.get('params')` 获取:
222
+
223
+ ```ts
224
+ export default async function handler(c: ContextWithDb) {
225
+ const { id } = c.get("params");
226
+ // id 为 string 类型
227
+ }
228
+ ```
229
+
230
+ ### 2.7 响应与错误
231
+
232
+ **成功**:直接 return 业务数据,框架统一包装:
233
+
234
+ ```ts
235
+ return { id: 1, email: "a@b.com" };
236
+ // 实际响应: { "success": true, "data": { "id": 1, "email": "a@b.com" }, "code": 200 }
237
+ ```
238
+
239
+ **错误**:throw 异常,框架统一捕获:
240
+
241
+ ```ts
242
+ import { ServerError } from "@lightfish/server/shared";
243
+
244
+ // 通用业务错误(code=400)
245
+ throw new ServerError("用户不存在");
246
+
247
+ // 自定义业务码
248
+ throw new ServerError("余额不足", 1001);
249
+
250
+ // 也可直接 throw Error(code=500)
251
+ throw new Error("系统异常");
252
+ ```
253
+
254
+ ### 2.8 import schema 的相对路径
255
+
256
+ | 文件位置 | import 路径 |
257
+ | ------------------------------ | -------------------- |
258
+ | `server/router/xxx/index.ts` | `../../schema` |
259
+ | `server/router/a/b/index.ts` | `../../../schema` |
260
+ | `server/router/a/b/c/index.ts` | `../../../../schema` |
261
+
262
+ ---
263
+
264
+ ## 三、Schema 变更与数据库同步
265
+
266
+ 修改 `server/schema/` 后,**必须**在项目根目录执行以下命令将变更同步到数据库:
267
+
268
+ ```bash
269
+ # 推荐(已安装全局 @lightfish/server)
270
+ lightfish-server db push
271
+
272
+ # 或
273
+ npx lightfish-server db push
274
+ ```
275
+
276
+ **重要规则**:
277
+
278
+ - **禁止**直接执行 `drizzle-kit` 或 `npx drizzle-kit`,其配置由 `lightfish-server` 统一管理
279
+ - 执行 `db push` 会自动处理 schema 命名空间和迁移记录
280
+
281
+ ---
282
+
283
+ ## 四、前端调用后端 API
284
+
285
+ ### 4.1 apiRequest
286
+
287
+ 前端调用本地后端服务统一使用 `@lightfish/server/api` 导出的 `apiRequest`:
288
+
289
+ ```ts
290
+ import { apiRequest } from "@lightfish/server/api";
291
+ ```
292
+
293
+ **签名与返回类型**:
294
+
295
+ ```ts
296
+ apiRequest<T = any>(
297
+ path: string, // 以 / 开头,对应 server/router/ 下的路径
298
+ config?: AxiosRequestConfig // axios 配置(method, data, headers 等)
299
+ ): Promise<{
300
+ success: boolean;
301
+ data?: T;
302
+ message?: string;
303
+ code: number;
304
+ }>
305
+ ```
306
+
307
+ **使用示例**:
308
+
309
+ ```ts
310
+ // GET 请求
311
+ const res = await apiRequest<{ id: number; name: string }>("/users");
312
+ if (res.success && res.data) {
313
+ console.log(res.data);
314
+ }
315
+
316
+ // POST 请求
317
+ const res = await apiRequest<{ id: number }>("/auth/login", {
318
+ method: "POST",
319
+ data: { email: "a@b.com", password: "123456" },
320
+ });
321
+
322
+ // 带动态参数
323
+ const res = await apiRequest(`/users/${userId}`);
324
+ ```
325
+
326
+ ### 4.2 path 规则
327
+
328
+ `path` 以 `/` 开头,对应 `server/router/` 下的文件路径。无需、也不应手动拼写任何前缀。
329
+
330
+ ### 4.3 与 openapi-fetch 的区分
331
+
332
+ | 工具 | 用途 |
333
+ | --------------------------------------- | ------------------------------------------------------------- |
334
+ | `@lightfish/server/api` 的 `apiRequest` | **调本地 lightfish-server 后端**(`server/router/` 下的接口) |
335
+ | `@/.lightfish` 的 `API` (openapi-fetch) | 调 yingdao 开放平台 API |
336
+
337
+ **简单判断**:访问自己在 `server/router/` 写的接口 → 用 `apiRequest`。
338
+
339
+ ---
340
+
341
+ ## 五、端到端开发流(文档 + 匿名问卷)
342
+
343
+ 以下是一个从零到完整调用的示例,展示了 Schema → Router → Frontend 的完整链路。
344
+
345
+ ### 5.1 定义 Schema
346
+
347
+ ```ts
348
+ // server/schema/index.ts
349
+ import {
350
+ pgSchema,
351
+ integer,
352
+ varchar,
353
+ timestamp,
354
+ jsonb,
355
+ } from "drizzle-orm/pg-core";
356
+
357
+ const appSchema = pgSchema("lightfish-front-app-test2");
358
+
359
+ export const surveysTable = appSchema.table("surveys", {
360
+ id: integer().primaryKey().generatedAlwaysAsIdentity(),
361
+ title: varchar({ length: 255 }).notNull(),
362
+ createdAt: timestamp().notNull().defaultNow(),
363
+ });
364
+ ```
365
+
366
+ ### 5.2 同步数据库
367
+
368
+ ```bash
369
+ lightfish-server db push
370
+ ```
371
+
372
+ ### 5.3 写路由
373
+
374
+ ```ts
375
+ // server/router/surveys/index.ts
376
+ import type { ContextWithDb } from "@lightfish/server";
377
+ import { ServerError } from "@lightfish/server/shared";
378
+ import { surveysTable } from "../../schema";
379
+
380
+ export const method = ["GET", "POST"];
381
+
382
+ export default async function surveys(c: ContextWithDb) {
383
+ const db = c.get("db");
384
+ if (!db) throw new ServerError("Database not configured", 503);
385
+
386
+ if (c.req.method === "POST") {
387
+ const body = await c.req.json<{ title: string }>();
388
+ if (!body.title?.trim()) throw new ServerError("Title is required");
389
+
390
+ const [survey] = await db
391
+ .insert(surveysTable)
392
+ .values({ title: body.title })
393
+ .returning();
394
+ return survey;
395
+ }
396
+
397
+ const list = await db.select().from(surveysTable);
398
+ return list;
399
+ }
400
+ ```
401
+
402
+ ### 5.4 前端调用
403
+
404
+ ```ts
405
+ import { apiRequest } from "@lightfish/server/api";
406
+
407
+ // GET
408
+ const res =
409
+ await apiRequest<{ id: number; title: string; createdAt: string }[]>(
410
+ "/surveys",
411
+ );
412
+
413
+ // POST
414
+ const res = await apiRequest<{ id: number; title: string }>("/surveys", {
415
+ method: "POST",
416
+ data: { title: "Q1 feedback" },
417
+ });
418
+ ```
419
+
420
+ ---
421
+
422
+ ## 六、生产环境约束
423
+
424
+ 本地 dev 是 Node.js 直接 `import()`,线上是 **VM2 沙盒 + CJS**。关键差异:
425
+
426
+ - 代码经 esbuild 打包为 CJS,**大部分 npm 依赖会被内联 bundle**,直接可用
427
+ - 仅 `EXTERNAL_PACKAGES`(`drizzle-orm`、`@lightfish/server/shared`)会留成 `require()` 调用,VM2 需允许
428
+ - 显式 **不可用**:`fs`、`import.meta`、`process.env`、`Buffer` — esbuild 无法内联 builtin 模块
429
+
430
+ 所以写代码时正常 import 纯 JS 的 npm 包即可,esbuild 会帮 bundle 进去。怀疑不一致时:`npx lightfish-server build` → 检查 `dist/server/` 下产物看依赖是否被正确内联。
431
+
432
+ ---
433
+
434
+ ## 检查清单
435
+
436
+ - [ ] 改 schema 后在项目根执行 `npx lightfish-server db push`(勿直接 `drizzle-kit`)
437
+ - [ ] 路由文件必须是 `index.ts`(非 `index.ts` 不会被识别)
438
+ - [ ] 动态参数目录名用方括号 `[param]`
439
+ - [ ] Handler 默认导出 + `ContextWithDb` 类型标注
440
+ - [ ] 使用数据库时先 `c.get('db')` 并判空
441
+ - [ ] 请求体用 `c.req.json<T>()` 读取
442
+ - [ ] 动态参数用 `c.get('params')` 获取
443
+ - [ ] 成功 return 业务数据;错误 `throw new ServerError()` / `throw new Error()`
444
+ - [ ] HTTP 方法约束用 `export const method = 'METHOD'` 声明
445
+ - [ ] 前端统一用 `apiRequest` 调本地后端
446
+ - [ ] `apiRequest` 的 `path` 以 `/` 开头,对应 `server/router/` 下的文件路径
447
+ - [ ] 不使用 esbuild 无法内联的内容(`fs`、`import.meta`、`process.env`、`Buffer`)
@@ -0,0 +1,42 @@
1
+ {
2
+ "search.exclude": {
3
+ "/node_modules": true,
4
+ "dist": true,
5
+ "pnpm-lock.sh": true
6
+ },
7
+ "editor.formatOnSave": true,
8
+ "[javascript]": {
9
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
10
+ },
11
+ "[javascriptreact]": {
12
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
13
+ },
14
+ "[typescript]": {
15
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
16
+ },
17
+ "[typescriptreact]": {
18
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
19
+ },
20
+ "[json]": {
21
+ "editor.defaultFormatter": "vscode.json-language-features"
22
+ },
23
+ "[html]": {
24
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
25
+ },
26
+ "[markdown]": {
27
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
28
+ },
29
+ "[css]": {
30
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
31
+ },
32
+ "[less]": {
33
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
34
+ },
35
+ "[scss]": {
36
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
37
+ },
38
+ "editor.codeActionsOnSave": {
39
+ "source.fixAll.eslint": "explicit",
40
+ "source.fixAll.stylelint": "explicit"
41
+ }
42
+ }