chanjs 2.7.4 → 2.7.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 (94) 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/BaseComponent.js +27 -0
  5. package/core/Container.js +68 -0
  6. package/core/Controller.js +29 -0
  7. package/core/Database.js +93 -0
  8. package/core/Repository.js +323 -0
  9. package/core/Service.js +11 -0
  10. package/core/bootstrap/error-handler.js +101 -0
  11. package/core/bootstrap/hook-runner.js +64 -0
  12. package/core/bootstrap/middleware.js +35 -0
  13. package/core/bootstrap/router-loader.js +53 -0
  14. package/core/errors.js +251 -0
  15. package/core/loader.js +89 -0
  16. package/core/registry.js +17 -0
  17. package/doc/Cache.md +279 -106
  18. package/doc/Common.md +590 -134
  19. package/doc/Controller.md +166 -95
  20. package/doc/Help.md +299 -698
  21. package/doc/QuickStart.md +116 -0
  22. package/doc/Repository.md +560 -0
  23. package/doc/Service.md +201 -527
  24. package/index.js +61 -37
  25. package/middleware/body.js +17 -0
  26. package/middleware/cookie.js +7 -15
  27. package/middleware/cors.js +9 -27
  28. package/middleware/favicon.js +15 -17
  29. package/middleware/header.js +15 -16
  30. package/middleware/index.js +11 -11
  31. package/middleware/log.js +26 -56
  32. package/middleware/static.js +15 -28
  33. package/middleware/template.js +75 -115
  34. package/middleware/validate.js +79 -0
  35. package/middleware/waf.js +176 -197
  36. package/package.json +9 -2
  37. package/response/code.js +73 -0
  38. package/response/index.js +9 -6
  39. package/response/response.js +82 -236
  40. package/security/checker.js +26 -74
  41. package/security/index.js +4 -9
  42. package/security/jwt.js +84 -139
  43. package/security/keywords.js +33 -137
  44. package/security/rate-limit.js +38 -80
  45. package/security/sign.js +83 -176
  46. package/security/xss-filter.js +21 -53
  47. package/storage/cache.js +58 -198
  48. package/storage/index.js +3 -6
  49. package/storage/redis.js +124 -181
  50. package/storage/store.js +163 -188
  51. package/utils/data-parse.js +42 -186
  52. package/utils/file.js +73 -244
  53. package/utils/filter.js +22 -25
  54. package/utils/html.js +49 -33
  55. package/utils/index.js +20 -7
  56. package/utils/ip.js +31 -71
  57. package/utils/logger.js +117 -0
  58. package/utils/pages.js +55 -0
  59. package/utils/paths.js +18 -0
  60. package/utils/request.js +95 -136
  61. package/utils/signal.js +87 -0
  62. package/utils/time.js +33 -75
  63. package/utils/tree.js +112 -104
  64. package/App.js +0 -533
  65. package/base/Aop.js +0 -195
  66. package/base/Container.js +0 -161
  67. package/base/Controller.js +0 -65
  68. package/base/Database.js +0 -133
  69. package/base/Event.js +0 -61
  70. package/base/Repository.js +0 -644
  71. package/common/api.js +0 -35
  72. package/common/code.js +0 -52
  73. package/common/email.js +0 -191
  74. package/common/index.js +0 -5
  75. package/common/pages.js +0 -120
  76. package/common/utils.js +0 -73
  77. package/config/code.js +0 -166
  78. package/config/paths.js +0 -60
  79. package/doc/Aop.md +0 -269
  80. package/doc/Email.md +0 -114
  81. package/doc/Event.md +0 -232
  82. package/global/env.js +0 -11
  83. package/global/import.js +0 -39
  84. package/global/index.js +0 -8
  85. package/helper/index.js +0 -79
  86. package/loader/index.js +0 -6
  87. package/loader/loader.js +0 -138
  88. package/middleware/compress.js +0 -185
  89. package/middleware/setBody.js +0 -32
  90. package/realtime/index.js +0 -7
  91. package/realtime/sse.js +0 -424
  92. package/realtime/websocket.js +0 -540
  93. package/schedule/index.js +0 -6
  94. package/schedule/schedule.js +0 -491
package/helper/index.js DELETED
@@ -1,79 +0,0 @@
1
- /**
2
- * 聚合导出层 - 从各子模块统一 re-export
3
- *
4
- * 框架内部已按职责拆分到 storage/security/realtime/schedule/loader/response/utils
5
- * 本文件提供扁平化聚合访问,业务侧可用:
6
- * import { helper } from "chanjs";
7
- * const { getIp, formatDateFields, filterXSS } = helper;
8
- *
9
- * 也可直接从子模块导入(更精确):
10
- * import { utils, security, storage } from "chanjs";
11
- */
12
-
13
- // 加载器
14
- export { loaderSort, loadConfig, clearConfigCache, loadController } from "../loader/index.js";
15
-
16
- // 时间处理
17
- export { formatTime, formatDateFields } from "../utils/time.js";
18
-
19
- // 存储相关
20
- export { cache, DEFAULT_INCR_TTL } from "../storage/index.js";
21
- export { store } from "../storage/index.js";
22
- export { default as RedisBackend } from "../storage/redis.js";
23
-
24
- // 文件操作
25
- export {
26
- dirname,
27
- delImg,
28
- getFileTree,
29
- readFileContent,
30
- saveFileContent,
31
- isPathSafe,
32
- getFolders,
33
- } from "../utils/file.js";
34
-
35
- // HTML 处理
36
- export { htmlDecode, htmlEncode, escapeScript } from "../utils/html.js";
37
-
38
- // IP 获取
39
- export { getIp } from "../utils/ip.js";
40
-
41
- // JWT 令牌
42
- export { verifyToken, generateToken, setToken, getToken } from "../security/jwt.js";
43
-
44
- // 签名/加密
45
- export { signData, verifySign, aesEncrypt, aesDecrypt } from "../security/sign.js";
46
-
47
- // 网络请求
48
- export { request } from "../utils/request.js";
49
-
50
- // 数据解析
51
- export { dataParse, arrToObj, parseJsonFields, buildTree } from "../utils/data-parse.js";
52
-
53
- // 树形结构
54
- export { tree, treeById } from "../utils/tree.js";
55
-
56
- // 字段过滤
57
- export { filterFields } from "../utils/filter.js";
58
-
59
- // 响应格式化
60
- export {
61
- success,
62
- fail,
63
- error,
64
- parseDatabaseError,
65
- notFoundResponse,
66
- errorResponse,
67
- } from "../response/index.js";
68
-
69
- // 安全检查
70
- export { checkKeywords, isIgnored } from "../security/checker.js";
71
- export { filterXSS } from "../security/xss-filter.js";
72
- export { createRateLimitMiddleware } from "../security/rate-limit.js";
73
-
74
- // 定时任务
75
- export { schedule } from "../schedule/index.js";
76
-
77
- // 实时通信
78
- export { sse, SSEManager } from "../realtime/index.js";
79
- export { websocket, WebSocketManager } from "../realtime/index.js";
package/loader/index.js DELETED
@@ -1,6 +0,0 @@
1
- /**
2
- * 加载器模块 - 配置加载/控制器加载/排序
3
- * - loadConfig: 依赖 ESM 模块缓存(无 LRU,避免配置更新问题)
4
- * - loadController: Symbol 标记幂等绑定
5
- */
6
- export { loaderSort, loadConfig, clearConfigCache, loadController } from "./loader.js";
package/loader/loader.js DELETED
@@ -1,138 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import { Paths } from "../config/paths.js";
4
-
5
- /**
6
- * 对模块列表进行排序,将 web 模块移到最后
7
- * @param {Array<string>} modules - 模块名称数组
8
- * @returns {Array<string>} 排序后的模块数组
9
- * @description
10
- * 确保 web 模块始终在模块列表的最后,以便优先加载其他模块的路由
11
- * @example
12
- * const modules = ['api', 'web', 'admin'];
13
- * const sorted = loaderSort(modules);
14
- * // 返回: ['api', 'admin', 'web']
15
- */
16
- export function loaderSort(modules = []) {
17
- const index = modules.indexOf("web");
18
- if (index !== -1) {
19
- const web = modules.splice(index, 1);
20
- modules.push(web[0]);
21
- }
22
- return modules;
23
- }
24
-
25
- /**
26
- * 加载应用配置文件
27
- * @async
28
- * @returns {Promise<Object>} 配置对象
29
- * @description
30
- * 从 config/index.js 加载配置
31
- *
32
- * 性能优化(P2 #21):
33
- * 删除 LRU 缓存层,直接依赖 ESM 模块缓存
34
- * - ESM import 同一文件返回缓存的模块实例(Node.js 内置机制)
35
- * - 旧版 LRU 缓存有 5 分钟 TTL,配置变更后不生效,需要手动 clearConfigCache
36
- * - 现在配置变更直接重启进程即可,符合生产实践
37
- */
38
- export async function loadConfig() {
39
- const configPath = path.join(Paths.configPath, "index.js");
40
- try {
41
- const module = await import(`file://${configPath}`);
42
- return module.default || module;
43
- } catch (error) {
44
- console.error("[Config] 加载配置失败", error.message);
45
- return {};
46
- }
47
- }
48
-
49
- /**
50
- * 清除配置缓存(已废弃)
51
- * @deprecated P2 #21 优化后,配置直接依赖 ESM 缓存,无需手动清理
52
- * 保留函数仅为向后兼容,调用为空操作
53
- * @example
54
- * clearConfigCache(); // 空操作,无需调用
55
- */
56
- export function clearConfigCache() {
57
- // ESM 模块缓存由 Node.js 管理,无需手动清理
58
- }
59
-
60
- /**
61
- * 已绑定方法标记(Symbol 幂等)
62
- * 避免重复 loadController 时对同一方法多次 bind 形成嵌套
63
- * @private
64
- */
65
- const BOUND_SYMBOL = Symbol('chanjs:bound');
66
-
67
- /**
68
- * 加载指定模块的所有控制器
69
- * @async
70
- * @param {string} moduleName - 模块名称
71
- * @returns {Promise<Object>} 控制器对象,键为控制器名称
72
- * @description
73
- * 从 modules/{moduleName}/controller 目录加载所有 .js 文件
74
- * 每个文件会被加载并绑定实例,然后以文件名作为键存储
75
- *
76
- * 性能优化(P2 #18):
77
- * - 用 Symbol 标记已 bind 过的方法,避免重复 bind 形成嵌套
78
- * - ESM 模块缓存导致同一实例可能被多次 loadController,原版每次都重新 bind
79
- * - 现在首次 bind 后标记,后续调用直接跳过
80
- *
81
- * @example
82
- * const controllers = await loadController('api');
83
- * console.log(controllers.User); // User 控制器实例
84
- */
85
- export async function loadController(moduleName) {
86
- const controller = {};
87
-
88
- const dir = path.join(Paths.modulesPath, moduleName, "controller");
89
-
90
- if (!fs.existsSync(dir)) {
91
- console.error(`模块路径不存在,跳过加载控制器: ${dir}`);
92
- return controller;
93
- }
94
-
95
- const files = fs.readdirSync(dir).filter((file) => file.endsWith(".js"));
96
-
97
- for (const file of files) {
98
- const filePath = path.join(dir, file);
99
- const name = file.replace(/\.js$/i, "");
100
-
101
- try {
102
- const module = await import(`file://${filePath}`);
103
- let instance = module?.default || module;
104
-
105
- // 绑定实例的所有方法(Symbol 幂等,避免重复 bind)
106
- if (instance && typeof instance === 'object') {
107
- const proto = Object.getPrototypeOf(instance);
108
- if (proto) {
109
- Object.getOwnPropertyNames(proto).forEach((methodName) => {
110
- if (
111
- methodName !== "constructor" &&
112
- typeof instance[methodName] === "function"
113
- ) {
114
- const method = instance[methodName];
115
- // 已 bind 过的方法直接跳过(避免重复 bind 嵌套)
116
- if (method[BOUND_SYMBOL]) return;
117
- // bind 创建新函数,标记后存回实例
118
- const bound = method.bind(instance);
119
- Object.defineProperty(bound, BOUND_SYMBOL, {
120
- value: true,
121
- enumerable: false,
122
- writable: false,
123
- configurable: false,
124
- });
125
- instance[methodName] = bound;
126
- }
127
- });
128
- }
129
- }
130
-
131
- controller[name] = instance;
132
- } catch (e) {
133
- console.error(`加载控制器失败: ${filePath}`, e);
134
- }
135
- }
136
-
137
- return controller;
138
- }
@@ -1,185 +0,0 @@
1
- import zlib from 'zlib';
2
-
3
- /**
4
- * 响应压缩中间件(零依赖,基于 Node 原生 zlib)
5
- * 改进点:
6
- * 1. 支持 br / gzip / deflate,按 Accept-Encoding 优先级选择
7
- * 2. 仅压缩可压缩文本类型(html/css/js/json/plain/svg+xml)
8
- * 3. 跳过 SSE(text/event-stream)和 WebSocket 升级请求
9
- * 4. 阈值 1KB,避免小包压缩负收益
10
- * 5. 压缩失败自动回退原样输出,不影响请求
11
- * 6. chunks 缓冲上限 10MB,超出后 bypass 直接放行不压缩,避免内存爆炸
12
- * 7. 拦截 res.write/res.end 后在 finish/close 时恢复,避免泄漏
13
- */
14
-
15
- // 压缩阈值:响应体 > 1KB 才压缩
16
- const DEFAULT_THRESHOLD = 1024;
17
-
18
- // chunks 缓冲上限:10MB,超出后 bypass 直接放行不压缩
19
- const MAX_BUFFER_BYTES = 10 * 1024 * 1024;
20
-
21
- // 可压缩的 Content-Type 集合(精确匹配,charset 后缀在判断时已剥离)
22
- const COMPRESSIBLE_TYPES = new Set([
23
- 'text/html',
24
- 'text/css',
25
- 'application/json',
26
- 'application/javascript',
27
- 'text/plain',
28
- 'image/svg+xml',
29
- ]);
30
-
31
- /**
32
- * 根据 Accept-Encoding 选择最优编码
33
- * 优先级:br > gzip > deflate
34
- * @param {string} acceptEncoding - 请求头 Accept-Encoding 值
35
- * @returns {'br'|'gzip'|'deflate'|null}
36
- */
37
- function pickEncoding(acceptEncoding) {
38
- if (!acceptEncoding) return null;
39
- const enc = acceptEncoding.toLowerCase();
40
- if (enc.includes('br')) return 'br';
41
- if (enc.includes('gzip')) return 'gzip';
42
- if (enc.includes('deflate')) return 'deflate';
43
- return null;
44
- }
45
-
46
- /**
47
- * 使用 Node 原生 zlib 同步压缩 Buffer
48
- * @param {Buffer} buf - 待压缩数据
49
- * @param {'br'|'gzip'|'deflate'} encoding - 压缩编码
50
- * @returns {Buffer} 压缩后的 Buffer
51
- */
52
- function compressBuffer(buf, encoding) {
53
- switch (encoding) {
54
- case 'br': return zlib.brotliCompressSync(buf);
55
- case 'gzip': return zlib.gzipSync(buf);
56
- case 'deflate': return zlib.deflateSync(buf);
57
- default: return buf;
58
- }
59
- }
60
-
61
- /**
62
- * 创建压缩中间件
63
- * @param {Object} [options]
64
- * @param {number} [options.threshold=1024] - 压缩阈值(字节),响应体小于该值不压缩
65
- * @returns {Function} Express 中间件
66
- */
67
- export function compress(options = {}) {
68
- const threshold = options.threshold || DEFAULT_THRESHOLD;
69
-
70
- return (req, res, next) => {
71
- // WebSocket 升级请求直接跳过(不适用 HTTP 响应压缩)
72
- if (req.headers.upgrade && req.headers.upgrade.toLowerCase() === 'websocket') {
73
- return next();
74
- }
75
-
76
- const encoding = pickEncoding(req.headers['accept-encoding'] || '');
77
- // 客户端不支持任何压缩编码,直接放行
78
- if (!encoding) return next();
79
-
80
- // 缓存原方法(bind 避免 this 丢失)
81
- const originalEnd = res.end.bind(res);
82
- const originalWrite = res.write.bind(res);
83
- const chunks = [];
84
- let totalSize = 0;
85
- let bypassed = false; // 超过缓冲上限后 bypass 标志
86
-
87
- // 恢复 res.write/res.end 的函数:在 finish/close 时调用,避免拦截函数泄漏
88
- const restore = () => {
89
- res.write = originalWrite;
90
- res.end = originalEnd;
91
- };
92
-
93
- // 拦截 res.write:收集 buffer 片段(流式输出场景)
94
- res.write = (chunk, ...args) => {
95
- if (bypassed) return originalWrite(chunk, ...args);
96
- if (chunk) {
97
- if (typeof chunk === 'string') {
98
- chunk = Buffer.from(chunk, args[0] || 'utf8');
99
- }
100
- if (Buffer.isBuffer(chunk)) {
101
- chunks.push(chunk);
102
- totalSize += chunk.length;
103
- // 缓冲超限:bypass 直接放行后续 write/end
104
- if (totalSize > MAX_BUFFER_BYTES) {
105
- console.warn(`[compress] 响应体超过 ${MAX_BUFFER_BYTES} 字节,bypass 直接放行不压缩`);
106
- bypassed = true;
107
- }
108
- }
109
- }
110
- return true;
111
- };
112
-
113
- // 拦截 res.end:收集最后一片 buffer 后决定是否压缩
114
- res.end = (chunk, encodingArg) => {
115
- if (bypassed) {
116
- // bypass 模式:直接走原 end,恢复后退出
117
- restore();
118
- if (chunk) return originalEnd(chunk, encodingArg);
119
- return originalEnd();
120
- }
121
-
122
- // res.end 多种签名处理:end() / end(chunk) / end(chunk, encoding) / end(callback)
123
- if (chunk) {
124
- if (typeof chunk === 'string') {
125
- const buf = Buffer.from(chunk, typeof encodingArg === 'string' ? encodingArg : 'utf8');
126
- chunks.push(buf);
127
- totalSize += buf.length;
128
- } else if (Buffer.isBuffer(chunk)) {
129
- chunks.push(chunk);
130
- totalSize += chunk.length;
131
- }
132
- // function 类型为 end(callback) 签名,不收集
133
- }
134
-
135
- const body = chunks.length ? Buffer.concat(chunks) : Buffer.alloc(0);
136
-
137
- // 取 Content-Type 主类型(去掉 charset 等参数)
138
- const contentType = (res.getHeader('Content-Type') || '').toString().split(';')[0].trim().toLowerCase();
139
-
140
- // 不满足压缩条件:原样输出
141
- // - 响应体小于阈值
142
- // - 非可压缩类型
143
- // - SSE(text/event-stream)需要实时推送,不压缩
144
- if (
145
- body.length < threshold ||
146
- !COMPRESSIBLE_TYPES.has(contentType) ||
147
- contentType === 'text/event-stream'
148
- ) {
149
- if (!res.headersSent) {
150
- res.setHeader('Content-Length', body.length);
151
- }
152
- restore();
153
- originalEnd(body);
154
- return res;
155
- }
156
-
157
- // 执行压缩
158
- try {
159
- const compressed = compressBuffer(body, encoding);
160
- // 移除原 Content-Length,避免与压缩后大小不匹配
161
- res.removeHeader('Content-Length');
162
- res.setHeader('Content-Encoding', encoding);
163
- res.setHeader('Vary', 'Accept-Encoding');
164
- res.setHeader('Content-Length', compressed.length);
165
- restore();
166
- originalEnd(compressed);
167
- } catch (e) {
168
- // 压缩失败:回退原样输出,避免请求中断
169
- console.error('[compress] 压缩失败,回退原样输出:', e.message);
170
- if (!res.headersSent) {
171
- res.setHeader('Content-Length', body.length);
172
- }
173
- restore();
174
- originalEnd(body);
175
- }
176
- return res;
177
- };
178
-
179
- // 响应结束后恢复,防止 res.write/res.end 拦截泄漏到后续中间件复用
180
- res.on('finish', restore);
181
- res.on('close', restore);
182
-
183
- next();
184
- };
185
- }
@@ -1,32 +0,0 @@
1
- import express from "express";
2
-
3
- /**
4
- * 请求体解析中间件配置
5
- * 配置请求体的解析方式
6
- */
7
-
8
- /**
9
- * 设置请求体解析中间件
10
- * @param {Object} app - Express 应用实例
11
- * @param {string} JSON_LIMIT - JSON 请求体大小限制
12
- * @description
13
- * 为 Express 应用配置请求体解析中间件
14
- * 支持 JSON、URL 编码和 XML 格式的请求体
15
- *
16
- * 安全改进:
17
- * - urlencoded 也设置 limit 和 parameterLimit,防止参数爆炸攻击
18
- * - extended: true 支持嵌套对象(与 qs 行为对齐)
19
- * @example
20
- * setBody(app, '10mb');
21
- */
22
- let setBody = function (app, JSON_LIMIT) {
23
- app.use(express.raw({ type: "application/xml", limit: JSON_LIMIT }));
24
- app.use(express.json({ limit: JSON_LIMIT }));
25
- app.use(express.urlencoded({
26
- extended: true,
27
- limit: JSON_LIMIT,
28
- parameterLimit: 100,
29
- }));
30
- };
31
-
32
- export { setBody };
package/realtime/index.js DELETED
@@ -1,7 +0,0 @@
1
- /**
2
- * 实时通信模块 - SSE + WebSocket(零第三方依赖)
3
- * - sse: Server-Sent Events,单向推送
4
- * - websocket: 基于 Node.js v22.4.0+ 原生 WebSocket API
5
- */
6
- export { sse, SSEManager } from "./sse.js";
7
- export { websocket, WebSocketManager } from "./websocket.js";