@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/market/bootstrap.ts
CHANGED
|
@@ -36,7 +36,9 @@ import { parseFixtureSpec } from "./fixture";
|
|
|
36
36
|
import { publishPackage } from "./publish";
|
|
37
37
|
|
|
38
38
|
/** 种子管理员(隔离库全新 seed;与 templates seed.sql 一致) */
|
|
39
|
-
|
|
39
|
+
// 种子管理员 = superadmin:模板 seed 账号名=角色名(3d93063f 起 admin 被幂等解绑 superadmin
|
|
40
|
+
// 角色),写 market 安全设置要求 actor 持 superadmin 角色——用 admin 会 403(2026-10-23 演练实踩)。
|
|
41
|
+
const SEED_ADMIN_USER = "superadmin";
|
|
40
42
|
const SEED_ADMIN_PASS = "123456";
|
|
41
43
|
|
|
42
44
|
/** Ed25519 PKCS#8 DER 前缀(RFC 8410 固定头;与 gen-signed-fixtures.mjs 同一手法) */
|
|
@@ -50,7 +52,7 @@ export interface IsolatedPublisher {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
export interface IsolatedSite {
|
|
53
|
-
/** 临时根(含 config.yaml / src/ / api/
|
|
55
|
+
/** 临时根(含 config.yaml / src/ / dist/api/ / bin/ / cdn/) */
|
|
54
56
|
root: string
|
|
55
57
|
port: number
|
|
56
58
|
baseUrl: string
|
|
@@ -96,7 +98,7 @@ export async function bootstrapIsolatedSite(opts: BootstrapOptions = {}): Promis
|
|
|
96
98
|
|
|
97
99
|
const root = mkdtempSync(path.join(tmpdir(), "ojm-trial-"));
|
|
98
100
|
// 新布局(`src/<name>/{api,web}`):config.yaml 在隔离根,源码树根为 <root>/src,
|
|
99
|
-
// 构建产物仍是扁平根 <root>/api
|
|
101
|
+
// 构建产物仍是扁平根 <root>/dist/api(与真实工程同形)。
|
|
100
102
|
const srcDir = path.join(root, "src");
|
|
101
103
|
const configPath = path.join(root, "config.yaml");
|
|
102
104
|
const cdnDir = path.join(root, "cdn");
|
|
@@ -143,9 +145,9 @@ export async function bootstrapIsolatedSite(opts: BootstrapOptions = {}): Promis
|
|
|
143
145
|
mkdirSync(siteDir, { recursive: true });
|
|
144
146
|
writeFileSync(path.join(siteDir, "index.html"), "<!doctype html><html><body>ojm trial site</body></html>\n", "utf-8");
|
|
145
147
|
cfg = cfg.replace("web: ./cdn", `web: ${cdnDir}\n - prefix: /\n web: ${siteDir}\n spa: true`);
|
|
146
|
-
// 安装目标恒为 release 部署:装好的模块落在 api
|
|
148
|
+
// 安装目标恒为 release 部署:装好的模块落在 dist/api,dev 形态(--api-path src)发现不了。
|
|
147
149
|
// 故隔离站点以 dist 起站,platform.root 同步改写(oj 校验 root 必须是生效的 api 挂载目录)。
|
|
148
|
-
// root 相对 **config 所在目录** 解析——config 在隔离根,产物在 <root>/api
|
|
150
|
+
// root 相对 **config 所在目录** 解析——config 在隔离根,产物在 <root>/dist/api,故不能写死
|
|
149
151
|
// `dist`(那是 config 还在 api/ 时的旧口径),按布局取值。
|
|
150
152
|
const apiDistRel = path.relative(root, resolveLayout(root).apiDist).split(path.sep).join("/");
|
|
151
153
|
cfg = cfg.replace(" root: src", ` root: ${apiDistRel}`);
|
|
@@ -172,14 +174,14 @@ export async function bootstrapIsolatedSite(opts: BootstrapOptions = {}): Promis
|
|
|
172
174
|
await publishPackage({ projectRoot: root, fixture: opts.fixture, id: keyId, privateKeyPem: publisher.privateKeyPem });
|
|
173
175
|
await buildIndex({ projectRoot: root, id: keyId, privateKeyPem: publisher.privateKeyPem });
|
|
174
176
|
|
|
175
|
-
// 7) 后端构建(api
|
|
177
|
+
// 7) 后端构建(dist/api + versions.json;安装与 reloading 都依赖它)
|
|
176
178
|
await buildBackend(root);
|
|
177
179
|
|
|
178
180
|
// 7b) 起 oj(starter 注入供测试);ready 失败即整体作废,不留孤儿
|
|
179
181
|
const starter = opts.starter ?? ((cfgPath: string, base: string, apiSrc: string) => startOj(cfgPath, base, apiSrc));
|
|
180
182
|
let oj: OjProcess;
|
|
181
183
|
try {
|
|
182
|
-
oj = starter(configPath, "/api",
|
|
184
|
+
oj = starter(configPath, "/api", resolveLayout(root).apiDist);
|
|
183
185
|
await oj.ready;
|
|
184
186
|
}
|
|
185
187
|
catch (error) {
|
|
@@ -258,7 +260,7 @@ function teardownReport(root: string, port: number): string[] {
|
|
|
258
260
|
`隔离根:${root}`,
|
|
259
261
|
" config.yaml + config/ 证书 + db.sqlite 站点配置面",
|
|
260
262
|
" src/ 后端模块源码树(src/<name>/api,拷自 cli 内置模板)",
|
|
261
|
-
" api/
|
|
263
|
+
" dist/api/ 构建产物(安装目标)",
|
|
262
264
|
" bin/ oj 二进制与插件(拷贝自源工程)",
|
|
263
265
|
" cdn/ 隔离 CDN(fixture 包 + 两份索引 + 签名)",
|
|
264
266
|
`监听端口:127.0.0.1:${port}(进程已停)`,
|
package/src/market/cdn-layout.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 两条不能破的规矩:
|
|
5
5
|
* 1. **不许硬编码端口/主机/挂载路径**——全部从 `api/config.yaml` 读。
|
|
6
|
-
* dev
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* (2026-10-23 起 dev 形态 ojm 已把 /cdn 反代给 oj 子进程,`framework.market_cdn_url`
|
|
7
|
+
* 允许写相对路径 `/cdn` = 本站点自供,由消费侧按请求源补全;本模块的绝对基址推导
|
|
8
|
+
* 只服务 `market setup` 写外部地址的场景。)
|
|
9
9
|
* 2. **基址无尾斜杠**——`market/api.ts` 按 `${base}/bundles.yaml` 拼。
|
|
10
10
|
*
|
|
11
11
|
* 文本版(`resolveCdnLayoutFromConfig`)与工程版(`resolveCdnLayout`)分工:前者纯函数便于单测,
|
package/src/market/command.ts
CHANGED
|
@@ -114,26 +114,3 @@ export async function marketCommand(sub: string, argv: string[]): Promise<void>
|
|
|
114
114
|
throw new Error(`[ojm] 未知子命令:ojm market ${sub}\n可用:setup / publish / build-index`);
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
-
|
|
118
|
-
/** `ojm market` 无子命令时的提示(与 usage.ts 同源风格) */
|
|
119
|
-
export function marketUsage(): string {
|
|
120
|
-
return [
|
|
121
|
-
"ojm market setup [--url <站点地址>] [--user <u> --pass <p>] [--new-pass <p>] 把市场地址指向本地 cdn/",
|
|
122
|
-
"ojm market publish [--fixture <name>@<version>] [--real <方案工程>] [--id <key>] [--rebuild-index]",
|
|
123
|
-
"ojm market build-index [--id <key>] [--check] 扫描 cdn/ 重建索引",
|
|
124
|
-
"ojm market verify [--file <tgz>] [--rebuild-index] 离线验包(不起站点、不装)",
|
|
125
|
-
"ojm market trial [--fixture <name>@<version>] [--keep] 隔离站点试装(跑完即毁)",
|
|
126
|
-
"ojm market walkthrough [--only <op>] [--strict] [--keep] 隔离站点跑全套市场操作并出 AC 表",
|
|
127
|
-
].join("\n");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** 进程入口:从 argv 取子命令(index.ts 负责 dispatch 到此处) */
|
|
131
|
-
export async function runMarket(argv: string[]): Promise<void> {
|
|
132
|
-
const sub = argv[0] ?? "";
|
|
133
|
-
if (sub === "" || sub === "--help" || sub === "-h") {
|
|
134
|
-
console.log(marketUsage());
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
await marketCommand(sub, argv.slice(1));
|
|
138
|
-
void process;
|
|
139
|
-
}
|
package/src/market/setup.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `ojm market setup`(T5):把 `framework.market_cdn_url` 指向本地模拟 CDN。
|
|
3
3
|
*
|
|
4
|
+
* 2026-10-23 起**非必需**:脚手架 seed 的缺省值即相对路径 `/cdn`(本站点自供,
|
|
5
|
+
* dev 形态 ojm 已把 /cdn 反代给 oj 子进程),新工程免 setup。本命令只剩一个场景:
|
|
6
|
+
* 指向**外部真 CDN**(或本地但要写绝对地址)时显式改写设置。
|
|
7
|
+
*
|
|
4
8
|
* 两件容易踩的事都收在这一处:
|
|
5
|
-
* 1. **地址必须是绝对的**,且端口来自 config 的 `server.port
|
|
6
|
-
*
|
|
9
|
+
* 1. **地址必须是绝对的**,且端口来自 config 的 `server.port`——写给外部访问方的地址,
|
|
10
|
+
* 相对路径对浏览器/服务端消费另有按请求源补全的口径(market/api.ts withSiteOrigin)。
|
|
7
11
|
* 2. **首跑凭据悬挂**:seed admin `must_change=1`,登录拿的是限权 token,写不了 system 设置;
|
|
8
12
|
* 须先 `PUT /api/auth/password` 改密换全权 token(该端点是悬挂期唯一可用写端)。
|
|
9
13
|
*/
|
|
@@ -56,6 +60,14 @@ export async function setupMarketUrl(opts: SetupOptions = {}): Promise<SetupResu
|
|
|
56
60
|
};
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
/**
|
|
64
|
+
* setup 后用于登录的口令:改密闭环走了才用 newPass,否则原口令。
|
|
65
|
+
* 当前 seed must_change=0(账号名=角色名口径)时 setup 不改密,恒用 newPass 会 401。
|
|
66
|
+
*/
|
|
67
|
+
export function loginPasswordAfterSetup(result: SetupResult, newPass: string, originalPass: string): string {
|
|
68
|
+
return result.changedPassword ? newPass : originalPass;
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
/** 登录并在必要时走改密闭环,返回可用 token */
|
|
60
72
|
async function obtainToken(fetchFn: typeof fetch, base: string, user: string, pass: string, newPass: string | undefined, projectRoot: string): Promise<{ value: string, changedPassword: boolean, generatedPassword?: string }> {
|
|
61
73
|
let login: { access_token?: string, restricted?: boolean };
|
package/src/market/trial.ts
CHANGED
|
@@ -15,7 +15,7 @@ import process from "node:process";
|
|
|
15
15
|
import { callApi, login, pollTask } from "../site";
|
|
16
16
|
import { bootstrapIsolatedSite } from "./bootstrap";
|
|
17
17
|
import { parseFixtureSpec } from "./fixture";
|
|
18
|
-
import { setupMarketUrl } from "./setup";
|
|
18
|
+
import { loginPasswordAfterSetup, setupMarketUrl } from "./setup";
|
|
19
19
|
|
|
20
20
|
export interface TrialOptions {
|
|
21
21
|
/** 源工程(提供 bin/oj 与 cli 模板);缺省 process.cwd() */
|
|
@@ -53,7 +53,7 @@ export async function trialInstall(opts: TrialOptions = {}): Promise<TrialResult
|
|
|
53
53
|
try {
|
|
54
54
|
// 1) 打开市场地址:隔离库是全新 seed,首登必然处于改密悬挂期 → setup 顺带完成改密
|
|
55
55
|
const newPass = "trial-pass-1";
|
|
56
|
-
await setupMarketUrl({
|
|
56
|
+
const setup = await setupMarketUrl({
|
|
57
57
|
projectRoot: site.root,
|
|
58
58
|
url: site.baseUrl,
|
|
59
59
|
user: site.admin.user,
|
|
@@ -61,7 +61,7 @@ export async function trialInstall(opts: TrialOptions = {}): Promise<TrialResult
|
|
|
61
61
|
newPass,
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
const token = await login(fetch, site.baseUrl, site.admin.user, newPass);
|
|
64
|
+
const token = await login(fetch, site.baseUrl, site.admin.user, loginPasswordAfterSetup(setup, newPass, site.admin.pass));
|
|
65
65
|
|
|
66
66
|
// 2) 首次安装:预期停在 needs_trust(发布方是本次临时生成的身份)
|
|
67
67
|
let task = await installOnce(site.baseUrl, token, spec.name, spec.version);
|
|
@@ -19,6 +19,7 @@ import fs from "node:fs";
|
|
|
19
19
|
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
import process from "node:process";
|
|
22
|
+
import { resolveLayout } from "../layout";
|
|
22
23
|
import { signCommand } from "../sign";
|
|
23
24
|
import { callApi, login, pollTask, unwrapEnvelope } from "../site";
|
|
24
25
|
import { bootstrapIsolatedSite, derivePublisherKey } from "./bootstrap";
|
|
@@ -27,7 +28,7 @@ import { FAILURE_PLAYBOOK } from "./failure-playbook";
|
|
|
27
28
|
import { buildEvilZipSlipTgz, parseFixtureSpec } from "./fixture";
|
|
28
29
|
import { publishPackage } from "./publish";
|
|
29
30
|
import { writeWalkthroughReport } from "./report";
|
|
30
|
-
import { setupMarketUrl } from "./setup";
|
|
31
|
+
import { loginPasswordAfterSetup, setupMarketUrl } from "./setup";
|
|
31
32
|
|
|
32
33
|
interface Ctx {
|
|
33
34
|
site: IsolatedSite
|
|
@@ -154,7 +155,7 @@ const OPS: Op[] = [
|
|
|
154
155
|
if (task.status !== "done")
|
|
155
156
|
return { ok: false, detail: `升级安装未 done:status=${task.status} stage=${task.stage ?? "?"}:${task.message}` };
|
|
156
157
|
const card = await indexCard(ctx);
|
|
157
|
-
const distDir =
|
|
158
|
+
const distDir = resolveLayout(ctx.site.root).apiDist;
|
|
158
159
|
const dir010 = path.join(distDir, `${ctx.name}-0.1.0`);
|
|
159
160
|
const dir020 = path.join(distDir, `${ctx.name}-0.2.0`);
|
|
160
161
|
const mig010 = path.join(dir010, "migrations", "0001__init.sql");
|
|
@@ -185,7 +186,7 @@ const OPS: Op[] = [
|
|
|
185
186
|
return { ok: false, detail: `前置安装 0.2.0 未 done:status=${pre.status}:${pre.message}` };
|
|
186
187
|
card = await indexCard(ctx);
|
|
187
188
|
}
|
|
188
|
-
const distDir =
|
|
189
|
+
const distDir = resolveLayout(ctx.site.root).apiDist;
|
|
189
190
|
const before = fs.readdirSync(distDir).sort(); // 零副作用基线:0.1.0 目录是前序合法安装的产物,不代表本次落盘
|
|
190
191
|
const task = await installOnce(ctx, "0.1.0"); // 包内最大 seq 1 < head 2
|
|
191
192
|
const blocked = task.status === "failed" && task.stage === "migrating";
|
|
@@ -340,7 +341,7 @@ const OPS: Op[] = [
|
|
|
340
341
|
run: async (ctx) => {
|
|
341
342
|
const evil = path.join(ctx.site.cdnDir, `${ctx.name}-evil.tgz`);
|
|
342
343
|
buildEvilZipSlipTgz(evil); // 成员含 ../evil.sql,无需发布进索引(file 源直传)
|
|
343
|
-
const distDir =
|
|
344
|
+
const distDir = resolveLayout(ctx.site.root).apiDist;
|
|
344
345
|
const before = fs.readdirSync(distDir).sort(); // 零副作用基线:9.9.9 目录可能是前序 neg-stranger 的合法产物
|
|
345
346
|
try {
|
|
346
347
|
let msg = "";
|
|
@@ -373,8 +374,8 @@ export async function walkthrough(opts: WalkthroughOptions = {}): Promise<Walkth
|
|
|
373
374
|
const site = await bootstrapIsolatedSite({ projectRoot: opts.projectRoot, fixture: opts.fixture, keep: opts.keep, onLog: log });
|
|
374
375
|
try {
|
|
375
376
|
const newPass = "trial-pass-1";
|
|
376
|
-
await setupMarketUrl({ projectRoot: site.root, url: site.baseUrl, user: site.admin.user, pass: site.admin.pass, newPass });
|
|
377
|
-
const token = await login(fetch, site.baseUrl, site.admin.user, newPass);
|
|
377
|
+
const setup = await setupMarketUrl({ projectRoot: site.root, url: site.baseUrl, user: site.admin.user, pass: site.admin.pass, newPass });
|
|
378
|
+
const token = await login(fetch, site.baseUrl, site.admin.user, loginPasswordAfterSetup(setup, newPass, site.admin.pass));
|
|
378
379
|
const ctx: Ctx = { site, base: site.baseUrl, token, name: spec.name, version: spec.version, log };
|
|
379
380
|
|
|
380
381
|
const selected = opts.only === undefined ? OPS : OPS.filter(op => op.key === opts.only);
|
package/src/new.ts
CHANGED
|
@@ -124,6 +124,11 @@ title: ${name}
|
|
|
124
124
|
summary: ${name} 方案(ojm new 生成的骨架,替换为你的描述)
|
|
125
125
|
changelog:
|
|
126
126
|
- 0.1.0 首个版本(ojm new 生成)
|
|
127
|
+
# 市场验包五必填(market verify structureCheck):peerRuntime 对宿主运行时强制比对(C2-09),
|
|
128
|
+
# archive 为 Form A 占位(tgz 内仅允许 file 键,C2-29;发布侧由 publish 填 Form B)。
|
|
129
|
+
peerRuntime: ">=0.0.0"
|
|
130
|
+
archive:
|
|
131
|
+
file: ${name}-0.1.0.tgz
|
|
127
132
|
${api}web:
|
|
128
133
|
route_prefix: /${name}
|
|
129
134
|
entry: modules/${name}/0.1.0/entry.js
|
|
@@ -200,9 +205,22 @@ tables: []
|
|
|
200
205
|
}
|
|
201
206
|
|
|
202
207
|
function apiTs(name: string): string {
|
|
208
|
+
// 形状契约:oj 内省只读 default export、链尾须为 HTTP 动词——ping 做成嵌套组
|
|
209
|
+
// (链 ping.get → GET /api/<name>/ping);裸命名导出不进 default 会产出空 routes.js
|
|
210
|
+
// (20261023 市场链路演练实踩)。
|
|
203
211
|
return `// GET /api/${name}/ping —— 健康探针示例(保存即热更;出参挂平级 .response 即入 openapi.json)。
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
212
|
+
const ping = {
|
|
213
|
+
async get() {
|
|
214
|
+
json.ok({ pong: true, module: "${name}" });
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
ping.get.response = {
|
|
219
|
+
type: "object",
|
|
220
|
+
required: ["pong"],
|
|
221
|
+
properties: { pong: { type: "boolean" } },
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export default { ping };
|
|
207
225
|
`;
|
|
208
226
|
}
|
package/src/preview-config.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 单服务 preview 由 oj 一个进程同时出 API 与静态(web 挂载 + spa: true),
|
|
5
5
|
* 而 spa 只能来自 config 文件(CLI --app-path 的 upsert 保留但不设置
|
|
6
|
-
* spa——实测 oj 0.1.80)。用户在模板里手改 config 不可取(web
|
|
6
|
+
* spa——实测 oj 0.1.80)。用户在模板里手改 config 不可取(dist/web 缺失时
|
|
7
7
|
* oj serve 整体拒启,会连带挂掉 ojm dev),故 preview 现场生成一份
|
|
8
8
|
* 派生 config:
|
|
9
9
|
*
|
|
@@ -37,7 +37,7 @@ export interface DerivePreviewConfigOptions {
|
|
|
37
37
|
*/
|
|
38
38
|
prefix?: string
|
|
39
39
|
/**
|
|
40
|
-
* release 形态的后端模块根(api
|
|
40
|
+
* release 形态的后端模块根(layout.apiDist = dist/api)。给了且原 config 声明了 platform.modules 时,
|
|
41
41
|
* 把 `platform.root` 一并改写到该目录——特权模块的受控根必须与 `--api-path` 同源,
|
|
42
42
|
* 否则 release 下 reloadConfig 会按 dev 的 src 判定归属(装好的方案热更不生效)。
|
|
43
43
|
*/
|
package/src/preview.ts
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* **单进程同时出 API 与静态**)。ojm 不再自建 http server、不再反代——
|
|
7
7
|
* 前后端同端口同进程,与「oj serve + web 挂载」的生产公式逐字同形。
|
|
8
8
|
*
|
|
9
|
-
* 派生 config 为临时产物(
|
|
10
|
-
*
|
|
9
|
+
* 派生 config 为临时产物(module-root:工程根 .ojm-preview.yaml,与原件同目录
|
|
10
|
+
* 保证相对路径解析一致;历史布局才是 api/.ojm-preview.yaml),三路清理:ready
|
|
11
|
+
* 拒绝路径 / SIGINT / process exit 兜底
|
|
11
12
|
* (语义与落盘见 preview-config.ts)。
|
|
12
13
|
*
|
|
13
14
|
* web.config.ts 的 htmlTransform 是 serve 期钩子,单服务下 oj 直出 HTML
|
|
@@ -59,7 +60,7 @@ export async function runPreview(projectRoot: string, opts: PreviewOptions = {})
|
|
|
59
60
|
const siteDir = opts.siteDir ?? layout.distDir;
|
|
60
61
|
const ojBin = resolveOjBin(projectRoot);
|
|
61
62
|
const configPath = resolveConfigPath(projectRoot);
|
|
62
|
-
// release 根恒为扁平 api
|
|
63
|
+
// release 根恒为扁平 dist/api(module-root 只改源码布局,不改产物形状——设计 C-3/C-7)
|
|
63
64
|
const apiDist = layout.apiDist;
|
|
64
65
|
|
|
65
66
|
// fail-fast 四查:缺一即提示对应补救(init 幂等补缺 / ojm build 产物)
|
|
@@ -89,7 +90,7 @@ export async function runPreview(projectRoot: string, opts: PreviewOptions = {})
|
|
|
89
90
|
// 派生 config 在 migrate 成功后落盘:失败即退路径零残留
|
|
90
91
|
const base = readOjApiPrefix(configPath);
|
|
91
92
|
const writer = opts.configWriter ?? ((root: string, o: { siteDir: string, prefix?: string, port?: number, apiRoot?: string }) => writePreviewConfig(root, o));
|
|
92
|
-
// apiRoot 交给派生器:release 下 platform.root 必须指向 api
|
|
93
|
+
// apiRoot 交给派生器:release 下 platform.root 必须指向 dist/api(与 --api-path 同源)
|
|
93
94
|
const preview = writer(projectRoot, { siteDir, prefix: sitePrefix, port: opts.port, apiRoot: apiDist });
|
|
94
95
|
|
|
95
96
|
// / api 挂载与注入的 web 挂载配对时 oj 不开 SPA 回落(oj 语义,见 devkit 手册 §13)。
|
package/src/publish.ts
CHANGED
|
@@ -126,25 +126,3 @@ export function publishCommand(dir: string, opts: PublishOptions = {}): PublishR
|
|
|
126
126
|
}
|
|
127
127
|
return { bundlesPath, manifestsPath, bundlesSigPath, manifestsSigPath, keyId, publicKeyHex, fingerprint: fingerprintOfPublicKey(publicKeyHex), checkPassed };
|
|
128
128
|
}
|
|
129
|
-
|
|
130
|
-
/** `ojm publish <dir> [--id <publisher.id>] [--key <pem>] [--check]` 参数解析 */
|
|
131
|
-
export function parsePublishArgs(argv: string[]): { positionals: string[], id?: string, key?: string, check?: boolean } {
|
|
132
|
-
const values: Record<string, string> = {};
|
|
133
|
-
let check = false;
|
|
134
|
-
const positionals: string[] = [];
|
|
135
|
-
for (let i = 0; i < argv.length; i++) {
|
|
136
|
-
const a = argv[i]!;
|
|
137
|
-
if (a === "--check") {
|
|
138
|
-
check = true;
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
if (a === "--id" || a === "--key") {
|
|
142
|
-
values[a] = argv[i + 1] ?? "";
|
|
143
|
-
i++;
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
if (!a.startsWith("--"))
|
|
147
|
-
positionals.push(a);
|
|
148
|
-
}
|
|
149
|
-
return { positionals, id: values["--id"], key: values["--key"], check };
|
|
150
|
-
}
|
package/src/sign.ts
CHANGED
|
@@ -104,28 +104,6 @@ export function signCommand(tgz: string, opts: SignOptions = {}): SignResult {
|
|
|
104
104
|
return { sigPath, keyId, publicKeyHex, fingerprint: fingerprintOfPublicKey(publicKeyHex), sha256, canonical, checkPassed };
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
/** `ojm sign <tgz> [--id <publisher.id>] [--key <pem>] [--check]` 参数解析 */
|
|
108
|
-
export function parseSignArgs(argv: string[]): { positionals: string[], id?: string, key?: string, check?: boolean } {
|
|
109
|
-
const values: Record<string, string> = {};
|
|
110
|
-
let check = false;
|
|
111
|
-
const positionals: string[] = [];
|
|
112
|
-
for (let i = 0; i < argv.length; i++) {
|
|
113
|
-
const a = argv[i]!;
|
|
114
|
-
if (a === "--check") {
|
|
115
|
-
check = true;
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
if (a === "--id" || a === "--key") {
|
|
119
|
-
values[a] = argv[i + 1] ?? "";
|
|
120
|
-
i++;
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
if (!a.startsWith("--"))
|
|
124
|
-
positionals.push(a);
|
|
125
|
-
}
|
|
126
|
-
return { positionals, id: values["--id"], key: values["--key"], check };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
107
|
/** 身份从文件名推导(<name>-<version>.tgz;与 ojm install parseTarget 同规则,Finding C 正则) */
|
|
130
108
|
function deriveIdentity(stem: string): { name: string, version: string } {
|
|
131
109
|
const m = /^(.+)-(\d[\w.]+(?:-[\w.]+)?)$/.exec(stem);
|