@oj-module/cli 0.2.8 → 0.2.15
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 +70 -24
- package/devkit/framework-dev.md +14 -5
- package/devkit/manual.md +69 -6
- package/devkit/module-dev-and-cdn-playground.md +197 -0
- package/devkit/sql-builder-missing-capabilities-pr.md +38 -4
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +262 -45
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/build-backend.test.ts +64 -0
- package/src/__tests__/versions.test.ts +26 -0
- package/src/build.ts +9 -0
- package/src/bundle.ts +8 -5
- package/src/contract/__tests__/market-client.integration.test.ts +13 -10
- package/src/index.ts +6 -0
- package/src/init.ts +22 -0
- package/src/lifecycle/__tests__/bundle.test.ts +3 -3
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/en-US.json +2 -1
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/zh-CN.json +2 -1
- package/src/lifecycle/__tests__/manifest-contract.test.ts +70 -1
- package/src/manifest-contract.ts +25 -2
- package/src/market/__tests__/bootstrap.test.ts +204 -0
- package/src/market/__tests__/build-index.test.ts +154 -0
- package/src/market/__tests__/cdn-layout.test.ts +96 -0
- package/src/market/__tests__/failure-playbook.test.ts +33 -0
- package/src/market/__tests__/fixture.test.ts +48 -0
- package/src/market/__tests__/publish.test.ts +109 -0
- package/src/market/__tests__/report.test.ts +51 -0
- package/src/market/__tests__/setup.test.ts +114 -0
- package/src/market/__tests__/verify.test.ts +111 -0
- package/src/market/bootstrap.ts +255 -0
- package/src/market/build-index.ts +159 -0
- package/src/market/cdn-layout.ts +122 -0
- package/src/market/command.ts +139 -0
- package/src/market/failure-playbook.ts +57 -0
- package/src/market/fixture.ts +138 -0
- package/src/market/index-builder.ts +76 -0
- package/src/market/publish.ts +156 -0
- package/src/market/report.ts +67 -0
- package/src/market/scan.ts +99 -0
- package/src/market/setup.ts +144 -0
- package/src/market/trial.ts +121 -0
- package/src/market/verify.ts +298 -0
- package/src/market/walkthrough.ts +506 -0
- package/src/oj.ts +3 -1
- package/src/preview-config.ts +14 -0
- package/src/preview.ts +4 -3
- package/src/sign.ts +21 -6
- package/src/usage.ts +19 -0
- package/src/vendor.ts +4 -2
- package/src/versions.ts +28 -0
- package/templates/api/.ojm-api-exempt.json +6 -2
- package/templates/api/config.yaml +20 -0
- package/templates/api/src/_platform/_lifecycle/steps/extract.ts +8 -8
- package/templates/api/src/_platform/_lifecycle/uninstall.ts +21 -53
- package/templates/api/src/_platform/_shared/service/domain/contacts.ts +15 -10
- package/templates/api/src/_platform/_shared/service/domain/solution-perms.ts +8 -6
- package/templates/api/src/_platform/_shared/service/infra/oplog.ts +16 -23
- package/templates/api/src/_platform/_shared/service/infra/query.ts +18 -0
- package/templates/api/src/_platform/_shared/service/infra/rate.ts +4 -1
- package/templates/api/src/_platform/fixtures/l1-test-user.sql +13 -0
- package/templates/api/src/_platform/manifest.yaml +1 -0
- package/templates/api/src/_platform/migrations/0013__users_profile.sql +4 -0
- package/templates/api/src/_platform/schema.yaml +12 -0
- package/templates/api/src/auth/_shared/session.ts +2 -2
- package/templates/api/src/demo/todos/api.ts +8 -9
- package/templates/api/src/notification/_shared/notify.ts +64 -26
- package/templates/api/src/notification/api.ts +1 -1
- package/templates/api/src/notification/manifest.yaml +1 -0
- package/templates/api/src/notification/migrations/0004__notifications_broadcast_hidden.sql +12 -0
- package/templates/api/src/notification/schema.yaml +7 -0
- package/templates/api/src/notification/seed.sql +5 -4
- package/templates/api/src/personal-center/profile/api.ts +63 -0
- package/templates/api/src/web/user-info/api.ts +3 -1
- package/templates/api/tests/fixture.ts +8 -16
- package/templates/api/tests/l1-builder.test.ts +91 -0
- package/templates/api/tests/lifecycle/installer.spec.ts +1 -4
- package/templates/api/tests/lifecycle/market-api.spec.ts +1 -3
- package/templates/api/tests/lifecycle/steps-fs.spec.ts +5 -8
- package/templates/api/tests/lifecycle/uninstall.spec.ts +28 -88
- package/templates/api/tests/query-utils.spec.ts +85 -1
- package/templates/api/tests/rbac-settings.spec.ts +64 -19
- package/templates/cdn/README.md +48 -0
- package/templates/global.d.ts +36 -2
- package/templates/init.include.yaml +3 -2
- package/templates/openapi.json +55 -10
- package/templates/web/src/notification/entry.ts +46 -9
- package/templates/web/src/notification/locales/en-US.json +19 -0
- package/templates/web/src/notification/locales/zh-CN.json +19 -0
- package/templates/web/src/notification/pages/center/index.tsx +131 -0
- package/templates/web/src/oj-market/entry.ts +8 -2
- package/templates/web/src/personal-center/__client/api.schemas.ts +9 -0
- package/templates/web/src/personal-center/__client/api.ts +25 -1
- package/templates/web/src/personal-center/entry.ts +5 -0
- package/templates/web/src/personal-center/pages/my-profile/index.tsx +25 -3
- package/templates/web/src/system/entry.ts +2 -1
- package/templates/web.config.ts +10 -7
- package/vendor/host-versions.json +3 -3
- package/templates/api/src/notifications/api.ts +0 -29
- package/templates/api/src/notifications/manifest.yaml +0 -5
- package/templates/web/src/notifications/__client/api.schemas.ts +0 -8
- package/templates/web/src/notifications/__client/api.ts +0 -80
package/shell-dist/index.html
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" href="/favicon.ico" />
|
|
7
7
|
<title>React Antd Admin · Shell</title>
|
|
8
|
-
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' https://modules.cdn.example.com 'wasm-unsafe-eval' 'nonce
|
|
8
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' https://modules.cdn.example.com 'wasm-unsafe-eval' 'nonce-+MhU9b9nCIgdxcVv98WPgQ=='; script-src-attr 'none'; connect-src 'self' data: https://modules.cdn.example.com; style-src 'self' https://modules.cdn.example.com 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-src https://ant.design https://react.dev https://condorheroblog.github.io; form-action 'none'; base-uri 'none'; object-src 'none'; upgrade-insecure-requests" />
|
|
9
9
|
|
|
10
|
-
<script type="importmap" nonce="
|
|
10
|
+
<script type="importmap" nonce="+MhU9b9nCIgdxcVv98WPgQ==">{
|
|
11
11
|
"imports": {
|
|
12
12
|
"react": "/assets/react.js",
|
|
13
13
|
"react/jsx-runtime": "/assets/jsx-runtime.js",
|
package/shell-dist/versions.json
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
"react-router": "7.18.2",
|
|
8
8
|
"react-router/dom": "7.18.2",
|
|
9
9
|
"@tanstack/react-query": "5.102.8",
|
|
10
|
-
"@oj-module/runtime": "0.2.
|
|
11
|
-
"@oj-module/runtime/contract": "0.2.
|
|
12
|
-
"@oj-module/runtime/contract/errors": "0.2.
|
|
10
|
+
"@oj-module/runtime": "0.2.15",
|
|
11
|
+
"@oj-module/runtime/contract": "0.2.15",
|
|
12
|
+
"@oj-module/runtime/contract/errors": "0.2.15",
|
|
13
13
|
"@plane/kit": "0.1.20",
|
|
14
14
|
"@plane/editor": "0.1.20",
|
|
15
15
|
"antd": "6.6.1",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// buildBackend spec:宿主 versions.json 双落点(方案安装的 C2-09 前置)。
|
|
2
|
+
//
|
|
3
|
+
// 安装器按 B-11 布局读 `<api>/dist/versions.json`(与 manifests.yaml 同层)做 peerRuntime
|
|
4
|
+
// 强制比对;而站点壳的 versions.json 落在 web/dist。两者不同源时,新工程 `ojm build`
|
|
5
|
+
// 完仍会在 preflight 被 C2-09 拒装——这条用例钉住「同源双写」,防止回归。
|
|
6
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
|
|
11
|
+
import { buildBackend } from "../build";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 假 oj 二进制:只需成功退出——本 spec 验的是 buildBackend 在 oj build 之后的收尾动作
|
|
15
|
+
* (versions.json 同源落地),oj build 自身的产物由用例预先放好模拟。
|
|
16
|
+
*/
|
|
17
|
+
function fakeOj(projectRoot: string): void {
|
|
18
|
+
mkdirSync(join(projectRoot, "bin"), { recursive: true });
|
|
19
|
+
const bin = join(projectRoot, "bin", "oj");
|
|
20
|
+
writeFileSync(bin, "#!/bin/sh\nexit 0\n");
|
|
21
|
+
chmodSync(bin, 0o755);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function newProject(): string {
|
|
25
|
+
const root = mkdtempSync(join(tmpdir(), "ojm-build-backend-"));
|
|
26
|
+
mkdirSync(join(root, "api", "src", "demo"), { recursive: true });
|
|
27
|
+
writeFileSync(join(root, "api", "src", "demo", "manifest.yaml"), "name: demo\nversion: 0.1.0\n", "utf-8");
|
|
28
|
+
writeFileSync(join(root, "api", "config.yaml"), "server:\n port: 9778\n", "utf-8");
|
|
29
|
+
// 模拟 oj build 已产出的 manifests.yaml(验证收尾动作不破坏既有产物)
|
|
30
|
+
mkdirSync(join(root, "api", "dist"), { recursive: true });
|
|
31
|
+
writeFileSync(join(root, "api", "dist", "manifests.yaml"), "demo: 0.1.0\n", "utf-8");
|
|
32
|
+
fakeOj(root);
|
|
33
|
+
return root;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("given 宿主工程有 api/src When ojm build 后端", () => {
|
|
37
|
+
it("then api/dist/versions.json 与 shell-dist 同源落地(安装器 C2-09 前置)", async () => {
|
|
38
|
+
const root = newProject();
|
|
39
|
+
|
|
40
|
+
await buildBackend(root);
|
|
41
|
+
|
|
42
|
+
const hostVersions = join(root, "api", "dist", "versions.json");
|
|
43
|
+
expect(existsSync(hostVersions)).toBe(true);
|
|
44
|
+
const parsed = JSON.parse(readFileSync(hostVersions, "utf-8")) as Record<string, string>;
|
|
45
|
+
expect(typeof parsed["@oj-module/runtime"]).toBe("string");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("then 既有 oj build 产物不被覆盖(manifests.yaml 原样保留)", async () => {
|
|
49
|
+
const root = newProject();
|
|
50
|
+
|
|
51
|
+
await buildBackend(root);
|
|
52
|
+
|
|
53
|
+
expect(readFileSync(join(root, "api", "dist", "manifests.yaml"), "utf-8")).toBe("demo: 0.1.0\n");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("given 纯前端工程(无 api/src)When ojm build 后端", () => {
|
|
58
|
+
it("then 跳过构建、不产出 api/dist", async () => {
|
|
59
|
+
const root = mkdtempSync(join(tmpdir(), "ojm-build-backend-fe-"));
|
|
60
|
+
|
|
61
|
+
expect(await buildBackend(root)).toBe(false);
|
|
62
|
+
expect(existsSync(join(root, "api", "dist"))).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { hostArtifactFreshnessError } from "../versions";
|
|
4
|
+
|
|
5
|
+
describe("hostArtifactFreshnessError(源码形态宿主产物过期守卫)", () => {
|
|
6
|
+
it("版本一致 → 通过", () => {
|
|
7
|
+
expect(hostArtifactFreshnessError("0.2.11", "0.2.11")).toBeUndefined();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("任一侧缺失 → 跳过守卫", () => {
|
|
11
|
+
expect(hostArtifactFreshnessError(undefined, "0.2.11")).toBeUndefined();
|
|
12
|
+
expect(hostArtifactFreshnessError("0.2.11", undefined)).toBeUndefined();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("宿主产物落后于 workspace runtime → 报真因并指向 build:shell", () => {
|
|
16
|
+
const err = hostArtifactFreshnessError("0.2.10", "0.2.11");
|
|
17
|
+
expect(err).toContain("shell-dist/versions.json)过期");
|
|
18
|
+
expect(err).toContain("0.2.10");
|
|
19
|
+
expect(err).toContain("0.2.11");
|
|
20
|
+
expect(err).toContain("build:shell");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("宿主产物领先(跨仓消费旧版 runtime)→ 同样拦截", () => {
|
|
24
|
+
expect(hostArtifactFreshnessError("0.2.12", "0.2.11")).toContain("过期");
|
|
25
|
+
});
|
|
26
|
+
});
|
package/src/build.ts
CHANGED
|
@@ -511,6 +511,15 @@ export async function buildBackend(projectRoot: string): Promise<boolean> {
|
|
|
511
511
|
// SyntaxError: Invalid left-hand side in assignment(端点 500)。
|
|
512
512
|
// 0.1.32 / 0.1.50 均复现。产物体积换正确性,等上游修好再撤这个旗标。
|
|
513
513
|
execFileSync(oj, ["build", ...configArgs, "-d", apiSrc, "-o", apiDist, "--no-minify"], { stdio: "inherit" });
|
|
514
|
+
// 宿主 versions.json 双落点:站点壳从 shell-dist 拷进 web/dist(mergeShellSite),
|
|
515
|
+
// 而方案安装器读的是 `<api>/dist/versions.json`(B-11:与 manifests.yaml 同层,用于
|
|
516
|
+
// peerRuntime 强制比对)。两处必须同源,否则新工程 `ojm build` 完仍装不上方案——
|
|
517
|
+
// preflight 直接 C2-09 fail-closed「无法读取宿主 runtime 版本」。
|
|
518
|
+
const versionsSrc = path.join(resolveShellDist(), "versions.json");
|
|
519
|
+
if (fs.existsSync(versionsSrc)) {
|
|
520
|
+
fs.mkdirSync(apiDist, { recursive: true });
|
|
521
|
+
fs.copyFileSync(versionsSrc, path.join(apiDist, "versions.json"));
|
|
522
|
+
}
|
|
514
523
|
return true;
|
|
515
524
|
}
|
|
516
525
|
|
package/src/bundle.ts
CHANGED
|
@@ -116,8 +116,14 @@ export async function buildBundle(projectRoot: string, opts: { out?: string } =
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
/**
|
|
120
|
-
|
|
119
|
+
/**
|
|
120
|
+
* C2-31 分层核对:模块级 manifest 仅 oj 原生键,且 name/version 与 bundle 级逐一相等
|
|
121
|
+
* (oj 启动强校验同源)。
|
|
122
|
+
*
|
|
123
|
+
* 变更(2026-10-21):`desc === summary` 的等值要求已移除(与安装侧 extract.ts 同步)——
|
|
124
|
+
* 那是构建期映射的产物而不是安全边界,保留它会让 desc 与 summary 一字之差的包直接装不上。
|
|
125
|
+
*/
|
|
126
|
+
function checkModuleManifest(built: string, name: string, version: string, _summary: string): string[] {
|
|
121
127
|
const p = path.join(built, "manifest.yaml");
|
|
122
128
|
let doc: unknown;
|
|
123
129
|
try {
|
|
@@ -137,9 +143,6 @@ function checkModuleManifest(built: string, name: string, version: string, summa
|
|
|
137
143
|
if (mm.version !== version) {
|
|
138
144
|
out.push(`模块级 version「${String(mm.version)}」必须 === bundle version「${version}」`);
|
|
139
145
|
}
|
|
140
|
-
if (mm.desc !== summary) {
|
|
141
|
-
out.push(`模块级 desc「${String(mm.desc)}」必须 === bundle summary「${summary}」(安装器写 desc 即取 summary)`);
|
|
142
|
-
}
|
|
143
146
|
return out;
|
|
144
147
|
}
|
|
145
148
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
// ScopedRequestLike 真源在 runtime contract(生成物 __client/api.ts 也从这里 import,
|
|
2
|
+
// 自身不再 re-export——旧写法 type import 自 __client 会在再生后 TS2305)。
|
|
3
|
+
import type { ScopedRequestLike } from "@oj-module/runtime/contract/errors";
|
|
2
4
|
|
|
3
5
|
// 集成回归:真实 httpMarketClient(统一后委托 ojm 生成 client)的委托行为。
|
|
4
6
|
// 经 bindRequest 注入 mock request,验证每条 MarketClient 方法 → 正确 URL/body/query + 返回形状;
|
|
@@ -43,9 +45,8 @@ function mockRequest(responder: (c: Call) => { status?: number, data?: unknown }
|
|
|
43
45
|
calls.push({ method, url, opts });
|
|
44
46
|
const r = responder({ method, url, opts });
|
|
45
47
|
if (r.status && r.status >= 400) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
throw err;
|
|
48
|
+
// ky 形态:HTTPError 携带 response.status(httpMarketClient.isUnconfigured 依赖它)
|
|
49
|
+
throw Object.assign(new Error(`HTTP ${r.status}`), { response: { status: r.status } });
|
|
49
50
|
}
|
|
50
51
|
return { json: () => Promise.resolve({ code: 0, data: r.data ?? null }) };
|
|
51
52
|
};
|
|
@@ -61,14 +62,12 @@ function mockRequest(responder: (c: Call) => { status?: number, data?: unknown }
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
// 让 index() 直连的 request 与生成 client 的 bindRequest 指向同一 mock,行为一致。
|
|
65
|
+
// 桥接:占位方法零参签名 ↔ ScopedRequestLike 真实签名,经 any 双向打通(测试桩,无运行时差异)。
|
|
64
66
|
function useMock(m: ReturnType<typeof mockRequest>) {
|
|
65
67
|
bindRequest(m.req);
|
|
66
|
-
|
|
67
|
-
requestImpl
|
|
68
|
-
|
|
69
|
-
requestImpl.delete = m.req.delete;
|
|
70
|
-
requestImpl.patch = m.req.patch;
|
|
71
|
-
requestImpl.head = m.req.head;
|
|
68
|
+
const req = m.req as unknown as Record<keyof typeof requestImpl, () => never>;
|
|
69
|
+
for (const k of Object.keys(requestImpl) as Array<keyof typeof requestImpl>)
|
|
70
|
+
requestImpl[k] = req[k];
|
|
72
71
|
}
|
|
73
72
|
|
|
74
73
|
describe("httpMarketClient · 统一委托生成 client 集成回归", () => {
|
|
@@ -101,6 +100,10 @@ describe("httpMarketClient · 统一委托生成 client 集成回归", () => {
|
|
|
101
100
|
useMock(mock);
|
|
102
101
|
const r = await httpMarketClient.index();
|
|
103
102
|
expect(r).toEqual([]);
|
|
103
|
+
// 503 是配置态而非故障:silentError 抑制全局 toast + retry:0 不重试(原
|
|
104
|
+
// market-client-unconfigured 用例的断言并入此处——工厂形态退役后唯一活口)
|
|
105
|
+
expect(mock.calls).toHaveLength(1);
|
|
106
|
+
expect(mock.calls[0].opts).toMatchObject({ silentError: true, retry: 0 });
|
|
104
107
|
});
|
|
105
108
|
|
|
106
109
|
it("install(input) → POST _platform/market/install,body 含 name/version/source", async () => {
|
package/src/index.ts
CHANGED
|
@@ -161,6 +161,12 @@ async function main() {
|
|
|
161
161
|
publishCommand(positionals[0] ?? "", { id, key, check });
|
|
162
162
|
break;
|
|
163
163
|
}
|
|
164
|
+
case "market": {
|
|
165
|
+
// 本地模拟 CDN 与市场链路(setup / publish / build-index)
|
|
166
|
+
const { runMarket } = await import("./market/command");
|
|
167
|
+
await runMarket(process.argv.slice(3));
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
164
170
|
default: {
|
|
165
171
|
if (command && OJ_PASSTHROUGH.has(command)) {
|
|
166
172
|
const { runOjSubcommand } = await import("./oj-cmd");
|
package/src/init.ts
CHANGED
|
@@ -98,6 +98,13 @@ export async function initProject(destDir: string, opts: InitOptions = {}): Prom
|
|
|
98
98
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
99
99
|
for (const name of ["SKILL.md", "api-manual.md"])
|
|
100
100
|
copyIfAbsent(path.join(devkit, name), path.join(skillDir, name), report);
|
|
101
|
+
// v0.1.82 起 devkit 新增两份专题指南(api-manual/SKILL 均引用它们):
|
|
102
|
+
// SQL 构造器能力矩阵/方言差异、oj test L1 测试指南。它们是**增量文档**、
|
|
103
|
+
// 不影响 ojm 运行,故「有则拷、无则记跳过」——bin/devkit 来自用户本地
|
|
104
|
+
// oj 版本(MIN_OJ_VERSION 只钉到 0.1.73),旧版 devkit 没有这两个文件,
|
|
105
|
+
// 为两份文档把整个 init 打挂不值当(用户升 oj 后重跑 init 即补上)。
|
|
106
|
+
for (const name of ["sql-builder-guide.md", "oj-test-guide.md"])
|
|
107
|
+
copyIfPresent(path.join(devkit, name), path.join(skillDir, name), report);
|
|
101
108
|
copyIfAbsent(path.join(devkit, "global.d.ts"), path.join(destDir, "global.d.ts"), report);
|
|
102
109
|
}
|
|
103
110
|
else {
|
|
@@ -367,6 +374,21 @@ function copyIfAbsent(src: string, dest: string, report: InitReport): void {
|
|
|
367
374
|
report.created.push(path.relative(process.cwd(), dest) || path.basename(dest));
|
|
368
375
|
}
|
|
369
376
|
|
|
377
|
+
/**
|
|
378
|
+
* 同 copyIfAbsent,但**源缺失**时记一条跳过而非抛错。
|
|
379
|
+
*
|
|
380
|
+
* 用于从 bin/devkit 拷第三方版本化内容(两份专题指南):devkit 随用户本地 oj
|
|
381
|
+
* 版本而变,旧版没有新文件属正常版本 skew,不是安装包损坏——缺了只少份文档,
|
|
382
|
+
* 不该让 init 失败。源在而目标已存在仍走 copyIfAbsent 的幂等跳过。
|
|
383
|
+
*/
|
|
384
|
+
function copyIfPresent(src: string, dest: string, report: InitReport): void {
|
|
385
|
+
if (!fs.existsSync(src)) {
|
|
386
|
+
report.skipped.push(`${path.basename(src)}(bin/devkit 无此文件,跳过)`);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
copyIfAbsent(src, dest, report);
|
|
390
|
+
}
|
|
391
|
+
|
|
370
392
|
/**
|
|
371
393
|
* 新工程 .gitignore:不存在则生成;已存在则幂等跳过(用户规则永不覆盖)。
|
|
372
394
|
*
|
|
@@ -33,7 +33,7 @@ afterEach(() => {
|
|
|
33
33
|
|
|
34
34
|
/** 预置「已构建」产物:oj build(api/dist/<name>-<version>/)与站点构建(web/dist/modules/) */
|
|
35
35
|
function makePrebuiltArtifacts(): void {
|
|
36
|
-
// oj build 产物(模块级 manifest 仅 oj 原生七键;name/version
|
|
36
|
+
// oj build 产物(模块级 manifest 仅 oj 原生七键;name/version 与 bundle 级逐一相等,C2-31)
|
|
37
37
|
const apiDist = join(project, "api/dist/demo-solution-0.1.0");
|
|
38
38
|
mkdirSync(join(apiDist, "ping"), { recursive: true });
|
|
39
39
|
mkdirSync(join(apiDist, "migrations"), { recursive: true });
|
|
@@ -101,9 +101,9 @@ describe("buildBundle · 校验先于打包(fail-fast)", () => {
|
|
|
101
101
|
await expect(pack()).rejects.toThrow(/manifest\.yaml/);
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
-
it("c2-31 分层核对:模块级 desc ≠ bundle summary →
|
|
104
|
+
it("c2-31 分层核对:模块级 desc ≠ bundle summary → 放行(等值要求已移除)", async () => {
|
|
105
105
|
writeFileSync(join(project, "api/dist/demo-solution-0.1.0/manifest.yaml"), "name: demo-solution\ndesc: 不一致描述\nversion: 0.1.0\n");
|
|
106
|
-
await expect(pack()).
|
|
106
|
+
await expect(pack()).resolves.toMatchObject({ name: "demo-solution" });
|
|
107
107
|
});
|
|
108
108
|
|
|
109
109
|
it("c2-31 分层核对:api/dist 缺 <name>-<version>/ → 可读报错指明期望产物路径", async () => {
|
|
@@ -14,7 +14,7 @@ import { parse } from "yaml";
|
|
|
14
14
|
// 跨端引用(双端一致钉):被测实现同仓分居两运行时(计划 §1「两运行时取舍」),
|
|
15
15
|
// 测试直接互调,比「两套件各读同一夹具」更强——漂移即本文件红。
|
|
16
16
|
import { preflight } from "../../../templates/api/src/_platform/_lifecycle/steps/preflight";
|
|
17
|
-
import { checkLocalesComplete, checkTablesAgainstSchema, parseManifest, validateContract } from "../../manifest-contract";
|
|
17
|
+
import { checkLocalesComplete, checkTablesAgainstSchema, groupI18nKeys, parseManifest, validateContract } from "../../manifest-contract";
|
|
18
18
|
|
|
19
19
|
const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), "fixtures");
|
|
20
20
|
const readText = (p: string) => readFileSync(join(FIXTURES, p), "utf8");
|
|
@@ -214,6 +214,75 @@ describe("checkLocalesComplete(C2-25:构建期缺 key fail-fast)", () => {
|
|
|
214
214
|
});
|
|
215
215
|
});
|
|
216
216
|
|
|
217
|
+
// ── C2-43 归组父标题 i18n(solution menu grouping;纯内容入参)──
|
|
218
|
+
|
|
219
|
+
describe("c2-43 归组父标题 i18n(solution menu grouping)", () => {
|
|
220
|
+
const baseManifest = (): any => ({
|
|
221
|
+
name: "demo-solution",
|
|
222
|
+
version: "0.1.0",
|
|
223
|
+
routes: [
|
|
224
|
+
{ path: "/demo-solution", category: "main", order: 200, title_key: "demo-solution:menu.main" },
|
|
225
|
+
],
|
|
226
|
+
});
|
|
227
|
+
const ZH_FULL = JSON.stringify({ menu: { main: "主", group: "分组" } });
|
|
228
|
+
const EN_FULL = JSON.stringify({ menu: { main: "Main", group: "Group" } });
|
|
229
|
+
|
|
230
|
+
it("groupI18nKeys:声明 main/admin 路由才产一条 <name>:menu.group", () => {
|
|
231
|
+
expect(groupI18nKeys(baseManifest())).toEqual(["demo-solution:menu.group"]);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("groupI18nKeys:多条 main 路由去重只产一条(C5)", () => {
|
|
235
|
+
const m = baseManifest();
|
|
236
|
+
m.routes = [
|
|
237
|
+
{ path: "/a", category: "main", order: 200, title_key: "demo-solution:menu.a" },
|
|
238
|
+
{ path: "/b", category: "main", order: 210, title_key: "demo-solution:menu.b" },
|
|
239
|
+
];
|
|
240
|
+
expect(groupI18nKeys(m)).toEqual(["demo-solution:menu.group"]);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("groupI18nKeys:仅 hidden 路由不产 group key(C3)", () => {
|
|
244
|
+
const m = baseManifest();
|
|
245
|
+
m.routes = [{ path: "/x", category: "hidden", order: 210, title_key: "demo-solution:menu.x" }];
|
|
246
|
+
expect(groupI18nKeys(m)).toEqual([]);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("c1 正例:menu.group 双语齐备 → 零违规", () => {
|
|
250
|
+
expect(checkLocalesComplete(baseManifest(), ZH_FULL, EN_FULL)).toEqual([]);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("c2 反例:zh-CN 缺 menu.group → C2-43(非 C2-25,去重生效)", () => {
|
|
254
|
+
const zh = JSON.stringify({ menu: { main: "主" } });
|
|
255
|
+
const v = checkLocalesComplete(baseManifest(), zh, EN_FULL);
|
|
256
|
+
const hit = v.find(x => x.includes("C2-43"));
|
|
257
|
+
expect(hit, `期望 C2-43,实际:\n${v.join("\n")}`).toBeTruthy();
|
|
258
|
+
expect(hit).not.toContain("C2-25");
|
|
259
|
+
expect(hit).toContain("menu.group");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("c4 反例:en-US 缺 menu.group → C2-43 指明 en-US", () => {
|
|
263
|
+
const en = JSON.stringify({ menu: { main: "Main" } });
|
|
264
|
+
const v = checkLocalesComplete(baseManifest(), ZH_FULL, en);
|
|
265
|
+
const hit = v.find(x => x.includes("C2-43"));
|
|
266
|
+
expect(hit).toBeTruthy();
|
|
267
|
+
expect(hit).toContain("en-US");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("去重:多条 main 路由只产一条 group key,缺失时按语言各报一次(非按路由重复)", () => {
|
|
271
|
+
const m = baseManifest();
|
|
272
|
+
m.routes = [
|
|
273
|
+
{ path: "/a", category: "main", order: 200, title_key: "demo-solution:menu.a" },
|
|
274
|
+
{ path: "/b", category: "main", order: 210, title_key: "demo-solution:menu.b" },
|
|
275
|
+
];
|
|
276
|
+
const v = checkLocalesComplete(m, JSON.stringify({ menu: { main: "主" } }), JSON.stringify({ menu: { main: "Main" } }));
|
|
277
|
+
const c243 = v.filter(x => x.includes("C2-43"));
|
|
278
|
+
// 关键:2 条路由只产 1 个 group key → 缺失时按语言各报 1 次 = 2 条 C2-43,
|
|
279
|
+
// 而非 2 路由 × 2 语言 = 4 条(去重生效的判据是「< 路由数 × 语言数」)
|
|
280
|
+
expect(c243.length).toBe(2);
|
|
281
|
+
expect(c243.some(x => x.includes("zh-CN 缺少归组父标题 key「demo-solution:menu.group」"))).toBe(true);
|
|
282
|
+
expect(c243.some(x => x.includes("en-US 缺少归组父标题 key「demo-solution:menu.group」"))).toBe(true);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
217
286
|
// ── isolate 侧 preflight 复用助手(与 templates/api/tests/lifecycle/preflight.spec.ts 同构)──
|
|
218
287
|
|
|
219
288
|
function makeCtx(manifest: unknown): InstallContext {
|
package/src/manifest-contract.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// bundle 级 manifest.yaml 契约校验(P1-11,`ojm build --bundle` 构建期执行点)。
|
|
2
|
-
// 规则真源:docs/prd/solution/202610171254-lifecycle-manifest-contract.md §2/§4(C2-01~33
|
|
2
|
+
// 规则真源:docs/prd/solution/202610171254-lifecycle-manifest-contract.md §2/§4(C2-01~33 构建列;C2-34~42 为归档签名与信任,见 sign.ts;本文件新增 C2-43);
|
|
3
3
|
// 期望真源:src/lifecycle/__tests__/fixtures/(fixture-valid 零违规 / fixture-invalid 七码齐报)。
|
|
4
4
|
// 双端一致:本实现与 isolate 侧 preflight(templates/api/.../steps/preflight.ts)共享同一组夹具,
|
|
5
5
|
// 正则与规则**逐字一致**(§4.1 矩阵构建列 vs 安装列的执行点差异见各规则注释)——
|
|
@@ -581,6 +581,15 @@ export function validateContract(m: unknown): string[] {
|
|
|
581
581
|
return v0;
|
|
582
582
|
}
|
|
583
583
|
|
|
584
|
+
/** C2-43 引用面:声明 main/admin 路由的方案须提供归组父标题 `<name>:menu.group`(单 bundle 单 name,去重只产一条) */
|
|
585
|
+
export function groupI18nKeys(m: BundleManifest): string[] {
|
|
586
|
+
if (!isObj(m) || typeof m.name !== "string")
|
|
587
|
+
return [];
|
|
588
|
+
const routes = Array.isArray(m.routes) ? m.routes : [];
|
|
589
|
+
const hasGroupable = routes.some(r => isObj(r) && (r.category === "main" || r.category === "admin"));
|
|
590
|
+
return hasGroupable ? [`${m.name}:menu.group`] : [];
|
|
591
|
+
}
|
|
592
|
+
|
|
584
593
|
/** 收集 manifest 引用的全部 i18n key(C2-25 locales 齐备检查的引用面;bundle.ts 以此判定 locales 文件是否必查) */
|
|
585
594
|
export function referencedI18nKeys(m: BundleManifest): string[] {
|
|
586
595
|
const keys: string[] = [];
|
|
@@ -611,6 +620,10 @@ export function referencedI18nKeys(m: BundleManifest): string[] {
|
|
|
611
620
|
}
|
|
612
621
|
}
|
|
613
622
|
}
|
|
623
|
+
// C2-43:归组父标题纳入引用面,交由 checkLocalesComplete 双语齐备(改报 C2-43)
|
|
624
|
+
for (const k of groupI18nKeys(m)) {
|
|
625
|
+
keys.push(k);
|
|
626
|
+
}
|
|
614
627
|
return keys;
|
|
615
628
|
}
|
|
616
629
|
|
|
@@ -633,9 +646,12 @@ export function checkLocalesComplete(m: unknown, zhText: string, enText: string)
|
|
|
633
646
|
if (!isObj(m))
|
|
634
647
|
return [v("C2-25", "manifest 不是键值对象,无法核对 locales 引用")];
|
|
635
648
|
const locales: [string, Record<string, unknown> | null][] = [["zh-CN", parseJson(zhText)], ["en-US", parseJson(enText)]];
|
|
649
|
+
// group key 识别(C2-43):归组父标题缺失须归因 C2-43 而非笼统 C2-25,避免重复报错
|
|
650
|
+
const groupKeys = new Set(groupI18nKeys(m));
|
|
636
651
|
const out: string[] = [];
|
|
637
652
|
for (const key of referencedI18nKeys(m)) {
|
|
638
653
|
const nsKey = key.includes(":") ? key.slice(key.indexOf(":") + 1) : key;
|
|
654
|
+
const isGroup = groupKeys.has(key);
|
|
639
655
|
for (const [lang, tree] of locales) {
|
|
640
656
|
if (tree === null) {
|
|
641
657
|
out.push(v("C2-25", `${lang} locale 不是合法 JSON 对象,无法核对引用 key`));
|
|
@@ -643,7 +659,14 @@ export function checkLocalesComplete(m: unknown, zhText: string, enText: string)
|
|
|
643
659
|
}
|
|
644
660
|
const val = lookupKey(tree, nsKey);
|
|
645
661
|
if (typeof val !== "string" || val === "") {
|
|
646
|
-
out.push(
|
|
662
|
+
out.push(
|
|
663
|
+
v(
|
|
664
|
+
isGroup ? "C2-43" : "C2-25",
|
|
665
|
+
isGroup
|
|
666
|
+
? `${lang} 缺少归组父标题 key「${key}」(须为嵌套 menu.group,C2-43;声明 main/admin 路由即须提供)`
|
|
667
|
+
: `${lang} 缺少 manifest 引用的 key「${key}」(双语齐备,D4;构建期 fail-fast)`,
|
|
668
|
+
),
|
|
669
|
+
);
|
|
647
670
|
}
|
|
648
671
|
}
|
|
649
672
|
}
|