@oj-module/cli 0.2.23 → 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 +7 -5
- package/devkit/module-dev-and-cdn-playground.md +10 -10
- package/package.json +4 -4
- package/shell-dist/assets/runtime.js +4 -3
- 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 +4 -4
- 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/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/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
|
@@ -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
|
*
|
package/src/bundle.ts
CHANGED
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
// <project>/solution.yaml | manifest.yaml → 工程根契约(旧单方案写法)
|
|
24
24
|
// <project>/src/<模块>/solution.yaml | manifest.yaml → 模块级契约(绝不进 <模块>/api/,§1.2 红线)
|
|
25
25
|
// 任一处均 → <name>-<version>/manifest.yaml(Form A;tgz 内名固定为 manifest.yaml)
|
|
26
|
-
// <project>/api
|
|
27
|
-
// <project>/web/
|
|
26
|
+
// <project>/dist/api/<name>-<version>/ → <name>-<version>/api/<name>-<version>/(oj build 产物原样,C2-31 分层核对)
|
|
27
|
+
// <project>/dist/web/modules/<name>/ → <name>-<version>/web/modules/<name>/(只入包方案自己的模块;
|
|
28
28
|
// web.entry/css 存在性复核)
|
|
29
29
|
// 红线:modules.json 禁入包(C2-31——该清单由安装器在目标站点 upsert,防覆盖宿主清单);
|
|
30
30
|
// 文件面契约(tables↔schema.yaml、locales 双语齐备)构建期 fail-fast(契约 §4.1 构建列)。
|
|
@@ -257,8 +257,8 @@ async function planBundle(projectRoot: string, layout: ProjectLayout, target: Bu
|
|
|
257
257
|
|
|
258
258
|
/**
|
|
259
259
|
* 组装段(写盘):staging 单根 <name>-<version>/ → 红线自检 → 确定性 tgz + sha256。
|
|
260
|
-
* 入包内容与 §11.1 一致:只组装本方案的模块产物(
|
|
261
|
-
*
|
|
260
|
+
* 入包内容与 §11.1 一致:只组装本方案的模块产物(layout.apiDist/<name>-<version>/ 与
|
|
261
|
+
* layout.distDir/modules/<name>/),模板/宿主模块不随包。
|
|
262
262
|
*/
|
|
263
263
|
async function assembleBundle(projectRoot: string, layout: ProjectLayout, plan: BundlePlan, outDir: string): Promise<BundleBuildResult> {
|
|
264
264
|
const m = plan.manifest;
|
|
@@ -274,7 +274,7 @@ async function assembleBundle(projectRoot: string, layout: ProjectLayout, plan:
|
|
|
274
274
|
|
|
275
275
|
// api:oj build 产物原样入包(编译编排不在此重跑——作者先 oj build,本命令只校验与组装)
|
|
276
276
|
if (m.api !== undefined) {
|
|
277
|
-
const built = path.join(
|
|
277
|
+
const built = path.join(layout.apiDist, `${name}-${version}`);
|
|
278
278
|
if (!fs.existsSync(built)) {
|
|
279
279
|
throw new Error(`[ojm] 方案包构建失败:缺少 oj build 产物 ${path.relative(projectRoot, built)}(manifest 声明了 api 段;先对 api/ 运行 oj build,产物原样入包)`);
|
|
280
280
|
}
|
|
@@ -286,18 +286,18 @@ async function assembleBundle(projectRoot: string, layout: ProjectLayout, plan:
|
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
// web:只入包方案自己的模块产物(web 段缺省 = 纯后端方案,整段跳过)。
|
|
289
|
-
// 为何不是整树:web/
|
|
289
|
+
// 为何不是整树:dist/web/modules 里除本方案模块外还有脚手架模板模块(home/login/
|
|
290
290
|
// personal-center/notification/system/oj-market…),而安装器 deployWeb 会把包内
|
|
291
291
|
// web/modules 整树覆盖进目标站 <dist>/modules——整树入包等于用开发机上的模板产物
|
|
292
292
|
// 覆盖目标站同名前端。方案包只该带自己那一份,模板模块属宿主面,不由方案分发。
|
|
293
293
|
if (m.web !== undefined) {
|
|
294
|
-
const webModules = path.join(
|
|
294
|
+
const webModules = path.join(layout.distDir, "modules");
|
|
295
295
|
if (!fs.existsSync(webModules)) {
|
|
296
|
-
throw new Error(
|
|
296
|
+
throw new Error(`[ojm] 方案包构建失败:缺少站点构建产物 ${path.relative(projectRoot, webModules)}(manifest 声明了 web 段;先运行 ojm build 生成模块产物)`);
|
|
297
297
|
}
|
|
298
298
|
const ownWeb = path.join(webModules, name);
|
|
299
299
|
if (!fs.existsSync(ownWeb)) {
|
|
300
|
-
throw new Error(`[ojm] 方案包构建失败:缺少本方案的站点产物
|
|
300
|
+
throw new Error(`[ojm] 方案包构建失败:缺少本方案的站点产物 ${path.relative(projectRoot, ownWeb)}/(方案包只入包方案自己的模块;请确认 web.config.ts 已登记 ${name} 且本次 ojm build 已产出)`);
|
|
301
301
|
}
|
|
302
302
|
fs.mkdirSync(path.join(root, "web", "modules"), { recursive: true });
|
|
303
303
|
fs.cpSync(ownWeb, path.join(root, "web", "modules", name), { recursive: true });
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import type { CAC } from "cac";
|
|
2
|
+
import type { HelpEntry, Lang } from "./usage";
|
|
3
|
+
/**
|
|
4
|
+
* `ojm` 的 cac 接线:命令注册 + action 薄分发(解析 → 既有 handler)。
|
|
5
|
+
* 帮助文本(双语注册表)在 usage.ts;handler 真逻辑在各业务模块,本模块不掺业务。
|
|
6
|
+
*/
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import process from "node:process";
|
|
10
|
+
import cac from "cac";
|
|
11
|
+
import { findCommand, groupHelpText, HELP_GROUPS, resolveLang, topHelpSections, usageText } from "./usage";
|
|
12
|
+
|
|
13
|
+
export interface CliDeps { exit?: (code: number) => never }
|
|
14
|
+
|
|
15
|
+
/** `ojm help [cmd [sub]]` → `<cmd> [sub] --help`:词形归一,交给 cac 的 help 机制。 */
|
|
16
|
+
export function rewriteHelpArgv(argv: string[]): string[] {
|
|
17
|
+
if (argv[0] !== "help")
|
|
18
|
+
return argv;
|
|
19
|
+
if (argv.length === 1)
|
|
20
|
+
return ["--help"];
|
|
21
|
+
return [...argv.slice(1), "--help"];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 未知命令兜底结果(index.ts 打印 + 退出用)。
|
|
26
|
+
* help 形态(ojm nope --help / ojm help nope):报「未知命令」exit 1——打帮助退 0 会掩盖拼写错误。
|
|
27
|
+
*/
|
|
28
|
+
export function unknownCommandResult(cmd: string, lang: Lang, helpRequested: boolean): { text: string, code: number } {
|
|
29
|
+
if (helpRequested) {
|
|
30
|
+
return {
|
|
31
|
+
text: lang === "zh-CN"
|
|
32
|
+
? `[ojm] 未知命令:${cmd}(ojm --help 查看全部命令)`
|
|
33
|
+
: `[ojm] Unknown command: ${cmd} (run ojm --help for the full command list)`,
|
|
34
|
+
code: 1,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return { text: usageText(lang), code: 1 };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** cac 的内置报错是英文裸消息(Unknown option)——按语言重写成人话,带修复指引。 */
|
|
41
|
+
export function localizeCacError(error: unknown, lang: Lang): unknown {
|
|
42
|
+
if (!(error instanceof Error))
|
|
43
|
+
return error;
|
|
44
|
+
const m = /^Unknown option `([^`]+)`/.exec(error.message);
|
|
45
|
+
if (!m)
|
|
46
|
+
return error;
|
|
47
|
+
return new Error(lang === "zh-CN"
|
|
48
|
+
? `[ojm] 未知选项:${m[1]}(ojm <cmd> --help 查看可用选项)`
|
|
49
|
+
: `[ojm] Unknown option: ${m[1]} (run ojm <cmd> --help for available options)`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 与旧 index.ts 顶层 catch 同源:message 已是人话,堆栈仅 OJM_DEBUG=1(兼容旧名 RAM_DEBUG)。 */
|
|
53
|
+
export function reportCliError(error: unknown, exit: (code: number) => never): void {
|
|
54
|
+
const isDebug = !!(process.env.OJM_DEBUG ?? process.env.RAM_DEBUG);
|
|
55
|
+
const body = error instanceof Error ? (isDebug ? error.stack ?? error.message : error.message) : String(error);
|
|
56
|
+
console.error(/^\[ojm/.test(body) ? body : `[ojm] ${body}`);
|
|
57
|
+
exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface SiteOpts { url?: string, user?: string, pass?: string }
|
|
61
|
+
|
|
62
|
+
function makeAction(def: HelpEntry, projectRoot: string, lang: Lang, exit: (code: number) => never): (...args: any[]) => Promise<void> {
|
|
63
|
+
switch (def.key) {
|
|
64
|
+
case "init":
|
|
65
|
+
return async (dir?: string, opts: { yes?: boolean } = {}) => {
|
|
66
|
+
const { initProject } = await import("./init");
|
|
67
|
+
await initProject(dir ? path.resolve(dir) : projectRoot, { yes: !!opts.yes });
|
|
68
|
+
};
|
|
69
|
+
case "dev":
|
|
70
|
+
return async (port?: string) => {
|
|
71
|
+
const { devServer } = await import("./dev");
|
|
72
|
+
const portArg = Number(port);
|
|
73
|
+
await devServer(projectRoot, { port: Number.isFinite(portArg) && portArg > 0 ? portArg : 5174 });
|
|
74
|
+
};
|
|
75
|
+
case "build":
|
|
76
|
+
return async (opts: { bundle?: unknown, out?: string } = {}) => {
|
|
77
|
+
// mri 可选值:裸 --bundle 得到 "",--bundle name / --bundle=name 得到字符串
|
|
78
|
+
const bundle = opts.bundle !== undefined;
|
|
79
|
+
const bundleName = typeof opts.bundle === "string" && opts.bundle !== "" ? opts.bundle : undefined;
|
|
80
|
+
if (bundle) {
|
|
81
|
+
const { buildBundle } = await import("./bundle");
|
|
82
|
+
const results = await buildBundle(projectRoot, { out: opts.out, name: bundleName });
|
|
83
|
+
if (results.length > 1)
|
|
84
|
+
console.log(`[ojm] 共打包 ${results.length} 份方案包(逐包签名:ojm sign dist/<name>-<version>.tgz --id <publisher>.<key> --check)`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const { buildBackend, buildModules } = await import("./build");
|
|
88
|
+
await buildBackend(projectRoot);
|
|
89
|
+
await buildModules(projectRoot, { mergeSite: true });
|
|
90
|
+
};
|
|
91
|
+
case "new":
|
|
92
|
+
return async (name: string | undefined, extra: string[] = [], opts: { layout?: string, api?: boolean } = {}) => {
|
|
93
|
+
if (!name) {
|
|
94
|
+
console.error("[ojm] 用法:ojm new <name> [--layout container|none] [--api](非交互环境必须显式给 name)");
|
|
95
|
+
exit(1);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// 统一审查 F8:多余位置参数显式拒绝,不能静默丢弃(ojm new crm erp 只建 crm 是事故)
|
|
99
|
+
if (extra.length > 0)
|
|
100
|
+
throw new Error(`[ojm] ojm new 只接受一个方案名(多余参数:${extra.join(" ")})`);
|
|
101
|
+
if (opts.layout !== undefined && opts.layout !== "container" && opts.layout !== "none")
|
|
102
|
+
throw new Error(`[ojm] --layout 只接受 container | none(实际:${opts.layout})`);
|
|
103
|
+
const { createSolution } = await import("./new");
|
|
104
|
+
const created = createSolution(projectRoot, name, { layout: opts.layout as "container" | "none" | undefined, api: !!opts.api });
|
|
105
|
+
console.log(`[ojm] 已生成方案骨架:${path.relative(process.cwd(), created.dir)}`);
|
|
106
|
+
for (const f of created.files)
|
|
107
|
+
console.log(`[ojm] + ${f}`);
|
|
108
|
+
console.log(`[ojm] 下一步:pnpm dev 看效果 → ojm build && ojm build --bundle ${name} 打包`);
|
|
109
|
+
};
|
|
110
|
+
case "preview":
|
|
111
|
+
return async (port?: string) => {
|
|
112
|
+
const { runPreview } = await import("./preview");
|
|
113
|
+
const portArg = Number(port);
|
|
114
|
+
await runPreview(projectRoot, { port: Number.isInteger(portArg) && portArg > 0 ? portArg : undefined });
|
|
115
|
+
};
|
|
116
|
+
case "api":
|
|
117
|
+
return async (dir?: string, opts: { check?: boolean, docs?: boolean, exempt?: string } = {}) => {
|
|
118
|
+
// S5:显式目录必须先存在——否则静默在错的目录下生成/对账
|
|
119
|
+
if (dir && !fs.existsSync(path.resolve(dir)))
|
|
120
|
+
throw new Error(`[ojm-api] 项目目录不存在:${dir}`);
|
|
121
|
+
const apiRoot = dir ? path.resolve(dir) : projectRoot;
|
|
122
|
+
if (opts.docs) {
|
|
123
|
+
const { runApiDocs } = await import("./contract/run");
|
|
124
|
+
const out = await runApiDocs(apiRoot);
|
|
125
|
+
console.log(`[ojm-api] 文档站已生成:${path.relative(projectRoot, out)}(浏览器直接打开即可)`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (opts.check) {
|
|
129
|
+
const { checkApi } = await import("./contract/check");
|
|
130
|
+
// --exempt <path>:相对路径按项目根解析;缺省按布局找 .ojm-api-exempt.json(工程根 → api/)
|
|
131
|
+
const { violations, hints } = await checkApi({ cwd: apiRoot, exempt: opts.exempt ? path.resolve(projectRoot, opts.exempt) : undefined });
|
|
132
|
+
for (const v of violations)
|
|
133
|
+
console[v.level === "error" ? "error" : "warn"](`${v.level === "error" ? "✗" : "⚠"} ${v.message}`);
|
|
134
|
+
for (const h of hints)
|
|
135
|
+
console.log(h);
|
|
136
|
+
if (violations.length === 0)
|
|
137
|
+
console.log("[ojm-api] --check 通过:生成物同步、route 双向对账、routes.js 均无 drift。");
|
|
138
|
+
const errors = violations.filter(v => v.level === "error").length;
|
|
139
|
+
console.log(`[ojm-api] --check 结果:${errors} error / ${violations.length - errors} warn`);
|
|
140
|
+
if (errors > 0)
|
|
141
|
+
exit(1);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const { runApi } = await import("./contract/run");
|
|
145
|
+
const result = await runApi({ cwd: apiRoot });
|
|
146
|
+
console.log(`[ojm-api] 契约 ${result.contracts} 份;写入 ${result.written.length} 个文件,跳过(未变)${result.skipped.length} 个;stub 新建 ${result.stubs.created} / 更新 ${result.stubs.updated} / 跳过 ${result.stubs.skipped}`);
|
|
147
|
+
for (const p of result.written)
|
|
148
|
+
console.log(` + ${path.relative(apiRoot, p)}`);
|
|
149
|
+
};
|
|
150
|
+
case "info":
|
|
151
|
+
return async (opts: { all?: boolean } = {}) => {
|
|
152
|
+
const { printInfo } = await import("./info");
|
|
153
|
+
await printInfo(projectRoot, undefined, { all: !!opts.all });
|
|
154
|
+
};
|
|
155
|
+
case "vendor":
|
|
156
|
+
return async (tag?: string, opts: { force?: boolean } = {}) => {
|
|
157
|
+
if (tag !== undefined && !/^v?\d+\.\d+\.\d+(?:-[\w.]+)?$/.test(tag))
|
|
158
|
+
throw new Error(`[ojm] 非法版本号:${tag}(形如 v0.1.11 或 0.1.11)`);
|
|
159
|
+
const { vendorCommand } = await import("./vendor");
|
|
160
|
+
await vendorCommand(projectRoot, { force: !!opts.force, tag: tag === undefined ? undefined : (tag.startsWith("v") ? tag : `v${tag}`) });
|
|
161
|
+
};
|
|
162
|
+
case "merge":
|
|
163
|
+
return async (out?: string, inputs: string[] = []) => {
|
|
164
|
+
const { mergeManifests } = await import("./info");
|
|
165
|
+
await mergeManifests(out ?? "", inputs);
|
|
166
|
+
};
|
|
167
|
+
case "keygen":
|
|
168
|
+
return async (opts: { id?: string, out?: string, yes?: boolean } = {}) => {
|
|
169
|
+
const { keygenCommand } = await import("./keygen");
|
|
170
|
+
keygenCommand({ id: opts.id, out: opts.out, yes: opts.yes });
|
|
171
|
+
};
|
|
172
|
+
case "sign":
|
|
173
|
+
return async (file: string | undefined, opts: { id?: string, key?: string, check?: boolean } = {}) => {
|
|
174
|
+
const { signCommand } = await import("./sign");
|
|
175
|
+
signCommand(file ?? "", { id: opts.id, key: opts.key, check: opts.check });
|
|
176
|
+
};
|
|
177
|
+
case "publish":
|
|
178
|
+
return async (dir: string | undefined, opts: { id?: string, key?: string, check?: boolean } = {}) => {
|
|
179
|
+
const { publishCommand } = await import("./publish");
|
|
180
|
+
publishCommand(dir ?? "", { id: opts.id, key: opts.key, check: opts.check });
|
|
181
|
+
};
|
|
182
|
+
case "install":
|
|
183
|
+
return async (target: string | undefined, opts: SiteOpts & { locale?: string, name?: string, version?: string } = {}) => {
|
|
184
|
+
const { installCommand } = await import("./install");
|
|
185
|
+
await installCommand(target ?? "", { url: opts.url, user: opts.user, pass: opts.pass, locale: opts.locale, name: opts.name, version: opts.version });
|
|
186
|
+
};
|
|
187
|
+
case "enable":
|
|
188
|
+
case "disable":
|
|
189
|
+
return async (name: string | undefined, opts: SiteOpts = {}) => {
|
|
190
|
+
const { setEnabledCommand } = await import("./enable");
|
|
191
|
+
await setEnabledCommand(name ?? "", def.key === "enable", { url: opts.url, user: opts.user, pass: opts.pass });
|
|
192
|
+
};
|
|
193
|
+
case "uninstall":
|
|
194
|
+
return async (name: string | undefined, opts: SiteOpts = {}) => {
|
|
195
|
+
const { uninstallCommand } = await import("./uninstall");
|
|
196
|
+
await uninstallCommand(name ?? "", { url: opts.url, user: opts.user, pass: opts.pass });
|
|
197
|
+
};
|
|
198
|
+
case "trust":
|
|
199
|
+
return async (sub: string | undefined, keyId: string | undefined, pub: string | undefined, opts: SiteOpts & { task?: string } = {}) => {
|
|
200
|
+
const { trustCommand } = await import("./trust");
|
|
201
|
+
await trustCommand(sub ?? "", [keyId, pub].filter((v): v is string => v !== undefined), { url: opts.url, user: opts.user, pass: opts.pass, task: opts.task });
|
|
202
|
+
};
|
|
203
|
+
case "market":
|
|
204
|
+
return async (sub: string | undefined) => {
|
|
205
|
+
if (!sub) {
|
|
206
|
+
// bare `ojm market`:打印该组帮助(子命令索引),同旧 marketUsage 语义
|
|
207
|
+
console.log(groupHelpText("market-cdn", lang));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// market 子命令自带解析器(trial/walkthrough 等 flag 各异):raw 转发 process.argv
|
|
211
|
+
const { marketCommand } = await import("./market/command");
|
|
212
|
+
await marketCommand(sub, process.argv.slice(4));
|
|
213
|
+
};
|
|
214
|
+
case "oj":
|
|
215
|
+
return async () => {
|
|
216
|
+
// 透传:参数原样转发(process.argv 未被 help 词形改写污染——真实调用才走到这里)
|
|
217
|
+
const { runOjSubcommand } = await import("./oj-cmd");
|
|
218
|
+
runOjSubcommand(projectRoot, def.name, process.argv.slice(3));
|
|
219
|
+
};
|
|
220
|
+
default:
|
|
221
|
+
throw new Error(`[ojm] 未接线的命令键:${def.key}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function buildCli(projectRoot: string, lang: Lang = resolveLang(), deps: CliDeps = {}): CAC {
|
|
226
|
+
const exit = deps.exit ?? ((code: number) => process.exit(code) as never);
|
|
227
|
+
const cli = cac("ojm");
|
|
228
|
+
for (const group of HELP_GROUPS) {
|
|
229
|
+
for (const def of group.commands) {
|
|
230
|
+
const cmd = cli.command(def.name, lang === "zh-CN" ? def.zh : def.en);
|
|
231
|
+
for (const o of def.options ?? [])
|
|
232
|
+
cmd.option(o.raw, lang === "zh-CN" ? o.zh : o.en);
|
|
233
|
+
if (def.allowUnknownOptions)
|
|
234
|
+
cmd.allowUnknownOptions();
|
|
235
|
+
const action = makeAction(def, projectRoot, lang, exit);
|
|
236
|
+
cmd.action((...args: unknown[]) => {
|
|
237
|
+
Promise.resolve(action(...args)).catch((error: unknown) => reportCliError(error, exit));
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// help 回调:顶层 = 分组 sections;单命令 = 本地化小节标题 + 详解/子命令详解注入
|
|
243
|
+
const SECTION_TITLES: Record<string, { zh: string, en: string }> = {
|
|
244
|
+
Usage: { zh: "用法", en: "Usage" },
|
|
245
|
+
Options: { zh: "选项", en: "Options" },
|
|
246
|
+
Examples: { zh: "示例", en: "Examples" },
|
|
247
|
+
};
|
|
248
|
+
cli.help((sections) => {
|
|
249
|
+
const matched = cli.matchedCommand;
|
|
250
|
+
if (!matched)
|
|
251
|
+
return topHelpSections(lang);
|
|
252
|
+
const def = findCommand(matched.rawName);
|
|
253
|
+
const out = sections.map((s) => {
|
|
254
|
+
const t = s.title && SECTION_TITLES[s.title];
|
|
255
|
+
return t ? { ...s, title: lang === "zh-CN" ? t.zh : t.en } : s;
|
|
256
|
+
});
|
|
257
|
+
if (def?.detail)
|
|
258
|
+
out.push({ title: lang === "zh-CN" ? "详细说明" : "Details", body: lang === "zh-CN" ? def.detail.zh : def.detail.en });
|
|
259
|
+
const sub = cli.args[0];
|
|
260
|
+
if (def?.subs && sub && def.subs[sub])
|
|
261
|
+
out.push({ title: `${sub}:`, body: lang === "zh-CN" ? def.subs[sub].zh : def.subs[sub].en });
|
|
262
|
+
return out;
|
|
263
|
+
});
|
|
264
|
+
return cli;
|
|
265
|
+
}
|
package/src/contract/check.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { findOpenapiJson, groupIrByModule } from "./run";
|
|
|
18
18
|
* ① 生成物同步:内存重生成 vs 磁盘逐字节 diff(含 stub 待更新)
|
|
19
19
|
* ② route 双向对账:AST 扫 oj api.ts(default 导出方法名 + 语句起始 `.route = "..."` 赋值)
|
|
20
20
|
* vs 契约路由表:未实现 warn / 未登记 error / 参数段不一致 error
|
|
21
|
-
* ③ routes.js diff:release 路由表(oj build 产物,api
|
|
21
|
+
* ③ routes.js diff:release 路由表(oj build 产物,dist/api 下各模块的 routes.js)vs openapi.json
|
|
22
22
|
* 契约路由表;无 dist 给提示不判违规
|
|
23
23
|
*
|
|
24
24
|
* 原则:check 永不修文件——修复动作永远由人执行(重跑 ojm api / 补 handler / oj build)。
|
|
@@ -360,8 +360,8 @@ export async function checkApi(opts: { cwd: string, exempt?: string }): Promise<
|
|
|
360
360
|
// ④ WS 路由鉴权对账(oj 0.1.30 起握手过鉴权守卫)
|
|
361
361
|
violations.push(...checkWsAnonymous(roots, resolveConfigPath(opts.cwd)));
|
|
362
362
|
|
|
363
|
-
// ③ routes.js diff
|
|
364
|
-
const distDir = join(opts.cwd, "
|
|
363
|
+
// ③ routes.js diff(release 路由表读 layout.apiDist;无布局工程回退同一约定 dist/api)
|
|
364
|
+
const distDir = tryResolveLayout(opts.cwd)?.apiDist ?? join(opts.cwd, "dist", "api");
|
|
365
365
|
const distRows: { method: string, pattern: string }[] = [];
|
|
366
366
|
if (existsSync(distDir)) {
|
|
367
367
|
for (const entry of readdirSync(distDir, { withFileTypes: true })) {
|
|
@@ -373,7 +373,7 @@ export async function checkApi(opts: { cwd: string, exempt?: string }): Promise<
|
|
|
373
373
|
}
|
|
374
374
|
}
|
|
375
375
|
if (distRows.length === 0) {
|
|
376
|
-
hints.push(
|
|
376
|
+
hints.push(`[ojm-api] 未发现 ${relative(opts.cwd, distDir)}/**/routes.js——release 路由表对账跳过;发布前请先 oj build 再跑 ojm api --check。`);
|
|
377
377
|
}
|
|
378
378
|
else {
|
|
379
379
|
// 尾斜杠规范化:契约侧 fullPath 对「模块根路径」带尾斜杠(stripBase 的
|
package/src/dev.ts
CHANGED
|
@@ -35,6 +35,7 @@ import { loadProjectMocks, mockStatusCode } from "./dev-mock";
|
|
|
35
35
|
import { createReloadHub, proxyApi, proxyWsUpgrade, sseScript } from "./dev-proxy";
|
|
36
36
|
import { loadHtmlTransform } from "./html-transform";
|
|
37
37
|
import { resolveConfigPath, resolveLayout } from "./layout";
|
|
38
|
+
import { resolveCdnLayout } from "./market/cdn-layout";
|
|
38
39
|
import { startOj } from "./oj";
|
|
39
40
|
import { requireModuleRootCapability } from "./oj-caps";
|
|
40
41
|
import { ensureDevCert } from "./oj-cert";
|
|
@@ -123,6 +124,16 @@ export async function devServer(projectRoot: string, opts: DevOptions = {}): Pro
|
|
|
123
124
|
|
|
124
125
|
const hub = createReloadHub();
|
|
125
126
|
|
|
127
|
+
// /cdn 反代(feature:market_cdn_url 可为相对路径 = 本站点自供):config 有 /cdn 挂载
|
|
128
|
+
// 才反代到 oj;无挂载(旧模板/手改工程)或纯前端形态 → 回落静态,行为不变
|
|
129
|
+
let cdnPrefix: string | null = null;
|
|
130
|
+
try {
|
|
131
|
+
cdnPrefix = resolveCdnLayout(projectRoot).prefix;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
cdnPrefix = null;
|
|
135
|
+
}
|
|
136
|
+
|
|
126
137
|
// 3) 静态半边(dev:模块产物优先,shell dist 兜底;no-store;注入刷新通道)
|
|
127
138
|
// hostRoots 钉死 shell dist:ojm build 的合并残留不得反向遮蔽宿主(F11)
|
|
128
139
|
// transform 来自 web.config.ts(可选):按路由改写 title/OG,先于 reload 注入
|
|
@@ -175,6 +186,12 @@ export async function devServer(projectRoot: string, opts: DevOptions = {}): Pro
|
|
|
175
186
|
return;
|
|
176
187
|
}
|
|
177
188
|
|
|
189
|
+
// 相对基址 CDN(/cdn):站点自供挂载直发 oj(不剥前缀),否则静态层会回落成 index.html
|
|
190
|
+
if (cdnPrefix !== null && ojTarget !== undefined && (urlPath === cdnPrefix || urlPath.startsWith(`${cdnPrefix}/`))) {
|
|
191
|
+
void proxyApi(ojTarget)(req, res);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
178
195
|
serveStatic(req, res);
|
|
179
196
|
});
|
|
180
197
|
|