@oj-module/cli 0.2.56 → 0.2.57
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/devkit/README.md +7 -4
- package/devkit/SKILL.md +4 -1
- package/devkit/manual.md +4 -2
- package/devkit/module-dev-and-cdn-playground.md +7 -0
- package/devkit/publish-walkthrough.md +199 -0
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +5 -5
- package/shell-dist/index.html +3 -3
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/vendor-devkit.test.ts +87 -0
- package/src/build/backend.ts +131 -0
- package/src/build/module-build.ts +364 -0
- package/src/build/module-def.ts +381 -0
- package/src/build/site-merge.ts +33 -0
- package/src/build.ts +16 -885
- package/src/bundle.ts +61 -0
- package/src/cli.ts +4 -1
- package/src/init.ts +12 -35
- package/src/lifecycle/__tests__/bundle-summary.test.ts +106 -0
- package/src/lifecycle/__tests__/bundle.test.ts +6 -0
- package/src/vendor.ts +34 -0
- package/templates/openapi.json +7 -0
- package/templates/src/app/api/_platform/_lifecycle/installer.ts +8 -3
- package/templates/src/app/api/_platform/_lifecycle/steps/register.ts +53 -0
- package/templates/src/app/api/_platform/_shared/service/domain/roles.ts +4 -1
- package/templates/src/app/api/_platform/_shared/service/domain/solution-perms.ts +7 -3
- package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.mysql.sql +3 -0
- package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.pg.sql +3 -0
- package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.sql +8 -0
- package/templates/src/app/api/_platform/schema.yaml +2 -0
- package/templates/src/app/api/system/roles/api.ts +2 -1
- package/templates/src/app/web/system/__client/api.schemas.ts +1 -0
- package/templates/src/app/web/system/pages/roles/index.tsx +7 -1
- package/templates/tests/lifecycle/role-title-translate.spec.ts +171 -0
- package/templates/tests/rbac-roles.spec.ts +9 -0
- package/vendor/host-versions.json +3 -3
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// BDD:solution 角色 title 落库时同步翻译(R1–R6,计划 docs/plans/20261103-0948-role-title-translate)。
|
|
2
|
+
// title = 解析文本(站点默认语言)、title_key = 原文(UI 实时翻译锚);解析失败回退裸 key,
|
|
3
|
+
// 绝不失败安装(R1);升级元数据覆盖 = 重解析(R3);迁移 0018 对存量 solution 角色回填、
|
|
4
|
+
// builtin 不动(R2/AC4);sys_permission 不变(R5);name/code/绑定不变(R6)。
|
|
5
|
+
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写(register.spec 同约定) */
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
9
|
+
import { registerSolution, resolveDeclTitles, siteLanguage } from "../../src/app/api/_platform/_lifecycle/steps/register";
|
|
10
|
+
import { installDb } from "../fixture";
|
|
11
|
+
|
|
12
|
+
const req = createRequire(import.meta.url);
|
|
13
|
+
const { DatabaseSync } = req("node:sqlite");
|
|
14
|
+
const { readFileSync } = req("node:fs");
|
|
15
|
+
const { dirname, join } = req("node:path");
|
|
16
|
+
|
|
17
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const enc = new TextEncoder();
|
|
19
|
+
|
|
20
|
+
let dbo: ReturnType<typeof installDb>;
|
|
21
|
+
const g = globalThis as any;
|
|
22
|
+
|
|
23
|
+
beforeAll(() => {
|
|
24
|
+
dbo = installDb();
|
|
25
|
+
g.http = {};
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
g.http = {};
|
|
30
|
+
dbo.exec("DELETE FROM sys_role_permission");
|
|
31
|
+
dbo.exec("DELETE FROM sys_user_role");
|
|
32
|
+
dbo.exec("DELETE FROM sys_permission");
|
|
33
|
+
dbo.exec("DELETE FROM sys_role");
|
|
34
|
+
dbo.exec("DELETE FROM sys_solution_route");
|
|
35
|
+
dbo.exec("DELETE FROM sys_setting");
|
|
36
|
+
dbo.exec("DELETE FROM sys_oplog");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function roleRows() {
|
|
40
|
+
return dbo.prepare("SELECT name, title, title_key FROM sys_role ORDER BY name").all() as Array<{ name: string, title: string | null, title_key: string | null }>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** readFile 假面:包内 locales 按路径精确映射,未命中即「文件不存在」 */
|
|
44
|
+
function fakeReadFile(staging: string, files: Record<string, string>) {
|
|
45
|
+
return async (p: string) => {
|
|
46
|
+
const hit = files[p];
|
|
47
|
+
if (hit === undefined)
|
|
48
|
+
throw new Error(`ENOENT: ${p}`);
|
|
49
|
+
return enc.encode(hit);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const STAGING = "/staging";
|
|
54
|
+
const PM_LOCALES = {
|
|
55
|
+
[`${STAGING}/web/modules/pm/locales/zh-CN.json`]: JSON.stringify({ role: { sales: "销售", admin: "方案管理员" } }),
|
|
56
|
+
[`${STAGING}/web/modules/pm/locales/en-US.json`]: JSON.stringify({ role: { sales: "Sales", admin: "Solution Admin" } }),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function pmDecl(title: string) {
|
|
60
|
+
return {
|
|
61
|
+
module: "pm",
|
|
62
|
+
version: "1.0.0",
|
|
63
|
+
roles: [{ name: "pm.sales", title, title_key: title, permissions: ["pm.doc.create"] as string[] }],
|
|
64
|
+
permissions: ["pm.doc.create"],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe("R4 语言取值", () => {
|
|
69
|
+
it("缺省 zh-CN(sys_setting 无 site.language 行)", async () => {
|
|
70
|
+
expect(await siteLanguage()).toBe("zh-CN");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("站点设置 site.language 生效(R4:站点默认语言)", async () => {
|
|
74
|
+
await g.db.table("sys_setting").insert({ key: "site.language", module: "framework", group: "site", type: "string", value: "en-US", update_time: Date.now(), deprecated: 0 }).run();
|
|
75
|
+
expect(await siteLanguage()).toBe("en-US");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("R1 注册时解析(resolveDeclTitles)", () => {
|
|
80
|
+
it("AC1 首装:title_key 解析为站点语言 locale 文本", async () => {
|
|
81
|
+
const resolved = await resolveDeclTitles(pmDecl("pm:role.sales"), STAGING, "zh-CN", fakeReadFile(STAGING, PM_LOCALES));
|
|
82
|
+
expect(resolved.roles[0]!.title).toBe("销售");
|
|
83
|
+
expect(resolved.roles[0]!.title_key).toBe("pm:role.sales");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("R4:语言参数决定读取的 locale 文件", async () => {
|
|
87
|
+
const resolved = await resolveDeclTitles(pmDecl("pm:role.sales"), STAGING, "en-US", fakeReadFile(STAGING, PM_LOCALES));
|
|
88
|
+
expect(resolved.roles[0]!.title).toBe("Sales");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("AC3 缺 key:保留裸 key(title 原样),绝不抛错", async () => {
|
|
92
|
+
const files = { [`${STAGING}/web/modules/pm/locales/zh-CN.json`]: JSON.stringify({ role: { sales: "销售" } }) };
|
|
93
|
+
const resolved = await resolveDeclTitles(pmDecl("pm:role.admin"), STAGING, "zh-CN", fakeReadFile(STAGING, files));
|
|
94
|
+
expect(resolved.roles[0]!.title).toBe("pm:role.admin");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("R1 边界:locale 文件缺失 / 坏 JSON / 非 string 值 / 空 key 形状不符 → 原文保留", async () => {
|
|
98
|
+
const noFile = await resolveDeclTitles(pmDecl("pm:role.sales"), STAGING, "zh-CN", fakeReadFile(STAGING, {}));
|
|
99
|
+
expect(noFile.roles[0]!.title).toBe("pm:role.sales");
|
|
100
|
+
|
|
101
|
+
const badJson = await resolveDeclTitles(pmDecl("pm:role.sales"), STAGING, "zh-CN", fakeReadFile(STAGING, { [`${STAGING}/web/modules/pm/locales/zh-CN.json`]: "{not-json" }));
|
|
102
|
+
expect(badJson.roles[0]!.title).toBe("pm:role.sales");
|
|
103
|
+
|
|
104
|
+
const nonString = await resolveDeclTitles(pmDecl("pm:role.sales"), STAGING, "zh-CN", fakeReadFile(STAGING, { [`${STAGING}/web/modules/pm/locales/zh-CN.json`]: JSON.stringify({ role: { sales: 42 } }) }));
|
|
105
|
+
expect(nonString.roles[0]!.title).toBe("pm:role.sales");
|
|
106
|
+
|
|
107
|
+
// 非 key 形状(中文文本)不做解析尝试——已解析文本二次注册不被误改
|
|
108
|
+
const plain = await resolveDeclTitles(pmDecl("销售"), STAGING, "zh-CN", fakeReadFile(STAGING, PM_LOCALES));
|
|
109
|
+
expect(plain.roles[0]!.title).toBe("销售");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("R1 边界:ns ≠ 模块名不越界解析(跨 ns 无本包 locale)", async () => {
|
|
113
|
+
const decl = { ...pmDecl("wiki:role.sales"), module: "pm" };
|
|
114
|
+
const resolved = await resolveDeclTitles(decl, STAGING, "zh-CN", fakeReadFile(STAGING, PM_LOCALES));
|
|
115
|
+
expect(resolved.roles[0]!.title).toBe("wiki:role.sales");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("R1–R3 落库(registerSolution 消费解析后的 decl)", () => {
|
|
120
|
+
it("AC1 首装:sys_role 落 (title=销售, title_key=pm:role.sales),权限点照常", async () => {
|
|
121
|
+
const resolved = await resolveDeclTitles(pmDecl("pm:role.sales"), STAGING, "zh-CN", fakeReadFile(STAGING, PM_LOCALES));
|
|
122
|
+
const r = await registerSolution(resolved);
|
|
123
|
+
expect(r.ok).toBe(true);
|
|
124
|
+
const rows = roleRows();
|
|
125
|
+
expect(rows).toEqual([{ name: "pm.sales", title: "销售", title_key: "pm:role.sales" }]);
|
|
126
|
+
const perms = dbo.prepare("SELECT code FROM sys_permission").all() as Array<{ code: string }>;
|
|
127
|
+
expect(perms.map(p => p.code)).toEqual(["pm.doc.create"]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("AC2 升级:元数据覆盖写新解析文本 + title_key 刷新(R3 locale 改进随升级传播)", async () => {
|
|
131
|
+
await registerSolution(await resolveDeclTitles(pmDecl("pm:role.sales"), STAGING, "zh-CN", fakeReadFile(STAGING, PM_LOCALES)));
|
|
132
|
+
const improved = fakeReadFile(STAGING, { [`${STAGING}/web/modules/pm/locales/zh-CN.json`]: JSON.stringify({ role: { sales: "销售冠军" } }) });
|
|
133
|
+
const r = await registerSolution(await resolveDeclTitles(pmDecl("pm:role.sales"), STAGING, "zh-CN", improved));
|
|
134
|
+
expect(r.ok).toBe(true);
|
|
135
|
+
expect(roleRows()).toEqual([{ name: "pm.sales", title: "销售冠军", title_key: "pm:role.sales" }]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("AC3 缺 key:注册不失败,title = 裸 key(现行回退形态)", async () => {
|
|
139
|
+
const r = await registerSolution(await resolveDeclTitles(pmDecl("pm:role.viewer"), STAGING, "zh-CN", fakeReadFile(STAGING, PM_LOCALES)));
|
|
140
|
+
expect(r.ok).toBe(true);
|
|
141
|
+
expect(roleRows()).toEqual([{ name: "pm.sales", title: "pm:role.viewer", title_key: "pm:role.viewer" }]);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("R2 迁移 0018(sys_role.title_key 回填)", () => {
|
|
146
|
+
it("AC4 夹具真实迁移后 sys_role 带 title_key 列(列进库 ↔ schema.yaml 一致守卫的库侧半边)", () => {
|
|
147
|
+
const cols = (dbo.prepare("PRAGMA table_info(sys_role)").all() as Array<{ name: string }>).map(c => c.name);
|
|
148
|
+
expect(cols).toContain("title_key");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("AC4 存量 solution 角色按 key 形状回填,builtin 角色不动、title 原文不改", () => {
|
|
152
|
+
const raw = new DatabaseSync(":memory:");
|
|
153
|
+
raw.exec(`
|
|
154
|
+
CREATE TABLE sys_role (
|
|
155
|
+
name TEXT PRIMARY KEY, module TEXT NOT NULL, title TEXT, description TEXT,
|
|
156
|
+
builtin INTEGER NOT NULL DEFAULT 0, deprecated INTEGER NOT NULL DEFAULT 0
|
|
157
|
+
);
|
|
158
|
+
INSERT INTO sys_role (name, module, title, builtin) VALUES
|
|
159
|
+
('pm.sales', 'pm', 'pm:role.sales', 0),
|
|
160
|
+
('wiki.admin', 'wiki', 'wiki:role.admin', 0),
|
|
161
|
+
('admin', 'framework', '系统管理员', 1);
|
|
162
|
+
`);
|
|
163
|
+
raw.exec(readFileSync(join(HERE, "../../src/app/api/_platform/migrations/0018__sys_role_title_key.sql"), "utf8"));
|
|
164
|
+
const rows = raw.prepare("SELECT name, title, title_key FROM sys_role ORDER BY name").all() as Array<{ name: string, title: string, title_key: string | null }>;
|
|
165
|
+
expect(rows).toEqual([
|
|
166
|
+
{ name: "admin", title: "系统管理员", title_key: null },
|
|
167
|
+
{ name: "pm.sales", title: "pm:role.sales", title_key: "pm:role.sales" },
|
|
168
|
+
{ name: "wiki.admin", title: "wiki:role.admin", title_key: "wiki:role.admin" },
|
|
169
|
+
]);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -157,6 +157,15 @@ describe("列表支撑(C2 页面数据源)", () => {
|
|
|
157
157
|
expect(perms.find(p => p.code === "wiki.new")?.deprecated).toBe(false);
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
+
it("listRoles 带出 title_key(R2:solution 角色 UI 实时翻译锚;custom/builtin 为 null)", async () => {
|
|
161
|
+
// solution 角色带 title_key(0018 起);custom 角色管理员手填 title、无 key
|
|
162
|
+
dbo.prepare("INSERT INTO sys_role (name, module, title, title_key, builtin) VALUES ('pm.sales', 'pm', '销售', 'pm:role.sales', 0)").run();
|
|
163
|
+
await createRole({ name: "custom.tk", title: "手填" });
|
|
164
|
+
const roles = await listRoles();
|
|
165
|
+
expect(roles.find(x => x.name === "pm.sales")?.title_key).toBe("pm:role.sales");
|
|
166
|
+
expect(roles.find(x => x.name === "custom.tk")?.title_key).toBe(null);
|
|
167
|
+
});
|
|
168
|
+
|
|
160
169
|
it("listRoles 原样返回 superadmin(只在 /system/roles 展示层隐去)", async () => {
|
|
161
170
|
// 成员资格页(users/roles.tsx)把 superadmin 当可授予项(仅 superadmin 可勾选),
|
|
162
171
|
// 页面数据源里少了它会让「授予超级管理员」直接不可用。过滤只做在展示层。
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
"react-router": "7.18.2",
|
|
9
9
|
"react-router/dom": "7.18.2",
|
|
10
10
|
"@tanstack/react-query": "5.102.8",
|
|
11
|
-
"@oj-module/runtime": "0.2.
|
|
12
|
-
"@oj-module/runtime/contract": "0.2.
|
|
13
|
-
"@oj-module/runtime/contract/errors": "0.2.
|
|
11
|
+
"@oj-module/runtime": "0.2.57",
|
|
12
|
+
"@oj-module/runtime/contract": "0.2.57",
|
|
13
|
+
"@oj-module/runtime/contract/errors": "0.2.57",
|
|
14
14
|
"@plane/kit": "0.1.20",
|
|
15
15
|
"@plane/editor": "0.1.20",
|
|
16
16
|
"antd": "6.6.1",
|