@oj-module/cli 0.2.23 → 0.2.26
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 +7 -5
- package/devkit/module-dev-and-cdn-playground.md +10 -10
- package/package.json +4 -4
- package/shell-dist/assets/runtime.js +5 -5
- 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__/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/bundle.ts +9 -9
- package/src/cli.ts +265 -0
- package/src/contract/check.ts +12 -5
- package/src/dev.ts +17 -0
- package/src/index.ts +31 -211
- package/src/init.ts +2 -4
- 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__/bundle.test.ts +24 -24
- package/src/lifecycle/__tests__/install.test.ts +0 -23
- package/src/lifecycle/__tests__/new.test.ts +1 -1
- package/src/lifecycle/__tests__/smoke.test.ts +2 -2
- 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/setup.ts +14 -2
- package/src/market/trial.ts +3 -3
- package/src/market/walkthrough.ts +7 -6
- package/src/new.ts +21 -3
- 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 -210
- package/src/vendor.ts +5 -1
- package/templates/README.md +3 -3
- package/templates/cdn/README.md +6 -6
- package/templates/config.yaml +5 -4
- package/templates/init.include.yaml +38 -2
- package/templates/seed.mysql.sql +2 -1
- package/templates/seed.pg.sql +2 -1
- package/templates/src/_platform/api/market/api.ts +11 -8
- package/templates/src/_platform/api/seed.sql +2 -1
- package/templates/src/_platform/api/solutions/api.ts +2 -2
- package/templates/src/system/web/pages/users/roles.tsx +3 -1
- 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/vendor/host-versions.json +3 -3
- package/src/args.ts +0 -172
- package/src/lifecycle/__tests__/args.test.ts +0 -20
package/src/index.ts
CHANGED
|
@@ -1,223 +1,43 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
1
|
import process from "node:process";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { buildBundle } from "./bundle";
|
|
7
|
-
import { devServer } from "./dev";
|
|
8
|
-
import { mergeManifests, printInfo } from "./info";
|
|
9
|
-
import { initProject } from "./init";
|
|
10
|
-
import { createSolution } from "./new";
|
|
11
|
-
import { runPreview } from "./preview";
|
|
12
|
-
import { getCommandHelp, resolveHelpRequest, usageText } from "./usage";
|
|
13
|
-
import { vendorCommand } from "./vendor";
|
|
2
|
+
import { buildCli, localizeCacError, reportCliError, rewriteHelpArgv, unknownCommandResult } from "./cli";
|
|
3
|
+
import { isKnownCommand, resolveLang, usageText } from "./usage";
|
|
14
4
|
import { readCliVersion } from "./versions";
|
|
15
5
|
|
|
16
|
-
/** ojm → oj 透传子命令(其余参数原样转发,见 src/oj-cmd.ts) */
|
|
17
|
-
const OJ_PASSTHROUGH = new Set(["test", "exec", "openapi", "migrate", "schema"]);
|
|
18
|
-
|
|
19
|
-
const [command] = process.argv.slice(2);
|
|
20
|
-
|
|
21
|
-
function usage(): never {
|
|
22
|
-
console.log(usageText());
|
|
23
|
-
process.exit(command ? 1 : 0);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
6
|
async function main() {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
// 帮助请求:`ojm <cmd> --help` / `ojm help <cmd>` / 顶层 `ojm --help`
|
|
30
|
-
// 必须在 switch 之前拦截,否则会真正执行子命令(如 `ojm market setup --help` 会去建 CDN)。
|
|
31
|
-
const helpReq = resolveHelpRequest();
|
|
32
|
-
if (helpReq !== null) {
|
|
33
|
-
if (!helpReq.cmd) {
|
|
34
|
-
console.log(usageText());
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
const text = getCommandHelp(helpReq.cmd, helpReq.sub);
|
|
38
|
-
if (text) {
|
|
39
|
-
console.log(text);
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
console.error(`[ojm] 未知命令:${helpReq.cmd}${helpReq.sub ? ` ${helpReq.sub}` : ""}`);
|
|
43
|
-
process.exit(1);
|
|
44
|
-
}
|
|
7
|
+
const lang = resolveLang();
|
|
8
|
+
const argv = process.argv.slice(2);
|
|
45
9
|
|
|
46
|
-
// 顶层 --version / -V
|
|
47
|
-
if (
|
|
10
|
+
// 顶层 --version / -V:裸版本号(与 ojm info 同源);不经 cac,避免其 platformInfo 后缀
|
|
11
|
+
if (argv[0] === "--version" || argv[0] === "-V") {
|
|
48
12
|
console.log(readCliVersion());
|
|
49
13
|
return;
|
|
50
14
|
}
|
|
51
15
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
process.exit(1);
|
|
73
|
-
}
|
|
74
|
-
const created = createSolution(projectRoot, name, { layout, api });
|
|
75
|
-
console.log(`[ojm] 已生成方案骨架:${path.relative(process.cwd(), created.dir)}`);
|
|
76
|
-
for (const f of created.files)
|
|
77
|
-
console.log(`[ojm] + ${f}`);
|
|
78
|
-
console.log(`[ojm] 下一步:pnpm dev 看效果 → ojm build && ojm build --bundle ${name} 打包`);
|
|
79
|
-
break;
|
|
80
|
-
}
|
|
81
|
-
case "init": {
|
|
82
|
-
const { dest, yes } = parseInitArgs(process.argv.slice(3));
|
|
83
|
-
await initProject(dest ? path.resolve(dest) : projectRoot, { yes });
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
case "preview": {
|
|
87
|
-
const previewPortArg = Number(process.argv[3]);
|
|
88
|
-
const previewPort = Number.isInteger(previewPortArg) && previewPortArg > 0 ? previewPortArg : undefined;
|
|
89
|
-
await runPreview(projectRoot, { port: previewPort });
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
92
|
-
case "dev": {
|
|
93
|
-
const portArg = Number(process.argv[3]);
|
|
94
|
-
const port = Number.isFinite(portArg) && portArg > 0 ? portArg : 5174;
|
|
95
|
-
await devServer(projectRoot, { port });
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
case "info":
|
|
99
|
-
await printInfo(projectRoot, undefined, { all: process.argv.slice(3).includes("--all") });
|
|
100
|
-
break;
|
|
101
|
-
case "vendor": {
|
|
102
|
-
const { force, tag } = parseVendorArgs(process.argv.slice(3));
|
|
103
|
-
await vendorCommand(projectRoot, { force, tag });
|
|
104
|
-
break;
|
|
105
|
-
}
|
|
106
|
-
case "api": {
|
|
107
|
-
const { dir, check, docs, exempt } = parseApiArgs(process.argv.slice(3));
|
|
108
|
-
// S5:显式目录必须先存在——否则静默在错的目录下生成/对账
|
|
109
|
-
if (dir && !fs.existsSync(path.resolve(dir)))
|
|
110
|
-
throw new Error(`[ojm-api] 项目目录不存在:${dir}`);
|
|
111
|
-
const apiRoot = dir ? path.resolve(dir) : projectRoot;
|
|
112
|
-
if (docs) {
|
|
113
|
-
const { runApiDocs } = await import("./contract/run");
|
|
114
|
-
const out = await runApiDocs(apiRoot);
|
|
115
|
-
console.log(`[ojm-api] 文档站已生成:${path.relative(projectRoot, out)}(浏览器直接打开即可)`);
|
|
116
|
-
break;
|
|
117
|
-
}
|
|
118
|
-
if (check) {
|
|
119
|
-
const { checkApi } = await import("./contract/check");
|
|
120
|
-
// --exempt <path>:相对路径按项目根解析;缺省按布局找 .ojm-api-exempt.json(工程根 → api/)
|
|
121
|
-
// (旧名 api/.ram-api-exempt.json 仍被读取,R4)
|
|
122
|
-
const { violations, hints } = await checkApi({ cwd: apiRoot, exempt: exempt ? path.resolve(projectRoot, exempt) : undefined });
|
|
123
|
-
for (const v of violations)
|
|
124
|
-
console[v.level === "error" ? "error" : "warn"](`${v.level === "error" ? "✗" : "⚠"} ${v.message}`);
|
|
125
|
-
for (const h of hints)
|
|
126
|
-
console.log(h);
|
|
127
|
-
if (violations.length === 0)
|
|
128
|
-
console.log("[ojm-api] --check 通过:生成物同步、route 双向对账、routes.js 均无 drift。");
|
|
129
|
-
const errors = violations.filter(v => v.level === "error").length;
|
|
130
|
-
console.log(`[ojm-api] --check 结果:${errors} error / ${violations.length - errors} warn`);
|
|
131
|
-
if (errors > 0)
|
|
132
|
-
process.exit(1);
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
|
-
const { runApi } = await import("./contract/run");
|
|
136
|
-
const result = await runApi({ cwd: apiRoot });
|
|
137
|
-
console.log(`[ojm-api] 契约 ${result.contracts} 份;写入 ${result.written.length} 个文件,跳过(未变)${result.skipped.length} 个;stub 新建 ${result.stubs.created} / 更新 ${result.stubs.updated} / 跳过 ${result.stubs.skipped}`);
|
|
138
|
-
for (const p of result.written)
|
|
139
|
-
console.log(` + ${path.relative(apiRoot, p)}`);
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
case "merge":
|
|
143
|
-
await mergeManifests(process.argv[3] ?? "", process.argv.slice(4));
|
|
144
|
-
break;
|
|
145
|
-
case "keygen": {
|
|
146
|
-
// 签名密钥对生成(P2-4,作者工具链):缺省 ~/.ojm/keys/<id>,工程树内落盘需 --yes
|
|
147
|
-
const { parseKeygenArgs } = await import("./keygen");
|
|
148
|
-
const { keygenCommand } = await import("./keygen");
|
|
149
|
-
const { id, out, yes } = parseKeygenArgs(process.argv.slice(3));
|
|
150
|
-
keygenCommand({ id, out, yes });
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
153
|
-
case "sign": {
|
|
154
|
-
// detached 侧车签名(P2-4,G4 载体):ojm sign <tgz> [--id <publisher.id>] [--key <pem>] [--check]
|
|
155
|
-
const { parseSignArgs } = await import("./sign");
|
|
156
|
-
const { signCommand } = await import("./sign");
|
|
157
|
-
const { positionals, id, key, check } = parseSignArgs(process.argv.slice(3));
|
|
158
|
-
signCommand(positionals[0] ?? "", { id, key, check });
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
case "install": {
|
|
162
|
-
// 方案包安装瘦客户端(P1-13,D6):file 上传 / CDN 市场源 → 轮询任务至终态
|
|
163
|
-
const { positionals, url, user, pass, locale, name, version } = parseLifecycleArgs(process.argv.slice(3));
|
|
164
|
-
const { installCommand } = await import("./install");
|
|
165
|
-
await installCommand(positionals[0] ?? "", { url, user, pass, locale, name, version });
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
case "enable":
|
|
169
|
-
case "disable": {
|
|
170
|
-
// 生命周期开关瘦客户端(P1-13,D8):站点侧 setEnabled 四处同写
|
|
171
|
-
const { positionals, url, user, pass } = parseLifecycleArgs(process.argv.slice(3));
|
|
172
|
-
const { setEnabledCommand } = await import("./enable");
|
|
173
|
-
await setEnabledCommand(positionals[0] ?? "", command === "enable", { url, user, pass });
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
case "uninstall": {
|
|
177
|
-
// 卸载瘦客户端(P1-15a,缺口 B):站点侧 runUninstall 四处同删 + 权限级联
|
|
178
|
-
const { positionals, url, user, pass } = parseLifecycleArgs(process.argv.slice(3));
|
|
179
|
-
const { uninstallCommand } = await import("./uninstall");
|
|
180
|
-
await uninstallCommand(positionals[0] ?? "", { url, user, pass });
|
|
181
|
-
break;
|
|
182
|
-
}
|
|
183
|
-
case "trust": {
|
|
184
|
-
// 信任库运维瘦客户端(P2-2,C2-38/39):ls / add <key_id> <pubkey_hex> / revoke <key_id> / confirm --task
|
|
185
|
-
const { positionals, url, user, pass, task } = parseLifecycleArgs(process.argv.slice(3));
|
|
186
|
-
const { trustCommand } = await import("./trust");
|
|
187
|
-
await trustCommand(positionals[0] ?? "", positionals.slice(1), { url, user, pass, task });
|
|
188
|
-
break;
|
|
189
|
-
}
|
|
190
|
-
case "publish": {
|
|
191
|
-
// 索引发布(P3-5/6/7,作者工具链):重算 bundles/manifests 索引 + detached .sig 签名写回
|
|
192
|
-
const { parsePublishArgs } = await import("./publish");
|
|
193
|
-
const { publishCommand } = await import("./publish");
|
|
194
|
-
const { positionals, id, key, check } = parsePublishArgs(process.argv.slice(3));
|
|
195
|
-
publishCommand(positionals[0] ?? "", { id, key, check });
|
|
196
|
-
break;
|
|
197
|
-
}
|
|
198
|
-
case "market": {
|
|
199
|
-
// 本地模拟 CDN 与市场链路(setup / publish / build-index)
|
|
200
|
-
const { runMarket } = await import("./market/command");
|
|
201
|
-
await runMarket(process.argv.slice(3));
|
|
202
|
-
break;
|
|
203
|
-
}
|
|
204
|
-
default: {
|
|
205
|
-
if (command && OJ_PASSTHROUGH.has(command)) {
|
|
206
|
-
const { runOjSubcommand } = await import("./oj-cmd");
|
|
207
|
-
runOjSubcommand(projectRoot, command, process.argv.slice(3));
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
210
|
-
usage();
|
|
211
|
-
}
|
|
16
|
+
const rewritten = rewriteHelpArgv(argv);
|
|
17
|
+
const cli = buildCli(process.cwd(), lang);
|
|
18
|
+
let unknown: string | undefined;
|
|
19
|
+
cli.on("command:*", () => {
|
|
20
|
+
// cac 在 --help 输出后会 unsetMatchedCommand 再发 command:*(args[0] 可能是子命令,如 verify)——
|
|
21
|
+
// 误判根源。按「首位置参数是否是已知命令」判定:market 等已知命令的子命令不误判,未知首参才兜底
|
|
22
|
+
const first = rewritten.find(a => !a.startsWith("-"));
|
|
23
|
+
if (first !== undefined && !isKnownCommand(first))
|
|
24
|
+
unknown = first;
|
|
25
|
+
});
|
|
26
|
+
cli.parse(["node", "ojm", ...rewritten]);
|
|
27
|
+
|
|
28
|
+
// 未知命令:help 形态报「未知命令」、否则打帮助;均 exit 1(裸 ojm 打帮助退 0 不变)
|
|
29
|
+
if (unknown !== undefined) {
|
|
30
|
+
const r = unknownCommandResult(unknown, lang, !!cli.options.help);
|
|
31
|
+
if (cli.options.help)
|
|
32
|
+
console.error(r.text);
|
|
33
|
+
else
|
|
34
|
+
console.log(r.text);
|
|
35
|
+
process.exit(r.code);
|
|
212
36
|
}
|
|
37
|
+
if (argv.length === 0)
|
|
38
|
+
console.log(usageText(lang));
|
|
213
39
|
}
|
|
214
40
|
|
|
215
|
-
main().catch((error: unknown) => {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
// 文案常自带 [ojm] 前缀(bundle/publish/sign 等抛出的错误),顶层不再叠一个,否则变 [ojm] [ojm]。
|
|
219
|
-
const isDebug = !!(process.env.OJM_DEBUG ?? process.env.RAM_DEBUG);
|
|
220
|
-
const body = error instanceof Error ? (isDebug ? error.stack ?? error.message : error.message) : String(error);
|
|
221
|
-
console.error(/^\[ojm/.test(body) ? body : `[ojm] ${body}`);
|
|
222
|
-
process.exit(1);
|
|
223
|
-
});
|
|
41
|
+
main().catch((error: unknown) => reportCliError(localizeCacError(error, resolveLang()), (code) => {
|
|
42
|
+
process.exit(code);
|
|
43
|
+
}));
|
package/src/init.ts
CHANGED
|
@@ -489,10 +489,8 @@ logs/
|
|
|
489
489
|
# 现场签发的开发证书与凭据私钥(ojm dev 缺证时自动签发,见 oj-cert.ts);私钥绝不入仓
|
|
490
490
|
config/*.pem
|
|
491
491
|
config/cert.jws
|
|
492
|
-
#
|
|
493
|
-
|
|
494
|
-
web/dist/
|
|
495
|
-
modules/dist/
|
|
492
|
+
# 构建产物(ojm build:站点 dist/web + 后端 release dist/api)
|
|
493
|
+
dist/
|
|
496
494
|
`;
|
|
497
495
|
|
|
498
496
|
function ensureGitignore(destDir: string, report: InitReport): void {
|
package/src/keygen.ts
CHANGED
|
@@ -24,25 +24,6 @@ export interface KeygenOptions {
|
|
|
24
24
|
|
|
25
25
|
export const DEFAULT_KEY_ID = "local.dev";
|
|
26
26
|
|
|
27
|
-
/** `ojm keygen [--id <publisher.id>] [--out <dir>] [--yes]` 参数解析(值式 flag 逐项消费,P6 同款纪律) */
|
|
28
|
-
export function parseKeygenArgs(argv: string[]): { id?: string, out?: string, yes?: boolean } {
|
|
29
|
-
const values: Record<string, string> = {};
|
|
30
|
-
let yes = false;
|
|
31
|
-
for (let i = 0; i < argv.length; i++) {
|
|
32
|
-
const a = argv[i]!;
|
|
33
|
-
if (a === "--yes") {
|
|
34
|
-
yes = true;
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
if (a === "--id" || a === "--out") {
|
|
38
|
-
values[a] = argv[i + 1] ?? "";
|
|
39
|
-
i++;
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return { id: values["--id"], out: values["--out"], yes };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
27
|
/** C2-37 key_id 命名空间(与 isolate 侧 trust.ts isValidKeyId 同规则;禁裸通用名单段) */
|
|
47
28
|
export function isValidKeyId(keyId: string): boolean {
|
|
48
29
|
return /^[a-z][a-z0-9-]*(?:\.[a-z0-9-]+)+$/.test(keyId);
|
package/src/layout.ts
CHANGED
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
* `src/<x>/api/manifest.yaml` 与 `src/<x>/web/entry.ts` 是唯一真源(设计 C-6:
|
|
15
15
|
* 一侧可缺省,两侧皆无则不是模块)。
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
* api
|
|
19
|
-
* web
|
|
17
|
+
* 产物根(2026-10-22 起收进工程根单一 dist/,原 {web,api}/dist 与源码混树):
|
|
18
|
+
* dist/api oj 扁平 release 根(<name>-<version>/ 同层兄弟 + manifests.yaml),三种布局一致
|
|
19
|
+
* dist/web 站点产物(shell + modules/ + modules.json);flat 例外:框架根仓维持 build/
|
|
20
20
|
* 产物按模块聚合已被设计 §2.2 否决(实证见该节),禁止在此引入第二套形状。
|
|
21
21
|
*
|
|
22
|
-
* watch 目标绝不含产物目录:若 watch `web/`,重建产物(web
|
|
22
|
+
* watch 目标绝不含产物目录:若 watch `web/`,重建产物(dist/web)
|
|
23
23
|
* 会反过来触发重建,形成自触发循环(设计 §4)。
|
|
24
24
|
*/
|
|
25
25
|
|
|
@@ -42,9 +42,9 @@ export interface ProjectLayout {
|
|
|
42
42
|
projectRoot: string
|
|
43
43
|
/** 模块源码目录:module-root → `<project>/src`;new → `<project>/web/src`;flat → `<project>/web` */
|
|
44
44
|
modulesSrc: string
|
|
45
|
-
/** 站点产物根:module-root / new → `<project>/web
|
|
45
|
+
/** 站点产物根:module-root / new → `<project>/dist/web`;flat → `<project>/build`(框架根仓约定) */
|
|
46
46
|
distDir: string
|
|
47
|
-
/** oj 扁平 release 根:恒为 `<project>/
|
|
47
|
+
/** oj 扁平 release 根:恒为 `<project>/dist/api`(不随布局变化;无布局回退同值) */
|
|
48
48
|
apiDist: string
|
|
49
49
|
/** fs.watch 目标(纯源码,永不落产物) */
|
|
50
50
|
watchTarget: string
|
|
@@ -161,8 +161,8 @@ export function tryResolveLayout(projectRoot: string): ProjectLayout | null {
|
|
|
161
161
|
kind: "module-root",
|
|
162
162
|
projectRoot,
|
|
163
163
|
modulesSrc: path.join(projectRoot, "src"),
|
|
164
|
-
distDir: path.join(projectRoot, "
|
|
165
|
-
apiDist: path.join(projectRoot, "
|
|
164
|
+
distDir: path.join(projectRoot, "dist", "web"),
|
|
165
|
+
apiDist: path.join(projectRoot, "dist", "api"),
|
|
166
166
|
watchTarget: path.join(projectRoot, "src"),
|
|
167
167
|
moduleRoot: path.join(projectRoot, "src"),
|
|
168
168
|
};
|
|
@@ -173,8 +173,8 @@ export function tryResolveLayout(projectRoot: string): ProjectLayout | null {
|
|
|
173
173
|
kind: "new",
|
|
174
174
|
projectRoot,
|
|
175
175
|
modulesSrc: path.join(projectRoot, "web", "src"),
|
|
176
|
-
distDir: path.join(projectRoot, "
|
|
177
|
-
apiDist: path.join(projectRoot, "
|
|
176
|
+
distDir: path.join(projectRoot, "dist", "web"),
|
|
177
|
+
apiDist: path.join(projectRoot, "dist", "api"),
|
|
178
178
|
watchTarget: path.join(projectRoot, "web", "src"),
|
|
179
179
|
};
|
|
180
180
|
}
|
|
@@ -186,7 +186,7 @@ export function tryResolveLayout(projectRoot: string): ProjectLayout | null {
|
|
|
186
186
|
projectRoot,
|
|
187
187
|
modulesSrc: path.join(projectRoot, "web"),
|
|
188
188
|
distDir: path.join(projectRoot, "build"),
|
|
189
|
-
apiDist: path.join(projectRoot, "
|
|
189
|
+
apiDist: path.join(projectRoot, "dist", "api"),
|
|
190
190
|
watchTarget: path.join(projectRoot, "web"),
|
|
191
191
|
};
|
|
192
192
|
}
|
|
@@ -5,8 +5,8 @@ import fs from "node:fs";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
8
|
-
import { DEFAULT_KEY_ID, isValidKeyId, keygenCommand
|
|
9
|
-
import {
|
|
8
|
+
import { DEFAULT_KEY_ID, isValidKeyId, keygenCommand } from "../../keygen";
|
|
9
|
+
import { signCommand } from "../../sign";
|
|
10
10
|
import { readPrivateKeyPem } from "../keygen";
|
|
11
11
|
import { fingerprintOfPublicKey, signCanonical as signCanonicalLocal, verifyCanonical } from "../sign";
|
|
12
12
|
|
|
@@ -19,11 +19,7 @@ afterEach(() => {
|
|
|
19
19
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
describe("ojm keygen
|
|
23
|
-
it("parseKeygenArgs:--id/--out 值式消费 + --yes 独立", () => {
|
|
24
|
-
expect(parseKeygenArgs(["--id", "acme.market", "--out", "./k", "--yes"]))
|
|
25
|
-
.toEqual({ id: "acme.market", out: "./k", yes: true });
|
|
26
|
-
});
|
|
22
|
+
describe("ojm keygen key_id 命名空间(C2-37)", () => {
|
|
27
23
|
it("isValidKeyId:<publisher>.<id> 合法 / 裸名·大写·空非法", () => {
|
|
28
24
|
expect(isValidKeyId("acme.market")).toBe(true);
|
|
29
25
|
expect(isValidKeyId("acme.dev.key-1")).toBe(true);
|
|
@@ -63,13 +59,6 @@ describe("ojm keygen 命令体(Ed25519 落盘 + 私钥红线)", () => {
|
|
|
63
59
|
});
|
|
64
60
|
});
|
|
65
61
|
|
|
66
|
-
describe("ojm sign 参数解析", () => {
|
|
67
|
-
it("parseSignArgs:位置参数 + --id/--key 值式消费 + --check 独立", () => {
|
|
68
|
-
expect(parseSignArgs(["pkg.tgz", "--id", "acme.market", "--key", "k.pem", "--check"]))
|
|
69
|
-
.toEqual({ positionals: ["pkg.tgz"], id: "acme.market", key: "k.pem", check: true });
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
62
|
describe("ojm sign 命令体(detached .sig,G4 载体)", () => {
|
|
74
63
|
const TGZ = path.join(tmp, "demo-solution-0.1.0.tgz");
|
|
75
64
|
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// 组装契约(对齐 tgz 内布局,契约 §2.9 + P1-9 staging copy 源定版):
|
|
3
3
|
// <root>/solution.yaml | manifest.yaml ← 必填(solution.yaml 优先,避开 oj 的 api 枚举冲突),契约校验 fail-fast 先于打包
|
|
4
4
|
// <root>/src/<模块>/solution.yaml | manifest.yaml ← 模块级方案:契约与所打包模块同位(同优先级回退)
|
|
5
|
-
// api
|
|
6
|
-
// web/
|
|
5
|
+
// dist/api/<name>-<version>/ ← oj build 产物原样 → tgz 内 api/<name>-<version>/(C2-31 分层核对)
|
|
6
|
+
// dist/web/modules/<name>/ ← 站点构建模块产物 → tgz 内 web/modules/<name>/(只入包方案自己的模块)
|
|
7
7
|
// 禁携 modules.json(C2-31 红线);产物确定性(C2-32)由 deterministic-tar 承担,此处断言端到端复现。
|
|
8
8
|
import { execFileSync } from "node:child_process";
|
|
9
9
|
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -32,18 +32,18 @@ afterEach(() => {
|
|
|
32
32
|
rmSync(out, { recursive: true, force: true });
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
/** 预置「已构建」产物:oj build(api
|
|
35
|
+
/** 预置「已构建」产物:oj build(dist/api/<name>-<version>/)与站点构建(dist/web/modules/) */
|
|
36
36
|
function makePrebuiltArtifacts(): void {
|
|
37
37
|
// oj build 产物(模块级 manifest 仅 oj 原生七键;name/version 与 bundle 级逐一相等,C2-31)
|
|
38
|
-
const apiDist = join(project, "api/
|
|
38
|
+
const apiDist = join(project, "dist/api/demo-solution-0.1.0");
|
|
39
39
|
mkdirSync(join(apiDist, "ping"), { recursive: true });
|
|
40
40
|
mkdirSync(join(apiDist, "migrations"), { recursive: true });
|
|
41
41
|
writeFileSync(join(apiDist, "manifest.yaml"), "name: demo-solution\ndesc: manifest.yaml 契约全字段合法夹具\nversion: 0.1.0\ntables:\n - note\n");
|
|
42
42
|
writeFileSync(join(apiDist, "schema.yaml"), "tables:\n note: {}\n");
|
|
43
43
|
writeFileSync(join(apiDist, "routes.js"), "export {};\n");
|
|
44
44
|
writeFileSync(join(apiDist, "ping/api.js"), "export async function ping() {}\n");
|
|
45
|
-
// 站点构建产物(web/
|
|
46
|
-
const webDist = join(project, "web/
|
|
45
|
+
// 站点构建产物(dist/web/modules/<name>/<version>/entry.js + css,对应 web.entry/css 声明)
|
|
46
|
+
const webDist = join(project, "dist/web/modules/demo-solution/0.1.0");
|
|
47
47
|
mkdirSync(webDist, { recursive: true });
|
|
48
48
|
writeFileSync(join(webDist, "entry.js"), "export default {};\n");
|
|
49
49
|
writeFileSync(join(webDist, "demo-solution.css"), ".demo{}\n");
|
|
@@ -64,7 +64,7 @@ function addWebSolution(name: string, version = "0.1.0"): void {
|
|
|
64
64
|
` entry: modules/${name}/${version}/entry.js`,
|
|
65
65
|
"",
|
|
66
66
|
].join("\n"));
|
|
67
|
-
const webDist = join(project, "web/
|
|
67
|
+
const webDist = join(project, "dist/web/modules", name, version);
|
|
68
68
|
mkdirSync(webDist, { recursive: true });
|
|
69
69
|
writeFileSync(join(webDist, "entry.js"), "export default {};\n");
|
|
70
70
|
}
|
|
@@ -85,7 +85,7 @@ describe("buildBundle · 正路径", () => {
|
|
|
85
85
|
|
|
86
86
|
it("只入包方案自己的前端模块:模板模块(home/login…)不随包", async () => {
|
|
87
87
|
// 安装器 deployWeb 把包内 web/modules 整树覆盖进目标站,模板产物随包会覆盖宿主同名前端
|
|
88
|
-
const hostWeb = join(project, "web/
|
|
88
|
+
const hostWeb = join(project, "dist/web/modules/home/0.1.0");
|
|
89
89
|
mkdirSync(hostWeb, { recursive: true });
|
|
90
90
|
writeFileSync(join(hostWeb, "entry.js"), "export default {};\n");
|
|
91
91
|
const names = tarList((await pack()).tgz);
|
|
@@ -149,18 +149,18 @@ describe("buildBundle · 正路径", () => {
|
|
|
149
149
|
});
|
|
150
150
|
|
|
151
151
|
it("产物树混入 modules.json → 打包自检拒绝(C2-31 红线)", async () => {
|
|
152
|
-
writeFileSync(join(project, "web/
|
|
152
|
+
writeFileSync(join(project, "dist/web/modules/demo-solution/0.1.0/modules.json"), "[]");
|
|
153
153
|
await expect(pack()).rejects.toThrow(/modules\.json/);
|
|
154
154
|
});
|
|
155
155
|
|
|
156
156
|
it("私钥/凭证混入包树 → 打包自检拒绝(P2-4 入包红线:*.pem/*.key/.env(.local…)/id_ed25519/id_rsa/*.p12/*.pfx)", async () => {
|
|
157
|
-
writeFileSync(join(project, "api/
|
|
157
|
+
writeFileSync(join(project, "dist/api/demo-solution-0.1.0/signing.pem"), "-----BEGIN PRIVATE KEY-----\n");
|
|
158
158
|
await expect(pack()).rejects.toThrow(/私钥\/凭证/);
|
|
159
|
-
rmSync(join(project, "api/
|
|
160
|
-
writeFileSync(join(project, "web/
|
|
159
|
+
rmSync(join(project, "dist/api/demo-solution-0.1.0/signing.pem"));
|
|
160
|
+
writeFileSync(join(project, "dist/web/modules/demo-solution/0.1.0/.env"), "SECRET=1\n");
|
|
161
161
|
await expect(pack()).rejects.toThrow(/私钥\/凭证/);
|
|
162
|
-
rmSync(join(project, "web/
|
|
163
|
-
writeFileSync(join(project, "web/
|
|
162
|
+
rmSync(join(project, "dist/web/modules/demo-solution/0.1.0/.env"));
|
|
163
|
+
writeFileSync(join(project, "dist/web/modules/demo-solution/0.1.0/.env.production"), "SECRET=1\n");
|
|
164
164
|
await expect(pack()).rejects.toThrow(/私钥\/凭证/);
|
|
165
165
|
});
|
|
166
166
|
});
|
|
@@ -218,7 +218,7 @@ describe("buildBundle · 多方案与内置排除(B3/B7/B8/B9/B10/B12)", ()
|
|
|
218
218
|
const dir = join(project, "src", "crm");
|
|
219
219
|
mkdirSync(dir, { recursive: true });
|
|
220
220
|
writeFileSync(join(dir, "manifest.yaml"), "name: crm\nversion: 0.1.0\nsummary: x\nchangelog: x\nweb:\n entry: modules/crm/0.1.0/entry.js\n");
|
|
221
|
-
const webDist = join(project, "web/
|
|
221
|
+
const webDist = join(project, "dist/web/modules/crm/0.1.0");
|
|
222
222
|
mkdirSync(webDist, { recursive: true });
|
|
223
223
|
writeFileSync(join(webDist, "entry.js"), "export default {};\n");
|
|
224
224
|
const results = await buildBundle(project, { out });
|
|
@@ -275,23 +275,23 @@ describe("buildBundle · 校验先于打包(fail-fast)", () => {
|
|
|
275
275
|
});
|
|
276
276
|
|
|
277
277
|
it("c2-31 分层核对:模块级 desc ≠ bundle summary → 放行(等值要求已移除)", async () => {
|
|
278
|
-
writeFileSync(join(project, "api/
|
|
278
|
+
writeFileSync(join(project, "dist/api/demo-solution-0.1.0/manifest.yaml"), "name: demo-solution\ndesc: 不一致描述\nversion: 0.1.0\n");
|
|
279
279
|
await expect(pack()).resolves.toMatchObject({ name: "demo-solution" });
|
|
280
280
|
});
|
|
281
281
|
|
|
282
|
-
it("c2-31 分层核对:api
|
|
283
|
-
rmSync(join(project, "api
|
|
284
|
-
await expect(pack()).rejects.toThrow(/api\/
|
|
282
|
+
it("c2-31 分层核对:dist/api 缺 <name>-<version>/ → 可读报错指明期望产物路径", async () => {
|
|
283
|
+
rmSync(join(project, "dist/api"), { recursive: true, force: true });
|
|
284
|
+
await expect(pack()).rejects.toThrow(/dist\/api\/demo-solution-0\.1\.0/);
|
|
285
285
|
});
|
|
286
286
|
|
|
287
287
|
it("web 段存在但站点产物缺失 → 可读报错指引先构建", async () => {
|
|
288
|
-
rmSync(join(project, "web
|
|
289
|
-
await expect(pack()).rejects.toThrow(/web\/
|
|
288
|
+
rmSync(join(project, "dist/web"), { recursive: true, force: true });
|
|
289
|
+
await expect(pack()).rejects.toThrow(/dist\/web\/modules/);
|
|
290
290
|
});
|
|
291
291
|
|
|
292
|
-
it("web/
|
|
293
|
-
rmSync(join(project, "web/
|
|
294
|
-
await expect(pack()).rejects.toThrow(/web\/
|
|
292
|
+
it("dist/web/modules 在但缺本方案目录 → 可读报错(不整树入包)", async () => {
|
|
293
|
+
rmSync(join(project, "dist/web/modules/demo-solution"), { recursive: true, force: true });
|
|
294
|
+
await expect(pack()).rejects.toThrow(/dist\/web\/modules\/demo-solution\//);
|
|
295
295
|
});
|
|
296
296
|
|
|
297
297
|
it("c2-12a:api.tables 与 schema.yaml 表集不一致 → 构建期拒绝", async () => {
|
|
@@ -6,7 +6,6 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
9
|
-
import { parseLifecycleArgs } from "../../args";
|
|
10
9
|
import { setEnabledCommand } from "../../enable";
|
|
11
10
|
import { installCommand, parseTarget, renderOutcome } from "../../install";
|
|
12
11
|
import { pollTask, resolveSite } from "../../site";
|
|
@@ -61,28 +60,6 @@ afterEach(() => {
|
|
|
61
60
|
vi.restoreAllMocks();
|
|
62
61
|
});
|
|
63
62
|
|
|
64
|
-
describe("parseLifecycleArgs · 参数解析", () => {
|
|
65
|
-
it("位置参数与值式 flag 分离:--url 等的值不被误当位置参数(P6 同款坑)", () => {
|
|
66
|
-
const a = parseLifecycleArgs(["demo-solution@0.1.0", "--url", "http://x:1", "--user", "u", "--pass", "p", "--locale", "en-US"]);
|
|
67
|
-
expect(a.positionals).toEqual(["demo-solution@0.1.0"]);
|
|
68
|
-
expect(a.url).toBe("http://x:1");
|
|
69
|
-
expect(a.user).toBe("u");
|
|
70
|
-
expect(a.pass).toBe("p");
|
|
71
|
-
expect(a.locale).toBe("en-US");
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("install 专属 --name/--version 覆盖位", () => {
|
|
75
|
-
const a = parseLifecycleArgs(["./a.tgz", "--name", "demo-solution", "--version", "0.2.0"]);
|
|
76
|
-
expect(a.positionals).toEqual(["./a.tgz"]);
|
|
77
|
-
expect(a.name).toBe("demo-solution");
|
|
78
|
-
expect(a.version).toBe("0.2.0");
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("未知 --flag 忽略不炸", () => {
|
|
82
|
-
expect(parseLifecycleArgs(["x", "--future"]).positionals).toEqual(["x"]);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
63
|
describe("resolveSite · 站点寻址(flag > 环境变量 > 缺省模板 serve 端口 9778)", () => {
|
|
87
64
|
const ENV_BACKUP = { ...process.env };
|
|
88
65
|
|
|
@@ -29,7 +29,7 @@ function makeModuleRootProject(builtin = true): void {
|
|
|
29
29
|
|
|
30
30
|
/** 给已生成的方案补站点产物,模拟「ojm build 之后」的打包前置 */
|
|
31
31
|
function addWebArtifacts(name: string, version = "0.1.0"): void {
|
|
32
|
-
const webDist = join(project, "web/
|
|
32
|
+
const webDist = join(project, "dist/web/modules", name, version);
|
|
33
33
|
mkdirSync(webDist, { recursive: true });
|
|
34
34
|
writeFileSync(join(webDist, "entry.js"), "export default {};\n");
|
|
35
35
|
}
|
|
@@ -34,7 +34,7 @@ afterEach(() => {
|
|
|
34
34
|
describe.skipIf(!smokeReady)("真机冒烟(需 OJM_SMOKE_URL/OJM_SMOKE_USER/OJM_SMOKE_PASS)", () => {
|
|
35
35
|
it("file 源安装 fixture-valid 包 → 终态 done", { timeout: 600_000 }, async () => {
|
|
36
36
|
// 先出可安装 tgz(同 bundle.test.ts:预置已构建产物再 --bundle 组装)
|
|
37
|
-
const apiDist = join(project, "api/
|
|
37
|
+
const apiDist = join(project, "dist/api/demo-solution-0.1.0");
|
|
38
38
|
const { mkdirSync, writeFileSync } = await import("node:fs");
|
|
39
39
|
mkdirSync(join(apiDist, "ping"), { recursive: true });
|
|
40
40
|
// 迁移 SQL 随夹具 src 真源(0001__init.sql 建 note 表);B-11 硬验收:appliedSeq > 0 可断言
|
|
@@ -45,7 +45,7 @@ describe.skipIf(!smokeReady)("真机冒烟(需 OJM_SMOKE_URL/OJM_SMOKE_USER/OJ
|
|
|
45
45
|
cpSync(join(FIXTURE_VALID, "api/src/demo-solution/schema.yaml"), join(apiDist, "schema.yaml"));
|
|
46
46
|
writeFileSync(join(apiDist, "routes.js"), "export {};\n");
|
|
47
47
|
writeFileSync(join(apiDist, "ping/api.js"), "export async function ping() {}\n");
|
|
48
|
-
const webDist = join(project, "web/
|
|
48
|
+
const webDist = join(project, "dist/web/modules/demo-solution/0.1.0");
|
|
49
49
|
mkdirSync(webDist, { recursive: true });
|
|
50
50
|
writeFileSync(join(webDist, "entry.js"), "export default {};\n");
|
|
51
51
|
writeFileSync(join(webDist, "demo-solution.css"), ".demo{}\n");
|
|
@@ -100,6 +100,30 @@ describe("given 一个装了 oj 二进制的源工程 When 自举隔离站点",
|
|
|
100
100
|
await site.cleanup();
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
+
it("then 起站 api 路径 = 构建产物实际位置 <root>/dist/api(module-root;旧写死 api/dist 起站即挂)", async () => {
|
|
104
|
+
const src = newSourceProject();
|
|
105
|
+
const { started, starter } = fakeStarter();
|
|
106
|
+
|
|
107
|
+
const site = await bootstrapIsolatedSite({ projectRoot: src, starter });
|
|
108
|
+
|
|
109
|
+
// starter 第三参是 oj serve 的 --api-path:必须与 buildBackend 产物落点同源
|
|
110
|
+
// (layout.ts module-root 口径 apiDist = <root>/dist/api;写死 api/dist 会在
|
|
111
|
+
// mounts 校验报 No such file or directory,2026-10-23 演练实踩)
|
|
112
|
+
expect(started[0]?.apiSrc).toBe(join(site.root, "dist", "api"));
|
|
113
|
+
await site.cleanup();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("then 种子管理员 = superadmin(seed 账号名=角色名口径;admin 已被解绑,用它会 403)", async () => {
|
|
117
|
+
const src = newSourceProject();
|
|
118
|
+
const { starter } = fakeStarter();
|
|
119
|
+
|
|
120
|
+
const site = await bootstrapIsolatedSite({ projectRoot: src, starter });
|
|
121
|
+
|
|
122
|
+
expect(site.admin.user).toBe("superadmin");
|
|
123
|
+
expect(site.admin.pass).toBe("123456");
|
|
124
|
+
await site.cleanup();
|
|
125
|
+
});
|
|
126
|
+
|
|
103
127
|
it("then CDN 目录在临时根内且已落好 fixture 包与索引", async () => {
|
|
104
128
|
const src = newSourceProject();
|
|
105
129
|
const { starter } = fakeStarter();
|
|
@@ -8,7 +8,7 @@ import { tmpdir } from "node:os";
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { describe, expect, it } from "vitest";
|
|
10
10
|
|
|
11
|
-
import { setupMarketUrl } from "../setup";
|
|
11
|
+
import { loginPasswordAfterSetup, setupMarketUrl } from "../setup";
|
|
12
12
|
|
|
13
13
|
const MOUNTS = " - prefix: /api\n api: src\n - prefix: /cdn\n web: ../cdn\n";
|
|
14
14
|
const CONFIG = `mounts:\n${MOUNTS}server:\n host: 127.0.0.1\n port: 9778\n`;
|
|
@@ -112,3 +112,10 @@ describe("given 改密后写设置仍被拒 When setup", () => {
|
|
|
112
112
|
.toThrow(/must_change|权限|设置/);
|
|
113
113
|
});
|
|
114
114
|
});
|
|
115
|
+
|
|
116
|
+
describe("given setup 返回 changedPassword When 选 setup 后的登录口令", () => {
|
|
117
|
+
it("then 改密已发生用 newPass,未改密用原种子口令(must_change=0 是当前 seed 常态)", () => {
|
|
118
|
+
expect(loginPasswordAfterSetup({ siteUrl: "", cdnBaseUrl: "", changedPassword: true }, "new-pass", "123456")).toBe("new-pass");
|
|
119
|
+
expect(loginPasswordAfterSetup({ siteUrl: "", cdnBaseUrl: "", changedPassword: false }, "new-pass", "123456")).toBe("123456");
|
|
120
|
+
});
|
|
121
|
+
});
|