befly 3.9.40 → 3.10.1
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/README.md +47 -19
- package/befly.config.ts +19 -2
- package/checks/checkApi.ts +79 -77
- package/checks/checkHook.ts +48 -0
- package/checks/checkMenu.ts +168 -0
- package/checks/checkPlugin.ts +48 -0
- package/checks/checkTable.ts +137 -183
- package/docs/README.md +17 -11
- package/docs/api/api.md +16 -2
- package/docs/guide/quickstart.md +31 -10
- package/docs/hooks/hook.md +2 -2
- package/docs/hooks/rateLimit.md +1 -1
- package/docs/infra/redis.md +26 -14
- package/docs/plugins/plugin.md +23 -21
- package/docs/quickstart.md +5 -328
- package/docs/reference/addon.md +0 -4
- package/docs/reference/config.md +14 -31
- package/docs/reference/logger.md +3 -3
- package/docs/reference/sync.md +132 -237
- package/docs/reference/table.md +28 -30
- package/hooks/auth.ts +3 -4
- package/hooks/cors.ts +4 -6
- package/hooks/parser.ts +3 -4
- package/hooks/permission.ts +3 -4
- package/hooks/validator.ts +3 -4
- package/lib/cacheHelper.ts +89 -153
- package/lib/cacheKeys.ts +1 -1
- package/lib/connect.ts +9 -13
- package/lib/dbDialect.ts +285 -0
- package/lib/dbHelper.ts +179 -507
- package/lib/dbUtils.ts +450 -0
- package/lib/logger.ts +41 -5
- package/lib/redisHelper.ts +1 -0
- package/lib/sqlBuilder.ts +358 -58
- package/lib/sqlCheck.ts +136 -0
- package/lib/validator.ts +1 -1
- package/loader/loadApis.ts +23 -126
- package/loader/loadHooks.ts +31 -46
- package/loader/loadPlugins.ts +37 -52
- package/main.ts +58 -19
- package/package.json +24 -25
- package/paths.ts +14 -14
- package/plugins/cache.ts +12 -6
- package/plugins/cipher.ts +2 -2
- package/plugins/config.ts +6 -8
- package/plugins/db.ts +14 -19
- package/plugins/jwt.ts +6 -7
- package/plugins/logger.ts +7 -9
- package/plugins/redis.ts +8 -10
- package/plugins/tool.ts +3 -4
- package/router/api.ts +3 -2
- package/router/static.ts +7 -5
- package/sync/syncApi.ts +80 -235
- package/sync/syncCache.ts +16 -0
- package/sync/syncDev.ts +167 -202
- package/sync/syncMenu.ts +230 -444
- package/sync/syncTable.ts +1247 -0
- package/tests/_mocks/mockSqliteDb.ts +204 -0
- package/tests/addonHelper-cache.test.ts +32 -0
- package/tests/apiHandler-routePath-only.test.ts +32 -0
- package/tests/cacheHelper.test.ts +16 -51
- package/tests/checkApi-routePath-strict.test.ts +166 -0
- package/tests/checkMenu.test.ts +346 -0
- package/tests/checkTable-smoke.test.ts +157 -0
- package/tests/dbDialect-cache.test.ts +23 -0
- package/tests/dbDialect.test.ts +46 -0
- package/tests/dbHelper-advanced.test.ts +1 -1
- package/tests/dbHelper-all-array-types.test.ts +15 -15
- package/tests/dbHelper-batch-write.test.ts +90 -0
- package/tests/dbHelper-columns.test.ts +36 -54
- package/tests/dbHelper-execute.test.ts +26 -26
- package/tests/dbHelper-joins.test.ts +85 -176
- package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +3 -0
- package/tests/fixtures/scanFilesApis/a.ts +3 -0
- package/tests/fixtures/scanFilesApis/sub/b.ts +3 -0
- package/tests/loadPlugins-order-smoke.test.ts +75 -0
- package/tests/logger.test.ts +6 -6
- package/tests/redisHelper.test.ts +6 -1
- package/tests/scanFiles-routePath.test.ts +46 -0
- package/tests/smoke-sql.test.ts +24 -0
- package/tests/sqlBuilder-advanced.test.ts +18 -5
- package/tests/sqlBuilder.test.ts +24 -0
- package/tests/sync-init-guard.test.ts +105 -0
- package/tests/syncApi-insBatch-fields-consistent.test.ts +61 -0
- package/tests/syncApi-obsolete-records.test.ts +69 -0
- package/tests/syncApi-type-compat.test.ts +72 -0
- package/tests/syncDev-permissions.test.ts +81 -0
- package/tests/syncMenu-disableMenus-hard-delete.test.ts +88 -0
- package/tests/syncMenu-duplicate-path.test.ts +122 -0
- package/tests/syncMenu-obsolete-records.test.ts +161 -0
- package/tests/syncMenu-parentPath-from-tree.test.ts +75 -0
- package/tests/syncMenu-paths.test.ts +0 -9
- package/tests/{syncDb-apply.test.ts → syncTable-apply.test.ts} +14 -24
- package/tests/{syncDb-array-number.test.ts → syncTable-array-number.test.ts} +31 -31
- package/tests/syncTable-constants.test.ts +101 -0
- package/tests/syncTable-db-integration.test.ts +237 -0
- package/tests/{syncDb-ddl.test.ts → syncTable-ddl.test.ts} +67 -53
- package/tests/{syncDb-helpers.test.ts → syncTable-helpers.test.ts} +12 -26
- package/tests/syncTable-schema.test.ts +99 -0
- package/tests/syncTable-testkit.test.ts +25 -0
- package/tests/syncTable-types.test.ts +122 -0
- package/tests/tableRef-and-deserialize.test.ts +67 -0
- package/tsconfig.json +1 -1
- package/types/api.d.ts +1 -1
- package/types/befly.d.ts +13 -12
- package/types/cache.d.ts +2 -2
- package/types/context.d.ts +1 -1
- package/types/database.d.ts +0 -5
- package/types/hook.d.ts +1 -10
- package/types/plugin.d.ts +2 -96
- package/types/sync.d.ts +19 -25
- package/utils/convertBigIntFields.ts +38 -0
- package/utils/disableMenusGlob.ts +85 -0
- package/utils/importDefault.ts +21 -0
- package/utils/isDirentDirectory.ts +23 -0
- package/utils/loadMenuConfigs.ts +145 -0
- package/utils/processFields.ts +25 -0
- package/utils/scanAddons.ts +72 -0
- package/utils/scanFiles.ts +129 -21
- package/utils/scanSources.ts +64 -0
- package/utils/sortModules.ts +137 -0
- package/checks/checkApp.ts +0 -55
- package/docs/cipher.md +0 -582
- package/docs/database.md +0 -1176
- package/hooks/rateLimit.ts +0 -276
- package/sync/syncAll.ts +0 -35
- package/sync/syncDb/apply.ts +0 -192
- package/sync/syncDb/constants.ts +0 -119
- package/sync/syncDb/ddl.ts +0 -251
- package/sync/syncDb/helpers.ts +0 -84
- package/sync/syncDb/schema.ts +0 -202
- package/sync/syncDb/sqlite.ts +0 -48
- package/sync/syncDb/table.ts +0 -207
- package/sync/syncDb/tableCreate.ts +0 -163
- package/sync/syncDb/types.ts +0 -132
- package/sync/syncDb/version.ts +0 -69
- package/sync/syncDb.ts +0 -168
- package/tests/rateLimit-hook.test.ts +0 -477
- package/tests/syncDb-constants.test.ts +0 -130
- package/tests/syncDb-schema.test.ts +0 -179
- package/tests/syncDb-types.test.ts +0 -139
- package/utils/addonHelper.ts +0 -90
- package/utils/modules.ts +0 -98
- package/utils/route.ts +0 -23
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Glob } from "bun";
|
|
2
|
+
|
|
3
|
+
export type DisableMenuGlobRule = {
|
|
4
|
+
type: "glob";
|
|
5
|
+
raw: string;
|
|
6
|
+
glob: Glob;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const compiledGlobCache = new Map<string, Glob>();
|
|
10
|
+
|
|
11
|
+
function normalizeDisableMenusRules(disableMenus: unknown): string[] {
|
|
12
|
+
if (typeof disableMenus === "undefined") {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!Array.isArray(disableMenus)) {
|
|
17
|
+
throw new Error("disableMenus 配置不合法:必须是 string[]");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const normalized: string[] = [];
|
|
21
|
+
|
|
22
|
+
for (const rawRule of disableMenus) {
|
|
23
|
+
if (typeof rawRule !== "string") {
|
|
24
|
+
throw new Error("disableMenus 配置不合法:数组元素必须是 string");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const rule = rawRule.trim();
|
|
28
|
+
if (!rule) {
|
|
29
|
+
throw new Error("disableMenus 配置不合法:不允许空字符串");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
normalized.push(rule);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return normalized;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getOrCreateGlob(pattern: string): Glob {
|
|
39
|
+
const existed = compiledGlobCache.get(pattern);
|
|
40
|
+
if (existed) {
|
|
41
|
+
return existed;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const created = new Glob(pattern);
|
|
45
|
+
compiledGlobCache.set(pattern, created);
|
|
46
|
+
return created;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 将 disableMenus 编译为 Bun.Glob 规则(带进程级缓存)。
|
|
51
|
+
* - 仅使用 Bun.Glob 的语法与 API。
|
|
52
|
+
* - 此函数也会做基础的 disableMenus 配置校验(数组/string/非空)。
|
|
53
|
+
*/
|
|
54
|
+
export function compileDisableMenuGlobRules(disableMenus: unknown): DisableMenuGlobRule[] {
|
|
55
|
+
const normalized = normalizeDisableMenusRules(disableMenus);
|
|
56
|
+
if (normalized.length === 0) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const rules: DisableMenuGlobRule[] = [];
|
|
61
|
+
|
|
62
|
+
for (const rule of normalized) {
|
|
63
|
+
try {
|
|
64
|
+
const glob = getOrCreateGlob(rule);
|
|
65
|
+
rules.push({
|
|
66
|
+
type: "glob",
|
|
67
|
+
raw: rule,
|
|
68
|
+
glob: glob
|
|
69
|
+
});
|
|
70
|
+
} catch (error: any) {
|
|
71
|
+
throw new Error(`disableMenus 配置不合法:glob 规则 ${rule} 解析失败:${error?.message || String(error)}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return rules;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function isMenuPathDisabledByGlobRules(path: string, rules: DisableMenuGlobRule[]): boolean {
|
|
79
|
+
for (const rule of rules) {
|
|
80
|
+
if (rule.glob.match(path)) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 动态导入模块并优先返回其 default 导出。
|
|
3
|
+
*
|
|
4
|
+
* - import() 报错:返回 defaultValue
|
|
5
|
+
* - default 导出为 null/undefined:返回 defaultValue
|
|
6
|
+
*/
|
|
7
|
+
import { Logger } from "../lib/logger.js";
|
|
8
|
+
|
|
9
|
+
export async function importDefault<T>(file: string, defaultValue: T): Promise<T> {
|
|
10
|
+
try {
|
|
11
|
+
const mod = (await import(file)) as { default?: unknown } | null | undefined;
|
|
12
|
+
const value = mod?.default;
|
|
13
|
+
if (value === null || value === undefined) {
|
|
14
|
+
return defaultValue;
|
|
15
|
+
}
|
|
16
|
+
return value as T;
|
|
17
|
+
} catch (err: any) {
|
|
18
|
+
Logger.warn({ err: err, file: file }, "importDefault 导入失败,已回退到默认值");
|
|
19
|
+
return defaultValue;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Dirent } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { statSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
import { join } from "pathe";
|
|
6
|
+
|
|
7
|
+
export const isDirentDirectory = (parentDir: string, entry: Dirent): boolean => {
|
|
8
|
+
if (entry.isDirectory()) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// 兼容 Windows 下的 junction / workspace link:Dirent.isDirectory() 可能为 false,但它实际指向目录。
|
|
13
|
+
if (!entry.isSymbolicLink()) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const stats = statSync(join(parentDir, entry.name));
|
|
19
|
+
return stats.isDirectory();
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { MenuConfig } from "../types/sync.js";
|
|
2
|
+
import type { AddonInfo } from "./scanAddons.js";
|
|
3
|
+
import type { ViewDirMeta } from "befly-shared/utils/scanViewsDir";
|
|
4
|
+
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
7
|
+
|
|
8
|
+
import { cleanDirName, extractDefinePageMetaFromScriptSetup, extractScriptSetupBlock } from "befly-shared/utils/scanViewsDir";
|
|
9
|
+
import { join } from "pathe";
|
|
10
|
+
|
|
11
|
+
import { Logger } from "../lib/logger.js";
|
|
12
|
+
import { isDirentDirectory } from "./isDirentDirectory.js";
|
|
13
|
+
|
|
14
|
+
export async function scanViewsDirToMenuConfigs(viewsDir: string, prefix: string, parentPath: string = ""): Promise<MenuConfig[]> {
|
|
15
|
+
if (!existsSync(viewsDir)) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const menus: MenuConfig[] = [];
|
|
20
|
+
const entries = await readdir(viewsDir, { withFileTypes: true });
|
|
21
|
+
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (!isDirentDirectory(viewsDir, entry) || entry.name === "components") {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const dirPath = join(viewsDir, entry.name);
|
|
28
|
+
const indexVuePath = join(dirPath, "index.vue");
|
|
29
|
+
|
|
30
|
+
if (!existsSync(indexVuePath)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let meta: ViewDirMeta | null = null;
|
|
35
|
+
try {
|
|
36
|
+
const content = await readFile(indexVuePath, "utf-8");
|
|
37
|
+
|
|
38
|
+
const scriptSetup = extractScriptSetupBlock(content);
|
|
39
|
+
if (!scriptSetup) {
|
|
40
|
+
Logger.warn({ path: indexVuePath }, "index.vue 缺少 <script setup>,已跳过该目录菜单同步");
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
meta = extractDefinePageMetaFromScriptSetup(scriptSetup);
|
|
45
|
+
if (!meta?.title) {
|
|
46
|
+
Logger.warn({ path: indexVuePath }, "index.vue 未声明 definePage({ meta: { title, order? } }),已跳过该目录菜单同步");
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
} catch (error: any) {
|
|
50
|
+
Logger.warn({ err: error, path: indexVuePath }, "读取 index.vue 失败");
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!meta?.title) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const cleanName = cleanDirName(entry.name);
|
|
59
|
+
let menuPath: string;
|
|
60
|
+
if (cleanName === "index") {
|
|
61
|
+
menuPath = parentPath;
|
|
62
|
+
} else {
|
|
63
|
+
menuPath = parentPath ? `${parentPath}/${cleanName}` : `/${cleanName}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const fullPath = prefix ? (menuPath ? `${prefix}${menuPath}` : prefix) : menuPath || "/";
|
|
67
|
+
|
|
68
|
+
const menu: MenuConfig = {
|
|
69
|
+
name: meta.title,
|
|
70
|
+
path: fullPath,
|
|
71
|
+
sort: meta.order ?? 999999
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const children = await scanViewsDirToMenuConfigs(dirPath, prefix, menuPath);
|
|
75
|
+
if (children.length > 0) {
|
|
76
|
+
menu.children = children;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
menus.push(menu);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
menus.sort((a, b) => (a.sort ?? 999999) - (b.sort ?? 999999));
|
|
83
|
+
|
|
84
|
+
return menus;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getParentPath(path: string): string {
|
|
88
|
+
// "/a/b" => "/a"
|
|
89
|
+
// "/a" => ""
|
|
90
|
+
const parts = path.split("/").filter((p) => !!p);
|
|
91
|
+
if (parts.length <= 1) {
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
94
|
+
return `/${parts.slice(0, -1).join("/")}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function loadMenuConfigs(addons: AddonInfo[]): Promise<MenuConfig[]> {
|
|
98
|
+
const allMenus: MenuConfig[] = [];
|
|
99
|
+
|
|
100
|
+
for (const addon of addons) {
|
|
101
|
+
const adminViewsDirByTopLevel = join(addon.fullPath, "adminViews");
|
|
102
|
+
const adminViewsDirByViews = join(addon.fullPath, "views", "admin");
|
|
103
|
+
const adminViewsDir = existsSync(adminViewsDirByTopLevel) ? adminViewsDirByTopLevel : existsSync(adminViewsDirByViews) ? adminViewsDirByViews : null;
|
|
104
|
+
if (!adminViewsDir) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const prefix = `/${addon.source}/${addon.name}`;
|
|
110
|
+
const menus = await scanViewsDirToMenuConfigs(adminViewsDir, prefix);
|
|
111
|
+
if (menus.length > 0) {
|
|
112
|
+
for (const menu of menus) {
|
|
113
|
+
allMenus.push(menu);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch (error: any) {
|
|
117
|
+
Logger.warn(
|
|
118
|
+
{
|
|
119
|
+
err: error,
|
|
120
|
+
addon: addon.name,
|
|
121
|
+
addonSource: addon.source,
|
|
122
|
+
dir: adminViewsDir
|
|
123
|
+
},
|
|
124
|
+
"扫描 addon views 目录失败"
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const menusJsonPath = join(process.cwd(), "menus.json");
|
|
130
|
+
if (existsSync(menusJsonPath)) {
|
|
131
|
+
try {
|
|
132
|
+
const content = await readFile(menusJsonPath, "utf-8");
|
|
133
|
+
const appMenus = JSON.parse(content);
|
|
134
|
+
if (Array.isArray(appMenus) && appMenus.length > 0) {
|
|
135
|
+
for (const menu of appMenus) {
|
|
136
|
+
allMenus.push(menu);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch (error: any) {
|
|
140
|
+
Logger.warn({ err: error }, "读取项目 menus.json 失败");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return allMenus;
|
|
145
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { presetFields } from "../configs/presetFields.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 处理字段定义:将 @ 符号引用替换为实际字段定义
|
|
5
|
+
*/
|
|
6
|
+
export function processFields(fields: Record<string, any>, apiName: string, routePath: string): Record<string, any> {
|
|
7
|
+
if (!fields || typeof fields !== "object") return fields;
|
|
8
|
+
|
|
9
|
+
const processed: Record<string, any> = {};
|
|
10
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
11
|
+
if (typeof value === "string" && value.startsWith("@")) {
|
|
12
|
+
if (presetFields[value]) {
|
|
13
|
+
processed[key] = presetFields[value];
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const validKeys = Object.keys(presetFields).join(", ");
|
|
18
|
+
throw new Error(`API [${apiName}] (${routePath}) 字段 [${key}] 引用了未定义的预设字段 "${value}"。可用的预设字段有: ${validKeys}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
processed[key] = value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return processed;
|
|
25
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { camelCase } from "es-toolkit/string";
|
|
4
|
+
import { join, resolve } from "pathe";
|
|
5
|
+
|
|
6
|
+
import { appAddonsDir, appDir } from "../paths.js";
|
|
7
|
+
import { isDirentDirectory } from "./isDirentDirectory.js";
|
|
8
|
+
|
|
9
|
+
export type AddonSource = "addon" | "app";
|
|
10
|
+
|
|
11
|
+
export interface AddonInfo {
|
|
12
|
+
/** addon 来源 */
|
|
13
|
+
source: AddonSource;
|
|
14
|
+
|
|
15
|
+
/** addon 来源中文名 */
|
|
16
|
+
sourceName: string;
|
|
17
|
+
|
|
18
|
+
/** addon 名称(目录名,通常是 demo/admin 等) */
|
|
19
|
+
name: string;
|
|
20
|
+
|
|
21
|
+
/** camelCase(name) */
|
|
22
|
+
camelName: string;
|
|
23
|
+
|
|
24
|
+
/** addon 根目录绝对路径 */
|
|
25
|
+
rootDir: string;
|
|
26
|
+
|
|
27
|
+
/** 兼容字段:历史上使用 fullPath 表示 rootDir */
|
|
28
|
+
fullPath: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 扫描 node_modules/@befly-addon + 项目 addons/(项目优先级更高) */
|
|
32
|
+
export const scanAddons = (): AddonInfo[] => {
|
|
33
|
+
const addonMap = new Map<string, AddonInfo>();
|
|
34
|
+
|
|
35
|
+
const scanBaseDir = (baseDir: string, source: AddonSource, sourceName: string) => {
|
|
36
|
+
if (!existsSync(baseDir)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const entries = readdirSync(baseDir, { withFileTypes: true });
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
if (entry.name.startsWith("_")) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!isDirentDirectory(baseDir, entry)) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const rootDir = resolve(baseDir, entry.name);
|
|
51
|
+
|
|
52
|
+
const info: AddonInfo = {
|
|
53
|
+
source: source,
|
|
54
|
+
sourceName: sourceName,
|
|
55
|
+
name: entry.name,
|
|
56
|
+
camelName: camelCase(entry.name),
|
|
57
|
+
rootDir: rootDir,
|
|
58
|
+
fullPath: rootDir
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
addonMap.set(entry.name, info);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// node_modules 中的 @befly-addon
|
|
66
|
+
scanBaseDir(join(appDir, "node_modules", "@befly-addon"), "addon", "组件");
|
|
67
|
+
|
|
68
|
+
// 项目本地 addons(同名覆盖 node_modules)
|
|
69
|
+
scanBaseDir(appAddonsDir, "app", "项目");
|
|
70
|
+
|
|
71
|
+
return Array.from(addonMap.values());
|
|
72
|
+
};
|
package/utils/scanFiles.ts
CHANGED
|
@@ -1,48 +1,156 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
|
|
3
|
+
import { forOwn } from "es-toolkit/compat";
|
|
4
|
+
import { camelCase } from "es-toolkit/string";
|
|
3
5
|
import { relative, normalize, parse, join } from "pathe";
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
import { importDefault } from "./importDefault.js";
|
|
8
|
+
|
|
9
|
+
export type ScanFileSource = "app" | "addon" | "core";
|
|
10
|
+
|
|
11
|
+
export type ScanFileType = "api" | "table" | "plugin" | "hook";
|
|
12
|
+
|
|
13
|
+
export interface ScanFileResultBase {
|
|
14
|
+
source: ScanFileSource; // 文件来源
|
|
15
|
+
type: ScanFileType; // 文件类型(api/table/plugin/hook)
|
|
16
|
+
sourceName: string; // 来源名称(用于日志展示)
|
|
6
17
|
filePath: string; // 绝对路径
|
|
7
18
|
relativePath: string; // 相对路径(无扩展名)
|
|
8
19
|
fileName: string; // 文件名(无扩展名)
|
|
20
|
+
|
|
21
|
+
/** 模块名(用于 deps 依赖图 key 与运行时挂载 key) */
|
|
22
|
+
moduleName: string;
|
|
23
|
+
|
|
24
|
+
/** addon 名:addon 来源为真实值;core/app 统一为空字符串("") */
|
|
25
|
+
addonName: string;
|
|
26
|
+
|
|
27
|
+
fileBaseName: string;
|
|
28
|
+
fileDir: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ScanFileResult =
|
|
32
|
+
| (ScanFileResultBase & {
|
|
33
|
+
type: "table";
|
|
34
|
+
content: any;
|
|
35
|
+
})
|
|
36
|
+
| (ScanFileResultBase & {
|
|
37
|
+
type: Exclude<ScanFileType, "table">;
|
|
38
|
+
} & Record<string, any>);
|
|
39
|
+
|
|
40
|
+
function parseAddonNameFromPath(normalizedPath: string): string | null {
|
|
41
|
+
// 期望路径中包含:/node_modules/@befly-addon/<addonName>/...
|
|
42
|
+
const parts = normalizedPath.split("/").filter(Boolean);
|
|
43
|
+
const idx = parts.indexOf("@befly-addon");
|
|
44
|
+
if (idx < 0) return null;
|
|
45
|
+
const addonName = parts[idx + 1];
|
|
46
|
+
if (typeof addonName !== "string" || addonName.trim() === "") return null;
|
|
47
|
+
return addonName;
|
|
9
48
|
}
|
|
10
49
|
|
|
11
50
|
/**
|
|
12
51
|
* 扫描指定目录下的文件
|
|
13
52
|
* @param dir 目录路径
|
|
14
|
-
* @param
|
|
53
|
+
* @param source 文件来源(app/addon/core)
|
|
54
|
+
* @param type 文件类型(api/table/plugin/hook)
|
|
55
|
+
* @param pattern Glob模式
|
|
15
56
|
*/
|
|
16
|
-
export async function scanFiles(dir: string, pattern: string
|
|
57
|
+
export async function scanFiles(dir: string, source: ScanFileSource, type: ScanFileType, pattern: string): Promise<ScanFileResult[]> {
|
|
17
58
|
if (!existsSync(dir)) return [];
|
|
18
59
|
|
|
19
60
|
const normalizedDir = normalize(dir);
|
|
20
61
|
const glob = new Bun.Glob(pattern);
|
|
21
62
|
const results: ScanFileResult[] = [];
|
|
22
63
|
|
|
23
|
-
|
|
24
|
-
|
|
64
|
+
try {
|
|
65
|
+
const files = await glob.scan({
|
|
66
|
+
cwd: dir,
|
|
67
|
+
onlyFiles: true,
|
|
68
|
+
absolute: true,
|
|
69
|
+
followSymlinks: true
|
|
70
|
+
});
|
|
25
71
|
|
|
26
|
-
|
|
27
|
-
|
|
72
|
+
for await (const file of files) {
|
|
73
|
+
if (file.endsWith(".d.ts")) continue;
|
|
28
74
|
|
|
29
|
-
|
|
30
|
-
|
|
75
|
+
// 使用 pathe.normalize 统一路径分隔符为 /
|
|
76
|
+
const normalizedFile = normalize(file);
|
|
31
77
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const parsedRelativePath = parse(relativePathWithExt);
|
|
35
|
-
const relativePath = parsedRelativePath.dir ? join(parsedRelativePath.dir, parsedRelativePath.name) : parsedRelativePath.name;
|
|
78
|
+
// 获取文件名(去除扩展名)
|
|
79
|
+
const fileName = parse(normalizedFile).name;
|
|
36
80
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
81
|
+
// 计算相对路径(去除扩展名)
|
|
82
|
+
const relativePathWithExt = relative(normalizedDir, normalizedFile);
|
|
83
|
+
const parsedRelativePath = parse(relativePathWithExt);
|
|
84
|
+
const relativePath = parsedRelativePath.dir ? join(parsedRelativePath.dir, parsedRelativePath.name) : parsedRelativePath.name;
|
|
40
85
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
relativePath
|
|
44
|
-
|
|
45
|
-
|
|
86
|
+
// 固定默认过滤(不可关闭):忽略下划线开头的文件/目录
|
|
87
|
+
if (fileName.startsWith("_")) continue;
|
|
88
|
+
if (relativePath.split("/").some((part) => part.startsWith("_"))) continue;
|
|
89
|
+
const content = await importDefault(normalizedFile, {});
|
|
90
|
+
|
|
91
|
+
const baseName = camelCase(fileName);
|
|
92
|
+
let addonName = "";
|
|
93
|
+
let moduleName = "";
|
|
94
|
+
|
|
95
|
+
if (source === "core") {
|
|
96
|
+
moduleName = baseName;
|
|
97
|
+
} else if (source === "app") {
|
|
98
|
+
moduleName = `app_${baseName}`;
|
|
99
|
+
} else {
|
|
100
|
+
const parsedAddonName = parseAddonNameFromPath(normalizedFile);
|
|
101
|
+
if (!parsedAddonName) {
|
|
102
|
+
throw new Error(`scanFiles addon moduleName 解析失败:未找到 @befly-addon/<addon>/ 段落:${normalizedFile}`);
|
|
103
|
+
}
|
|
104
|
+
addonName = parsedAddonName;
|
|
105
|
+
moduleName = `addon_${camelCase(addonName)}_${baseName}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const base: Record<string, any> = {
|
|
109
|
+
source: source,
|
|
110
|
+
type: type,
|
|
111
|
+
sourceName: { core: "核心", addon: "组件", app: "项目" }[source],
|
|
112
|
+
filePath: normalizedFile,
|
|
113
|
+
relativePath: relativePath,
|
|
114
|
+
fileName: fileName,
|
|
115
|
+
moduleName: moduleName,
|
|
116
|
+
addonName: addonName,
|
|
117
|
+
fileBaseName: parse(normalizedFile).base,
|
|
118
|
+
fileDir: dir
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
if (type === "table") {
|
|
122
|
+
base.content = content;
|
|
123
|
+
results.push(base as ScanFileResult);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (type === "api") {
|
|
127
|
+
base.auth = true;
|
|
128
|
+
base.rawBody = false;
|
|
129
|
+
base.method = "POST";
|
|
130
|
+
base.fields = {};
|
|
131
|
+
base.required = [];
|
|
132
|
+
}
|
|
133
|
+
if (type === "plugin" || type === "hook") {
|
|
134
|
+
base.deps = [];
|
|
135
|
+
base.name = "";
|
|
136
|
+
base.handler = null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
forOwn(content, (value, key) => {
|
|
140
|
+
base[key] = value;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (type === "api") {
|
|
144
|
+
base.routePrefix = source === "core" ? "/core" : source === "app" ? "/app" : `/addon/${addonName}`;
|
|
145
|
+
base.routePath = `/api${base.routePrefix}/${relativePath}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
results.push(base as ScanFileResult);
|
|
149
|
+
}
|
|
150
|
+
} catch (error: any) {
|
|
151
|
+
const wrappedError = new Error(`scanFiles failed: source=${source} type=${type} dir=${normalizedDir} pattern=${pattern}`);
|
|
152
|
+
(wrappedError as any).cause = error;
|
|
153
|
+
throw wrappedError;
|
|
46
154
|
}
|
|
47
155
|
return results;
|
|
48
156
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { AddonInfo } from "./scanAddons.js";
|
|
2
|
+
import type { ScanFileResult } from "./scanFiles.js";
|
|
3
|
+
|
|
4
|
+
import { join } from "pathe";
|
|
5
|
+
|
|
6
|
+
import { coreDir, appDir } from "../paths.js";
|
|
7
|
+
import { scanAddons } from "./scanAddons.js";
|
|
8
|
+
import { scanFiles } from "./scanFiles.js";
|
|
9
|
+
|
|
10
|
+
export type ScanSourcesResult = {
|
|
11
|
+
hooks: ScanFileResult[];
|
|
12
|
+
plugins: ScanFileResult[];
|
|
13
|
+
apis: ScanFileResult[];
|
|
14
|
+
tables: ScanFileResult[];
|
|
15
|
+
addons: AddonInfo[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const scanSources = async (): Promise<ScanSourcesResult> => {
|
|
19
|
+
const apis: ScanFileResult[] = [];
|
|
20
|
+
const plugins: ScanFileResult[] = [];
|
|
21
|
+
const hooks: ScanFileResult[] = [];
|
|
22
|
+
const tables: ScanFileResult[] = [];
|
|
23
|
+
|
|
24
|
+
const addons: AddonInfo[] = await scanAddons();
|
|
25
|
+
|
|
26
|
+
// 处理表格
|
|
27
|
+
tables.push(...(await scanFiles(join(appDir, "tables"), "app", "table", "*.json")));
|
|
28
|
+
|
|
29
|
+
for (const addon of addons) {
|
|
30
|
+
tables.push(...(await scanFiles(join(addon.fullPath, "tables"), "addon", "table", "*.json")));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 处理插件
|
|
34
|
+
plugins.push(...(await scanFiles(join(coreDir, "plugins"), "core", "plugin", "*.ts")));
|
|
35
|
+
plugins.push(...(await scanFiles(join(appDir, "plugins"), "app", "plugin", "*.ts")));
|
|
36
|
+
|
|
37
|
+
for (const addon of addons) {
|
|
38
|
+
plugins.push(...(await scanFiles(join(addon.fullPath, "plugins"), "addon", "plugin", "*.ts")));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 处理接口
|
|
42
|
+
apis.push(...(await scanFiles(join(coreDir, "apis"), "core", "api", "**/*.ts")));
|
|
43
|
+
apis.push(...(await scanFiles(join(appDir, "apis"), "app", "api", "**/*.ts")));
|
|
44
|
+
|
|
45
|
+
for (const addon of addons) {
|
|
46
|
+
apis.push(...(await scanFiles(join(addon.fullPath, "apis"), "addon", "api", "**/*.ts")));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 处理钩子
|
|
50
|
+
hooks.push(...(await scanFiles(join(coreDir, "hooks"), "core", "hook", "*.ts")));
|
|
51
|
+
hooks.push(...(await scanFiles(join(appDir, "hooks"), "app", "hook", "*.ts")));
|
|
52
|
+
|
|
53
|
+
for (const addon of addons) {
|
|
54
|
+
hooks.push(...(await scanFiles(join(addon.fullPath, "hooks"), "addon", "hook", "*.ts")));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
hooks: hooks,
|
|
59
|
+
plugins: plugins,
|
|
60
|
+
apis: apis,
|
|
61
|
+
tables: tables,
|
|
62
|
+
addons: addons
|
|
63
|
+
};
|
|
64
|
+
};
|