@oj-module/cli 0.2.27 → 0.2.28
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/README.md +1 -1
- package/devkit/manual.md +70 -5
- package/package.json +2 -2
- package/shell-dist/assets/{index-B3g19Uet.js → index-CtTVbohp.js} +4 -3
- package/shell-dist/assets/runtime.js +147 -46
- package/shell-dist/index.html +3 -3
- package/shell-dist/versions.json +3 -3
- package/src/build.ts +48 -8
- package/src/bundle.ts +8 -1
- package/src/config.ts +12 -0
- package/src/contract/run.ts +29 -5
- package/src/manifest-contract.ts +14 -1
- package/src/new.ts +2 -1
- package/src/site-base.ts +26 -12
- package/templates/src/_platform/api/_lifecycle/steps/deploy-web.ts +12 -4
- package/templates/src/_platform/api/_lifecycle/steps/extract.ts +2 -0
- package/templates/src/_platform/api/_lifecycle/types.ts +5 -0
- package/vendor/host-versions.json +3 -3
package/devkit/README.md
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
| 文件 | 用途 |
|
|
25
25
|
|---|---|
|
|
26
26
|
| `SKILL.md` | Claude Code 等 agent 的 skill 入口:工作流、红线、新模块 checklist、陷阱速查,按章节号引用手册 |
|
|
27
|
-
| `manual.md` | 模块开发手册(13 章):工程结构(module-root 单树 + 两处产物根;含 §1.7 handler 四红线——oj 运行时实测钉出,后端必读;§1.8 `#` 别名与 TS 投影)、写模块、契约与代码生成(含 §3.10 旧 contract.ts 工程迁移指南)、provider 接管(覆盖框架内置功能)、布局、i18n、构建发布、排查、CLI 子命令参考(ojm 原生命令 vs oj
|
|
27
|
+
| `manual.md` | 模块开发手册(13 章):工程结构(module-root 单树 + 两处产物根;含 §1.7 handler 四红线——oj 运行时实测钉出,后端必读;§1.8 `#` 别名与 TS 投影)、写模块、契约与代码生成(含 §3.10 旧 contract.ts 工程迁移指南)、provider 接管(覆盖框架内置功能)、布局、i18n、构建发布、排查、CLI 子命令参考(ojm 原生命令 vs oj 透传命令)、方案包打包与分发(含 §11.1.1 引用式方案)、签名与信任;含 §13.1.1 web.route_prefix 自定义根路径 |
|
|
28
28
|
| `framework-dev.md` | 框架贡献者路线(**仅框架仓库**):改 `packages/runtime`、`packages/cli` 时的入口与红线 |
|
|
29
29
|
| `module-dev-and-cdn-playground.md` | 教学文档:开发态 ≠ 分发态、「怎么安装自己的模块 / 会不会重复」、本地 CDN 市场链路五步跑通(setup → publish → build-index → verify → 安装) |
|
|
30
30
|
| `sql-builder-missing-capabilities-pr.md` | 构造器能力缺口 PR 参考(**仅框架仓库**):7 项诉求的闭环记录 |
|
package/devkit/manual.md
CHANGED
|
@@ -822,7 +822,8 @@ export default { get };
|
|
|
822
822
|
逐个改 import 与调用;`tsc --noEmit` 应收敛到 0 错误。
|
|
823
823
|
6. **删旧文件**:删除各 `contract.ts`,以及旧流水线产物 `routes.json` /
|
|
824
824
|
`openapi.yaml`(模块目录下;ojm 不再自产。`ojm api --docs` 渲染的静态文档站另放:
|
|
825
|
-
|
|
825
|
+
`<projectRoot>/src/docs/<name>/api`(与模块树同位、树外,不参与模块枚举与 handler 路由扫描,
|
|
826
|
+
杜绝 docs 被当 handler 镜像导致端点翻倍;legacy 布局同形、落在 `api/src` 扫描树外,保留)。
|
|
826
827
|
7. **对账**:`ojm api --check` 应 `0 error / 0 warn`;内置端点(`auth/*`、`web/*`、
|
|
827
828
|
`notification/*`)经 `.ojm-api-exempt.json` 豁免(init 已内置)。
|
|
828
829
|
|
|
@@ -1681,6 +1682,9 @@ ojm build --bundle crm # 打包
|
|
|
1681
1682
|
```
|
|
1682
1683
|
|
|
1683
1684
|
|
|
1685
|
+
> 生成的 `src/<name>/solution.yaml` 不再写出 `web.route_prefix: /<name>`(与缺省重复),只在
|
|
1686
|
+
> `web` 段留一行注释;要自定义根路径时取消注释改值即可(见 §13.1.1)。
|
|
1687
|
+
|
|
1684
1688
|
#### `ojm preview [port]`
|
|
1685
1689
|
|
|
1686
1690
|
生产形态预览(设计 §6,2026-10-19 单服务化):
|
|
@@ -1705,8 +1709,11 @@ ojm build --bundle crm # 打包
|
|
|
1705
1709
|
- `dir`:项目目录(缺省 `cwd`;显式目录必须先存在,否则报错)。
|
|
1706
1710
|
- `--check`:**四重对账**(只读、绝不写盘):① 生成物同步 ② route 双向对账
|
|
1707
1711
|
③ `routes.js` diff ④ WS 路由鉴权(§3.6)。
|
|
1708
|
-
- `--docs`:聚合 OpenAPI → redoc 自包含静态文档站(离线可看),落
|
|
1709
|
-
|
|
1712
|
+
- `--docs`:聚合 OpenAPI → redoc 自包含静态文档站(离线可看),落
|
|
1713
|
+
`<projectRoot>/src/docs/<name>/api`(与模块树同位、树外——不参与模块枚举与 handler 路由扫描,
|
|
1714
|
+
杜绝 docs 被当 handler 镜像导致端点翻倍 `wikiDocsDocs*`)。`name` 推导:cwd 为 `src/<name>/api`
|
|
1715
|
+
时取父目录名;module-root 单模块时取该模块名;否则回退工程目录名(legacy 布局同形,
|
|
1716
|
+
落在 `api/src` 扫描树外)。
|
|
1710
1717
|
- `--exempt <path>`:指定豁免清单,覆盖默认探测顺序 `.ojm-api-exempt.json` →
|
|
1711
1718
|
`api/.ojm-api-exempt.json` → `api/.ram-api-exempt.json`(旧名回退),§3.8。
|
|
1712
1719
|
|
|
@@ -1830,6 +1837,24 @@ ojm build --bundle crm # 只打 src/crm/ 这一份;--bundle=crm 等价
|
|
|
1830
1837
|
- **入包红线**:`*.pem` / `*.key` / `*.p12` / `*.pfx` / `.env` 系 / `id_ed25519` / `id_rsa`
|
|
1831
1838
|
等私钥形态文件禁入包(与 `modules.json` 禁入同款自检,0.2.23 起扩面)——私钥入包即信任根基烧毁。
|
|
1832
1839
|
|
|
1840
|
+
#### 11.1.1 引用式方案:`api` 段省略 `tables` / `deps`
|
|
1841
|
+
|
|
1842
|
+
方案包契约的 `api` 段有两种合法形态:
|
|
1843
|
+
|
|
1844
|
+
- **全量形态**:给出 `tables` 与/或 `deps`(外加 `module` / `prefix` / `anonymous` 等),
|
|
1845
|
+
契约按全量校验,`api.tables` 须与 `api/src/<name>/schema.yaml` 表集逐一相等(oj 冗余校验同源)。
|
|
1846
|
+
- **引用形态**:`api` 段只给 `module` / `prefix` 等、**省略 `tables` 与 `deps`**——表与依赖清单
|
|
1847
|
+
**引用包内** oj 原生 `<module>/manifest.yaml`(七键清单,tgz 打包时已随包携带)。契约**显式承认**
|
|
1848
|
+
此形态(不再仅靠字段可选性漏过),安装期 `extract` 阶段读回该原生 manifest 做占用检测与依赖装配
|
|
1849
|
+
(`_platform` 除外);两字段任一给出即按全量优先。
|
|
1850
|
+
|
|
1851
|
+
`ojm build --bundle` 校验通过后会打印一行提示,避免把省略误读成漏配:
|
|
1852
|
+
|
|
1853
|
+
```
|
|
1854
|
+
[ojm] 引用式方案:api.tables/api.deps 缺省,引用包内 api/<name>-<version>/manifest.yaml
|
|
1855
|
+
(安装期复核其存在,依赖装配据其回读,非漏配)
|
|
1856
|
+
```
|
|
1857
|
+
|
|
1833
1858
|
### 11.2 两种分发源
|
|
1834
1859
|
|
|
1835
1860
|
| 源 | 形态 | 说明 |
|
|
@@ -2011,6 +2036,45 @@ export default defineModule({
|
|
|
2011
2036
|
});
|
|
2012
2037
|
```
|
|
2013
2038
|
|
|
2039
|
+
#### 13.1.1 自定义模块根路径:`web.route_prefix`
|
|
2040
|
+
|
|
2041
|
+
单根约定下,模块根路由首段默认就是模块名 `name`(即 `web.route_prefix` 缺省 `/<name>`)。
|
|
2042
|
+
要换根(比如让 `wiki` 模块挂在 `/kb`),在 `src/<name>/solution.yaml` 的 `web` 段声明:
|
|
2043
|
+
|
|
2044
|
+
```yaml
|
|
2045
|
+
web:
|
|
2046
|
+
route_prefix: /kb # 形如 /<小写字母开头,字母数字短横线>
|
|
2047
|
+
```
|
|
2048
|
+
|
|
2049
|
+
构建期 `ojm build` 会把**生效前缀段**(`kb`,去首斜杠)写进 `modules.json` 每个模块条目的
|
|
2050
|
+
`routePrefix` 字段;运行时模块加载器据此**只改写路由根段**(单根约定下仅首段——
|
|
2051
|
+
`applyRoutePrefix` 保持原 path 的前导斜杠风格:相对 `wiki`→`kb`、绝对 `/wiki`→`/kb`,
|
|
2052
|
+
children 不变)。因此**缺省(不声明)= name 时路径字段逐字节不变(零漂移)**,已有模块升级无感。
|
|
2053
|
+
|
|
2054
|
+
改写后 `rewriteAdminPaths` 会自动跟随(它从顶层路径推导根),`category: "admin"` 的路由落到
|
|
2055
|
+
`/system/<前缀>/admin`,菜单归组一并跟随——菜单、深链、模块内跳转、tab 键、admin 提升全部落在
|
|
2056
|
+
同一个根。
|
|
2057
|
+
|
|
2058
|
+
模块内路径**不要硬编码根段**,统一经 `getModuleRoutePrefix(name)` 单点消费(运行时导出,读
|
|
2059
|
+
`modules.json` 的 `routePrefix`,未声明回落 `name`):
|
|
2060
|
+
|
|
2061
|
+
```ts
|
|
2062
|
+
// paths.ts:把根段参数化,所有 navigate / Link / matchPath 都从它派生
|
|
2063
|
+
export const createWikiPaths = (root: string) => ({
|
|
2064
|
+
home: `/${root}`,
|
|
2065
|
+
space: `/${root}/space`,
|
|
2066
|
+
admin: `/${root}/admin`,
|
|
2067
|
+
});
|
|
2068
|
+
|
|
2069
|
+
// 使用时懒取(组件渲染 / 事件处理内),不要缓存到模块顶层常量——
|
|
2070
|
+
// 模块静态求值期模块表尚未就绪,须等 loadAll 之后才取值
|
|
2071
|
+
import { getModuleRoutePrefix } from "@oj-module/runtime";
|
|
2072
|
+
const paths = createWikiPaths(getModuleRoutePrefix("wiki"));
|
|
2073
|
+
```
|
|
2074
|
+
|
|
2075
|
+
> `ojm new` 生成的骨架不再写出 `route_prefix: /<name>`(与缺省重复),只留注释,需要时取消
|
|
2076
|
+
> 注释改值即可(见 §10.1)。
|
|
2077
|
+
|
|
2014
2078
|
### 13.2 category 三值语义
|
|
2015
2079
|
|
|
2016
2080
|
`handle.category`:
|
|
@@ -2025,6 +2089,7 @@ export default defineModule({
|
|
|
2025
2089
|
|
|
2026
2090
|
- 模块根菜单标题与 /system 下的管理子组标题**共用** `<模块>:menu.main` 一个 i18n key(嵌套在 `menu.main`,是模块级标题的唯一真源,缺则构建期 fail-fast)。
|
|
2027
2091
|
- 模块内所有前端路径——`navigate` / `Link to` / `useMatch`·`matchPath` / `currentActiveMenu` / SEO 正则——统一从模块基路径助手(如 `paths.ts` 的 `wikiPath(...)` / `wikiBase`)派生,不散落 `/wiki/...` 字面量;`currentActiveMenu` 必须写绝对路径(指向真实菜单 key)。`tree/bus.ts` 的 room 名是后端 WS 契约,非前端路由,不动。
|
|
2092
|
+
- 模块基路径的真源是 `getModuleRoutePrefix(name)`(运行时导出,读 `modules.json` 的 `routePrefix`,缺省 `name`)——`paths.ts` 助手应以其为参数,而非写死 `/<name>`;自定义根路径见 §13.1.1。
|
|
2028
2093
|
|
|
2029
2094
|
### 13.4 开发者的该做 / 别做
|
|
2030
2095
|
|
|
@@ -2060,6 +2125,6 @@ export default defineModule({
|
|
|
2060
2125
|
| §8 排查 | 五步排查法、症状速查表 | 任何「没生效」 |
|
|
2061
2126
|
| §9 参考 | 可抄的工程内文件、oj-api-dev 手册指引 | 找范本 |
|
|
2062
2127
|
| §10 CLI 子命令参考 | ojm 原生命令 vs oj 透传命令;`market` 子命令(本地模拟 CDN) | 查命令用法;`ojm <cmd> --help` 看单命令帮助 |
|
|
2063
|
-
| §11 方案包:打包与分发 | build --bundle、Form A/B
|
|
2128
|
+
| §11 方案包:打包与分发 | build --bundle、Form A/B、分发源、索引规范、引用式方案(api 段省略 tables/deps,引用包内原生 manifest) | 发布方案包 |
|
|
2064
2129
|
| §12 签名与信任 | keygen/sign/install 发布流程、信任库、TOFU、bootstrap key | 首次签名发布;管理信任 |
|
|
2065
|
-
| §13 方案菜单归组规则 |
|
|
2130
|
+
| §13 方案菜单归组规则 | 单根模块约定(根路由=第一级菜单)、自定义根路径 `web.route_prefix`(构建写 modules.json、运行时只改根段、getModuleRoutePrefix 单点消费)、category 三值(普通/hidden/admin)、admin 真重写 `/system/<模块 root>/*` + 视觉归组、menu.main 统一、模块内路径助手、开发者 DO/DON'T | 写方案要让菜单按模块分组、admin 归系统设置 |
|
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.28",
|
|
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.
|
|
52
|
+
"@oj-module/runtime": "0.2.28",
|
|
53
53
|
"@redocly/cli": "^2.51.0",
|
|
54
54
|
"@tailwindcss/vite": "^4.3.0",
|
|
55
55
|
"cac": "^6.7.14",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StyleProvider } from "@ant-design/cssinjs";
|
|
2
|
-
import { AntdApp, ErrorBoundary, LayoutEffects, PageError,
|
|
2
|
+
import { AntdApp, ErrorBoundary, LayoutEffects, PageError, getHostRoutes, loadAll, setupI18n, useAuthStore, usePreferences, useUserStore } from "@oj-module/runtime";
|
|
3
3
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
4
4
|
import { ConfigProvider, theme } from "antd";
|
|
5
5
|
import { useEffect, useState } from "react";
|
|
@@ -300,6 +300,7 @@ function Boot() {
|
|
|
300
300
|
await hydrateUser();
|
|
301
301
|
await loadAll(manifest);
|
|
302
302
|
if (cancelled) return;
|
|
303
|
+
const hostRoutes = getHostRoutes();
|
|
303
304
|
setRouter(createBrowserRouter([{
|
|
304
305
|
path: "/",
|
|
305
306
|
element: /* @__PURE__ */ jsxs(ErrorBoundary, {
|
|
@@ -310,11 +311,11 @@ function Boot() {
|
|
|
310
311
|
{
|
|
311
312
|
index: true,
|
|
312
313
|
element: /* @__PURE__ */ jsx(Navigate, {
|
|
313
|
-
to:
|
|
314
|
+
to: hostRoutes[0]?.path ?? "/",
|
|
314
315
|
replace: true
|
|
315
316
|
})
|
|
316
317
|
},
|
|
317
|
-
...
|
|
318
|
+
...hostRoutes,
|
|
318
319
|
{
|
|
319
320
|
path: "*",
|
|
320
321
|
element: /* @__PURE__ */ jsx(CatchAll, {})
|
|
@@ -304,10 +304,10 @@ function getAppInfo() {
|
|
|
304
304
|
return {
|
|
305
305
|
"pkg": {
|
|
306
306
|
"name": "@oj-module/runtime",
|
|
307
|
-
"version": "0.2.
|
|
307
|
+
"version": "0.2.28",
|
|
308
308
|
"license": "MIT"
|
|
309
309
|
},
|
|
310
|
-
"lastBuildTime": "2026-10-24
|
|
310
|
+
"lastBuildTime": "2026-10-24 18:30:15"
|
|
311
311
|
};
|
|
312
312
|
}
|
|
313
313
|
var init_get_app_info = __esmMin((() => {}));
|
|
@@ -7203,6 +7203,42 @@ var init_resolve_layout = __esmMin((() => {
|
|
|
7203
7203
|
warnedUnknownLayouts = /* @__PURE__ */ new Set();
|
|
7204
7204
|
}));
|
|
7205
7205
|
//#endregion
|
|
7206
|
+
//#region src/router/utils/rewrite-admin-paths.ts
|
|
7207
|
+
/** 解析路由绝对路径(与 generate-menu-items-from-routes 同语义) */
|
|
7208
|
+
function resolveAbs(path, parentAbs) {
|
|
7209
|
+
if (!path) return parentAbs;
|
|
7210
|
+
return path.startsWith("/") ? path : `${parentAbs.replace(/\/+$/, "")}/${path}`;
|
|
7211
|
+
}
|
|
7212
|
+
function rewriteAdminPaths(routes, systemKey = SYSTEM_KEY) {
|
|
7213
|
+
const hoisted = [];
|
|
7214
|
+
return [...routes.map((r) => processRoute(r, "/", systemKey, hoisted, void 0)).filter((r) => r !== null), ...hoisted];
|
|
7215
|
+
}
|
|
7216
|
+
function processRoute(route, parentAbs, systemKey, hoisted, moduleRootAbs) {
|
|
7217
|
+
const abs = resolveAbs(route.path, parentAbs);
|
|
7218
|
+
const rootAbs = moduleRootAbs ?? abs;
|
|
7219
|
+
if (route.handle?.category === "admin") {
|
|
7220
|
+
const newAbs = `${systemKey}${rootAbs}${abs.slice(rootAbs.length) || ""}`;
|
|
7221
|
+
hoisted.push({
|
|
7222
|
+
...route,
|
|
7223
|
+
path: newAbs
|
|
7224
|
+
});
|
|
7225
|
+
return null;
|
|
7226
|
+
}
|
|
7227
|
+
if (!route.children || route.children.length === 0) return { ...route };
|
|
7228
|
+
const newChildren = [];
|
|
7229
|
+
for (const c of route.children) {
|
|
7230
|
+
const processed = processRoute(c, abs, systemKey, hoisted, rootAbs);
|
|
7231
|
+
if (processed !== null) newChildren.push(processed);
|
|
7232
|
+
}
|
|
7233
|
+
const result = { ...route };
|
|
7234
|
+
result.children = newChildren;
|
|
7235
|
+
return result;
|
|
7236
|
+
}
|
|
7237
|
+
var SYSTEM_KEY;
|
|
7238
|
+
var init_rewrite_admin_paths = __esmMin((() => {
|
|
7239
|
+
SYSTEM_KEY = "/system";
|
|
7240
|
+
}));
|
|
7241
|
+
//#endregion
|
|
7206
7242
|
//#region src/store/header-provider.ts
|
|
7207
7243
|
function registerHeaderProvider(moduleName, provider) {
|
|
7208
7244
|
if (current) {
|
|
@@ -7378,11 +7414,12 @@ function satisfiesSemver(version, range) {
|
|
|
7378
7414
|
var init_semver = __esmMin((() => {}));
|
|
7379
7415
|
//#endregion
|
|
7380
7416
|
//#region src/module-loader/index.ts
|
|
7381
|
-
function createModuleContext(definition) {
|
|
7417
|
+
function createModuleContext(definition, routePrefix) {
|
|
7382
7418
|
return {
|
|
7383
7419
|
module: {
|
|
7384
7420
|
name: definition.name,
|
|
7385
|
-
version: definition.version
|
|
7421
|
+
version: definition.version,
|
|
7422
|
+
routePrefix
|
|
7386
7423
|
},
|
|
7387
7424
|
utils: { request: createScopedRequest(definition.name, () => registeredApiPrefixes.get(definition.name)) },
|
|
7388
7425
|
register: {
|
|
@@ -7485,6 +7522,7 @@ async function loadAll(manifest) {
|
|
|
7485
7522
|
console.error(`[module-loader] 模块 "${entry.name}" 与宿主 runtime 版本不兼容:期望 ${peerRuntime},实际 ${manifest.runtimeVersion}。已跳过加载。修复建议:升级宿主 runtime 或按兼容范围重新构建该模块(US-5)。`);
|
|
7486
7523
|
modules.set(entry.name, {
|
|
7487
7524
|
definition,
|
|
7525
|
+
routePrefix: entry.routePrefix ?? rootSegmentOf(definition.routes) ?? entry.name,
|
|
7488
7526
|
status: "error",
|
|
7489
7527
|
error: /* @__PURE__ */ new Error(`模块 "${entry.name}" peerRuntime 不兼容:期望 ${peerRuntime},实际 ${manifest.runtimeVersion}`)
|
|
7490
7528
|
});
|
|
@@ -7494,6 +7532,7 @@ async function loadAll(manifest) {
|
|
|
7494
7532
|
validEntries.push(entry);
|
|
7495
7533
|
modules.set(entry.name, {
|
|
7496
7534
|
definition,
|
|
7535
|
+
routePrefix: entry.routePrefix ?? rootSegmentOf(definition.routes) ?? entry.name,
|
|
7497
7536
|
status: "loaded"
|
|
7498
7537
|
});
|
|
7499
7538
|
} else modules.set(entry.name, {
|
|
@@ -7503,6 +7542,7 @@ async function loadAll(manifest) {
|
|
|
7503
7542
|
version: "0.0.0",
|
|
7504
7543
|
routes: []
|
|
7505
7544
|
},
|
|
7545
|
+
routePrefix: entry.routePrefix ?? entry.name,
|
|
7506
7546
|
status: "error",
|
|
7507
7547
|
error: /* @__PURE__ */ new Error("Failed to load module entry")
|
|
7508
7548
|
});
|
|
@@ -7514,6 +7554,7 @@ async function loadAll(manifest) {
|
|
|
7514
7554
|
console.error(`[module-loader] 模块 "${entry.name}" 依赖缺失:${missing.join(", ")} 未加载。已跳过该模块(不执行生命周期、不注册路由)。修复建议:先部署依赖模块,或在清单中移除该依赖(US-9)。`);
|
|
7515
7555
|
modules.set(entry.name, {
|
|
7516
7556
|
definition,
|
|
7557
|
+
routePrefix: entry.routePrefix ?? rootSegmentOf(definition.routes) ?? entry.name,
|
|
7517
7558
|
status: "missing-deps",
|
|
7518
7559
|
error: /* @__PURE__ */ new Error(`模块 "${entry.name}" 依赖缺失:${missing.join(", ")}`)
|
|
7519
7560
|
});
|
|
@@ -7524,7 +7565,7 @@ async function loadAll(manifest) {
|
|
|
7524
7565
|
const sortedEntries = topologicalSort(validEntries, definitions);
|
|
7525
7566
|
for (const entry of sortedEntries) {
|
|
7526
7567
|
const definition = definitions.get(entry.name);
|
|
7527
|
-
const ctx = createModuleContext(definition);
|
|
7568
|
+
const ctx = createModuleContext(definition, entry.routePrefix ?? rootSegmentOf(definition.routes) ?? entry.name);
|
|
7528
7569
|
try {
|
|
7529
7570
|
if (definition.lifecycle?.beforeInit) await definition.lifecycle.beforeInit(ctx);
|
|
7530
7571
|
if (definition.lifecycle?.onInit) await definition.lifecycle.onInit(ctx);
|
|
@@ -7533,6 +7574,7 @@ async function loadAll(manifest) {
|
|
|
7533
7574
|
console.error(`[module-loader] Lifecycle error for "${entry.name}":`, error);
|
|
7534
7575
|
modules.set(entry.name, {
|
|
7535
7576
|
definition,
|
|
7577
|
+
routePrefix: entry.routePrefix ?? rootSegmentOf(definition.routes) ?? entry.name,
|
|
7536
7578
|
status: "error",
|
|
7537
7579
|
error: error instanceof Error ? error : new Error(String(error))
|
|
7538
7580
|
});
|
|
@@ -7556,6 +7598,19 @@ function getModule(name) {
|
|
|
7556
7598
|
return modules.get(name);
|
|
7557
7599
|
}
|
|
7558
7600
|
/**
|
|
7601
|
+
* 模块生效 web 路由前缀段取值口(R1.3)。
|
|
7602
|
+
*
|
|
7603
|
+
* 模块内深链/跳转应经此单点消费,勿硬编码根段:
|
|
7604
|
+
* `createWikiPaths(getModuleRoutePrefix("wiki") ?? "wiki")`。
|
|
7605
|
+
*
|
|
7606
|
+
* 注意时序:须在 `loadAll` 之后调用——模块静态求值期 `modules` 未就绪。
|
|
7607
|
+
* 模块内导航应懒取(组件渲染 / 事件处理器内),不要缓存于模块顶层常量。
|
|
7608
|
+
* 未加载模块回落 `name`(与缺省语义一致)。
|
|
7609
|
+
*/
|
|
7610
|
+
function getModuleRoutePrefix(name) {
|
|
7611
|
+
return modules.get(name)?.routePrefix ?? name;
|
|
7612
|
+
}
|
|
7613
|
+
/**
|
|
7559
7614
|
* 给模块路由打 module 标(R1,0.2.16):递归 children,非突变返回新对象。
|
|
7560
7615
|
* module 来自 `definition.name`,作者无需手写——归组据此甄别方案路由、
|
|
7561
7616
|
* 生成 `solgroup:<category>:<module>` 父键,不再依赖宿主 DB 行。
|
|
@@ -7573,6 +7628,35 @@ function tagModuleRoutes(routes, name) {
|
|
|
7573
7628
|
return tagged;
|
|
7574
7629
|
});
|
|
7575
7630
|
}
|
|
7631
|
+
/**
|
|
7632
|
+
* 按生效前缀改写模块路由根(R1.2,单根约定下仅改首段/根段)。
|
|
7633
|
+
* 顶层路由 path 的首段替换为 prefix;children 为相对路径不变。
|
|
7634
|
+
* 保持原 path 的「前导斜杠」风格:相对 `wiki` → `kb`、绝对 `/wiki` → `/kb`,
|
|
7635
|
+
* 因此缺省 prefix = name 时路径字段逐字节不变(零漂移),仅段值一致。
|
|
7636
|
+
*/
|
|
7637
|
+
function applyRoutePrefix(routes, prefix) {
|
|
7638
|
+
return routes.map((route) => {
|
|
7639
|
+
const p = typeof route.path === "string" ? route.path : "";
|
|
7640
|
+
const hadLeadingSlash = p.startsWith("/");
|
|
7641
|
+
const firstSeg = p.replace(/^\/?/, "").split("/")[0] ?? "";
|
|
7642
|
+
const afterFirst = hadLeadingSlash ? p.slice(1 + firstSeg.length) : p.slice(firstSeg.length);
|
|
7643
|
+
const newPath = hadLeadingSlash ? `/${prefix}${afterFirst}` : `${prefix}${afterFirst}`;
|
|
7644
|
+
return {
|
|
7645
|
+
...route,
|
|
7646
|
+
path: newPath
|
|
7647
|
+
};
|
|
7648
|
+
});
|
|
7649
|
+
}
|
|
7650
|
+
/**
|
|
7651
|
+
* 取模块路由根段(首段,去前导斜杠);无路由返回 undefined。
|
|
7652
|
+
* 用于 `routePrefix` 缺省:单根约定下根段即模块身份,故缺省前缀 = 根段
|
|
7653
|
+
* (约定等于 name;fixture/异常态下 name 与根段不一致时回落根段而非 name,避免改写真实根)。
|
|
7654
|
+
*/
|
|
7655
|
+
function rootSegmentOf(routes) {
|
|
7656
|
+
const p = routes[0]?.path;
|
|
7657
|
+
if (typeof p !== "string" || p.length === 0) return void 0;
|
|
7658
|
+
return p.replace(/^\/?/, "").split("/")[0] || void 0;
|
|
7659
|
+
}
|
|
7576
7660
|
function getRoutes() {
|
|
7577
7661
|
const { roles, permissions = [] } = useUserStore.getState();
|
|
7578
7662
|
const routes = [];
|
|
@@ -7586,6 +7670,22 @@ function getRoutes() {
|
|
|
7586
7670
|
}
|
|
7587
7671
|
return addRouteIdByPath(routes);
|
|
7588
7672
|
}
|
|
7673
|
+
/**
|
|
7674
|
+
* 宿主链建路由表的唯一取口:与菜单管线同源套 rewriteAdminPaths。
|
|
7675
|
+
*
|
|
7676
|
+
* 0.2.17 起方案 admin 路由被真重写为 /system/<模块根>/*(菜单项 key 已随之改写),
|
|
7677
|
+
* 宿主若直接用 getRoutes() 建表(cli/shell host.tsx 回归,2026-10),渲染路由表
|
|
7678
|
+
* 停留在 /wiki/admin/* 而菜单指向 /system/wiki/admin/* → 方案管理页菜单点击必 404。
|
|
7679
|
+
* getRoutes() 只该给 access store 登记、keep-alive 聚合等「无需重写」的场景用。
|
|
7680
|
+
*/
|
|
7681
|
+
function getHostRoutes() {
|
|
7682
|
+
return rewriteAdminPaths(getRoutes().map((route) => {
|
|
7683
|
+
const mod = route.handle?.module;
|
|
7684
|
+
if (!mod) return route;
|
|
7685
|
+
const prefix = modules.get(mod)?.routePrefix ?? mod;
|
|
7686
|
+
return applyRoutePrefix([route], prefix)[0] ?? route;
|
|
7687
|
+
}));
|
|
7688
|
+
}
|
|
7589
7689
|
function getRegisteredStore(name) {
|
|
7590
7690
|
return registeredStores.get(name);
|
|
7591
7691
|
}
|
|
@@ -7600,7 +7700,7 @@ function getRegisteredApiPrefix(moduleName) {
|
|
|
7600
7700
|
async function unloadModule(name) {
|
|
7601
7701
|
const instance = modules.get(name);
|
|
7602
7702
|
if (instance) {
|
|
7603
|
-
const ctx = createModuleContext(instance.definition);
|
|
7703
|
+
const ctx = createModuleContext(instance.definition, instance.routePrefix);
|
|
7604
7704
|
if (instance.definition.lifecycle?.onDestroy) await instance.definition.lifecycle.onDestroy(ctx);
|
|
7605
7705
|
}
|
|
7606
7706
|
removeModuleSlots(name);
|
|
@@ -7632,6 +7732,7 @@ var init_module_loader = __esmMin((() => {
|
|
|
7632
7732
|
init_layout_registry();
|
|
7633
7733
|
init_add_route_id_by_path();
|
|
7634
7734
|
init_resolve_layout();
|
|
7735
|
+
init_rewrite_admin_paths();
|
|
7635
7736
|
init_access();
|
|
7636
7737
|
init_api_provider();
|
|
7637
7738
|
init_auth_provider();
|
|
@@ -8702,42 +8803,6 @@ var init_resolve_login_route = __esmMin((() => {
|
|
|
8702
8803
|
isInternalLoginRoute = (r) => r?.handle?.login === true && r?.handle?.internal === true;
|
|
8703
8804
|
}));
|
|
8704
8805
|
//#endregion
|
|
8705
|
-
//#region src/router/utils/rewrite-admin-paths.ts
|
|
8706
|
-
/** 解析路由绝对路径(与 generate-menu-items-from-routes 同语义) */
|
|
8707
|
-
function resolveAbs(path, parentAbs) {
|
|
8708
|
-
if (!path) return parentAbs;
|
|
8709
|
-
return path.startsWith("/") ? path : `${parentAbs.replace(/\/+$/, "")}/${path}`;
|
|
8710
|
-
}
|
|
8711
|
-
function rewriteAdminPaths(routes, systemKey = SYSTEM_KEY) {
|
|
8712
|
-
const hoisted = [];
|
|
8713
|
-
return [...routes.map((r) => processRoute(r, "/", systemKey, hoisted, void 0)).filter((r) => r !== null), ...hoisted];
|
|
8714
|
-
}
|
|
8715
|
-
function processRoute(route, parentAbs, systemKey, hoisted, moduleRootAbs) {
|
|
8716
|
-
const abs = resolveAbs(route.path, parentAbs);
|
|
8717
|
-
const rootAbs = moduleRootAbs ?? abs;
|
|
8718
|
-
if (route.handle?.category === "admin") {
|
|
8719
|
-
const newAbs = `${systemKey}${rootAbs}${abs.slice(rootAbs.length) || ""}`;
|
|
8720
|
-
hoisted.push({
|
|
8721
|
-
...route,
|
|
8722
|
-
path: newAbs
|
|
8723
|
-
});
|
|
8724
|
-
return null;
|
|
8725
|
-
}
|
|
8726
|
-
if (!route.children || route.children.length === 0) return { ...route };
|
|
8727
|
-
const newChildren = [];
|
|
8728
|
-
for (const c of route.children) {
|
|
8729
|
-
const processed = processRoute(c, abs, systemKey, hoisted, rootAbs);
|
|
8730
|
-
if (processed !== null) newChildren.push(processed);
|
|
8731
|
-
}
|
|
8732
|
-
const result = { ...route };
|
|
8733
|
-
result.children = newChildren;
|
|
8734
|
-
return result;
|
|
8735
|
-
}
|
|
8736
|
-
var SYSTEM_KEY;
|
|
8737
|
-
var init_rewrite_admin_paths = __esmMin((() => {
|
|
8738
|
-
SYSTEM_KEY = "/system";
|
|
8739
|
-
}));
|
|
8740
|
-
//#endregion
|
|
8741
8806
|
//#region src/store/access.ts
|
|
8742
8807
|
/**
|
|
8743
8808
|
* 切分主区/底部固定区路由(ascending 已双段排序:负 order 段整体在后)。
|
|
@@ -8877,6 +8942,29 @@ var init_auth = __esmMin((() => {
|
|
|
8877
8942
|
}), { name: getAppNamespace("access-token") }));
|
|
8878
8943
|
}));
|
|
8879
8944
|
//#endregion
|
|
8945
|
+
//#region src/utils/api-base.ts
|
|
8946
|
+
/** 归一为不以 `/` 结尾;绝对域(http(s)://)原样剥尾斜杠;空值回落默认 */
|
|
8947
|
+
function normalizeApiBase(raw) {
|
|
8948
|
+
const trimmed = raw?.trim();
|
|
8949
|
+
if (!trimmed) return DEFAULT_API_BASE;
|
|
8950
|
+
return trimmed.replace(/\/+$/, "") || DEFAULT_API_BASE;
|
|
8951
|
+
}
|
|
8952
|
+
/**
|
|
8953
|
+
* 当前 API 端点 base(ky `prefix`,恒不以 `/` 结尾)。
|
|
8954
|
+
*
|
|
8955
|
+
* 每次调用都重新读取——不做模块级缓存(与 `resolveSiteBase()` 同理:
|
|
8956
|
+
* 测试需要在不重启模块的情况下模拟不同部署)。
|
|
8957
|
+
*/
|
|
8958
|
+
function resolveApiBase() {
|
|
8959
|
+
const injected = globalThis.__OJ_API_BASE__;
|
|
8960
|
+
if (typeof injected === "string") return normalizeApiBase(injected);
|
|
8961
|
+
return normalizeApiBase("/api");
|
|
8962
|
+
}
|
|
8963
|
+
var DEFAULT_API_BASE;
|
|
8964
|
+
var init_api_base = __esmMin((() => {
|
|
8965
|
+
DEFAULT_API_BASE = "/api";
|
|
8966
|
+
}));
|
|
8967
|
+
//#endregion
|
|
8880
8968
|
//#region src/utils/request/error-response.ts
|
|
8881
8969
|
/**
|
|
8882
8970
|
* 处理错误响应
|
|
@@ -8950,13 +9038,24 @@ var init_global_progress = __esmMin((() => {
|
|
|
8950
9038
|
}));
|
|
8951
9039
|
//#endregion
|
|
8952
9040
|
//#region src/utils/remember-route/index.ts
|
|
9041
|
+
/**
|
|
9042
|
+
* 回跳参数取**路由空间**路径(剥掉站点 base)。
|
|
9043
|
+
*
|
|
9044
|
+
* 消费端(login 模块 `navigate(getRedirectPath(search))`)走的是 react-router
|
|
9045
|
+
* navigate——basename 会被再补一次。这里若直接用 `window.location.pathname`
|
|
9046
|
+
* (URL 空间,含 `/oj` 前缀),子路径部署下登录后落 `/oj/oj/home`(base 双拼,
|
|
9047
|
+
* 2026-10 子路径部署实测回归)。goLogin 的硬跳转负责补 base,本函数只管剥。
|
|
9048
|
+
*/
|
|
8953
9049
|
function rememberRoute() {
|
|
8954
9050
|
const { pathname, search } = window.location;
|
|
8955
|
-
|
|
9051
|
+
const base = resolveSiteBase();
|
|
9052
|
+
const routePath = base === "/" ? pathname : `/${pathname.slice(base.length)}`;
|
|
9053
|
+
if (routePath.length > 1 && routePath !== "/login") return `?redirect=${routePath}${search}`;
|
|
8956
9054
|
return "";
|
|
8957
9055
|
}
|
|
8958
9056
|
var init_remember_route = __esmMin((() => {
|
|
8959
9057
|
init_extra_info();
|
|
9058
|
+
init_site_base();
|
|
8960
9059
|
}));
|
|
8961
9060
|
//#endregion
|
|
8962
9061
|
//#region src/utils/request/go-login.ts
|
|
@@ -9102,6 +9201,7 @@ var init_request = __esmMin((() => {
|
|
|
9102
9201
|
init_auth();
|
|
9103
9202
|
init_header_provider();
|
|
9104
9203
|
init_preferences$3();
|
|
9204
|
+
init_api_base();
|
|
9105
9205
|
init_constants$3();
|
|
9106
9206
|
init_error_response();
|
|
9107
9207
|
init_global_progress();
|
|
@@ -9110,7 +9210,7 @@ var init_request = __esmMin((() => {
|
|
|
9110
9210
|
init_refresh();
|
|
9111
9211
|
init_whitelist();
|
|
9112
9212
|
defaultConfig = {
|
|
9113
|
-
prefix:
|
|
9213
|
+
prefix: resolveApiBase(),
|
|
9114
9214
|
timeout: 1e4,
|
|
9115
9215
|
retry: { limit: 3 },
|
|
9116
9216
|
hooks: {
|
|
@@ -9678,9 +9778,10 @@ function BasicContent(props) {
|
|
|
9678
9778
|
//#endregion
|
|
9679
9779
|
//#region src/components/basic-form/form-items/form-avatar-item.tsx
|
|
9680
9780
|
init_api_provider();
|
|
9781
|
+
init_api_base();
|
|
9681
9782
|
function FormAvatarItem({ value, onChange }) {
|
|
9682
9783
|
const uploadProvider = getUploadApiProvider();
|
|
9683
|
-
const uploadAction = uploadProvider?.action ??
|
|
9784
|
+
const uploadAction = uploadProvider?.action ?? `${resolveApiBase()}/upload`;
|
|
9684
9785
|
const uploadHeaders = uploadProvider?.headers() ?? { authorization: "authorization-text" };
|
|
9685
9786
|
return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("div", {
|
|
9686
9787
|
className: "flex items-center gap-5",
|
|
@@ -28102,4 +28203,4 @@ init_ri();
|
|
|
28102
28203
|
init_menu_icons();
|
|
28103
28204
|
init_tree();
|
|
28104
28205
|
//#endregion
|
|
28105
|
-
export { AccessControl, AccessControlRoles, AntdApp, BasicButton, BasicContent, BasicTable, EmbeddedIcon, ErrorBoundary, ExternalIcon, FormAvatarItem, FormTreeItem, Iframe, LayoutCenterIcon, LayoutEffects, LayoutLeftIcon, LayoutRightIcon, MixedNavigationIcon, OutsidePageIcon, PageError, ProfileCardIcon, RiAccountCircleLine, RiContrastFill, RiFullscreenExitLine, RiFullscreenLine, RiMailCheckLine, RiMoonIcon, RiReactjsLine, RiSunIcon, RiUserSettingsLine, ServerErrorIcon, SideNavigationIcon, TopNavigationIcon, TwoColumnNavigationIcon, accessControlCodes, bindingDiff, canAssignRole, canDeleteRole, canEditRoleBindings, defineModule, fetchAddMenuItem, fetchAddRoleItem, fetchAsyncRoutes, fetchChangePassword, fetchDeleteMenuItem, fetchDeleteRoleItem, fetchLdapLogin, fetchLogin, fetchLoginMethods, fetchLogout, fetchMenuByRoleId, fetchMenuList, fetchRbacAdminResetPassword, fetchRbacContactDetail, fetchRbacContacts, fetchRbacCreateRole, fetchRbacCreateUser, fetchRbacDeleteRole, fetchRbacOplog, fetchRbacOplogPost, fetchRbacPermissionList, fetchRbacRoleList, fetchRbacSetForbidden, fetchRbacSettings, fetchRbacUpdateBindings, fetchRbacUpdateSetting, fetchRbacUpdateUserRoles, fetchRbacUserList, fetchRefreshToken, fetchRoleList, fetchRoleMenu, fetchSolutionRoutes, fetchUpdateMenuItem, fetchUpdateRoleItem, fetchUserInfo, filterTree, getAllExpandedKeys, getAppInfo, getBooleanOptions, getModule, getModules, getRedirectPath, getRegisteredApiPrefix, getRegisteredStore, getRoutes, getYesNoOptions, groupByModule, groupSettingsByModule, handleTree, loadAll, mapTree, menuIcons, notify, permissionPrefix, request, resolveSiteBase, roleLabel, setupI18n, traverseTreeValues, unloadModule, useAccess, useAuthStore, usePreferences, useSlotNodes, useSolutionRoutes, useTabKey, useTabsStore, useUserStore, withSiteBase, external_exports as z };
|
|
28206
|
+
export { AccessControl, AccessControlRoles, AntdApp, BasicButton, BasicContent, BasicTable, EmbeddedIcon, ErrorBoundary, ExternalIcon, FormAvatarItem, FormTreeItem, Iframe, LayoutCenterIcon, LayoutEffects, LayoutLeftIcon, LayoutRightIcon, MixedNavigationIcon, OutsidePageIcon, PageError, ProfileCardIcon, RiAccountCircleLine, RiContrastFill, RiFullscreenExitLine, RiFullscreenLine, RiMailCheckLine, RiMoonIcon, RiReactjsLine, RiSunIcon, RiUserSettingsLine, ServerErrorIcon, SideNavigationIcon, TopNavigationIcon, TwoColumnNavigationIcon, accessControlCodes, bindingDiff, canAssignRole, canDeleteRole, canEditRoleBindings, defineModule, fetchAddMenuItem, fetchAddRoleItem, fetchAsyncRoutes, fetchChangePassword, fetchDeleteMenuItem, fetchDeleteRoleItem, fetchLdapLogin, fetchLogin, fetchLoginMethods, fetchLogout, fetchMenuByRoleId, fetchMenuList, fetchRbacAdminResetPassword, fetchRbacContactDetail, fetchRbacContacts, fetchRbacCreateRole, fetchRbacCreateUser, fetchRbacDeleteRole, fetchRbacOplog, fetchRbacOplogPost, fetchRbacPermissionList, fetchRbacRoleList, fetchRbacSetForbidden, fetchRbacSettings, fetchRbacUpdateBindings, fetchRbacUpdateSetting, fetchRbacUpdateUserRoles, fetchRbacUserList, fetchRefreshToken, fetchRoleList, fetchRoleMenu, fetchSolutionRoutes, fetchUpdateMenuItem, fetchUpdateRoleItem, fetchUserInfo, filterTree, getAllExpandedKeys, getAppInfo, getBooleanOptions, getHostRoutes, getModule, getModuleRoutePrefix, getModules, getRedirectPath, getRegisteredApiPrefix, getRegisteredStore, getRoutes, getYesNoOptions, groupByModule, groupSettingsByModule, handleTree, loadAll, mapTree, menuIcons, notify, permissionPrefix, request, resolveSiteBase, roleLabel, setupI18n, traverseTreeValues, unloadModule, useAccess, useAuthStore, usePreferences, useSlotNodes, useSolutionRoutes, useTabKey, useTabsStore, useUserStore, withSiteBase, external_exports as z };
|
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-mh43xEkINEUP2sZNPLqVdA=='; 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="mh43xEkINEUP2sZNPLqVdA==">{
|
|
11
11
|
"imports": {
|
|
12
12
|
"react": "/assets/react.js",
|
|
13
13
|
"react/jsx-runtime": "/assets/jsx-runtime.js",
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
"dayjs/locale/zh-cn": "/assets/ojm-dayjs-locale-zh-cn.js"
|
|
134
134
|
}
|
|
135
135
|
}</script>
|
|
136
|
-
<script type="module" crossorigin src="/assets/index-
|
|
136
|
+
<script type="module" crossorigin src="/assets/index-CtTVbohp.js"></script>
|
|
137
137
|
</head>
|
|
138
138
|
<body>
|
|
139
139
|
<div id="root"></div>
|
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.28",
|
|
11
|
+
"@oj-module/runtime/contract": "0.2.28",
|
|
12
|
+
"@oj-module/runtime/contract/errors": "0.2.28",
|
|
13
13
|
"@plane/kit": "0.1.20",
|
|
14
14
|
"@plane/editor": "0.1.20",
|
|
15
15
|
"antd": "6.6.1",
|
package/src/build.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Plugin as EsbuildPlugin } from "esbuild";
|
|
2
2
|
import type { Plugin } from "vite";
|
|
3
|
+
import type { ProjectLayout } from "./layout";
|
|
3
4
|
import type { RouteDefinitionLike } from "./manifest-routes-derive";
|
|
4
5
|
import { execFileSync } from "node:child_process";
|
|
5
6
|
import crypto from "node:crypto";
|
|
@@ -10,9 +11,10 @@ import { pathToFileURL } from "node:url";
|
|
|
10
11
|
import tailwindcss from "@tailwindcss/vite";
|
|
11
12
|
import { build as esbuild } from "esbuild";
|
|
12
13
|
import { build } from "vite";
|
|
14
|
+
import { parse as parseYaml } from "yaml";
|
|
13
15
|
import { ALIAS_PATHS_FILE, syncAliasPaths } from "./alias-paths";
|
|
14
16
|
import { assertModuleList, loadSiteConfig, mergeModuleList, resolveModuleEntry } from "./config";
|
|
15
|
-
import { resolveConfigPath, resolveLayout, scanModules, tryResolveLayout } from "./layout";
|
|
17
|
+
import { moduleDir, resolveConfigPath, resolveLayout, scanModules, tryResolveLayout } from "./layout";
|
|
16
18
|
import { requireModuleRootCapability } from "./oj-caps";
|
|
17
19
|
import { isSharedDep, SHARED_DEPS } from "./shared-deps";
|
|
18
20
|
import { normalizeSiteBase, rewriteIndexHtml } from "./site-base";
|
|
@@ -154,6 +156,9 @@ export const defineGuard = (...a) => a[a.length - 1];
|
|
|
154
156
|
export const getModules = () => [];
|
|
155
157
|
export const getModule = () => undefined;
|
|
156
158
|
export const getRoutes = () => [];
|
|
159
|
+
export const getHostRoutes = () => [];
|
|
160
|
+
// 模块生效 web 路由前缀段取值口(R1.3:写进 modules.json 的 routePrefix,未声明回落 name)
|
|
161
|
+
export const getModuleRoutePrefix = _fn;
|
|
157
162
|
export const getRegisteredStore = () => undefined;
|
|
158
163
|
export const getRegisteredApiPrefix = () => undefined;
|
|
159
164
|
export const loadAll = async () => [];
|
|
@@ -341,6 +346,37 @@ export interface BuiltModule {
|
|
|
341
346
|
integrity: string
|
|
342
347
|
css: string[]
|
|
343
348
|
chunks: ChunkEntry[]
|
|
349
|
+
/**
|
|
350
|
+
* 生效的 web 路由前缀段(契约 C2-14:`web.route_prefix` 缺省 `/<name>`)。
|
|
351
|
+
* 运行期据其改写模块路由根(单根约定下仅根段),缺省 = name 保证零漂移。
|
|
352
|
+
*/
|
|
353
|
+
routePrefix: string
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* 解析模块生效的 web 路由前缀段(R1.1)。
|
|
358
|
+
* module-root:读 `src/<name>/solution.yaml` 的 `web.route_prefix`(契约 C2-14,缺省 `/<name>`);
|
|
359
|
+
* 未配置/非法/非 module-root → 缺省 `name`,与现有路由根(顶层段即 name)逐字节一致。
|
|
360
|
+
*/
|
|
361
|
+
function resolveRoutePrefix(layout: ProjectLayout, name: string): string {
|
|
362
|
+
if (layout.kind !== "module-root")
|
|
363
|
+
return name;
|
|
364
|
+
const solPath = path.join(moduleDir(layout, name), "solution.yaml");
|
|
365
|
+
if (!fs.existsSync(solPath))
|
|
366
|
+
return name;
|
|
367
|
+
try {
|
|
368
|
+
const doc = parseYaml(fs.readFileSync(solPath, "utf-8")) as Record<string, unknown>;
|
|
369
|
+
const web = doc.web;
|
|
370
|
+
if (web && typeof web === "object" && !Array.isArray(web)) {
|
|
371
|
+
const rp = (web as Record<string, unknown>).route_prefix;
|
|
372
|
+
if (typeof rp === "string" && /^\/[a-z][a-z0-9-]{0,63}$/.test(rp))
|
|
373
|
+
return rp.slice(1);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
// YAML 解析失败 → 缺省,不阻断构建(契约校验会在别处暴露)
|
|
378
|
+
}
|
|
379
|
+
return name;
|
|
344
380
|
}
|
|
345
381
|
|
|
346
382
|
const ALGORITHM = "sha384";
|
|
@@ -565,19 +601,22 @@ export async function buildBackend(projectRoot: string): Promise<boolean> {
|
|
|
565
601
|
* dev 的 / 与 /assets/* 直接服务 shell dist,合并产物无人消费。
|
|
566
602
|
*
|
|
567
603
|
* siteBase 非空时(站点挂载在 `/oj` 这类子路径)改写 index.html 的站内绝对
|
|
568
|
-
* URL
|
|
569
|
-
*
|
|
604
|
+
* URL;apiBase 非空时注入 `window.__OJ_API_BASE__`(API 挂载前缀部署期可配,
|
|
605
|
+
* 见 site-base.ts)。两者都为空则产物与既有行为逐字节一致。
|
|
570
606
|
*/
|
|
571
|
-
function mergeShellSite(distDir: string, siteBase: string): void {
|
|
607
|
+
function mergeShellSite(distDir: string, siteBase: string, apiBase: string): void {
|
|
572
608
|
const shellDist = resolveShellDist();
|
|
573
609
|
fs.rmSync(distDir, { recursive: true, force: true });
|
|
574
610
|
fs.mkdirSync(path.dirname(distDir), { recursive: true });
|
|
575
611
|
fs.cpSync(shellDist, distDir, { recursive: true });
|
|
576
612
|
|
|
577
613
|
const indexPath = path.join(distDir, "index.html");
|
|
578
|
-
if (normalizeSiteBase(siteBase) !== "/") {
|
|
579
|
-
fs.writeFileSync(indexPath, rewriteIndexHtml(fs.readFileSync(indexPath, "utf-8"), siteBase));
|
|
580
|
-
|
|
614
|
+
if (normalizeSiteBase(siteBase) !== "/" || apiBase.trim()) {
|
|
615
|
+
fs.writeFileSync(indexPath, rewriteIndexHtml(fs.readFileSync(indexPath, "utf-8"), siteBase, apiBase));
|
|
616
|
+
if (normalizeSiteBase(siteBase) !== "/")
|
|
617
|
+
console.log(`[ojm] 站点 index.html 已按 siteBase=${normalizeSiteBase(siteBase)} 重写(子路径部署)`);
|
|
618
|
+
if (apiBase.trim())
|
|
619
|
+
console.log(`[ojm] API 前缀已注入 index.html:${apiBase.trim().replace(/\/+$/, "")}(apiBase)`);
|
|
581
620
|
}
|
|
582
621
|
console.log(`[ojm] 已合并宿主站点 → ${distDir}`);
|
|
583
622
|
}
|
|
@@ -607,7 +646,7 @@ export async function buildModules(
|
|
|
607
646
|
const baseUrl = config.baseUrl ?? "";
|
|
608
647
|
|
|
609
648
|
if (opts.mergeSite)
|
|
610
|
-
mergeShellSite(outDir, config.siteBase ?? "");
|
|
649
|
+
mergeShellSite(outDir, config.siteBase ?? "", config.apiBase ?? "");
|
|
611
650
|
|
|
612
651
|
// P4.5 / C4 / D12:构建前先过版本矩阵门禁,版本漂移直接拒绝
|
|
613
652
|
checkSharedVersions(projectRoot, resolveShellDist());
|
|
@@ -756,6 +795,7 @@ export async function buildModules(
|
|
|
756
795
|
integrity: sha384(path.join(moduleOutDir, "entry.js")),
|
|
757
796
|
css,
|
|
758
797
|
chunks,
|
|
798
|
+
routePrefix: resolveRoutePrefix(layout, definition.name),
|
|
759
799
|
});
|
|
760
800
|
}
|
|
761
801
|
|
package/src/bundle.ts
CHANGED
|
@@ -37,7 +37,7 @@ import { readModuleDefinition } from "./build";
|
|
|
37
37
|
import { readBuiltinModules } from "./builtin";
|
|
38
38
|
import { createDeterministicTgz } from "./deterministic-tar";
|
|
39
39
|
import { apiSrcOf, resolveLayout, webSrcOf } from "./layout";
|
|
40
|
-
import { assertValidSolutionName, checkLocalesComplete, checkTablesAgainstSchema, NAME_RE, parseManifest, referencedI18nKeys, validateContract } from "./manifest-contract";
|
|
40
|
+
import { assertValidSolutionName, checkLocalesComplete, checkTablesAgainstSchema, isReferenceApiForm, NAME_RE, parseManifest, referencedI18nKeys, validateContract } from "./manifest-contract";
|
|
41
41
|
import { deriveManifestRoutes, reconcileManifestRoutes } from "./manifest-routes-derive";
|
|
42
42
|
|
|
43
43
|
export interface BundleBuildResult {
|
|
@@ -232,6 +232,13 @@ async function planBundle(projectRoot: string, layout: ProjectLayout, target: Bu
|
|
|
232
232
|
if (violations.length > 0) {
|
|
233
233
|
throw new Error(`[ojm] 方案包构建失败:manifest.yaml 契约校验未通过(${violations.length} 项违规,全量列出):\n${violations.map(x => `- ${x}`).join("\n")}`);
|
|
234
234
|
}
|
|
235
|
+
// R2.2:引用式 solution 提示——api.tables/api.deps 缺省 = 引用包内 manifest.yaml(非漏配)
|
|
236
|
+
if (isReferenceApiForm(m.api)) {
|
|
237
|
+
console.log(
|
|
238
|
+
`[ojm] 引用式方案:api.tables/api.deps 缺省,引用包内 api/${String(m.name)}-${String(m.version)}/manifest.yaml`
|
|
239
|
+
+ "(安装期 C2-31 复核其存在,依赖装配据其回读,非漏配)",
|
|
240
|
+
);
|
|
241
|
+
}
|
|
235
242
|
const version = m.version as string;
|
|
236
243
|
|
|
237
244
|
// 文件面契约(§4.1 构建列专属执行点;manifest 内在规则已在 validateContract 全量通过)
|
package/src/config.ts
CHANGED
|
@@ -26,6 +26,18 @@ export interface ModulesConfig {
|
|
|
26
26
|
* 绝对域),`siteBase` 是**站点自身**的部署位置;两者正交,可同时使用。
|
|
27
27
|
*/
|
|
28
28
|
siteBase?: string
|
|
29
|
+
/**
|
|
30
|
+
* API 端点前缀(如 `"/gw"`),留空 = 构建期默认 `/api`。
|
|
31
|
+
*
|
|
32
|
+
* oj 的 API 挂载前缀是部署期配置(`config.yaml` 的 `mounts` api 条),
|
|
33
|
+
* 而 runtime 是预构建产物——构建期内联的默认前缀改不了。API 挂载不在
|
|
34
|
+
* `/api` 时**必须**配它:`ojm build` 注入 `window.__OJ_API_BASE__`,
|
|
35
|
+
* runtime 的 ky `prefix` 运行期读它(`resolveApiBase()`),端点仍一律相对。
|
|
36
|
+
*
|
|
37
|
+
* 与 `siteBase` 正交:站点可挂 `/oj` 而 API 恒在 `/api`;也可只换 API
|
|
38
|
+
* 前缀而站点在根。允许绝对域(如 `https://api.example.com`)。
|
|
39
|
+
*/
|
|
40
|
+
apiBase?: string
|
|
29
41
|
modules?: ModuleConfigEntry[]
|
|
30
42
|
/**
|
|
31
43
|
* 站点 HTML 变换模块(可选):相对工程根的路径(`./html-transform.ts`)
|
package/src/contract/run.ts
CHANGED
|
@@ -4,11 +4,11 @@ import type { OpenApiDoc } from "./openapi-ir";
|
|
|
4
4
|
import { execFile as execFileCb } from "node:child_process";
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
|
-
import { dirname, join } from "node:path";
|
|
7
|
+
import { basename, dirname, join } from "node:path";
|
|
8
8
|
import process from "node:process";
|
|
9
9
|
import { promisify } from "node:util";
|
|
10
10
|
import { stringify as stringifyYaml } from "yaml";
|
|
11
|
-
import { apiModuleRoots, apiModulesSrc, apiSrcOf, legacyApiModuleRoots, tryResolveLayout, webSrcOf } from "../layout";
|
|
11
|
+
import { apiModuleRoots, apiModulesSrc, apiSrcOf, legacyApiModuleRoots, scanModules, tryResolveLayout, webSrcOf } from "../layout";
|
|
12
12
|
import { emitClient } from "./emit-client";
|
|
13
13
|
import { applyStubWrites, planStubWrites } from "./emit-stub";
|
|
14
14
|
import { implementedRouteKeys } from "./handler-scan";
|
|
@@ -125,7 +125,10 @@ export async function runApi(opts: { cwd: string, openapi?: string, rawPaths?: S
|
|
|
125
125
|
|
|
126
126
|
/**
|
|
127
127
|
* R5:`ojm api --docs`——聚合 openapi.json 的 paths → redoc 渲染静态站。
|
|
128
|
-
*
|
|
128
|
+
* 文档统一落 `<projectRoot>/src/docs/<name>/api`(与模块树同位、树外——不参与模块
|
|
129
|
+
* 枚举与 handler 路由扫描,杜绝 docs 被当 handler 镜像导致端点翻倍 wikiDocsDocs*)。
|
|
130
|
+
* `name` 推导:cwd 为 `src/<name>/api` 时取父目录名;module-root 单模块时取该模块名;
|
|
131
|
+
* 否则回退工程目录名。legacy 布局同样落 `src/docs/<name>/api`(位于 api/src 扫描树外)。
|
|
129
132
|
* redoc CLI 缺失时人话报错。
|
|
130
133
|
*/
|
|
131
134
|
export async function runApiDocs(cwd: string, opts: { redocBin?: string, fetchJs?: (url: string) => Promise<string> } = {}): Promise<string> {
|
|
@@ -136,9 +139,30 @@ export async function runApiDocs(cwd: string, opts: { redocBin?: string, fetchJs
|
|
|
136
139
|
const doc = JSON.parse(readFileSync(openapiPath, "utf8")) as { paths?: Record<string, Record<string, unknown>> };
|
|
137
140
|
const paths = doc.paths ?? {};
|
|
138
141
|
const layout = tryResolveLayout(cwd);
|
|
139
|
-
const isUniDev = existsSync(layout ? apiModulesSrc(layout) : join(cwd, "api/src"));
|
|
140
142
|
|
|
141
|
-
|
|
143
|
+
// 修正B:文档统一落 <projectRoot>/src/docs/<name>/api(与模块树同位、树外——
|
|
144
|
+
// 无 manifest.yaml、顶层不被 scanModuleRoot 枚举,walkApiFiles 也不命中,
|
|
145
|
+
// 不会把 docs 当 handler 路由镜像,杜绝 wikiDocsDocs* 类端点翻倍)。
|
|
146
|
+
const projectRoot = layout?.projectRoot ?? cwd;
|
|
147
|
+
let name = basename(projectRoot);
|
|
148
|
+
if (basename(cwd) === "api") {
|
|
149
|
+
// cwd 为模块 api 目录(src/<name>/api)时取父目录名为模块名
|
|
150
|
+
const candidate = basename(dirname(cwd));
|
|
151
|
+
if (layout?.kind === "module-root") {
|
|
152
|
+
const mods = scanModules(layout).map(m => m.name);
|
|
153
|
+
if (mods.includes(candidate))
|
|
154
|
+
name = candidate;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
name = candidate;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else if (layout?.kind === "module-root") {
|
|
161
|
+
const mods = scanModules(layout).map(m => m.name);
|
|
162
|
+
if (mods.length === 1)
|
|
163
|
+
name = mods[0]!;
|
|
164
|
+
}
|
|
165
|
+
const outDir = join(projectRoot, "src", "docs", name, "api");
|
|
142
166
|
mkdirSync(outDir, { recursive: true });
|
|
143
167
|
const specPath = join(outDir, "openapi.yaml");
|
|
144
168
|
writeFileSync(specPath, stringifyYaml({ openapi: "3.1.0", info: { title: "API 文档", version: "0.0.0" }, paths }));
|
package/src/manifest-contract.ts
CHANGED
|
@@ -38,6 +38,19 @@ export const RESERVED_NAMES = new Set(["_platform", "system", "auth", "market",
|
|
|
38
38
|
/** C2-02 字符集:[0-9A-Za-z.-](禁 +——oj validate_version 拒装,附录 B-1;禁 _——契约收紧) */
|
|
39
39
|
const VERSION_CHARS_RE = /^[0-9A-Z.-]+$/i;
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* 引用式 api 段判定(R2.1):`api` 段存在但 `tables` 与 `deps` 均缺省 =
|
|
43
|
+
* 引用包内 `<module>/manifest.yaml`(oj 原生七键清单,tgz 已随包携带,安装期 C2-31 复核)。
|
|
44
|
+
* 契约**显式承认**此形态(不再仅靠字段可选性「漏过」通过):安装器据 native manifest
|
|
45
|
+
* 回读做占用检测/装配(R2.2),全量 form(两字段任一给出)优先。
|
|
46
|
+
*/
|
|
47
|
+
export function isReferenceApiForm(api: unknown): boolean {
|
|
48
|
+
if (!isObj(api))
|
|
49
|
+
return false;
|
|
50
|
+
const a = api as Record<string, unknown>;
|
|
51
|
+
return a.tables === undefined && a.deps === undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
/** §2.2 范围算子语法(peer 四字段/api.deps 值共用) */
|
|
42
55
|
const RANGE_RE = /^(>=|<=|[><^=])?([0-9A-Z.-]+)$/i;
|
|
43
56
|
|
|
@@ -253,7 +266,7 @@ export function validateContract(m: unknown): string[] {
|
|
|
253
266
|
}
|
|
254
267
|
}
|
|
255
268
|
|
|
256
|
-
// ── api 段(C2-12/12a/12b/13/26)──
|
|
269
|
+
// ── api 段(C2-12/12a/12b/13/26;R2.1 引用式:tables/deps 均缺省 = 引用包内 manifest.yaml)──
|
|
257
270
|
let apiSeg = name;
|
|
258
271
|
if (m.api !== undefined) {
|
|
259
272
|
const api = m.api;
|
package/src/new.ts
CHANGED
package/src/site-base.ts
CHANGED
|
@@ -71,31 +71,45 @@ function rewriteImportmap(html: string, base: string): string {
|
|
|
71
71
|
/**
|
|
72
72
|
* 重写全站 index.html:
|
|
73
73
|
* - importmap 映射值、`<script src>`、`<link href>` 统一加 `siteBase`;
|
|
74
|
-
* - `<head>` 内注入 `window.__OJ_BASE__
|
|
75
|
-
* CSP 是 `script-src 'self' … 'nonce-xxx'
|
|
74
|
+
* - `<head>` 内注入 `window.__OJ_BASE__` / `window.__OJ_API_BASE__`
|
|
75
|
+
* (**复用 importmap 的 nonce**,CSP 是 `script-src 'self' … 'nonce-xxx'`,
|
|
76
|
+
* 无 nonce 的内联脚本会被直接拦掉)。
|
|
77
|
+
*
|
|
78
|
+
* 两者独立:siteBase 非根才重写 URL;apiBase 配了就注入(API 挂载前缀是
|
|
79
|
+
* 部署期事实,与站点位置正交)。都不涉及 → 逐字节原样返回(零回归)。
|
|
76
80
|
*/
|
|
77
|
-
export function rewriteIndexHtml(html: string, siteBase: string | undefined): string {
|
|
81
|
+
export function rewriteIndexHtml(html: string, siteBase: string | undefined, apiBase?: string): string {
|
|
78
82
|
const base = normalizeSiteBase(siteBase);
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
const api = apiBase?.trim();
|
|
84
|
+
// 根路径部署且未配 API 前缀:原样返回(保持与既有产物逐字节一致)
|
|
85
|
+
if (base === ROOT_BASE && !api)
|
|
81
86
|
return html;
|
|
82
87
|
|
|
83
88
|
const nonce = html.match(/<script type="importmap"[^>]*\snonce="([^"]*)"/)?.[1];
|
|
84
89
|
if (!nonce) {
|
|
85
90
|
throw new Error(
|
|
86
|
-
"[ojm] 站点 index.html 缺少带 nonce 的 importmap,无法注入 window.__OJ_BASE__"
|
|
91
|
+
"[ojm] 站点 index.html 缺少带 nonce 的 importmap,无法注入 window.__OJ_BASE__ / window.__OJ_API_BASE__"
|
|
87
92
|
+ "(CSP 会拦掉无 nonce 的内联脚本,子路径部署下路由 basename 会退化成 \"/\")。"
|
|
88
93
|
+ "请确认 shell-dist 是最新构建产物(pnpm build:shell)。",
|
|
89
94
|
);
|
|
90
95
|
}
|
|
91
96
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
+
const globals: string[] = [];
|
|
98
|
+
let out = html;
|
|
99
|
+
if (base !== ROOT_BASE) {
|
|
100
|
+
out = rewriteImportmap(out, base);
|
|
101
|
+
// 属性值:只要以单个 "/" 开头(排除 "//host")。importmap 是脚本正文、非属性,
|
|
102
|
+
// 上一步已处理,二者不重叠。
|
|
103
|
+
out = out.replace(/(\b(?:href|src)=")(\/[^/"][^"]*|\/")/g, (_m, attr: string, url: string) =>
|
|
104
|
+
`${attr}${withSiteBase(url, base)}`);
|
|
105
|
+
globals.push(`window.__OJ_BASE__=${JSON.stringify(base)};`);
|
|
106
|
+
}
|
|
107
|
+
if (api) {
|
|
108
|
+
// 归一为不以 / 结尾(ky v2 的 prefix 自剥尾斜杠,这里归一保证注入值确定)
|
|
109
|
+
globals.push(`window.__OJ_API_BASE__=${JSON.stringify(api.replace(/\/+$/, ""))};`);
|
|
110
|
+
}
|
|
97
111
|
|
|
98
|
-
const injected = `<script nonce="${nonce}"
|
|
112
|
+
const injected = `<script nonce="${nonce}">${globals.join("")}</script>`;
|
|
99
113
|
// 注入点选在 importmap 之前:importmap 本身必须在任何 module script 之前,
|
|
100
114
|
// 而内联 classic script 是解析期同步执行,因此宿主 main 与模块 entry 读
|
|
101
115
|
// `window.__OJ_BASE__` 时它一定已经在位。
|
|
@@ -40,9 +40,16 @@ function isObj(v: unknown): v is Record<string, unknown> {
|
|
|
40
40
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
/**
|
|
44
|
+
* manifest api.deps 的模块依赖键(排除 _platform——平台非 web 模块依赖,P2-15② extras)。
|
|
45
|
+
* 全量 form(C2 `api.deps` 给出)优先;引用式(R2.2)回退包内 native manifest.deps
|
|
46
|
+
* (oj 原生七键清单,tgz 随包携带,extract 段已解析留存 ctx.nativeManifest)。
|
|
47
|
+
*/
|
|
48
|
+
function apiDepsOf(m: Record<string, unknown>, native?: unknown): string[] {
|
|
49
|
+
const c2 = isObj(m.api) && isObj((m.api as Record<string, unknown>).deps)
|
|
50
|
+
? (m.api as Record<string, unknown>).deps as Record<string, unknown>
|
|
51
|
+
: undefined;
|
|
52
|
+
const deps = c2 ?? (isObj(native) && isObj((native as Record<string, unknown>).deps) ? (native as Record<string, unknown>).deps as Record<string, unknown> : {});
|
|
46
53
|
return Object.keys(deps).filter(k => k !== "_platform").sort();
|
|
47
54
|
}
|
|
48
55
|
|
|
@@ -157,7 +164,8 @@ export async function deployWeb(ctx: DeployWebContext): Promise<StageResult> {
|
|
|
157
164
|
// P2-15②(G7 defer-P2 承载):完整条目 extras——version/dependencies/chunks 补齐
|
|
158
165
|
// (deployWeb 全新生成 entry,构建器条目不透传的缺口;宿主 HostModule 全字段消费面)
|
|
159
166
|
version: ctx.version,
|
|
160
|
-
|
|
167
|
+
// R2.2:引用式 solution 的 dependencies 据包内 native manifest 回读装配
|
|
168
|
+
...(apiDepsOf(m, ctx.nativeManifest).length > 0 ? { dependencies: apiDepsOf(m, ctx.nativeManifest) } : {}),
|
|
161
169
|
chunks: [], // 单 entry 产物无懒加载 chunk(宿主 collectPreloads 消费 `?? []`,字段形状齐备)
|
|
162
170
|
...(Array.isArray(web.css) ? { css: web.css.map(c => posixPath(String(c))) } : {}),
|
|
163
171
|
...(integrity !== undefined ? { integrity } : {}),
|
|
@@ -189,6 +189,8 @@ async function checkApiSegment(ctx: ExtractContext): Promise<{ ok: true } | { ok
|
|
|
189
189
|
catch (e) {
|
|
190
190
|
return { ok: false, message: `解压复核失败:api/${ctx.name}-${ctx.version}/manifest.yaml 不可解析(${String(e)})——C2-31` };
|
|
191
191
|
}
|
|
192
|
+
// R2.2:引用式 solution 的 tables/deps 真源——保留 native manifest 供 deployWeb 回读装配。
|
|
193
|
+
ctx.nativeManifest = native;
|
|
192
194
|
return checkModuleManifest(ctx.manifest, native, ctx.name, ctx.version);
|
|
193
195
|
}
|
|
194
196
|
|
|
@@ -9,6 +9,11 @@ import type { StateStore } from "./state";
|
|
|
9
9
|
export interface InstallContext {
|
|
10
10
|
/** 已解析的 manifest.yaml(安装器读盘解析产物) */
|
|
11
11
|
manifest: unknown
|
|
12
|
+
/**
|
|
13
|
+
* 包内 oj 原生清单(C2-31 复核解析产物;引用式 solution 的 tables/deps 真源)。
|
|
14
|
+
* web-only 包(无 api 段)保持 undefined;引用式方案的依赖装配据其回读(R2.2)。
|
|
15
|
+
*/
|
|
16
|
+
nativeManifest?: unknown
|
|
12
17
|
/** 安装目标身份(与 manifest.name/version 同源;manifest 字段缺失/非字符串时兜底) */
|
|
13
18
|
name: string
|
|
14
19
|
version: string
|
|
@@ -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.
|
|
12
|
-
"@oj-module/runtime/contract": "0.2.
|
|
13
|
-
"@oj-module/runtime/contract/errors": "0.2.
|
|
11
|
+
"@oj-module/runtime": "0.2.28",
|
|
12
|
+
"@oj-module/runtime/contract": "0.2.28",
|
|
13
|
+
"@oj-module/runtime/contract/errors": "0.2.28",
|
|
14
14
|
"@plane/kit": "0.1.20",
|
|
15
15
|
"@plane/editor": "0.1.20",
|
|
16
16
|
"antd": "6.6.1",
|