@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/src/index.ts
CHANGED
|
@@ -1,203 +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 { runPreview } from "./preview";
|
|
11
|
-
import { getCommandHelp, resolveHelpRequest, usageText } from "./usage";
|
|
12
|
-
import { vendorCommand } from "./vendor";
|
|
2
|
+
import { buildCli, localizeCacError, reportCliError, rewriteHelpArgv, unknownCommandResult } from "./cli";
|
|
3
|
+
import { isKnownCommand, resolveLang, usageText } from "./usage";
|
|
13
4
|
import { readCliVersion } from "./versions";
|
|
14
5
|
|
|
15
|
-
/** ojm → oj 透传子命令(其余参数原样转发,见 src/oj-cmd.ts) */
|
|
16
|
-
const OJ_PASSTHROUGH = new Set(["test", "exec", "openapi", "migrate", "schema"]);
|
|
17
|
-
|
|
18
|
-
const [command] = process.argv.slice(2);
|
|
19
|
-
|
|
20
|
-
function usage(): never {
|
|
21
|
-
console.log(usageText());
|
|
22
|
-
process.exit(command ? 1 : 0);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
6
|
async function main() {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
// 帮助请求:`ojm <cmd> --help` / `ojm help <cmd>` / 顶层 `ojm --help`
|
|
29
|
-
// 必须在 switch 之前拦截,否则会真正执行子命令(如 `ojm market setup --help` 会去建 CDN)。
|
|
30
|
-
const helpReq = resolveHelpRequest();
|
|
31
|
-
if (helpReq !== null) {
|
|
32
|
-
if (!helpReq.cmd) {
|
|
33
|
-
console.log(usageText());
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
const text = getCommandHelp(helpReq.cmd, helpReq.sub);
|
|
37
|
-
if (text) {
|
|
38
|
-
console.log(text);
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
console.error(`[ojm] 未知命令:${helpReq.cmd}${helpReq.sub ? ` ${helpReq.sub}` : ""}`);
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
7
|
+
const lang = resolveLang();
|
|
8
|
+
const argv = process.argv.slice(2);
|
|
44
9
|
|
|
45
|
-
// 顶层 --version / -V
|
|
46
|
-
if (
|
|
10
|
+
// 顶层 --version / -V:裸版本号(与 ojm info 同源);不经 cac,避免其 platformInfo 后缀
|
|
11
|
+
if (argv[0] === "--version" || argv[0] === "-V") {
|
|
47
12
|
console.log(readCliVersion());
|
|
48
13
|
return;
|
|
49
14
|
}
|
|
50
15
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const previewPort = Number.isInteger(previewPortArg) && previewPortArg > 0 ? previewPortArg : undefined;
|
|
72
|
-
await runPreview(projectRoot, { port: previewPort });
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
|
-
case "dev": {
|
|
76
|
-
const portArg = Number(process.argv[3]);
|
|
77
|
-
const port = Number.isFinite(portArg) && portArg > 0 ? portArg : 5174;
|
|
78
|
-
await devServer(projectRoot, { port });
|
|
79
|
-
break;
|
|
80
|
-
}
|
|
81
|
-
case "info":
|
|
82
|
-
await printInfo(projectRoot);
|
|
83
|
-
break;
|
|
84
|
-
case "vendor": {
|
|
85
|
-
const { force, tag } = parseVendorArgs(process.argv.slice(3));
|
|
86
|
-
await vendorCommand(projectRoot, { force, tag });
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
case "api": {
|
|
90
|
-
const { dir, check, docs, exempt } = parseApiArgs(process.argv.slice(3));
|
|
91
|
-
// S5:显式目录必须先存在——否则静默在错的目录下生成/对账
|
|
92
|
-
if (dir && !fs.existsSync(path.resolve(dir)))
|
|
93
|
-
throw new Error(`[ojm-api] 项目目录不存在:${dir}`);
|
|
94
|
-
const apiRoot = dir ? path.resolve(dir) : projectRoot;
|
|
95
|
-
if (docs) {
|
|
96
|
-
const { runApiDocs } = await import("./contract/run");
|
|
97
|
-
const out = await runApiDocs(apiRoot);
|
|
98
|
-
console.log(`[ojm-api] 文档站已生成:${path.relative(projectRoot, out)}(浏览器直接打开即可)`);
|
|
99
|
-
break;
|
|
100
|
-
}
|
|
101
|
-
if (check) {
|
|
102
|
-
const { checkApi } = await import("./contract/check");
|
|
103
|
-
// --exempt <path>:相对路径按项目根解析;缺省按布局找 .ojm-api-exempt.json(工程根 → api/)
|
|
104
|
-
// (旧名 api/.ram-api-exempt.json 仍被读取,R4)
|
|
105
|
-
const { violations, hints } = await checkApi({ cwd: apiRoot, exempt: exempt ? path.resolve(projectRoot, exempt) : undefined });
|
|
106
|
-
for (const v of violations)
|
|
107
|
-
console[v.level === "error" ? "error" : "warn"](`${v.level === "error" ? "✗" : "⚠"} ${v.message}`);
|
|
108
|
-
for (const h of hints)
|
|
109
|
-
console.log(h);
|
|
110
|
-
if (violations.length === 0)
|
|
111
|
-
console.log("[ojm-api] --check 通过:生成物同步、route 双向对账、routes.js 均无 drift。");
|
|
112
|
-
const errors = violations.filter(v => v.level === "error").length;
|
|
113
|
-
console.log(`[ojm-api] --check 结果:${errors} error / ${violations.length - errors} warn`);
|
|
114
|
-
if (errors > 0)
|
|
115
|
-
process.exit(1);
|
|
116
|
-
break;
|
|
117
|
-
}
|
|
118
|
-
const { runApi } = await import("./contract/run");
|
|
119
|
-
const result = await runApi({ cwd: apiRoot });
|
|
120
|
-
console.log(`[ojm-api] 契约 ${result.contracts} 份;写入 ${result.written.length} 个文件,跳过(未变)${result.skipped.length} 个;stub 新建 ${result.stubs.created} / 更新 ${result.stubs.updated} / 跳过 ${result.stubs.skipped}`);
|
|
121
|
-
for (const p of result.written)
|
|
122
|
-
console.log(` + ${path.relative(apiRoot, p)}`);
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
case "merge":
|
|
126
|
-
await mergeManifests(process.argv[3] ?? "", process.argv.slice(4));
|
|
127
|
-
break;
|
|
128
|
-
case "keygen": {
|
|
129
|
-
// 签名密钥对生成(P2-4,作者工具链):缺省 ~/.ojm/keys/<id>,工程树内落盘需 --yes
|
|
130
|
-
const { parseKeygenArgs } = await import("./keygen");
|
|
131
|
-
const { keygenCommand } = await import("./keygen");
|
|
132
|
-
const { id, out, yes } = parseKeygenArgs(process.argv.slice(3));
|
|
133
|
-
keygenCommand({ id, out, yes });
|
|
134
|
-
break;
|
|
135
|
-
}
|
|
136
|
-
case "sign": {
|
|
137
|
-
// detached 侧车签名(P2-4,G4 载体):ojm sign <tgz> [--id <publisher.id>] [--key <pem>] [--check]
|
|
138
|
-
const { parseSignArgs } = await import("./sign");
|
|
139
|
-
const { signCommand } = await import("./sign");
|
|
140
|
-
const { positionals, id, key, check } = parseSignArgs(process.argv.slice(3));
|
|
141
|
-
signCommand(positionals[0] ?? "", { id, key, check });
|
|
142
|
-
break;
|
|
143
|
-
}
|
|
144
|
-
case "install": {
|
|
145
|
-
// 方案包安装瘦客户端(P1-13,D6):file 上传 / CDN 市场源 → 轮询任务至终态
|
|
146
|
-
const { positionals, url, user, pass, locale, name, version } = parseLifecycleArgs(process.argv.slice(3));
|
|
147
|
-
const { installCommand } = await import("./install");
|
|
148
|
-
await installCommand(positionals[0] ?? "", { url, user, pass, locale, name, version });
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
case "enable":
|
|
152
|
-
case "disable": {
|
|
153
|
-
// 生命周期开关瘦客户端(P1-13,D8):站点侧 setEnabled 四处同写
|
|
154
|
-
const { positionals, url, user, pass } = parseLifecycleArgs(process.argv.slice(3));
|
|
155
|
-
const { setEnabledCommand } = await import("./enable");
|
|
156
|
-
await setEnabledCommand(positionals[0] ?? "", command === "enable", { url, user, pass });
|
|
157
|
-
break;
|
|
158
|
-
}
|
|
159
|
-
case "uninstall": {
|
|
160
|
-
// 卸载瘦客户端(P1-15a,缺口 B):站点侧 runUninstall 四处同删 + 权限级联
|
|
161
|
-
const { positionals, url, user, pass } = parseLifecycleArgs(process.argv.slice(3));
|
|
162
|
-
const { uninstallCommand } = await import("./uninstall");
|
|
163
|
-
await uninstallCommand(positionals[0] ?? "", { url, user, pass });
|
|
164
|
-
break;
|
|
165
|
-
}
|
|
166
|
-
case "trust": {
|
|
167
|
-
// 信任库运维瘦客户端(P2-2,C2-38/39):ls / add <key_id> <pubkey_hex> / revoke <key_id> / confirm --task
|
|
168
|
-
const { positionals, url, user, pass, task } = parseLifecycleArgs(process.argv.slice(3));
|
|
169
|
-
const { trustCommand } = await import("./trust");
|
|
170
|
-
await trustCommand(positionals[0] ?? "", positionals.slice(1), { url, user, pass, task });
|
|
171
|
-
break;
|
|
172
|
-
}
|
|
173
|
-
case "publish": {
|
|
174
|
-
// 索引发布(P3-5/6/7,作者工具链):重算 bundles/manifests 索引 + detached .sig 签名写回
|
|
175
|
-
const { parsePublishArgs } = await import("./publish");
|
|
176
|
-
const { publishCommand } = await import("./publish");
|
|
177
|
-
const { positionals, id, key, check } = parsePublishArgs(process.argv.slice(3));
|
|
178
|
-
publishCommand(positionals[0] ?? "", { id, key, check });
|
|
179
|
-
break;
|
|
180
|
-
}
|
|
181
|
-
case "market": {
|
|
182
|
-
// 本地模拟 CDN 与市场链路(setup / publish / build-index)
|
|
183
|
-
const { runMarket } = await import("./market/command");
|
|
184
|
-
await runMarket(process.argv.slice(3));
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
187
|
-
default: {
|
|
188
|
-
if (command && OJ_PASSTHROUGH.has(command)) {
|
|
189
|
-
const { runOjSubcommand } = await import("./oj-cmd");
|
|
190
|
-
runOjSubcommand(projectRoot, command, process.argv.slice(3));
|
|
191
|
-
break;
|
|
192
|
-
}
|
|
193
|
-
usage();
|
|
194
|
-
}
|
|
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);
|
|
195
36
|
}
|
|
37
|
+
if (argv.length === 0)
|
|
38
|
+
console.log(usageText(lang));
|
|
196
39
|
}
|
|
197
40
|
|
|
198
|
-
main().catch((error: unknown) => {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
console.error(`[ojm] ${error instanceof Error ? ((process.env.OJM_DEBUG ?? process.env.RAM_DEBUG) ? error.stack : error.message) : String(error)}`);
|
|
202
|
-
process.exit(1);
|
|
203
|
-
});
|
|
41
|
+
main().catch((error: unknown) => reportCliError(localizeCacError(error, resolveLang()), (code) => {
|
|
42
|
+
process.exit(code);
|
|
43
|
+
}));
|
package/src/info.ts
CHANGED
|
@@ -12,8 +12,9 @@ import fs from "node:fs";
|
|
|
12
12
|
|
|
13
13
|
import http from "node:http";
|
|
14
14
|
import path from "node:path";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { readBuiltinModules } from "./builtin";
|
|
16
|
+
import { loadModulesConfig, loadSiteConfig, mergeModuleList } from "./config";
|
|
17
|
+
import { resolveConfigPath, scanModules, tryResolveLayout } from "./layout";
|
|
17
18
|
import { mergeModuleManifests } from "./manifest";
|
|
18
19
|
import { probeOjCaps } from "./oj-caps";
|
|
19
20
|
import { readOjApiPrefix, readOjServerField } from "./oj-config";
|
|
@@ -91,7 +92,50 @@ export function realOjObservability(projectRoot: string): OjObservability {
|
|
|
91
92
|
};
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
export
|
|
95
|
+
export interface PrintInfoOptions {
|
|
96
|
+
/** 展开框架自带模块(默认折叠为一行计数) */
|
|
97
|
+
all?: boolean
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 模块清单条目解析:module-root 用「扫描 + 登记覆盖」并集(与 build 同源,
|
|
102
|
+
* 未登记的用户模块也可见——旧实现只读 web.config.ts 会漏掉它们);历史布局
|
|
103
|
+
* 维持登记清单(loadModulesConfig)。
|
|
104
|
+
*/
|
|
105
|
+
async function resolveModuleEntries(projectRoot: string): Promise<{ name: string, entry: string, enabled: boolean }[]> {
|
|
106
|
+
const layout = tryResolveLayout(projectRoot);
|
|
107
|
+
if (layout?.kind === "module-root") {
|
|
108
|
+
const config = await loadSiteConfig(projectRoot);
|
|
109
|
+
return mergeModuleList(scanModules(layout), config.modules, projectRoot);
|
|
110
|
+
}
|
|
111
|
+
const config = await loadModulesConfig(projectRoot);
|
|
112
|
+
return config.modules.map(m => ({ name: m.name, entry: m.entry, enabled: m.enabled !== false }));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 清单渲染:自带模块默认折叠(--all 展开);坏名单文件告警降级为平铺,info 不失败 */
|
|
116
|
+
function renderModuleLines(entries: { name: string, entry: string, enabled: boolean }[], projectRoot: string, all: boolean): string {
|
|
117
|
+
let builtin = new Set<string>();
|
|
118
|
+
try {
|
|
119
|
+
builtin = readBuiltinModules(projectRoot);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
console.warn(`[ojm] ⚠️ ${error instanceof Error ? error.message : String(error)}——本次按无内置名单渲染。`);
|
|
123
|
+
}
|
|
124
|
+
const fmt = (m: { name: string, entry: string, enabled: boolean }) => ` · ${m.name}(${m.entry})${m.enabled ? "" : " [disabled]"}`;
|
|
125
|
+
const own = entries.filter(m => builtin.has(m.name));
|
|
126
|
+
const user = entries.filter(m => !builtin.has(m.name));
|
|
127
|
+
const lines = user.map(fmt);
|
|
128
|
+
if (own.length > 0) {
|
|
129
|
+
if (all)
|
|
130
|
+
lines.push(...own.map(fmt));
|
|
131
|
+
else
|
|
132
|
+
lines.push(` 框架自带:${own.length} 个(ojm info --all 展开)`);
|
|
133
|
+
}
|
|
134
|
+
// 零模块时留占位行,避免清单段整段空白像渲染故障
|
|
135
|
+
return lines.length > 0 ? lines.join("\n") : "(无 web 模块)";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function printInfo(projectRoot: string, oj: OjObservability = realOjObservability(projectRoot), opts: PrintInfoOptions = {}): Promise<void> {
|
|
95
139
|
const cliVersion = readPkgVersion(path.join(projectRoot, "node_modules/@oj-module/cli/package.json"));
|
|
96
140
|
const runtimeVersion = readPkgVersion(path.join(projectRoot, "node_modules/@oj-module/runtime/package.json"));
|
|
97
141
|
|
|
@@ -107,10 +151,8 @@ export async function printInfo(projectRoot: string, oj: OjObservability = realO
|
|
|
107
151
|
|
|
108
152
|
let moduleLines = "(web.config.ts 加载失败或无模块)";
|
|
109
153
|
try {
|
|
110
|
-
const
|
|
111
|
-
moduleLines =
|
|
112
|
-
.map(m => ` · ${m.name}(${m.entry})${m.enabled === false ? " [disabled]" : ""}`)
|
|
113
|
-
.join("\n");
|
|
154
|
+
const entries = await resolveModuleEntries(projectRoot);
|
|
155
|
+
moduleLines = renderModuleLines(entries, projectRoot, opts.all ?? false);
|
|
114
156
|
}
|
|
115
157
|
catch (error) {
|
|
116
158
|
moduleLines = `(加载失败:${(error as Error).message})`;
|
package/src/init.ts
CHANGED
|
@@ -17,6 +17,7 @@ import process from "node:process";
|
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
import yaml from "yaml";
|
|
19
19
|
import { ALIAS_PATHS_FILE, syncAliasPaths } from "./alias-paths";
|
|
20
|
+
import { BUILTIN_LIST_FILE, writeBuiltinList } from "./builtin";
|
|
20
21
|
import { resolveCertDir, resolveConfigPath } from "./layout";
|
|
21
22
|
import { probeOjCaps } from "./oj-caps";
|
|
22
23
|
import { mintDevCert } from "./oj-cert";
|
|
@@ -125,6 +126,14 @@ export async function initProject(destDir: string, opts: InitOptions = {}): Prom
|
|
|
125
126
|
}, report, include, templatesDir);
|
|
126
127
|
pruneUnincludedApiPaths(destDir, include, report);
|
|
127
128
|
|
|
129
|
+
// 1a) src/.ojm-builtin.json:脚手架自带模块身份(名单派生自上面的白名单,单一真源)。
|
|
130
|
+
// copyIfAbsent:旧工程重跑 init 时缺失才补写,已存在不覆盖用户手改。
|
|
131
|
+
const builtinStatus = writeBuiltinList(destDir, include);
|
|
132
|
+
if (builtinStatus === "created")
|
|
133
|
+
report.created.push(`src/${BUILTIN_LIST_FILE}`);
|
|
134
|
+
else if (builtinStatus === "skipped")
|
|
135
|
+
report.skipped.push(`${BUILTIN_LIST_FILE}(已存在,不覆盖用户手改)`);
|
|
136
|
+
|
|
128
137
|
// 1b) tsconfig.paths.json:把 oj 别名 `#/<模块名>/…` 投影成 TS 的 paths。
|
|
129
138
|
// 必须**现场生成**而不是随模板拷贝——模板兼作 demo 工程(多出 demo/home 两个
|
|
130
139
|
// 模块),而用户工程只有白名单里的那些;照拷会让 paths 指向不存在的目录。
|
|
@@ -233,7 +242,7 @@ export async function initProject(destDir: string, opts: InitOptions = {}): Prom
|
|
|
233
242
|
console.log(`[ojm] · 跳过 ${item}`);
|
|
234
243
|
for (const item of report.warnings)
|
|
235
244
|
console.warn(`[ojm] ! ${item}`);
|
|
236
|
-
console.log("[ojm] 下一步:pnpm install && pnpm dev(登录
|
|
245
|
+
console.log("[ojm] 下一步:pnpm install && pnpm dev(登录 superadmin / 123456,角色账号见 devkit 手册 §1.1)");
|
|
237
246
|
}
|
|
238
247
|
|
|
239
248
|
/** 默认密封器:bin/oj secret keygen + seal(失败返回 null,不抛) */
|
|
@@ -480,10 +489,8 @@ logs/
|
|
|
480
489
|
# 现场签发的开发证书与凭据私钥(ojm dev 缺证时自动签发,见 oj-cert.ts);私钥绝不入仓
|
|
481
490
|
config/*.pem
|
|
482
491
|
config/cert.jws
|
|
483
|
-
#
|
|
484
|
-
|
|
485
|
-
web/dist/
|
|
486
|
-
modules/dist/
|
|
492
|
+
# 构建产物(ojm build:站点 dist/web + 后端 release dist/api)
|
|
493
|
+
dist/
|
|
487
494
|
`;
|
|
488
495
|
|
|
489
496
|
function ensureGitignore(destDir: string, report: InitReport): void {
|
|
@@ -576,7 +583,8 @@ function mergePackageJson(existing: Record<string, any>, generated: Record<strin
|
|
|
576
583
|
return merged;
|
|
577
584
|
}
|
|
578
585
|
|
|
579
|
-
|
|
586
|
+
/** 生成脚手架 package.json(导出供守卫测试:scripts ⊆ 模板 scripts,防双写漂移) */
|
|
587
|
+
export function generatePackageJson(cliRoot: string, projectName: string) {
|
|
580
588
|
const { matrix: hostVersions } = resolveVersionMatrix(cliRoot);
|
|
581
589
|
const cliPkg = JSON.parse(fs.readFileSync(path.join(cliRoot, "package.json"), "utf-8"));
|
|
582
590
|
|
|
@@ -626,11 +634,17 @@ function generatePackageJson(cliRoot: string, projectName: string) {
|
|
|
626
634
|
type: "module",
|
|
627
635
|
private: true,
|
|
628
636
|
scripts: {
|
|
629
|
-
dev: "ojm dev",
|
|
630
|
-
build: "ojm build",
|
|
631
|
-
preview: "ojm preview",
|
|
632
|
-
info: "ojm info",
|
|
633
|
-
typecheck: "tsc --noEmit -p tsconfig.json",
|
|
637
|
+
"dev": "ojm dev",
|
|
638
|
+
"build": "ojm build",
|
|
639
|
+
"preview": "ojm preview",
|
|
640
|
+
"info": "ojm info",
|
|
641
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
642
|
+
// typecheck:api 指向 src/tsconfig.json(后端工程面,白名单收录)——
|
|
643
|
+
// 早期生成器漏发这条(与模板双写漂移),补上并钉守卫测试
|
|
644
|
+
"typecheck:api": "tsc --noEmit -p src/tsconfig.json",
|
|
645
|
+
// 多方案(phase 2/3):打包全部用户方案(自带自动排除)/ 生成新方案骨架
|
|
646
|
+
"bundle": "ojm build --bundle",
|
|
647
|
+
"new": "ojm new",
|
|
634
648
|
},
|
|
635
649
|
devDependencies: devDeps,
|
|
636
650
|
};
|
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
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// BDD:脚手架自带模块身份(`src/.ojm-builtin.json`)。
|
|
2
|
+
// 消费契约:bundle 排除打包 / ojm info 折叠 / ojm new 拒名;文件缺失 → 空名单(旧工程兼容)。
|
|
3
|
+
// 用例对应设计 docs/plans/2026-10-22-1640-multi-solution-bundle-design.md:
|
|
4
|
+
// B12(坏文件 fail-fast)/ T1(名单 == 白名单派生集)/ T2(copyIfAbsent,不覆盖用户手改)
|
|
5
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
9
|
+
import { BUILTIN_LIST_FILE, deriveBuiltinModules, readBuiltinModules, writeBuiltinList } from "../../builtin";
|
|
10
|
+
|
|
11
|
+
let project: string;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
project = mkdtempSync(join(tmpdir(), "ojm-builtin-"));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
rmSync(project, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function writeList(content: string): void {
|
|
22
|
+
mkdirSync(join(project, "src"), { recursive: true });
|
|
23
|
+
writeFileSync(join(project, "src", BUILTIN_LIST_FILE), content);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("readBuiltinModules", () => {
|
|
27
|
+
it("文件缺失 → 空名单(旧工程/框架仓无内置概念,行为退化兼容)", () => {
|
|
28
|
+
expect(readBuiltinModules(project).size).toBe(0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("合法文件 → 返回 modules 集合", () => {
|
|
32
|
+
writeList(JSON.stringify({ modules: ["auth", "home"] }));
|
|
33
|
+
expect([...readBuiltinModules(project)].sort()).toEqual(["auth", "home"]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("b12 坏 JSON → fail-fast,报错含文件路径与错因(不静默降级)", () => {
|
|
37
|
+
writeList("{ not json");
|
|
38
|
+
expect(() => readBuiltinModules(project)).toThrow(new RegExp(`${BUILTIN_LIST_FILE.replace(".", "\\.")}`));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("b12 modules 非数组 → fail-fast", () => {
|
|
42
|
+
writeList(JSON.stringify({ modules: "auth" }));
|
|
43
|
+
expect(() => readBuiltinModules(project)).toThrow(/modules/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("b12 modules 含非字符串项 → fail-fast", () => {
|
|
47
|
+
writeList(JSON.stringify({ modules: ["auth", 42] }));
|
|
48
|
+
expect(() => readBuiltinModules(project)).toThrow(/modules/);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("deriveBuiltinModules(名单派生自 init.include.yaml,单一真源)", () => {
|
|
53
|
+
it("t1 取 src/<name>/(api|web) 条目的模块名(含 web-only 模块),忽略无关路径", () => {
|
|
54
|
+
const include = new Set([
|
|
55
|
+
"src/auth/api",
|
|
56
|
+
"src/notification/api",
|
|
57
|
+
"src/login/web/entry.ts",
|
|
58
|
+
"src/oj-market/web/pages",
|
|
59
|
+
"src/styles.css",
|
|
60
|
+
"src/tsconfig.json",
|
|
61
|
+
"config.yaml",
|
|
62
|
+
"web.config.ts",
|
|
63
|
+
]);
|
|
64
|
+
expect([...deriveBuiltinModules(include)].sort())
|
|
65
|
+
.toEqual(["auth", "login", "notification", "oj-market"]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("白名单无模块条目 → 空集(不凭空造身份)", () => {
|
|
69
|
+
expect(deriveBuiltinModules(new Set(["config.yaml", "README.md"])).size).toBe(0);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("writeBuiltinList(init 接线:copyIfAbsent)", () => {
|
|
74
|
+
it("t1 写入派生名单(排序)+ 自注释头,返回 created", () => {
|
|
75
|
+
const include = new Set(["src/home/web/entry.ts", "src/auth/api", "src/login/web"]);
|
|
76
|
+
expect(writeBuiltinList(project, include)).toBe("created");
|
|
77
|
+
const p = join(project, "src", BUILTIN_LIST_FILE);
|
|
78
|
+
expect(existsSync(p)).toBe(true);
|
|
79
|
+
const doc = JSON.parse(readFileSync(p, "utf-8"));
|
|
80
|
+
expect(doc.modules).toEqual(["auth", "home", "login"]);
|
|
81
|
+
expect(typeof doc["//"]).toBe("string");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("t2 已存在 → 返回 skipped 且内容原样不动(用户手改权)", () => {
|
|
85
|
+
const include = new Set(["src/auth/api"]);
|
|
86
|
+
expect(writeBuiltinList(project, include)).toBe("created");
|
|
87
|
+
const p = join(project, "src", BUILTIN_LIST_FILE);
|
|
88
|
+
writeFileSync(p, JSON.stringify({ modules: ["my-edit"] }));
|
|
89
|
+
expect(writeBuiltinList(project, include)).toBe("skipped");
|
|
90
|
+
expect(JSON.parse(readFileSync(p, "utf-8")).modules).toEqual(["my-edit"]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("派生集为空 → 不写文件,返回 none", () => {
|
|
94
|
+
expect(writeBuiltinList(project, new Set(["config.yaml"]))).toBe("none");
|
|
95
|
+
expect(existsSync(join(project, "src", BUILTIN_LIST_FILE))).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
});
|