befly 3.8.0 → 3.8.2

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.
@@ -1,427 +0,0 @@
1
- /**
2
- * 插件和API加载器
3
- * 负责加载和初始化插件以及API路由
4
- */
5
-
6
- import { relative, basename } from 'pathe';
7
- import { existsSync } from 'node:fs';
8
- import { isPlainObject } from 'es-toolkit/compat';
9
- import { Logger } from '../lib/logger.js';
10
- import { calcPerfTime } from '../util.js';
11
- import { corePluginDir, projectPluginDir, coreApiDir, projectApiDir } from '../paths.js';
12
- import { Addon } from '../lib/addon.js';
13
- import type { Plugin } from '../types/plugin.js';
14
- import type { ApiRoute } from '../types/api.js';
15
- import type { BeflyContext } from '../types/befly.js';
16
-
17
- /**
18
- * API 默认字段定义
19
- * 这些字段会自动合并到所有 API 的 fields 中
20
- * API 自定义的同名字段可以覆盖这些默认值
21
- */
22
- const DEFAULT_API_FIELDS = {
23
- id: 'ID|number|1|null|null|0|null',
24
- page: '页码|number|1|9999|1|0|null',
25
- limit: '每页数量|number|1|100|10|0|null',
26
- keyword: '关键词|string|1|50|null|0|null',
27
- state: '状态|number|0|2|1|1|null'
28
- } as const;
29
-
30
- /**
31
- * 排序插件(根据依赖关系)
32
- */
33
- export const sortPlugins = (plugins: Plugin[]): Plugin[] | false => {
34
- const result: Plugin[] = [];
35
- const visited = new Set<string>();
36
- const visiting = new Set<string>();
37
- const pluginMap: Record<string, Plugin> = Object.fromEntries(plugins.map((p) => [p.name, p]));
38
- let isPass = true;
39
-
40
- const visit = (name: string): void => {
41
- if (visited.has(name)) return;
42
- if (visiting.has(name)) {
43
- isPass = false;
44
- return;
45
- }
46
-
47
- const plugin = pluginMap[name];
48
- if (!plugin) return;
49
-
50
- visiting.add(name);
51
- (plugin.dependencies || []).forEach(visit);
52
- visiting.delete(name);
53
- visited.add(name);
54
- result.push(plugin);
55
- };
56
-
57
- plugins.forEach((p) => visit(p.name));
58
- return isPass ? result : false;
59
- };
60
-
61
- /**
62
- * 带超时的动态导入函数
63
- * @param filePath - 文件路径
64
- * @param timeout - 超时时间(毫秒),默认 3000ms
65
- * @returns 导入的模块
66
- */
67
- async function importWithTimeout(filePath: string, timeout: number = 3000): Promise<any> {
68
- return Promise.race([
69
- import(filePath),
70
- new Promise((_, reject) =>
71
- setTimeout(() => {
72
- reject(new Error(`模块导入超时 (${timeout}ms),可能存在死循环或模块依赖问题`));
73
- }, timeout)
74
- )
75
- ]);
76
- }
77
-
78
- /**
79
- * 加载器类
80
- */
81
- export class Loader {
82
- /**
83
- * 加载所有插件
84
- * @param befly - Befly实例(需要访问 pluginLists 和 appContext)
85
- */
86
- static async loadPlugins(befly: { pluginLists: Plugin[]; appContext: BeflyContext }): Promise<void> {
87
- try {
88
- const loadStartTime = Bun.nanoseconds();
89
-
90
- const glob = new Bun.Glob('*.ts');
91
- const corePlugins: Plugin[] = [];
92
- const addonPlugins: Plugin[] = [];
93
- const userPlugins: Plugin[] = [];
94
- const loadedPluginNames = new Set<string>(); // 用于跟踪已加载的插件名称
95
- let hadCorePluginError = false; // 核心插件错误(关键)
96
- let hadAddonPluginError = false; // Addon 插件错误(警告)
97
- let hadUserPluginError = false; // 用户插件错误(警告)
98
-
99
- // 扫描核心插件目录
100
- const corePluginsScanStart = Bun.nanoseconds();
101
- for await (const file of glob.scan({
102
- cwd: corePluginDir,
103
- onlyFiles: true,
104
- absolute: true
105
- })) {
106
- const fileName = basename(file).replace(/\.ts$/, '');
107
- if (fileName.startsWith('_')) continue;
108
-
109
- try {
110
- const plugin = await importWithTimeout(file);
111
- const pluginInstance = plugin.default;
112
- pluginInstance.pluginName = fileName;
113
- corePlugins.push(pluginInstance);
114
- loadedPluginNames.add(fileName); // 记录已加载的核心插件名称
115
- } catch (err: any) {
116
- hadCorePluginError = true;
117
- Logger.error(`核心插件 ${fileName} 导入失败`, error);
118
- process.exit(1);
119
- }
120
- }
121
- const corePluginsScanTime = calcPerfTime(corePluginsScanStart);
122
-
123
- const sortedCorePlugins = sortPlugins(corePlugins);
124
- if (sortedCorePlugins === false) {
125
- Logger.warn('核心插件依赖关系错误,请检查插件的 after 属性');
126
- process.exit(1);
127
- }
128
-
129
- // 初始化核心插件
130
- const corePluginsInitStart = Bun.nanoseconds();
131
- for (const plugin of sortedCorePlugins) {
132
- try {
133
- befly.pluginLists.push(plugin);
134
- if (typeof plugin?.onInit === 'function') {
135
- const pluginInitStart = Bun.nanoseconds();
136
- befly.appContext[plugin.pluginName] = await plugin?.onInit(befly.appContext);
137
- const pluginInitTime = calcPerfTime(pluginInitStart);
138
- } else {
139
- befly.appContext[plugin.pluginName] = {};
140
- }
141
- } catch (error: any) {
142
- hadCorePluginError = true;
143
- Logger.error(`核心插件 ${plugin.pluginName} 初始化失败`, error);
144
- process.exit(1);
145
- }
146
- }
147
- const corePluginsInitTime = calcPerfTime(corePluginsInitStart);
148
-
149
- // 扫描 addon 插件目录
150
- const addons = Addon.scan();
151
- if (addons.length > 0) {
152
- const addonPluginsScanStart = Bun.nanoseconds();
153
- for (const addon of addons) {
154
- if (!Addon.dirExists(addon, 'plugins')) continue;
155
-
156
- const addonPluginsDir = Addon.getDir(addon, 'plugins');
157
- for await (const file of glob.scan({
158
- cwd: addonPluginsDir,
159
- onlyFiles: true,
160
- absolute: true
161
- })) {
162
- const fileName = basename(file).replace(/\.ts$/, '');
163
- if (fileName.startsWith('_')) continue;
164
-
165
- const pluginFullName = `${addon}.${fileName}`;
166
-
167
- // 检查是否已经加载了同名插件
168
- if (loadedPluginNames.has(pluginFullName)) {
169
- continue;
170
- }
171
-
172
- try {
173
- const importStart = Bun.nanoseconds();
174
- const plugin = await importWithTimeout(file);
175
- const importTime = calcPerfTime(importStart);
176
- const pluginInstance = plugin.default;
177
- pluginInstance.pluginName = pluginFullName;
178
- addonPlugins.push(pluginInstance);
179
- loadedPluginNames.add(pluginFullName);
180
- } catch (err: any) {
181
- hadAddonPluginError = true;
182
- Logger.error(`组件${addon} ${fileName} 导入失败`, error);
183
- process.exit(1);
184
- }
185
- }
186
- }
187
- const addonPluginsScanTime = calcPerfTime(addonPluginsScanStart);
188
-
189
- const sortedAddonPlugins = sortPlugins(addonPlugins);
190
- if (sortedAddonPlugins === false) {
191
- Logger.warn({
192
- level: 'WARNING',
193
- msg: '组件插件依赖关系错误,请检查插件的 after 属性'
194
- });
195
- } else {
196
- // 初始化组件插件
197
- const addonPluginsInitStart = Bun.nanoseconds();
198
- for (const plugin of sortedAddonPlugins) {
199
- try {
200
- befly.pluginLists.push(plugin);
201
-
202
- if (typeof plugin?.onInit === 'function') {
203
- const pluginInitStart = Bun.nanoseconds();
204
- befly.appContext[plugin.pluginName] = await plugin?.onInit(befly.appContext);
205
- const pluginInitTime = calcPerfTime(pluginInitStart);
206
- } else {
207
- befly.appContext[plugin.pluginName] = {};
208
- }
209
- } catch (error: any) {
210
- hadAddonPluginError = true;
211
- Logger.error(`组件插件 ${plugin.pluginName} 初始化失败`, error);
212
- }
213
- }
214
- const addonPluginsInitTime = calcPerfTime(addonPluginsInitStart);
215
- }
216
- }
217
-
218
- // 扫描用户插件目录
219
- if (!existsSync(projectPluginDir)) {
220
- // 项目插件目录不存在,跳过
221
- } else {
222
- const userPluginsScanStart = Bun.nanoseconds();
223
- for await (const file of glob.scan({
224
- cwd: projectPluginDir,
225
- onlyFiles: true,
226
- absolute: true
227
- })) {
228
- const fileName = basename(file).replace(/\.ts$/, '');
229
- if (fileName.startsWith('_')) continue;
230
-
231
- // 检查是否已经加载了同名的核心插件
232
- if (loadedPluginNames.has(fileName)) {
233
- continue;
234
- }
235
-
236
- try {
237
- const plugin = await importWithTimeout(file);
238
- const pluginInstance = plugin.default;
239
- pluginInstance.pluginName = fileName;
240
- userPlugins.push(pluginInstance);
241
- } catch (err: any) {
242
- hadUserPluginError = true;
243
- Logger.error(`用户插件 ${fileName} 导入失败`, error);
244
- process.exit(1);
245
- }
246
- }
247
- }
248
-
249
- const sortedUserPlugins = sortPlugins(userPlugins);
250
- if (sortedUserPlugins === false) {
251
- Logger.warn({
252
- level: 'WARNING',
253
- msg: '用户插件依赖关系错误,请检查插件的 after 属性'
254
- });
255
- // 用户插件错误不退出,只是跳过这些插件
256
- return;
257
- }
258
-
259
- // 初始化用户插件
260
- if (userPlugins.length > 0) {
261
- const userPluginsInitStart = Bun.nanoseconds();
262
- for (const plugin of sortedUserPlugins) {
263
- try {
264
- befly.pluginLists.push(plugin);
265
-
266
- if (typeof plugin?.onInit === 'function') {
267
- befly.appContext[plugin.pluginName] = await plugin?.onInit(befly.appContext);
268
- } else {
269
- befly.appContext[plugin.pluginName] = {};
270
- }
271
- } catch (error: any) {
272
- hadUserPluginError = true;
273
- Logger.error(`用户插件 ${plugin.pluginName} 初始化失败`, error);
274
- }
275
- }
276
- const userPluginsInitTime = calcPerfTime(userPluginsInitStart);
277
- }
278
-
279
- const totalLoadTime = calcPerfTime(loadStartTime);
280
- const totalPluginCount = sortedCorePlugins.length + addonPlugins.length + sortedUserPlugins.length;
281
-
282
- // 核心插件失败 → 关键错误,必须退出
283
- if (hadCorePluginError) {
284
- Logger.warn('核心插件加载失败,无法继续启动');
285
- process.exit(1);
286
- }
287
-
288
- // Addon 插件失败 → 警告,可以继续运行
289
- if (hadAddonPluginError) {
290
- Logger.warn('部分 Addon 插件加载失败,但不影响核心功能');
291
- }
292
-
293
- // 用户插件失败 → 警告,可以继续运行
294
- if (hadUserPluginError) {
295
- Logger.warn('部分用户插件加载失败,但不影响核心功能');
296
- }
297
- } catch (error: any) {
298
- Logger.error('加载插件时发生错误', error);
299
- process.exit(1);
300
- }
301
- }
302
-
303
- /**
304
- * 加载API路由
305
- * @param dirName - 目录名称 ('core' | 'app' | addon名称)
306
- * @param apiRoutes - API路由映射表
307
- * @param options - 可选配置
308
- * @param options.where - API来源类型:'core' | 'addon' | 'app'
309
- * @param options.addonName - addon名称(仅当 where='addon' 时需要)
310
- */
311
- static async loadApis(dirName: string, apiRoutes: Map<string, ApiRoute>, options?: { where?: 'core' | 'addon' | 'app'; addonName?: string }): Promise<void> {
312
- try {
313
- const loadStartTime = Bun.nanoseconds();
314
- const where = options?.where || 'app';
315
- const addonName = options?.addonName || '';
316
- const dirDisplayName = where === 'core' ? '核心' : where === 'addon' ? `组件${addonName}` : '用户';
317
-
318
- const glob = new Bun.Glob('**/*.ts');
319
- let apiDir: string;
320
-
321
- if (where === 'core') {
322
- apiDir = coreApiDir;
323
- } else if (where === 'addon') {
324
- apiDir = Addon.getDir(addonName, 'apis');
325
- } else {
326
- apiDir = projectApiDir;
327
- }
328
-
329
- // 检查目录是否存在
330
- if (!existsSync(apiDir)) {
331
- return;
332
- }
333
-
334
- let totalApis = 0;
335
- let loadedApis = 0;
336
- let failedApis = 0;
337
-
338
- // 扫描指定目录
339
- for await (const file of glob.scan({
340
- cwd: apiDir,
341
- onlyFiles: true,
342
- absolute: true
343
- })) {
344
- const fileName = basename(file).replace(/\.ts$/, '');
345
- const apiPath = relative(apiDir, file).replace(/\.ts$/, '');
346
- if (apiPath.indexOf('_') !== -1) continue;
347
-
348
- totalApis++;
349
- const singleApiStart = Bun.nanoseconds();
350
-
351
- try {
352
- const importStart = Bun.nanoseconds();
353
- const api = (await importWithTimeout(file)).default;
354
- const importTime = calcPerfTime(importStart);
355
- // 验证必填属性:name 和 handler
356
- if (typeof api.name !== 'string' || api.name.trim() === '') {
357
- throw new Error(`接口 ${apiPath} 的 name 属性必须是非空字符串`);
358
- }
359
- if (typeof api.handler !== 'function') {
360
- throw new Error(`接口 ${apiPath} 的 handler 属性必须是函数`);
361
- }
362
-
363
- // 设置默认值
364
- api.method = api.method || 'POST';
365
- api.auth = api.auth !== undefined ? api.auth : true;
366
-
367
- // 合并默认字段:先设置默认字段,再用 API 自定义字段覆盖
368
- api.fields = { ...DEFAULT_API_FIELDS, ...(api.fields || {}) };
369
- api.required = api.required || [];
370
-
371
- // 验证可选属性的类型(如果提供了)
372
- if (api.method && !['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'].includes(api.method.toUpperCase())) {
373
- throw new Error(`接口 ${apiPath} 的 method 属性必须是有效的 HTTP 方法`);
374
- }
375
- if (api.auth !== undefined && typeof api.auth !== 'boolean') {
376
- throw new Error(`接口 ${apiPath} 的 auth 属性必须是布尔值 (true=需登录, false=公开)`);
377
- }
378
- if (api.fields && !isPlainObject(api.fields)) {
379
- throw new Error(`接口 ${apiPath} 的 fields 属性必须是对象`);
380
- }
381
- if (api.required && !Array.isArray(api.required)) {
382
- throw new Error(`接口 ${apiPath} 的 required 属性必须是数组`);
383
- }
384
- if (api.required && api.required.some((item: any) => typeof item !== 'string')) {
385
- throw new Error(`接口 ${apiPath} 的 required 属性必须是字符串数组`);
386
- }
387
- // 构建路由:
388
- // - core 接口: /api/core/{apiPath}
389
- // - addon 接口: /api/addon/{addonName}/{apiPath}
390
- // - 项目接口: /api/{apiPath}
391
- if (where === 'core') {
392
- api.route = `${api.method.toUpperCase()}/api/core/${apiPath}`;
393
- } else if (where === 'addon') {
394
- api.route = `${api.method.toUpperCase()}/api/addon/${addonName}/${apiPath}`;
395
- } else {
396
- api.route = `${api.method.toUpperCase()}/api/${apiPath}`;
397
- }
398
- apiRoutes.set(api.route, api);
399
-
400
- loadedApis++;
401
- } catch (error: any) {
402
- failedApis++;
403
-
404
- // 记录详细错误信息
405
- Logger.error(`[${dirDisplayName}] 接口 ${apiPath} 加载失败`, error);
406
-
407
- process.exit(1);
408
- }
409
- }
410
-
411
- const totalLoadTime = calcPerfTime(loadStartTime);
412
-
413
- // 检查是否有加载失败的 API(理论上不会到达这里,因为上面已经 critical 退出)
414
- if (failedApis > 0) {
415
- Logger.warn(`有 ${failedApis} 个${dirDisplayName}接口加载失败,无法继续启动服务`, {
416
- dirName,
417
- totalApis,
418
- failedApis
419
- });
420
- process.exit(1);
421
- }
422
- } catch (error: any) {
423
- Logger.error(`加载接口时发生错误`, error);
424
- process.exit(1);
425
- }
426
- }
427
- }