chanjs 2.7.4 → 2.7.5

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 (93) hide show
  1. package/USAGE.md +533 -0
  2. package/config/index.js +37 -6
  3. package/core/App.js +166 -0
  4. package/core/Container.js +77 -0
  5. package/core/Controller.js +29 -0
  6. package/core/Database.js +93 -0
  7. package/core/Repository.js +327 -0
  8. package/core/Service.js +11 -0
  9. package/core/bootstrap/error-handler.js +104 -0
  10. package/core/bootstrap/hook-runner.js +64 -0
  11. package/core/bootstrap/middleware.js +35 -0
  12. package/core/bootstrap/router-loader.js +53 -0
  13. package/core/errors.js +224 -0
  14. package/core/loader.js +89 -0
  15. package/core/registry.js +17 -0
  16. package/doc/Cache.md +279 -106
  17. package/doc/Common.md +590 -134
  18. package/doc/Controller.md +166 -95
  19. package/doc/Help.md +299 -698
  20. package/doc/QuickStart.md +116 -0
  21. package/doc/Repository.md +560 -0
  22. package/doc/Service.md +201 -527
  23. package/index.js +75 -37
  24. package/middleware/body.js +17 -0
  25. package/middleware/cookie.js +7 -15
  26. package/middleware/cors.js +9 -27
  27. package/middleware/favicon.js +7 -17
  28. package/middleware/header.js +15 -16
  29. package/middleware/index.js +11 -11
  30. package/middleware/log.js +26 -56
  31. package/middleware/static.js +15 -28
  32. package/middleware/template.js +75 -115
  33. package/middleware/validate.js +79 -0
  34. package/middleware/waf.js +174 -197
  35. package/package.json +9 -2
  36. package/response/code.js +73 -0
  37. package/response/index.js +9 -6
  38. package/response/response.js +82 -236
  39. package/security/checker.js +26 -74
  40. package/security/index.js +4 -9
  41. package/security/jwt.js +69 -142
  42. package/security/keywords.js +32 -136
  43. package/security/rate-limit.js +38 -80
  44. package/security/sign.js +83 -176
  45. package/security/xss-filter.js +21 -53
  46. package/storage/cache.js +57 -196
  47. package/storage/index.js +3 -6
  48. package/storage/redis.js +123 -181
  49. package/storage/store.js +163 -188
  50. package/utils/data-parse.js +42 -186
  51. package/utils/file.js +73 -244
  52. package/utils/filter.js +22 -25
  53. package/utils/html.js +49 -33
  54. package/utils/index.js +21 -7
  55. package/utils/ip.js +31 -71
  56. package/utils/logger.js +117 -0
  57. package/utils/pages.js +55 -0
  58. package/utils/paths.js +18 -0
  59. package/utils/request.js +94 -136
  60. package/utils/signal.js +87 -0
  61. package/utils/time.js +33 -75
  62. package/utils/tree.js +112 -104
  63. package/App.js +0 -533
  64. package/base/Aop.js +0 -195
  65. package/base/Container.js +0 -161
  66. package/base/Controller.js +0 -65
  67. package/base/Database.js +0 -133
  68. package/base/Event.js +0 -61
  69. package/base/Repository.js +0 -644
  70. package/common/api.js +0 -35
  71. package/common/code.js +0 -52
  72. package/common/email.js +0 -191
  73. package/common/index.js +0 -5
  74. package/common/pages.js +0 -120
  75. package/common/utils.js +0 -73
  76. package/config/code.js +0 -166
  77. package/config/paths.js +0 -60
  78. package/doc/Aop.md +0 -269
  79. package/doc/Email.md +0 -114
  80. package/doc/Event.md +0 -232
  81. package/global/env.js +0 -11
  82. package/global/import.js +0 -39
  83. package/global/index.js +0 -8
  84. package/helper/index.js +0 -79
  85. package/loader/index.js +0 -6
  86. package/loader/loader.js +0 -138
  87. package/middleware/compress.js +0 -185
  88. package/middleware/setBody.js +0 -32
  89. package/realtime/index.js +0 -7
  90. package/realtime/sse.js +0 -424
  91. package/realtime/websocket.js +0 -540
  92. package/schedule/index.js +0 -6
  93. package/schedule/schedule.js +0 -491
package/App.js DELETED
@@ -1,533 +0,0 @@
1
- import express from "express";
2
- import fs from "fs";
3
- import path from "path";
4
-
5
- // 引入基础组件
6
- import DatabaseManager from "./base/Database.js";
7
-
8
- // 引入配置和工具
9
- import { Paths } from "./config/index.js";
10
- import { loadConfig, loaderSort, loadController } from "./loader/index.js";
11
- import { Cors, setBody, setCookie, setFavicon, setHeader, setStatic, setTemplate, waf, compress, log } from "./middleware/index.js";
12
- import { notFoundResponse, parseDatabaseError, errorResponse } from "./response/index.js";
13
- import { store } from "./storage/index.js";
14
- import { schedule } from "./schedule/schedule.js";
15
- import { sse } from "./realtime/sse.js";
16
- import { websocket } from "./realtime/websocket.js";
17
- import { importFile } from "./global/import.js";
18
-
19
- import "./global/index.js";
20
-
21
- /**
22
- * Chan 应用核心类
23
- * @class Chan
24
- * @description 管理应用程序的路由、中间件、数据库连接和服务
25
- * @example
26
- * const app = new Chan();
27
- * await app.start();
28
- */
29
- class Chan {
30
-
31
- /**
32
- * 构造函数
33
- * @constructor
34
- * @description 初始化 Express 应用和核心组件
35
- */
36
- constructor() {
37
- this.app = express();
38
- // 数据库连接管理
39
- this.dbManager = new DatabaseManager();
40
- // 应用路由
41
- this.router = express.Router();
42
- }
43
-
44
- /**
45
- * 启动应用程序
46
- * @async
47
- * @description 执行完整的启动流程
48
- * 包括:初始化配置、数据库、扩展、中间件、路由、错误处理
49
- * @returns {Promise<void>}
50
- */
51
- async start() {
52
- //加载配置
53
- await this.config();
54
- //暴露 Paths 到 Chan 静态属性(DI:业务侧通过 Chan.paths 或 this.paths 访问)
55
- Chan.paths = Paths;
56
- //初始化 Redis 适配器(WAF 封禁 / Rate Limit 共享存储)
57
- await this.loadRedis();
58
- //加载数据库
59
- await this.loadDB();
60
- //加载扩展方法
61
- await this.loadExtend();
62
- //加载中间件
63
- await this.loadAppMiddleware();
64
- //设置app
65
- this.setApp();
66
- //加载路由
67
- await this.loadRouter();
68
- await this.loadCommonRouter();
69
- this.useRouter();
70
- //404 500 处理(放在最后,确保所有路由都已注册)
71
- this.setErrorHandler();
72
- //执行 beforeStart 钩子(用户自定义的初始化逻辑)
73
- await this._runBeforeStart();
74
- }
75
-
76
- /**
77
- * 注册 beforeStart 钩子
78
- * @param {Function} fn - 钩子函数(支持 async)
79
- * @description 在 start() 流程末尾、run() 之前执行
80
- * 适合做:缓存预热、定时任务注册、健康检查初始化等
81
- * @example
82
- * chan.beforeStart(async () => {
83
- * schedule.every('clean-log', 60000, cleanLogs);
84
- * });
85
- */
86
- beforeStart(fn) {
87
- if (typeof fn !== 'function') {
88
- throw new Error('beforeStart 参数必须是函数');
89
- }
90
- if (!this._beforeStartHooks) this._beforeStartHooks = [];
91
- this._beforeStartHooks.push(fn);
92
- }
93
-
94
- /**
95
- * 执行 beforeStart 钩子
96
- * @private
97
- */
98
- async _runBeforeStart() {
99
- if (!this._beforeStartHooks || !this._beforeStartHooks.length) return;
100
- for (const fn of this._beforeStartHooks) {
101
- try {
102
- await fn();
103
- } catch (err) {
104
- console.error('[Chan] beforeStart 钩子执行失败:', err.message);
105
- }
106
- }
107
- }
108
-
109
- /**
110
- * 优雅停机
111
- * @async
112
- * @description 关闭 HTTP 服务、定时任务、数据库连接、Redis
113
- * 监听 SIGTERM / SIGINT 信号自动触发
114
- * @returns {Promise<void>}
115
- */
116
- async shutdown() {
117
- if (this._shuttingDown) return;
118
- this._shuttingDown = true;
119
- console.log('[Chan] 开始优雅停机...');
120
-
121
- // 5 秒强制退出兜底(原 30s 过长,连接已主动销毁,5s 足够收尾)
122
- const forceExitTimer = setTimeout(() => {
123
- console.error('[Chan] 优雅停机超时(5s),强制退出');
124
- process.exit(1);
125
- }, 5000);
126
- forceExitTimer.unref?.();
127
-
128
- try {
129
- // 1. 停止接收新请求 + 主动销毁所有 keep-alive 连接
130
- // 说明:server.close() 只停止 listen,会等所有现有连接关闭才回调,
131
- // 浏览器默认 keep-alive 会导致 30s 超时,所以必须主动 destroy。
132
- if (this.server) {
133
- if (this._sockets && this._sockets.size > 0) {
134
- for (const socket of this._sockets) {
135
- try { socket.destroy(); } catch (e) { /* 忽略已关闭的 socket */ }
136
- }
137
- this._sockets.clear();
138
- }
139
- await new Promise(resolve => this.server.close(resolve));
140
- console.log('[Chan] HTTP 服务已关闭');
141
- }
142
-
143
- // 2. 关闭实时通信(SSE / WebSocket,先于定时任务关闭,避免推送失败)
144
- try {
145
- sse.shutdown();
146
- } catch (e) {
147
- console.error('[Chan] 关闭 SSE 失败:', e.message);
148
- }
149
- try {
150
- websocket.shutdown();
151
- } catch (e) {
152
- console.error('[Chan] 关闭 WebSocket 失败:', e.message);
153
- }
154
-
155
- // 3. 停止所有定时任务
156
- try {
157
- schedule.shutdown();
158
- } catch (e) {
159
- console.error('[Chan] 停止定时任务失败:', e.message);
160
- }
161
-
162
- // 4. 关闭数据库连接
163
- try {
164
- await this.dbManager.closeAll();
165
- } catch (e) {
166
- console.error('[Chan] 关闭数据库失败:', e.message);
167
- }
168
-
169
- // 5. 关闭 Redis / 存储适配层
170
- try {
171
- await store.close();
172
- } catch (e) {
173
- console.error('[Chan] 关闭存储适配层失败:', e.message);
174
- }
175
-
176
- console.log('[Chan] 优雅停机完成');
177
- } finally {
178
- clearTimeout(forceExitTimer);
179
- process.exit(0);
180
- }
181
- }
182
-
183
- /**
184
- * 注册进程信号监听器(自动触发优雅停机)
185
- * @private
186
- */
187
- _registerSignalHandlers() {
188
- if (this._signalRegistered) return;
189
- this._signalRegistered = true;
190
-
191
- const handler = async (signal) => {
192
- // 幂等守卫:首次信号触发停机,后续信号直接忽略,避免日志噪音
193
- if (this._shuttingDown) {
194
- // 第二次 ctrl+c:直接强制退出,让用户能快速中断
195
- console.error(`[Chan] 再次收到 ${signal},强制退出`);
196
- process.exit(1);
197
- }
198
- console.log(`[Chan] 收到 ${signal} 信号`);
199
- await this.shutdown();
200
- };
201
-
202
- process.on('SIGTERM', () => handler('SIGTERM'));
203
- process.on('SIGINT', () => handler('SIGINT'));
204
- }
205
-
206
- /**
207
- * 加载配置
208
- * @async
209
- * @returns {Promise<void>}
210
- * @description 加载应用配置并设置到全局变量
211
- */
212
- async config() {
213
- let config = await loadConfig();
214
- Chan.config = config;
215
- }
216
-
217
- /**
218
- * 初始化存储适配层
219
- * @async
220
- * @description
221
- * 根据 config.REDIS_ENABLED 决定使用 Redis 还是内存模式
222
- * - Redis 启用且连接成功 → 使用 Redis 后端
223
- * - Redis 未启用或连接失败 → 降级到内存模式(Map+TTL,上限 10 万条)
224
- * WAF 封禁状态、Rate Limit 计数都依赖此适配层
225
- */
226
- async loadRedis() {
227
- const config = Chan.config || {};
228
- try {
229
- await store.init({
230
- REDIS_ENABLED: config.REDIS_ENABLED,
231
- REDIS: config.REDIS,
232
- });
233
- } catch (err) {
234
- console.warn(`[Chan] 存储适配层初始化失败,使用内存模式: ${err.message}`);
235
- }
236
- }
237
-
238
- /**
239
- * 加载数据库
240
- * @async
241
- * @returns {Promise<void>}
242
- * @description 根据配置初始化所有数据库连接
243
- */
244
- async loadDB() {
245
- const dbList = Chan.config?.db || [];
246
- for (const [index, item] of dbList.entries()) {
247
- const key = item.key || String(index);
248
- try {
249
- // 已通过 dbList.entries() 拿到 item,无需再做 if (!dbConfig) return 死代码判断
250
- const connection = this.dbManager.add(key, item, { isDefault: index === 0 });
251
- if (index === 0) {
252
- Chan.db = this.dbManager.get();
253
- }
254
- } catch (error) {
255
- console.error(`[DB] 数据库 ${key} 初始化失败: ${error.message}`);
256
- }
257
- }
258
-
259
- console.log(`[DB] 初始化完成,已加载 ${dbList.length} 个数据库`);
260
- }
261
-
262
- /**
263
- * 加载扩展模块
264
- * @async
265
- * @returns {Promise<void>}
266
- * @description 加载 common、helper、extend 目录下的扩展模块
267
- */
268
- async loadExtend() {
269
- const extensions = [
270
- { _path: Paths.commonPath, key: "common" },
271
- { _path: Paths.helperPath, key: "helper" },
272
- { _path: Paths.extendPath, key: "extend" },
273
- ];
274
-
275
- for (const { _path, key } of extensions) {
276
- await this.loadFn(_path, key);
277
- }
278
-
279
- if (!Chan.helper) {
280
- Chan.helper = {};
281
- }
282
- Chan.helper.loadController = loadController;
283
- }
284
-
285
- /**
286
- * 加载模块文件
287
- * @async
288
- * @param {string} _path - 模块目录路径
289
- * @param {string} key - 模块键名
290
- * @returns {Promise<void>}
291
- * @description 从指定目录加载所有 JS 模块并合并到 Chan 静态属性
292
- */
293
- async loadFn(_path, key) {
294
- if (fs.existsSync(_path)) {
295
- const files = fs.readdirSync(_path).filter((file) => file.endsWith(".js"));
296
- for (const file of files) {
297
- const filePath = path.join(_path, file);
298
- let helperModule = await importFile(filePath);
299
- if (!Chan[key]) {
300
- Chan[key] = {};
301
- }
302
- Object.assign(Chan[key], helperModule);
303
- }
304
- }
305
- }
306
-
307
- /**
308
- * 初始化中间件
309
- * @async
310
- * @returns {Promise<void>}
311
- * @description 配置并注册所有应用中间件
312
- */
313
- async loadAppMiddleware() {
314
- const config = Chan.config;
315
- const {
316
- views = [],
317
- NODE_ENV = "dev",
318
- APP_NAME = "ChanCMS",
319
- APP_VERSION = "1.0.0",
320
- cookieKey,
321
- BODY_LIMIT = "10mb",
322
- statics = [],
323
- cors = {},
324
- PROXY = "false",
325
- logger = {},
326
- waf: wafConfig = { enabled: false }
327
- } = config;
328
-
329
- await waf(this.app, wafConfig);
330
- setFavicon(this.app);
331
- setStatic(this.app, statics);
332
- // 响应压缩中间件:放在 setStatic 之后(静态资源已由 express.static 处理流式响应)、Cors 之前
333
- // 跳过 SSE / WebSocket 升级请求,仅对可压缩文本类型且 > 1KB 的响应体压缩
334
- this.app.use(compress());
335
- setCookie(this.app, cookieKey);
336
- setBody(this.app, BODY_LIMIT);
337
- Cors(this.app, cors);
338
- log(this.app, logger);
339
- setTemplate(this.app, { views, NODE_ENV });
340
- setHeader(this.app, { APP_NAME, APP_VERSION });
341
- }
342
-
343
- /**
344
- * 应用配置
345
- * @private
346
- * @description 设置 Express 应用配置
347
- * trust proxy 根据 config.PROXY 动态设置,避免默认 true 导致 X-Forwarded-For 伪造
348
- */
349
- setApp() {
350
- const proxyConfig = Chan.config?.PROXY;
351
- if (proxyConfig === 'true' || proxyConfig === true) {
352
- // 信任所有代理(仅在反代可信时使用)
353
- this.app.set("trust proxy", true);
354
- } else if (proxyConfig === 'loopback' || !proxyConfig) {
355
- // 默认仅信任本机回环代理
356
- this.app.set("trust proxy", "loopback");
357
- } else {
358
- // 其他值按字面传入(IP / CIDR / 跳数)
359
- this.app.set("trust proxy", proxyConfig);
360
- }
361
- }
362
-
363
- /**
364
- * 应用路由
365
- * @private
366
- * @description 将所有路由应用到 Express 应用
367
- */
368
- useRouter() {
369
- this.app.use(this.router);
370
- }
371
-
372
- /**
373
- * 应用错误处理404 500
374
- * @private
375
- * @description 配置全局错误处理中间件
376
- */
377
- setErrorHandler() {
378
- //404(标准 Express 中间件签名,保留 next 参数)
379
- this.app.use((req, res, next) => {
380
- res.status(404).json(notFoundResponse(req));
381
- });
382
-
383
- // 500
384
- this.app.use((err, req, res, next) => {
385
- if (res.headersSent) return next(err);
386
-
387
- const errorInfo = err?.stack ? this._parseErrorStack(err.stack) : { message: err?.message || err };
388
- const requestInfo = `${req.method} ${req.originalUrl}`;
389
- console.error(`[Global Error] ${requestInfo} - ${errorInfo.file}:${errorInfo.line || "?"} - ${errorInfo.message}`);
390
-
391
- const parsedError = parseDatabaseError(err);
392
- console.error(`[Global Error Details] Database Error - ${parsedError.msg}`);
393
- res.status(parsedError.statusCode).json(errorResponse(err, req));
394
- });
395
- }
396
-
397
- /**
398
- * 解析错误堆栈
399
- * @private
400
- * @param {string} stack - 错误堆栈字符串
401
- * @returns {Object} 解析后的错误信息
402
- * @description 从错误堆栈中提取文件名、行号、列号等信息
403
- */
404
- _parseErrorStack(stack) {
405
- if (!stack) return { message: '未知错误' };
406
-
407
- const stackLines = stack.split('\n');
408
- if (stackLines.length < 2) {
409
- return { message: stackLines[0] || '未知错误' };
410
- }
411
-
412
- const errorLine = stackLines[1].trim();
413
- const match = errorLine.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/);
414
-
415
- if (match) {
416
- return {
417
- message: stackLines[0] || '未知错误',
418
- function: match[1],
419
- file: match[2],
420
- line: match[3],
421
- column: match[4]
422
- };
423
- }
424
-
425
- const fileMatch = errorLine.match(/at\s+(.+?):(\d+):(\d+)/);
426
- if (fileMatch) {
427
- return {
428
- message: stackLines[0] || '未知错误',
429
- file: fileMatch[1],
430
- line: fileMatch[2],
431
- column: fileMatch[3]
432
- };
433
- }
434
-
435
- return { message: stackLines[0] || '未知错误', file: errorLine };
436
- }
437
-
438
-
439
-
440
- /**
441
- * 加载模块路由
442
- * @async
443
- * @returns {Promise<void>}
444
- * @description 从 app/modules 目录加载所有模块的路由
445
- */
446
- async loadRouter() {
447
- const configPath = path.join(Paths.appPath, "modules");
448
- if (fs.existsSync(configPath)) {
449
- const dirs = loaderSort(Chan.config.modules);
450
- for (const item of dirs) {
451
- let routerFn = await importFile(path.join(Paths.modulesPath, item, "router.js"));
452
- // 为每个模块创建子路由并添加前缀
453
- const subRouter = express.Router();
454
- routerFn(this.app, subRouter, Chan.config);
455
- this.app.use(`/${item}`, subRouter);
456
- }
457
- }
458
- }
459
-
460
- /**
461
- * 加载公共路由
462
- * @async
463
- * @returns {Promise<void>}
464
- * @description 加载 app/router.js 中的公共路由
465
- */
466
- async loadCommonRouter() {
467
- let router = await importFile(path.join(Paths.appPath, "router.js"));
468
- if (router) {
469
- router(this.app, this.router, Chan.config);
470
- }
471
- }
472
-
473
- /**
474
- * 准备启动
475
- * @param {Function} [cb] - 回调函数
476
- * @description 启动 HTTP 服务,并按配置挂载 WebSocket(SSE 由业务侧通过 sse.middleware() 注册路由)
477
- */
478
- run(cb) {
479
- this.server = this.app.listen(Chan.config.PORT, () => {
480
- console.log(`Server running on port ${Chan.config.PORT}`);
481
- cb?.(Chan.config.PORT);
482
- });
483
-
484
- // 跟踪所有 HTTP socket,用于优雅停机时主动销毁(避免 keep-alive 卡住 server.close)
485
- this._sockets = new Set();
486
- this.server.on('connection', (socket) => {
487
- this._sockets.add(socket);
488
- socket.on('close', () => this._sockets.delete(socket));
489
- });
490
-
491
- // 按配置挂载 WebSocket(config.WEBSOCKET.enabled = true 时启用)
492
- this._attachWebSocket();
493
- // 注册 SIGTERM / SIGINT 信号处理,支持优雅停机
494
- this._registerSignalHandlers();
495
- }
496
-
497
- /**
498
- * 挂载 WebSocket 服务到 HTTP server
499
- * @private
500
- * @description 根据 config.WEBSOCKET 配置自动挂载,业务侧通过 Chan.websocket 使用
501
- * @example
502
- * // config.js
503
- * module.exports = {
504
- * WEBSOCKET: { enabled: true, path: '/ws', heartbeatInterval: 30000 }
505
- * };
506
- * // 业务侧
507
- * import { websocket } from 'chanjs';
508
- * websocket.on('chat', (socket, payload) => { ... });
509
- */
510
- _attachWebSocket() {
511
- const wsConfig = Chan.config?.WEBSOCKET || {};
512
- if (!wsConfig.enabled) return;
513
-
514
- try {
515
- websocket.attach(this.server, {
516
- path: wsConfig.path || '/ws',
517
- heartbeatInterval: wsConfig.heartbeatInterval,
518
- verifyClient: wsConfig.verifyClient,
519
- onConnection: wsConfig.onConnection,
520
- });
521
- // 暴露到 Chan 静态属性,便于业务侧通过 import Chan 访问
522
- Chan.websocket = websocket;
523
- } catch (err) {
524
- console.error('[Chan] WebSocket 挂载失败:', err.message);
525
- }
526
- }
527
- }
528
-
529
- // 不再使用 global.Chan 全局污染
530
- // 业务侧通过 `import Chan from 'chanjs'` 获取同一个类引用(ESM 模块缓存保证单例)
531
- // 框架内部直接使用类名 Chan(同一模块作用域)
532
-
533
- export default Chan;