befly 3.10.0 → 3.10.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.
Files changed (79) hide show
  1. package/.gitignore +0 -0
  2. package/README.md +10 -13
  3. package/configs/presetFields.ts +10 -0
  4. package/configs/presetRegexp.ts +225 -0
  5. package/docs/README.md +17 -11
  6. package/docs/api/api.md +15 -1
  7. package/docs/guide/quickstart.md +19 -5
  8. package/docs/infra/redis.md +23 -11
  9. package/docs/quickstart.md +5 -335
  10. package/docs/reference/addon.md +0 -15
  11. package/docs/reference/config.md +1 -1
  12. package/docs/reference/logger.md +3 -3
  13. package/docs/reference/sync.md +99 -73
  14. package/docs/reference/table.md +1 -1
  15. package/package.json +15 -16
  16. package/docs/cipher.md +0 -582
  17. package/docs/database.md +0 -1176
  18. package/tests/_mocks/mockSqliteDb.ts +0 -204
  19. package/tests/addonHelper-cache.test.ts +0 -32
  20. package/tests/api-integration-array-number.test.ts +0 -282
  21. package/tests/apiHandler-routePath-only.test.ts +0 -32
  22. package/tests/befly-config-env.test.ts +0 -78
  23. package/tests/cacheHelper.test.ts +0 -323
  24. package/tests/cacheKeys.test.ts +0 -41
  25. package/tests/checkApi-routePath-strict.test.ts +0 -166
  26. package/tests/checkMenu.test.ts +0 -346
  27. package/tests/checkTable-smoke.test.ts +0 -157
  28. package/tests/cipher.test.ts +0 -249
  29. package/tests/dbDialect-cache.test.ts +0 -23
  30. package/tests/dbDialect.test.ts +0 -46
  31. package/tests/dbHelper-advanced.test.ts +0 -723
  32. package/tests/dbHelper-all-array-types.test.ts +0 -316
  33. package/tests/dbHelper-array-serialization.test.ts +0 -258
  34. package/tests/dbHelper-batch-write.test.ts +0 -90
  35. package/tests/dbHelper-columns.test.ts +0 -234
  36. package/tests/dbHelper-execute.test.ts +0 -187
  37. package/tests/dbHelper-joins.test.ts +0 -221
  38. package/tests/fields-redis-cache.test.ts +0 -127
  39. package/tests/fields-validate.test.ts +0 -99
  40. package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +0 -3
  41. package/tests/fixtures/scanFilesApis/a.ts +0 -3
  42. package/tests/fixtures/scanFilesApis/sub/b.ts +0 -3
  43. package/tests/getClientIp.test.ts +0 -54
  44. package/tests/integration.test.ts +0 -189
  45. package/tests/jwt.test.ts +0 -65
  46. package/tests/loadPlugins-order-smoke.test.ts +0 -75
  47. package/tests/logger.test.ts +0 -325
  48. package/tests/redisHelper.test.ts +0 -495
  49. package/tests/redisKeys.test.ts +0 -9
  50. package/tests/scanConfig.test.ts +0 -144
  51. package/tests/scanFiles-routePath.test.ts +0 -46
  52. package/tests/smoke-sql.test.ts +0 -24
  53. package/tests/sqlBuilder-advanced.test.ts +0 -608
  54. package/tests/sqlBuilder.test.ts +0 -209
  55. package/tests/sync-connection.test.ts +0 -183
  56. package/tests/sync-init-guard.test.ts +0 -105
  57. package/tests/syncApi-insBatch-fields-consistent.test.ts +0 -61
  58. package/tests/syncApi-obsolete-records.test.ts +0 -69
  59. package/tests/syncApi-type-compat.test.ts +0 -72
  60. package/tests/syncDev-permissions.test.ts +0 -81
  61. package/tests/syncMenu-disableMenus-hard-delete.test.ts +0 -88
  62. package/tests/syncMenu-duplicate-path.test.ts +0 -122
  63. package/tests/syncMenu-obsolete-records.test.ts +0 -161
  64. package/tests/syncMenu-parentPath-from-tree.test.ts +0 -75
  65. package/tests/syncMenu-paths.test.ts +0 -59
  66. package/tests/syncTable-apply.test.ts +0 -279
  67. package/tests/syncTable-array-number.test.ts +0 -160
  68. package/tests/syncTable-constants.test.ts +0 -101
  69. package/tests/syncTable-db-integration.test.ts +0 -237
  70. package/tests/syncTable-ddl.test.ts +0 -245
  71. package/tests/syncTable-helpers.test.ts +0 -99
  72. package/tests/syncTable-schema.test.ts +0 -99
  73. package/tests/syncTable-testkit.test.ts +0 -25
  74. package/tests/syncTable-types.test.ts +0 -122
  75. package/tests/tableRef-and-deserialize.test.ts +0 -67
  76. package/tests/util.test.ts +0 -100
  77. package/tests/validator-array-number.test.ts +0 -310
  78. package/tests/validator-default.test.ts +0 -373
  79. package/tests/validator.test.ts +0 -679
@@ -1,72 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
-
3
- import { syncApi } from "../sync/syncApi.js";
4
-
5
- describe("syncApi - type compatibility", () => {
6
- test("缺少 type 时应视为 api;非 api type 应被跳过", async () => {
7
- const existingRecords = [
8
- { id: 1, routePath: "/api/app/keep", name: "Keep", addonName: "", state: 0 },
9
- { id: 2, routePath: "/api/app/skip", name: "Skip", addonName: "", state: 0 }
10
- ];
11
-
12
- const calls = {
13
- delForceBatch: [] as any[],
14
- getAllArgs: null as any
15
- };
16
-
17
- const dbHelper = {
18
- tableExists: async () => true,
19
- updBatch: async () => 0,
20
- insBatch: async () => [],
21
- getAll: async (options: any) => {
22
- calls.getAllArgs = options;
23
- return { lists: existingRecords };
24
- },
25
- delForceBatch: async (_table: any, ids: any[]) => {
26
- calls.delForceBatch.push(ids);
27
- return ids.length;
28
- }
29
- } as any;
30
-
31
- const ctx = {
32
- db: dbHelper,
33
- addons: [],
34
- cache: {
35
- cacheApis: async () => {},
36
- rebuildRoleApiPermissions: async () => {}
37
- }
38
- } as any;
39
-
40
- const apiItems = [
41
- // 不带 type:应按 api 处理,保留 keep
42
- {
43
- source: "app",
44
- sourceName: "项目",
45
- filePath: "DUMMY",
46
- relativePath: "keep",
47
- fileName: "keep",
48
- moduleName: "app_keep",
49
- name: "Keep",
50
- routePath: "/api/app/keep",
51
- addonName: "",
52
- fileBaseName: "keep.ts",
53
- fileDir: "DUMMY",
54
- content: { name: "Keep", handler: async () => {} }
55
- },
56
- // 带非 api type:应被跳过,因此 DB 中的 skip 会被当作“配置不存在”而删除
57
- {
58
- type: "menu",
59
- name: "Skip",
60
- routePath: "/api/app/skip",
61
- addonName: ""
62
- }
63
- ] as any;
64
-
65
- await syncApi(ctx, apiItems);
66
-
67
- expect(calls.getAllArgs?.fields).toEqual(["id", "routePath", "name", "addonName", "state"]);
68
-
69
- expect(calls.delForceBatch).toHaveLength(1);
70
- expect(calls.delForceBatch[0]).toEqual([2]);
71
- });
72
- });
@@ -1,81 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
-
3
- import { syncDev } from "../sync/syncDev.js";
4
-
5
- describe("syncDev - dev role permissions", () => {
6
- test("dev 角色应拥有所有菜单和接口(state>=0)", async () => {
7
- const calls = {
8
- getAll: [],
9
- insData: [],
10
- updData: [],
11
- rebuildRoleApiPermissionsCount: 0
12
- };
13
-
14
- let nextId = 100;
15
-
16
- const ctx = {
17
- db: {
18
- tableExists: async (table) => {
19
- return table === "addon_admin_admin" || table === "addon_admin_role" || table === "addon_admin_menu" || table === "addon_admin_api";
20
- },
21
- getAll: async (options) => {
22
- calls.getAll.push(options);
23
-
24
- if (options?.table === "addon_admin_menu") {
25
- return {
26
- lists: [
27
- { path: "/dashboard", state: 0 },
28
- { path: "/permission/role", state: 0 }
29
- ]
30
- };
31
- }
32
- if (options?.table === "addon_admin_api") {
33
- return {
34
- lists: [
35
- { routePath: "/api/health", state: 0 },
36
- { routePath: "/api/addon/addonAdmin/auth/login", state: 0 }
37
- ]
38
- };
39
- }
40
-
41
- return { lists: [] };
42
- },
43
- getOne: async (_options) => {
44
- // 让所有角色/管理员都走插入逻辑,便于断言插入数据
45
- return null;
46
- },
47
- insData: async (options) => {
48
- calls.insData.push(options);
49
- nextId += 1;
50
- return nextId;
51
- },
52
- updData: async (options) => {
53
- calls.updData.push(options);
54
- return 1;
55
- }
56
- },
57
- cache: {
58
- rebuildRoleApiPermissions: async () => {
59
- calls.rebuildRoleApiPermissionsCount += 1;
60
- }
61
- }
62
- };
63
-
64
- await syncDev(ctx, { devEmail: "dev@qq.com", devPassword: "dev-password" });
65
-
66
- // 断言读取菜单/接口时按 state>=0 查询
67
- const menuGetAll = calls.getAll.find((c) => c?.table === "addon_admin_menu");
68
- expect(menuGetAll?.where?.state$gte).toBe(0);
69
-
70
- const apiGetAll = calls.getAll.find((c) => c?.table === "addon_admin_api");
71
- expect(apiGetAll?.where?.state$gte).toBe(0);
72
-
73
- // 断言 dev 角色写入时包含“所有路径”(按查询结果写入;统一为 pathname,不包含 method)
74
- const devRoleInsert = calls.insData.find((c) => c?.table === "addon_admin_role" && c?.data?.code === "dev");
75
- expect(devRoleInsert).toBeTruthy();
76
- expect(devRoleInsert.data.menus).toEqual(["/dashboard", "/permission/role"]);
77
- expect(devRoleInsert.data.apis).toEqual(["/api/health", "/api/addon/addonAdmin/auth/login"]);
78
-
79
- expect(calls.rebuildRoleApiPermissionsCount).toBe(0);
80
- });
81
- });
@@ -1,88 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
- import { join } from "node:path";
4
-
5
- import { checkMenu } from "../checks/checkMenu.js";
6
- import { syncMenu } from "../sync/syncMenu.js";
7
-
8
- describe("syncMenu - disableMenus hard delete", () => {
9
- test("命中 disableMenus 的菜单应被强制删除(不分 state)", async () => {
10
- const originalCwd = process.cwd();
11
- const projectDir = join(originalCwd, "temp", `syncMenu-disableMenus-hard-delete-${Date.now()}-${Math.random().toString(16).slice(2)}`);
12
- const menusJsonPath = join(projectDir, "menus.json");
13
-
14
- // /addon/admin/403 为禁用项,即使 state=-1 也应该被硬删除
15
- const existingMenus = [
16
- { id: 1, path: "/addon/admin/403", parentPath: "", name: "403", sort: 1, state: -1 },
17
- { id: 2, path: "/keep", parentPath: "", name: "Keep", sort: 2, state: 0 }
18
- ];
19
-
20
- const calls = {
21
- delForceBatch: [] as Array<{ table: string; ids: number[] }>
22
- };
23
-
24
- const dbHelper = {
25
- tableExists: async () => true,
26
- trans: async (callback: any) => {
27
- return await callback(dbHelper);
28
- },
29
- getAll: async (options: any) => {
30
- // syncMenu 会调用一次“全量不带 where”,一次 state>=0 的逻辑已在内存过滤
31
- if (options?.table === "addon_admin_menu") {
32
- return { lists: existingMenus };
33
- }
34
- return { lists: [] };
35
- },
36
- updBatch: async () => 0,
37
- insBatch: async () => [],
38
- delForceBatch: async (table: string, ids: number[]) => {
39
- calls.delForceBatch.push({ table: table, ids: ids });
40
- return ids.length;
41
- }
42
- } as any;
43
-
44
- const ctx = {
45
- db: dbHelper,
46
- addons: [],
47
- config: {
48
- disableMenus: ["**/403"]
49
- },
50
- cache: {
51
- cacheMenus: async () => {}
52
- }
53
- } as any;
54
-
55
- try {
56
- mkdirSync(projectDir, { recursive: true });
57
- process.chdir(projectDir);
58
-
59
- // 配置中仅保留 /keep
60
- writeFileSync(
61
- menusJsonPath,
62
- JSON.stringify(
63
- [
64
- {
65
- name: "Keep",
66
- path: "/keep",
67
- sort: 2
68
- }
69
- ],
70
- null,
71
- 4
72
- ),
73
- { encoding: "utf8" }
74
- );
75
-
76
- const menus = await checkMenu(ctx.addons, { disableMenus: ctx.config.disableMenus });
77
- await syncMenu(ctx, menus);
78
- } finally {
79
- process.chdir(originalCwd);
80
- rmSync(projectDir, { recursive: true, force: true });
81
- }
82
-
83
- expect(calls.delForceBatch).toHaveLength(1);
84
- expect(calls.delForceBatch[0].table).toBe("addon_admin_menu");
85
- // 应包含禁用菜单 id=1;/keep 不应被删
86
- expect(calls.delForceBatch[0].ids).toEqual([1]);
87
- });
88
- });
@@ -1,122 +0,0 @@
1
- import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test";
2
- import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
- import { join } from "node:path";
4
-
5
- import { checkMenu } from "../checks/checkMenu.js";
6
- import { setMockLogger } from "../lib/logger.js";
7
- import { syncMenu } from "../sync/syncMenu.js";
8
-
9
- // Mock pino logger
10
- const mockPino = {
11
- info: mock(() => {}),
12
- warn: mock(() => {}),
13
- error: mock(() => {}),
14
- debug: mock(() => {}),
15
- fatal: mock(() => {}),
16
- trace: mock(() => {}),
17
- silent: mock(() => {}),
18
- child: mock(() => mockPino),
19
- level: "info"
20
- };
21
-
22
- describe("syncMenu - duplicate path records", () => {
23
- beforeEach(() => {
24
- setMockLogger(mockPino as any);
25
- mockPino.warn.mockClear();
26
- });
27
-
28
- afterEach(() => {
29
- setMockLogger(null);
30
- });
31
-
32
- test("应检测重复 path 并删除多余记录(保留 id 最大的一条)", async () => {
33
- const originalCwd = process.cwd();
34
- const projectDir = join(originalCwd, "temp", `syncMenu-duplicate-path-${Date.now()}-${Math.random().toString(16).slice(2)}`);
35
- const menusJsonPath = join(projectDir, "menus.json");
36
-
37
- const existingMenus = [
38
- { id: 10, path: "/keep", parentPath: "", name: "Keep", sort: 999, state: 0 },
39
- { id: 20, path: "/keep", parentPath: "", name: "Keep", sort: 999, state: 0 }
40
- ];
41
-
42
- const calls = {
43
- delForceBatch: [] as any[],
44
- updBatchCount: 0,
45
- insBatchCount: 0
46
- };
47
-
48
- const dbHelper = {
49
- tableExists: async () => true,
50
- trans: async (callback: any) => {
51
- return await callback(dbHelper);
52
- },
53
- getAll: async (options: any) => {
54
- const stateGte = options?.where?.state$gte;
55
- if (typeof stateGte === "number") {
56
- return { lists: existingMenus.filter((m) => typeof m.state === "number" && m.state >= stateGte) };
57
- }
58
- return { lists: existingMenus };
59
- },
60
- updBatch: async () => {
61
- calls.updBatchCount += 1;
62
- return 0;
63
- },
64
- insBatch: async () => {
65
- calls.insBatchCount += 1;
66
- return [];
67
- },
68
- delForceBatch: async (table: string, ids: number[]) => {
69
- calls.delForceBatch.push({ table: table, ids: ids });
70
- return ids.length;
71
- }
72
- } as any;
73
-
74
- const ctx = {
75
- db: dbHelper,
76
- addons: [],
77
- config: {
78
- disableMenus: []
79
- },
80
- cache: {
81
- cacheMenus: async () => {}
82
- }
83
- } as any;
84
-
85
- try {
86
- mkdirSync(projectDir, { recursive: true });
87
- process.chdir(projectDir);
88
-
89
- writeFileSync(
90
- menusJsonPath,
91
- JSON.stringify(
92
- [
93
- {
94
- name: "Keep",
95
- path: "/keep",
96
- sort: 999
97
- }
98
- ],
99
- null,
100
- 4
101
- ),
102
- { encoding: "utf8" }
103
- );
104
-
105
- const menus = await checkMenu(ctx.addons);
106
- await syncMenu(ctx, menus);
107
- } finally {
108
- process.chdir(originalCwd);
109
- rmSync(projectDir, { recursive: true, force: true });
110
- }
111
-
112
- expect(mockPino.warn).toHaveBeenCalledTimes(1);
113
- expect(calls.updBatchCount).toBe(0);
114
- expect(calls.insBatchCount).toBe(0);
115
-
116
- expect(calls.delForceBatch).toHaveLength(1);
117
- expect(calls.delForceBatch[0].table).toBe("addon_admin_menu");
118
-
119
- // 只应删除较小 id 的重复记录,保留 id=20
120
- expect(calls.delForceBatch[0].ids).toEqual([10]);
121
- });
122
- });
@@ -1,161 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
- import { join } from "node:path";
4
-
5
- import { checkMenu } from "../checks/checkMenu.js";
6
- import { syncMenu } from "../sync/syncMenu.js";
7
-
8
- describe("syncMenu - delete obsolete records", () => {
9
- test("应删除不在配置中的菜单记录(仅 state>=0)", async () => {
10
- const originalCwd = process.cwd();
11
- const projectDir = join(originalCwd, "temp", `syncMenu-obsolete-records-${Date.now()}-${Math.random().toString(16).slice(2)}`);
12
- const menusJsonPath = join(projectDir, "menus.json");
13
-
14
- const existingMenus = [
15
- { id: 1, path: "/a", parentPath: "", state: 0 },
16
- { id: 2, path: "/b", parentPath: "", state: -1 },
17
- { id: 3, path: "", parentPath: "", state: 0 }
18
- ];
19
-
20
- const calls = {
21
- getAllCount: 0,
22
- delForceBatch: [] as any[]
23
- };
24
-
25
- const dbHelper = {
26
- tableExists: async () => true,
27
- trans: async (callback: any) => {
28
- return await callback(dbHelper);
29
- },
30
- getAll: async (options: any) => {
31
- calls.getAllCount += 1;
32
- const stateGte = options?.where?.state$gte;
33
- if (typeof stateGte === "number") {
34
- return { lists: existingMenus.filter((m) => typeof m.state === "number" && m.state >= stateGte) };
35
- }
36
- return { lists: existingMenus };
37
- },
38
- updBatch: async () => {
39
- return 0;
40
- },
41
- insBatch: async () => {
42
- return [];
43
- },
44
- delForceBatch: async (table: string, ids: number[]) => {
45
- calls.delForceBatch.push({ table: table, ids: ids });
46
- return ids.length;
47
- }
48
- } as any;
49
-
50
- const ctx = {
51
- db: dbHelper,
52
- addons: [],
53
- config: {
54
- disableMenus: []
55
- },
56
- cache: {
57
- cacheMenus: async () => {}
58
- }
59
- } as any;
60
-
61
- try {
62
- mkdirSync(projectDir, { recursive: true });
63
- process.chdir(projectDir);
64
-
65
- writeFileSync(menusJsonPath, "[]", { encoding: "utf8" });
66
- const menus = await checkMenu(ctx.addons);
67
- await syncMenu(ctx, menus);
68
- } finally {
69
- process.chdir(originalCwd);
70
- rmSync(projectDir, { recursive: true, force: true });
71
- }
72
-
73
- expect(calls.getAllCount).toBe(1);
74
- expect(calls.delForceBatch).toHaveLength(1);
75
- expect(calls.delForceBatch[0].table).toBe("addon_admin_menu");
76
- expect(calls.delForceBatch[0].ids).toEqual([1]);
77
- });
78
-
79
- test("不应删除仍在配置中的菜单记录", async () => {
80
- const originalCwd = process.cwd();
81
- const projectDir = join(originalCwd, "temp", `syncMenu-obsolete-records-${Date.now()}-${Math.random().toString(16).slice(2)}`);
82
- const menusJsonPath = join(projectDir, "menus.json");
83
-
84
- const existingMenus = [
85
- { id: 1, path: "/keep", parentPath: "", name: "Keep", sort: 999, state: 0 },
86
- { id: 2, path: "/remove", parentPath: "", name: "Remove", sort: 999, state: 0 }
87
- ];
88
-
89
- const calls = {
90
- delForceBatch: [] as any[],
91
- updBatchCount: 0
92
- };
93
-
94
- const dbHelper = {
95
- tableExists: async () => true,
96
- trans: async (callback: any) => {
97
- return await callback(dbHelper);
98
- },
99
- getAll: async (options: any) => {
100
- const stateGte = options?.where?.state$gte;
101
- if (typeof stateGte === "number") {
102
- return { lists: existingMenus.filter((m) => typeof m.state === "number" && m.state >= stateGte) };
103
- }
104
- return { lists: existingMenus };
105
- },
106
- updBatch: async () => {
107
- calls.updBatchCount += 1;
108
- return 0;
109
- },
110
- insBatch: async () => {
111
- return [];
112
- },
113
- delForceBatch: async (table: string, ids: number[]) => {
114
- calls.delForceBatch.push({ table: table, ids: ids });
115
- return ids.length;
116
- }
117
- } as any;
118
-
119
- const ctx = {
120
- db: dbHelper,
121
- addons: [],
122
- config: {
123
- disableMenus: []
124
- },
125
- cache: {
126
- cacheMenus: async () => {}
127
- }
128
- } as any;
129
-
130
- try {
131
- mkdirSync(projectDir, { recursive: true });
132
- process.chdir(projectDir);
133
-
134
- writeFileSync(
135
- menusJsonPath,
136
- JSON.stringify(
137
- [
138
- {
139
- name: "Keep",
140
- path: "/keep",
141
- sort: 999
142
- }
143
- ],
144
- null,
145
- 4
146
- ),
147
- { encoding: "utf8" }
148
- );
149
- const menus = await checkMenu(ctx.addons);
150
- await syncMenu(ctx, menus);
151
- } finally {
152
- process.chdir(originalCwd);
153
- rmSync(projectDir, { recursive: true, force: true });
154
- }
155
-
156
- expect(calls.delForceBatch).toHaveLength(1);
157
- expect(calls.delForceBatch[0].table).toBe("addon_admin_menu");
158
- expect(calls.delForceBatch[0].ids).toEqual([2]);
159
- expect(calls.updBatchCount).toBe(0);
160
- });
161
- });
@@ -1,75 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
-
3
- import { __test__ } from "../sync/syncMenu.js";
4
-
5
- describe("syncMenu - parentPath derived from tree", () => {
6
- test("根级菜单不应强制按 URL path 推导 parentPath(避免把同级菜单挂到首页下面)", () => {
7
- const mergedMenus: any[] = [
8
- {
9
- name: "首页",
10
- path: "/addon/admin",
11
- sort: 1
12
- },
13
- {
14
- name: "日志管理",
15
- path: "/addon/admin/log",
16
- sort: 40
17
- },
18
- {
19
- name: "配置管理",
20
- path: "/addon/admin/config",
21
- sort: 30
22
- }
23
- ];
24
-
25
- const map = __test__.flattenMenusToDefMap(mergedMenus as any);
26
-
27
- expect(map.get("/addon/admin")?.parentPath).toBe("");
28
- expect(map.get("/addon/admin/log")?.parentPath).toBe("");
29
- expect(map.get("/addon/admin/config")?.parentPath).toBe("");
30
- });
31
-
32
- test("子菜单应跟随 children 嵌套关系设置 parentPath", () => {
33
- const mergedMenus: any[] = [
34
- {
35
- name: "日志管理",
36
- path: "/addon/admin/log",
37
- sort: 40,
38
- children: [
39
- {
40
- name: "登录日志",
41
- path: "/addon/admin/log/login",
42
- sort: 1
43
- }
44
- ]
45
- }
46
- ];
47
-
48
- const map = __test__.flattenMenusToDefMap(mergedMenus as any);
49
-
50
- expect(map.get("/addon/admin/log")?.parentPath).toBe("");
51
- expect(map.get("/addon/admin/log/login")?.parentPath).toBe("/addon/admin/log");
52
- });
53
-
54
- test("显式 parentPath(包括空字符串)优先生效", () => {
55
- const mergedMenus: any[] = [
56
- {
57
- name: "自定义根",
58
- path: "/x",
59
- parentPath: "/custom",
60
- sort: 1
61
- },
62
- {
63
- name: "显式根",
64
- path: "/y",
65
- parentPath: "",
66
- sort: 2
67
- }
68
- ];
69
-
70
- const map = __test__.flattenMenusToDefMap(mergedMenus as any);
71
-
72
- expect(map.get("/x")?.parentPath).toBe("/custom");
73
- expect(map.get("/y")?.parentPath).toBe("");
74
- });
75
- });
@@ -1,59 +0,0 @@
1
- import { describe, test, expect, beforeAll, afterAll } from "bun:test";
2
- import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
- import { join } from "node:path";
4
-
5
- import { __test__ } from "../sync/syncMenu.js";
6
-
7
- const testRootDir = join(process.cwd(), "temp", "test-sync-menu-views");
8
- const viewsDir = join(testRootDir, "views");
9
-
10
- function writeVueIndex(dir: string, title: string, order?: number): void {
11
- if (!existsSync(dir)) {
12
- mkdirSync(dir, { recursive: true });
13
- }
14
-
15
- const metaOrder = typeof order === "number" ? `, order: ${order}` : "";
16
-
17
- const content = `<script setup>\ndefinePage({ meta: { title: "${title}"${metaOrder} } });\n</script>\n\n<template>\n <div />\n</template>\n`;
18
- writeFileSync(join(dir, "index.vue"), content, { encoding: "utf8" });
19
- }
20
-
21
- beforeAll(() => {
22
- if (existsSync(testRootDir)) {
23
- rmSync(testRootDir, { recursive: true, force: true });
24
- }
25
- mkdirSync(viewsDir, { recursive: true });
26
-
27
- // 根级 index:应映射为 prefix 本身(不带尾随 /)
28
- writeVueIndex(join(viewsDir, "index"), "Root", 2);
29
-
30
- // 带数字后缀目录:应清理后缀
31
- writeVueIndex(join(viewsDir, "user_1"), "User", 1);
32
- });
33
-
34
- afterAll(() => {
35
- if (existsSync(testRootDir)) {
36
- rmSync(testRootDir, { recursive: true, force: true });
37
- }
38
- });
39
-
40
- describe("syncMenu - scanViewsDir paths", () => {
41
- test("根级 index 不应生成尾随斜杠", async () => {
42
- const prefix = "/addon/addonAdmin";
43
- const menus = await __test__.scanViewsDir(viewsDir, prefix);
44
-
45
- const root = menus.find((m) => m.path === prefix);
46
- expect(root).toBeDefined();
47
- expect(root?.path).toBe(prefix);
48
- });
49
-
50
- test("目录名 _数字 后缀应被清理", async () => {
51
- const prefix = "/addon/addonAdmin";
52
- const menus = await __test__.scanViewsDir(viewsDir, prefix);
53
-
54
- const user = menus.find((m) => m.path === "/addon/addonAdmin/user");
55
- expect(user).toBeDefined();
56
- expect(user?.name).toBe("User");
57
- expect(user?.sort).toBe(1);
58
- });
59
- });