@oj-module/cli 0.2.26 → 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.26",
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.26",
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.26",
307
+ "version": "0.2.27",
308
308
  "license": "MIT"
309
309
  },
310
- "lastBuildTime": "2026-10-23 21:49:09"
310
+ "lastBuildTime": "2026-10-24 00:06:50"
311
311
  };
312
312
  }
313
313
  var init_get_app_info = __esmMin((() => {}));
@@ -7272,24 +7272,6 @@ var init_scoped = __esmMin((() => {
7272
7272
  init_request();
7273
7273
  }));
7274
7274
  //#endregion
7275
- //#region src/router/utils/flatten-routes.ts
7276
- /**
7277
- * 将路由扁平化为一个对象,键为路由的 path,值为路由对象
7278
- */
7279
- function flattenRoutes(routes) {
7280
- const result = {};
7281
- function traverse(items, parent) {
7282
- items.forEach((item) => {
7283
- if (item.index && parent?.path) result[`${parent.path}/`] = item;
7284
- if (item.path) result[item.path] = item;
7285
- if (item.children && item.children.length > 0) traverse(item.children, item);
7286
- });
7287
- }
7288
- traverse(routes);
7289
- return result;
7290
- }
7291
- var init_flatten_routes = __esmMin((() => {}));
7292
- //#endregion
7293
7275
  //#region src/module-loader/keep-alive.ts
7294
7276
  /** 汇总一组模块定义里的全部路由(供扁平化与 exclude 计算) */
7295
7277
  function collectAllRoutes(definitions) {
@@ -7298,24 +7280,53 @@ function collectAllRoutes(definitions) {
7298
7280
  return routes;
7299
7281
  }
7300
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
+ /**
7301
7294
  * 纯函数:从一组路由中收集 `handle.keepAlive === false` 的路由 key。
7302
- * key 与 `flattenRoutes` 产出的键一致,从而能和 KeepAlive 的 `activeCacheKey`
7303
- * (= pathname)精确匹配。
7295
+ * key = 路由绝对路径,与 activeCacheKey(= pathname)匹配。
7304
7296
  *
7305
7297
  * 这是 P2.1 把 exclude 计算从 access store 的 `flatRouteList` 反转到
7306
7298
  * module-loader 的核心:缓存是否排除只取决于「模块声明」,不再依赖某条路由
7307
7299
  * 是否套了 ContainerLayout(见设计文档 B13 / R9)。
7308
7300
  */
7309
7301
  function collectKeepAliveExcludes(routes) {
7310
- const flat = flattenRoutes(routes);
7311
- return Object.entries(flat).reduce((acc, [key, value]) => {
7312
- if (value.handle?.keepAlive === false) acc.push(key);
7313
- return acc;
7314
- }, []);
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;
7315
7313
  }
7316
- /** 纯函数:收集所有路由 key(关闭多 tab 时用于整体排除,仅保留切换动画) */
7314
+ /**
7315
+ * 纯函数:收集所有路由 key(关闭多 tab 时用于整体排除,仅保留切换动画)。
7316
+ * 与 collectKeepAliveExcludes 同一套绝对 key 口径(tab 关闭分支的 exclude
7317
+ * 自此真正有意义),按出现序去重。
7318
+ */
7317
7319
  function collectAllRoutePaths(routes) {
7318
- 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];
7319
7330
  }
7320
7331
  /** 基于一组模块定义计算 exclude key(供单元测试) */
7321
7332
  function getKeepAliveExcludes(definitions) {
@@ -7325,9 +7336,7 @@ function getKeepAliveExcludes(definitions) {
7325
7336
  function getAllRoutePaths(definitions) {
7326
7337
  return collectAllRoutePaths(collectAllRoutes(definitions));
7327
7338
  }
7328
- var init_keep_alive = __esmMin((() => {
7329
- init_flatten_routes();
7330
- }));
7339
+ var init_keep_alive = __esmMin((() => {}));
7331
7340
  //#endregion
7332
7341
  //#region src/module-loader/semver.ts
7333
7342
  function parse$2(version) {
@@ -8436,6 +8445,24 @@ var init_router = __esmMin((() => {
8436
8445
  router = createRouter();
8437
8446
  }));
8438
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
8439
8466
  //#region src/router/utils/generate-menu-items-from-routes.ts
8440
8467
  /**
8441
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-yoOEdSzDoQ4YJ5XCof4eFQ=='; 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="yoOEdSzDoQ4YJ5XCof4eFQ==">{
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.26",
11
- "@oj-module/runtime/contract": "0.2.26",
12
- "@oj-module/runtime/contract/errors": "0.2.26",
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",
@@ -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.26",
12
- "@oj-module/runtime/contract": "0.2.26",
13
- "@oj-module/runtime/contract/errors": "0.2.26",
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",