@oj-module/cli 0.2.25 → 0.2.27

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/README.md CHANGED
@@ -23,81 +23,107 @@
23
23
  ## 命令
24
24
 
25
25
  ```bash
26
- ojm dev [port] # 启动开发服务器(默认 5174):宿主代理 + 本地模块重建
27
- ojm build # 构建模块产物与 dist/modules.json
28
- ojm init [dir] [--yes] # 脚手架:模板工程 + 证书签发 + 联网下载 oj 到 bin/
29
- ojm vendor [tag] [--force] # 下载/重装 oj vendor(经 npm 包 @oj-bin/oj,tag 缺省取最新版本,形如 v0.1.13)
30
- ojm preview [port] # 生产形态预览:oj migrate → oj serve 单服务(API + 静态 SPA 同进程)
31
- ojm info # 输出报障所需的版本矩阵与后端观测
32
- ojm api [dir] # 契约产物生成(client/openapi/mock stub)
33
- ojm merge <out> <in...> # 合并多份 modules.json
26
+ # 工程脚手架
27
+ ojm init [dir] [--yes] # 前后端一体化脚手架(幂等补缺:重跑只补缺失,永不覆盖已有文件)
28
+ ojm new <name> [--layout container|none] [--api] # 脚手架内生成用户方案骨架(src/<name>/ + solution.yaml)
29
+
30
+ # 开发与构建
31
+ ojm dev [port] # 开发服务器:/api 反代 oj + 模块重建 + SSE 自动刷新(快捷键 r/h/c/o/q)
32
+ ojm build [--bundle [name]] [--out <dir>] # 构建后端(oj build)与模块产物;--bundle 打确定性方案包
33
+ ojm preview [port] # 生产形态预览:migrate → oj serve 单服务(API + 静态 SPA 同进程)
34
+ ojm api [dir] [--check] [--docs] [--exempt <p>] # 契约代码生成(client/routes/openapi/stub);--check 四重对账
35
+
36
+ # 清单与诊断
37
+ ojm info [--all] # 版本矩阵与模块清单(报障用)
38
+ ojm merge <out.json> <in1.json> [in2.json ...] # 合并多团队清单(清单先后 = 接管优先级)
39
+ ojm vendor [tag] [--force] # 下载/重装 oj vendor(经 npm 包 @oj-bin/oj,tag 形如 v0.1.13)
40
+
41
+ # 方案市场(站点侧生命周期)
42
+ ojm install <file.tgz | name[@version]> # 安装方案包到目标站点(file 上传 / CDN 市场源)
43
+ ojm enable|disable <name> # 启用/禁用已装方案(锁/sidecar/路由清单/模块清单四处同写)
44
+ ojm uninstall <name> # 卸载已装方案(四处同删 + 权限级联清理)
45
+ ojm trust ls|add|revoke|confirm # 发布方信任库管理(TOFU 确认)
46
+
47
+ # 发布与签名(作者侧)
48
+ ojm keygen --id <publisher.id> # 生成 Ed25519 签名密钥对(私钥缺省 ~/.ojm/keys/<id>/)
49
+ ojm sign <file.tgz> --id <id> [--check] # detached 侧车签名(产出 <tgz>.sig)
50
+ ojm publish <dir> --id <id> [--check] # 索引发布(重算 bundles/manifests + 侧车签名)
51
+
52
+ # 本地市场模拟 CDN
53
+ ojm market setup|publish|build-index|verify|trial|walkthrough
54
+
55
+ # oj 子命令透传
56
+ ojm test|exec|openapi|migrate|schema [args...] # 透传给 oj 同名子命令(自动补 -c <project>/config.yaml)
34
57
  ```
35
58
 
36
- > `ojm init` / `ojm vendor` 需要访问 npm registry(经 `npm i @oj-bin/oj` 安装 oj 到 `bin/`,走 `.npmrc` 镜像配置)。已装版本记录在 `bin/.oj-version`,`ojm info` 可查。
59
+ > 每个命令都有中英双语帮助:`ojm <cmd> --help` 看单命令详解,`ojm help` 看全量索引
60
+ > (语言按 `OJM_LANG` / `LANG` 探测,缺省 zh-CN)。`ojm --version` 显示 cli 包版本。
61
+ > 方案市场的完整链路(打包 → 签名 → 索引 → 安装 → 生命周期)见 devkit 手册 §10–§12。
37
62
 
38
63
  ### `ojm build`
39
64
 
40
- 读取 `web.config.ts`,逐个模块构建(硬切换后只认 web/src + web/dist 布局):
65
+ 构建后端与前端产物(module-root 布局为默认,模块由扫描 `src/*/web/entry.ts` 收录,
66
+ `web.config.ts` 仅作可选覆盖;历史布局仍以 `web.config.ts` 的 `modules` 为真源):
41
67
 
42
68
  ```
43
69
  web/dist/
44
- ├── modules.json # 模块清单(供宿主 fetch)
70
+ ├── index.html # 全站合并后的宿主页(仅 build 做清场合并)
71
+ ├── modules.json # 模块清单(供宿主 fetch)
45
72
  └── modules/<name>/<version>/
46
- ├── entry.js # 模块入口
47
- ├── <chunk>.js # 保留 code splitting
73
+ ├── entry.js # 模块入口
74
+ ├── <chunk>.js # 保留 code splitting
48
75
  └── *.css
49
76
  ```
50
77
 
51
78
  - 只有共享表内的依赖被 external,其余依赖会被打进模块产物(不在表内时 CLI 会告警);
52
79
  - 每个 chunk 计算 `sha384` 完整性摘要写入 `modules.json`,懒加载 chunk 标记 `lazy: true`;
53
80
  - 模块元信息(`name` / `version` / `peerRuntime`)通过 esbuild 打包后真实 `import()` entry 读取,而非正则解析——因此配置文件里可以写注释、用变量、做条件判断;
54
- - 读取元信息时 `@oj-module/runtime` 被替换为只读占位模块,避免在 Node 下加载含 Vite 专有 svg 导入的框架运行时。
81
+ - 读取元信息时 `@oj-module/runtime` 被替换为只读占位模块,避免在 Node 下加载含 Vite 专有 svg 导入的框架运行时;
82
+ - `--bundle [name]`:只组装不构建,先 `ojm build` 再打包——两段式契约校验(fail-fast)后产出确定性
83
+ `dist/<name>-<version>.tgz` + sha256(市场安装器的上游产物);无参 = 全部用户方案。
55
84
 
56
85
  ### `ojm dev`
57
86
 
58
87
  1. 直接取本包内置的预构建宿主 `shell-dist`(随 cli 发布,**不再**查询 `node_modules/@oj-module/shell`,也无 monorepo 路径回退);
59
88
  2. 先跑一次 `ojm build`;
60
- 3. 起静态服务器:
89
+ 3. 起开发服务器:
61
90
 
62
91
  | 路由 | 来源 |
63
92
  |---|---|
93
+ | `/api/*` | 反代到 oj 后端(oj 端口以 `config.yaml` 的 `server.port` 为准,默认 9778) |
64
94
  | `/`、`/index.html` | `@oj-module/cli/shell-dist/index.html`(含 importmap) |
65
95
  | `/assets/*` | `@oj-module/cli/shell-dist/assets/*` |
66
96
  | `/modules.json` | 本地 `web/dist/modules.json` |
67
97
  | `/modules/*` | 本地 `web/dist/modules/*` |
68
98
 
69
- 4. 监听 `web/src/` 变更并增量重建(防抖 300ms)。
70
-
71
- > 当前为「保存即重建 + 手动刷新」。完整 HMR(react-refresh preamble、dev-runtime 映射)在后续阶段接入。
99
+ 4. 监听 `web/src/`(module-root 为 `src/*/web/`)变更并增量重建(防抖 300ms),经 SSE 通道**自动刷新浏览器**;
100
+ 5. TTY 终端启用类 Vite 快捷键:`r` 强制重建热更新 · `h` 帮助 · `c` 清屏 · `o` 浏览器打开 · `q` 退出(后端 oj 一并终止,不留孤儿占端口)。
72
101
 
73
102
  ## 配置文件
74
103
 
75
- 工程根目录的 `web.config.ts`:
104
+ 工程根目录的 `web.config.ts`(module-root 布局下可选,仅 `baseUrl` / `siteBase` /
105
+ `htmlTransform` 等站点级配置才需要它;`modules` 登记只对历史布局必填):
76
106
 
77
107
  ```ts
78
108
  export default {
79
109
  /** 产物 URL 前缀,留空表示同源相对路径;跨源时填 CDN 绝对地址 */
80
110
  baseUrl: "",
111
+ /** 站点部署子路径(如 "/oj"),留空 = 挂在域名根;与 baseUrl 正交可同用 */
112
+ siteBase: "",
113
+ /** 历史布局的模块登记;module-root 布局由扫描 src/*/web/entry.ts 得出 */
81
114
  modules: [
82
115
  { name: "demo", entry: "./web/src/demo/entry.ts", enabled: true },
83
116
  ],
117
+ /** 按路由改写 HTML 的模块(仅 ojm dev 生效,见下) */
118
+ htmlTransform: "./html-transform.ts",
84
119
  };
85
120
  ```
86
121
 
87
- `name` 必须与 `entry.ts` 中 `defineModule({ name })` 一致,不一致会直接构建失败。`enabled: false` 的模块会被跳过。
122
+ `name` 必须与 `entry.ts` 中 `defineModule({ name})` 一致,不一致会直接构建失败。`enabled: false` 的模块会被跳过。
88
123
 
89
124
  ### 按路由改写 HTML(`htmlTransform`,可选)
90
125
 
91
- 模块联邦站点每条路由都出同一份 `index.html`,逐路由的 `<title>` / OpenGraph 无从表达——IM 链接预览与非 JS 爬虫只会看到默认 title。配 `htmlTransform` 指向一个模块(相对工程根的路径,或已安装的包名),`ojm dev` 与 `ojm preview` 会加载它,对 `/`、`/index.html` 与 SPA 回落的深链接统一生效:
92
-
93
- ```ts
94
- // web.config.ts
95
- export default {
96
- baseUrl: "",
97
- modules: [/* … */],
98
- htmlTransform: "./html-transform.ts",
99
- };
100
- ```
126
+ 模块联邦站点每条路由都出同一份 `index.html`,逐路由的 `<title>` / OpenGraph 无从表达——IM 链接预览与非 JS 爬虫只会看到默认 title。配 `htmlTransform` 指向一个模块(相对工程根的路径,或已安装的包名),`ojm dev` 会加载它,对 `/`、`/index.html` 与 SPA 回落的深链接统一生效:
101
127
 
102
128
  ```ts
103
129
  // html-transform.ts —— default 导出即变换函数
@@ -109,10 +135,11 @@ export default async function (path: string, html: string) {
109
135
  ```
110
136
 
111
137
  - `path` 是解码后的请求路径(查询串已剥离),`html` 是宿主原文;
112
- - 先变换、后注入 dev 刷新脚本,故 `ojm dev` 的热刷新不受影响;
138
+ - 先变换、后注入 dev 刷新脚本,故热刷新不受影响;
113
139
  - 变换抛错或返回非字符串 → 回退原文,站点不 500;
114
140
  - 模块缺失或 default 不是函数 → 启动即人话报错(fail-fast);
115
- - 非 HTML 资产(`/assets/*`、`/modules/*`)不受影响。
141
+ - 非 HTML 资产(`/assets/*`、`/modules/*`)不受影响;
142
+ - **仅 `ojm dev` 生效**:单服务 `ojm preview` 由 oj 直出 HTML,钩子无从介入(启动 WARN 并指向 oj 的 `server.html_meta` / `server.html_meta_handler`)。
116
143
 
117
144
  ## 子出口
118
145
 
@@ -122,6 +149,15 @@ import { generateImportmap, generateShellEntries, isSharedDep, SHARED_DEPS } fro
122
149
 
123
150
  // 配置加载
124
151
  import { loadModulesConfig, resolveModuleEntry } from "@oj-module/cli/config";
152
+
153
+ // 构建 / 诊断 / 校验(Node 侧工具链复用)
154
+ import { buildModules } from "@oj-module/cli/build";
155
+ import { printInfo, mergeManifests } from "@oj-module/cli/info";
156
+ import { collectExportGaps } from "@oj-module/cli/esm-exports";
157
+ import { mergeModuleManifests } from "@oj-module/cli/manifest";
158
+
159
+ // 预构建宿主资产(随包发布)
160
+ import "@oj-module/cli/shell-dist/index.html";
125
161
  ```
126
162
 
127
163
  **硬共享**(`react` / `react-router` / `@tanstack/react-query` / `@oj-module/runtime` …)破坏即崩溃,必须由宿主提供,模块不得自带;**软共享**(`antd` / `dayjs` / `i18next` …)允许版本漂移。
package/devkit/manual.md CHANGED
@@ -355,7 +355,7 @@ export default defineModule({
355
355
  | `order` | 菜单排序值(双段序轴):`≥0` 主菜单区升序;`<0` 底部固定区(pin 在侧边栏底部,区内仍按 order 升序);未声明按登记下标兜底;同 order 按根 path 字母序 |
356
356
  | `title` | 菜单标题,用 `"<模块名>:menu.<key>"` |
357
357
  | `icon` | `createElement(XxxOutlined)` |
358
- | `keepAlive` | 页面缓存(**必须挂在 `layout: "container"` 下才生效**) |
358
+ | `keepAlive` | 页面缓存(多 tab 开启时有效;与 layout 解绑;相对/绝对声明均可,key 取路由绝对路径) |
359
359
  | `hideInMenu` | 不在菜单显示 |
360
360
  | `roles` / `permissions` | 路由级权限(页面级 / 按钮级) |
361
361
  | `iframeLink` | 内嵌 iframe 的外链地址 |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oj-module/cli",
3
3
  "type": "module",
4
- "version": "0.2.25",
4
+ "version": "0.2.27",
5
5
  "description": "React Antd 模块工程命令行工具(含预构建宿主 shell-dist)",
6
6
  "license": "MIT",
7
7
  "exports": {
@@ -49,7 +49,7 @@
49
49
  ],
50
50
  "dependencies": {
51
51
  "@faker-js/faker": "^10.4.0",
52
- "@oj-module/runtime": "0.2.25",
52
+ "@oj-module/runtime": "0.2.27",
53
53
  "@redocly/cli": "^2.51.0",
54
54
  "@tailwindcss/vite": "^4.3.0",
55
55
  "cac": "^6.7.14",
@@ -304,10 +304,10 @@ function getAppInfo() {
304
304
  return {
305
305
  "pkg": {
306
306
  "name": "@oj-module/runtime",
307
- "version": "0.2.25",
307
+ "version": "0.2.27",
308
308
  "license": "MIT"
309
309
  },
310
- "lastBuildTime": "2026-10-23 15:39:15"
310
+ "lastBuildTime": "2026-10-24 00:06:50"
311
311
  };
312
312
  }
313
313
  var init_get_app_info = __esmMin((() => {}));
@@ -6961,8 +6961,7 @@ function LayoutTabbar() {
6961
6961
  location,
6962
6962
  currentRoute,
6963
6963
  setActiveKey,
6964
- addTab,
6965
- activeKey
6964
+ addTab
6966
6965
  ]);
6967
6966
  return /* @__PURE__ */ jsx("div", {
6968
6967
  className: classes.tabsContainer,
@@ -7273,24 +7272,6 @@ var init_scoped = __esmMin((() => {
7273
7272
  init_request();
7274
7273
  }));
7275
7274
  //#endregion
7276
- //#region src/router/utils/flatten-routes.ts
7277
- /**
7278
- * 将路由扁平化为一个对象,键为路由的 path,值为路由对象
7279
- */
7280
- function flattenRoutes(routes) {
7281
- const result = {};
7282
- function traverse(items, parent) {
7283
- items.forEach((item) => {
7284
- if (item.index && parent?.path) result[`${parent.path}/`] = item;
7285
- if (item.path) result[item.path] = item;
7286
- if (item.children && item.children.length > 0) traverse(item.children, item);
7287
- });
7288
- }
7289
- traverse(routes);
7290
- return result;
7291
- }
7292
- var init_flatten_routes = __esmMin((() => {}));
7293
- //#endregion
7294
7275
  //#region src/module-loader/keep-alive.ts
7295
7276
  /** 汇总一组模块定义里的全部路由(供扁平化与 exclude 计算) */
7296
7277
  function collectAllRoutes(definitions) {
@@ -7299,24 +7280,53 @@ function collectAllRoutes(definitions) {
7299
7280
  return routes;
7300
7281
  }
7301
7282
  /**
7283
+ * 绝对路径求解:绝对 path 原样;相对 path 拼父层;index(无 path)即父层本身。
7284
+ * 规则同 addRouteIdByPath / generateMenuItemsFromRoutes / rewriteAdminPaths 的
7285
+ * resolveAbs——解决方案模块一律相对路径声明(安装时方可加 prefix 于顶层),
7286
+ * 运行时绝对路径由此规则涌现,exclude key 必须落进同一空间才能与
7287
+ * activeCacheKey(= pathname)匹配。
7288
+ */
7289
+ function resolveAbsPath(path, parentAbs) {
7290
+ if (!path) return parentAbs;
7291
+ return path.startsWith("/") ? path : `${parentAbs.replace(/\/+$/, "")}/${path}`;
7292
+ }
7293
+ /**
7302
7294
  * 纯函数:从一组路由中收集 `handle.keepAlive === false` 的路由 key。
7303
- * key 与 `flattenRoutes` 产出的键一致,从而能和 KeepAlive 的 `activeCacheKey`
7304
- * (= pathname)精确匹配。
7295
+ * key = 路由绝对路径,与 activeCacheKey(= pathname)匹配。
7305
7296
  *
7306
7297
  * 这是 P2.1 把 exclude 计算从 access store 的 `flatRouteList` 反转到
7307
7298
  * module-loader 的核心:缓存是否排除只取决于「模块声明」,不再依赖某条路由
7308
7299
  * 是否套了 ContainerLayout(见设计文档 B13 / R9)。
7309
7300
  */
7310
7301
  function collectKeepAliveExcludes(routes) {
7311
- const flat = flattenRoutes(routes);
7312
- return Object.entries(flat).reduce((acc, [key, value]) => {
7313
- if (value.handle?.keepAlive === false) acc.push(key);
7314
- return acc;
7315
- }, []);
7302
+ const out = [];
7303
+ const walk = (items, parentAbs) => {
7304
+ for (const item of items) {
7305
+ const abs = resolveAbsPath(item.path, parentAbs);
7306
+ const key = item.index ? parentAbs : abs;
7307
+ if (item.handle?.keepAlive === false) out.push(key);
7308
+ if (item.children?.length) walk(item.children, abs);
7309
+ }
7310
+ };
7311
+ walk(routes, "");
7312
+ return out;
7316
7313
  }
7317
- /** 纯函数:收集所有路由 key(关闭多 tab 时用于整体排除,仅保留切换动画) */
7314
+ /**
7315
+ * 纯函数:收集所有路由 key(关闭多 tab 时用于整体排除,仅保留切换动画)。
7316
+ * 与 collectKeepAliveExcludes 同一套绝对 key 口径(tab 关闭分支的 exclude
7317
+ * 自此真正有意义),按出现序去重。
7318
+ */
7318
7319
  function collectAllRoutePaths(routes) {
7319
- return Object.keys(flattenRoutes(routes));
7320
+ const seen = /* @__PURE__ */ new Set();
7321
+ const walk = (items, parentAbs) => {
7322
+ for (const item of items) {
7323
+ const abs = resolveAbsPath(item.path, parentAbs);
7324
+ seen.add(item.index ? parentAbs : abs);
7325
+ if (item.children?.length) walk(item.children, abs);
7326
+ }
7327
+ };
7328
+ walk(routes, "");
7329
+ return [...seen];
7320
7330
  }
7321
7331
  /** 基于一组模块定义计算 exclude key(供单元测试) */
7322
7332
  function getKeepAliveExcludes(definitions) {
@@ -7326,9 +7336,7 @@ function getKeepAliveExcludes(definitions) {
7326
7336
  function getAllRoutePaths(definitions) {
7327
7337
  return collectAllRoutePaths(collectAllRoutes(definitions));
7328
7338
  }
7329
- var init_keep_alive = __esmMin((() => {
7330
- init_flatten_routes();
7331
- }));
7339
+ var init_keep_alive = __esmMin((() => {}));
7332
7340
  //#endregion
7333
7341
  //#region src/module-loader/semver.ts
7334
7342
  function parse$2(version) {
@@ -8437,6 +8445,24 @@ var init_router = __esmMin((() => {
8437
8445
  router = createRouter();
8438
8446
  }));
8439
8447
  //#endregion
8448
+ //#region src/router/utils/flatten-routes.ts
8449
+ /**
8450
+ * 将路由扁平化为一个对象,键为路由的 path,值为路由对象
8451
+ */
8452
+ function flattenRoutes(routes) {
8453
+ const result = {};
8454
+ function traverse(items, parent) {
8455
+ items.forEach((item) => {
8456
+ if (item.index && parent?.path) result[`${parent.path}/`] = item;
8457
+ if (item.path) result[item.path] = item;
8458
+ if (item.children && item.children.length > 0) traverse(item.children, item);
8459
+ });
8460
+ }
8461
+ traverse(routes);
8462
+ return result;
8463
+ }
8464
+ var init_flatten_routes = __esmMin((() => {}));
8465
+ //#endregion
8440
8466
  //#region src/router/utils/generate-menu-items-from-routes.ts
8441
8467
  /**
8442
8468
  * 解析菜单 key 为绝对路径。
@@ -5,9 +5,9 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" href="/favicon.ico" />
7
7
  <title>React Antd Admin · Shell</title>
8
- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' https://modules.cdn.example.com 'wasm-unsafe-eval' 'nonce-rkh8kQ0W+uFw3wTdIkSSUA=='; script-src-attr 'none'; connect-src 'self' data: https://modules.cdn.example.com; style-src 'self' https://modules.cdn.example.com 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-src https://ant.design https://react.dev https://condorheroblog.github.io; form-action 'none'; base-uri 'none'; object-src 'none'; upgrade-insecure-requests" />
8
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' https://modules.cdn.example.com 'wasm-unsafe-eval' 'nonce-LoiG1HDggaNmP/FtNKL2EQ=='; script-src-attr 'none'; connect-src 'self' data: https://modules.cdn.example.com; style-src 'self' https://modules.cdn.example.com 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-src https://ant.design https://react.dev https://condorheroblog.github.io; form-action 'none'; base-uri 'none'; object-src 'none'; upgrade-insecure-requests" />
9
9
 
10
- <script type="importmap" nonce="rkh8kQ0W+uFw3wTdIkSSUA==">{
10
+ <script type="importmap" nonce="LoiG1HDggaNmP/FtNKL2EQ==">{
11
11
  "imports": {
12
12
  "react": "/assets/react.js",
13
13
  "react/jsx-runtime": "/assets/jsx-runtime.js",
@@ -7,9 +7,9 @@
7
7
  "react-router": "7.18.2",
8
8
  "react-router/dom": "7.18.2",
9
9
  "@tanstack/react-query": "5.102.8",
10
- "@oj-module/runtime": "0.2.25",
11
- "@oj-module/runtime/contract": "0.2.25",
12
- "@oj-module/runtime/contract/errors": "0.2.25",
10
+ "@oj-module/runtime": "0.2.27",
11
+ "@oj-module/runtime/contract": "0.2.27",
12
+ "@oj-module/runtime/contract/errors": "0.2.27",
13
13
  "@plane/kit": "0.1.20",
14
14
  "@plane/editor": "0.1.20",
15
15
  "antd": "6.6.1",
@@ -216,7 +216,14 @@ function reconcileRoutes(contractIr: IrEndpoint[], handlers: HandlerRow[], exemp
216
216
  const contractAbs = `${wirePrefixOf(ep)}${ep.route}`;
217
217
 
218
218
  // ① 精确全路径:嵌套组 / 绝对精确命中
219
- const exact = byPath.get(pathKey(ep.method, ojFull)) ?? byPath.get(pathKey(ep.method, contractAbs));
219
+ // 模块根动词(契约 route "/",如 GET /api/books 的聚合根形态):ojFull 带尾斜杠
220
+ // ("books/"),handler 全路径是模块名("books")——去尾斜杠再查一次,否则
221
+ // 会掉进②模糊兜底被误报 route-params-mismatch(0.2.25 演练实踩)
222
+ const ojFullBare = ojFull.replace(/\/+$/, "");
223
+ let exact = byPath.get(pathKey(ep.method, ojFull));
224
+ if (!exact && ojFullBare !== ojFull)
225
+ exact = byPath.get(pathKey(ep.method, ojFullBare));
226
+ exact ??= byPath.get(pathKey(ep.method, contractAbs));
220
227
  if (exact) {
221
228
  claimed.add(exact);
222
229
  continue;
package/src/vendor.ts CHANGED
@@ -39,8 +39,12 @@ export const NPM_PKG = "@oj-bin/oj";
39
39
  * 与 `-d`/`--api-path` 互斥);旧版收到该旗标即报错。0.1.85 同时落地了
40
40
  * `manifest.name` 与目录名脱钩、`#/m/x` 按 name 解析(见
41
41
  * docs/prd/202610220010-layout-module-root-design.md §5)。
42
+ * **0.2.26 抬到 0.1.86**:openapi 的 dev 分支裸包解析截断(module-root 下预生成
43
+ * `__client` 内省 fail-fast,`oj openapi --module-root` 契约链整体不可用)修复于
44
+ * 0.1.86(oj-bin 0f8a6e2,第三次基线发布通道实证);module-root 工程是 0.2.20 起
45
+ * 的默认布局,门禁必须盖住契约链(见 playbook §9 与 2026-10-23 第三次基线)。
42
46
  */
43
- export const MIN_OJ_VERSION = "0.1.85";
47
+ export const MIN_OJ_VERSION = "0.1.86";
44
48
 
45
49
  /** 按段数值比较(字符串比较会把 0.1.9 判成大于 0.1.69);非数字段按 0 处理 */
46
50
  export function isSupportedOjVersion(version: string): boolean {
@@ -46,7 +46,43 @@ include:
46
46
  - seed.mysql.sql
47
47
  - seed.pg.sql
48
48
  - vitest.config.ts
49
- - tests
49
+ # —— tests 逐条白名单(不收 tests/ 整目录)——
50
+ # 业务工程只拷「纯 node:sqlite 夹具、零 cli 包环境假设」的测试。以下 6 个不分发
51
+ # (oj-wiki 0.2.25 升级实踩:拷进业务工程即死文件,重跑 init 还会复活):
52
+ # lifecycle/preflight・installer・market-api・steps-fs・sig-fixtures —— 硬编码
53
+ # oj-bin 仓夹具路径(../../src/lifecycle/__tests__/fixtures,模板树内不存在);
54
+ # lifecycle/bootstrap —— 私钥红线钉测硬编码「packages/cli 全树」扫描,业务工程
55
+ # 扫 node_modules 必超时。它们只活在 oj-module/oj-bin 的 cli 包测试语境。
56
+ - tests/fixture.ts
57
+ - tests/auth-ldap.spec.ts
58
+ - tests/auth-session.spec.ts
59
+ - tests/auth-throttle.spec.ts
60
+ - tests/guard.spec.ts
61
+ - tests/l1-builder.test.ts
62
+ - tests/query-utils.spec.ts
63
+ - tests/rbac-audit.spec.ts
64
+ - tests/rbac-contacts.spec.ts
65
+ - tests/rbac-guard.spec.ts
66
+ - tests/rbac-roles.spec.ts
67
+ - tests/rbac-seed.spec.ts
68
+ - tests/rbac-settings.spec.ts
69
+ - tests/rbac-solution-perms.spec.ts
70
+ - tests/rbac-tables.spec.ts
71
+ - tests/rbac-users.spec.ts
72
+ - tests/seed-role-accounts.spec.ts
73
+ - tests/lifecycle/atomic-state.spec.ts
74
+ - tests/lifecycle/deploy-smoke.spec.ts
75
+ - tests/lifecycle/migrate.spec.ts
76
+ - tests/lifecycle/register.spec.ts
77
+ - tests/lifecycle/semver.spec.ts
78
+ - tests/lifecycle/sha384.spec.ts
79
+ - tests/lifecycle/solutions-api.spec.ts
80
+ - tests/lifecycle/text-edit.spec.ts
81
+ - tests/lifecycle/tgz-guard.spec.ts
82
+ - tests/lifecycle/trust.spec.ts
83
+ - tests/lifecycle/uninstall.spec.ts
84
+ - tests/lifecycle/verify-checks.spec.ts
85
+ - tests/lifecycle/verify-report.spec.ts
50
86
  - src/_platform/api
51
87
  - src/auth/api
52
88
  - src/notification/api
@@ -8,9 +8,9 @@
8
8
  "react-router": "7.18.2",
9
9
  "react-router/dom": "7.18.2",
10
10
  "@tanstack/react-query": "5.102.8",
11
- "@oj-module/runtime": "0.2.25",
12
- "@oj-module/runtime/contract": "0.2.25",
13
- "@oj-module/runtime/contract/errors": "0.2.25",
11
+ "@oj-module/runtime": "0.2.27",
12
+ "@oj-module/runtime/contract": "0.2.27",
13
+ "@oj-module/runtime/contract/errors": "0.2.27",
14
14
  "@plane/kit": "0.1.20",
15
15
  "@plane/editor": "0.1.20",
16
16
  "antd": "6.6.1",