befly 3.8.31 → 3.8.33
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/befly.config.ts +90 -0
- package/lib/redisHelper.ts +14 -25
- package/package.json +5 -5
- package/sync/syncDb/table.ts +1 -1
- package/tests/redisHelper.test.ts +3 -3
- package/util.ts +283 -0
package/befly.config.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Befly 配置模块
|
|
3
|
+
* 自动加载 configs 目录下的配置文件并与默认配置合并
|
|
4
|
+
* 支持环境分离:befly.common.json + befly.dev/prod.json + befly.local.json
|
|
5
|
+
*/
|
|
6
|
+
import { scanConfig } from 'befly-shared/scanConfig';
|
|
7
|
+
|
|
8
|
+
import type { BeflyOptions } from './types/befly.js';
|
|
9
|
+
|
|
10
|
+
/** 默认配置 */
|
|
11
|
+
const defaultOptions: BeflyOptions = {
|
|
12
|
+
// ========== 核心参数 ==========
|
|
13
|
+
nodeEnv: (process.env.NODE_ENV as any) || 'development',
|
|
14
|
+
appName: '野蜂飞舞',
|
|
15
|
+
appPort: 3000,
|
|
16
|
+
appHost: '127.0.0.1',
|
|
17
|
+
devEmail: 'dev@qq.com',
|
|
18
|
+
devPassword: 'beflydev123456',
|
|
19
|
+
bodyLimit: 1048576, // 1MB
|
|
20
|
+
tz: 'Asia/Shanghai',
|
|
21
|
+
|
|
22
|
+
// ========== 日志配置 ==========
|
|
23
|
+
logger: {
|
|
24
|
+
debug: 1,
|
|
25
|
+
excludeFields: 'password,token,secret',
|
|
26
|
+
dir: './logs',
|
|
27
|
+
console: 1,
|
|
28
|
+
maxSize: 10485760 // 10MB
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
// ========== 数据库配置 ==========
|
|
32
|
+
db: {
|
|
33
|
+
type: 'mysql',
|
|
34
|
+
host: '127.0.0.1',
|
|
35
|
+
port: 3306,
|
|
36
|
+
username: 'root',
|
|
37
|
+
password: 'root',
|
|
38
|
+
database: 'befly_demo',
|
|
39
|
+
poolMax: 10
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
// ========== Redis 配置 ==========
|
|
43
|
+
redis: {
|
|
44
|
+
host: '127.0.0.1',
|
|
45
|
+
port: 6379,
|
|
46
|
+
username: '',
|
|
47
|
+
password: '',
|
|
48
|
+
db: 0,
|
|
49
|
+
prefix: 'befly_demo:'
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// ========== 认证配置 ==========
|
|
53
|
+
auth: {
|
|
54
|
+
secret: 'befly-secret',
|
|
55
|
+
expiresIn: '7d',
|
|
56
|
+
algorithm: 'HS256'
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
// ========== CORS 配置 ==========
|
|
60
|
+
cors: {
|
|
61
|
+
origin: '*',
|
|
62
|
+
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
|
63
|
+
allowedHeaders: 'Content-Type,Authorization',
|
|
64
|
+
exposedHeaders: '',
|
|
65
|
+
maxAge: 86400,
|
|
66
|
+
credentials: 'true'
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// ========== 禁用配置 ==========
|
|
70
|
+
disableHooks: [],
|
|
71
|
+
disablePlugins: [],
|
|
72
|
+
hiddenMenus: [],
|
|
73
|
+
|
|
74
|
+
// ========== Addon 配置 ==========
|
|
75
|
+
addons: {}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// 确定环境
|
|
79
|
+
const nodeEnv = process.env.NODE_ENV || 'development';
|
|
80
|
+
const envSuffix = nodeEnv === 'production' ? 'prod' : 'dev';
|
|
81
|
+
|
|
82
|
+
// 使用 scanConfig 一次性加载并合并所有配置文件
|
|
83
|
+
// 合并顺序:defaultOptions ← befly.common.json ← befly.dev/prod.json ← befly.local.json
|
|
84
|
+
export const beflyConfig = (await scanConfig({
|
|
85
|
+
dirs: ['configs'],
|
|
86
|
+
files: ['befly.common', `befly.${envSuffix}`, 'befly.local'],
|
|
87
|
+
extensions: ['.json'],
|
|
88
|
+
mode: 'merge',
|
|
89
|
+
defaults: defaultOptions
|
|
90
|
+
})) as BeflyOptions;
|
package/lib/redisHelper.ts
CHANGED
|
@@ -86,53 +86,42 @@ export class RedisHelper {
|
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
88
|
* 生成基于时间的唯一 ID
|
|
89
|
-
* 格式:
|
|
90
|
-
* 容量:
|
|
91
|
-
* 范围: 到 2286年
|
|
92
|
-
* @returns 唯一 ID (
|
|
89
|
+
* 格式: 毫秒时间戳(13位) + 3位自增 = 16位纯数字
|
|
90
|
+
* 容量: 1000/毫秒 = 1,000,000/秒
|
|
91
|
+
* 范围: 到 2286年
|
|
92
|
+
* @returns 唯一 ID (16位纯数字)
|
|
93
93
|
*/
|
|
94
94
|
async genTimeID(): Promise<number> {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const counter = await this.client.incr(key);
|
|
99
|
-
await this.client.expire(key, 1);
|
|
100
|
-
|
|
101
|
-
const counterSuffix = (counter % 10000).toString().padStart(4, '0');
|
|
102
|
-
|
|
103
|
-
return Number(`${timestamp}${counterSuffix}`);
|
|
95
|
+
const ids = await this.genTimeIDBatch(1);
|
|
96
|
+
return ids[0];
|
|
104
97
|
}
|
|
105
98
|
|
|
106
99
|
/**
|
|
107
100
|
* 批量生成基于时间的唯一 ID
|
|
108
|
-
* 格式:
|
|
101
|
+
* 格式: 毫秒时间戳(13位) + 3位自增 = 16位纯数字
|
|
109
102
|
* @param count - 需要生成的 ID 数量
|
|
110
|
-
* @returns ID 数组 (
|
|
103
|
+
* @returns ID 数组 (16位纯数字)
|
|
111
104
|
*/
|
|
112
105
|
async genTimeIDBatch(count: number): Promise<number[]> {
|
|
113
106
|
if (count <= 0) {
|
|
114
107
|
return [];
|
|
115
108
|
}
|
|
116
109
|
|
|
117
|
-
|
|
118
|
-
const MAX_BATCH_SIZE = 10000;
|
|
110
|
+
const MAX_BATCH_SIZE = 1000;
|
|
119
111
|
if (count > MAX_BATCH_SIZE) {
|
|
120
112
|
throw new Error(`批量大小 ${count} 超过最大限制 ${MAX_BATCH_SIZE}`);
|
|
121
113
|
}
|
|
122
114
|
|
|
123
|
-
const timestamp =
|
|
115
|
+
const timestamp = Date.now();
|
|
124
116
|
const key = `${this.prefix}time_id_counter:${timestamp}`;
|
|
125
|
-
|
|
126
|
-
// 使用 INCRBY 一次性获取 N 个连续计数
|
|
127
|
-
const startCounter = await this.client.incrby(key, count);
|
|
117
|
+
const endCounter = await this.client.incrby(key, count);
|
|
128
118
|
await this.client.expire(key, 1);
|
|
129
119
|
|
|
130
|
-
// 生成 ID 数组
|
|
131
120
|
const ids: number[] = [];
|
|
132
121
|
for (let i = 0; i < count; i++) {
|
|
133
|
-
const counter =
|
|
134
|
-
const
|
|
135
|
-
ids.push(Number(`${timestamp}${
|
|
122
|
+
const counter = endCounter - count + i + 1;
|
|
123
|
+
const suffix = (counter % 1000).toString().padStart(3, '0');
|
|
124
|
+
ids.push(Number(`${timestamp}${suffix}`));
|
|
136
125
|
}
|
|
137
126
|
|
|
138
127
|
return ids;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "befly",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.33",
|
|
4
4
|
"description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -50,15 +50,15 @@
|
|
|
50
50
|
".npmrc",
|
|
51
51
|
".prettierignore",
|
|
52
52
|
".prettierrc",
|
|
53
|
+
"befly.config.ts",
|
|
53
54
|
"bunfig.toml",
|
|
54
|
-
"config.ts",
|
|
55
55
|
"LICENSE",
|
|
56
56
|
"main.ts",
|
|
57
57
|
"paths.ts",
|
|
58
58
|
"package.json",
|
|
59
59
|
"README.md",
|
|
60
60
|
"tsconfig.json",
|
|
61
|
-
"
|
|
61
|
+
"util.ts"
|
|
62
62
|
],
|
|
63
63
|
"engines": {
|
|
64
64
|
"bun": ">=1.3.0"
|
|
@@ -67,13 +67,13 @@
|
|
|
67
67
|
"befly-shared": "^1.1.2",
|
|
68
68
|
"chalk": "^5.6.2",
|
|
69
69
|
"es-toolkit": "^1.42.0",
|
|
70
|
-
"fast-jwt": "^6.0
|
|
70
|
+
"fast-jwt": "^6.1.0",
|
|
71
71
|
"fast-xml-parser": "^5.3.2",
|
|
72
72
|
"pathe": "^2.0.3",
|
|
73
73
|
"pino": "^10.1.0",
|
|
74
74
|
"pino-roll": "^4.0.0"
|
|
75
75
|
},
|
|
76
|
-
"gitHead": "
|
|
76
|
+
"gitHead": "d470173b6ce7461cf8b66adbebe2ec7888e7b32d",
|
|
77
77
|
"devDependencies": {
|
|
78
78
|
"typescript": "^5.9.3"
|
|
79
79
|
}
|
package/sync/syncDb/table.ts
CHANGED
|
@@ -124,7 +124,7 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
|
|
|
124
124
|
}
|
|
125
125
|
} else {
|
|
126
126
|
const lenPart = isStringOrArrayType(fieldDef.type) ? ` 长度:${parseInt(String(fieldDef.max))}` : '';
|
|
127
|
-
Logger.debug(` + 新增字段 ${dbFieldName} (${fieldDef.type}${lenPart})`);
|
|
127
|
+
// Logger.debug(` + 新增字段 ${dbFieldName} (${fieldDef.type}${lenPart})`);
|
|
128
128
|
addClauses.push(generateDDLClause(fieldKey, fieldDef, true));
|
|
129
129
|
changed = true;
|
|
130
130
|
}
|
|
@@ -461,7 +461,7 @@ describe('RedisHelper - ID 生成', () => {
|
|
|
461
461
|
expect(typeof id1).toBe('number');
|
|
462
462
|
expect(typeof id2).toBe('number');
|
|
463
463
|
expect(id1).not.toBe(id2);
|
|
464
|
-
expect(id1.toString().length).toBe(
|
|
464
|
+
expect(id1.toString().length).toBe(16);
|
|
465
465
|
});
|
|
466
466
|
|
|
467
467
|
test('genTimeIDBatch - 批量生成 ID', async () => {
|
|
@@ -469,7 +469,7 @@ describe('RedisHelper - ID 生成', () => {
|
|
|
469
469
|
|
|
470
470
|
expect(ids.length).toBe(10);
|
|
471
471
|
expect(ids.every((id) => typeof id === 'number')).toBe(true);
|
|
472
|
-
expect(ids.every((id) => id.toString().length ===
|
|
472
|
+
expect(ids.every((id) => id.toString().length === 16)).toBe(true);
|
|
473
473
|
|
|
474
474
|
// 验证 ID 唯一性
|
|
475
475
|
const uniqueIds = new Set(ids);
|
|
@@ -483,7 +483,7 @@ describe('RedisHelper - ID 生成', () => {
|
|
|
483
483
|
|
|
484
484
|
test('genTimeIDBatch - 超过最大限制', async () => {
|
|
485
485
|
try {
|
|
486
|
-
await redis.genTimeIDBatch(
|
|
486
|
+
await redis.genTimeIDBatch(1001);
|
|
487
487
|
expect(true).toBe(false); // 不应该执行到这里
|
|
488
488
|
} catch (error: any) {
|
|
489
489
|
expect(error.message).toContain('超过最大限制');
|
package/util.ts
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 核心工具函数
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// 内部依赖
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 进程角色信息
|
|
10
|
+
*/
|
|
11
|
+
export interface ProcessRole {
|
|
12
|
+
/** 进程角色:primary(主进程)或 worker(工作进程) */
|
|
13
|
+
role: 'primary' | 'worker';
|
|
14
|
+
/** 实例 ID(PM2 或 Bun Worker) */
|
|
15
|
+
instanceId: string | null;
|
|
16
|
+
/** 运行环境:bun-cluster、pm2-cluster 或 standalone */
|
|
17
|
+
env: 'bun-cluster' | 'pm2-cluster' | 'standalone';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 获取当前进程角色信息
|
|
22
|
+
* @returns 进程角色、实例 ID 和运行环境
|
|
23
|
+
*/
|
|
24
|
+
export function getProcessRole(): ProcessRole {
|
|
25
|
+
const bunWorkerId = process.env.BUN_WORKER_ID;
|
|
26
|
+
const pm2InstanceId = process.env.PM2_INSTANCE_ID;
|
|
27
|
+
|
|
28
|
+
// Bun 集群模式
|
|
29
|
+
if (bunWorkerId !== undefined) {
|
|
30
|
+
return {
|
|
31
|
+
role: bunWorkerId === '' ? 'primary' : 'worker',
|
|
32
|
+
instanceId: bunWorkerId || '0',
|
|
33
|
+
env: 'bun-cluster'
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// PM2 集群模式
|
|
38
|
+
if (pm2InstanceId !== undefined) {
|
|
39
|
+
return {
|
|
40
|
+
role: pm2InstanceId === '0' ? 'primary' : 'worker',
|
|
41
|
+
instanceId: pm2InstanceId,
|
|
42
|
+
env: 'pm2-cluster'
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 单进程模式
|
|
47
|
+
return {
|
|
48
|
+
role: 'primary',
|
|
49
|
+
instanceId: null,
|
|
50
|
+
env: 'standalone'
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 检测当前进程是否为主进程
|
|
56
|
+
* 用于集群模式下避免重复执行同步任务
|
|
57
|
+
* - Bun 集群:BUN_WORKER_ID 为空时是主进程
|
|
58
|
+
* - PM2 集群:PM2_INSTANCE_ID 为 '0' 或不存在时是主进程
|
|
59
|
+
* @returns 是否为主进程
|
|
60
|
+
*/
|
|
61
|
+
export function isPrimaryProcess(): boolean {
|
|
62
|
+
return getProcessRole().role === 'primary';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 外部依赖
|
|
66
|
+
import { camelCase } from 'es-toolkit/string';
|
|
67
|
+
import { scanFiles } from 'befly-shared/scanFiles';
|
|
68
|
+
|
|
69
|
+
// 相对导入
|
|
70
|
+
import { Logger } from './lib/logger.js';
|
|
71
|
+
|
|
72
|
+
// 类型导入
|
|
73
|
+
import type { Plugin } from './types/plugin.js';
|
|
74
|
+
import type { Hook } from './types/hook.js';
|
|
75
|
+
import type { CorsConfig, BeflyContext } from './types/befly.js';
|
|
76
|
+
import type { RequestContext } from './types/context.js';
|
|
77
|
+
import type { PluginRequestHook, Next } from './types/plugin.js';
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 创建错误响应(专用于 Hook 中间件)
|
|
81
|
+
* 在钩子中提前拦截请求时使用
|
|
82
|
+
* @param ctx - 请求上下文
|
|
83
|
+
* @param msg - 错误消息
|
|
84
|
+
* @param code - 错误码,默认 1
|
|
85
|
+
* @param data - 附加数据,默认 null
|
|
86
|
+
* @param detail - 详细信息,用于标记具体提示位置,默认 null
|
|
87
|
+
* @returns Response 对象
|
|
88
|
+
*/
|
|
89
|
+
export function ErrorResponse(ctx: RequestContext, msg: string, code: number = 1, data: any = null, detail: any = null): Response {
|
|
90
|
+
// 记录拦截日志
|
|
91
|
+
if (ctx.requestId) {
|
|
92
|
+
const duration = Date.now() - ctx.now;
|
|
93
|
+
const user = ctx.user?.id ? `[User:${ctx.user.id}]` : '[Guest]';
|
|
94
|
+
Logger.info(`[${ctx.requestId}] ${ctx.route} ${user} ${duration}ms [${msg}]`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return Response.json(
|
|
98
|
+
{
|
|
99
|
+
code: code,
|
|
100
|
+
msg: msg,
|
|
101
|
+
data: data,
|
|
102
|
+
detail: detail
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
headers: ctx.corsHeaders
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 创建最终响应(专用于 API 路由结尾)
|
|
112
|
+
* 自动处理 ctx.response/ctx.result,并记录请求日志
|
|
113
|
+
* @param ctx - 请求上下文
|
|
114
|
+
* @returns Response 对象
|
|
115
|
+
*/
|
|
116
|
+
export function FinalResponse(ctx: RequestContext): Response {
|
|
117
|
+
// 记录请求日志
|
|
118
|
+
if (ctx.api && ctx.requestId) {
|
|
119
|
+
const duration = Date.now() - ctx.now;
|
|
120
|
+
const user = ctx.user?.id ? `[User:${ctx.user.id}]` : '[Guest]';
|
|
121
|
+
Logger.info(`[${ctx.requestId}] ${ctx.route} ${user} ${duration}ms`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 1. 如果已经有 response,直接返回
|
|
125
|
+
if (ctx.response) {
|
|
126
|
+
return ctx.response;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 2. 如果有 result,格式化为响应
|
|
130
|
+
if (ctx.result !== undefined) {
|
|
131
|
+
let result = ctx.result;
|
|
132
|
+
|
|
133
|
+
// 如果是字符串,自动包裹为成功响应
|
|
134
|
+
if (typeof result === 'string') {
|
|
135
|
+
result = {
|
|
136
|
+
code: 0,
|
|
137
|
+
msg: result
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// 如果是对象,自动补充 code: 0
|
|
141
|
+
else if (result && typeof result === 'object') {
|
|
142
|
+
if (!('code' in result)) {
|
|
143
|
+
result = {
|
|
144
|
+
code: 0,
|
|
145
|
+
...result
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 处理 BigInt 序列化问题
|
|
151
|
+
if (result && typeof result === 'object') {
|
|
152
|
+
const jsonString = JSON.stringify(result, (key, value) => (typeof value === 'bigint' ? value.toString() : value));
|
|
153
|
+
return new Response(jsonString, {
|
|
154
|
+
headers: {
|
|
155
|
+
...ctx.corsHeaders,
|
|
156
|
+
'Content-Type': 'application/json'
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
} else {
|
|
160
|
+
return Response.json(result, {
|
|
161
|
+
headers: ctx.corsHeaders
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 3. 默认响应:没有生成响应
|
|
167
|
+
return Response.json(
|
|
168
|
+
{
|
|
169
|
+
code: 1,
|
|
170
|
+
msg: '未生成响应'
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
headers: ctx.corsHeaders
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 设置 CORS 响应头
|
|
180
|
+
* @param req - 请求对象
|
|
181
|
+
* @param config - CORS 配置(可选)
|
|
182
|
+
* @returns CORS 响应头对象
|
|
183
|
+
*/
|
|
184
|
+
export function setCorsOptions(req: Request, config: CorsConfig = {}): Record<string, string> {
|
|
185
|
+
const origin = config.origin || '*';
|
|
186
|
+
return {
|
|
187
|
+
'Access-Control-Allow-Origin': origin === '*' ? req.headers.get('origin') || '*' : origin,
|
|
188
|
+
'Access-Control-Allow-Methods': config.methods || 'GET, POST, PUT, DELETE, OPTIONS',
|
|
189
|
+
'Access-Control-Allow-Headers': config.allowedHeaders || 'Content-Type, Authorization, authorization, token',
|
|
190
|
+
'Access-Control-Expose-Headers': config.exposedHeaders || 'Content-Range, X-Content-Range, Authorization, authorization, token',
|
|
191
|
+
'Access-Control-Max-Age': String(config.maxAge || 86400),
|
|
192
|
+
'Access-Control-Allow-Credentials': config.credentials || 'true'
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 扫描模块(插件或钩子)
|
|
198
|
+
* @param dir - 目录路径
|
|
199
|
+
* @param type - 模块类型(core/addon/app)
|
|
200
|
+
* @param moduleLabel - 模块标签(如"插件"、"钩子")
|
|
201
|
+
* @param addonName - 组件名称(仅 type='addon' 时需要)
|
|
202
|
+
* @returns 模块列表
|
|
203
|
+
*/
|
|
204
|
+
export async function scanModules<T extends Plugin | Hook>(dir: string, type: 'core' | 'addon' | 'app', moduleLabel: string, addonName?: string): Promise<T[]> {
|
|
205
|
+
if (!existsSync(dir)) return [];
|
|
206
|
+
|
|
207
|
+
const items: T[] = [];
|
|
208
|
+
const files = await scanFiles(dir, '*.{ts,js}');
|
|
209
|
+
|
|
210
|
+
for (const { filePath, fileName } of files) {
|
|
211
|
+
// 生成模块名称
|
|
212
|
+
const name = camelCase(fileName);
|
|
213
|
+
const moduleName = type === 'core' ? name : type === 'addon' ? `addon_${camelCase(addonName!)}_${name}` : `app_${name}`;
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const normalizedFilePath = filePath.replace(/\\/g, '/');
|
|
217
|
+
const moduleImport = await import(normalizedFilePath);
|
|
218
|
+
const item = moduleImport.default;
|
|
219
|
+
|
|
220
|
+
item.name = moduleName;
|
|
221
|
+
// 为 addon 模块记录 addon 名称
|
|
222
|
+
if (type === 'addon' && addonName) {
|
|
223
|
+
item.addonName = addonName;
|
|
224
|
+
}
|
|
225
|
+
items.push(item);
|
|
226
|
+
} catch (err: any) {
|
|
227
|
+
const typeLabel = type === 'core' ? '核心' : type === 'addon' ? `组件${addonName}` : '项目';
|
|
228
|
+
Logger.error({ err: err, module: fileName }, `${typeLabel}${moduleLabel} 导入失败`);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return items;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 排序模块(根据依赖关系)
|
|
238
|
+
* @param modules - 待排序的模块列表
|
|
239
|
+
* @returns 排序后的模块列表,如果存在循环依赖或依赖不存在则返回 false
|
|
240
|
+
*/
|
|
241
|
+
export function sortModules<T extends { name?: string; after?: string[] }>(modules: T[]): T[] | false {
|
|
242
|
+
const result: T[] = [];
|
|
243
|
+
const visited = new Set<string>();
|
|
244
|
+
const visiting = new Set<string>();
|
|
245
|
+
const moduleMap: Record<string, T> = Object.fromEntries(modules.map((m) => [m.name!, m]));
|
|
246
|
+
let isPass = true;
|
|
247
|
+
|
|
248
|
+
// 检查依赖是否存在
|
|
249
|
+
for (const module of modules) {
|
|
250
|
+
if (module.after) {
|
|
251
|
+
for (const dep of module.after) {
|
|
252
|
+
if (!moduleMap[dep]) {
|
|
253
|
+
Logger.error({ module: module.name, dependency: dep }, '依赖的模块未找到');
|
|
254
|
+
isPass = false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!isPass) return false;
|
|
261
|
+
|
|
262
|
+
const visit = (name: string): void => {
|
|
263
|
+
if (visited.has(name)) return;
|
|
264
|
+
if (visiting.has(name)) {
|
|
265
|
+
Logger.error({ module: name }, '模块循环依赖');
|
|
266
|
+
isPass = false;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const module = moduleMap[name];
|
|
271
|
+
if (!module) return;
|
|
272
|
+
|
|
273
|
+
visiting.add(name);
|
|
274
|
+
(module.after || []).forEach(visit);
|
|
275
|
+
visiting.delete(name);
|
|
276
|
+
visited.add(name);
|
|
277
|
+
result.push(module);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
modules.forEach((m) => visit(m.name!));
|
|
281
|
+
|
|
282
|
+
return isPass ? result : false;
|
|
283
|
+
}
|