befly 3.8.1 → 3.8.3

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.
@@ -0,0 +1,244 @@
1
+ /**
2
+ * 插件加载器
3
+ * 负责扫描和初始化所有插件(核心、组件、用户)
4
+ */
5
+
6
+ import { basename } from 'pathe';
7
+ import { existsSync } from 'node:fs';
8
+ import { camelCase } from 'es-toolkit/string';
9
+ import { Logger } from '../lib/logger.js';
10
+ import { calcPerfTime } from '../util.js';
11
+ import { corePluginDir, projectPluginDir } from '../paths.js';
12
+ import { scanAddons, getAddonDir, addonDirExists } from '../util.js';
13
+ import type { Plugin } from '../types/plugin.js';
14
+ import type { BeflyContext } from '../types/befly.js';
15
+
16
+ /**
17
+ * 排序插件(根据依赖关系)
18
+ */
19
+ function sortPlugins(plugins: Plugin[]): Plugin[] | false {
20
+ const result: Plugin[] = [];
21
+ const visited = new Set<string>();
22
+ const visiting = new Set<string>();
23
+ const pluginMap: Record<string, Plugin> = Object.fromEntries(plugins.map((p) => [p.pluginName || p.name, p]));
24
+ let isPass = true;
25
+
26
+ const visit = (name: string): void => {
27
+ if (visited.has(name)) return;
28
+ if (visiting.has(name)) {
29
+ isPass = false;
30
+ return;
31
+ }
32
+
33
+ const plugin = pluginMap[name];
34
+ if (!plugin) return;
35
+
36
+ visiting.add(name);
37
+ (plugin.after || []).forEach(visit);
38
+ visiting.delete(name);
39
+ visited.add(name);
40
+ result.push(plugin);
41
+ };
42
+
43
+ plugins.forEach((p) => visit(p.pluginName || p.name));
44
+ return isPass ? result : false;
45
+ }
46
+
47
+ /**
48
+ * 扫描核心插件
49
+ */
50
+ async function scanCorePlugins(loadedPluginNames: Set<string>): Promise<Plugin[]> {
51
+ const plugins: Plugin[] = [];
52
+ const glob = new Bun.Glob('*.ts');
53
+
54
+ for await (const file of glob.scan({
55
+ cwd: corePluginDir,
56
+ onlyFiles: true,
57
+ absolute: true
58
+ })) {
59
+ const fileName = basename(file).replace(/\.ts$/, '');
60
+ if (fileName.startsWith('_')) continue;
61
+
62
+ try {
63
+ const pluginImport = await import(file);
64
+ const plugin = pluginImport.default;
65
+ plugin.pluginName = fileName;
66
+ plugins.push(plugin);
67
+ loadedPluginNames.add(fileName);
68
+ } catch (err: any) {
69
+ Logger.error(`核心插件 ${fileName} 导入失败`, err);
70
+ process.exit(1);
71
+ }
72
+ }
73
+
74
+ return plugins;
75
+ }
76
+
77
+ /**
78
+ * 扫描组件插件
79
+ */
80
+ async function scanAddonPlugins(loadedPluginNames: Set<string>): Promise<Plugin[]> {
81
+ const plugins: Plugin[] = [];
82
+ const glob = new Bun.Glob('*.ts');
83
+ const addons = scanAddons();
84
+
85
+ for (const addon of addons) {
86
+ if (!addonDirExists(addon, 'plugins')) continue;
87
+
88
+ const addonPluginsDir = getAddonDir(addon, 'plugins');
89
+ for await (const file of glob.scan({
90
+ cwd: addonPluginsDir,
91
+ onlyFiles: true,
92
+ absolute: true
93
+ })) {
94
+ const fileName = basename(file).replace(/\.ts$/, '');
95
+ if (fileName.startsWith('_')) continue;
96
+
97
+ const addonCamelCase = camelCase(addon);
98
+ const fileNameCamelCase = camelCase(fileName);
99
+ const pluginFullName = `addon${addonCamelCase.charAt(0).toUpperCase() + addonCamelCase.slice(1)}_${fileNameCamelCase}`;
100
+
101
+ if (loadedPluginNames.has(pluginFullName)) {
102
+ continue;
103
+ }
104
+
105
+ try {
106
+ const pluginImport = await import(file);
107
+ const plugin = pluginImport.default;
108
+ plugin.pluginName = pluginFullName;
109
+ plugins.push(pluginInstance);
110
+ loadedPluginNames.add(pluginFullName);
111
+ } catch (err: any) {
112
+ Logger.error(`组件${addon} ${fileName} 导入失败`, err);
113
+ process.exit(1);
114
+ }
115
+ }
116
+ }
117
+
118
+ return plugins;
119
+ }
120
+
121
+ /**
122
+ * 扫描用户插件
123
+ */
124
+ async function scanUserPlugins(loadedPluginNames: Set<string>): Promise<Plugin[]> {
125
+ const plugins: Plugin[] = [];
126
+
127
+ if (!existsSync(projectPluginDir)) {
128
+ return plugins;
129
+ }
130
+
131
+ const glob = new Bun.Glob('*.ts');
132
+ for await (const file of glob.scan({
133
+ cwd: projectPluginDir,
134
+ onlyFiles: true,
135
+ absolute: true
136
+ })) {
137
+ const fileName = basename(file).replace(/\.ts$/, '');
138
+ if (fileName.startsWith('_')) continue;
139
+
140
+ const fileNameCamelCase = camelCase(fileName);
141
+ const pluginFullName = `app${fileNameCamelCase.charAt(0).toUpperCase() + fileNameCamelCase.slice(1)}`;
142
+
143
+ if (loadedPluginNames.has(pluginFullName)) {
144
+ continue;
145
+ }
146
+
147
+ try {
148
+ const pluginImport = await import(file);
149
+ const plugin = pluginImport.default;
150
+ plugin.pluginName = pluginFullName;
151
+ plugins.push(pluginInstance);
152
+ loadedPluginNames.add(pluginFullName);
153
+ } catch (err: any) {
154
+ Logger.error(`用户插件 ${fileName} 导入失败`, err);
155
+ process.exit(1);
156
+ }
157
+ }
158
+
159
+ return plugins;
160
+ }
161
+
162
+ /**
163
+ * 初始化单个插件
164
+ */
165
+ async function initPlugin(befly: { pluginLists: Plugin[]; appContext: BeflyContext }, plugin: Plugin): Promise<void> {
166
+ befly.pluginLists.push(plugin);
167
+
168
+ if (typeof plugin?.onInit === 'function') {
169
+ befly.appContext[plugin.pluginName] = await plugin?.onInit(befly.appContext);
170
+ } else {
171
+ befly.appContext[plugin.pluginName] = {};
172
+ }
173
+ }
174
+
175
+ /**
176
+ * 加载所有插件
177
+ * @param befly - Befly实例(需要访问 pluginLists 和 appContext)
178
+ */
179
+ export async function loadPlugins(befly: { pluginLists: Plugin[]; appContext: BeflyContext }): Promise<void> {
180
+ try {
181
+ const loadStartTime = Bun.nanoseconds();
182
+ const loadedPluginNames = new Set<string>();
183
+
184
+ // 阶段1:扫描所有插件
185
+ const corePlugins = await scanCorePlugins(loadedPluginNames);
186
+ const addonPlugins = await scanAddonPlugins(loadedPluginNames);
187
+ const userPlugins = await scanUserPlugins(loadedPluginNames);
188
+
189
+ // 阶段2:分层排序插件
190
+ const sortedCorePlugins = sortPlugins(corePlugins);
191
+ if (sortedCorePlugins === false) {
192
+ Logger.error('核心插件依赖关系错误,请检查插件的 after 属性');
193
+ process.exit(1);
194
+ }
195
+
196
+ const sortedAddonPlugins = sortPlugins(addonPlugins);
197
+ if (sortedAddonPlugins === false) {
198
+ Logger.error('组件插件依赖关系错误,请检查插件的 after 属性');
199
+ process.exit(1);
200
+ }
201
+
202
+ const sortedUserPlugins = sortPlugins(userPlugins);
203
+ if (sortedUserPlugins === false) {
204
+ Logger.error('用户插件依赖关系错误,请检查插件的 after 属性');
205
+ process.exit(1);
206
+ }
207
+
208
+ // 阶段3:分层初始化插件(核心 → 组件 → 用户)
209
+ // 3.1 初始化核心插件
210
+ for (const plugin of sortedCorePlugins) {
211
+ try {
212
+ await initPlugin(befly, plugin);
213
+ } catch (error: any) {
214
+ Logger.error(`核心插件 ${plugin.pluginName} 初始化失败`, error);
215
+ process.exit(1);
216
+ }
217
+ }
218
+
219
+ // 3.2 初始化组件插件
220
+ for (const plugin of sortedAddonPlugins) {
221
+ try {
222
+ await initPlugin(befly, plugin);
223
+ } catch (error: any) {
224
+ Logger.error(`组件插件 ${plugin.pluginName} 初始化失败`, error);
225
+ process.exit(1);
226
+ }
227
+ }
228
+
229
+ // 3.3 初始化用户插件
230
+ for (const plugin of sortedUserPlugins) {
231
+ try {
232
+ await initPlugin(befly, plugin);
233
+ } catch (error: any) {
234
+ Logger.error(`用户插件 ${plugin.pluginName} 初始化失败`, error);
235
+ process.exit(1);
236
+ }
237
+ }
238
+
239
+ const totalLoadTime = calcPerfTime(loadStartTime);
240
+ } catch (error: any) {
241
+ Logger.error('加载插件时发生错误', error);
242
+ process.exit(1);
243
+ }
244
+ }
package/main.ts CHANGED
@@ -1,46 +1,136 @@
1
1
  /**
2
2
  * Befly 框架主入口文件
3
- * 提供简洁的框架接口,核心逻辑已提取到 lifecycle
3
+ * 提供简洁的框架接口,核心逻辑已提取到 loader
4
4
  */
5
5
 
6
6
  import { Env } from './env.js';
7
- import { Yes, No } from './util.js';
7
+
8
8
  import { Logger } from './lib/logger.js';
9
9
  import { Cipher } from './lib/cipher.js';
10
10
  import { Jwt } from './lib/jwt.js';
11
11
  import { Database } from './lib/database.js';
12
- import { Lifecycle } from './lifecycle/lifecycle.js';
12
+ import { loadPlugins } from './loader/loadPlugins.js';
13
+ import { loadApis } from './loader/loadApis.js';
14
+ import { rootHandler } from './router/root.js';
15
+ import { apiHandler } from './router/api.js';
16
+ import { staticHandler } from './router/static.js';
13
17
  import { coreDir } from './paths.js';
14
18
  import { DbHelper } from './lib/dbHelper.js';
15
19
  import { RedisHelper } from './lib/redisHelper.js';
16
- import { Addon } from './lib/addon.js';
17
- import { checkDefault } from './check.js';
18
- import * as utilFunctions from './util.js';
20
+ import { checkTable, checkApi, checkApp } from './check.js';
21
+ import {
22
+ //
23
+ Yes,
24
+ No,
25
+ keysToSnake,
26
+ keysToCamel,
27
+ arrayKeysToCamel,
28
+ pickFields,
29
+ fieldClear,
30
+ calcPerfTime,
31
+ scanAddons,
32
+ getAddonDir,
33
+ addonDirExists
34
+ } from './util.js';
19
35
 
20
36
  import type { Server } from 'bun';
21
- import type { BeflyContext, BeflyOptions } from './types/befly.js';
37
+ import type { BeflyContext } from './types/befly.js';
38
+ import type { Plugin } from './types/plugin.js';
39
+ import type { ApiRoute } from './types/api.js';
22
40
  /**
23
41
  * Befly 框架核心类
24
42
  * 职责:管理应用上下文和生命周期
25
43
  */
26
44
  export class Befly {
27
- /** 生命周期管理器 */
28
- private lifecycle: Lifecycle;
45
+ /** API 路由映射表 */
46
+ private apiRoutes: Map<string, ApiRoute> = new Map();
47
+
48
+ /** 插件列表 */
49
+ private pluginLists: Plugin[] = [];
29
50
 
30
51
  /** 应用上下文 */
31
52
  public appContext: BeflyContext;
32
53
 
33
- constructor(options: BeflyOptions = {}) {
34
- this.lifecycle = new Lifecycle(options);
54
+ constructor() {
35
55
  this.appContext = {};
36
56
  }
37
57
 
58
+ /**
59
+ * 启动完整的生命周期流程
60
+ * @returns HTTP 服务器实例
61
+ */
62
+ private async start(): Promise<Server> {
63
+ const serverStartTime = Bun.nanoseconds();
64
+
65
+ // 1. 加载所有 API(动态导入必须在最前面,避免 Bun 1.3.2 的崩溃 bug)
66
+ await loadApis(this.apiRoutes);
67
+
68
+ // 2. 执行项目结构检查
69
+ const appCheckResult = await checkApp();
70
+ if (!appCheckResult) {
71
+ Logger.error('项目结构检查失败,程序退出');
72
+ process.exit(1);
73
+ }
74
+
75
+ // 3. 执行表定义检查
76
+ const tableCheckResult = await checkTable();
77
+ if (!tableCheckResult) {
78
+ Logger.error('表定义检查失败,程序退出');
79
+ process.exit(1);
80
+ }
81
+
82
+ // 4. 执行 API 定义检查
83
+ const apiCheckResult = await checkApi();
84
+ if (!apiCheckResult) {
85
+ Logger.error('API 定义检查失败,程序退出');
86
+ process.exit(1);
87
+ }
88
+
89
+ // 5. 加载插件
90
+ await loadPlugins({
91
+ pluginLists: this.pluginLists,
92
+ appContext: this.appContext
93
+ });
94
+
95
+ // 4. 启动 HTTP 服务器
96
+ const totalStartupTime = calcPerfTime(serverStartTime);
97
+
98
+ return await this.startServer();
99
+ }
100
+
101
+ /**
102
+ * 启动 HTTP 服务器
103
+ * @returns HTTP 服务器实例
104
+ */
105
+ private async startServer(): Promise<Server> {
106
+ const startTime = Bun.nanoseconds();
107
+
108
+ const server = Bun.serve({
109
+ port: Env.APP_PORT,
110
+ hostname: Env.APP_HOST,
111
+ routes: {
112
+ '/': rootHandler,
113
+ '/api/*': apiHandler(this.apiRoutes, this.pluginLists, this.appContext),
114
+ '/*': staticHandler
115
+ },
116
+ error: (error: Error) => {
117
+ Logger.error('服务启动时发生错误', error);
118
+ return Response.json({ code: 1, msg: '内部服务器错误' });
119
+ }
120
+ });
121
+
122
+ const finalStartupTime = calcPerfTime(startTime);
123
+ Logger.info(`${Env.APP_NAME} 服务器启动成功! 服务器启动耗时: ${finalStartupTime}`);
124
+ Logger.info(`服务器监听地址: http://${Env.APP_HOST}:${Env.APP_PORT}`);
125
+
126
+ return server;
127
+ }
128
+
38
129
  /**
39
130
  * 启动服务器并注册优雅关闭处理
40
- * @param callback - 启动完成后的回调函数
41
131
  */
42
- async listen(callback?: (server: Server) => void): Promise<Server> {
43
- const server = await this.lifecycle.start(this.appContext, callback);
132
+ async listen(): Promise<Server> {
133
+ const server = await this.start();
44
134
 
45
135
  const gracefulShutdown = async (signal: string) => {
46
136
  // 1. 停止接收新请求
@@ -78,18 +168,21 @@ export {
78
168
  Database,
79
169
  DbHelper,
80
170
  RedisHelper,
81
- Addon,
82
171
  coreDir,
83
- checkDefault
172
+ checkTable,
173
+ checkApi,
174
+ checkApp
84
175
  };
85
176
 
86
177
  // 工具函数命名空间导出
87
178
  export const utils = {
88
- keysToSnake: utilFunctions.keysToSnake,
89
- keysToCamel: utilFunctions.keysToCamel,
90
- arrayKeysToCamel: utilFunctions.arrayKeysToCamel,
91
- pickFields: utilFunctions.pickFields,
92
- fieldClear: utilFunctions.fieldClear,
93
- calcPerfTime: utilFunctions.calcPerfTime,
94
- parseRule: utilFunctions.parseRule
179
+ keysToSnake: keysToSnake,
180
+ keysToCamel: keysToCamel,
181
+ arrayKeysToCamel: arrayKeysToCamel,
182
+ pickFields: pickFields,
183
+ fieldClear: fieldClear,
184
+ calcPerfTime: calcPerfTime,
185
+ scanAddons: scanAddons,
186
+ getAddonDir: getAddonDir,
187
+ addonDirExists: addonDirExists
95
188
  };
package/menu.json CHANGED
@@ -1,22 +1,22 @@
1
1
  [
2
2
  {
3
3
  "name": "首页",
4
- "path": "/",
5
- "icon": "Home",
4
+ "path": "/addon/admin",
5
+ "icon": "",
6
6
  "sort": 1,
7
7
  "type": 1
8
8
  },
9
9
  {
10
10
  "name": "人员管理",
11
- "path": "/internal/people",
12
- "icon": "UserCircle",
11
+ "path": "_people",
12
+ "icon": "",
13
13
  "sort": 2,
14
14
  "type": 1,
15
15
  "children": [
16
16
  {
17
17
  "name": "管理员管理",
18
- "path": "/admin",
19
- "icon": "Users",
18
+ "path": "/addon/admin/admin",
19
+ "icon": "",
20
20
  "sort": 2,
21
21
  "type": 1
22
22
  }
@@ -24,22 +24,22 @@
24
24
  },
25
25
  {
26
26
  "name": "权限设置",
27
- "path": "/internal/settings",
28
- "icon": "Settings",
27
+ "path": "_permission",
28
+ "icon": "",
29
29
  "sort": 3,
30
30
  "type": 1,
31
31
  "children": [
32
32
  {
33
33
  "name": "菜单管理",
34
- "path": "/menu",
35
- "icon": "Menu",
34
+ "path": "/addon/admin/menu",
35
+ "icon": "",
36
36
  "sort": 4,
37
37
  "type": 1
38
38
  },
39
39
  {
40
40
  "name": "角色管理",
41
- "path": "/role",
42
- "icon": "Users",
41
+ "path": "/addon/admin/role",
42
+ "icon": "",
43
43
  "sort": 5,
44
44
  "type": 1
45
45
  }
@@ -47,8 +47,8 @@
47
47
  },
48
48
  {
49
49
  "name": "字典管理",
50
- "path": "/internal/dict",
51
- "icon": "BookOpen",
50
+ "path": "/addon/admin/dict",
51
+ "icon": "",
52
52
  "sort": 6,
53
53
  "type": 1
54
54
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.8.1",
3
+ "version": "3.8.3",
4
4
  "description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
5
5
  "type": "module",
6
6
  "private": false,
@@ -39,8 +39,8 @@
39
39
  "license": "Apache-2.0",
40
40
  "files": [
41
41
  "lib/",
42
- "lifecycle/",
43
- "plugins/",
42
+ "loader/",
43
+ "middleware/",
44
44
  "router/",
45
45
  "types/",
46
46
  ".gitignore",
@@ -69,5 +69,5 @@
69
69
  "es-toolkit": "^1.41.0",
70
70
  "pathe": "^2.0.3"
71
71
  },
72
- "gitHead": "da3ca0f3da53574d9b1053ad5cb2247acc11d384"
72
+ "gitHead": "51d86162e5f0b94ecc44a833a98ebf97c27f7bd6"
73
73
  }
package/paths.ts CHANGED
@@ -89,3 +89,10 @@ export const projectApiDir = join(projectDir, 'apis');
89
89
  * @usage 存放用户业务表定义(JSON 格式)
90
90
  */
91
91
  export const projectTableDir = join(projectDir, 'tables');
92
+
93
+ /**
94
+ * 项目组件目录
95
+ * @description {projectDir}/addons/
96
+ * @usage 存放本地组件(优先级高于 node_modules 中的组件)
97
+ */
98
+ export const projectAddonsDir = join(projectDir, 'addons');
package/types/common.d.ts CHANGED
@@ -21,9 +21,33 @@ export interface ValidationResult {
21
21
  }
22
22
 
23
23
  /**
24
- * 字段规则字符串
24
+ * 字段定义类型(对象格式)
25
+ */
26
+ export interface FieldDefinition {
27
+ name: string; // 字段标签/描述
28
+ detail: string; // 字段详细说明
29
+ type: 'string' | 'number' | 'text' | 'array_string' | 'array_text';
30
+ min: number | null; // 最小值/最小长度
31
+ max: number | null; // 最大值/最大长度
32
+ default: any; // 默认值
33
+ index: boolean; // 是否创建索引
34
+ unique: boolean; // 是否唯一
35
+ comment: string; // 字段注释
36
+ nullable: boolean; // 是否允许为空
37
+ unsigned: boolean; // 是否无符号(仅number类型)
38
+ regexp: string | null; // 正则验证
39
+ }
40
+
41
+ /**
42
+ * 表定义类型(对象格式)
43
+ */
44
+ export type TableDefinition = Record<string, FieldDefinition>;
45
+
46
+ /**
47
+ * 字段规则字符串(已废弃,保留用于兼容)
25
48
  * 格式: "字段名|类型|最小值|最大值|默认值|是否索引|正则约束"
26
49
  *
50
+ * @deprecated 请使用 FieldDefinition 对象格式
27
51
  * @example
28
52
  * "用户名|string|2|50|null|1|^[a-zA-Z0-9_]+$"
29
53
  * "年龄|number|0|150|18|0|null"
@@ -31,12 +55,9 @@ export interface ValidationResult {
31
55
  export type FieldRule = string;
32
56
 
33
57
  /**
34
- * 表定义类型
35
- */
36
- export type TableDefinition = Record<string, FieldRule>;
37
-
38
- /**
39
- * 解析后的字段规则
58
+ * 解析后的字段规则(已废弃,保留用于兼容)
59
+ *
60
+ * @deprecated 请使用 FieldDefinition 对象格式
40
61
  */
41
62
  export interface ParsedFieldRule {
42
63
  name: string; // 字段名称