befly 3.2.0 → 3.3.0
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/bin/index.ts +138 -0
- package/checks/conflict.ts +35 -25
- package/checks/table.ts +6 -6
- package/commands/addon.ts +57 -0
- package/commands/build.ts +74 -0
- package/commands/dev.ts +94 -0
- package/commands/index.ts +252 -0
- package/commands/script.ts +308 -0
- package/commands/start.ts +80 -0
- package/commands/syncApi.ts +328 -0
- package/{scripts → commands}/syncDb/apply.ts +2 -2
- package/{scripts → commands}/syncDb/constants.ts +13 -7
- package/{scripts → commands}/syncDb/ddl.ts +7 -5
- package/{scripts → commands}/syncDb/helpers.ts +18 -18
- package/{scripts → commands}/syncDb/index.ts +37 -23
- package/{scripts → commands}/syncDb/sqlite.ts +1 -1
- package/{scripts → commands}/syncDb/state.ts +10 -4
- package/{scripts → commands}/syncDb/table.ts +7 -7
- package/{scripts → commands}/syncDb/tableCreate.ts +7 -6
- package/{scripts → commands}/syncDb/types.ts +5 -5
- package/{scripts → commands}/syncDb/version.ts +1 -1
- package/commands/syncDb.ts +35 -0
- package/commands/syncDev.ts +174 -0
- package/commands/syncMenu.ts +368 -0
- package/config/env.ts +4 -4
- package/config/menu.json +67 -0
- package/{utils/crypto.ts → lib/cipher.ts} +16 -67
- package/lib/database.ts +296 -0
- package/{utils → lib}/dbHelper.ts +102 -56
- package/{utils → lib}/jwt.ts +124 -151
- package/{utils → lib}/logger.ts +47 -24
- package/lib/middleware.ts +271 -0
- package/{utils → lib}/redisHelper.ts +4 -4
- package/{utils/validate.ts → lib/validator.ts} +101 -78
- package/lifecycle/bootstrap.ts +63 -0
- package/lifecycle/checker.ts +165 -0
- package/lifecycle/cluster.ts +241 -0
- package/lifecycle/lifecycle.ts +139 -0
- package/lifecycle/loader.ts +513 -0
- package/main.ts +14 -12
- package/package.json +21 -9
- package/paths.ts +34 -0
- package/plugins/cache.ts +187 -0
- package/plugins/db.ts +4 -4
- package/plugins/logger.ts +1 -1
- package/plugins/redis.ts +4 -4
- package/router/api.ts +155 -0
- package/router/root.ts +53 -0
- package/router/static.ts +76 -0
- package/types/api.d.ts +0 -36
- package/types/befly.d.ts +8 -6
- package/types/common.d.ts +1 -1
- package/types/context.d.ts +3 -3
- package/types/util.d.ts +45 -0
- package/util.ts +301 -0
- package/config/fields.ts +0 -55
- package/config/regexAliases.ts +0 -51
- package/config/reserved.ts +0 -96
- package/scripts/syncDb/tests/constants.test.ts +0 -105
- package/scripts/syncDb/tests/ddl.test.ts +0 -134
- package/scripts/syncDb/tests/helpers.test.ts +0 -70
- package/scripts/syncDb.ts +0 -10
- package/types/index.d.ts +0 -450
- package/types/index.ts +0 -438
- package/types/validator.ts +0 -43
- package/utils/colors.ts +0 -221
- package/utils/database.ts +0 -348
- package/utils/helper.ts +0 -812
- package/utils/index.ts +0 -33
- package/utils/requestContext.ts +0 -167
- /package/{scripts → commands}/syncDb/schema.ts +0 -0
- /package/{utils → lib}/sqlBuilder.ts +0 -0
- /package/{utils → lib}/xml.ts +0 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 插件和API加载器
|
|
3
|
+
* 负责加载和初始化插件以及API路由
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { relative, basename } from 'pathe';
|
|
7
|
+
import { isPlainObject } from 'es-toolkit/compat';
|
|
8
|
+
import { Logger } from '../lib/logger.js';
|
|
9
|
+
import { calcPerfTime } from '../util.js';
|
|
10
|
+
import { paths } from '../paths.js';
|
|
11
|
+
import { scanAddons, getAddonDir, addonDirExists } from '../util.js';
|
|
12
|
+
import type { Plugin } from '../types/plugin.js';
|
|
13
|
+
import type { ApiRoute } from '../types/api.js';
|
|
14
|
+
import type { BeflyContext } from '../types/befly.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* API 默认字段定义
|
|
18
|
+
* 这些字段会自动合并到所有 API 的 fields 中
|
|
19
|
+
* API 自定义的同名字段可以覆盖这些默认值
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_API_FIELDS = {
|
|
22
|
+
id: 'ID|number|1|null|null|0|null',
|
|
23
|
+
page: '页码|number|1|9999|1|0|null',
|
|
24
|
+
limit: '每页数量|number|1|100|10|0|null',
|
|
25
|
+
keyword: '关键词|string|1|50|null|0|null',
|
|
26
|
+
state: '状态|number|0|2|1|1|null'
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 排序插件(根据依赖关系)
|
|
31
|
+
*/
|
|
32
|
+
export const sortPlugins = (plugins: Plugin[]): Plugin[] | false => {
|
|
33
|
+
const result: Plugin[] = [];
|
|
34
|
+
const visited = new Set<string>();
|
|
35
|
+
const visiting = new Set<string>();
|
|
36
|
+
const pluginMap: Record<string, Plugin> = Object.fromEntries(plugins.map((p) => [p.name, p]));
|
|
37
|
+
let isPass = true;
|
|
38
|
+
|
|
39
|
+
const visit = (name: string): void => {
|
|
40
|
+
if (visited.has(name)) return;
|
|
41
|
+
if (visiting.has(name)) {
|
|
42
|
+
isPass = false;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const plugin = pluginMap[name];
|
|
47
|
+
if (!plugin) return;
|
|
48
|
+
|
|
49
|
+
visiting.add(name);
|
|
50
|
+
(plugin.dependencies || []).forEach(visit);
|
|
51
|
+
visiting.delete(name);
|
|
52
|
+
visited.add(name);
|
|
53
|
+
result.push(plugin);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
plugins.forEach((p) => visit(p.name));
|
|
57
|
+
return isPass ? result : false;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 带超时的动态导入函数
|
|
62
|
+
* @param filePath - 文件路径
|
|
63
|
+
* @param timeout - 超时时间(毫秒),默认 3000ms
|
|
64
|
+
* @returns 导入的模块
|
|
65
|
+
*/
|
|
66
|
+
async function importWithTimeout(filePath: string, timeout: number = 3000): Promise<any> {
|
|
67
|
+
return Promise.race([
|
|
68
|
+
import(filePath),
|
|
69
|
+
new Promise((_, reject) =>
|
|
70
|
+
setTimeout(() => {
|
|
71
|
+
reject(new Error(`模块导入超时 (${timeout}ms),可能存在死循环或模块依赖问题`));
|
|
72
|
+
}, timeout)
|
|
73
|
+
)
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 加载器类
|
|
79
|
+
*/
|
|
80
|
+
export class Loader {
|
|
81
|
+
/**
|
|
82
|
+
* 加载所有插件
|
|
83
|
+
* @param befly - Befly实例(需要访问 pluginLists 和 appContext)
|
|
84
|
+
*/
|
|
85
|
+
static async loadPlugins(befly: { pluginLists: Plugin[]; appContext: BeflyContext }): Promise<void> {
|
|
86
|
+
try {
|
|
87
|
+
const loadStartTime = Bun.nanoseconds();
|
|
88
|
+
|
|
89
|
+
const glob = new Bun.Glob('*.ts');
|
|
90
|
+
const corePlugins: Plugin[] = [];
|
|
91
|
+
const addonPlugins: Plugin[] = [];
|
|
92
|
+
const userPlugins: Plugin[] = [];
|
|
93
|
+
const loadedPluginNames = new Set<string>(); // 用于跟踪已加载的插件名称
|
|
94
|
+
let hadCorePluginError = false; // 核心插件错误(关键)
|
|
95
|
+
let hadAddonPluginError = false; // Addon 插件错误(警告)
|
|
96
|
+
let hadUserPluginError = false; // 用户插件错误(警告)
|
|
97
|
+
|
|
98
|
+
// 扫描核心插件目录
|
|
99
|
+
const corePluginsScanStart = Bun.nanoseconds();
|
|
100
|
+
for await (const file of glob.scan({
|
|
101
|
+
cwd: paths.rootPluginDir,
|
|
102
|
+
onlyFiles: true,
|
|
103
|
+
absolute: true
|
|
104
|
+
})) {
|
|
105
|
+
const fileName = basename(file).replace(/\.ts$/, '');
|
|
106
|
+
if (fileName.startsWith('_')) continue;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const importStart = Bun.nanoseconds();
|
|
110
|
+
Logger.debug(`准备导入核心插件: ${fileName}`);
|
|
111
|
+
const plugin = await importWithTimeout(file);
|
|
112
|
+
const importTime = calcPerfTime(importStart);
|
|
113
|
+
Logger.debug(`核心插件 ${fileName} 导入成功,耗时: ${importTime}`);
|
|
114
|
+
|
|
115
|
+
const pluginInstance = plugin.default;
|
|
116
|
+
pluginInstance.pluginName = fileName;
|
|
117
|
+
corePlugins.push(pluginInstance);
|
|
118
|
+
loadedPluginNames.add(fileName); // 记录已加载的核心插件名称
|
|
119
|
+
|
|
120
|
+
Logger.info(`核心插件 ${fileName} 导入耗时: ${importTime}`);
|
|
121
|
+
} catch (err: any) {
|
|
122
|
+
hadCorePluginError = true;
|
|
123
|
+
Logger.error(`核心插件 ${fileName} 导入失败`, error);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const corePluginsScanTime = calcPerfTime(corePluginsScanStart);
|
|
128
|
+
Logger.info(`核心插件扫描完成,耗时: ${corePluginsScanTime},共找到 ${corePlugins.length} 个插件`);
|
|
129
|
+
|
|
130
|
+
Logger.debug(`调试模式已开启`);
|
|
131
|
+
Logger.debug(`开始排序核心插件,插件列表: ${corePlugins.map((p) => p.pluginName).join(', ')}`);
|
|
132
|
+
|
|
133
|
+
const sortedCorePlugins = sortPlugins(corePlugins);
|
|
134
|
+
if (sortedCorePlugins === false) {
|
|
135
|
+
Logger.warn('核心插件依赖关系错误,请检查插件的 after 属性');
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
Logger.debug(`核心插件排序完成,顺序: ${sortedCorePlugins.map((p) => p.pluginName).join(' -> ')}`);
|
|
140
|
+
|
|
141
|
+
// 初始化核心插件
|
|
142
|
+
const corePluginsInitStart = Bun.nanoseconds();
|
|
143
|
+
Logger.info(`开始初始化核心插件...`);
|
|
144
|
+
for (const plugin of sortedCorePlugins) {
|
|
145
|
+
try {
|
|
146
|
+
Logger.debug(`准备初始化核心插件: ${plugin.pluginName}`);
|
|
147
|
+
|
|
148
|
+
befly.pluginLists.push(plugin);
|
|
149
|
+
|
|
150
|
+
Logger.debug(`检查插件 ${plugin.pluginName} 是否有 onInit 方法: ${typeof plugin?.onInit === 'function'}`);
|
|
151
|
+
|
|
152
|
+
if (typeof plugin?.onInit === 'function') {
|
|
153
|
+
Logger.debug(`开始执行插件 ${plugin.pluginName} 的 onInit 方法`);
|
|
154
|
+
|
|
155
|
+
const pluginInitStart = Bun.nanoseconds();
|
|
156
|
+
befly.appContext[plugin.pluginName] = await plugin?.onInit(befly.appContext);
|
|
157
|
+
const pluginInitTime = calcPerfTime(pluginInitStart);
|
|
158
|
+
|
|
159
|
+
Logger.debug(`插件 ${plugin.pluginName} 初始化完成,耗时: ${pluginInitTime}`);
|
|
160
|
+
} else {
|
|
161
|
+
befly.appContext[plugin.pluginName] = {};
|
|
162
|
+
Logger.debug(`插件 ${plugin.pluginName} 没有 onInit 方法,跳过初始化`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
Logger.info(`核心插件 ${plugin.pluginName} 初始化成功`);
|
|
166
|
+
} catch (error: any) {
|
|
167
|
+
hadCorePluginError = true;
|
|
168
|
+
Logger.error(`核心插件 ${plugin.pluginName} 初始化失败`, error);
|
|
169
|
+
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const corePluginsInitTime = calcPerfTime(corePluginsInitStart);
|
|
174
|
+
Logger.info(`核心插件初始化完成,耗时: ${corePluginsInitTime}`);
|
|
175
|
+
|
|
176
|
+
// 扫描 addon 插件目录
|
|
177
|
+
const addons = scanAddons();
|
|
178
|
+
if (addons.length > 0) {
|
|
179
|
+
const addonPluginsScanStart = Bun.nanoseconds();
|
|
180
|
+
for (const addon of addons) {
|
|
181
|
+
if (!addonDirExists(addon, 'plugins')) continue;
|
|
182
|
+
|
|
183
|
+
const addonPluginsDir = getAddonDir(addon, 'plugins');
|
|
184
|
+
for await (const file of glob.scan({
|
|
185
|
+
cwd: addonPluginsDir,
|
|
186
|
+
onlyFiles: true,
|
|
187
|
+
absolute: true
|
|
188
|
+
})) {
|
|
189
|
+
const fileName = basename(file).replace(/\.ts$/, '');
|
|
190
|
+
if (fileName.startsWith('_')) continue;
|
|
191
|
+
|
|
192
|
+
const pluginFullName = `${addon}.${fileName}`;
|
|
193
|
+
|
|
194
|
+
// 检查是否已经加载了同名插件
|
|
195
|
+
if (loadedPluginNames.has(pluginFullName)) {
|
|
196
|
+
Logger.info(`跳过组件插件 ${pluginFullName},因为同名插件已存在`);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const importStart = Bun.nanoseconds();
|
|
202
|
+
Logger.debug(`准备导入 addon 插件: ${addon}.${fileName}`);
|
|
203
|
+
const plugin = await importWithTimeout(file);
|
|
204
|
+
const importTime = calcPerfTime(importStart);
|
|
205
|
+
Logger.debug(`Addon 插件 ${addon}.${fileName} 导入成功,耗时: ${importTime}`);
|
|
206
|
+
|
|
207
|
+
const pluginInstance = plugin.default;
|
|
208
|
+
pluginInstance.pluginName = pluginFullName;
|
|
209
|
+
addonPlugins.push(pluginInstance);
|
|
210
|
+
loadedPluginNames.add(pluginFullName);
|
|
211
|
+
|
|
212
|
+
Logger.info(`组件${addon} ${fileName} 导入耗时: ${importTime}`);
|
|
213
|
+
} catch (err: any) {
|
|
214
|
+
hadAddonPluginError = true;
|
|
215
|
+
Logger.error(`组件${addon} ${fileName} 导入失败`, error);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const addonPluginsScanTime = calcPerfTime(addonPluginsScanStart);
|
|
221
|
+
Logger.info(`组件插件扫描完成,耗时: ${addonPluginsScanTime},共找到 ${addonPlugins.length} 个插件`);
|
|
222
|
+
|
|
223
|
+
const sortedAddonPlugins = sortPlugins(addonPlugins);
|
|
224
|
+
if (sortedAddonPlugins === false) {
|
|
225
|
+
Logger.warn({
|
|
226
|
+
level: 'WARNING',
|
|
227
|
+
msg: '组件插件依赖关系错误,请检查插件的 after 属性'
|
|
228
|
+
});
|
|
229
|
+
} else {
|
|
230
|
+
// 初始化组件插件
|
|
231
|
+
const addonPluginsInitStart = Bun.nanoseconds();
|
|
232
|
+
Logger.info(`开始初始化组件插件...`);
|
|
233
|
+
for (const plugin of sortedAddonPlugins) {
|
|
234
|
+
try {
|
|
235
|
+
Logger.debug(`准备初始化组件插件: ${plugin.pluginName}`);
|
|
236
|
+
|
|
237
|
+
befly.pluginLists.push(plugin);
|
|
238
|
+
|
|
239
|
+
if (typeof plugin?.onInit === 'function') {
|
|
240
|
+
Logger.debug(`开始执行组件插件 ${plugin.pluginName} 的 onInit 方法`);
|
|
241
|
+
|
|
242
|
+
const pluginInitStart = Bun.nanoseconds();
|
|
243
|
+
befly.appContext[plugin.pluginName] = await plugin?.onInit(befly.appContext);
|
|
244
|
+
const pluginInitTime = calcPerfTime(pluginInitStart);
|
|
245
|
+
|
|
246
|
+
Logger.debug(`组件插件 ${plugin.pluginName} 初始化完成,耗时: ${pluginInitTime}`);
|
|
247
|
+
} else {
|
|
248
|
+
befly.appContext[plugin.pluginName] = {};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
Logger.info(`组件插件 ${plugin.pluginName} 初始化成功`);
|
|
252
|
+
} catch (error: any) {
|
|
253
|
+
hadAddonPluginError = true;
|
|
254
|
+
Logger.error(`组件插件 ${plugin.pluginName} 初始化失败`, error);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const addonPluginsInitTime = calcPerfTime(addonPluginsInitStart);
|
|
258
|
+
Logger.info(`组件插件初始化完成,耗时: ${addonPluginsInitTime}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 扫描用户插件目录
|
|
263
|
+
const userPluginsScanStart = Bun.nanoseconds();
|
|
264
|
+
for await (const file of glob.scan({
|
|
265
|
+
cwd: paths.projectPluginDir,
|
|
266
|
+
onlyFiles: true,
|
|
267
|
+
absolute: true
|
|
268
|
+
})) {
|
|
269
|
+
const fileName = basename(file).replace(/\.ts$/, '');
|
|
270
|
+
if (fileName.startsWith('_')) continue;
|
|
271
|
+
|
|
272
|
+
// 检查是否已经加载了同名的核心插件
|
|
273
|
+
if (loadedPluginNames.has(fileName)) {
|
|
274
|
+
Logger.info(`跳过用户插件 ${fileName},因为同名的核心插件已存在`);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const importStart = Bun.nanoseconds();
|
|
280
|
+
Logger.debug(`准备导入用户插件: ${fileName}`);
|
|
281
|
+
const plugin = await importWithTimeout(file);
|
|
282
|
+
const importTime = calcPerfTime(importStart);
|
|
283
|
+
Logger.debug(`用户插件 ${fileName} 导入成功,耗时: ${importTime}`);
|
|
284
|
+
|
|
285
|
+
const pluginInstance = plugin.default;
|
|
286
|
+
pluginInstance.pluginName = fileName;
|
|
287
|
+
userPlugins.push(pluginInstance);
|
|
288
|
+
|
|
289
|
+
Logger.info(`用户插件 ${fileName} 导入耗时: ${importTime}`);
|
|
290
|
+
} catch (err: any) {
|
|
291
|
+
hadUserPluginError = true;
|
|
292
|
+
Logger.error(`用户插件 ${fileName} 导入失败`, error);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const userPluginsScanTime = calcPerfTime(userPluginsScanStart);
|
|
297
|
+
Logger.info(`用户插件扫描完成,耗时: ${userPluginsScanTime},共找到 ${userPlugins.length} 个插件`);
|
|
298
|
+
|
|
299
|
+
const sortedUserPlugins = sortPlugins(userPlugins);
|
|
300
|
+
if (sortedUserPlugins === false) {
|
|
301
|
+
Logger.warn({
|
|
302
|
+
level: 'WARNING',
|
|
303
|
+
msg: '用户插件依赖关系错误,请检查插件的 after 属性'
|
|
304
|
+
});
|
|
305
|
+
// 用户插件错误不退出,只是跳过这些插件
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 初始化用户插件
|
|
310
|
+
if (userPlugins.length > 0) {
|
|
311
|
+
const userPluginsInitStart = Bun.nanoseconds();
|
|
312
|
+
Logger.info(`开始初始化用户插件...`);
|
|
313
|
+
for (const plugin of sortedUserPlugins) {
|
|
314
|
+
try {
|
|
315
|
+
Logger.debug(`准备初始化用户插件: ${plugin.pluginName}`);
|
|
316
|
+
|
|
317
|
+
befly.pluginLists.push(plugin);
|
|
318
|
+
|
|
319
|
+
if (typeof plugin?.onInit === 'function') {
|
|
320
|
+
Logger.debug(`开始执行用户插件 ${plugin.pluginName} 的 onInit 方法`);
|
|
321
|
+
|
|
322
|
+
const pluginInitStart = Bun.nanoseconds();
|
|
323
|
+
befly.appContext[plugin.pluginName] = await plugin?.onInit(befly.appContext);
|
|
324
|
+
const pluginInitTime = calcPerfTime(pluginInitStart);
|
|
325
|
+
|
|
326
|
+
Logger.debug(`用户插件 ${plugin.pluginName} 初始化完成,耗时: ${pluginInitTime}`);
|
|
327
|
+
} else {
|
|
328
|
+
befly.appContext[plugin.pluginName] = {};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
Logger.info(`用户插件 ${plugin.pluginName} 初始化成功`);
|
|
332
|
+
} catch (error: any) {
|
|
333
|
+
hadUserPluginError = true;
|
|
334
|
+
Logger.error(`用户插件 ${plugin.pluginName} 初始化失败`, error);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const userPluginsInitTime = calcPerfTime(userPluginsInitStart);
|
|
338
|
+
Logger.info(`用户插件初始化完成,耗时: ${userPluginsInitTime}`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const totalLoadTime = calcPerfTime(loadStartTime);
|
|
342
|
+
const totalPluginCount = sortedCorePlugins.length + addonPlugins.length + sortedUserPlugins.length;
|
|
343
|
+
Logger.info(`插件加载完成! 总耗时: ${totalLoadTime},共加载 ${totalPluginCount} 个插件`);
|
|
344
|
+
|
|
345
|
+
// 核心插件失败 → 关键错误,必须退出
|
|
346
|
+
if (hadCorePluginError) {
|
|
347
|
+
Logger.warn('核心插件加载失败,无法继续启动', {
|
|
348
|
+
corePluginCount: sortedCorePlugins.length,
|
|
349
|
+
totalPluginCount
|
|
350
|
+
});
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Addon 插件失败 → 警告,可以继续运行
|
|
355
|
+
if (hadAddonPluginError) {
|
|
356
|
+
Logger.info({
|
|
357
|
+
level: 'INFO',
|
|
358
|
+
msg: '部分 Addon 插件加载失败,但不影响核心功能',
|
|
359
|
+
addonPluginCount: addonPlugins.length
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// 用户插件失败 → 警告,可以继续运行
|
|
364
|
+
if (hadUserPluginError) {
|
|
365
|
+
Logger.info({
|
|
366
|
+
level: 'INFO',
|
|
367
|
+
msg: '部分用户插件加载失败,但不影响核心功能',
|
|
368
|
+
userPluginCount: sortedUserPlugins.length
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
} catch (error: any) {
|
|
372
|
+
Logger.error('加载插件时发生错误', error);
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* 加载API路由
|
|
379
|
+
* @param dirName - 目录名称 ('core' | 'app' | addon名称)
|
|
380
|
+
* @param apiRoutes - API路由映射表
|
|
381
|
+
* @param options - 可选配置
|
|
382
|
+
* @param options.where - API来源类型:'core' | 'addon' | 'app'
|
|
383
|
+
* @param options.addonName - addon名称(仅当 where='addon' 时需要)
|
|
384
|
+
*/
|
|
385
|
+
static async loadApis(dirName: string, apiRoutes: Map<string, ApiRoute>, options?: { where?: 'core' | 'addon' | 'app'; addonName?: string }): Promise<void> {
|
|
386
|
+
try {
|
|
387
|
+
const loadStartTime = Bun.nanoseconds();
|
|
388
|
+
const where = options?.where || 'app';
|
|
389
|
+
const addonName = options?.addonName || '';
|
|
390
|
+
const dirDisplayName = where === 'core' ? '核心' : where === 'addon' ? `组件${addonName}` : '用户';
|
|
391
|
+
|
|
392
|
+
const glob = new Bun.Glob('**/*.ts');
|
|
393
|
+
let apiDir: string;
|
|
394
|
+
|
|
395
|
+
if (where === 'core') {
|
|
396
|
+
apiDir = paths.rootApiDir;
|
|
397
|
+
} else if (where === 'addon') {
|
|
398
|
+
apiDir = getAddonDir(addonName, 'apis');
|
|
399
|
+
} else {
|
|
400
|
+
apiDir = paths.projectApiDir;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
let totalApis = 0;
|
|
404
|
+
let loadedApis = 0;
|
|
405
|
+
let failedApis = 0;
|
|
406
|
+
|
|
407
|
+
// 扫描指定目录
|
|
408
|
+
for await (const file of glob.scan({
|
|
409
|
+
cwd: apiDir,
|
|
410
|
+
onlyFiles: true,
|
|
411
|
+
absolute: true
|
|
412
|
+
})) {
|
|
413
|
+
const fileName = basename(file).replace(/\.ts$/, '');
|
|
414
|
+
const apiPath = relative(apiDir, file).replace(/\.ts$/, '');
|
|
415
|
+
if (apiPath.indexOf('_') !== -1) continue;
|
|
416
|
+
|
|
417
|
+
totalApis++;
|
|
418
|
+
const singleApiStart = Bun.nanoseconds();
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
Logger.debug(`[${dirDisplayName}] 准备导入 API 文件: ${apiPath}`);
|
|
422
|
+
Logger.debug(`[${dirDisplayName}] 文件绝对路径: ${file}`);
|
|
423
|
+
|
|
424
|
+
const importStart = Bun.nanoseconds();
|
|
425
|
+
const api = (await importWithTimeout(file)).default;
|
|
426
|
+
const importTime = calcPerfTime(importStart);
|
|
427
|
+
|
|
428
|
+
Logger.debug(`[${dirDisplayName}] API 文件导入成功: ${apiPath},耗时: ${importTime}`);
|
|
429
|
+
|
|
430
|
+
Logger.debug(`[${dirDisplayName}] 开始验证 API 属性: ${apiPath}`);
|
|
431
|
+
|
|
432
|
+
// 验证必填属性:name 和 handler
|
|
433
|
+
if (typeof api.name !== 'string' || api.name.trim() === '') {
|
|
434
|
+
throw new Error(`接口 ${apiPath} 的 name 属性必须是非空字符串`);
|
|
435
|
+
}
|
|
436
|
+
if (typeof api.handler !== 'function') {
|
|
437
|
+
throw new Error(`接口 ${apiPath} 的 handler 属性必须是函数`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// 设置默认值
|
|
441
|
+
api.method = api.method || 'POST';
|
|
442
|
+
api.auth = api.auth !== undefined ? api.auth : true;
|
|
443
|
+
|
|
444
|
+
// 合并默认字段:先设置默认字段,再用 API 自定义字段覆盖
|
|
445
|
+
api.fields = { ...DEFAULT_API_FIELDS, ...(api.fields || {}) };
|
|
446
|
+
api.required = api.required || [];
|
|
447
|
+
|
|
448
|
+
// 验证可选属性的类型(如果提供了)
|
|
449
|
+
if (api.method && !['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'].includes(api.method.toUpperCase())) {
|
|
450
|
+
throw new Error(`接口 ${apiPath} 的 method 属性必须是有效的 HTTP 方法`);
|
|
451
|
+
}
|
|
452
|
+
if (api.auth !== undefined && typeof api.auth !== 'boolean') {
|
|
453
|
+
throw new Error(`接口 ${apiPath} 的 auth 属性必须是布尔值 (true=需登录, false=公开)`);
|
|
454
|
+
}
|
|
455
|
+
if (api.fields && !isPlainObject(api.fields)) {
|
|
456
|
+
throw new Error(`接口 ${apiPath} 的 fields 属性必须是对象`);
|
|
457
|
+
}
|
|
458
|
+
if (api.required && !Array.isArray(api.required)) {
|
|
459
|
+
throw new Error(`接口 ${apiPath} 的 required 属性必须是数组`);
|
|
460
|
+
}
|
|
461
|
+
if (api.required && api.required.some((item: any) => typeof item !== 'string')) {
|
|
462
|
+
throw new Error(`接口 ${apiPath} 的 required 属性必须是字符串数组`);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
Logger.debug(`[${dirDisplayName}] API 属性验证通过: ${apiPath}`);
|
|
466
|
+
|
|
467
|
+
// 构建路由:
|
|
468
|
+
// - core 接口: /api/core/{apiPath}
|
|
469
|
+
// - addon 接口: /api/addon/{addonName}/{apiPath}
|
|
470
|
+
// - 项目接口: /api/{apiPath}
|
|
471
|
+
if (where === 'core') {
|
|
472
|
+
api.route = `${api.method.toUpperCase()}/api/core/${apiPath}`;
|
|
473
|
+
} else if (where === 'addon') {
|
|
474
|
+
api.route = `${api.method.toUpperCase()}/api/addon/${addonName}/${apiPath}`;
|
|
475
|
+
} else {
|
|
476
|
+
api.route = `${api.method.toUpperCase()}/api/${apiPath}`;
|
|
477
|
+
}
|
|
478
|
+
apiRoutes.set(api.route, api);
|
|
479
|
+
|
|
480
|
+
const singleApiTime = calcPerfTime(singleApiStart);
|
|
481
|
+
loadedApis++;
|
|
482
|
+
Logger.debug(`[${dirDisplayName}] API 注册成功 - 名称: ${api.name}, 路由: ${api.route}, 耗时: ${singleApiTime}`);
|
|
483
|
+
} catch (error: any) {
|
|
484
|
+
const singleApiTime = calcPerfTime(singleApiStart);
|
|
485
|
+
failedApis++;
|
|
486
|
+
|
|
487
|
+
const errorMessage = error?.message || '未知错误';
|
|
488
|
+
|
|
489
|
+
// 记录详细错误信息
|
|
490
|
+
Logger.error(`[${dirDisplayName}] 接口 ${apiPath} 加载失败 (${singleApiTime})`, error);
|
|
491
|
+
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const totalLoadTime = calcPerfTime(loadStartTime);
|
|
497
|
+
Logger.info(`${dirDisplayName}接口加载完成! 总耗时: ${totalLoadTime},总数: ${totalApis}, 成功: ${loadedApis}, 失败: ${failedApis}`);
|
|
498
|
+
|
|
499
|
+
// 检查是否有加载失败的 API(理论上不会到达这里,因为上面已经 critical 退出)
|
|
500
|
+
if (failedApis > 0) {
|
|
501
|
+
Logger.warn(`有 ${failedApis} 个${dirDisplayName}接口加载失败,无法继续启动服务`, {
|
|
502
|
+
dirName,
|
|
503
|
+
totalApis,
|
|
504
|
+
failedApis
|
|
505
|
+
});
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
508
|
+
} catch (error: any) {
|
|
509
|
+
Logger.error(`加载${dirDisplayName}接口时发生错误`, error);
|
|
510
|
+
process.exit(1);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
package/main.ts
CHANGED
|
@@ -4,20 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { Env } from './config/env.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { Jwt } from './
|
|
11
|
-
import { Validator } from './utils/validate.js';
|
|
12
|
-
import { Crypto2 } from './utils/crypto.js';
|
|
13
|
-
import { DbHelper } from './utils/dbHelper.js';
|
|
14
|
-
import { createSqlClient, getRedis, getSql, getDbHelper, initDatabase, closeDatabase } from './utils/database.js';
|
|
15
|
-
import { RedisHelper } from './utils/redisHelper.js';
|
|
7
|
+
import { Yes, No } from './util.js';
|
|
8
|
+
import { Logger } from './lib/logger.js';
|
|
9
|
+
import { Cipher } from './lib/cipher.js';
|
|
10
|
+
import { Jwt } from './lib/jwt.js';
|
|
16
11
|
import { Lifecycle } from './lifecycle/lifecycle.js';
|
|
17
12
|
|
|
18
13
|
import type { Server } from 'bun';
|
|
19
14
|
import type { BeflyContext, BeflyOptions } from './types/befly.js';
|
|
20
|
-
|
|
21
15
|
/**
|
|
22
16
|
* Befly 框架核心类
|
|
23
17
|
* 职责:管理应用上下文和生命周期
|
|
@@ -43,5 +37,13 @@ export class Befly {
|
|
|
43
37
|
}
|
|
44
38
|
}
|
|
45
39
|
|
|
46
|
-
//
|
|
47
|
-
export {
|
|
40
|
+
// 核心类和工具导出
|
|
41
|
+
export {
|
|
42
|
+
// 配置
|
|
43
|
+
Env,
|
|
44
|
+
Logger,
|
|
45
|
+
Cipher,
|
|
46
|
+
Jwt,
|
|
47
|
+
Yes,
|
|
48
|
+
No
|
|
49
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "befly",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
},
|
|
11
11
|
"main": "main.ts",
|
|
12
12
|
"types": "./types/index.d.ts",
|
|
13
|
+
"bin": {
|
|
14
|
+
"befly": "./bin/index.ts"
|
|
15
|
+
},
|
|
13
16
|
"exports": {
|
|
14
17
|
".": {
|
|
15
18
|
"default": "./main.ts"
|
|
@@ -27,6 +30,7 @@
|
|
|
27
30
|
"api",
|
|
28
31
|
"framework",
|
|
29
32
|
"core",
|
|
33
|
+
"cli",
|
|
30
34
|
"typescript",
|
|
31
35
|
"javascript",
|
|
32
36
|
"backend",
|
|
@@ -38,14 +42,15 @@
|
|
|
38
42
|
"homepage": "https://chensuiyi.me",
|
|
39
43
|
"license": "Apache-2.0",
|
|
40
44
|
"files": [
|
|
41
|
-
"
|
|
45
|
+
"bin/",
|
|
42
46
|
"checks/",
|
|
47
|
+
"commands/",
|
|
43
48
|
"config/",
|
|
49
|
+
"lib/",
|
|
50
|
+
"lifecycle/",
|
|
44
51
|
"plugins/",
|
|
45
|
-
"
|
|
46
|
-
"tables/",
|
|
52
|
+
"router/",
|
|
47
53
|
"types/",
|
|
48
|
-
"utils/",
|
|
49
54
|
".gitignore",
|
|
50
55
|
".npmignore",
|
|
51
56
|
".bunignore",
|
|
@@ -57,14 +62,21 @@
|
|
|
57
62
|
"tsconfig.json",
|
|
58
63
|
"LICENSE",
|
|
59
64
|
"main.ts",
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"system.js",
|
|
65
|
+
"paths.ts",
|
|
66
|
+
"util.ts",
|
|
63
67
|
"package.json",
|
|
64
68
|
"README.md"
|
|
65
69
|
],
|
|
66
70
|
"engines": {
|
|
67
71
|
"bun": ">=1.3.0"
|
|
68
72
|
},
|
|
69
|
-
"
|
|
73
|
+
"dependencies": {
|
|
74
|
+
"chalk": "^5.6.2",
|
|
75
|
+
"commander": "^14.0.2",
|
|
76
|
+
"es-toolkit": "^1.41.0",
|
|
77
|
+
"inquirer": "^12.10.0",
|
|
78
|
+
"ora": "^9.0.0",
|
|
79
|
+
"pathe": "^2.0.3"
|
|
80
|
+
},
|
|
81
|
+
"gitHead": "24b35d0ef83d78db3dea67784e052516d4a83fc5"
|
|
70
82
|
}
|
package/paths.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Befly 框架路径配置
|
|
3
|
+
* 提供统一的路径变量,供整个框架使用
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { dirname, join } from 'pathe';
|
|
8
|
+
|
|
9
|
+
// 当前文件的路径信息
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
|
|
13
|
+
// 项目根目录(befly 框架的使用方项目)
|
|
14
|
+
const projectRoot = process.cwd();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 系统路径配置对象
|
|
18
|
+
*/
|
|
19
|
+
export const paths = {
|
|
20
|
+
rootDir: __dirname,
|
|
21
|
+
rootScriptDir: join(__dirname, 'scripts'),
|
|
22
|
+
rootConfigDir: join(__dirname, 'config'),
|
|
23
|
+
rootCheckDir: join(__dirname, 'checks'),
|
|
24
|
+
rootPluginDir: join(__dirname, 'plugins'),
|
|
25
|
+
rootApiDir: join(__dirname, 'apis'),
|
|
26
|
+
coreTableDir: join(__dirname, 'tables'),
|
|
27
|
+
projectDir: projectRoot,
|
|
28
|
+
projectScriptDir: join(projectRoot, 'scripts'),
|
|
29
|
+
projectConfigDir: join(projectRoot, 'config'),
|
|
30
|
+
projectCheckDir: join(projectRoot, 'checks'),
|
|
31
|
+
projectPluginDir: join(projectRoot, 'plugins'),
|
|
32
|
+
projectApiDir: join(projectRoot, 'apis'),
|
|
33
|
+
projectTableDir: join(projectRoot, 'tables')
|
|
34
|
+
} as const;
|