@oj-module/cli 0.2.20 → 0.2.23
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 +93 -7
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +59 -34
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/init-packagejson.test.ts +29 -0
- package/src/args.ts +54 -4
- package/src/builtin.ts +93 -0
- package/src/bundle.ts +230 -26
- package/src/contract/__tests__/market-client.integration.test.ts +12 -12
- package/src/dev.ts +58 -2
- package/src/index.ts +26 -6
- package/src/info.ts +49 -7
- package/src/init.ts +23 -7
- package/src/lifecycle/__tests__/args.test.ts +20 -0
- package/src/lifecycle/__tests__/builtin.test.ts +97 -0
- package/src/lifecycle/__tests__/bundle.test.ts +168 -5
- package/src/lifecycle/__tests__/new.test.ts +129 -0
- package/src/lifecycle/__tests__/smoke.test.ts +2 -1
- package/src/manifest-contract.ts +16 -3
- package/src/market/publish.ts +10 -2
- package/src/new.ts +208 -0
- package/src/usage.ts +18 -6
- package/templates/package.json +3 -1
- package/templates/src/_platform/api/fixtures/l1-test-user.sql +4 -5
- package/templates/src/_platform/api/seed.mysql.sql +11 -3
- package/templates/src/_platform/api/seed.pg.sql +11 -3
- package/templates/src/_platform/api/seed.sql +16 -5
- package/templates/src/oj-market/web/api/context.ts +2 -2
- package/templates/src/oj-market/web/components/InstallDialog.tsx +13 -6
- package/templates/src/oj-market/web/components/RollbackDialog.tsx +7 -3
- package/templates/src/oj-market/web/components/UninstallConfirm.tsx +18 -7
- package/templates/src/oj-market/web/hooks/useMarket.ts +30 -18
- package/templates/src/system/web/locales/en-US.json +1 -1
- package/templates/src/system/web/locales/zh-CN.json +1 -1
- package/templates/tests/l1-builder.test.ts +2 -2
- package/templates/tests/rbac-seed.spec.ts +12 -10
- package/templates/tests/seed-role-accounts.spec.ts +87 -0
- package/vendor/host-versions.json +3 -3
package/devkit/manual.md
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
├── tests/ # 后端 BDD 测试(node:sqlite 内存库 + 真实迁移建表)
|
|
40
40
|
├── src/
|
|
41
41
|
│ ├── tsconfig.json # 后端 typecheck 工程(extends 上两者,exclude */web)
|
|
42
|
+
│ ├── .ojm-builtin.json # 框架自带方案名单(init 派生,可手改/删除;打包排除 + info 折叠,见 §11)
|
|
42
43
|
│ ├── styles.css
|
|
43
44
|
│ ├── _platform/ # 框架级:users 表 / rbac / 市场编排 / 生命周期
|
|
44
45
|
│ │ ├── api/ # + _shared/(respond、service/、site-facade)
|
|
@@ -74,7 +75,19 @@
|
|
|
74
75
|
> **历史布局仍被识别**:`web/src/<name>`(uni-dev)与 `web/<name>`(框架根仓)照旧可用,
|
|
75
76
|
> 只是 `ojm init` 不再产出。两套源码树**互斥**——同时存在时 `ojm` 直接报错,而不是静默选一个。
|
|
76
77
|
|
|
77
|
-
|
|
78
|
+
登录账号:**账号名=内置角色名**一一对应,口令均 `123456`,且全部**免首登改密**(must_change=0,纯开发便利,勿用于生产):
|
|
79
|
+
|
|
80
|
+
| 账号 | 角色 | 能干什么 |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `superadmin` | superadmin | 全部能力(含安全组设置、成员资格管理) |
|
|
83
|
+
| `admin` | admin | 日常管理(不含 superadmin 专属项) |
|
|
84
|
+
| `editor` | editor | 页面门编辑级 |
|
|
85
|
+
| `viewer` | viewer | 页面门访客级 |
|
|
86
|
+
|
|
87
|
+
> 旧脚手架库(admin 绑 superadmin / must_change=1)**无需手工迁移**:seed 每次启动幂等重放,
|
|
88
|
+
> 自动收敛为账号名=角色名并清零 must_change(`DELETE` 旧绑定 + `UPDATE`,见 `_platform/api/seed.sql`)。
|
|
89
|
+
|
|
90
|
+
端口:前端 devServer **5174**,oj 后端 **9778**
|
|
78
91
|
(以工程根 `config.yaml` 的 `server.port` 为准)。
|
|
79
92
|
|
|
80
93
|
### 1.2 两个模块体系,同名不同物
|
|
@@ -93,6 +106,33 @@
|
|
|
93
106
|
历史布局下两侧分居 `web/src/<模块>/` 与 `api/src/<模块>/`——**同一个名字、两个目录**,
|
|
94
107
|
是布局迁移时最容易对不上的地方。
|
|
95
108
|
|
|
109
|
+
#### manifest.yaml 与 solution.yaml:两种身份,别混淆
|
|
110
|
+
|
|
111
|
+
工程里会出现两个名字里带 "manifest" 的文件,用途**完全不同**,混用会出构建/运行期怪问题:
|
|
112
|
+
|
|
113
|
+
| 文件 | 层级 | 身份 | 谁读它 | 写什么 |
|
|
114
|
+
|---|---|---|---|---|
|
|
115
|
+
| `src/<模块>/api/manifest.yaml` | **模块级**(每个后端模块一份) | oj **原生七键**清单(`name`/`desc`/`version`/`tables`/…) | **oj 运行时**——按 `manifest.yaml` 枚举发现并校验 api 后端模块 | 模块名、版本、表清单(与 `schema.yaml` 双向一致,§1.5) |
|
|
116
|
+
| `solution.yaml`(`src/<模块>/` 或工程根) | **方案级**(整个方案一份) | 方案包契约(身份 / 路由 / api 段 / web 段 / 权限 / …) | **`ojm build --bundle`**——打包入口与契约校验 | 方案名、版本、`api` 段(模块+前缀+tables+deps+anonymous)、`web` 段(entry/css/integrity)、`routes`、`health` |
|
|
117
|
+
|
|
118
|
+
关键区别与红线:
|
|
119
|
+
|
|
120
|
+
- **模块级 `manifest.yaml` 是 oj 发现 api 后端的唯一真源**:`ojm dev/build` 递归扫描
|
|
121
|
+
`src/<模块>/api/manifest.yaml` 认领后端模块;oj 启动也用它做强校验(`name ==` 目录名、版本同源)。
|
|
122
|
+
它**只能有 oj 原生七键**,写任何方案级字段(`routes` / `permissions` / …)都会被拒启。
|
|
123
|
+
- **方案级契约以前也叫 `manifest.yaml`(工程根)**,但它会和上面的「oj 按 `manifest.yaml` 枚举 api 模块」
|
|
124
|
+
撞名:**工程根再放一份 `manifest.yaml` 当 bundle 契约,会被 oj 的搜索枚举误当成 api 模块**,污染后端发现。
|
|
125
|
+
所以解决方案统一改名为 **`solution.yaml`**,`ojm build --bundle` 优先认它(`manifest.yaml` 仅作旧方案回退)——
|
|
126
|
+
**后端按 `manifest.yaml` 枚举 api、打包按 `solution.yaml` 取契约**,两条枚举互不干扰。
|
|
127
|
+
**位置**:单模块方案推荐放 `src/<模块>/solution.yaml`(契约与所打包模块同位);`ojm build --bundle`
|
|
128
|
+
也兼容工程根 `solution.yaml`,以及两处的 `manifest.yaml` 回退。**模块级契约只认 `src/<模块>/` 根下、
|
|
129
|
+
绝不进 `<模块>/api/`**——否则照样会和 oj 的 api 枚举撞车。
|
|
130
|
+
- 进包后文件名固定回到 `manifest.yaml`(安装侧只认这个名字):`ojm build --bundle` 把工程根或
|
|
131
|
+
`src/<模块>/` 下的 `solution.yaml`(或旧 `manifest.yaml`)拷贝进 tgz 内的 `<name>-<version>/manifest.yaml`,安装器/市场仍按老名字消费。
|
|
132
|
+
|
|
133
|
+
> 一句话:**`manifest.yaml` 是给 oj 认模块用的(模块级、七键、oj 枚举);`solution.yaml` 是给 `ojm build --bundle`
|
|
134
|
+
> 打包用的(方案级、工程根)。两者文件名不同、层级不同、读者不同,别写错层。**
|
|
135
|
+
|
|
96
136
|
### 1.3 后端路由 = 目录镜像
|
|
97
137
|
|
|
98
138
|
```
|
|
@@ -1377,12 +1417,15 @@ useEffect(() => {
|
|
|
1377
1417
|
pnpm dev # 开发:devServer + oj 后端(src/<模块>/api 保存即热更)
|
|
1378
1418
|
# 交互快捷键(仅 TTY 终端启用,类 Vite):
|
|
1379
1419
|
# r 强制重建并热更新 h 显示帮助 c 清屏 o 浏览器打开 q 退出(oj 一并终止)
|
|
1420
|
+
# 提示行常驻终端最末行(滚动区),日志在其上方滚动不会被顶走(ojm ≥ 0.2.22)
|
|
1380
1421
|
# Ctrl+C(SIGINT)/ kill(SIGTERM)终止 dev 时,前端与后端 oj 一起终止:
|
|
1381
1422
|
# 先关 SSE 通道 → 停 oj 子进程 → 打印「后端 oj 已随前端一起终止」,不留 oj 孤儿占端口
|
|
1382
1423
|
pnpm build # = ojm build:oj build(生成 routes.js)+ 前端全站合并到 web/dist
|
|
1383
1424
|
pnpm preview # = ojm preview:oj migrate(verify 闸)→ oj serve 单服务(API + 静态 SPA 同进程)
|
|
1384
1425
|
pnpm typecheck # = tsc --noEmit(前端工程)
|
|
1385
1426
|
pnpm typecheck:api # = tsc --noEmit -p src/tsconfig.json(后端工程,见 §6.5)
|
|
1427
|
+
pnpm new # = ojm new <name>:脚手架内生成用户方案骨架(见 §10.1)
|
|
1428
|
+
pnpm bundle # = ojm build --bundle:打包全部用户方案(自带方案自动排除,见 §11)
|
|
1386
1429
|
pnpm exec ojm info # 版本矩阵 + 模块清单(报障用)
|
|
1387
1430
|
pnpm exec ojm vendor # 下载/重装 oj 二进制
|
|
1388
1431
|
```
|
|
@@ -1616,6 +1659,28 @@ export default async (path: string, html: string) => {
|
|
|
1616
1659
|
> release 下只有 `routes.js` 里的路由存在,目录镜像路由不生效——所以参数路由要先
|
|
1617
1660
|
> `ojm build` 再测(见 §7)。
|
|
1618
1661
|
|
|
1662
|
+
#### `ojm build --bundle [name]`
|
|
1663
|
+
|
|
1664
|
+
打包**方案包**(solution bundle,可分发产物;语义细节见 §11)。只组装不构建,先 `ojm build` 再打包。
|
|
1665
|
+
|
|
1666
|
+
- `--bundle` 无参:工程根有 `solution.yaml` 则只打根契约(旧单方案行为零变化);
|
|
1667
|
+
否则收集 `src/*/` 下全部**用户方案**契约逐个打包(框架自带方案自动排除,日志注明「框架自带,跳过」)。
|
|
1668
|
+
- `--bundle <name>` / `--bundle=<name>`:只打 `src/<name>/solution.yaml` 这一份(manifest.yaml 回退)。
|
|
1669
|
+
- 两段式:全部契约校验通过才开始写盘;零用户方案时报错指路 `ojm new`。
|
|
1670
|
+
|
|
1671
|
+
#### `ojm new <name> [--layout container|none] [--api]`
|
|
1672
|
+
|
|
1673
|
+
脚手架内生成一份用户方案骨架(module-root 布局):`src/<name>/solution.yaml` +
|
|
1674
|
+
`web/{entry.ts,pages/,locales/}`(`--api` 再加 `api/{manifest.yaml,api.ts}`)。
|
|
1675
|
+
生成即被扫描收录,补产物后 `ojm build --bundle <name>` 直接可打(round-trip)。
|
|
1676
|
+
非 module-root 布局拒绝(会先要求迁移);名字撞内置方案 / 保留名 / 已存在目录都会拒绝。
|
|
1677
|
+
```bash
|
|
1678
|
+
ojm new crm --layout container # 建 src/crm/ 骨架
|
|
1679
|
+
pnpm dev # 看效果
|
|
1680
|
+
ojm build --bundle crm # 打包
|
|
1681
|
+
```
|
|
1682
|
+
|
|
1683
|
+
|
|
1619
1684
|
#### `ojm preview [port]`
|
|
1620
1685
|
|
|
1621
1686
|
生产形态预览(设计 §6,2026-10-19 单服务化):
|
|
@@ -1647,9 +1712,11 @@ export default async (path: string, html: string) => {
|
|
|
1647
1712
|
|
|
1648
1713
|
生成物重跑即覆盖(eslint 已忽略);唯一例外是带 `// ojm-api:stub` 指纹的 stub,人碰过则工具永不写删。
|
|
1649
1714
|
|
|
1650
|
-
#### `ojm info`
|
|
1715
|
+
#### `ojm info [--all]`
|
|
1651
1716
|
|
|
1652
1717
|
输出版本矩阵(`@oj-module/cli` / `@oj-module/runtime` / 钉版 `@plane/*` / `oj` 二进制)与模块清单。
|
|
1718
|
+
module-root 下数据源是「扫描 + `web.config.ts` 登记」并集(未登记的用户模块也可见)。
|
|
1719
|
+
框架自带方案默认折叠为一行计数(`--all` 展开平铺)。
|
|
1653
1720
|
**报障用**:拿不准「工程里到底装了什么版本、哪些模块被登记」时,先 `ojm info` 贴出来。
|
|
1654
1721
|
|
|
1655
1722
|
#### `ojm vendor [tag] [--force]`
|
|
@@ -1722,25 +1789,44 @@ export default async (path: string, html: string) => {
|
|
|
1722
1789
|
## §11 方案包:打包与分发
|
|
1723
1790
|
|
|
1724
1791
|
「站点级构建」见 §7;本节讲**方案包**(solution bundle)——单方案的可分发产物与到达站点的方式。
|
|
1792
|
+
一个脚手架可放多份用户方案(`ojm new` 逐个生成,各自一份 `src/<name>/solution.yaml`),
|
|
1793
|
+
`ojm build --bundle` 无参时全部打包、逐个校验独立出包;框架自带方案自动排除(名单见
|
|
1794
|
+
`src/.ojm-builtin.json`,`ojm info` 默认折叠自带模块)。
|
|
1795
|
+
**从开发到发布的完整过程视角(含签名与私钥保存纪律)见**
|
|
1796
|
+
[`docs/guides/solution-dev-build-publish.md`](../guides/solution-dev-build-publish.md)。
|
|
1725
1797
|
|
|
1726
1798
|
### 11.1 打包:`ojm build --bundle`
|
|
1727
1799
|
|
|
1728
1800
|
```bash
|
|
1729
1801
|
ojm build --bundle # --out <dir> 缺省工程根 dist/(方案包输出目录,与后端产物 api/dist 无关)
|
|
1730
|
-
#
|
|
1802
|
+
ojm build --bundle crm # 只打 src/crm/ 这一份;--bundle=crm 等价
|
|
1803
|
+
# [ojm] 方案包已生成 → dist/<name>-<version>.tgz(多份用户方案时逐份输出,共打包 N 份)
|
|
1731
1804
|
# [ojm] sha256:…(确定性打包:同源同字节)
|
|
1805
|
+
# 没有可打包的用户方案时:报错指向 ojm new(不静默产出空批次)
|
|
1732
1806
|
```
|
|
1733
1807
|
|
|
1734
1808
|
> `--bundle` 只**组装**,不重跑构建:入包内容取自 `api/dist/<name>-<version>/` 与
|
|
1735
1809
|
> `web/dist/modules/<name>/`——先 `ojm build` 再打包。
|
|
1736
1810
|
|
|
1737
|
-
-
|
|
1738
|
-
|
|
1811
|
+
- **契约入口(按优先级)**:`src/<模块>/solution.yaml` ≻ 工程根 `solution.yaml` ≻
|
|
1812
|
+
`src/<模块>/manifest.yaml` ≻ 工程根 `manifest.yaml`(方案级;`manifest.yaml` 为旧方案回退)。
|
|
1813
|
+
**单模块方案推荐把契约放在 `src/<模块>/solution.yaml`**——契约与它所打包的模块同位,最直观。
|
|
1814
|
+
文件名刻意避开 `manifest.yaml`:oj 按 `manifest.yaml` 枚举发现 api 后端模块(§1.2),工程根若放一份
|
|
1815
|
+
bundle 契约会污染那条搜索枚举;故用 `solution.yaml` 隔开两条枚举——**后端按 `manifest.yaml` 枚举 api、
|
|
1816
|
+
打包按 `solution.yaml` 取契约**,不再冲突。模块级契约同样**只认 `src/<模块>/` 根下、绝不进 `<模块>/api/`**,
|
|
1817
|
+
否则会与 oj 的 api 枚举撞车。tgz 内固定写回 `manifest.yaml`(安装侧只认这个名字)。
|
|
1818
|
+
- **两段式**:先对全部目标契约跑完校验(纯读零写盘),全绿才开始逐个写 tgz——任一校验失败
|
|
1819
|
+
dist/ 零增量,报错指名失败方案(发布者不会把半截批次扫进 CDN 索引)。校验项(fail-fast,
|
|
1820
|
+
坏清单出不了包):方案 solution 契约(字段齐全 / 类型白名单 / 保留名 / 键空间 → 可读违规清单)
|
|
1821
|
+
→ 文件面(`api.tables` ↔ `schema.yaml`、locales 双语齐备)→ 模块级契约 `name` 必须 === 目录名。
|
|
1822
|
+
- 框架自带方案不参与打包:`src/.ojm-builtin.json` 名单内的模块契约被跳过(逐条日志);
|
|
1823
|
+
该文件由 `ojm init` 从模板白名单派生,**可手改/删除**(删名 = 恢复参与打包),重跑 init 缺失才补写。
|
|
1824
|
+
它只是打包排除 / info 折叠提示,**不是安全边界**:文件永不入包,安装侧信任链(签名 + 信任库 + TOFU)与它无关。
|
|
1739
1825
|
- **只出包不签名**——签名见 §12(关注点分离:作者签名是显式动作,不是构建副产品)。
|
|
1740
1826
|
- 产物内 `manifest.yaml` 的 `archive` 段是 **Form A**(仅 `file`);`sha256`/`size` 由
|
|
1741
1827
|
file 源上传时点补齐(宿主安装器)或发布通道 Form B 自带。
|
|
1742
|
-
- **入包红线**:`*.pem` / `*.key` / `.env` / `id_ed25519` / `id_rsa`
|
|
1743
|
-
|
|
1828
|
+
- **入包红线**:`*.pem` / `*.key` / `*.p12` / `*.pfx` / `.env` 系 / `id_ed25519` / `id_rsa`
|
|
1829
|
+
等私钥形态文件禁入包(与 `modules.json` 禁入同款自检,0.2.23 起扩面)——私钥入包即信任根基烧毁。
|
|
1744
1830
|
|
|
1745
1831
|
### 11.2 两种分发源
|
|
1746
1832
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oj-module/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.23",
|
|
5
5
|
"description": "React Antd Admin 模块工程命令行工具(含预构建宿主 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.
|
|
52
|
+
"@oj-module/runtime": "0.2.23",
|
|
53
53
|
"@redocly/cli": "^2.51.0",
|
|
54
54
|
"@tailwindcss/vite": "^4.3.0",
|
|
55
55
|
"esbuild": "^0.28.2",
|
|
@@ -204,6 +204,7 @@ var init_site_base = __esmMin((() => {
|
|
|
204
204
|
* 子路径部署(/oj/wiki/...)下直接与 loginPath 比较恒为 false,401 时会重复 goLogin。
|
|
205
205
|
* React 组件内请用 `useLocation()`(react-router 已剥离 basename),无需本函数。
|
|
206
206
|
*
|
|
207
|
+
* @param pathname 原始路径(含部署 base,如 `location.pathname`)
|
|
207
208
|
* @param base 部署 base,恒以 `/` 结尾;缺省取 `resolveSiteBase()`(部署期注入优先,
|
|
208
209
|
* 见 utils/site-base.ts —— runtime 产物里的 `import.meta.env.BASE_URL` 是构建期
|
|
209
210
|
* 内联的 `/`,子路径部署下直接用它恒错)
|
|
@@ -303,10 +304,10 @@ function getAppInfo() {
|
|
|
303
304
|
return {
|
|
304
305
|
"pkg": {
|
|
305
306
|
"name": "@oj-module/runtime",
|
|
306
|
-
"version": "0.2.
|
|
307
|
+
"version": "0.2.23",
|
|
307
308
|
"license": "MIT"
|
|
308
309
|
},
|
|
309
|
-
"lastBuildTime": "2026-10-22
|
|
310
|
+
"lastBuildTime": "2026-10-22 18:50:31"
|
|
310
311
|
};
|
|
311
312
|
}
|
|
312
313
|
var init_get_app_info = __esmMin((() => {}));
|
|
@@ -429,7 +430,7 @@ function PageError({ error, resetErrorBoundary }) {
|
|
|
429
430
|
};
|
|
430
431
|
useEffect(() => {
|
|
431
432
|
if (enableDynamicTitle) document.title = t("exception.pageErrorTitle");
|
|
432
|
-
}, [enableDynamicTitle]);
|
|
433
|
+
}, [enableDynamicTitle, t]);
|
|
433
434
|
return /* @__PURE__ */ jsx(Result, {
|
|
434
435
|
status: "error",
|
|
435
436
|
icon: /* @__PURE__ */ jsx("div", {
|
|
@@ -3495,7 +3496,7 @@ function useCurrentRoute() {
|
|
|
3495
3496
|
const matches = useMatches();
|
|
3496
3497
|
return useMemo(() => {
|
|
3497
3498
|
return matches[matches.length - 1];
|
|
3498
|
-
}, [matches
|
|
3499
|
+
}, [matches]);
|
|
3499
3500
|
}
|
|
3500
3501
|
var init_use_current_route = __esmMin((() => {}));
|
|
3501
3502
|
//#endregion
|
|
@@ -3595,7 +3596,7 @@ function useCssVar(name, root = defaultRoot, options = {}) {
|
|
|
3595
3596
|
}), [name, root]);
|
|
3596
3597
|
useEffect(() => {
|
|
3597
3598
|
if (options?.initialValue) controls.set(options.initialValue);
|
|
3598
|
-
}, [options?.initialValue]);
|
|
3599
|
+
}, [options?.initialValue, controls]);
|
|
3599
3600
|
useEffect(() => {
|
|
3600
3601
|
return () => controls.remove();
|
|
3601
3602
|
}, [controls]);
|
|
@@ -3656,7 +3657,7 @@ var init_dom = __esmMin((() => {}));
|
|
|
3656
3657
|
* @en Get the style of the layout content area
|
|
3657
3658
|
*/
|
|
3658
3659
|
function useLayoutContentStyle() {
|
|
3659
|
-
const
|
|
3660
|
+
const contentElementRef = useRef(null);
|
|
3660
3661
|
const [visibleDomRect, setVisibleDomRect] = useState(null);
|
|
3661
3662
|
const resizeObserverRef = useRef(null);
|
|
3662
3663
|
const contentHeightControls = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT);
|
|
@@ -3673,7 +3674,7 @@ function useLayoutContentStyle() {
|
|
|
3673
3674
|
};
|
|
3674
3675
|
}, [visibleDomRect]);
|
|
3675
3676
|
const debouncedCalcHeight = useDebounceFn((_entries) => {
|
|
3676
|
-
const rect = getElementVisibleRect(
|
|
3677
|
+
const rect = getElementVisibleRect(contentElementRef.current);
|
|
3677
3678
|
setVisibleDomRect(rect);
|
|
3678
3679
|
if (rect) {
|
|
3679
3680
|
contentHeightControls.set(`${rect.height}px`);
|
|
@@ -3681,10 +3682,10 @@ function useLayoutContentStyle() {
|
|
|
3681
3682
|
}
|
|
3682
3683
|
}, { wait: 16 });
|
|
3683
3684
|
useEffect(() => {
|
|
3684
|
-
if (
|
|
3685
|
+
if (contentElementRef.current && !resizeObserverRef.current) {
|
|
3685
3686
|
const resizeObserver = new ResizeObserver(debouncedCalcHeight.run);
|
|
3686
3687
|
resizeObserverRef.current = resizeObserver;
|
|
3687
|
-
resizeObserver.observe(
|
|
3688
|
+
resizeObserver.observe(contentElementRef.current);
|
|
3688
3689
|
}
|
|
3689
3690
|
return () => {
|
|
3690
3691
|
resizeObserverRef.current?.disconnect();
|
|
@@ -3692,7 +3693,7 @@ function useLayoutContentStyle() {
|
|
|
3692
3693
|
};
|
|
3693
3694
|
}, [debouncedCalcHeight]);
|
|
3694
3695
|
return {
|
|
3695
|
-
|
|
3696
|
+
contentElementRef,
|
|
3696
3697
|
overlayStyle,
|
|
3697
3698
|
visibleDomRect
|
|
3698
3699
|
};
|
|
@@ -4173,7 +4174,7 @@ function KeepAliveLayer() {
|
|
|
4173
4174
|
(aliveRef.current?.getCacheNodes?.())?.forEach((node) => {
|
|
4174
4175
|
if (!openTabs.has(node.cacheKey)) aliveRef.current?.destroy(node.cacheKey);
|
|
4175
4176
|
});
|
|
4176
|
-
}, [openTabs]);
|
|
4177
|
+
}, [openTabs, aliveRef]);
|
|
4177
4178
|
/**
|
|
4178
4179
|
* 关闭多 tab 功能时,清空所有非当前页面缓存
|
|
4179
4180
|
*/
|
|
@@ -4181,10 +4182,18 @@ function KeepAliveLayer() {
|
|
|
4181
4182
|
if (!tabbarEnable) (aliveRef.current?.getCacheNodes?.())?.forEach((node) => {
|
|
4182
4183
|
if (node.cacheKey !== cacheKey) aliveRef.current?.destroy(node.cacheKey);
|
|
4183
4184
|
});
|
|
4184
|
-
}, [
|
|
4185
|
+
}, [
|
|
4186
|
+
tabbarEnable,
|
|
4187
|
+
aliveRef,
|
|
4188
|
+
cacheKey
|
|
4189
|
+
]);
|
|
4185
4190
|
useEffect(() => {
|
|
4186
4191
|
if (tabbarEnable && isRefresh) aliveRef.current?.refresh();
|
|
4187
|
-
}, [
|
|
4192
|
+
}, [
|
|
4193
|
+
isRefresh,
|
|
4194
|
+
aliveRef,
|
|
4195
|
+
tabbarEnable
|
|
4196
|
+
]);
|
|
4188
4197
|
const keepAliveExclude = useMemo(() => {
|
|
4189
4198
|
/**
|
|
4190
4199
|
* 不开启多 tab 时不需要 KeepAlive,把所有路由放进 exclude 仅保留切换动画
|
|
@@ -4212,7 +4221,7 @@ var init_keep_alive_layer = __esmMin((() => {
|
|
|
4212
4221
|
//#region src/layout/layout-content/index.tsx
|
|
4213
4222
|
function LayoutContent() {
|
|
4214
4223
|
const { token: { colorBgLayout } } = theme.useToken();
|
|
4215
|
-
const {
|
|
4224
|
+
const { contentElementRef } = useLayoutContentStyle();
|
|
4216
4225
|
const enableFooter = usePreferencesStore((state) => state.enableFooter);
|
|
4217
4226
|
const fixedFooter = usePreferencesStore((state) => state.fixedFooter);
|
|
4218
4227
|
/**
|
|
@@ -4221,7 +4230,7 @@ function LayoutContent() {
|
|
|
4221
4230
|
*/
|
|
4222
4231
|
return /* @__PURE__ */ jsx("main", {
|
|
4223
4232
|
id: ELEMENT_ID_MAIN_CONTENT,
|
|
4224
|
-
ref:
|
|
4233
|
+
ref: contentElementRef,
|
|
4225
4234
|
className: "relative overflow-y-auto overflow-x-hidden grow",
|
|
4226
4235
|
style: { backgroundColor: colorBgLayout },
|
|
4227
4236
|
children: /* @__PURE__ */ jsx(Scrollbar, { children: /* @__PURE__ */ jsx(GlobalSpin, { children: /* @__PURE__ */ jsxs("div", {
|
|
@@ -4447,7 +4456,11 @@ function GlobalSearch() {
|
|
|
4447
4456
|
useKeyPress("downarrow", handleDown);
|
|
4448
4457
|
useEffect(() => {
|
|
4449
4458
|
if (!keyword.length && Array.isArray(searchHistory)) setResultOptions(searchMenuList.filter((item) => searchHistory?.includes(item.key)));
|
|
4450
|
-
}, [
|
|
4459
|
+
}, [
|
|
4460
|
+
keyword,
|
|
4461
|
+
searchHistory,
|
|
4462
|
+
searchMenuList
|
|
4463
|
+
]);
|
|
4451
4464
|
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
4452
4465
|
onClick: () => setOpen((open) => !open),
|
|
4453
4466
|
className: "group flex justify-center items-center gap-2 md:bg-colorBgLayout px-3 py-1.5 rounded-full cursor-pointer text-colorTextSecondary hover:text-colorText md:mr-2.5",
|
|
@@ -5113,18 +5126,18 @@ function Sidebar() {
|
|
|
5113
5126
|
name: item.name,
|
|
5114
5127
|
checked: item.value,
|
|
5115
5128
|
onChange: handleChange,
|
|
5116
|
-
|
|
5117
|
-
|
|
5129
|
+
disabled: item.disabled,
|
|
5130
|
+
children: item.label
|
|
5118
5131
|
}, item.name);
|
|
5119
5132
|
}),
|
|
5120
5133
|
/* @__PURE__ */ jsx(SwitchItem, {
|
|
5121
5134
|
name: "sidebarTheme",
|
|
5122
5135
|
checked: sidebarTheme === "light",
|
|
5123
5136
|
onChange: (name, value) => setPreferences(name, value ? "light" : "dark"),
|
|
5124
|
-
children: t("preferences.sidebar.sidebarTheme"),
|
|
5125
5137
|
disabled: !sidebarEnable,
|
|
5126
5138
|
unCheckedChildren: /* @__PURE__ */ jsx(RiContrastFill, {}),
|
|
5127
|
-
checkedChildren: /* @__PURE__ */ jsx(RiMoonIcon, {})
|
|
5139
|
+
checkedChildren: /* @__PURE__ */ jsx(RiMoonIcon, {}),
|
|
5140
|
+
children: t("preferences.sidebar.sidebarTheme")
|
|
5128
5141
|
}),
|
|
5129
5142
|
/* @__PURE__ */ jsx(NumberInputSpinner, {
|
|
5130
5143
|
min: 40,
|
|
@@ -5702,7 +5715,7 @@ function UserMenu({ ...restProps }) {
|
|
|
5702
5715
|
}
|
|
5703
5716
|
if (key === "personal-center") navigate("/personal-center/my-profile");
|
|
5704
5717
|
};
|
|
5705
|
-
const altView = useMemo(() => isWindowsOs() ? "Alt" : "⌥", [
|
|
5718
|
+
const altView = useMemo(() => isWindowsOs() ? "Alt" : "⌥", []);
|
|
5706
5719
|
const items = [{
|
|
5707
5720
|
label: t("personal-center:menu.personalCenter"),
|
|
5708
5721
|
key: "personal-center",
|
|
@@ -5973,7 +5986,7 @@ function LayoutMenu({ mode = "inline", autoExpandCurrentMenu, handleMenuSelect,
|
|
|
5973
5986
|
isMobile,
|
|
5974
5987
|
sidebarCollapsed
|
|
5975
5988
|
]);
|
|
5976
|
-
const handleOpenChange = (keys) => {
|
|
5989
|
+
const handleOpenChange = useCallback((keys) => {
|
|
5977
5990
|
/**
|
|
5978
5991
|
* 1. 手风琴模式,点击菜单项,自动关闭其他菜单
|
|
5979
5992
|
* 2. 非手风琴模式且菜单是收起的,鼠标悬浮菜单自动关闭其他菜单
|
|
@@ -5992,7 +6005,12 @@ function LayoutMenu({ mode = "inline", autoExpandCurrentMenu, handleMenuSelect,
|
|
|
5992
6005
|
if (currentCloseKey) setOpenKeys(menuParentKeys[currentCloseKey]);
|
|
5993
6006
|
}
|
|
5994
6007
|
} else setOpenKeys(keys);
|
|
5995
|
-
}
|
|
6008
|
+
}, [
|
|
6009
|
+
accordion,
|
|
6010
|
+
sidebarCollapsed,
|
|
6011
|
+
openKeys,
|
|
6012
|
+
menuParentKeys
|
|
6013
|
+
]);
|
|
5996
6014
|
const menuOpenProps = useMemo(() => {
|
|
5997
6015
|
if (autoExpandCurrentMenu) return {
|
|
5998
6016
|
openKeys,
|
|
@@ -6019,7 +6037,8 @@ function LayoutMenu({ mode = "inline", autoExpandCurrentMenu, handleMenuSelect,
|
|
|
6019
6037
|
}, [
|
|
6020
6038
|
matches,
|
|
6021
6039
|
sidebarCollapsed,
|
|
6022
|
-
getSelectedKeys
|
|
6040
|
+
getSelectedKeys,
|
|
6041
|
+
accordion
|
|
6023
6042
|
]);
|
|
6024
6043
|
return /* @__PURE__ */ jsx(Menu, {
|
|
6025
6044
|
/**
|
|
@@ -6552,7 +6571,6 @@ function useDropdownMenu() {
|
|
|
6552
6571
|
}, [
|
|
6553
6572
|
t,
|
|
6554
6573
|
activeKey,
|
|
6555
|
-
homePath,
|
|
6556
6574
|
openTabs
|
|
6557
6575
|
]);
|
|
6558
6576
|
/**
|
|
@@ -6569,6 +6587,8 @@ function useDropdownMenu() {
|
|
|
6569
6587
|
[TabActionKeys.CLOSE_OTHERS]: closeOtherTabs,
|
|
6570
6588
|
[TabActionKeys.CLOSE_ALL]: closeAllTabs
|
|
6571
6589
|
}), [
|
|
6590
|
+
refresh,
|
|
6591
|
+
setIsRefresh,
|
|
6572
6592
|
removeTab,
|
|
6573
6593
|
closeRightTabs,
|
|
6574
6594
|
closeLeftTabs,
|
|
@@ -6814,7 +6834,7 @@ function LayoutTabbar() {
|
|
|
6814
6834
|
hash: ""
|
|
6815
6835
|
};
|
|
6816
6836
|
navigate(key + historyState.search + historyState.hash);
|
|
6817
|
-
}, [openTabs]);
|
|
6837
|
+
}, [openTabs, navigate]);
|
|
6818
6838
|
/**
|
|
6819
6839
|
* 处理标签页编辑(关闭)
|
|
6820
6840
|
* @param {React.MouseEvent | React.KeyboardEvent | string} key - 被编辑的标签页的key
|
|
@@ -6941,7 +6961,8 @@ function LayoutTabbar() {
|
|
|
6941
6961
|
location,
|
|
6942
6962
|
currentRoute,
|
|
6943
6963
|
setActiveKey,
|
|
6944
|
-
addTab
|
|
6964
|
+
addTab,
|
|
6965
|
+
activeKey
|
|
6945
6966
|
]);
|
|
6946
6967
|
return /* @__PURE__ */ jsx("div", {
|
|
6947
6968
|
className: classes.tabsContainer,
|
|
@@ -7023,7 +7044,7 @@ function ContainerLayout() {
|
|
|
7023
7044
|
if (screens.lg && !screens.xl) setPreferences("sidebarCollapsed", true);
|
|
7024
7045
|
else if (screens.xl) setPreferences("sidebarCollapsed", false);
|
|
7025
7046
|
else if (screens.xs || screens.sm && !screens.md) setPreferences("sidebarCollapsed", false);
|
|
7026
|
-
}, [screens]);
|
|
7047
|
+
}, [screens, setPreferences]);
|
|
7027
7048
|
const sidebarEnableState = useMemo(() => !isTopNav && sidebarEnable, [isTopNav, sidebarEnable]);
|
|
7028
7049
|
const computedSidebarWidth = useMemo(() => {
|
|
7029
7050
|
if (isMaximize || isMobile) return 0;
|
|
@@ -7049,7 +7070,7 @@ function ContainerLayout() {
|
|
|
7049
7070
|
let height = 48;
|
|
7050
7071
|
if (tabbarEnable) height += 35;
|
|
7051
7072
|
return height;
|
|
7052
|
-
}, [tabbarEnable
|
|
7073
|
+
}, [tabbarEnable]);
|
|
7053
7074
|
useEffect(() => {
|
|
7054
7075
|
setLayoutHeaderHeight(isMaximize ? 35 : headerWrapperHeight);
|
|
7055
7076
|
}, [headerWrapperHeight, isMaximize]);
|
|
@@ -7934,7 +7955,7 @@ async function generateRoutesFromBackend(backendRoutes) {
|
|
|
7934
7955
|
/**
|
|
7935
7956
|
* 转换路由配置
|
|
7936
7957
|
* @param route 原始路由配置
|
|
7937
|
-
* @param
|
|
7958
|
+
* @param parentComponentPath 父级组件路径(用于嵌套路由)
|
|
7938
7959
|
* @returns 转换后的路由配置
|
|
7939
7960
|
*/
|
|
7940
7961
|
const transformRoute = async (route, parentComponentPath) => {
|
|
@@ -8918,8 +8939,6 @@ var init_remember_route = __esmMin((() => {
|
|
|
8918
8939
|
* 必须带上部署 base(`resolveSiteBase()`):子路径部署下 `location.href = "/login"`
|
|
8919
8940
|
* 会跳到宿主机根,而不是 `/oj/login`。不能用 `import.meta.env.BASE_URL`——预构建
|
|
8920
8941
|
* 产物里它是构建期内联的 `/`。
|
|
8921
|
-
*
|
|
8922
|
-
* @returns 无返回值
|
|
8923
8942
|
*/
|
|
8924
8943
|
function goLogin() {
|
|
8925
8944
|
useAuthStore.getState().reset();
|
|
@@ -9022,6 +9041,8 @@ function onRefreshFailed(error) {
|
|
|
9022
9041
|
* 订阅者对象应包含 resolve 和 reject 方法。
|
|
9023
9042
|
*
|
|
9024
9043
|
* @param subscriber 订阅者对象,包含 resolve 和 reject 方法
|
|
9044
|
+
* @param subscriber.resolve 当 token 刷新成功时调用的函数,传入新的 token
|
|
9045
|
+
* @param subscriber.reject 当 token 刷新失败时调用的函数,传入错误信息
|
|
9025
9046
|
*/
|
|
9026
9047
|
function addRefreshSubscriber(subscriber) {
|
|
9027
9048
|
refreshSubscribers.push(subscriber);
|
|
@@ -9447,6 +9468,10 @@ function useAccess() {
|
|
|
9447
9468
|
* 权限验证组件
|
|
9448
9469
|
*
|
|
9449
9470
|
* @param AccessControlProps 权限验证组件的属性
|
|
9471
|
+
* @param AccessControlProps.type 权限类型,默认为 code
|
|
9472
|
+
* @param AccessControlProps.codes 权限值,可以是字符串或字符串数组
|
|
9473
|
+
* @param AccessControlProps.children 有权限时渲染的子组件
|
|
9474
|
+
* @param AccessControlProps.fallback 无权限时显示的内容
|
|
9450
9475
|
* @returns 若子组件存在,并且传入的权限值有效,则返回子组件;否则返回 null
|
|
9451
9476
|
*/
|
|
9452
9477
|
function AccessControl({ type = "code", codes, children, fallback }) {
|
|
@@ -9800,7 +9825,7 @@ function BasicTable(props) {
|
|
|
9800
9825
|
if (enableFooter && fixedFooter) return 40;
|
|
9801
9826
|
return 0;
|
|
9802
9827
|
}, [enableFooter, fixedFooter]);
|
|
9803
|
-
const getPaginationProps = () => {
|
|
9828
|
+
const getPaginationProps = useCallback(() => {
|
|
9804
9829
|
if (props.pagination === false) return false;
|
|
9805
9830
|
return {
|
|
9806
9831
|
placement: ["bottomStart"],
|
|
@@ -9810,7 +9835,7 @@ function BasicTable(props) {
|
|
|
9810
9835
|
showTotal: (total) => t("common.pagination", { total }),
|
|
9811
9836
|
...props.pagination
|
|
9812
9837
|
};
|
|
9813
|
-
};
|
|
9838
|
+
}, [props.pagination, t]);
|
|
9814
9839
|
/**
|
|
9815
9840
|
* @description 计算分页器的高度
|
|
9816
9841
|
* 如果分页器被禁用,则返回 0,否则根据分页器的大小返回相应的高度
|
package/shell-dist/index.html
CHANGED
|
@@ -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-
|
|
8
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' https://modules.cdn.example.com 'wasm-unsafe-eval' 'nonce-KUwgkL5p/qZWtW8VUNiLzg=='; 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="
|
|
10
|
+
<script type="importmap" nonce="KUwgkL5p/qZWtW8VUNiLzg==">{
|
|
11
11
|
"imports": {
|
|
12
12
|
"react": "/assets/react.js",
|
|
13
13
|
"react/jsx-runtime": "/assets/jsx-runtime.js",
|
package/shell-dist/versions.json
CHANGED
|
@@ -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.
|
|
11
|
-
"@oj-module/runtime/contract": "0.2.
|
|
12
|
-
"@oj-module/runtime/contract/errors": "0.2.
|
|
10
|
+
"@oj-module/runtime": "0.2.23",
|
|
11
|
+
"@oj-module/runtime/contract": "0.2.23",
|
|
12
|
+
"@oj-module/runtime/contract/errors": "0.2.23",
|
|
13
13
|
"@plane/kit": "0.1.20",
|
|
14
14
|
"@plane/editor": "0.1.20",
|
|
15
15
|
"antd": "6.6.1",
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// BDD:脚手架 package.json scripts 与模板的双写守卫(T3,发布工程师评审建议)。
|
|
2
|
+
// 生成器(init.ts generatePackageJson)与模板(templates/package.json)各写一份 scripts,
|
|
3
|
+
// 人肉同步必漂移(实证:typecheck:api 曾漏发生成器)——钉成测试:生成 scripts 的每个键
|
|
4
|
+
// 都必须存在于模板 scripts(值一致),新增 script 先改模板再改生成器。
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import { generatePackageJson } from "../init";
|
|
10
|
+
|
|
11
|
+
const cliRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
12
|
+
|
|
13
|
+
describe("脚手架 package.json scripts 双写守卫(T3)", () => {
|
|
14
|
+
it("生成 scripts 的每个键都存在于模板且值一致", () => {
|
|
15
|
+
const template = JSON.parse(readFileSync(join(cliRoot, "templates/package.json"), "utf-8"));
|
|
16
|
+
const generated = generatePackageJson(cliRoot, "guard-check");
|
|
17
|
+
for (const [key, value] of Object.entries(generated.scripts)) {
|
|
18
|
+
expect(template.scripts[key], `生成器有而模板无的 script:${key}(先改 templates/package.json)`)
|
|
19
|
+
.toBe(value);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("多方案 scripts 齐备:new / bundle / typecheck:api", () => {
|
|
24
|
+
const generated = generatePackageJson(cliRoot, "guard-check");
|
|
25
|
+
expect(generated.scripts.new).toBe("ojm new");
|
|
26
|
+
expect(generated.scripts.bundle).toBe("ojm build --bundle");
|
|
27
|
+
expect(generated.scripts["typecheck:api"]).toBe("tsc --noEmit -p src/tsconfig.json");
|
|
28
|
+
});
|
|
29
|
+
});
|
package/src/args.ts
CHANGED
|
@@ -40,13 +40,16 @@ export function parseApiArgs(argv: string[]): { dir: string, check: boolean, doc
|
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* `ojm build [--bundle] [--out <dir>]`。
|
|
43
|
-
* `--bundle
|
|
44
|
-
*
|
|
43
|
+
* `--bundle`:方案包构建(契约校验 → 确定性 tgz + sha256)。bundle 级契约位置:优先工程根
|
|
44
|
+
* `solution.yaml`、或 `src/<模块>/solution.yaml`(模块级方案,契约与所打包模块同位);旧方案回退
|
|
45
|
+
* `manifest.yaml`(同两处位置)。与站点级 `ojm build`(后端 oj build + 模块产物合并)互斥分工
|
|
46
|
+
* (devkit 手册「打包与分发」)。
|
|
45
47
|
* `--out` 的值不得以 `--` 开头(同 parseApiArgs 的消费式扫描,P6 同款坑)。
|
|
46
48
|
*/
|
|
47
|
-
export function parseBuildArgs(argv: string[]): { bundle: boolean, out?: string } {
|
|
49
|
+
export function parseBuildArgs(argv: string[]): { bundle: boolean, bundleName?: string, out?: string } {
|
|
48
50
|
const flags = new Set<string>();
|
|
49
51
|
let out: string | undefined;
|
|
52
|
+
let bundleName: string | undefined;
|
|
50
53
|
for (let i = 0; i < argv.length; i++) {
|
|
51
54
|
const a = argv[i];
|
|
52
55
|
if (a === "--out") {
|
|
@@ -54,11 +57,58 @@ export function parseBuildArgs(argv: string[]): { bundle: boolean, out?: string
|
|
|
54
57
|
i++; // 消费紧随的值,不被当 flag
|
|
55
58
|
continue;
|
|
56
59
|
}
|
|
60
|
+
// --bundle <name> / --bundle=<name>:只打这一份方案(无值 = 全部用户方案)
|
|
61
|
+
if (a === "--bundle") {
|
|
62
|
+
flags.add(a);
|
|
63
|
+
const next = argv[i + 1];
|
|
64
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
65
|
+
bundleName = next;
|
|
66
|
+
i++;
|
|
67
|
+
}
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (a.startsWith("--bundle=")) {
|
|
71
|
+
flags.add("--bundle");
|
|
72
|
+
bundleName = a.slice("--bundle=".length);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
57
75
|
if (a.startsWith("--")) {
|
|
58
76
|
flags.add(a);
|
|
59
77
|
}
|
|
60
78
|
}
|
|
61
|
-
return { bundle: flags.has("--bundle"), out };
|
|
79
|
+
return { bundle: flags.has("--bundle"), bundleName, out };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* `ojm new <name> [--layout container|none] [--api]`:新方案骨架生成参数。
|
|
84
|
+
* name 为首个非 flag 位置参数(必填,由调用方判空——非交互/CI 友好)。
|
|
85
|
+
*/
|
|
86
|
+
export function parseNewArgs(argv: string[]): { name?: string, layout?: "container" | "none", api?: boolean } {
|
|
87
|
+
let name: string | undefined;
|
|
88
|
+
let layout: "container" | "none" | undefined;
|
|
89
|
+
let api = false;
|
|
90
|
+
for (let i = 0; i < argv.length; i++) {
|
|
91
|
+
const a = argv[i];
|
|
92
|
+
if (a === "--layout") {
|
|
93
|
+
const v = argv[i + 1];
|
|
94
|
+
if (v !== "container" && v !== "none")
|
|
95
|
+
throw new Error(`[ojm] --layout 只接受 container | none(实际:${v})`);
|
|
96
|
+
layout = v;
|
|
97
|
+
i++;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (a === "--api") {
|
|
101
|
+
api = true;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (a !== undefined && !a.startsWith("--")) {
|
|
105
|
+
if (name !== undefined)
|
|
106
|
+
throw new Error(`[ojm] ojm new 只接受一个方案名(多余参数:${a})`);
|
|
107
|
+
name = a;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { name, layout, api };
|
|
62
112
|
}
|
|
63
113
|
|
|
64
114
|
/**
|