befly 3.9.39 → 3.10.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.
Files changed (141) hide show
  1. package/README.md +39 -8
  2. package/befly.config.ts +19 -2
  3. package/checks/checkApi.ts +79 -77
  4. package/checks/checkHook.ts +48 -0
  5. package/checks/checkMenu.ts +168 -0
  6. package/checks/checkPlugin.ts +48 -0
  7. package/checks/checkTable.ts +137 -183
  8. package/docs/README.md +1 -1
  9. package/docs/api/api.md +1 -1
  10. package/docs/guide/quickstart.md +16 -9
  11. package/docs/hooks/hook.md +2 -2
  12. package/docs/hooks/rateLimit.md +1 -1
  13. package/docs/infra/redis.md +7 -7
  14. package/docs/plugins/plugin.md +23 -21
  15. package/docs/quickstart.md +16 -9
  16. package/docs/reference/addon.md +12 -1
  17. package/docs/reference/config.md +13 -30
  18. package/docs/reference/sync.md +62 -193
  19. package/docs/reference/table.md +27 -29
  20. package/hooks/auth.ts +3 -4
  21. package/hooks/cors.ts +4 -6
  22. package/hooks/parser.ts +3 -4
  23. package/hooks/permission.ts +3 -4
  24. package/hooks/validator.ts +4 -5
  25. package/lib/cacheHelper.ts +89 -153
  26. package/lib/cacheKeys.ts +1 -1
  27. package/lib/connect.ts +9 -13
  28. package/lib/dbDialect.ts +285 -0
  29. package/lib/dbHelper.ts +179 -507
  30. package/lib/dbUtils.ts +450 -0
  31. package/lib/logger.ts +41 -5
  32. package/lib/redisHelper.ts +1 -0
  33. package/lib/sqlBuilder.ts +358 -58
  34. package/lib/sqlCheck.ts +136 -0
  35. package/lib/validator.ts +1 -1
  36. package/loader/loadApis.ts +23 -126
  37. package/loader/loadHooks.ts +31 -46
  38. package/loader/loadPlugins.ts +37 -52
  39. package/main.ts +58 -19
  40. package/package.json +24 -25
  41. package/paths.ts +14 -14
  42. package/plugins/cache.ts +12 -6
  43. package/plugins/cipher.ts +2 -2
  44. package/plugins/config.ts +6 -8
  45. package/plugins/db.ts +14 -19
  46. package/plugins/jwt.ts +6 -7
  47. package/plugins/logger.ts +7 -9
  48. package/plugins/redis.ts +8 -10
  49. package/plugins/tool.ts +3 -4
  50. package/router/api.ts +3 -2
  51. package/router/static.ts +7 -5
  52. package/sync/syncApi.ts +80 -235
  53. package/sync/syncCache.ts +16 -0
  54. package/sync/syncDev.ts +167 -202
  55. package/sync/syncMenu.ts +230 -444
  56. package/sync/syncTable.ts +1247 -0
  57. package/tests/_mocks/mockSqliteDb.ts +204 -0
  58. package/tests/addonHelper-cache.test.ts +32 -0
  59. package/tests/apiHandler-routePath-only.test.ts +32 -0
  60. package/tests/cacheHelper.test.ts +16 -51
  61. package/tests/checkApi-routePath-strict.test.ts +166 -0
  62. package/tests/checkMenu.test.ts +346 -0
  63. package/tests/checkTable-smoke.test.ts +157 -0
  64. package/tests/dbDialect-cache.test.ts +23 -0
  65. package/tests/dbDialect.test.ts +46 -0
  66. package/tests/dbHelper-advanced.test.ts +1 -1
  67. package/tests/dbHelper-all-array-types.test.ts +15 -15
  68. package/tests/dbHelper-batch-write.test.ts +90 -0
  69. package/tests/dbHelper-columns.test.ts +36 -54
  70. package/tests/dbHelper-execute.test.ts +26 -26
  71. package/tests/dbHelper-joins.test.ts +85 -176
  72. package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +3 -0
  73. package/tests/fixtures/scanFilesApis/a.ts +3 -0
  74. package/tests/fixtures/scanFilesApis/sub/b.ts +3 -0
  75. package/tests/loadPlugins-order-smoke.test.ts +75 -0
  76. package/tests/logger.test.ts +6 -6
  77. package/tests/redisHelper.test.ts +6 -1
  78. package/tests/scanFiles-routePath.test.ts +46 -0
  79. package/tests/smoke-sql.test.ts +24 -0
  80. package/tests/sqlBuilder-advanced.test.ts +18 -5
  81. package/tests/sqlBuilder.test.ts +24 -0
  82. package/tests/sync-init-guard.test.ts +105 -0
  83. package/tests/syncApi-insBatch-fields-consistent.test.ts +61 -0
  84. package/tests/syncApi-obsolete-records.test.ts +69 -0
  85. package/tests/syncApi-type-compat.test.ts +72 -0
  86. package/tests/syncDev-permissions.test.ts +81 -0
  87. package/tests/syncMenu-disableMenus-hard-delete.test.ts +88 -0
  88. package/tests/syncMenu-duplicate-path.test.ts +122 -0
  89. package/tests/syncMenu-obsolete-records.test.ts +161 -0
  90. package/tests/syncMenu-parentPath-from-tree.test.ts +75 -0
  91. package/tests/syncMenu-paths.test.ts +0 -9
  92. package/tests/{syncDb-apply.test.ts → syncTable-apply.test.ts} +14 -24
  93. package/tests/{syncDb-array-number.test.ts → syncTable-array-number.test.ts} +31 -31
  94. package/tests/syncTable-constants.test.ts +101 -0
  95. package/tests/syncTable-db-integration.test.ts +237 -0
  96. package/tests/{syncDb-ddl.test.ts → syncTable-ddl.test.ts} +67 -53
  97. package/tests/{syncDb-helpers.test.ts → syncTable-helpers.test.ts} +12 -26
  98. package/tests/syncTable-schema.test.ts +99 -0
  99. package/tests/syncTable-testkit.test.ts +25 -0
  100. package/tests/syncTable-types.test.ts +122 -0
  101. package/tests/tableRef-and-deserialize.test.ts +67 -0
  102. package/tsconfig.json +1 -1
  103. package/types/api.d.ts +1 -1
  104. package/types/befly.d.ts +13 -12
  105. package/types/cache.d.ts +2 -2
  106. package/types/context.d.ts +1 -1
  107. package/types/database.d.ts +0 -5
  108. package/types/hook.d.ts +1 -10
  109. package/types/plugin.d.ts +2 -96
  110. package/types/sync.d.ts +19 -25
  111. package/utils/convertBigIntFields.ts +38 -0
  112. package/utils/disableMenusGlob.ts +85 -0
  113. package/utils/importDefault.ts +21 -0
  114. package/utils/isDirentDirectory.ts +23 -0
  115. package/utils/loadMenuConfigs.ts +145 -0
  116. package/utils/processFields.ts +25 -0
  117. package/utils/scanAddons.ts +72 -0
  118. package/utils/scanFiles.ts +129 -21
  119. package/utils/scanSources.ts +64 -0
  120. package/utils/sortModules.ts +137 -0
  121. package/checks/checkApp.ts +0 -55
  122. package/hooks/rateLimit.ts +0 -276
  123. package/sync/syncAll.ts +0 -35
  124. package/sync/syncDb/apply.ts +0 -192
  125. package/sync/syncDb/constants.ts +0 -119
  126. package/sync/syncDb/ddl.ts +0 -251
  127. package/sync/syncDb/helpers.ts +0 -84
  128. package/sync/syncDb/schema.ts +0 -202
  129. package/sync/syncDb/sqlite.ts +0 -48
  130. package/sync/syncDb/table.ts +0 -207
  131. package/sync/syncDb/tableCreate.ts +0 -163
  132. package/sync/syncDb/types.ts +0 -132
  133. package/sync/syncDb/version.ts +0 -69
  134. package/sync/syncDb.ts +0 -168
  135. package/tests/rateLimit-hook.test.ts +0 -477
  136. package/tests/syncDb-constants.test.ts +0 -130
  137. package/tests/syncDb-schema.test.ts +0 -179
  138. package/tests/syncDb-types.test.ts +0 -139
  139. package/utils/addonHelper.ts +0 -90
  140. package/utils/modules.ts +0 -98
  141. package/utils/route.ts +0 -23
@@ -0,0 +1,136 @@
1
+ /**
2
+ * SQL 入参校验工具(静态类)
3
+ *
4
+ * 目标:把“参数合法性/一致性/安全性”判断从 SqlBuilder 等拼接逻辑中拆出来,便于复用与维护。
5
+ *
6
+ * 说明:这里的校验仅关注“字符串/标识符/批量数据结构”层面的正确性;
7
+ * 具体 SQL 语义(如字段是否存在)不在此处校验。
8
+ */
9
+
10
+ export class SqlCheck {
11
+ private static readonly SAFE_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
12
+
13
+ static assertNonEmptyString(value: unknown, label: string): asserts value is string {
14
+ if (typeof value !== "string") {
15
+ throw new Error(`${label} 必须是字符串 (value: ${String(value)})`);
16
+ }
17
+ if (!value.trim()) {
18
+ throw new Error(`${label} 不能为空`);
19
+ }
20
+ }
21
+
22
+ static assertNoUndefinedParam(value: unknown, label: string): void {
23
+ if (value === undefined) {
24
+ throw new Error(`${label} 不能为 undefined`);
25
+ }
26
+ }
27
+
28
+ static startsWithQuote(value: string): boolean {
29
+ const trimmed = value.trim();
30
+ return trimmed.startsWith("`") || trimmed.startsWith('"');
31
+ }
32
+
33
+ static isQuotedIdentPaired(value: string): boolean {
34
+ const trimmed = value.trim();
35
+ if (trimmed.length < 2) return false;
36
+
37
+ const first = trimmed[0];
38
+ const last = trimmed[trimmed.length - 1];
39
+
40
+ if (first === "`" && last === "`") return true;
41
+ if (first === '"' && last === '"') return true;
42
+
43
+ return false;
44
+ }
45
+
46
+ static assertPairedQuotedIdentIfStartsWithQuote(value: string, label: string): void {
47
+ if (SqlCheck.startsWithQuote(value) && !SqlCheck.isQuotedIdentPaired(value)) {
48
+ throw new Error(`${label} 引用不完整,请使用成对的 \`...\` 或 "..." (value: ${value})`);
49
+ }
50
+ }
51
+
52
+ static assertSafeIdentifierPart(part: string, kind: "table" | "schema" | "alias" | "field"): void {
53
+ // 这里仅允许常规标识符(字母/数字/下划线),避免把复杂表达式混进“自动转义”路径。
54
+ if (!SqlCheck.SAFE_IDENTIFIER_RE.test(part)) {
55
+ throw new Error(`无效的 ${kind} 标识符: ${part}`);
56
+ }
57
+ }
58
+
59
+ static assertSafeAlias(aliasPart: string): void {
60
+ // alias 允许两种:
61
+ // 1) 已经被引用(`alias` 或 "alias")
62
+ // 2) 普通标识符(不允许带空格/符号),避免注入
63
+ if (SqlCheck.isQuotedIdentPaired(aliasPart)) return;
64
+ if (!SqlCheck.SAFE_IDENTIFIER_RE.test(aliasPart)) {
65
+ throw new Error(`无效的字段别名: ${aliasPart}`);
66
+ }
67
+ }
68
+
69
+ static assertNoExprField(field: string): void {
70
+ if (typeof field !== "string") return;
71
+ const trimmed = field.trim();
72
+ if (!trimmed) return;
73
+
74
+ // 收紧:包含函数/表达式(括号)不允许走自动转义路径
75
+ // 这类表达式应显式使用 selectRaw/whereRaw 以避免误拼接和注入风险
76
+ if (trimmed.includes("(") || trimmed.includes(")")) {
77
+ throw new Error(`字段包含函数/表达式,请使用 selectRaw/whereRaw (field: ${trimmed})`);
78
+ }
79
+ }
80
+
81
+ static assertNoUndefinedInRecord(row: Record<string, unknown>, label: string): void {
82
+ for (const [key, value] of Object.entries(row)) {
83
+ if (value === undefined) {
84
+ throw new Error(`${label} 存在 undefined 字段值 (field: ${key})`);
85
+ }
86
+ }
87
+ }
88
+
89
+ static assertBatchInsertRowsConsistent(rows: Array<Record<string, unknown>>, options: { table: string }): string[] {
90
+ if (!Array.isArray(rows)) {
91
+ throw new Error("批量插入 rows 必须是数组");
92
+ }
93
+ if (rows.length === 0) {
94
+ throw new Error(`插入数据不能为空 (table: ${options.table})`);
95
+ }
96
+
97
+ const first = rows[0];
98
+ if (!first || typeof first !== "object" || Array.isArray(first)) {
99
+ throw new Error(`批量插入的每一行必须是对象 (table: ${options.table}, rowIndex: 0)`);
100
+ }
101
+
102
+ const fields = Object.keys(first);
103
+ if (fields.length === 0) {
104
+ throw new Error(`插入数据必须至少有一个字段 (table: ${options.table})`);
105
+ }
106
+
107
+ const fieldSet = new Set(fields);
108
+
109
+ for (let i = 0; i < rows.length; i++) {
110
+ const row = rows[i];
111
+ if (!row || typeof row !== "object" || Array.isArray(row)) {
112
+ throw new Error(`批量插入的每一行必须是对象 (table: ${options.table}, rowIndex: ${i})`);
113
+ }
114
+
115
+ const rowKeys = Object.keys(row);
116
+ if (rowKeys.length !== fields.length) {
117
+ throw new Error(`批量插入每行字段必须一致 (table: ${options.table}, rowIndex: ${i})`);
118
+ }
119
+
120
+ for (const key of rowKeys) {
121
+ if (!fieldSet.has(key)) {
122
+ throw new Error(`批量插入每行字段必须一致 (table: ${options.table}, rowIndex: ${i}, extraField: ${key})`);
123
+ }
124
+ }
125
+
126
+ for (const field of fields) {
127
+ if (!(field in row)) {
128
+ throw new Error(`批量插入缺少字段 (table: ${options.table}, rowIndex: ${i}, field: ${field})`);
129
+ }
130
+ SqlCheck.assertNoUndefinedParam((row as any)[field], `批量插入字段值 (table: ${options.table}, rowIndex: ${i}, field: ${field})`);
131
+ }
132
+ }
133
+
134
+ return fields;
135
+ }
136
+ }
package/lib/validator.ts CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  import type { TableDefinition, FieldDefinition, ValidateResult, SingleResult } from "../types/validate.js";
7
7
 
8
- import { RegexAliases, getCompiledRegex } from "../utils/regex.js";
8
+ import { RegexAliases, getCompiledRegex } from "../configs/presetRegexp.js";
9
9
 
10
10
  /**
11
11
  * 验证器类
@@ -5,142 +5,39 @@
5
5
 
6
6
  // 类型导入
7
7
  import type { ApiRoute } from "../types/api.js";
8
+ import type { ScanFileResult } from "../utils/scanFiles.js";
8
9
 
9
10
  import { Logger } from "../lib/logger.js";
10
- import { projectApiDir } from "../paths.js";
11
- import { scanAddons, getAddonDir, addonDirExists } from "../utils/addonHelper.js";
12
- import { makeRouteKey } from "../utils/route.js";
13
- // 相对导入
14
- import { scanFiles } from "../utils/scanFiles.js";
15
-
16
- /**
17
- * 预定义的默认字段
18
- */
19
- const PRESET_FIELDS: Record<string, any> = {
20
- "@id": { name: "ID", type: "number", min: 1, max: null },
21
- "@page": { name: "页码", type: "number", min: 1, max: 9999, default: 1 },
22
- "@limit": { name: "每页数量", type: "number", min: 1, max: 100, default: 30 },
23
- "@keyword": { name: "关键词", type: "string", min: 0, max: 50 },
24
- "@state": { name: "状态", type: "number", regex: "^[0-2]$" }
25
- };
26
-
27
- /**
28
- * 处理字段定义:将 @ 符号引用替换为实际字段定义
29
- */
30
- function processFields(fields: Record<string, any>, apiName: string, routePath: string): Record<string, any> {
31
- if (!fields || typeof fields !== "object") return fields;
32
-
33
- const processed: Record<string, any> = {};
34
- for (const [key, value] of Object.entries(fields)) {
35
- if (typeof value === "string" && value.startsWith("@")) {
36
- if (PRESET_FIELDS[value]) {
37
- processed[key] = PRESET_FIELDS[value];
38
- continue;
39
- }
40
-
41
- const validKeys = Object.keys(PRESET_FIELDS).join(", ");
42
- throw new Error(`API [${apiName}] (${routePath}) 字段 [${key}] 引用了未定义的预设字段 "${value}"。可用的预设字段有: ${validKeys}`);
43
- }
44
-
45
- processed[key] = value;
46
- }
47
-
48
- return processed;
49
- }
11
+ import { processFields } from "../utils/processFields.js";
50
12
 
51
13
  /**
52
14
  * 加载所有 API 路由
53
- * @param apis - API 跁由映射表
15
+ * @param apiItems - scanSources/scanFiles 扫描到的 API 条目数组
16
+ * @returns API 路由映射表
54
17
  */
55
- export async function loadApis(apis: Map<string, ApiRoute>): Promise<void> {
56
- try {
57
- // 1. 扫描项目 API
58
- const projectApiFiles = await scanFiles(projectApiDir);
59
- const projectApiList = projectApiFiles.map((file) => ({
60
- filePath: file.filePath,
61
- relativePath: file.relativePath,
62
- type: "project" as const,
63
- routePrefix: "/",
64
- typeName: "项目"
65
- }));
66
-
67
- // 2. 扫描组件 API
68
- const addonApiList: Array<{
69
- filePath: string;
70
- relativePath: string;
71
- type: "addon";
72
- routePrefix: string;
73
- typeName: string;
74
- }> = [];
75
- const addons = scanAddons();
76
- for (const addon of addons) {
77
- if (!addonDirExists(addon, "apis")) continue;
78
-
79
- const addonApiDir = getAddonDir(addon, "apis");
80
- const addonApiFiles = await scanFiles(addonApiDir);
81
-
82
- for (const file of addonApiFiles) {
83
- addonApiList.push({
84
- filePath: file.filePath,
85
- relativePath: file.relativePath,
86
- type: "addon" as const,
87
- routePrefix: `/addon/${addon}/`, // 组件 API 默认带斜杠
88
- typeName: `组件${addon}`
89
- });
90
- }
18
+ export async function loadApis(apis: ScanFileResult[]): Promise<Map<string, ApiRoute>> {
19
+ const apisMap = new Map<string, ApiRoute>();
20
+
21
+ for (const api of apis) {
22
+ const apiType = (api as any).type;
23
+ // 兼容:scanFiles 的结果或测试构造数据可能缺少 type 字段;缺少时默认按 API 处理。
24
+ // 仅在 type 显式存在且不等于 "api" 时跳过,避免错误过滤。
25
+ if (apiType && apiType !== "api") {
26
+ continue;
91
27
  }
92
28
 
93
- // 3. 合并所有 API 文件
94
- const allApiFiles = [...projectApiList, ...addonApiList];
29
+ try {
30
+ const apiRoute = api as any;
95
31
 
96
- // 4. 遍历处理所有 API 文件
97
- for (const apiFile of allApiFiles) {
98
- try {
99
- // Windows 下路径需要转换为正斜杠格式
100
- const normalizedFilePath = apiFile.filePath.replace(/\\/g, "/");
101
- const apiImport = await import(normalizedFilePath);
102
- const api = apiImport.default;
32
+ // 处理字段定义,将 @ 引用替换为实际字段定义
33
+ apiRoute.fields = processFields(apiRoute.fields || {}, apiRoute.name, apiRoute.routePath);
103
34
 
104
- api.name = api.name || apiFile.relativePath;
105
-
106
- // 设置默认值
107
- const methodStr = (api.method || "POST").toUpperCase();
108
- api.auth = api.auth !== undefined ? api.auth : true;
109
-
110
- // 构建路由路径(用于错误提示)
111
- const routePath = `/api${apiFile.routePrefix}${apiFile.relativePath}`;
112
-
113
- // 处理字段定义,将 @ 引用替换为实际字段定义
114
- api.fields = processFields(api.fields || {}, api.name, routePath);
115
- api.required = api.required || [];
116
-
117
- // 支持逗号分隔的多方法,拆分后分别注册
118
- const methods = methodStr
119
- .split(",")
120
- .map((m: string) => m.trim())
121
- .filter((m: string) => m);
122
- for (const method of methods) {
123
- const route = makeRouteKey(method, routePath);
124
- // 为每个方法创建独立的路由对象
125
- const routeApi: ApiRoute = {
126
- name: api.name,
127
- handler: api.handler,
128
- method: method,
129
- auth: api.auth,
130
- fields: api.fields,
131
- required: api.required,
132
- rawBody: api.rawBody,
133
- route: route
134
- };
135
- apis.set(route, routeApi);
136
- }
137
- } catch (error: any) {
138
- Logger.error({ err: error, api: apiFile.relativePath, type: apiFile.typeName }, "接口加载失败");
139
- process.exit(1);
140
- }
35
+ apisMap.set(apiRoute.routePath, apiRoute as ApiRoute);
36
+ } catch (error: any) {
37
+ Logger.error({ err: error, api: api.relativePath, file: api.filePath }, "接口加载失败");
38
+ throw error;
141
39
  }
142
- } catch (error: any) {
143
- Logger.error({ err: error }, "加载 API 时发生错误");
144
- process.exit(1);
145
40
  }
41
+
42
+ return apisMap;
146
43
  }
@@ -1,66 +1,51 @@
1
1
  /**
2
2
  * 钩子加载器
3
- * 默认只加载核心钩子
4
- * 可选:通过配置开启组件/项目钩子(默认关闭以保持稳定性与可预期性)
3
+ * 默认加载所有来源钩子(core/addon/app)
4
+ * 可通过 disableHooks 禁用指定钩子
5
5
  */
6
6
 
7
7
  // 类型导入
8
8
  import type { Hook } from "../types/hook.js";
9
+ import type { ScanFileResult } from "../utils/scanFiles.js";
9
10
 
10
- import { beflyConfig } from "../befly.config.js";
11
11
  import { Logger } from "../lib/logger.js";
12
- import { coreHookDir, projectHookDir } from "../paths.js";
13
- // 相对导入
14
- import { scanAddons, getAddonDir, addonDirExists } from "../utils/addonHelper.js";
15
- import { scanModules } from "../utils/modules.js";
12
+ import { sortModules } from "../utils/sortModules.js";
16
13
 
17
- export async function loadHooks(hooks: Hook[]): Promise<void> {
18
- try {
19
- const allHooks: Hook[] = [];
14
+ export async function loadHooks(hooks: ScanFileResult[], disableHooks: string[] = []): Promise<Hook[]> {
15
+ const hooksMap: Hook[] = [];
20
16
 
21
- // 1. 扫描核心钩子
22
- const coreHooks = await scanModules<Hook>(coreHookDir, "core", "钩子");
23
- allHooks.push(...coreHooks);
17
+ if (disableHooks.length > 0) {
18
+ Logger.info({ hooks: disableHooks }, "禁用钩子");
19
+ }
24
20
 
25
- // 2. 可选:扫描组件钩子(默认关闭)
26
- const enableAddonHooks = Boolean((beflyConfig as any).enableAddonHooks);
27
- if (enableAddonHooks) {
28
- const addonHooks: Hook[] = [];
29
- const addons = scanAddons();
30
- for (const addon of addons) {
31
- if (!addonDirExists(addon, "hooks")) continue;
32
- const dir = getAddonDir(addon, "hooks");
33
- const items = await scanModules<Hook>(dir, "addon", "钩子", addon);
34
- addonHooks.push(...items);
35
- }
36
- allHooks.push(...addonHooks);
21
+ const enabledHooks = hooks.filter((item: any) => {
22
+ const moduleName = item?.moduleName;
23
+ if (typeof moduleName !== "string" || moduleName.trim() === "") {
24
+ return false;
37
25
  }
38
26
 
39
- // 3. 可选:扫描项目钩子(默认关闭)
40
- const enableAppHooks = Boolean((beflyConfig as any).enableAppHooks);
41
- if (enableAppHooks) {
42
- const appHooks = await scanModules<Hook>(projectHookDir, "app", "钩子");
43
- allHooks.push(...appHooks);
27
+ if (disableHooks.includes(moduleName)) {
28
+ return false;
44
29
  }
45
30
 
46
- // 4. 过滤禁用的钩子
47
- const disableHooks = beflyConfig.disableHooks || [];
48
- const enabledHooks = allHooks.filter((hook) => hook.name && !disableHooks.includes(hook.name));
31
+ return true;
32
+ });
49
33
 
50
- if (disableHooks.length > 0) {
51
- Logger.info({ hooks: disableHooks }, "禁用钩子");
52
- }
34
+ const sortedHooks = sortModules(enabledHooks, { moduleLabel: "钩子" });
35
+ if (sortedHooks === false) {
36
+ throw new Error("钩子依赖关系错误");
37
+ }
53
38
 
54
- // 5. order 排序
55
- const sortedHooks = enabledHooks.sort((a, b) => {
56
- const orderA = a.order ?? 999;
57
- const orderB = b.order ?? 999;
58
- return orderA - orderB;
59
- });
39
+ for (const item of sortedHooks) {
40
+ const hookName = (item as any).moduleName as string;
41
+ const hook = item as any as Hook;
60
42
 
61
- hooks.push(...sortedHooks);
62
- } catch (error: any) {
63
- Logger.error({ err: error }, "加载钩子时发生错误");
64
- process.exit(1);
43
+ hooksMap.push({
44
+ name: hookName,
45
+ deps: hook.deps,
46
+ handler: hook.handler
47
+ });
65
48
  }
49
+
50
+ return hooksMap;
66
51
  }
@@ -5,67 +5,52 @@
5
5
 
6
6
  import type { BeflyContext } from "../types/befly.js";
7
7
  import type { Plugin } from "../types/plugin.js";
8
+ import type { ScanFileResult } from "../utils/scanFiles.js";
8
9
 
9
- import { beflyConfig } from "../befly.config.js";
10
10
  import { Logger } from "../lib/logger.js";
11
- import { corePluginDir, projectPluginDir } from "../paths.js";
12
- import { scanAddons, getAddonDir } from "../utils/addonHelper.js";
13
- import { sortModules, scanModules } from "../utils/modules.js";
11
+ import { sortModules } from "../utils/sortModules.js";
14
12
 
15
- export async function loadPlugins(plugins: Plugin[], context: BeflyContext): Promise<void> {
16
- try {
17
- const allPlugins: Plugin[] = [];
13
+ export async function loadPlugins(plugins: ScanFileResult[], context: BeflyContext, disablePlugins: string[] = []): Promise<Plugin[]> {
14
+ const pluginsMap: Plugin[] = [];
18
15
 
19
- // 1. 扫描核心插件
20
- const corePlugins = await scanModules<Plugin>(corePluginDir, "core", "插件");
21
-
22
- // 2. 扫描组件插件
23
- const addonPlugins: Plugin[] = [];
24
- const addons = scanAddons();
25
- for (const addon of addons) {
26
- const dir = getAddonDir(addon, "plugins");
27
- const plugins = await scanModules<Plugin>(dir, "addon", "插件", addon);
28
- addonPlugins.push(...plugins);
29
- }
30
-
31
- // 3. 扫描项目插件
32
- const appPlugins = await scanModules<Plugin>(projectPluginDir, "app", "插件");
33
-
34
- // 4. 合并所有插件
35
- allPlugins.push(...corePlugins);
36
- allPlugins.push(...addonPlugins);
37
- allPlugins.push(...appPlugins);
38
-
39
- // 5. 过滤禁用的插件
40
- const disablePlugins = beflyConfig.disablePlugins || [];
41
- const enabledPlugins = allPlugins.filter((plugin) => plugin.name && !disablePlugins.includes(plugin.name));
16
+ if (disablePlugins.length > 0) {
17
+ Logger.info({ plugins: disablePlugins }, "禁用插件");
18
+ }
42
19
 
43
- if (disablePlugins.length > 0) {
44
- Logger.info({ plugins: disablePlugins }, "禁用插件");
20
+ const enabledPlugins = plugins.filter((item: any) => {
21
+ const moduleName = item?.moduleName;
22
+ if (typeof moduleName !== "string" || moduleName.trim() === "") {
23
+ return false;
45
24
  }
46
-
47
- // 6. 排序与初始化
48
- const sortedPlugins = sortModules(enabledPlugins);
49
- if (sortedPlugins === false) {
50
- Logger.error("插件依赖关系错误,请检查 after 属性");
51
- process.exit(1);
25
+ if (disablePlugins.includes(moduleName)) {
26
+ return false;
52
27
  }
28
+ return true;
29
+ });
53
30
 
54
- for (const plugin of sortedPlugins) {
55
- try {
56
- plugins.push(plugin);
57
-
58
- const pluginInstance = typeof plugin.handler === "function" ? await plugin.handler(context) : {};
31
+ const sortedPlugins = sortModules(enabledPlugins, { moduleLabel: "插件" });
32
+ if (sortedPlugins === false) {
33
+ throw new Error("插件依赖关系错误");
34
+ }
59
35
 
60
- // 直接挂载到 befly
61
- (context as any)[plugin.name!] = pluginInstance;
62
- } catch (error: any) {
63
- Logger.error({ err: error, plugin: plugin.name }, "插件初始化失败");
64
- process.exit(1);
65
- }
36
+ for (const item of sortedPlugins) {
37
+ const pluginName = (item as any).moduleName as string;
38
+ const plugin = item as any as Plugin;
39
+
40
+ try {
41
+ const pluginInstance = typeof plugin.handler === "function" ? await plugin.handler(context) : {};
42
+ (context as any)[pluginName] = pluginInstance;
43
+
44
+ pluginsMap.push({
45
+ name: pluginName,
46
+ deps: plugin.deps,
47
+ handler: plugin.handler
48
+ });
49
+ } catch (error: any) {
50
+ Logger.error({ err: error, plugin: pluginName }, "插件初始化失败");
51
+ throw error;
66
52
  }
67
- } catch (error: any) {
68
- Logger.error({ err: error }, "加载插件时发生错误");
69
- process.exit(1);
70
53
  }
54
+
55
+ return pluginsMap;
71
56
  }
package/main.ts CHANGED
@@ -9,11 +9,12 @@ import type { BeflyContext, BeflyOptions } from "./types/befly.js";
9
9
  import type { Hook } from "./types/hook.js";
10
10
  import type { Plugin } from "./types/plugin.js";
11
11
 
12
- // ========== 相对导入(项目内部文件) ==========
13
- // 启动检查
14
12
  import { checkApi } from "./checks/checkApi.js";
15
- import { checkApp } from "./checks/checkApp.js";
13
+ import { checkHook } from "./checks/checkHook.js";
14
+ import { checkMenu } from "./checks/checkMenu.js";
15
+ import { checkPlugin } from "./checks/checkPlugin.js";
16
16
  import { checkTable } from "./checks/checkTable.js";
17
+ // ========== 相对导入(项目内部文件) ==========
17
18
  // 基础设施
18
19
  import { Connect } from "./lib/connect.js";
19
20
  import { Logger } from "./lib/logger.js";
@@ -25,10 +26,15 @@ import { loadPlugins } from "./loader/loadPlugins.js";
25
26
  import { apiHandler } from "./router/api.js";
26
27
  import { staticHandler } from "./router/static.js";
27
28
  // 同步
28
- import { syncAllCommand } from "./sync/syncAll.js";
29
+ import { syncApi } from "./sync/syncApi.js";
30
+ import { syncCache } from "./sync/syncCache.js";
31
+ import { syncDev } from "./sync/syncDev.js";
32
+ import { syncMenu } from "./sync/syncMenu.js";
33
+ import { syncTable } from "./sync/syncTable.js";
29
34
  // 工具
30
35
  import { calcPerfTime } from "./utils/calcPerfTime.js";
31
- import { isPrimaryProcess, getProcessRole } from "./utils/process.js";
36
+ import { getProcessRole } from "./utils/process.js";
37
+ import { scanSources } from "./utils/scanSources.js";
32
38
 
33
39
  /**
34
40
  * Befly 框架核心类
@@ -62,28 +68,61 @@ export class Befly {
62
68
  const { beflyConfig } = await import("./befly.config.js");
63
69
  this.config = beflyConfig;
64
70
 
65
- // 1. 执行启动检查
66
- await checkApp();
67
- await checkTable();
68
- await checkApi();
71
+ // 将配置注入到 ctx,供插件/Hook/sync 等按需读取
72
+ this.context.config = this.config;
73
+
74
+ const { apis, tables, plugins, hooks, addons } = await scanSources();
75
+
76
+ // 让后续 syncMenu 能拿到 addon 的 views 路径等信息
77
+ this.context.addons = addons;
78
+
79
+ await checkApi(apis);
80
+ await checkTable(tables);
81
+ await checkPlugin(plugins);
82
+ await checkHook(hooks);
83
+ const checkedMenus = await checkMenu(addons, { disableMenus: this.config.disableMenus || [] });
84
+
85
+ // 1. 启动期建立基础连接(SQL + Redis)
86
+ // 说明:连接职责收敛到启动期单点;插件只消费已连接实例(Connect.getSql/getRedis)。
87
+ await Connect.connect({
88
+ db: this.config.db || {},
89
+ redis: this.config.redis || {}
90
+ });
69
91
 
70
92
  // 2. 加载插件
71
- await loadPlugins(this.plugins, this.context as BeflyContext);
93
+ this.plugins = await loadPlugins(plugins as any, this.context as BeflyContext, this.config!.disablePlugins || []);
94
+
95
+ // 启动期依赖完整性检查:避免 sync 阶段出现 undefined 调用
96
+ // 注意:这里不做兼容别名(例如 dbHelper=db),要求上下文必须注入标准字段。
97
+ if (!(this.context as any).redis) {
98
+ throw new Error("启动失败:ctx.redis 未初始化(Redis 插件未加载或注入失败)");
99
+ }
100
+ if (!(this.context as any).db) {
101
+ throw new Error("启动失败:ctx.db 未初始化(Db 插件未加载或注入失败)");
102
+ }
103
+ if (!(this.context as any).cache) {
104
+ throw new Error("启动失败:ctx.cache 未初始化(cache 插件未加载或注入失败)");
105
+ }
106
+
107
+ // 5. 自动同步 (仅主进程执行,避免集群模式下重复执行)
108
+ await syncTable(this.context as BeflyContext, tables);
109
+ await syncApi(this.context as BeflyContext, apis as any);
110
+
111
+ await syncMenu(this.context as BeflyContext, checkedMenus);
112
+ await syncDev(this.context as BeflyContext, { devEmail: this.config.devEmail, devPassword: this.config.devPassword });
113
+
114
+ // 缓存同步:统一在所有同步完成后执行(cacheApis + cacheMenus + rebuildRoleApiPermissions)
115
+ await syncCache(this.context as BeflyContext);
72
116
 
73
117
  // 3. 加载钩子
74
- await loadHooks(this.hooks);
118
+ this.hooks = await loadHooks(hooks as any, this.config!.disableHooks || []);
75
119
 
76
120
  // 4. 加载所有 API
77
- await loadApis(this.apis);
78
-
79
- // 5. 自动同步 (仅主进程执行,避免集群模式下重复执行)
80
- if (isPrimaryProcess()) {
81
- await syncAllCommand();
82
- }
121
+ this.apis = await loadApis(apis as any);
83
122
 
84
- // 6. 启动 HTTP 服务器
123
+ // 6. 启动 HTTP服务器
85
124
  const apiFetch = apiHandler(this.apis, this.hooks, this.context as BeflyContext);
86
- const staticFetch = staticHandler();
125
+ const staticFetch = staticHandler(this.config!.cors);
87
126
 
88
127
  const server = Bun.serve({
89
128
  port: this.config!.appPort,