@oj-module/cli 0.2.50 → 0.2.53

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.
Files changed (49) hide show
  1. package/devkit/wecom-internal-app.md +111 -4
  2. package/package.json +2 -2
  3. package/shell-dist/assets/{index-FYt9CjFd.js → index-e_Mp2mb5.js} +5 -1
  4. package/shell-dist/assets/runtime.js +24 -3
  5. package/shell-dist/index.html +9 -12
  6. package/shell-dist/versions.json +3 -3
  7. package/src/build.ts +19 -2
  8. package/src/shared-deps.ts +27 -9
  9. package/src/site-base.ts +1 -1
  10. package/templates/init.include.yaml +4 -7
  11. package/templates/openapi.json +2520 -2412
  12. package/templates/src/_platform/api/_shared/service/domain/users.ts +40 -4
  13. package/templates/src/auth/api/_shared/wecom/admin.ts +4 -0
  14. package/templates/src/auth/api/_shared/wecom/client.ts +9 -2
  15. package/templates/src/auth/api/_shared/wecom/events.ts +3 -0
  16. package/templates/src/auth/api/_shared/wecom/tenant-admin.ts +194 -0
  17. package/templates/src/auth/api/wecom/admin/tenant-admin/api.ts +41 -0
  18. package/templates/src/auth/api/wecom/admin/tenant-admin/password/api.ts +66 -0
  19. package/templates/src/login/web/pages/login.tsx +54 -24
  20. package/templates/src/system/api/settings/api.ts +3 -3
  21. package/templates/src/system/web/entry.ts +4 -4
  22. package/templates/src/system/web/locales/en-US.json +14 -0
  23. package/templates/src/system/web/locales/zh-CN.json +14 -0
  24. package/templates/src/system/web/pages/users/index.tsx +7 -4
  25. package/templates/src/system/web/pages/users/roles.tsx +14 -3
  26. package/templates/src/system/web/pages/wecom/index.tsx +130 -1
  27. package/templates/tsconfig.paths.json +0 -1
  28. package/templates/web.config.ts +1 -2
  29. package/vendor/host-versions.json +3 -3
  30. package/shell-dist/assets/echarts-charts.js +0 -58571
  31. package/shell-dist/assets/echarts-features.js +0 -24057
  32. package/shell-dist/assets/echarts-for-react.js +0 -704
  33. package/shell-dist/assets/echarts.js +0 -82916
  34. package/shell-dist/assets/editor.js +0 -161457
  35. package/shell-dist/assets/vditor-index-css.js +0 -7
  36. package/shell-dist/assets/vditor.js +0 -15223
  37. package/templates/src/demo/api/manifest.yaml +0 -5
  38. package/templates/src/demo/api/migrations/0001__create_todos.sql +0 -9
  39. package/templates/src/demo/api/migrations/0002__tenant.mysql.sql +0 -2
  40. package/templates/src/demo/api/migrations/0002__tenant.sql +0 -2
  41. package/templates/src/demo/api/schema.yaml +0 -12
  42. package/templates/src/demo/api/seed.sql +0 -5
  43. package/templates/src/demo/api/todos/api.ts +0 -59
  44. package/templates/src/demo/web/__client/api.schemas.ts +0 -20
  45. package/templates/src/demo/web/__client/api.ts +0 -89
  46. package/templates/src/demo/web/entry.ts +0 -42
  47. package/templates/src/demo/web/locales/en-US.json +0 -10
  48. package/templates/src/demo/web/locales/zh-CN.json +0 -10
  49. package/templates/src/demo/web/pages/index.tsx +0 -45
@@ -13,9 +13,9 @@
13
13
 
14
14
  | 你是 | 读 | 大约要做什么 |
15
15
  |---|---|---|
16
- | 部署 / 运维 | §2 §3 §4 §7 §8 | 备凭据、填 `config.yaml`、在后台点「验证并接入」、日常启停与轮换;真机验收照 **§7.5** 打勾 |
16
+ | 部署 / 运维 | §2 §3 §4 §7 §8 **§9** | 备凭据、填 `config.yaml`、在后台点「验证并接入」、日常启停与轮换;真机验收照 **§7.5** 打勾 |
17
17
  | 企业管理员(客户 IT) | §2.3 §3 §4.3 §7.5 | 在自家企微后台建自建应用、交出三个值、配可信 IP / 可信域名、把可见范围设成全公司、可选配通讯录事件回调 |
18
- | 本工程开发者 | §1 §6 §8 | 本地联调、加第二套凭据、写没有真实企业的自动化测试 |
18
+ | 本工程开发者 | §1 §6 §8 **§9** | 本地联调、加第二套凭据、写没有真实企业的自动化测试 |
19
19
  | 企业员工(用户) | §5 | 怎么进、要不要注册、进不去怎么办 |
20
20
 
21
21
  ---
@@ -589,7 +589,114 @@ wecom_allow_coexist: 'true' # 取值 1 / true / yes
589
589
 
590
590
  ---
591
591
 
592
- ## 9. 故障速查
592
+ ## 9. 租户与管理员模型(任命口径)
593
+
594
+ 这一节回答「接进来之后,**谁来管这家企业的数据**」。一句话:**租户键 = 企业 ID;
595
+ superadmin 是平台级的、只留一个;admin 是租户级的、每租户一个。**
596
+
597
+ ### 9.1 租户键就是企业 ID
598
+
599
+ - `tenantOfCorp(corpId)` 直接返回 `corpId`(`_shared/wecom/store.ts`)。
600
+ - 同一企业的**多个应用**(测试 / 正式、迁移期代开发与自建并存)**共用同一租户**——这是刻意
601
+ 的:企微的 `userid` 是**企业级唯一**,同一个人从不同应用进来就是同一个人(`wecom:<userid>`
602
+ 同一个账号),组织架构也是企业级的同一份。
603
+ - 需要按应用隔离的东西,在**应用这一维**已经隔离了:`wecom_app_profile` 主键
604
+ `(suite_id, corp_id, agent_id)`;同步任务 / 锁按 `corp_id + agent_id`(`jobScope`);
605
+ 登录链接带 `corp` + `agent`。
606
+ - 所以「同企业多应用」不需要、也**不该**拆成多个租户(拆了会把同一个人变成两个账号、
607
+ 组织树存两份)。
608
+
609
+ ### 9.2 角色定义全局唯一,角色授予按租户
610
+
611
+ | 表 | 是否带 `tenant_id` | 含义 |
612
+ |---|---|---|
613
+ | `sys_role` / `sys_permission` / `sys_role_permission` | **无** | 全站只有一套角色定义:superadmin / admin / editor / viewer(`framework` 内置) |
614
+ | `sys_user_role` | **有** | 授予是**按租户**的 |
615
+
616
+ 也就是说:不可能「每租户定义一套角色」,但可以「每租户各授一个 superadmin」——**但不要这么做**,理由见下。
617
+
618
+ ### 9.3 superadmin:平台级,建议只留一个(在 `default` 租户)
619
+
620
+ - `assertSuperAdmin()` 只判「当前用户在本租户有没有 superadmin」(`_platform/.../access.ts`)。
621
+ - 而 `/system/wecom` 每个端点都过这道闸,且它是**跨租户**的:`listApps()` 列出**所有企业**
622
+ 的档案。
623
+ - **推论**:任何租户里只要有人握着 superadmin,他带自己租户的头访问 `/system/wecom` 就能
624
+ 看到别家企业的档案 → 跨租户越权。
625
+ - 所以:superadmin = 平台级、唯一、只存在于 `default`(平台)租户。代码**没有强制**这条
626
+ (是运维纪律,不是代码约束)——别给企业租户的人授 superadmin。
627
+
628
+ ### 9.4 admin:租户级,每租户一个(自动开通)
629
+
630
+ - 企业租户是企微登录时由 `ensureTenant()` **隐式具象化**的(没有「建租户」这个动作),
631
+ 里面只有 JIT 出来的 `wecom:<userid>` 员工号,**默认一个管理员都没有** → 没人能做组织架构
632
+ 同步,也没人能在该租户内授权。
633
+ - 开通时机:自建「验证并接入」(`connectCorp`)与代开发授权(`create_auth`)各挂一次
634
+ `provisionTenantAdmin()`,幂等。
635
+ - **尽力而为**(失败只告警,不打断调用方):`create_auth` 有约 1 秒响应窗口,bcrypt 不该
636
+ 有机会拖垮回调;`connectCorp` 走到那步档案已落库,抛错只会让界面显示「接入失败」。
637
+ - **补开通道**:superadmin 在 `/system/wecom` → 「租户管理员」→ 重设口令(内部会先
638
+ `ensureTenantAdmin`)。老租户(本功能上线前就接入的)走这条路补开。
639
+ - 账号名固定 `admin`;初始口令**随机且无人知晓**(只存 bcrypt),`must_change=1`。
640
+ - **口令不存储**:superadmin 重设时**自己输入**新口令 → 他知道 → 线下告知对方。系统没有
641
+ 任何可逆副本,所以「回显已有口令」物理上做不到(bcrypt 单向)。
642
+ - 实现:`auth/api/_shared/wecom/tenant-admin.ts`,是**全仓唯一使用 `db.asSystem()` 的地方**
643
+ (平台请求绑在 `default`,而 `db.asTenant` 是「请求级、只能设一次」,没有任何正常通道能写
644
+ 企业租户)。框架注释明写「业务 handler 不得使用」,此处是有意识且被审计的**单一例外**,
645
+ **不得复制到别处**。
646
+
647
+ ### 9.5 任命 admin 已下放到租户内
648
+
649
+ `assertMembershipAllowed()` 按「被授予的角色」分层,不再一刀切:
650
+
651
+ | 授予的角色 | 谁能授 |
652
+ |---|---|
653
+ | `superadmin` | **仅 superadmin**(平台级、跨租户) |
654
+ | `admin` | 本租户内握有 `admin` **或** `superadmin` 的人(已下放) |
655
+ | `editor` / `viewer` 等 | 不校验(原行为) |
656
+
657
+ - **为什么必须下放**:平台 superadmin 因租户隔离**查不到**企业租户的用户(实测
658
+ `PUT /system/users/roles?uid=` 返回 `404 user not found`)。若 admin 任命也只许
659
+ superadmin,企业租户就**永远配不出管理员**——两头都够不着。
660
+ - **为什么这样是安全的**:判定侧 `rolesOf()` 读 `sys_user_role` 走请求租户注入,写入侧同样
661
+ 带 `tenant_id`,所以租户 admin 只能影响**自己租户**,越不到别家;而 `superadmin` 握跨租户
662
+ 权限,仍只许 superadmin 授。
663
+ - 前端:成员资格入口对 `admin` 也可见;非 superadmin 时 `superadmin` 选项**置灰**(提交后吃
664
+ 403 太晚也太含糊)。
665
+
666
+ > ⚠️ **首次登录必须先改密**:`must_change=1` 期间 `currentAccess()` 直接抛 `403 must_change`,
667
+ > **任何**走 `assertSystemAdmin()` 的动作(包括任命角色)都会被拦。管理员登录后会拿到
668
+ > `restricted` 会话,先改密才能做管理操作——这是预期行为,不是故障。
669
+
670
+ ### 9.6 审计:任命留痕带租户维度
671
+
672
+ `user.roles.update` 的 `detail`:
673
+
674
+ | 字段 | 说明 |
675
+ |---|---|
676
+ | `added` / `removed` | 本次增 / 删的角色 |
677
+ | `tenant` | **租户维度**(原本只有隐式的 `sys_oplog.tenant_id` 列) |
678
+ | `actorUid` / `targetUid` | 双方 uid |
679
+ | `elevated` | 是否牵涉 `admin` / `superadmin`(授予**或撤销**都算),便于直接筛「任命」类条目 |
680
+
681
+ 严重级:牵涉 `admin` / `superadmin` → `level=4`(warning,与「重设租户管理员口令」同级);
682
+ 普通角色变更 → `6`(info),避免审计被噪声淹没。
683
+
684
+ > 为什么要显式记 `tenant` + 双方 uid:各租户的管理员**用户名都叫 `admin`**,撞车。审计里
685
+ > `username='admin'` 既可能是 `default` 租户的平台 admin,也可能是某个企业租户的管理员,
686
+ > 只记 username 分不清「哪个租户的 admin 任命了谁」。
687
+
688
+ ### 9.7 登录时要选租户
689
+
690
+ - 登录页「企业代码」字段默认收起;**开了企业微信登录就默认展开**(判据:
691
+ `GET /system/login-methods` 的 `methods` 含 `wecom`)。
692
+ - 判据为什么不是「数租户」:匿名请求没有租户上下文,统计租户要动用 `db.asSystem`
693
+ (不为此破 §9.4 的「仅此一例」);而「开了企微登录 ⇒ 存在企业租户」语义成立。
694
+ - 运维填 `corp_id` + `admin` + 口令即可登录到该租户做管理;员工走企微登录(`?tenant=`
695
+ 自动回填,无需手填)。
696
+
697
+ ---
698
+
699
+ ## 10. 故障速查
593
700
 
594
701
  | 现象 / 文案 | 病根 | 处置 |
595
702
  |---|---|---|
@@ -628,7 +735,7 @@ wecom_allow_coexist: 'true' # 取值 1 / true / yes
628
735
 
629
736
  ---
630
737
 
631
- ## 10. 一分钟自检
738
+ ## 11. 一分钟自检
632
739
 
633
740
  ```text
634
741
  [ ] 五项 wecom_corp_* + wecom_enc_key + wecom_base_url 非空;代开发那几项留空(或显式开共存)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oj-module/cli",
3
3
  "type": "module",
4
- "version": "0.2.50",
4
+ "version": "0.2.53",
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.50",
52
+ "@oj-module/runtime": "0.2.53",
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, getHostRoutes, getRoutes, loadAll, setupI18n, useAccessStore, useAuthStore, usePreferences, useUserStore } from "@oj-module/runtime";
2
+ import { AntdApp, ErrorBoundary, LayoutEffects, PageError, getHostRoutes, getRoutes, loadAll, resolveApiBase, setupI18n, useAccessStore, 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";
@@ -312,6 +312,10 @@ function Boot() {
312
312
  let cancelled = false;
313
313
  (async () => {
314
314
  try {
315
+ if (typeof navigator !== "undefined" && /wxwork/i.test(navigator.userAgent) && location.pathname.endsWith("/login") && !new URLSearchParams(location.search).get("wecom_ticket")) {
316
+ window.location.assign(resolveApiBase() + "/auth/wecom/login");
317
+ return;
318
+ }
315
319
  ensureI18n();
316
320
  const base = resolveSiteBase();
317
321
  const [res, versionsRes] = await Promise.all([fetch(`${base}modules.json`), fetch(`${base}versions.json`).catch(() => null)]);
@@ -304,10 +304,10 @@ function getAppInfo() {
304
304
  return {
305
305
  "pkg": {
306
306
  "name": "@oj-module/runtime",
307
- "version": "0.2.50",
307
+ "version": "0.2.53",
308
308
  "license": "MIT"
309
309
  },
310
- "lastBuildTime": "2026-11-01 22:25:12"
310
+ "lastBuildTime": "2026-11-02 15:46:13"
311
311
  };
312
312
  }
313
313
  var init_get_app_info = __esmMin((() => {}));
@@ -7687,6 +7687,27 @@ function buildInstance(entry, definition, status, error) {
7687
7687
  error
7688
7688
  };
7689
7689
  }
7690
+ /**
7691
+ * 按 `handle.roles` 递归过滤路由(菜单与路由表同源)。
7692
+ *
7693
+ * 模块级 `config.requiredRoles` 只能整包放行/拦截;一个模块里既有全员可见页、
7694
+ * 又有仅超管可见页(/system/settings、/system/wecom)时,需要路由级粒度。
7695
+ * 语义与前端静态路由(generateRoutesByFrontend)一致:未声明 = 不限制;
7696
+ * 声明了则命中任一角色才保留,否则连同子树一起剔除。
7697
+ */
7698
+ function filterRoutesByRoles(routes, roles) {
7699
+ const out = [];
7700
+ for (const route of routes) {
7701
+ const required = route.handle?.roles;
7702
+ if (required?.length && !required.some((role) => roles.includes(role))) continue;
7703
+ const children = "children" in route && Array.isArray(route.children) ? filterRoutesByRoles(route.children, roles) : void 0;
7704
+ out.push(children ? {
7705
+ ...route,
7706
+ children
7707
+ } : route);
7708
+ }
7709
+ return out;
7710
+ }
7690
7711
  function getRoutes() {
7691
7712
  const { roles, permissions = [] } = useUserStore.getState();
7692
7713
  const routes = [];
@@ -7696,7 +7717,7 @@ function getRoutes() {
7696
7717
  if (requiredRoles?.length && !requiredRoles.some((role) => roles.includes(role))) continue;
7697
7718
  const requiredPermissions = instance.definition.config?.requiredPermissions;
7698
7719
  if (requiredPermissions?.length && !requiredPermissions.every((perm) => permissions.includes(perm))) continue;
7699
- if (instance.definition.routes.length > 0) routes.push(...resolveRouteLayouts(tagModuleRoutes(instance.definition.routes, instance.definition.name)));
7720
+ if (instance.definition.routes.length > 0) routes.push(...filterRoutesByRoles(resolveRouteLayouts(tagModuleRoutes(instance.definition.routes, instance.definition.name)), roles));
7700
7721
  }
7701
7722
  return addRouteIdByPath(routes);
7702
7723
  }
@@ -5,9 +5,8 @@
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-milCmNXuFsJqtDXLlLdu/g=='; 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
-
10
- <script type="importmap" nonce="milCmNXuFsJqtDXLlLdu/g==">{
8
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' https://modules.cdn.example.com 'wasm-unsafe-eval' 'nonce-vlbkVLVjOGWbsP3B+GCfRg=='; 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
+ <script type="importmap" nonce="vlbkVLVjOGWbsP3B+GCfRg==">{
11
10
  "imports": {
12
11
  "react": "/assets/react.js",
13
12
  "react/jsx-runtime": "/assets/jsx-runtime.js",
@@ -21,7 +20,6 @@
21
20
  "@oj-module/runtime/contract": "/assets/contract.js",
22
21
  "@oj-module/runtime/contract/errors": "/assets/contract-errors.js",
23
22
  "@plane/kit": "/assets/kit.js",
24
- "@plane/editor": "/assets/editor.js",
25
23
  "antd": "/assets/antd.js",
26
24
  "antd/locale/en_US": "/assets/antd-locale-en_US.js",
27
25
  "antd/locale/zh_CN": "/assets/antd-locale-zh_CN.js",
@@ -50,10 +48,6 @@
50
48
  "dayjs/plugin/weekday": "/assets/dayjs-plugin-weekday.js",
51
49
  "dayjs/plugin/weekOfYear": "/assets/dayjs-plugin-weekOfYear.js",
52
50
  "dayjs/plugin/weekYear": "/assets/dayjs-plugin-weekYear.js",
53
- "echarts": "/assets/echarts.js",
54
- "echarts/charts": "/assets/echarts-charts.js",
55
- "echarts/features.js": "/assets/echarts-features.js",
56
- "echarts-for-react": "/assets/echarts-for-react.js",
57
51
  "motion": "/assets/motion.js",
58
52
  "motion/react": "/assets/motion-react.js",
59
53
  "@dnd-kit/core": "/assets/dnd-kit-core.js",
@@ -73,8 +67,6 @@
73
67
  "simplebar-react": "/assets/simplebar-react.js",
74
68
  "spin-delay": "/assets/spin-delay.js",
75
69
  "tailwind-merge": "/assets/tailwind-merge.js",
76
- "vditor": "/assets/vditor.js",
77
- "vditor/dist/index.css": "/assets/vditor-index-css.js",
78
70
  "antd/es/modal": "/assets/ojm-antd-es-modal.js",
79
71
  "antd/es/upload": "/assets/ojm-antd-es-upload.js",
80
72
  "antd/es/button": "/assets/ojm-antd-es-button.js",
@@ -133,9 +125,14 @@
133
125
  "dayjs/locale/zh-cn": "/assets/ojm-dayjs-locale-zh-cn.js"
134
126
  }
135
127
  }</script>
136
- <script type="module" crossorigin src="/assets/index-FYt9CjFd.js"></script>
128
+ <style>
129
+ .boot-skeleton{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;color:#999;font-size:14px;background:#fff;z-index:9999}
130
+ .boot-skeleton:after{content:"";width:18px;height:18px;margin-left:8px;border:2px solid #d9d9d9;border-top-color:#1677ff;border-radius:50%;animation:bs-spin .8s linear infinite}
131
+ @keyframes bs-spin{to{transform:rotate(360deg)}}
132
+ </style>
133
+ <script nonce="vlbkVLVjOGWbsP3B+GCfRg==" data-ojm-entry="/assets/index-e_Mp2mb5.js">(function(){try{var ua=(navigator.userAgent||"").toLowerCase(),p=location.pathname;while(p.length>1&&p.charAt(p.length-1)==="/")p=p.slice(0,-1);if(ua.indexOf("wxwork")>=0&&p.slice(-6)==="/login"&&!new URLSearchParams(location.search).get("wecom_ticket")){var a=(typeof window.__OJ_API_BASE__==="string"&&window.__OJ_API_BASE__.trim())||"/api";while(a.length>1&&a.charAt(a.length-1)==="/")a=a.slice(0,-1);location.replace(a+"/auth/wecom/login");return}}catch(e){}var s=document.createElement("script");s.type="module";s.src=document.currentScript.getAttribute("data-ojm-entry");document.head.appendChild(s);})();</script>
137
134
  </head>
138
135
  <body>
139
- <div id="root"></div>
136
+ <div id="root"><div class="boot-skeleton">加载中…</div></div>
140
137
  </body>
141
138
  </html>
@@ -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.50",
11
- "@oj-module/runtime/contract": "0.2.50",
12
- "@oj-module/runtime/contract/errors": "0.2.50",
10
+ "@oj-module/runtime": "0.2.53",
11
+ "@oj-module/runtime/contract": "0.2.53",
12
+ "@oj-module/runtime/contract/errors": "0.2.53",
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
@@ -16,7 +16,7 @@ import { ALIAS_PATHS_FILE, syncAliasPaths } from "./alias-paths";
16
16
  import { assertModuleList, loadSiteConfig, mergeModuleList, resolveModuleEntry } from "./config";
17
17
  import { moduleDir, resolveConfigPath, resolveLayout, scanModules, tryResolveLayout } from "./layout";
18
18
  import { requireModuleRootCapability } from "./oj-caps";
19
- import { isSharedDep, SHARED_DEPS } from "./shared-deps";
19
+ import { isPrebuiltDep, isSharedDep, SHARED_DEPS } from "./shared-deps";
20
20
  import { normalizeSiteBase, rewriteIndexHtml } from "./site-base";
21
21
  import { resolveOjBin } from "./vendor";
22
22
  import { checkSharedVersions, resolveShellDist } from "./versions";
@@ -790,7 +790,7 @@ export async function buildModules(
790
790
  rolldownOptions: {
791
791
  // 只把共享依赖留给宿主 importmap;
792
792
  // 相对路径 / 绝对路径是模块自身代码,需要打包
793
- external: (id: string) => !id.startsWith(".") && !path.isAbsolute(id) && isSharedDep(id),
793
+ external: (id: string) => !id.startsWith(".") && !path.isAbsolute(id) && isPrebuiltDep(id),
794
794
  output: {
795
795
  // P7.x / e2e:模块产物内联动态 import(lazy 页面、i18n 等),
796
796
  // 输出为**单文件**。原因:
@@ -807,6 +807,23 @@ export async function buildModules(
807
807
  },
808
808
  });
809
809
 
810
+ // 运行期 require 门禁:CJS 依赖 require 了 external 共享包时,rolldown 只留一个浏览器
811
+ // 不可用的 require 桩("Calling `require` for ..."),表现为模块加载即抛错、页面空白。
812
+ // 这类依赖必须移出 bundle 走宿主 importmap(shell 侧有 CJS→ESM 垫片)。
813
+ const builtEntry = path.join(moduleOutDir, "entry.js");
814
+ if (fs.existsSync(builtEntry)) {
815
+ const code = fs.readFileSync(builtEntry, "utf-8");
816
+ if (code.includes("doesn't expose the `require` function")) {
817
+ const specs = [...new Set([...code.matchAll(/ne\("([^"]+)"\)/g)].map(m => m[1]))];
818
+ throw new Error(
819
+ `[ojm] ${definition.name}@${definition.version} 产物含运行期 require(${specs.join(", ") || "?"})`
820
+ + "——浏览器没有 require,模块加载即抛错。"
821
+ + "根因:该 CJS 依赖被标为 bundle(自带副本),但它 require 的共享包被 external 了。"
822
+ + "修复:把它从 SHARED_DEPS 的 bundle 列表移回共享资产,或改用该依赖的 ESM 入口。",
823
+ );
824
+ }
825
+ }
826
+
810
827
  const prefix = `${baseUrl}/modules/${definition.name}/${definition.version}/`;
811
828
  const chunks: ChunkEntry[] = [];
812
829
  const css: string[] = [];
@@ -20,6 +20,18 @@ export interface SharedDepEntry {
20
20
  asset?: string
21
21
  /** 硬共享:模块不得自带 */
22
22
  hard: boolean
23
+ /**
24
+ * 软共享但「不进宿主首屏」:宿主 shell 不预构建该资产、importmap 不列该键,
25
+ * 模块若 import 它则自行打包进模块产物(设计文档 §4.3 的「软共享缺席→模块自带
26
+ * 副本兜底」语义)。用于**仅模块使用、宿主与共享 chunk 永不 import**的重型依赖
27
+ * (编辑器 / 图表 / Markdown / 拖拽 …),避免把它们 5~6MB 的体量无谓铺进
28
+ * shell-dist 并要求公开链路多传一堆文件。宿主侧有真实 import 的共享依赖
29
+ * (antd / pro-components / icons 内部依赖的 motion / react-jss / dayjs /
30
+ * zustand 等)绝不能标 bundle —— 否则会被 antd 等共享 chunk 经 importmap 解析
31
+ * 不到而 404。判定依据:宿主 host.tsx + runtime.js 的传递 import 图,以及
32
+ * antd/pro-components/icons 的传递依赖图,都不含这些说明符。
33
+ */
34
+ bundle?: boolean
23
35
  }
24
36
 
25
37
  export const SHARED_DEPS: SharedDepEntry[] = [
@@ -58,7 +70,7 @@ export const SHARED_DEPS: SharedDepEntry[] = [
58
70
  * scopes 多版本共存兜底(C7)」——缺席时模块可自带副本而不硬失败。
59
71
  * 代价:多副本会重复约 5.4 MB 资产,故 scopes 只作兜底、默认仍走宿主。
60
72
  */
61
- { specifier: "@plane/editor", asset: "editor", hard: false },
73
+ { specifier: "@plane/editor", asset: "editor", hard: false, bundle: true },
62
74
  // 注:@rc-component/form(antd 6 Form 底层)曾尝试单例化以修 my-profile
63
75
  // 崩溃(React #130),假说未证实——多副本并非充分根因,已回退;见
64
76
  // docs/prd/202609010056-playground-full-modules-plan.md 差异项 D2。
@@ -108,10 +120,10 @@ export const SHARED_DEPS: SharedDepEntry[] = [
108
120
  { specifier: "dayjs/plugin/weekday", asset: "dayjs-plugin-weekday", hard: false },
109
121
  { specifier: "dayjs/plugin/weekOfYear", asset: "dayjs-plugin-weekOfYear", hard: false },
110
122
  { specifier: "dayjs/plugin/weekYear", asset: "dayjs-plugin-weekYear", hard: false },
111
- { specifier: "echarts", asset: "echarts", hard: false },
112
- { specifier: "echarts/charts", asset: "echarts-charts", hard: false },
113
- { specifier: "echarts/features.js", asset: "echarts-features", hard: false },
114
- { specifier: "echarts-for-react", asset: "echarts-for-react", hard: false },
123
+ { specifier: "echarts", asset: "echarts", hard: false, bundle: true },
124
+ { specifier: "echarts/charts", asset: "echarts-charts", hard: false, bundle: true },
125
+ { specifier: "echarts/features.js", asset: "echarts-features", hard: false, bundle: true },
126
+ { specifier: "echarts-for-react", asset: "echarts-for-react", hard: false, bundle: true },
115
127
  { specifier: "motion", asset: "motion", hard: false },
116
128
  { specifier: "motion/react", asset: "motion-react", hard: false },
117
129
  { specifier: "@dnd-kit/core", asset: "dnd-kit-core", hard: false },
@@ -127,6 +139,8 @@ export const SHARED_DEPS: SharedDepEntry[] = [
127
139
  { specifier: "react-error-boundary", asset: "react-error-boundary", hard: false },
128
140
  { specifier: "react-jss", asset: "react-jss", hard: false },
129
141
  { specifier: "clsx", asset: "clsx", hard: false },
142
+ // react-countup 只有 CJS 入口(main: build),其 require("react") 在模块自打包时会退化成
143
+ // 浏览器不可用的 require 桩 → 必须走宿主共享资产(shell 侧有 CJS→ESM 垫片)
130
144
  { specifier: "react-countup", asset: "react-countup", hard: false },
131
145
  { specifier: "simplebar-react", asset: "simplebar-react", hard: false },
132
146
  { specifier: "spin-delay", asset: "spin-delay", hard: false },
@@ -147,8 +161,8 @@ export const SHARED_DEPS: SharedDepEntry[] = [
147
161
  * 直接 import 它们,会命中前缀 external 但无 importmap 键 → 构建期深路径报错,
148
162
  * 属预期拦截(与 dayjs/plugin/* 同规则)。
149
163
  */
150
- { specifier: "vditor", asset: "vditor", hard: false },
151
- { specifier: "vditor/dist/index.css", asset: "vditor-index-css", hard: false },
164
+ { specifier: "vditor", asset: "vditor", hard: false, bundle: true },
165
+ { specifier: "vditor/dist/index.css", asset: "vditor-index-css", hard: false, bundle: true },
152
166
  ];
153
167
 
154
168
  /** 判断一个裸说明符是否命中共享表(命中主包前缀的深路径也按共享处理) */
@@ -161,7 +175,7 @@ export function isSharedDep(id: string): boolean {
161
175
  */
162
176
  export function generateShellEntries(): { name: string, pkg: string }[] {
163
177
  return SHARED_DEPS
164
- .filter(dep => dep.asset && dep.specifier !== "@oj-module/runtime")
178
+ .filter(dep => dep.asset && !dep.bundle && dep.specifier !== "@oj-module/runtime")
165
179
  .map(dep => ({ name: dep.asset!, pkg: dep.specifier }));
166
180
  }
167
181
 
@@ -172,9 +186,13 @@ export function generateShellEntries(): { name: string, pkg: string }[] {
172
186
  export function generateImportmap(baseUrl = ""): Record<string, string> {
173
187
  const map: Record<string, string> = {};
174
188
  for (const dep of SHARED_DEPS) {
175
- if (dep.asset) {
189
+ if (dep.asset && !dep.bundle) {
176
190
  map[dep.specifier] = `${baseUrl}/assets/${dep.asset}.js`;
177
191
  }
178
192
  }
179
193
  return map;
180
194
  }
195
+
196
+ export function isPrebuiltDep(id: string): boolean {
197
+ return SHARED_DEPS.some(dep => !dep.bundle && (id === dep.specifier || id.startsWith(`${dep.specifier}/`)));
198
+ }
package/src/site-base.ts CHANGED
@@ -100,7 +100,7 @@ export function rewriteIndexHtml(html: string, siteBase: string | undefined, api
100
100
  out = rewriteImportmap(out, base);
101
101
  // 属性值:只要以单个 "/" 开头(排除 "//host")。importmap 是脚本正文、非属性,
102
102
  // 上一步已处理,二者不重叠。
103
- out = out.replace(/(\b(?:href|src)=")(\/[^/"][^"]*|\/")/g, (_m, attr: string, url: string) =>
103
+ out = out.replace(/(\b(?:href|src|data-ojm-entry)=")(\/[^/"][^"]*|\/")/g, (_m, attr: string, url: string) =>
104
104
  `${attr}${withSiteBase(url, base)}`);
105
105
  globals.push(`window.__OJ_BASE__=${JSON.stringify(base)};`);
106
106
  }
@@ -20,9 +20,9 @@
20
20
  # `pnpm dev` 时忽略产物,不进发布包、也不进用户工程。守卫:
21
21
  # tests/cli/release-manifest.test.ts 逐条核对清单条目落在 tarball 内。
22
22
  #
23
- # 注意 web/src/{demo,home}/__client 不在清单:其 api 模块(api/src/demo、
24
- # api/src/home)是 demo-only 不进脚手架,client 生成物留着会指向不存在的端点,
25
- # 故 demo/home 只列页面源码(entry/locales/pages),不列整棵模块目录。
23
+ # 注意 web/src/{home}/__client 不在清单:其 api 模块(api/src/home)是 demo-only
24
+ # 不进脚手架,client 生成物留着会指向不存在的端点,故 home 只列页面源码
25
+ # (entry/locales/pages),不列整棵模块目录。
26
26
  include:
27
27
  # —— 工程级文件 ——
28
28
  - README.md
@@ -101,12 +101,9 @@ include:
101
101
  - src/personal-center/api
102
102
  - src/system/api
103
103
  - src/web/api
104
- # —— web 前端模块(demo/home 只列页面源码,__client 见顶部说明)——
104
+ # —— web 前端模块(home 只列页面源码,__client 见顶部说明)——
105
105
  - src/_platform/web
106
106
  - src/auth/web
107
- - src/demo/web/entry.ts
108
- - src/demo/web/locales
109
- - src/demo/web/pages
110
107
  - src/home/web/entry.ts
111
108
  - src/home/web/locales
112
109
  - src/home/web/pages