@oj-module/cli 0.2.66 → 0.2.68
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/bin/.oj-version +1 -0
- package/bin/oj +4 -0
- package/devkit/README.md +15 -7
- package/devkit/SKILL.md +38 -4
- package/devkit/framework-dev.md +10 -4
- package/devkit/manual.md +146 -24
- package/devkit/menu-rules.md +19 -4
- package/devkit/module-dev-and-cdn-playground.md +4 -0
- package/devkit/publish-walkthrough.md +132 -37
- package/devkit/wecom-internal-app.md +8 -3
- package/devkit/wecom-multi-corp-links.md +163 -0
- package/package.json +2 -2
- package/shell-dist/assets/{index.i0kqbxp050.js → index.6alqyx6ae0.js} +21 -1
- package/shell-dist/assets/{runtime.1xfsxcajj0.js → runtime.1xzahckt10.js} +4863 -4847
- package/shell-dist/index.html +4 -4
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/cli.test.ts +31 -0
- package/src/__tests__/new-scaffold.test.ts +13 -0
- package/src/__tests__/publish-keygen.test.ts +157 -0
- package/src/__tests__/templates-migration-schema-parity.test.ts +540 -0
- package/src/__tests__/templates-schema-columns.test.ts +15 -2
- package/src/__tests__/templates-tenant-scope.test.ts +6 -2
- package/src/build/module-def.ts +1 -0
- package/src/builtin.ts +3 -2
- package/src/bundle.ts +8 -3
- package/src/cli.ts +21 -5
- package/src/dev.ts +15 -10
- package/src/init.ts +47 -41
- package/src/install.ts +11 -4
- package/src/keygen.ts +54 -0
- package/src/lifecycle/__tests__/bundle.test.ts +21 -3
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/manifest.yaml +1 -1
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/migrations/0001__init.sql +2 -2
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/schema.yaml +5 -2
- package/src/lifecycle/__tests__/fixtures/fixture-valid/manifest.yaml +1 -1
- package/src/lifecycle/__tests__/manifest-contract.test.ts +75 -3
- package/src/lifecycle/__tests__/smoke.test.ts +2 -2
- package/src/manifest-contract.ts +91 -8
- package/src/market/bootstrap.ts +26 -2
- package/src/market/command.ts +1 -0
- package/src/market/publish.ts +8 -1
- package/src/market/walkthrough.ts +24 -8
- package/src/new.ts +2 -0
- package/src/scaffold-guard.ts +227 -0
- package/src/site.ts +2 -0
- package/src/usage.ts +13 -9
- package/src/vendor.ts +5 -1
- package/templates/config.yaml +4 -2
- package/templates/init.include.yaml +10 -3
- package/templates/openapi.json +460 -1
- package/templates/src/README.md +37 -0
- package/templates/src/app/api/_platform/_lifecycle/compensate.ts +136 -0
- package/templates/src/app/api/_platform/_lifecycle/installer.ts +78 -25
- package/templates/src/app/api/_platform/_lifecycle/lock.ts +23 -0
- package/templates/src/app/api/_platform/_lifecycle/steps/deploy-web.ts +28 -0
- package/templates/src/app/api/_platform/_lifecycle/steps/preflight.ts +46 -0
- package/templates/src/app/api/_platform/_lifecycle/steps/reload.ts +100 -36
- package/templates/src/app/api/_platform/_lifecycle/steps/smoke.ts +7 -15
- package/templates/src/app/api/_platform/_lifecycle/text-edit.ts +71 -19
- package/templates/src/app/api/_platform/_lifecycle/types.ts +26 -2
- package/templates/src/app/api/_platform/_lifecycle/uninstall.ts +93 -14
- package/templates/src/app/api/_platform/_shared/service/domain/settings.ts +8 -4
- package/templates/src/app/api/_platform/_shared/service/domain/solution-perms.ts +25 -10
- package/templates/src/app/api/_platform/manifest.yaml +1 -0
- package/templates/src/app/api/_platform/market/api.ts +235 -10
- package/templates/src/app/api/_platform/migrations/0001__baseline.mysql.sql +178 -0
- package/templates/src/app/api/_platform/migrations/0001__baseline.pg.sql +173 -0
- package/templates/src/app/api/_platform/migrations/0001__baseline.sql +188 -0
- package/templates/src/app/api/_platform/migrations/0002__market_release.mysql.sql +17 -0
- package/templates/src/app/api/_platform/migrations/0002__market_release.pg.sql +16 -0
- package/templates/src/app/api/_platform/migrations/0002__market_release.sql +33 -0
- package/templates/src/app/api/_platform/schema.yaml +18 -0
- package/templates/src/app/api/_platform/seed.mysql.sql +10 -0
- package/templates/src/app/api/_platform/seed.pg.sql +10 -0
- package/templates/src/app/api/auth/_shared/wecom/client.ts +24 -4
- package/templates/src/app/api/home/migrations/0001__create_home_tables.mysql.sql +14 -0
- package/templates/src/app/api/home/migrations/0001__create_home_tables.pg.sql +14 -0
- package/templates/src/app/api/home/migrations/0002__tenant.pg.sql +4 -0
- package/templates/src/app/api/home/seed.mysql.sql +71 -0
- package/templates/src/app/api/home/seed.pg.sql +71 -0
- package/templates/src/app/api/notification/_shared/notify.ts +6 -6
- package/templates/src/app/api/notification/migrations/0001__create_notifications.mysql.sql +11 -0
- package/templates/src/app/api/notification/migrations/0001__create_notifications.pg.sql +11 -0
- package/templates/src/app/api/notification/migrations/0002__notifications_user.mysql.sql +8 -0
- package/templates/src/app/api/notification/migrations/0003__notifications_broadcast.mysql.sql +21 -0
- package/templates/src/app/api/notification/migrations/0003__notifications_broadcast.pg.sql +22 -0
- package/templates/src/app/api/notification/migrations/0004__notifications_broadcast_hidden.mysql.sql +10 -0
- package/templates/src/app/api/notification/migrations/0004__notifications_broadcast_hidden.pg.sql +11 -0
- package/templates/src/app/api/notification/migrations/0005__tenant.pg.sql +6 -0
- package/templates/src/app/api/notification/seed.mysql.sql +8 -0
- package/templates/src/app/api/notification/seed.pg.sql +11 -0
- package/templates/src/app/api/publish/_shared/identity.ts +69 -0
- package/templates/src/app/api/publish/api.ts +349 -0
- package/templates/src/app/api/publish/manifest.yaml +8 -0
- package/templates/src/app/api/publish/migrations/0001__publish_credential.mysql.sql +23 -0
- package/templates/src/app/api/publish/migrations/0001__publish_credential.pg.sql +23 -0
- package/templates/src/app/api/publish/migrations/0001__publish_credential.sql +39 -0
- package/templates/src/app/api/publish/schema.yaml +33 -0
- package/templates/src/app/api/system/login-methods/api.ts +2 -1
- package/templates/src/app/web/_platform/__client/api.schemas.ts +15 -0
- package/templates/src/app/web/_platform/__client/index.ts +25 -1
- package/templates/src/app/web/entry.ts +9 -5
- package/templates/src/app/web/home/routes.ts +5 -0
- package/templates/src/app/web/login/locales/en-US.json +7 -1
- package/templates/src/app/web/login/locales/zh-CN.json +7 -1
- package/templates/src/app/web/login/pages/login.tsx +79 -86
- package/templates/src/app/web/login/routes.ts +54 -1
- package/templates/src/app/web/personal-center/locales/en-US.json +1 -0
- package/templates/src/app/web/personal-center/locales/zh-CN.json +1 -0
- package/templates/src/app/web/personal-center/pages/my-profile/index.tsx +5 -0
- package/templates/src/app/web/system/__client/api.schemas.ts +2 -1
- package/templates/src/app/web/system/locales/en-US.json +1 -8
- package/templates/src/app/web/system/locales/zh-CN.json +1 -8
- package/templates/src/app/web/system/pages/settings/index.tsx +17 -4
- package/templates/src/app/web/system/routes.ts +0 -6
- package/templates/src/oj-market/web/__tests__/CardGrid.test.tsx +33 -0
- package/templates/src/oj-market/web/__tests__/DetailDrawer.test.tsx +69 -0
- package/templates/src/oj-market/web/__tests__/marketClient.fake.ts +7 -1
- package/templates/src/oj-market/web/api/client.ts +3 -0
- package/templates/src/oj-market/web/api/defaultClient.ts +5 -0
- package/templates/src/oj-market/web/components/CardItem.tsx +44 -1
- package/templates/src/oj-market/web/components/DetailDrawer.tsx +17 -1
- package/templates/src/oj-market/web/entry.ts +31 -2
- package/templates/src/oj-market/web/locales/en-US.json +5 -1
- package/templates/src/oj-market/web/locales/zh-CN.json +5 -1
- package/templates/src/oj-market/web/publish/__client/api.schemas.ts +61 -0
- package/templates/src/oj-market/web/publish/__client/index.ts +164 -0
- package/templates/src/oj-market/web/publish/locales/en-US.json +80 -0
- package/templates/src/oj-market/web/publish/locales/zh-CN.json +80 -0
- package/templates/src/oj-market/web/publish/pages/history.tsx +112 -0
- package/templates/src/oj-market/web/publish/pages/index.tsx +196 -0
- package/templates/src/oj-market/web/publish/pages/model.ts +52 -0
- package/templates/src/oj-market/web/publish/pages/token-modal.tsx +82 -0
- package/templates/src/oj-market/web/publish/routes.ts +41 -0
- package/templates/src/oj-market/web/types.ts +30 -0
- package/templates/tests/lifecycle/compensate.spec.ts +207 -0
- package/templates/tests/lifecycle/deploy-smoke.spec.ts +38 -14
- package/templates/tests/lifecycle/installer.spec.ts +28 -9
- package/templates/tests/lifecycle/market-api.spec.ts +164 -12
- package/templates/tests/lifecycle/preflight.spec.ts +68 -0
- package/templates/tests/lifecycle/register.spec.ts +22 -0
- package/templates/tests/lifecycle/role-title-translate.spec.ts +4 -34
- package/templates/tests/lifecycle/text-edit.spec.ts +45 -27
- package/templates/tests/lifecycle/uninstall.spec.ts +36 -0
- package/templates/tests/publish-credential.spec.ts +354 -0
- package/templates/tests/publish-identity.spec.ts +44 -0
- package/templates/tests/rbac-solution-perms.spec.ts +48 -3
- package/templates/tests/wecom-client-timeout.spec.ts +33 -3
- package/templates/tests/wecom-landing.spec.ts +63 -1
- package/templates/tsconfig.paths.json +1 -0
- package/templates/web.config.ts +8 -4
- package/vendor/host-versions.json +3 -3
- package/templates/seed.mysql.sql +0 -6
- package/templates/seed.pg.sql +0 -6
- package/templates/src/app/api/_platform/migrations/0001__create_users.mysql.sql +0 -9
- package/templates/src/app/api/_platform/migrations/0001__create_users.pg.sql +0 -9
- package/templates/src/app/api/_platform/migrations/0001__create_users.sql +0 -7
- package/templates/src/app/api/_platform/migrations/0002__add_users_avatar_base64.mysql.sql +0 -3
- package/templates/src/app/api/_platform/migrations/0002__add_users_avatar_base64.sql +0 -2
- package/templates/src/app/api/_platform/migrations/0003__users_source_display_name.mysql.sql +0 -5
- package/templates/src/app/api/_platform/migrations/0003__users_source_display_name.sql +0 -5
- package/templates/src/app/api/_platform/migrations/0004__sys_rbac.mysql.sql +0 -70
- package/templates/src/app/api/_platform/migrations/0004__sys_rbac.pg.sql +0 -67
- package/templates/src/app/api/_platform/migrations/0004__sys_rbac.sql +0 -67
- package/templates/src/app/api/_platform/migrations/0005__users_must_change.mysql.sql +0 -3
- package/templates/src/app/api/_platform/migrations/0005__users_must_change.sql +0 -2
- package/templates/src/app/api/_platform/migrations/0006__users_is_forbidden.mysql.sql +0 -3
- package/templates/src/app/api/_platform/migrations/0006__users_is_forbidden.sql +0 -3
- package/templates/src/app/api/_platform/migrations/0007__users_email.sql +0 -3
- package/templates/src/app/api/_platform/migrations/0008__sys_refresh_token.mysql.sql +0 -10
- package/templates/src/app/api/_platform/migrations/0008__sys_refresh_token.pg.sql +0 -10
- package/templates/src/app/api/_platform/migrations/0008__sys_refresh_token.sql +0 -11
- package/templates/src/app/api/_platform/migrations/0009__sys_throttle.mysql.sql +0 -12
- package/templates/src/app/api/_platform/migrations/0009__sys_throttle.pg.sql +0 -12
- package/templates/src/app/api/_platform/migrations/0009__sys_throttle.sql +0 -13
- package/templates/src/app/api/_platform/migrations/0010__sys_solution_route.mysql.sql +0 -17
- package/templates/src/app/api/_platform/migrations/0010__sys_solution_route.pg.sql +0 -16
- package/templates/src/app/api/_platform/migrations/0010__sys_solution_route.sql +0 -18
- package/templates/src/app/api/_platform/migrations/0011__sys_setting_group_label.mysql.sql +0 -2
- package/templates/src/app/api/_platform/migrations/0011__sys_setting_group_label.sql +0 -4
- package/templates/src/app/api/_platform/migrations/0012__market_history.mysql.sql +0 -13
- package/templates/src/app/api/_platform/migrations/0012__market_history.pg.sql +0 -12
- package/templates/src/app/api/_platform/migrations/0012__market_history.sql +0 -14
- package/templates/src/app/api/_platform/migrations/0013__users_profile.sql +0 -4
- package/templates/src/app/api/_platform/migrations/0014__reg_source_seq.mysql.sql +0 -9
- package/templates/src/app/api/_platform/migrations/0014__reg_source_seq.pg.sql +0 -7
- package/templates/src/app/api/_platform/migrations/0014__reg_source_seq.sql +0 -8
- package/templates/src/app/api/_platform/migrations/0015__tenant.mysql.sql +0 -12
- package/templates/src/app/api/_platform/migrations/0015__tenant.pg.sql +0 -11
- package/templates/src/app/api/_platform/migrations/0015__tenant.sql +0 -44
- package/templates/src/app/api/_platform/migrations/0016__org.mysql.sql +0 -41
- package/templates/src/app/api/_platform/migrations/0016__org.pg.sql +0 -40
- package/templates/src/app/api/_platform/migrations/0016__org.sql +0 -62
- package/templates/src/app/api/_platform/migrations/0017__users_employee_no.mysql.sql +0 -7
- package/templates/src/app/api/_platform/migrations/0017__users_employee_no.pg.sql +0 -4
- package/templates/src/app/api/_platform/migrations/0017__users_employee_no.sql +0 -8
- package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.mysql.sql +0 -3
- package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.pg.sql +0 -3
- package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.sql +0 -8
- package/templates/src/app/web/system/pages/login-methods/index.tsx +0 -30
package/src/cli.ts
CHANGED
|
@@ -130,6 +130,10 @@ function makeAction(def: HelpEntry, projectRoot: string, lang: Lang, exit: (code
|
|
|
130
130
|
};
|
|
131
131
|
case "build":
|
|
132
132
|
return async (opts: { bundle?: unknown, out?: string } = {}) => {
|
|
133
|
+
// 脚手架完整性门禁(内置模块自治契约):普通与 --bundle 汇合前执行,
|
|
134
|
+
// 失败零产物——默认模块上游自治,本地修改不允许进包
|
|
135
|
+
const { assertDefaultModulesUntouched } = await import("./scaffold-guard");
|
|
136
|
+
assertDefaultModulesUntouched(projectRoot);
|
|
133
137
|
// mri 可选值:裸 --bundle 得到 "",--bundle name / --bundle=name 得到字符串
|
|
134
138
|
const bundle = opts.bundle !== undefined;
|
|
135
139
|
const bundleName = typeof opts.bundle === "string" && opts.bundle !== "" ? opts.bundle : undefined;
|
|
@@ -223,9 +227,21 @@ function makeAction(def: HelpEntry, projectRoot: string, lang: Lang, exit: (code
|
|
|
223
227
|
await mergeManifests(out ?? "", inputs);
|
|
224
228
|
};
|
|
225
229
|
case "keygen":
|
|
226
|
-
return async (opts: { id?: string, out?: string, yes?: boolean } = {}) => {
|
|
227
|
-
const { keygenCommand } = await import("./keygen");
|
|
228
|
-
|
|
230
|
+
return async (opts: { id?: string, out?: string, yes?: boolean, credential?: string, siteUrl?: string } = {}) => {
|
|
231
|
+
const { exchangePublishCredential, keygenCommand } = await import("./keygen");
|
|
232
|
+
// 令牌解析:--credential > OJM_PUBLISH_CREDENTIAL;都没有则走原离线路径(逐字节不变)
|
|
233
|
+
const credential = (opts.credential ?? process.env.OJM_PUBLISH_CREDENTIAL ?? "").trim();
|
|
234
|
+
if (credential === "") {
|
|
235
|
+
keygenCommand({ id: opts.id, out: opts.out, yes: opts.yes });
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
// 身份只来自凭证:--id 是另一条(离线)路径的入口,同时给必然有一方被静默忽略
|
|
239
|
+
if (opts.id !== undefined)
|
|
240
|
+
throw new UsageError("[ojm] --credential 与 --id 不能同时使用:凭证已绑定发布身份(要自定义身份请走离线 ojm keygen --id)");
|
|
241
|
+
const identity = await exchangePublishCredential(credential, { siteUrl: opts.siteUrl });
|
|
242
|
+
const who = identity.display_name === null ? identity.username : `${identity.display_name}(${identity.username})`;
|
|
243
|
+
console.log(`[ojm] 凭证已兑换:产品 ${identity.product} / 申请人 ${who}`);
|
|
244
|
+
keygenCommand({ id: identity.key_id, out: opts.out, yes: opts.yes });
|
|
229
245
|
};
|
|
230
246
|
case "sign":
|
|
231
247
|
return async (file: string | undefined, opts: { id?: string, key?: string, check?: boolean, registryId?: string } = {}) => {
|
|
@@ -283,9 +299,9 @@ function makeAction(def: HelpEntry, projectRoot: string, lang: Lang, exit: (code
|
|
|
283
299
|
exit(EXIT_FAILURE);
|
|
284
300
|
};
|
|
285
301
|
case "install":
|
|
286
|
-
return async (target: string | undefined, opts: SiteOpts & { locale?: string, name?: string, version?: string, registry?: string } = {}) => {
|
|
302
|
+
return async (target: string | undefined, opts: SiteOpts & { locale?: string, name?: string, version?: string, registry?: string, force?: boolean } = {}) => {
|
|
287
303
|
const { installCommand } = await import("./install");
|
|
288
|
-
await installCommand(target ?? "", { siteUrl: opts.siteUrl, user: opts.user, pass: opts.pass, locale: opts.locale, name: opts.name, version: opts.version, registry: opts.registry });
|
|
304
|
+
await installCommand(target ?? "", { siteUrl: opts.siteUrl, user: opts.user, pass: opts.pass, locale: opts.locale, name: opts.name, version: opts.version, registry: opts.registry, force: opts.force });
|
|
289
305
|
};
|
|
290
306
|
case "list":
|
|
291
307
|
return async (opts: SiteOpts & { all?: boolean, json?: boolean } = {}) => {
|
package/src/dev.ts
CHANGED
|
@@ -230,12 +230,6 @@ export async function devServer(projectRoot: string, opts: DevOptions = {}): Pro
|
|
|
230
230
|
proxyWsUpgrade(ojTarget)(req, socket, head);
|
|
231
231
|
});
|
|
232
232
|
|
|
233
|
-
// 退出回收:server 关闭 → SSE 通道 + oj 子进程一并回收
|
|
234
|
-
server.on("close", () => {
|
|
235
|
-
hub.close();
|
|
236
|
-
void oj?.stop();
|
|
237
|
-
});
|
|
238
|
-
|
|
239
233
|
// 终端 Ctrl+C / `kill`(SIGINT / SIGTERM)终止 dev 进程时,后端 oj 必须随之消亡:
|
|
240
234
|
// 起一个统一优雅停机,先停 oj 再退出;并打印后端终止信息。
|
|
241
235
|
// 终端 Ctrl-C 是进程组信号:外层 bin 转发一份 + 组送达一份,内层会收到
|
|
@@ -249,13 +243,24 @@ export async function devServer(projectRoot: string, opts: DevOptions = {}): Pro
|
|
|
249
243
|
shuttingDown = true;
|
|
250
244
|
void shutdownDev(server, oj, hub);
|
|
251
245
|
};
|
|
252
|
-
process.on("SIGINT", shutdown);
|
|
253
|
-
process.on("SIGTERM", shutdown);
|
|
254
|
-
|
|
255
246
|
// 兜底:dev 进程以任何非优雅路径退出(未走 SIGINT/SIGTERM,如未捕获异常)时,
|
|
256
247
|
// 同步强杀 oj,确保它不孤儿化继续占端口(shutdownDev 已停则 kill 为 no-op)。
|
|
257
|
-
|
|
248
|
+
const killOjOnExit = () => {
|
|
258
249
|
oj?.kill?.();
|
|
250
|
+
};
|
|
251
|
+
process.on("SIGINT", shutdown);
|
|
252
|
+
process.on("SIGTERM", shutdown);
|
|
253
|
+
process.once("exit", killOjOnExit);
|
|
254
|
+
|
|
255
|
+
// 退出回收:server 关闭 → SSE 通道 + oj 子进程一并回收,并摘除本实例注册的
|
|
256
|
+
// 进程级信号/exit handler。devServer 可在同一进程被多次调用(编程式 / 测试),
|
|
257
|
+
// 不摘除会累积闭包、触发 MaxListenersExceededWarning(与 preview 的 stop 同款)。
|
|
258
|
+
server.on("close", () => {
|
|
259
|
+
hub.close();
|
|
260
|
+
void oj?.stop();
|
|
261
|
+
process.removeListener("SIGINT", shutdown);
|
|
262
|
+
process.removeListener("SIGTERM", shutdown);
|
|
263
|
+
process.removeListener("exit", killOjOnExit);
|
|
259
264
|
});
|
|
260
265
|
|
|
261
266
|
// 端口被占用时自动顺延,避免 `EADDRINUSE` 直接让 `pnpm dev` 以非零码退出
|
package/src/init.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* 幂等补缺:对既有工程重跑只补缺失文件(bin/oj 已在则跳过下载,但先过 §7 能力闸门),
|
|
9
9
|
* config.yaml 与用户文件永不覆盖——「重新 init 覆盖脚手架」是审阅记录二明令禁止的行为。
|
|
10
|
+
* 唯一例外是 `--yes`:默认方案的显式恢复入口,仅覆盖/清理上游自治的保护根
|
|
11
|
+
* (src/app、src/oj-market,见 scaffold-guard.ts),自定义模块与工程配置仍不动。
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
import { execFileSync } from "node:child_process";
|
|
@@ -14,18 +16,23 @@ import crypto from "node:crypto";
|
|
|
14
16
|
import fs from "node:fs";
|
|
15
17
|
import path from "node:path";
|
|
16
18
|
import process from "node:process";
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
18
|
-
import yaml from "yaml";
|
|
19
19
|
import { ALIAS_PATHS_FILE, syncAliasPaths } from "./alias-paths";
|
|
20
20
|
import { BUILTIN_LIST_FILE, writeBuiltinList } from "./builtin";
|
|
21
21
|
import { resolveCertDir, resolveConfigPath } from "./layout";
|
|
22
22
|
import { probeOjCaps } from "./oj-caps";
|
|
23
23
|
import { mintDevCert } from "./oj-cert";
|
|
24
|
+
import {
|
|
25
|
+
isProtectedRel,
|
|
26
|
+
readInitInclude,
|
|
27
|
+
removeExtraProtectedFiles,
|
|
28
|
+
cliRoot as resolveCliRoot,
|
|
29
|
+
templatesRoot,
|
|
30
|
+
} from "./scaffold-guard";
|
|
24
31
|
import { isSupportedOjVersion, MIN_OJ_VERSION, resolveOjBin, syncOjDevkit, vendorCommand } from "./vendor";
|
|
25
32
|
import { readHostVersions } from "./versions";
|
|
26
33
|
|
|
27
34
|
export interface InitOptions {
|
|
28
|
-
/**
|
|
35
|
+
/** 跳过非空目录确认(自动化/测试用);对既有工程同时恢复上游自治的保护根 */
|
|
29
36
|
yes?: boolean
|
|
30
37
|
/** 注入桩 oj 安装器(测试);默认 vendorCommand 经 npm 包 @oj-bin/oj 联网安装 */
|
|
31
38
|
ojInstaller?: (binDir: string) => Promise<void>
|
|
@@ -109,26 +116,34 @@ export async function initProject(destDir: string, opts: InitOptions = {}): Prom
|
|
|
109
116
|
}
|
|
110
117
|
|
|
111
118
|
const report: InitReport = { created: [], skipped: [], warnings: [] };
|
|
112
|
-
const cliRoot =
|
|
119
|
+
const cliRoot = resolveCliRoot();
|
|
113
120
|
|
|
114
121
|
// 0) 已有 bin/oj 先过能力闸门(评审 BLOCKING #8):早前「bin/oj 存在即跳过」
|
|
115
122
|
// 会让从旧工程带来的 0.1.84 静默过关,等 `ojm build` 才死在 --module-root 不认。
|
|
116
123
|
assertExistingOjUsable(destDir, report);
|
|
117
124
|
|
|
118
|
-
// 1) 模板(占位符替换 +
|
|
119
|
-
const templatesDir =
|
|
125
|
+
// 1) 模板(占位符替换 + 幂等跳过;--yes 时保护根内改为覆盖恢复)
|
|
126
|
+
const templatesDir = templatesRoot();
|
|
120
127
|
const jwtSecret = crypto.randomBytes(32).toString("hex");
|
|
121
128
|
const projectName = path.basename(destDir);
|
|
122
129
|
const include = readInitInclude(templatesDir);
|
|
123
130
|
copyTemplates(templatesDir, destDir, {
|
|
124
131
|
__JWT_SECRET__: jwtSecret,
|
|
125
132
|
__PROJECT_NAME__: projectName,
|
|
126
|
-
}, report, include, templatesDir);
|
|
133
|
+
}, report, include, templatesDir, false, !!opts.yes);
|
|
127
134
|
pruneUnincludedApiPaths(destDir, include, report);
|
|
128
135
|
|
|
136
|
+
// 1a') --yes 恢复的清理面:删除保护根内模板没有的文件(留着会让 build 门禁
|
|
137
|
+
// 永久失败——extra 漂移不因 init 补缺而消失,必须显式清掉)
|
|
138
|
+
if (opts.yes) {
|
|
139
|
+
for (const rel of removeExtraProtectedFiles(destDir))
|
|
140
|
+
report.created.push(`恢复:删除多出文件 ${rel}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
129
143
|
// 1a) src/.ojm-builtin.json:脚手架自带模块身份(名单派生自上面的白名单,单一真源)。
|
|
130
|
-
// copyIfAbsent:旧工程重跑 init
|
|
131
|
-
|
|
144
|
+
// copyIfAbsent:旧工程重跑 init 时缺失才补写,已存在不覆盖用户手改;
|
|
145
|
+
// --yes 恢复语义下重写为上游名单。
|
|
146
|
+
const builtinStatus = writeBuiltinList(destDir, include, { overwrite: !!opts.yes });
|
|
132
147
|
if (builtinStatus === "created")
|
|
133
148
|
report.created.push(`src/${BUILTIN_LIST_FILE}`);
|
|
134
149
|
else if (builtinStatus === "skipped")
|
|
@@ -317,6 +332,9 @@ async function sealJwtSecret(destDir: string, report: InitReport, seal: Sealer =
|
|
|
317
332
|
* created/skipped(demo 模块/模板自运行产物/元数据的大头在清单外)。清单外的
|
|
318
333
|
* 目录仅在「有收录子孙」时才穿行(如 `api/`、`web/src/`、`web/src/demo/`——
|
|
319
334
|
* 只收录了后者底下的 entry/locales/pages),穿行时未收录的子项继续被挡下。
|
|
335
|
+
*
|
|
336
|
+
* `restore`(--yes):上游自治保护根(scaffold-guard.PROTECTED_ROOTS)内的
|
|
337
|
+
* 已存在文件改为覆盖写入——这是默认方案的显式恢复入口,其余文件仍不覆盖。
|
|
320
338
|
*/
|
|
321
339
|
function copyTemplates(
|
|
322
340
|
srcDir: string,
|
|
@@ -326,6 +344,7 @@ function copyTemplates(
|
|
|
326
344
|
include: Set<string>,
|
|
327
345
|
rootDir: string,
|
|
328
346
|
underIncluded = false,
|
|
347
|
+
restore = false,
|
|
329
348
|
): void {
|
|
330
349
|
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
331
350
|
const src = path.join(srcDir, entry.name);
|
|
@@ -343,24 +362,36 @@ function copyTemplates(
|
|
|
343
362
|
if (!dirHasFiles(src))
|
|
344
363
|
continue;
|
|
345
364
|
fs.mkdirSync(dest, { recursive: true });
|
|
346
|
-
copyTemplates(src, dest, placeholders, report, include, rootDir, underIncluded || listed);
|
|
365
|
+
copyTemplates(src, dest, placeholders, report, include, rootDir, underIncluded || listed, restore);
|
|
347
366
|
continue;
|
|
348
367
|
}
|
|
368
|
+
// 恢复判定用 srcRel(模板相对 == 工程相对,已归一正斜杠);destRel 是相对
|
|
369
|
+
// 当前递归层 destDir 的短路径,只配做报告标签,做不了保护根判定
|
|
349
370
|
const destRel = path.relative(destDir, dest);
|
|
350
371
|
if (fs.existsSync(dest)) {
|
|
372
|
+
if (restore && isProtectedRel(srcRel)) {
|
|
373
|
+
writeTemplateFile(src, dest, placeholders);
|
|
374
|
+
report.created.push(`恢复(--yes 覆盖)${srcRel}`);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
351
377
|
report.skipped.push(destRel);
|
|
352
378
|
continue;
|
|
353
379
|
}
|
|
354
|
-
|
|
355
|
-
for (const [token, value] of Object.entries(placeholders))
|
|
356
|
-
content = content.replaceAll(token, value);
|
|
357
|
-
content = stripRegion(content, "demo-only");
|
|
358
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
359
|
-
fs.writeFileSync(dest, content);
|
|
380
|
+
writeTemplateFile(src, dest, placeholders);
|
|
360
381
|
report.created.push(destRel);
|
|
361
382
|
}
|
|
362
383
|
}
|
|
363
384
|
|
|
385
|
+
/** 读模板 → 占位符替换 → 剥 demo-only 区间 → 落盘(创建与 --yes 恢复共用一条变换链) */
|
|
386
|
+
function writeTemplateFile(src: string, dest: string, placeholders: Record<string, string>): void {
|
|
387
|
+
let content = fs.readFileSync(src, "utf-8");
|
|
388
|
+
for (const [token, value] of Object.entries(placeholders))
|
|
389
|
+
content = content.replaceAll(token, value);
|
|
390
|
+
content = stripRegion(content, "demo-only");
|
|
391
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
392
|
+
fs.writeFileSync(dest, content);
|
|
393
|
+
}
|
|
394
|
+
|
|
364
395
|
/** srcRel 是否为白名单里某条收录路径的祖先目录——决定清单外目录要不要穿行 */
|
|
365
396
|
function hasIncludedDescendant(include: Set<string>, srcRel: string): boolean {
|
|
366
397
|
const prefix = `${srcRel}/`;
|
|
@@ -382,31 +413,6 @@ function dirHasFiles(dir: string): boolean {
|
|
|
382
413
|
return false;
|
|
383
414
|
}
|
|
384
415
|
|
|
385
|
-
/**
|
|
386
|
-
* 读取模板根 `init.include.yaml` 的 `include` 白名单,归一为相对模板根的路径集合。
|
|
387
|
-
* 清单随模板一起发布,缺失即安装包损坏——直接 fail-fast,不留「静默少拷」的
|
|
388
|
-
* 退化路径。条目指向不存在的路径同样 fail-fast:拼错一个目录名不会当场报错,
|
|
389
|
-
* 只会在用户工程里留下难以追溯的缺文件(旧 exclude 清单没有这类校验面)。
|
|
390
|
-
*/
|
|
391
|
-
function readInitInclude(templatesDir: string): Set<string> {
|
|
392
|
-
const yamlPath = path.join(templatesDir, "init.include.yaml");
|
|
393
|
-
if (!fs.existsSync(yamlPath))
|
|
394
|
-
throw new Error(`模板缺少 init.include.yaml:${yamlPath}(安装包损坏?)`);
|
|
395
|
-
const doc = yaml.parse(fs.readFileSync(yamlPath, "utf-8")) as { include?: string[] } | null;
|
|
396
|
-
const out = new Set<string>();
|
|
397
|
-
for (const raw of doc?.include ?? []) {
|
|
398
|
-
// 跨平台:include 清单用正斜杠书写,但 path.normalize 在 Windows 会把
|
|
399
|
-
// 正斜杠折成反斜杠,导致与 srcRel(path.relative 在 Windows 同样出反斜杠)
|
|
400
|
-
// 的对比永远落空、整棵 src 子树被 skip。这里只把反斜杠归正、剥 ./ 前缀,
|
|
401
|
-
// 不引入平台分隔符(修复:Windows 下 init 不拷贝 src 树)。
|
|
402
|
-
const p = raw.replace(/\\/g, "/").replace(/^(\.\/)+/, "");
|
|
403
|
-
out.add(p);
|
|
404
|
-
if (!fs.existsSync(path.join(templatesDir, p)))
|
|
405
|
-
throw new Error(`init.include.yaml 收录了不存在的路径:${p}(拼写错误或模板缺文件?)`);
|
|
406
|
-
}
|
|
407
|
-
return out;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
416
|
/**
|
|
411
417
|
* 从脚手架产出的 openapi.json 里删掉未进白名单的 api 模块端点。
|
|
412
418
|
*
|
package/src/install.ts
CHANGED
|
@@ -254,6 +254,11 @@ export interface InstallOptions {
|
|
|
254
254
|
locale?: string
|
|
255
255
|
/** REG-R9②:从指定 registry 源安装(白名单在服务端;旧站点 API < 0.2.0 不支持 → exit 1) */
|
|
256
256
|
registry?: string
|
|
257
|
+
/**
|
|
258
|
+
* 强制重装配(oj 0.1.123+ reloadConfig(force)):旁路「unchanged=已是最新装配」幂等短路,
|
|
259
|
+
* 强制宿主重做一次装配。运维显式重装配用;缺省 false。
|
|
260
|
+
*/
|
|
261
|
+
force?: boolean
|
|
257
262
|
/** 测试注入;缺省 globalThis.fetch */
|
|
258
263
|
fetchFn?: typeof fetch
|
|
259
264
|
/** 测试注入;缺省真睡 */
|
|
@@ -269,6 +274,8 @@ export async function installCommand(target: string, opts: InstallOptions = {}):
|
|
|
269
274
|
const fetchFn = opts.fetchFn ?? globalThis.fetch;
|
|
270
275
|
const { base, user, pass } = resolveSite(opts);
|
|
271
276
|
const token = await login(fetchFn, base, user, pass);
|
|
277
|
+
// force 透传(oj 0.1.123+ reloadConfig(force) 逃逸舱):仅运维显式重装配时用;常规安装走锁 diff
|
|
278
|
+
const forceQ = opts.force === true ? "?force=1" : "";
|
|
272
279
|
|
|
273
280
|
let response: { task_id: string };
|
|
274
281
|
if (opts.registry !== undefined) {
|
|
@@ -282,13 +289,13 @@ export async function installCommand(target: string, opts: InstallOptions = {}):
|
|
|
282
289
|
if (!isSiteApiAtLeast(meta.apiVersion, "0.2.0"))
|
|
283
290
|
throw new Error(`[ojm] 目标站点平台 API 版本过低(需 ≥ 0.2.0 才支持 --registry 源安装,实为 ${meta.apiVersion ?? "未知"})——请升级目标站点的 oj-module,或改用 CDN/file 源`);
|
|
284
291
|
const registryOrigin = originOf(src.url) ?? src.url;
|
|
285
|
-
response = await callApi<{ task_id: string }>(fetchFn, base, token, "POST",
|
|
292
|
+
response = await callApi<{ task_id: string }>(fetchFn, base, token, "POST", `/api/_platform/market/install${forceQ}`, {
|
|
286
293
|
name: slug,
|
|
287
294
|
version: opts.version ?? version,
|
|
288
295
|
source: "registry",
|
|
289
296
|
registry: registryOrigin,
|
|
290
297
|
});
|
|
291
|
-
console.log(`[ojm] 已从 registry 源提交安装任务(源 ${registryOrigin},物理名 ${slug})`);
|
|
298
|
+
console.log(`[ojm] 已从 registry 源提交安装任务(源 ${registryOrigin},物理名 ${slug}${opts.force === true ? ",强制重装配" : ""})`);
|
|
292
299
|
}
|
|
293
300
|
else {
|
|
294
301
|
const parsed = parseTarget(target);
|
|
@@ -314,7 +321,7 @@ export async function installCommand(target: string, opts: InstallOptions = {}):
|
|
|
314
321
|
else {
|
|
315
322
|
console.log(`[ojm] 未发现签名侧车(${path.basename(sigPath)})——宿主验签硬 fail-closed,未签名包将被拒(sig_missing);签名方法见 devkit「签名与信任」`);
|
|
316
323
|
}
|
|
317
|
-
const res = await fetchFn(`${base}/api/_platform/market/install`, {
|
|
324
|
+
const res = await fetchFn(`${base}/api/_platform/market/install${forceQ}`, {
|
|
318
325
|
method: "POST",
|
|
319
326
|
headers: { authorization: `Bearer ${token}`, ...tenantHeaders() },
|
|
320
327
|
body: form,
|
|
@@ -324,7 +331,7 @@ export async function installCommand(target: string, opts: InstallOptions = {}):
|
|
|
324
331
|
else {
|
|
325
332
|
if (opts.name !== undefined && opts.name !== parsed.name)
|
|
326
333
|
throw new Error(`[ojm] --name「${opts.name}」与目标「${parsed.name}」不一致——请二选一`);
|
|
327
|
-
response = await callApi<{ task_id: string }>(fetchFn, base, token, "POST",
|
|
334
|
+
response = await callApi<{ task_id: string }>(fetchFn, base, token, "POST", `/api/_platform/market/install${forceQ}`, {
|
|
328
335
|
name: parsed.name,
|
|
329
336
|
version: opts.version ?? parsed.version,
|
|
330
337
|
source: "cdn",
|
package/src/keygen.ts
CHANGED
|
@@ -10,6 +10,7 @@ import path from "node:path";
|
|
|
10
10
|
import process from "node:process";
|
|
11
11
|
import { generateKeyPair } from "./lifecycle/keygen";
|
|
12
12
|
import { fingerprintOfPublicKey } from "./lifecycle/sign";
|
|
13
|
+
import { DEFAULT_SITE_URL, tenantHeaders, unwrapEnvelope } from "./site";
|
|
13
14
|
|
|
14
15
|
export interface KeygenOptions {
|
|
15
16
|
/** 发布方 key_id(C2-37:<publisher>.<id> 全小写,禁裸通用名) */
|
|
@@ -67,3 +68,56 @@ export function keygenCommand(opts: KeygenOptions = {}): KeygenResult {
|
|
|
67
68
|
console.log(`[ojm] 下一步:ojm sign <pkg>.tgz --id ${keyId}(或 --key ${privateKeyPath} --id ${keyId})`);
|
|
68
69
|
return { keyId, publicKeyHex: kp.publicKeyHex, fingerprint, dir, privateKeyPath, publicKeyPath };
|
|
69
70
|
}
|
|
71
|
+
|
|
72
|
+
/** 兑换结果:服务端下发身份(CLI 零推导——编码规则只在 templates 侧一份) */
|
|
73
|
+
export interface ExchangedIdentity {
|
|
74
|
+
key_id: string
|
|
75
|
+
product: string
|
|
76
|
+
username: string
|
|
77
|
+
display_name: string | null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ExchangeOptions {
|
|
81
|
+
/** 站点地址;flag > OJM_SITE_URL > DEFAULT_SITE_URL */
|
|
82
|
+
siteUrl?: string
|
|
83
|
+
/** 测试注入 */
|
|
84
|
+
fetchFn?: typeof fetch
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 用一次性发布凭证换身份(匿名端点,不需要账号密码)。
|
|
89
|
+
*
|
|
90
|
+
* 只做「取回服务端算好的 key_id」——不做任何编码推导(规则漂移比多一次网络请求贵得多),
|
|
91
|
+
* 但**必须**校验拿回的 key_id 形状:它马上会被拼进 `~/.ojm/keys/<key_id>`,
|
|
92
|
+
* 一个被中间人改成 `../../x` 的响应就是任意路径写入。失败一律不落盘(调用方先兑换后生成)。
|
|
93
|
+
*/
|
|
94
|
+
export async function exchangePublishCredential(token: string, opts: ExchangeOptions = {}): Promise<ExchangedIdentity> {
|
|
95
|
+
const t = token.trim();
|
|
96
|
+
// 形状先挡:不是 64 位 hex 的输入连请求都不必发(与页面生成规则一致)
|
|
97
|
+
if (!/^[0-9a-f]{64}$/i.test(t))
|
|
98
|
+
throw new Error("[ojm] 发布凭证格式不对(应为 64 位十六进制):请在站点「发布管理」页复制完整凭证");
|
|
99
|
+
|
|
100
|
+
const base = (opts.siteUrl ?? process.env.OJM_SITE_URL ?? DEFAULT_SITE_URL).replace(/\/+$/, "");
|
|
101
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
102
|
+
let res: Response;
|
|
103
|
+
try {
|
|
104
|
+
res = await fetchFn(`${base}/api/publish/credential/exchange`, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
headers: { "content-type": "application/json", ...tenantHeaders() },
|
|
107
|
+
body: JSON.stringify({ token: t }),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
throw new Error(`[ojm] 无法连接站点 ${base}:${e instanceof Error ? e.message : String(e)}——请确认站点地址(--site-url / OJM_SITE_URL)与网络`);
|
|
112
|
+
}
|
|
113
|
+
const data = await unwrapEnvelope(res) as Partial<ExchangedIdentity> | null;
|
|
114
|
+
const keyId = String(data?.key_id ?? "");
|
|
115
|
+
if (!isValidKeyId(keyId))
|
|
116
|
+
throw new Error(`[ojm] 站点返回的 key_id 非法(C2-37):「${keyId}」——已中止,未写入任何密钥(请核对站点版本)`);
|
|
117
|
+
return {
|
|
118
|
+
key_id: keyId,
|
|
119
|
+
product: String(data?.product ?? ""),
|
|
120
|
+
username: String(data?.username ?? ""),
|
|
121
|
+
display_name: data?.display_name == null ? null : String(data.display_name),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -53,8 +53,8 @@ function makePrebuiltArtifacts(): void {
|
|
|
53
53
|
const apiDist = join(project, "dist/api/demo-solution-0.1.0");
|
|
54
54
|
mkdirSync(join(apiDist, "ping"), { recursive: true });
|
|
55
55
|
mkdirSync(join(apiDist, "migrations"), { recursive: true });
|
|
56
|
-
writeFileSync(join(apiDist, "manifest.yaml"), "name: demo-solution\ndesc: manifest.yaml 契约全字段合法夹具\nversion: 0.1.0\ntables:\n -
|
|
57
|
-
writeFileSync(join(apiDist, "schema.yaml"), "
|
|
56
|
+
writeFileSync(join(apiDist, "manifest.yaml"), "name: demo-solution\ndesc: manifest.yaml 契约全字段合法夹具\nversion: 0.1.0\ntables:\n - demo_solution_note\n");
|
|
57
|
+
writeFileSync(join(apiDist, "schema.yaml"), "prefix: demo_solution_\ntables:\n demo_solution_note: {}\n");
|
|
58
58
|
writeFileSync(join(apiDist, "routes.js"), "export {};\n");
|
|
59
59
|
writeFileSync(join(apiDist, "ping/api.js"), "export async function ping() {}\n");
|
|
60
60
|
// 站点构建产物(dist/web/modules/<name>/<version>/entry.js + css,对应 web.entry/css 声明)
|
|
@@ -317,11 +317,29 @@ describe("buildBundle · 校验先于打包(fail-fast)", () => {
|
|
|
317
317
|
|
|
318
318
|
it("c2-12a:api.tables 与 schema.yaml 表集不一致 → 构建期拒绝", async () => {
|
|
319
319
|
const m = readManifest();
|
|
320
|
-
m.api.tables = ["
|
|
320
|
+
m.api.tables = ["demo_solution_note", "ghost"];
|
|
321
321
|
writeManifest(m);
|
|
322
322
|
await expect(pack()).rejects.toThrow(/C2-12a/);
|
|
323
323
|
});
|
|
324
324
|
|
|
325
|
+
it("c2-12c:schema.yaml 缺 prefix / 表名未带方案前缀 → 构建期拒绝", async () => {
|
|
326
|
+
const schemaPath = join(project, "api/src/demo-solution/schema.yaml");
|
|
327
|
+
writeFileSync(schemaPath, "tables:\n demo_solution_note: {}\n");
|
|
328
|
+
await expect(pack()).rejects.toThrow(/C2-12c.*prefix/);
|
|
329
|
+
|
|
330
|
+
writeFileSync(schemaPath, "prefix: demo_solution_\ntables:\n note: {}\n");
|
|
331
|
+
await expect(pack()).rejects.toThrow(/C2-12c.*demo_solution_note/);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("c2-12c:prefix 落在第一方保留段 → 构建期拒绝", async () => {
|
|
335
|
+
const m = readManifest();
|
|
336
|
+
m.api.tables = ["oj_note"];
|
|
337
|
+
writeManifest(m);
|
|
338
|
+
const schemaPath = join(project, "api/src/demo-solution/schema.yaml");
|
|
339
|
+
writeFileSync(schemaPath, "prefix: oj_\ntables:\n oj_note: {}\n");
|
|
340
|
+
await expect(pack()).rejects.toThrow(/C2-12c.*保留段/);
|
|
341
|
+
});
|
|
342
|
+
|
|
325
343
|
it("c2-25:locales 缺引用 key → 构建期 fail-fast", async () => {
|
|
326
344
|
const m = readManifest();
|
|
327
345
|
m.roles[0].title_key = "demo-solution:role.absent";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
-- 0001__init:建
|
|
2
|
-
CREATE TABLE IF NOT EXISTS
|
|
1
|
+
-- 0001__init:建 demo_solution_note 表(与 schema.yaml 列集一致;oj migrate 按 seq 升序执行)
|
|
2
|
+
CREATE TABLE IF NOT EXISTS demo_solution_note (
|
|
3
3
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
4
4
|
title TEXT NOT NULL,
|
|
5
5
|
create_time INTEGER NOT NULL
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# 声明式表结构(形态对齐 apps/playground-oj/api/src/demo/schema.yaml)。
|
|
2
|
-
#
|
|
2
|
+
# C2-12c:prefix 必须 = 模块名 kebab→snake 再补尾部 `_`(demo-solution → demo_solution_),
|
|
3
|
+
# 且每张表名以它开头——表名全库共享,前缀把归属编码进名字,避免方案之间撞表。
|
|
4
|
+
prefix: demo_solution_
|
|
5
|
+
# 契约 C2-12a:bundle manifest 的 api.tables 必须与本表 keys 一致(oj 冗余校验 fail-fast)。
|
|
3
6
|
tables:
|
|
4
|
-
|
|
7
|
+
demo_solution_note:
|
|
5
8
|
pk: id
|
|
6
9
|
columns:
|
|
7
10
|
id: {type: integer, autoincrement: true}
|
|
@@ -26,7 +26,7 @@ peerPlatform: '>=0.1.0' # C2-11(P2 起对宿主 API_VERSION 强制;本仓 AP
|
|
|
26
26
|
api:
|
|
27
27
|
module: demo-solution # C2-12(存在时必须 === name)
|
|
28
28
|
prefix: /api/demo-solution # C2-13(默认形态显式写出)
|
|
29
|
-
tables: [
|
|
29
|
+
tables: [demo_solution_note] # 与 schema.yaml 表集一致(oj 冗余校验同源);表名带 C2-12c 前缀
|
|
30
30
|
deps: # 键 = 模块名(C2-01 语法或 _platform);值 = 范围算子语法
|
|
31
31
|
_platform: '>=0.1.0'
|
|
32
32
|
anonymous: # C2-26:首段 === 模块段;无登录语义端点
|
|
@@ -14,7 +14,7 @@ import { parse } from "yaml";
|
|
|
14
14
|
// 跨端引用(双端一致钉):被测实现同仓分居两运行时(计划 §1「两运行时取舍」),
|
|
15
15
|
// 测试直接互调,比「两套件各读同一夹具」更强——漂移即本文件红。
|
|
16
16
|
import { preflight } from "../../../templates/src/app/api/_platform/_lifecycle/steps/preflight";
|
|
17
|
-
import { checkLocalesComplete, checkTablesAgainstSchema, parseManifest, RESERVED_NAMES, reservedNameSuggestions, validateContract } from "../../manifest-contract";
|
|
17
|
+
import { checkLocalesComplete, checkSchemaTablePrefix, checkTablesAgainstSchema, parseManifest, RESERVED_NAMES, reservedNameSuggestions, validateContract } from "../../manifest-contract";
|
|
18
18
|
|
|
19
19
|
const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), "fixtures");
|
|
20
20
|
const readText = (p: string) => readFileSync(join(FIXTURES, p), "utf8");
|
|
@@ -214,19 +214,91 @@ describe("rEG-CS7 保留名扩项(红队 F5)", () => {
|
|
|
214
214
|
describe("checkTablesAgainstSchema(C2-12a:构建 ✅ 对照 schema.yaml)", () => {
|
|
215
215
|
const SCHEMA = readText("fixture-valid/api/src/demo-solution/schema.yaml");
|
|
216
216
|
|
|
217
|
-
it("fixture-valid tables
|
|
217
|
+
it("fixture-valid tables 与 schema.yaml 表集一致 → 零违规", () => {
|
|
218
218
|
expect(checkTablesAgainstSchema(structuredClone(VALID), SCHEMA)).toEqual([]);
|
|
219
219
|
});
|
|
220
220
|
|
|
221
221
|
it("tables 与 schema.yaml 表集不一致 → C2-12a 且指明差额", () => {
|
|
222
222
|
const m = structuredClone(VALID);
|
|
223
|
-
(m.api as any).tables = ["
|
|
223
|
+
(m.api as any).tables = ["demo_solution_note", "ghost"];
|
|
224
224
|
const v = checkTablesAgainstSchema(m, SCHEMA);
|
|
225
225
|
expect(v[0]).toContain("C2-12a");
|
|
226
226
|
expect(v[0]).toContain("ghost");
|
|
227
227
|
});
|
|
228
228
|
});
|
|
229
229
|
|
|
230
|
+
describe("checkSchemaTablePrefix(C2-12c:schema.yaml 表前缀)", () => {
|
|
231
|
+
const SCHEMA = readText("fixture-valid/api/src/demo-solution/schema.yaml");
|
|
232
|
+
|
|
233
|
+
it("fixture-valid 声明 prefix 且表名带前缀 → 零违规", () => {
|
|
234
|
+
expect(checkSchemaTablePrefix(SCHEMA)).toEqual([]);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("允许自选短前缀(长模块名可用 crm_ 之类,不强制由模块名派生)", () => {
|
|
238
|
+
expect(checkSchemaTablePrefix("prefix: dm_\ntables:\n dm_note: {}\n")).toEqual([]);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("缺 prefix → C2-12c", () => {
|
|
242
|
+
const v = checkSchemaTablePrefix("tables:\n demo_solution_note: {}\n");
|
|
243
|
+
expect(v[0]).toContain("C2-12c");
|
|
244
|
+
expect(v[0]).toContain("prefix");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("prefix 不以 _ 结尾 → C2-12c 语法违规", () => {
|
|
248
|
+
const v = checkSchemaTablePrefix("prefix: demo_solution\ntables:\n demo_solution_note: {}\n");
|
|
249
|
+
expect(v[0]).toContain("C2-12c");
|
|
250
|
+
expect(v[0]).toContain("_ 结尾");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("prefix 超过 32 字符 → C2-12c(吃掉表名可用空间)", () => {
|
|
254
|
+
const long = `${"a".repeat(33)}_`;
|
|
255
|
+
const v = checkSchemaTablePrefix(`prefix: ${long}\ntables:\n ${long}note: {}\n`);
|
|
256
|
+
expect(v[0]).toContain("C2-12c");
|
|
257
|
+
expect(v[0]).toContain("超过上限 32");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("表名未带前缀 → C2-12c 并给出改名建议", () => {
|
|
261
|
+
const v = checkSchemaTablePrefix("prefix: demo_solution_\ntables:\n note: {}\n");
|
|
262
|
+
expect(v[0]).toContain("C2-12c");
|
|
263
|
+
expect(v[0]).toContain("demo_solution_note");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("表名总长边界:63 合法、64 拒绝(PostgreSQL 上限 63 字节)", () => {
|
|
267
|
+
const p = "ab_";
|
|
268
|
+
const name63 = `${p}${"c".repeat(60)}`; // 3 + 60 = 63
|
|
269
|
+
const name64 = `${p}${"c".repeat(61)}`; // 3 + 61 = 64
|
|
270
|
+
expect(name63.length).toBe(63);
|
|
271
|
+
expect(checkSchemaTablePrefix(`prefix: ${p}\ntables:\n ${name63}: {}\n`)).toEqual([]);
|
|
272
|
+
const v = checkSchemaTablePrefix(`prefix: ${p}\ntables:\n ${name64}: {}\n`);
|
|
273
|
+
expect(v[0]).toContain("C2-12c");
|
|
274
|
+
expect(v[0]).toContain("63 字符");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("引用式方案(api.tables 缺省)也做前缀检查", () => {
|
|
278
|
+
const m = structuredClone(VALID);
|
|
279
|
+
delete (m.api as any).tables;
|
|
280
|
+
const v = checkTablesAgainstSchema(m, "tables:\n note: {}\n");
|
|
281
|
+
expect(v[0]).toContain("C2-12c");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("prefix 落在第一方保留段(oj_ / ojm_ 开头)→ C2-12c,整段禁用含派生形式", () => {
|
|
285
|
+
for (const p of ["oj_", "ojm_", "oj_crm_", "ojm_tools_"]) {
|
|
286
|
+
const v = checkSchemaTablePrefix(`prefix: ${p}\ntables:\n ${p}note: {}\n`);
|
|
287
|
+
expect(v.some(x => x.includes("C2-12c") && x.includes("保留段"))).toBe(true);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("自有前缀不落在保留段 → 零违规(保留段检查的对照)", () => {
|
|
292
|
+
expect(checkSchemaTablePrefix("prefix: crm_\ntables:\n crm_note: {}\n")).toEqual([]);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("_oj_ 因下划线开头只报语法违规,不重复报保留段", () => {
|
|
296
|
+
const v = checkSchemaTablePrefix("prefix: _oj_\ntables:\n _oj_note: {}\n");
|
|
297
|
+
expect(v[0]).toContain("必须匹配");
|
|
298
|
+
expect(v.every(x => !x.includes("保留段"))).toBe(true);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
230
302
|
describe("checkLocalesComplete(C2-25:构建期缺 key fail-fast)", () => {
|
|
231
303
|
const ZH = readText("fixture-valid/web/src/demo-solution/locales/zh-CN.json");
|
|
232
304
|
const EN = readText("fixture-valid/web/src/demo-solution/locales/en-US.json");
|
|
@@ -37,11 +37,11 @@ describe.skipIf(!smokeReady)("真机冒烟(需 OJM_SMOKE_URL/OJM_SMOKE_USER/OJ
|
|
|
37
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
|
-
// 迁移 SQL 随夹具 src 真源(0001__init.sql 建
|
|
40
|
+
// 迁移 SQL 随夹具 src 真源(0001__init.sql 建 demo_solution_note 表);B-11 硬验收:appliedSeq > 0 可断言
|
|
41
41
|
cpSync(join(FIXTURE_VALID, "api/src/demo-solution/migrations"), join(apiDist, "migrations"), { recursive: true });
|
|
42
42
|
// schema.yaml 同随夹具真源(列集与迁移 SQL 一致):oj migrate 在 apply 后跑 schema
|
|
43
43
|
// reconcile(migrate_cmd.rs migrate_one),声明缺列而实库多列 = Err——合成空列集会炸
|
|
44
|
-
writeFileSync(join(apiDist, "manifest.yaml"), "name: demo-solution\ndesc: manifest.yaml 契约全字段合法夹具\nversion: 0.1.0\ntables:\n -
|
|
44
|
+
writeFileSync(join(apiDist, "manifest.yaml"), "name: demo-solution\ndesc: manifest.yaml 契约全字段合法夹具\nversion: 0.1.0\ntables:\n - demo_solution_note\n");
|
|
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");
|