@oj-module/cli 0.2.20 → 0.2.25
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/manual.md +100 -12
- package/devkit/module-dev-and-cdn-playground.md +10 -10
- package/package.json +4 -4
- package/shell-dist/assets/runtime.js +61 -35
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/build-backend.test.ts +13 -13
- package/src/__tests__/cli.test.ts +225 -0
- package/src/__tests__/init-packagejson.test.ts +29 -0
- package/src/__tests__/layout.test.ts +8 -5
- package/src/__tests__/new-scaffold.test.ts +59 -0
- package/src/__tests__/usage.test.ts +45 -47
- package/src/build.ts +4 -4
- package/src/builtin.ts +93 -0
- package/src/bundle.ts +237 -33
- package/src/cli.ts +265 -0
- package/src/contract/__tests__/market-client.integration.test.ts +12 -12
- package/src/contract/check.ts +4 -4
- package/src/dev.ts +75 -2
- package/src/index.ts +31 -191
- package/src/info.ts +49 -7
- package/src/init.ts +25 -11
- package/src/keygen.ts +0 -19
- package/src/layout.ts +11 -11
- package/src/lifecycle/__tests__/authoring.test.ts +3 -14
- package/src/lifecycle/__tests__/builtin.test.ts +97 -0
- package/src/lifecycle/__tests__/bundle.test.ts +188 -25
- package/src/lifecycle/__tests__/install.test.ts +0 -23
- package/src/lifecycle/__tests__/new.test.ts +129 -0
- package/src/lifecycle/__tests__/smoke.test.ts +4 -3
- package/src/manifest-contract.ts +16 -3
- package/src/market/__tests__/bootstrap.test.ts +24 -0
- package/src/market/__tests__/setup.test.ts +8 -1
- package/src/market/bootstrap.ts +10 -8
- package/src/market/cdn-layout.ts +3 -3
- package/src/market/command.ts +0 -23
- package/src/market/publish.ts +10 -2
- package/src/market/setup.ts +14 -2
- package/src/market/trial.ts +3 -3
- package/src/market/walkthrough.ts +7 -6
- package/src/new.ts +226 -0
- package/src/preview-config.ts +2 -2
- package/src/preview.ts +5 -4
- package/src/publish.ts +0 -22
- package/src/sign.ts +0 -22
- package/src/usage.ts +482 -198
- package/templates/README.md +3 -3
- package/templates/cdn/README.md +6 -6
- package/templates/config.yaml +5 -4
- package/templates/init.include.yaml +1 -1
- package/templates/package.json +3 -1
- package/templates/seed.mysql.sql +2 -1
- package/templates/seed.pg.sql +2 -1
- package/templates/src/_platform/api/fixtures/l1-test-user.sql +4 -5
- package/templates/src/_platform/api/market/api.ts +11 -8
- package/templates/src/_platform/api/seed.mysql.sql +11 -3
- package/templates/src/_platform/api/seed.pg.sql +11 -3
- package/templates/src/_platform/api/seed.sql +18 -6
- package/templates/src/_platform/api/solutions/api.ts +2 -2
- package/templates/src/oj-market/web/api/context.ts +2 -2
- package/templates/src/oj-market/web/components/InstallDialog.tsx +13 -6
- package/templates/src/oj-market/web/components/RollbackDialog.tsx +7 -3
- package/templates/src/oj-market/web/components/UninstallConfirm.tsx +18 -7
- package/templates/src/oj-market/web/hooks/useMarket.ts +30 -18
- package/templates/src/system/web/locales/en-US.json +1 -1
- package/templates/src/system/web/locales/zh-CN.json +1 -1
- package/templates/src/system/web/pages/users/roles.tsx +3 -1
- package/templates/tests/l1-builder.test.ts +2 -2
- package/templates/tests/lifecycle/market-api.spec.ts +35 -5
- package/templates/tests/lifecycle/verify-checks.spec.ts +1 -1
- package/templates/tests/lifecycle/verify-report.spec.ts +1 -1
- package/templates/tests/rbac-seed.spec.ts +12 -10
- package/templates/tests/seed-role-accounts.spec.ts +87 -0
- package/vendor/host-versions.json +3 -3
- package/src/args.ts +0 -122
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.25",
|
|
11
|
+
"@oj-module/runtime/contract": "0.2.25",
|
|
12
|
+
"@oj-module/runtime/contract/errors": "0.2.25",
|
|
13
13
|
"@plane/kit": "0.1.20",
|
|
14
14
|
"@plane/editor": "0.1.20",
|
|
15
15
|
"antd": "6.6.1",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// buildBackend spec:宿主 versions.json 双落点(方案安装的 C2-09 前置)。
|
|
2
2
|
//
|
|
3
|
-
// 安装器按 B-11 布局读
|
|
4
|
-
// 强制比对;而站点壳的 versions.json 落在 web
|
|
3
|
+
// 安装器按 B-11 布局读 `dist/api/versions.json`(与 manifests.yaml 同层)做 peerRuntime
|
|
4
|
+
// 强制比对;而站点壳的 versions.json 落在 dist/web。两者不同源时,新工程 `ojm build`
|
|
5
5
|
// 完仍会在 preflight 被 C2-09 拒装——这条用例钉住「同源双写」,防止回归。
|
|
6
6
|
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
@@ -27,19 +27,19 @@ function newProject(): string {
|
|
|
27
27
|
writeFileSync(join(root, "api", "src", "demo", "manifest.yaml"), "name: demo\nversion: 0.1.0\n", "utf-8");
|
|
28
28
|
writeFileSync(join(root, "api", "config.yaml"), "server:\n port: 9778\n", "utf-8");
|
|
29
29
|
// 模拟 oj build 已产出的 manifests.yaml(验证收尾动作不破坏既有产物)
|
|
30
|
-
mkdirSync(join(root, "
|
|
31
|
-
writeFileSync(join(root, "
|
|
30
|
+
mkdirSync(join(root, "dist", "api"), { recursive: true });
|
|
31
|
+
writeFileSync(join(root, "dist", "api", "manifests.yaml"), "demo: 0.1.0\n", "utf-8");
|
|
32
32
|
fakeOj(root);
|
|
33
33
|
return root;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
describe("given 宿主工程有 api/src When ojm build 后端", () => {
|
|
37
|
-
it("then api/
|
|
37
|
+
it("then dist/api/versions.json 与 shell-dist 同源落地(安装器 C2-09 前置)", async () => {
|
|
38
38
|
const root = newProject();
|
|
39
39
|
|
|
40
40
|
await buildBackend(root);
|
|
41
41
|
|
|
42
|
-
const hostVersions = join(root, "
|
|
42
|
+
const hostVersions = join(root, "dist", "api", "versions.json");
|
|
43
43
|
expect(existsSync(hostVersions)).toBe(true);
|
|
44
44
|
const parsed = JSON.parse(readFileSync(hostVersions, "utf-8")) as Record<string, string>;
|
|
45
45
|
expect(typeof parsed["@oj-module/runtime"]).toBe("string");
|
|
@@ -50,16 +50,16 @@ describe("given 宿主工程有 api/src When ojm build 后端", () => {
|
|
|
50
50
|
|
|
51
51
|
await buildBackend(root);
|
|
52
52
|
|
|
53
|
-
expect(readFileSync(join(root, "
|
|
53
|
+
expect(readFileSync(join(root, "dist", "api", "manifests.yaml"), "utf-8")).toBe("demo: 0.1.0\n");
|
|
54
54
|
});
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
describe("given 纯前端工程(无 api/src)When ojm build 后端", () => {
|
|
58
|
-
it("then 跳过构建、不产出 api
|
|
58
|
+
it("then 跳过构建、不产出 dist/api", async () => {
|
|
59
59
|
const root = mkdtempSync(join(tmpdir(), "ojm-build-backend-fe-"));
|
|
60
60
|
|
|
61
61
|
expect(await buildBackend(root)).toBe(false);
|
|
62
|
-
expect(existsSync(join(root, "
|
|
62
|
+
expect(existsSync(join(root, "dist", "api"))).toBe(false);
|
|
63
63
|
});
|
|
64
64
|
});
|
|
65
65
|
|
|
@@ -93,7 +93,7 @@ function newModuleRootProject(): string {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
describe("given module-root 布局 When ojm build 后端", () => {
|
|
96
|
-
it("then 一次调用、单一输出根:--module-root src + -o api
|
|
96
|
+
it("then 一次调用、单一输出根:--module-root src + -o dist/api(与 -d 互斥)", async () => {
|
|
97
97
|
const root = newModuleRootProject();
|
|
98
98
|
fakeOjRecording(root, true);
|
|
99
99
|
|
|
@@ -102,9 +102,9 @@ describe("given module-root 布局 When ojm build 后端", () => {
|
|
|
102
102
|
// 逐模块 `-o <tmp>` 会让每个 tmp 各写一份 manifests.yaml,锁与兄弟目录关系全丢
|
|
103
103
|
const invoked = readFileSync(join(root, "bin", "invoked"), "utf-8").trim();
|
|
104
104
|
expect(invoked).toBe(
|
|
105
|
-
`build -c ${join(root, "config.yaml")} --module-root ${join(root, "src")} -o ${join(root, "
|
|
105
|
+
`build -c ${join(root, "config.yaml")} --module-root ${join(root, "src")} -o ${join(root, "dist", "api")} --no-minify`,
|
|
106
106
|
);
|
|
107
|
-
expect(existsSync(join(root, "
|
|
107
|
+
expect(existsSync(join(root, "dist", "api", "versions.json"))).toBe(true);
|
|
108
108
|
});
|
|
109
109
|
|
|
110
110
|
it("then oj 缺 --module-root 能力时 fail-fast,不落任何产物", async () => {
|
|
@@ -113,6 +113,6 @@ describe("given module-root 布局 When ojm build 后端", () => {
|
|
|
113
113
|
|
|
114
114
|
await expect(buildBackend(root)).rejects.toThrow(/不支持 --module-root/);
|
|
115
115
|
expect(existsSync(join(root, "bin", "invoked"))).toBe(false); // 没跑 oj build
|
|
116
|
-
expect(existsSync(join(root, "
|
|
116
|
+
expect(existsSync(join(root, "dist", "api"))).toBe(false);
|
|
117
117
|
});
|
|
118
118
|
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { Lang } from "../usage";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { buildCli, rewriteHelpArgv } from "../cli";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* dispatch 契约(cac 驱动后):
|
|
10
|
+
* - 帮助请求不执行 action(market setup --help 不建 CDN)
|
|
11
|
+
* - 未知命令触发 command:*(index.ts 兜底打帮助 + exit 1)
|
|
12
|
+
* - 校验从 parse*Args 搬入 action 后行为保持(new --layout / vendor tag)
|
|
13
|
+
* - 位置参数与 variadic 映射(merge out in1 in2)
|
|
14
|
+
* - --bundle 值映射到 buildBundle
|
|
15
|
+
* - help 词形重写(ojm help <cmd> → <cmd> --help)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const logs: string[] = [];
|
|
19
|
+
let exitSpy: ReturnType<typeof vi.fn>;
|
|
20
|
+
|
|
21
|
+
// 提升式 mock:vitest 测试体内注册的 vi.mock 拦不到 cli.ts 的 dynamic import,须 hoisted 顶层注册
|
|
22
|
+
const buildBundleMock = vi.hoisted(() => vi.fn(async () => [{ file: "x.tgz" }]));
|
|
23
|
+
vi.mock("../bundle", () => ({ buildBundle: buildBundleMock }));
|
|
24
|
+
const vendorCommandMock = vi.hoisted(() => vi.fn(async () => {}));
|
|
25
|
+
vi.mock("../vendor", () => ({ vendorCommand: vendorCommandMock }));
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
logs.length = 0;
|
|
29
|
+
exitSpy = vi.fn();
|
|
30
|
+
vi.spyOn(console, "log").mockImplementation((...args: unknown[]) => logs.push(args.join(" ")));
|
|
31
|
+
vi.spyOn(console, "error").mockImplementation((...args: unknown[]) => logs.push(args.join(" ")));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
vi.restoreAllMocks();
|
|
36
|
+
buildBundleMock.mockClear();
|
|
37
|
+
vendorCommandMock.mockClear();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
interface RunResult { out: string, unknown: string[], exitCode: number | undefined }
|
|
41
|
+
|
|
42
|
+
/** 跑一次 parse:捕获输出、command:* 与 exit;动作异步链经条件轮询拍平(vitest 下 dynamic import 解析需数百毫秒) */
|
|
43
|
+
async function run(argv: string[], lang: Lang, cwd: string, until?: () => boolean): Promise<RunResult> {
|
|
44
|
+
const cli = buildCli(cwd, lang, { exit: exitSpy as unknown as (code: number) => never });
|
|
45
|
+
const unknown: string[] = [];
|
|
46
|
+
cli.on("command:*", () => unknown.push(String(cli.args[0])));
|
|
47
|
+
cli.parse(["node", "ojm", ...argv], { run: true });
|
|
48
|
+
try {
|
|
49
|
+
await vi.waitUntil(() => logs.length > 0 || exitSpy.mock.calls.length > 0 || unknown.length > 0 || !!until?.(), { timeout: 2500, interval: 10 });
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// 无输出型动作(如帮助打屏后无日志):兜底继续
|
|
53
|
+
}
|
|
54
|
+
await new Promise(r => setTimeout(r, 50));
|
|
55
|
+
return { out: logs.join("\n"), unknown, exitCode: exitSpy.mock.calls[0]?.[0] as number | undefined };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let tmp: string;
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), "ojm-cli-test-"));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("帮助不执行 action", () => {
|
|
64
|
+
it("子命令 --help 只出单命令帮助(不叠顶层分组帮助)", async () => {
|
|
65
|
+
// 回归:cac --help 后 unsetMatchedCommand 会让 args[0]=子命令,误叠顶层帮助(index.ts 已按 options.help 过滤)
|
|
66
|
+
const r = await run(["market", "verify", "--help"], "zh-CN", tmp);
|
|
67
|
+
expect(r.out).toContain("离线验包");
|
|
68
|
+
expect(r.out).not.toContain("oj 子命令透传");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("market setup --help 打帮助、不触发 setup、不 exit", async () => {
|
|
72
|
+
const r = await run(["market", "setup", "--help"], "zh-CN", tmp);
|
|
73
|
+
expect(r.out).toContain("market");
|
|
74
|
+
expect(r.out).not.toContain("[ojm] 站点");
|
|
75
|
+
expect(r.exitCode).toBeUndefined();
|
|
76
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("单命令帮助(双语)", () => {
|
|
81
|
+
it("build --help 中文:语法 + 选项 + 详解", async () => {
|
|
82
|
+
const r = await run(["build", "--help"], "zh-CN", tmp);
|
|
83
|
+
expect(r.out).toContain("ojm build");
|
|
84
|
+
expect(r.out).toContain("--bundle");
|
|
85
|
+
expect(r.out).toContain("构建后端");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("build --help 英文", async () => {
|
|
89
|
+
const r = await run(["build", "--help"], "en-US", tmp);
|
|
90
|
+
expect(r.out).toContain("Builds the backend");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("market verify --help 按子命令注入详解(中/英)", async () => {
|
|
94
|
+
expect((await run(["market", "verify", "--help"], "zh-CN", tmp)).out).toContain("离线验包");
|
|
95
|
+
expect((await run(["market", "verify", "--help"], "en-US", tmp)).out).toMatch(/offline/i);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("trust add --help 按子命令注入详解(中/英)", async () => {
|
|
99
|
+
expect((await run(["trust", "add", "--help"], "zh-CN", tmp)).out).toContain("新增信任");
|
|
100
|
+
expect((await run(["trust", "add", "--help"], "en-US", tmp)).out).toContain("64-hex public key");
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("未知命令与兜底", () => {
|
|
105
|
+
it("未知命令触发 command:*", async () => {
|
|
106
|
+
const r = await run(["nope"], "zh-CN", tmp);
|
|
107
|
+
expect(r.unknown).toEqual(["nope"]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("反常识行为修正(20261023-0937)", () => {
|
|
112
|
+
it("isKnownCommand:已知命令 true / 未知 false", async () => {
|
|
113
|
+
const { isKnownCommand } = await import("../usage");
|
|
114
|
+
expect(isKnownCommand("market")).toBe(true);
|
|
115
|
+
expect(isKnownCommand("trust")).toBe(true);
|
|
116
|
+
expect(isKnownCommand("nope")).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("unknownCommandResult:help 形态报未知命令(双语),code 1", async () => {
|
|
120
|
+
const { unknownCommandResult } = await import("../cli");
|
|
121
|
+
const zh = unknownCommandResult("nope", "zh-CN", true);
|
|
122
|
+
expect(zh.text).toContain("[ojm] 未知命令:nope");
|
|
123
|
+
expect(zh.code).toBe(1);
|
|
124
|
+
const en = unknownCommandResult("nope", "en-US", true);
|
|
125
|
+
expect(en.text).toContain("[ojm] Unknown command: nope");
|
|
126
|
+
expect(en.code).toBe(1);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("unknownCommandResult:非 help 形态给顶层帮助,code 1", async () => {
|
|
130
|
+
const { unknownCommandResult } = await import("../cli");
|
|
131
|
+
const { usageText } = await import("../usage");
|
|
132
|
+
const r = unknownCommandResult("nope", "zh-CN", false);
|
|
133
|
+
expect(r.text).toBe(usageText("zh-CN"));
|
|
134
|
+
expect(r.code).toBe(1);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("localizeCacError:未知选项本地化(双语)+ 指引", async () => {
|
|
138
|
+
const { localizeCacError } = await import("../cli");
|
|
139
|
+
const zh = localizeCacError(new Error("Unknown option `--foo`"), "zh-CN");
|
|
140
|
+
expect(zh).toBeInstanceOf(Error);
|
|
141
|
+
expect((zh as Error).message).toContain("未知选项:--foo");
|
|
142
|
+
expect((zh as Error).message).toContain("--help");
|
|
143
|
+
const en = localizeCacError(new Error("Unknown option `--foo`"), "en-US");
|
|
144
|
+
expect((en as Error).message).toContain("Unknown option: --foo");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("localizeCacError:非匹配错误原样返回(同一实例)", async () => {
|
|
148
|
+
const { localizeCacError } = await import("../cli");
|
|
149
|
+
const err = new Error("[ojm] 别的错误");
|
|
150
|
+
expect(localizeCacError(err, "zh-CN")).toBe(err);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("校验搬入 action 后行为保持", () => {
|
|
155
|
+
it("new 多余位置参数显式拒绝(F8)", async () => {
|
|
156
|
+
const r = await run(["new", "crm", "erp"], "zh-CN", tmp);
|
|
157
|
+
expect(r.out).toContain("只接受一个方案名");
|
|
158
|
+
expect(r.exitCode).toBe(1);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("new --layout 非 container|none 报错", async () => {
|
|
162
|
+
const r = await run(["new", "demo", "--layout", "bogus"], "zh-CN", tmp);
|
|
163
|
+
expect(r.out).toContain("--layout 只接受 container | none");
|
|
164
|
+
expect(r.exitCode).toBe(1);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("new 缺 name 打印用法并 exit 1", async () => {
|
|
168
|
+
const r = await run(["new"], "zh-CN", tmp);
|
|
169
|
+
expect(r.out).toContain("ojm new <name>");
|
|
170
|
+
expect(r.exitCode).toBe(1);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("vendor 非法 tag 报错", async () => {
|
|
174
|
+
const r = await run(["vendor", "notatag"], "zh-CN", tmp);
|
|
175
|
+
expect(r.out).toContain("非法版本号");
|
|
176
|
+
expect(r.exitCode).toBe(1);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("vendor 0.1.12 规范化补 v 前缀后传入 vendorCommand", async () => {
|
|
180
|
+
await run(["vendor", "0.1.12"], "zh-CN", tmp, () => vendorCommandMock.mock.calls.length > 0);
|
|
181
|
+
expect(vendorCommandMock).toHaveBeenCalledWith(tmp, { force: false, tag: "v0.1.12" });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("vendor --force 透传 force", async () => {
|
|
185
|
+
await run(["vendor", "v0.1.12", "--force"], "zh-CN", tmp, () => vendorCommandMock.mock.calls.length > 0);
|
|
186
|
+
expect(vendorCommandMock).toHaveBeenCalledWith(tmp, { force: true, tag: "v0.1.12" });
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("参数映射", () => {
|
|
191
|
+
it("merge <out> <inputs...> variadic 映射(缺文件报错提及输入名)", async () => {
|
|
192
|
+
const r = await run(["merge", "out.json", "in1.json", "in2.json"], "zh-CN", tmp);
|
|
193
|
+
expect(r.out).toContain("in1.json");
|
|
194
|
+
expect(r.exitCode).toBe(1);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("build --bundle demo 映射 bundleName", async () => {
|
|
198
|
+
const r = await run(["build", "--bundle", "demo"], "zh-CN", tmp, () => buildBundleMock.mock.calls.length > 0);
|
|
199
|
+
expect(buildBundleMock).toHaveBeenCalledWith(tmp, { out: undefined, name: "demo" });
|
|
200
|
+
expect(r.exitCode).toBeUndefined();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("build 裸 --bundle 无名称(全部用户方案)", async () => {
|
|
204
|
+
await run(["build", "--bundle"], "zh-CN", tmp, () => buildBundleMock.mock.calls.length > 0);
|
|
205
|
+
expect(buildBundleMock).toHaveBeenCalledWith(tmp, { out: undefined, name: undefined });
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("help 词形", () => {
|
|
210
|
+
it("ojm help build → build --help", () => {
|
|
211
|
+
expect(rewriteHelpArgv(["help", "build"])).toEqual(["build", "--help"]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("ojm help trust add → trust add --help", () => {
|
|
215
|
+
expect(rewriteHelpArgv(["help", "trust", "add"])).toEqual(["trust", "add", "--help"]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("ojm help → 顶层 --help", () => {
|
|
219
|
+
expect(rewriteHelpArgv(["help"])).toEqual(["--help"]);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("非 help 调用不改动", () => {
|
|
223
|
+
expect(rewriteHelpArgv(["build", "--bundle"])).toEqual(["build", "--bundle"]);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// BDD:脚手架 package.json scripts 与模板的双写守卫(T3,发布工程师评审建议)。
|
|
2
|
+
// 生成器(init.ts generatePackageJson)与模板(templates/package.json)各写一份 scripts,
|
|
3
|
+
// 人肉同步必漂移(实证:typecheck:api 曾漏发生成器)——钉成测试:生成 scripts 的每个键
|
|
4
|
+
// 都必须存在于模板 scripts(值一致),新增 script 先改模板再改生成器。
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import { generatePackageJson } from "../init";
|
|
10
|
+
|
|
11
|
+
const cliRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
12
|
+
|
|
13
|
+
describe("脚手架 package.json scripts 双写守卫(T3)", () => {
|
|
14
|
+
it("生成 scripts 的每个键都存在于模板且值一致", () => {
|
|
15
|
+
const template = JSON.parse(readFileSync(join(cliRoot, "templates/package.json"), "utf-8"));
|
|
16
|
+
const generated = generatePackageJson(cliRoot, "guard-check");
|
|
17
|
+
for (const [key, value] of Object.entries(generated.scripts)) {
|
|
18
|
+
expect(template.scripts[key], `生成器有而模板无的 script:${key}(先改 templates/package.json)`)
|
|
19
|
+
.toBe(value);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("多方案 scripts 齐备:new / bundle / typecheck:api", () => {
|
|
24
|
+
const generated = generatePackageJson(cliRoot, "guard-check");
|
|
25
|
+
expect(generated.scripts.new).toBe("ojm new");
|
|
26
|
+
expect(generated.scripts.bundle).toBe("ojm build --bundle");
|
|
27
|
+
expect(generated.scripts["typecheck:api"]).toBe("tsc --noEmit -p src/tsconfig.json");
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -43,16 +43,16 @@ describe("resolveLayout / module-root", () => {
|
|
|
43
43
|
|
|
44
44
|
expect(layout.kind).toBe("module-root");
|
|
45
45
|
expect(layout.modulesSrc).toBe(join(root, "src"));
|
|
46
|
-
expect(layout.distDir).toBe(join(root, "
|
|
47
|
-
expect(layout.apiDist).toBe(join(root, "
|
|
46
|
+
expect(layout.distDir).toBe(join(root, "dist", "web"));
|
|
47
|
+
expect(layout.apiDist).toBe(join(root, "dist", "api"));
|
|
48
48
|
expect(layout.watchTarget).toBe(join(root, "src"));
|
|
49
49
|
// --module-root 取「源码树根目录」(oj 在其子树内递归找 manifest.yaml)
|
|
50
50
|
expect(layout.moduleRoot).toBe(join(root, "src"));
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
it("web
|
|
53
|
+
it("dist/web 存在不会被误判成 flat 布局(历史产物目录不算源码)", () => {
|
|
54
54
|
const root = newModuleRootProject();
|
|
55
|
-
mkdirSync(join(root, "
|
|
55
|
+
mkdirSync(join(root, "dist", "web", "modules", "wiki", "0.1.0"), { recursive: true });
|
|
56
56
|
expect(resolveLayout(root).kind).toBe("module-root");
|
|
57
57
|
});
|
|
58
58
|
|
|
@@ -102,11 +102,13 @@ describe("resolveLayout / 历史布局", () => {
|
|
|
102
102
|
expect(layout.kind).toBe("new");
|
|
103
103
|
expect(layout.modulesSrc).toBe(join(root, "web", "src"));
|
|
104
104
|
expect(layout.moduleRoot).toBeUndefined();
|
|
105
|
+
expect(layout.distDir).toBe(join(root, "dist", "web"));
|
|
106
|
+
expect(layout.apiDist).toBe(join(root, "dist", "api"));
|
|
105
107
|
expect(apiSrcOf(layout, "wiki")).toBe(join(root, "api", "src", "wiki"));
|
|
106
108
|
expect(webSrcOf(layout, "wiki")).toBe(join(root, "web", "src", "wiki"));
|
|
107
109
|
});
|
|
108
110
|
|
|
109
|
-
it("web/<name>/entry.ts 识别为 flat
|
|
111
|
+
it("web/<name>/entry.ts 识别为 flat,web 产物落 build/(框架根仓约定不变),api 产物归 dist/api", () => {
|
|
110
112
|
const root = newRoot();
|
|
111
113
|
writeFile(root, "web/home/entry.ts", "export default {};\n");
|
|
112
114
|
writeFile(root, "web/home/__client/api.ts", "export {};\n");
|
|
@@ -115,6 +117,7 @@ describe("resolveLayout / 历史布局", () => {
|
|
|
115
117
|
expect(layout.kind).toBe("flat");
|
|
116
118
|
expect(layout.modulesSrc).toBe(join(root, "web"));
|
|
117
119
|
expect(layout.distDir).toBe(join(root, "build"));
|
|
120
|
+
expect(layout.apiDist).toBe(join(root, "dist", "api"));
|
|
118
121
|
expect(webSrcOf(layout, "home")).toBe(join(root, "web", "home"));
|
|
119
122
|
});
|
|
120
123
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { createSolution } from "../new";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ojm new 骨架内容契约(20261023-1144 计划)。
|
|
10
|
+
* 背景:--api 骨架曾只产出裸命名导出 `export async function ping()`,
|
|
11
|
+
* oj 内省只读 default export 且链尾须为 HTTP 动词 → 打包后 routes.js 恒空、
|
|
12
|
+
* 装后探活 /api/<name>/ping 必然 404。骨架必须「生成即可路由」。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const tmpRoots: string[] = [];
|
|
16
|
+
|
|
17
|
+
function projectRoot(): string {
|
|
18
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "ojm-new-scaffold-"));
|
|
19
|
+
tmpRoots.push(root);
|
|
20
|
+
// createSolution 的 N0 工程判定:web.config.ts 与 src/ 至少一个
|
|
21
|
+
fs.mkdirSync(path.join(root, "src"), { recursive: true });
|
|
22
|
+
return root;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
for (const root of tmpRoots.splice(0)) {
|
|
27
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("ojm new --api 骨架可路由(BDD)", () => {
|
|
32
|
+
it("给定 createSolution(api: true),生成的 api.ts 是 default-export 嵌套组形状", () => {
|
|
33
|
+
const root = projectRoot();
|
|
34
|
+
const result = createSolution(root, "crm", { api: true, layout: "none" });
|
|
35
|
+
const apiTs = fs.readFileSync(path.join(result.dir, "api", "api.ts"), "utf8");
|
|
36
|
+
|
|
37
|
+
// Then:default export 承载路由组(oj 内省只读 default export)
|
|
38
|
+
expect(apiTs).toContain("export default { ping }");
|
|
39
|
+
// Then:链尾是 HTTP 动词 get(METHODS 白名单),组名 ping → GET /api/crm/ping
|
|
40
|
+
expect(apiTs).toMatch(/const ping = \{[\s\S]*async get\(\)/);
|
|
41
|
+
// Then:落实注释承诺——出参挂平级 .response(pong: boolean)
|
|
42
|
+
expect(apiTs).toContain("ping.get.response");
|
|
43
|
+
expect(apiTs).toContain("pong");
|
|
44
|
+
// Then:不再有「裸命名导出而无 default export」的旧形状
|
|
45
|
+
expect(apiTs).not.toMatch(/^export async function ping/m);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("给定 createSolution(api: true),solution.yaml 过自家 market verify 五必填(20261023-1155)", async () => {
|
|
49
|
+
const root = projectRoot();
|
|
50
|
+
const result = createSolution(root, "crm", { api: true, layout: "none" });
|
|
51
|
+
const { parse: yamlParse } = await import("yaml");
|
|
52
|
+
const doc = yamlParse(fs.readFileSync(path.join(result.dir, "solution.yaml"), "utf8")) as Record<string, unknown>;
|
|
53
|
+
|
|
54
|
+
// Then:market/verify.ts structureCheck 五必填 name/version/summary/peerRuntime/archive 齐备
|
|
55
|
+
for (const key of ["name", "version", "summary", "peerRuntime", "archive"])
|
|
56
|
+
expect(doc[key], `solution.yaml 缺 ${key}(market verify 必填)`).toBeDefined();
|
|
57
|
+
expect((doc.archive as Record<string, unknown>).file).toBe("crm-0.1.0.tgz");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -1,69 +1,67 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveLang, usageText } from "../usage";
|
|
3
3
|
|
|
4
|
-
describe("
|
|
5
|
-
it("
|
|
6
|
-
|
|
7
|
-
expect(
|
|
8
|
-
expect(t).toContain("ojm init");
|
|
4
|
+
describe("resolveLang", () => {
|
|
5
|
+
it("显式 OJM_LANG 优先于系统 locale", () => {
|
|
6
|
+
expect(resolveLang({ OJM_LANG: "en-US", LANG: "zh_CN.UTF-8" } as NodeJS.ProcessEnv)).toBe("en-US");
|
|
7
|
+
expect(resolveLang({ OJM_LANG: "zh-CN", LANG: "en_US.UTF-8" } as NodeJS.ProcessEnv)).toBe("zh-CN");
|
|
9
8
|
});
|
|
10
9
|
|
|
11
|
-
it("
|
|
12
|
-
expect(
|
|
10
|
+
it("无显式设置时按 LANG/LC_ALL 前缀探测英文", () => {
|
|
11
|
+
expect(resolveLang({ LANG: "en_US.UTF-8" } as NodeJS.ProcessEnv)).toBe("en-US");
|
|
12
|
+
expect(resolveLang({ LC_ALL: "en_GB.UTF-8" } as NodeJS.ProcessEnv)).toBe("en-US");
|
|
13
|
+
expect(resolveLang({ LANG: "C" } as NodeJS.ProcessEnv)).toBe("zh-CN");
|
|
13
14
|
});
|
|
14
15
|
|
|
15
|
-
it("
|
|
16
|
-
|
|
17
|
-
expect(t).toContain("ojm market");
|
|
16
|
+
it("非法值回退 zh-CN", () => {
|
|
17
|
+
expect(resolveLang({ OJM_LANG: "fr" } as NodeJS.ProcessEnv)).toBe("zh-CN");
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
it("
|
|
21
|
-
|
|
22
|
-
const t = getCommandHelp("trust", sub);
|
|
23
|
-
expect(t, `trust ${sub}`).toBeDefined();
|
|
24
|
-
expect(t).toContain(`ojm trust ${sub}`);
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("未知命令返回 undefined", () => {
|
|
29
|
-
expect(getCommandHelp("nope")).toBeUndefined();
|
|
20
|
+
it("全缺省为 zh-CN", () => {
|
|
21
|
+
expect(resolveLang({} as NodeJS.ProcessEnv)).toBe("zh-CN");
|
|
30
22
|
});
|
|
31
23
|
});
|
|
32
24
|
|
|
33
|
-
describe("
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
});
|
|
25
|
+
describe("usageText 分组(cac 渲染)", () => {
|
|
26
|
+
const ZH_TITLES = ["工程脚手架", "开发与构建", "清单与诊断", "方案市场(站点侧生命周期)", "发布与签名(作者侧)", "本地市场模拟 CDN", "oj 子命令透传"];
|
|
27
|
+
const EN_TITLES = ["Project scaffolding", "Development & build", "Manifest & diagnostics", "Marketplace (site-side lifecycle)", "Publishing & signing (author side)", "Local marketplace mock CDN", "oj subcommand passthrough"];
|
|
37
28
|
|
|
38
|
-
it("
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
it("ojm trust add --help → cmd=trust sub=add", () => {
|
|
47
|
-
expect(resolveHelpRequest(["trust", "add", "--help"])).toEqual({ cmd: "trust", sub: "add" });
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("ojm market setup --help → cmd=market sub=setup", () => {
|
|
51
|
-
expect(resolveHelpRequest(["market", "setup", "--help"])).toEqual({ cmd: "market", sub: "setup" });
|
|
29
|
+
it("中文分组头齐全且顺序固定", () => {
|
|
30
|
+
const text = usageText("zh-CN");
|
|
31
|
+
let last = -1;
|
|
32
|
+
for (const title of ZH_TITLES) {
|
|
33
|
+
const i = text.indexOf(`${title}:`);
|
|
34
|
+
expect(i, title).toBeGreaterThan(last);
|
|
35
|
+
last = i;
|
|
36
|
+
}
|
|
52
37
|
});
|
|
53
38
|
|
|
54
|
-
it("
|
|
55
|
-
|
|
39
|
+
it("英文分组头齐全且顺序固定", () => {
|
|
40
|
+
const text = usageText("en-US");
|
|
41
|
+
let last = -1;
|
|
42
|
+
for (const title of EN_TITLES) {
|
|
43
|
+
const i = text.indexOf(`${title}:`);
|
|
44
|
+
expect(i, title).toBeGreaterThan(last);
|
|
45
|
+
last = i;
|
|
46
|
+
}
|
|
56
47
|
});
|
|
57
48
|
|
|
58
|
-
it("
|
|
59
|
-
|
|
49
|
+
it("两种语言均含全部命令语法行", () => {
|
|
50
|
+
for (const lang of ["zh-CN", "en-US"] as const) {
|
|
51
|
+
const text = usageText(lang);
|
|
52
|
+
for (const cmd of ["ojm init [dir]", "ojm dev [port]", "ojm build", "ojm preview [port]", "ojm api [dir]", "ojm info", "ojm merge", "ojm vendor", "ojm install", "ojm trust", "ojm keygen", "ojm sign", "ojm publish", "ojm market verify", "ojm test", "ojm migrate"])
|
|
53
|
+
expect(text, `${lang} ${cmd}`).toContain(cmd);
|
|
54
|
+
}
|
|
60
55
|
});
|
|
61
56
|
|
|
62
|
-
it("
|
|
63
|
-
|
|
57
|
+
it("trust 子命令索引行是 short 一句话,不复读语法行(回归)", () => {
|
|
58
|
+
const zh = usageText("zh-CN");
|
|
59
|
+
expect(zh).toMatch(/ojm trust ls\s+列出已信任 key/);
|
|
60
|
+
expect(zh).not.toContain("ojm trust ls ojm trust ls");
|
|
64
61
|
});
|
|
65
62
|
|
|
66
|
-
it("
|
|
67
|
-
expect(
|
|
63
|
+
it("版本项保留(双语)", () => {
|
|
64
|
+
expect(usageText("zh-CN")).toContain("ojm --version | -V");
|
|
65
|
+
expect(usageText("en-US")).toContain("ojm --version | -V");
|
|
68
66
|
});
|
|
69
67
|
});
|
package/src/build.ts
CHANGED
|
@@ -522,7 +522,7 @@ export async function buildBackend(projectRoot: string): Promise<boolean> {
|
|
|
522
522
|
if (syncedPaths === "created" || syncedPaths === "updated")
|
|
523
523
|
console.log(`[ojm] 已${syncedPaths === "created" ? "生成" : "更新"} ${ALIAS_PATHS_FILE}(\`#/<模块名>/…\` 的 TS paths 投影)`);
|
|
524
524
|
|
|
525
|
-
const apiDist = layout?.apiDist ?? path.join(projectRoot, "
|
|
525
|
+
const apiDist = layout?.apiDist ?? path.join(projectRoot, "dist", "api");
|
|
526
526
|
console.log("[ojm] oj build(后端)…");
|
|
527
527
|
// 必须显式给 -c:oj build 的 -c 缺省是 ./config.yaml(相对 CWD),而 config 在
|
|
528
528
|
// <project>/config/ 下——漏传会静默读不到配置,连带 tasks 镜像目录回落默认 "tasks"、
|
|
@@ -546,8 +546,8 @@ export async function buildBackend(projectRoot: string): Promise<boolean> {
|
|
|
546
546
|
apiDist,
|
|
547
547
|
"--no-minify",
|
|
548
548
|
], { stdio: "inherit" });
|
|
549
|
-
// 宿主 versions.json 双落点:站点壳从 shell-dist 拷进 web
|
|
550
|
-
// 而方案安装器读的是
|
|
549
|
+
// 宿主 versions.json 双落点:站点壳从 shell-dist 拷进 dist/web(mergeShellSite),
|
|
550
|
+
// 而方案安装器读的是 `dist/api/versions.json`(B-11:与 manifests.yaml 同层,用于
|
|
551
551
|
// peerRuntime 强制比对)。两处必须同源,否则新工程 `ojm build` 完仍装不上方案——
|
|
552
552
|
// preflight 直接 C2-09 fail-closed「无法读取宿主 runtime 版本」。
|
|
553
553
|
const versionsSrc = path.join(resolveShellDist(), "versions.json");
|
|
@@ -585,7 +585,7 @@ function mergeShellSite(distDir: string, siteBase: string): void {
|
|
|
585
585
|
/**
|
|
586
586
|
* 构建模块工程。
|
|
587
587
|
*
|
|
588
|
-
* 产出(
|
|
588
|
+
* 产出(layout.distDir:module-root / new → dist/web;flat → build):
|
|
589
589
|
* <dist>/modules/<name>/<version>/{entry.js, chunk-*.js, *.css}
|
|
590
590
|
* <dist>/modules.json
|
|
591
591
|
*
|