@axewo/fin-micro 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -3
- package/cli/index.mjs +22 -6
- package/dist/core.cjs +1 -1
- package/dist/core.mjs +131 -127
- package/dist/layout.cjs +1 -0
- package/dist/layout.css +1 -0
- package/dist/layout.mjs +719 -0
- package/dist/types/layout/HostShell.vue.d.ts +528 -0
- package/dist/types/layout/ShellHeader.vue.d.ts +46 -0
- package/dist/types/layout/ShellMenu.vue.d.ts +12 -0
- package/dist/types/layout/ShellTabs.vue.d.ts +23 -0
- package/dist/types/layout/StandaloneMenuItem.vue.d.ts +11 -0
- package/dist/types/layout/StandaloneShell.vue.d.ts +40 -0
- package/dist/types/layout/index.d.ts +5 -0
- package/dist/types/layout/types.d.ts +65 -0
- package/dist/types/sdk/index.d.ts +2 -0
- package/dist/types/watermark/FinWatermark.vue.d.ts +16 -0
- package/dist/types/watermark/config.d.ts +36 -0
- package/dist/types/watermark/index.d.ts +4 -0
- package/dist/watermark.cjs +2 -0
- package/dist/watermark.mjs +119 -0
- package/package.json +28 -3
- package/scaffold/base/index.html +2 -1
- package/scaffold/base/pnpm-workspace.yaml +6 -0
- package/scaffold/base/src/router/demo.ts +1 -1
- package/scaffold/base/src/utils/errorUtils.ts +19 -2
- package/scaffold/base/src/utils/watermarkConfig.ts +18 -0
- package/scaffold/base/src/views/{DemoView.vue → demo/index.vue} +1 -1
- package/scaffold/host/src/layout/HostMenu.vue +10 -2
- package/scaffold/host/src/layout/HostTabs.vue +55 -5
- package/scaffold/host/src/layout/HostWorkspace.vue +12 -2
- package/scaffold/host/src/localPages.ts +2 -2
- package/scaffold/host/src/platform.ts +12 -6
- package/scaffold/host/src/router/index.ts +3 -3
- package/scaffold/host/src/styles/shell.css +16 -1
- package/scaffold/host/src/views/{LoginView.vue → login/components/LoginForm.vue} +7 -12
- package/scaffold/host/src/views/{LoginRoute.vue → login/index.vue} +5 -5
- package/scaffold/host/src/views/{ProfileView.vue → profile/index.vue} +1 -1
- package/scaffold/host/src/views/{WorkspaceRoute.vue → workspace/index.vue} +3 -3
- package/scaffold/layouts/standard/host/src/layout/HostLayout.vue +116 -0
- package/scaffold/layouts/standard/host/src/styles/shell.css +168 -0
- package/scaffold/layouts/standard/vue/src/layout/StandaloneLayout.vue +26 -0
- package/scaffold/vue/src/App.vue +13 -7
- package/scaffold/vue/src/bootstrap.ts +7 -2
- package/scaffold/vue/src/layout/Header.vue +15 -0
- package/scaffold/vue/src/layout/Sidebar.vue +21 -0
- package/scaffold/vue/src/layout/SidebarItem.vue +21 -0
- package/scaffold/vue/src/layout/StandaloneLayout.vue +34 -0
- package/scaffold/vue/src/router/index.ts +8 -5
- package/scaffold/vue/src/router/menu.ts +38 -0
- package/scaffold/vue/src/views/demo/configs/table.ts +24 -0
- package/scaffold/vue/src/views/demo/hooks/useRecordNavigation.ts +32 -0
- package/scaffold/vue/src/views/demo/index.vue +17 -0
- package/scaffold/vue/src/views/{DetailView.vue → demo/modules/detail/index.vue} +2 -2
- package/scaffold/vue/src/views/demo/types.ts +5 -0
- package/scaffold/vue/src/views/ListView.vue +0 -55
- /package/scaffold/host/src/views/{HomeView.vue → home/index.vue} +0 -0
- /package/scaffold/host/src/views/{NotFoundView.vue → not-found/index.vue} +0 -0
- /package/scaffold/vue/src/views/{NotFoundView.vue → not-found/index.vue} +0 -0
package/README.md
CHANGED
|
@@ -24,6 +24,71 @@ pnpm --filter @axewo/fin-docs dev
|
|
|
24
24
|
|
|
25
25
|
文档路径为 `/micro/`。模板位于 `apps/micro-host` 和 `apps/micro-vue`,首次接入建议先运行模板,再替换业务视图与权限规则。
|
|
26
26
|
|
|
27
|
+
## 可配置业务区水印
|
|
28
|
+
|
|
29
|
+
`@axewo/fin-micro/watermark` 独立导出 `FinWatermark`、`WatermarkConfig`、`normalizeWatermarkConfig`,核心入口不加载水印副作用。host/vue 的 standard/custom 模板均接入,配置在生成项目 `src/utils/watermarkConfig.ts`:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
export const watermarkConfig = reactive<WatermarkConfig>({
|
|
33
|
+
enabled: true,
|
|
34
|
+
text: ['内部资料', '自定义文字'],
|
|
35
|
+
color: '#64748b',
|
|
36
|
+
opacity: 0.12,
|
|
37
|
+
rotate: -22,
|
|
38
|
+
fontSize: 16,
|
|
39
|
+
fontFamily: 'sans-serif',
|
|
40
|
+
gap: [100, 80], // 水平/垂直间距,越小越密
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
当前为前端配置;后续由项目 `apis/` 适配真实接口并 `Object.assign(watermarkConfig, result)`,无需更换组件,也没有预设后端协议。不要把 Token 等凭据当水印内容。字号限制 10~64、角度 -180~180、间距 16~600、透明度 0~1,文字最多四行、每行 120 字。
|
|
45
|
+
|
|
46
|
+
水印只覆盖右侧稳定业务视口,业务长页滚动不带走水印,不覆盖头部、侧栏和页签;登录页不绘制。Vue 嵌入运行由基座统一绘制,独立运行才挂载自己的水印。自定义接入需将组件置于 `position: relative` 的非滚动视口内,与业务滚动容器作为兄弟节点;公共 `StandaloneShell` 提供 `workspace-overlay` 插槽。
|
|
47
|
+
|
|
48
|
+
组件使用 Canvas 平铺、`pointer-events: none`,观察自身属性和直接父容器的节点删除,并定时检查常见隐藏样式。合法配置更新、关闭和卸载均清理观察器和定时器。Canvas 不可用或绘制失败会触发 `error` 事件并告警,本轮不启动无限重试;应用可按合规要求自行决定是否阻断业务。
|
|
49
|
+
|
|
50
|
+
**纯前端水印不能保证无法通过 JS、样式或开发者工具移除**:祖先容器被隐藏、脚本停用、截图后处理等均不受此机制保障,自动恢复只能提高篡改成本,不能当安全边界。下载文件需要水印时应在服务端生成;更强溯源需配合服务端审计与权限控制。
|
|
51
|
+
|
|
52
|
+
## 标准公共外壳与自定义布局
|
|
53
|
+
|
|
54
|
+
CLI 支持 `--layout standard|custom`,省略时默认 `standard`;交互创建时也可选择。
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
npx @axewo/fin-micro@latest create my-host --template host --layout standard
|
|
58
|
+
npx @axewo/fin-micro@latest create my-business --template vue --layout custom
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- **standard**:生成项目从 `@axewo/fin-micro/layout` 导入 `HostShell` 或 `StandaloneShell`,并显式导入 `@axewo/fin-micro/layout/style.css`。默认头部、菜单、页签等展示由包维护,升级依赖并重新构建后获得未覆盖区域的公共修复。
|
|
62
|
+
- **custom**:保留完整可运行的本地布局 SFC 与样式,功能基线一致;展示代码的后续变更需要项目自行合并,不会被 npm 升级覆盖。
|
|
63
|
+
- 宿主侧栏菜单名称前展示图标:菜单 `icon` 使用 `HomeFilled`、`Folder`、`List`、`User` 等 Element Plus 图标标识;未配置或不在本地映射白名单中的名称会安全回退到文件/文件夹图标。后台只传标识字符串,不传组件代码、HTML 或远程图标 URL。独立 Vue 菜单使用路由 `meta.icon` 组件。
|
|
64
|
+
- 两种模式的认证、授权、Token、唯一 host/runtime、路由连接、页签事务和卸载逻辑仍由项目原有装配管理。公共外壳不创建第二套状态机,不导入项目的 `platform.ts` 或 `localPages.ts`。
|
|
65
|
+
- 子应用只有独立运行时使用独立外壳;嵌入模式即使暂未连接 bridge,也不显示自己的头部、侧栏。独立菜单仍由本地 router meta 映射,不额外请求菜单接口。
|
|
66
|
+
|
|
67
|
+
### 配置与插槽的升级边界
|
|
68
|
+
|
|
69
|
+
`HostShell` 支持 `brand`(`title`、`mark`、`workspaceTitle`)、受控 `collapsed`、`isFixedTab` 展示规则,以及 `brand`、`header-actions`、`header`、`sidebar`、`tabs` 插槽。业务内容使用默认插槽。`StandaloneShell` 支持 `brand`、`header-actions`、`sidebar` 和默认内容插槽。
|
|
70
|
+
|
|
71
|
+
```vue
|
|
72
|
+
<HostShell :apps="apps" :tabs="tabs" :active-key="activeKey" user-name="张三"
|
|
73
|
+
:brand="{ title: '业务工作台' }" @operate="operate" @open="open"
|
|
74
|
+
@logout="signOut" @profile="openProfile">
|
|
75
|
+
<template #header-actions><button @click="openHelp">帮助</button></template>
|
|
76
|
+
<HostWorkspace />
|
|
77
|
+
</HostShell>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
上例只示意展示扩展,实际项目继续使用生成模板中完整的属性、事件和认证接线。局部插槽不会把整个应用变成 custom;替换整个 `header` 后,仅该区域的默认展示升级不再体现,其他默认区域继续升级。插槽动作只发出事件,项目处理器仍必须执行原有核心授权和离页守卫;不得直接调用可信底层原语绕过事务。固定页签谓词也仅控制按钮,真正约束继续由项目的 `workspaceOperation` 实施。
|
|
81
|
+
|
|
82
|
+
公共样式限定 `.fin-micro-shell` / `.fin-micro-standalone`,不写全局 body/reset,不把主页业务样式放进公共库。可在外层设置 `--fin-shell-accent`、`--fin-shell-border`、`--fin-shell-muted`;独立外壳还支持 `--fin-shell-surface`、`--fin-shell-sidebar-width`。Element Plus 与图标是 layout 入口的可选 peer;仅使用 core/SDK 不会导入布局、Element 或 CSS。按需指构建依赖隔离,不是 npm 选择性下载包内容。
|
|
83
|
+
|
|
84
|
+
### 已有项目接入
|
|
85
|
+
|
|
86
|
+
升级依赖不会自动改写已经复制到项目中的模板。已有 custom 项目需要**一次性迁移**:保留认证/host/runtime/router/业务工作区,将头部、侧栏和页签展示改为公共 `HostShell`,接回原有事件处理器,并导入公共 CSS;Vue 独立布局可改用 `StandaloneShell`。不要重新生成覆盖现有项目。迁移后,再次升级依赖才会同步公共外壳未覆盖区域。反向切换 custom 时,把当前版本布局源码复制到项目并改回本地导入,核心接线不变。
|
|
87
|
+
|
|
88
|
+
### 刷新启动占位
|
|
89
|
+
|
|
90
|
+
公共 HTML 在模块加载前显示根节点内的启动提示;认证恢复和 `router.isReady()` 等待期间不提前渲染未授权工作区。Vue 挂载后自然替换。启动失败显示脱敏文案与重试,重试恢复 loading;取消可重试,旧导入、卸载后的按钮和迟到失败均受代次隔离。占位不使用 fixed 全屏遮罩,嵌入子应用只影响自身根区域。本次不是普通业务 HTTP 请求的全局遮罩,也不实现自动 Token 刷新。
|
|
91
|
+
|
|
27
92
|
## 按需获取当前 Token
|
|
28
93
|
|
|
29
94
|
```ts
|
|
@@ -55,7 +120,7 @@ if (import.meta.hot) import.meta.hot.dispose(dispose)
|
|
|
55
120
|
运行库与创建命令使用同一个包,不另发 create-fin-micro。包发布后可执行以下命令;当前开发不代表已经发布至 npm:
|
|
56
121
|
|
|
57
122
|
```sh
|
|
58
|
-
# 交互式:依次输入目录、选择 host/vue
|
|
123
|
+
# 交互式:依次输入目录、选择 host/vue、选择布局、输入子应用 app-id 和端口
|
|
59
124
|
npx @axewo/fin-micro@latest create
|
|
60
125
|
|
|
61
126
|
# 参数式:适合脚本和 CI,不触发问答
|
|
@@ -72,6 +137,18 @@ node packages/fin-micro/cli/index.mjs create ./order-app --template vue --app-id
|
|
|
72
137
|
|
|
73
138
|
`host` 默认端口 7100,`vue` 默认端口 7101;项目名与 appId 只接受小写字母、数字和连字符。子应用示例建议指定 `--app-id business`,与基座默认登记保持一致;或调整基座的环境变量。目标目录必须不存在,父目录必须已存在;命令不覆盖文件、不自动安装依赖、不自动注册远端应用、不执行发布。进入生成项目后运行 `pnpm install`、`pnpm dev` 和 `pnpm build`。生成源码属于项目,CLI 不升级或覆盖既有源码。
|
|
74
139
|
|
|
140
|
+
### 业务功能目录
|
|
141
|
+
|
|
142
|
+
生成项目的业务页按功能聚合:`views/demo/index.vue` 是列表装配层,`views/demo/configs/table.ts` 维护 fin-ui 表格/表单配置,`views/demo/hooks/useRecordNavigation.ts` 处理权限与详情导航,`views/demo/types.ts` 定义仅本功能使用的类型,`views/demo/modules/detail/index.vue` 是二级详情页。公共计数示例保留在 `views/demo/index.vue`(由 base 提供,子应用覆盖为业务列表)。宿主的 `views/login/index.vue` 编排登录会话,表单展示位于 `views/login/components/LoginForm.vue`;主页、个人中心、工作区和 404 分别在各自的 `views/{功能}/index.vue`。按实际职责扩展 `api/`、`configs/`、`hooks/`、`components/`、`modules/`,不建立空目录或纯转发 hook。功能独有请求放功能 `api/`,可复用请求遵循项目约定放 `src/api/{domain}`;已生成项目的旧路径不会被包升级自动改写。
|
|
143
|
+
|
|
144
|
+
### vue 独立布局与本地路由菜单
|
|
145
|
+
|
|
146
|
+
Vue 子应用独立运行显示头部、Element Plus 左侧菜单和内容区:standard 模式由公共 `StandaloneShell` 展示,custom 模式保留 `layout/Header.vue`、`Sidebar.vue` 和 `StandaloneLayout.vue` 本地源码;嵌入基座时均隐藏子应用头部和菜单。AppProviders 始终包裹全树,两种模式共用原业务页面、会话和 SDK 生命周期;嵌入继续使用原 MicroTabCache / MicroRouteView 及 tab.key 缓存契约。布局和 history 均使用 SDK `isMicroFrontend()` 判断 Wujie 环境,不以 bridge 是否连接、URL 或端口推断;缺桥的嵌入运行仍隐藏外壳并使用 memory history。
|
|
147
|
+
|
|
148
|
+
独立菜单由 `router.options.routes` 的本地声明树生成,不请求后台菜单,不新增后台或认证。`meta.title` 为菜单文字,`icon` 为 Vue 图标组件(示例使用 `@element-plus/icons-vue`),`order` 按同级升序排列(默认 0,同值保留声明顺序),`hidden` 隐藏整棵菜单子树,`activeMenu` 指定活动菜单完整路径。默认展示“业务列表 / 基础示例”;详情 `/detail` 设置 `hidden: true, activeMenu: '/list'`,可正常访问并高亮列表。默认跳转、404、动态参数路由不生成菜单;无标题父级提升可见子项,有标题且有可见子项的父级递归显示 Element 子菜单,相对子路径拼为完整路径。菜单不是授权规则,嵌入授权继续由基座负责。
|
|
149
|
+
|
|
150
|
+
CLI 回归编译真实 App/布局 SFC 并渲染 Element 菜单,覆盖独立与嵌入分支、菜单排序/过滤/递归及详情高亮。完整 `prepack` 保留原有测试;已有模板固定版本的 Vue、Vue Router、Pinia 和 TypeScript 时,可设置 `FIN_SCAFFOLD_DEPENDENCIES` 为这些依赖所在项目目录,只读复用并核对版本,避免测试临时安装依赖,不清理该目录。
|
|
151
|
+
|
|
75
152
|
### host 本地登录与主页体验
|
|
76
153
|
|
|
77
154
|
用户未指定已生成项目时,修改目标是 `scaffold/host`,不会自动覆盖其他生成项目。仓库内可在一个已存在的父目录创建全新项目:
|
|
@@ -83,7 +160,9 @@ pnpm install
|
|
|
83
160
|
pnpm dev
|
|
84
161
|
```
|
|
85
162
|
|
|
86
|
-
|
|
163
|
+
使用 pnpm 12.3.4 安装时,生成项目自带 `pnpm-workspace.yaml`,通过 `allowBuilds` 仅批准 `esbuild@0.21.5`、`vue-demi@0.14.10` 的安装脚本,不必为这两个版本重复执行 `pnpm approve-builds`。其他包或版本仍需审核,不启用全量脚本放行。已生成的项目须手动合并该配置;若项目已有 `pnpm-workspace.yaml`,保留原设置,只合并 `allowBuilds`,再执行 `pnpm install`。pnpm 低于 10.26 的版本需按其版本支持的方式授权;不要混用已移除的旧配置。
|
|
164
|
+
|
|
165
|
+
打开 Vite 输出的本机地址(默认端口 7100),在现有账号密码表单中使用 `demo / 123456`(开发模拟时默认预填),点击“演示登录,进入主页”。开发默认启用模拟登录,不需要后端或子应用,严格校验演示账号密码,错误或空凭据拒绝登录,成功后固定显示“演示用户”。此凭据仅用于开发模拟,请勿输入真实凭据;提交即清空密码输入,加载期间禁止重复提交。登录后默认打开真正的本地主页;左侧 Element 菜单、顶部搜索、个人菜单均可打开“主页 / 个人中心”。个人页展示公开身份,可回主页或退出。主页与 `apps/micro-host` 一样属于唯一 core tabs,而非独立入口或第二套标签状态:认证后在恢复前同步登记,初始、直链和刷新恢复均固定首位且不重复;缺少主页或主页顺序错误的旧快照自动迁移,业务页之间的顺序不变。主页无关闭按钮,右键菜单禁用刷新与关闭;“关闭全部业务页 / 其他 / 左侧 / 右侧”始终排除主页(个人中心也属于可关闭页)。业务页仍可正常刷新;关闭活动页在一次核心事务内回落主页,URL 经同一 Vue Router 更新,守卫拒绝则保持原页和 URL。不放置虚假业务数据或未实现业务按钮。
|
|
87
166
|
|
|
88
167
|
刷新仅凭当前标签页 `sessionStorage` 的 `fin-micro:demo-session:v1=1` 标记恢复固定演示身份,随后通过原 `connectMicroBrowser` 恢复已授权页签,产生新的生命周期 key。它不是 Token,不含账号密码,不访问认证接口。禁用浏览器存储时仍可登录,但无法跨刷新恢复。退出须经原核心离页守卫;确认后关闭全部页签、清除演示标记与私有凭据。认证失效也清除标记,组件卸载不清除标记以支持刷新。
|
|
89
168
|
|
|
@@ -172,7 +251,7 @@ const canManage = computed(() => permission.hasAll(['demo:read', 'demo:write']))
|
|
|
172
251
|
|
|
173
252
|
### locales / router / stores 联动示例
|
|
174
253
|
|
|
175
|
-
两类生成项目均包含 `locales/zh-CN.ts`、`locales/en.ts`、`stores/demo.ts`、`router/demo.ts` 和 `views/
|
|
254
|
+
两类生成项目均包含 `locales/zh-CN.ts`、`locales/en.ts`、`stores/demo.ts`、`router/demo.ts` 和 `views/demo/index.vue`。`installAppLocale` 将 `demo.*` 业务词条合并到 fin-ui 内置语言包;页面通过全局 `useI18n` 切换中文/英文,通过 `storeToRefs(useDemoStore())` 展示计数与双倍值,并调用 `increment/reset`。状态只属于本轮应用的 Pinia,不持久化、不跨应用共享,也不存储 Token。
|
|
176
255
|
|
|
177
256
|
- **vue 独立运行**:启动生成项目后访问 `/#/demo`(默认端口 7101)。`createMicroRouter` 复用 `demoRoutes`,原列表/详情与缓存接线不变。
|
|
178
257
|
- **vue 嵌入基座**:仅注册本地 `/demo` 不会获得访问授权。后台 `/api/menus` 须为当前已授权应用返回菜单,例如 `{ menuId: 'demo', name: '基础示例', target: { tabId: 'business:demo', route: '/demo' } }`(替换实际 appId 前缀),再从基座菜单打开。模板不自动添加 demo 菜单,也不放宽 `canOpenRoute`;未授权深链仍被拒绝。
|
package/cli/index.mjs
CHANGED
|
@@ -14,13 +14,14 @@ const HELP = `fin-micro 离线项目创建工具
|
|
|
14
14
|
|
|
15
15
|
用法:
|
|
16
16
|
fin-micro create
|
|
17
|
-
fin-micro create <directory> --template host|vue [--app-id <id>] [--port <port>]
|
|
17
|
+
fin-micro create <directory> --template host|vue [--app-id <id>] [--port <port>] [--layout standard|custom]
|
|
18
18
|
fin-micro --help
|
|
19
19
|
|
|
20
20
|
说明:
|
|
21
21
|
目标目录必须不存在,父目录必须真实存在。
|
|
22
22
|
目录名和应用 ID 仅支持小写字母、数字与连字符,首字符为字母或数字。
|
|
23
23
|
host 默认端口 7100,vue 默认端口 7101;端口范围为 1–65535。
|
|
24
|
+
layout 默认 standard(包内公共外壳),custom 生成完整本地布局源码。
|
|
24
25
|
不自动安装依赖,不访问网络。
|
|
25
26
|
`;
|
|
26
27
|
|
|
@@ -49,7 +50,7 @@ export function parseArgs(args, { partial = false } = {}) {
|
|
|
49
50
|
const directory = args[1] && !args[1].startsWith('-') ? args[1] : undefined;
|
|
50
51
|
if (!directory && !partial) throw new Error('请指定目标目录。');
|
|
51
52
|
const options = directory ? { directory } : {};
|
|
52
|
-
const names = { '--template': 'template', '--app-id': 'appId', '--port': 'port' };
|
|
53
|
+
const names = { '--template': 'template', '--app-id': 'appId', '--port': 'port', '--layout': 'layout' };
|
|
53
54
|
for (let index = directory ? 2 : 1; index < args.length; index += 2) {
|
|
54
55
|
const option = args[index];
|
|
55
56
|
if (!Object.hasOwn(names, option)) throw new Error(`未知选项:${option}`);
|
|
@@ -84,7 +85,8 @@ function collectTemplate(root, current = root, files = new Map()) {
|
|
|
84
85
|
|
|
85
86
|
/** 校验选项和真实父路径,不创建文件;交互重输与最终写入前复核共用此边界。 */
|
|
86
87
|
function validateOptions(options, cwd) {
|
|
87
|
-
const { directory, template } = options;
|
|
88
|
+
const { directory, template, layout = 'standard' } = options;
|
|
89
|
+
if (layout !== 'standard' && layout !== 'custom') throw new Error('布局只能是 standard 或 custom。');
|
|
88
90
|
if (template !== 'host' && template !== 'vue') throw new Error('模板只能是 host 或 vue。');
|
|
89
91
|
if (typeof directory !== 'string' || !directory || directory.endsWith('/') || directory.endsWith('\\')) {
|
|
90
92
|
throw new Error('目标目录必须包含有效目录名,且不能以分隔符结尾。');
|
|
@@ -107,7 +109,7 @@ function validateOptions(options, cwd) {
|
|
|
107
109
|
? canonicalParent.toLowerCase() === resolve(parent).toLowerCase()
|
|
108
110
|
: canonicalParent === resolve(parent);
|
|
109
111
|
if (!sameParent) throw new Error('目标的父路径必须是真实存在的目录,且不能通过符号链接访问。');
|
|
110
|
-
return { target, directory, template, projectName, appId, port };
|
|
112
|
+
return { target, directory, template, layout, projectName, appId, port };
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
/**
|
|
@@ -119,7 +121,7 @@ function validateOptions(options, cwd) {
|
|
|
119
121
|
* 可预见错误在写入前失败;磁盘故障或外部并发篡改仍可能留下部分新文件,不递归删除以免误删用户内容。
|
|
120
122
|
*/
|
|
121
123
|
export function createProject(options, { cwd = process.cwd(), packageRoot = PACKAGE_ROOT } = {}) {
|
|
122
|
-
const { target, directory, template, projectName, appId, port } = validateOptions(options, cwd);
|
|
124
|
+
const { target, directory, template, layout, projectName, appId, port } = validateOptions(options, cwd);
|
|
123
125
|
const metadata = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'));
|
|
124
126
|
if (typeof metadata.version !== 'string' || !metadata.version) throw new Error('当前包缺少有效版本号。');
|
|
125
127
|
const replacements = {
|
|
@@ -131,6 +133,14 @@ export function createProject(options, { cwd = process.cwd(), packageRoot = PACK
|
|
|
131
133
|
};
|
|
132
134
|
const files = collectTemplate(join(packageRoot, 'scaffold', 'base'));
|
|
133
135
|
for (const [path, content] of collectTemplate(join(packageRoot, 'scaffold', template))) files.set(path, content);
|
|
136
|
+
// 本地模板是完整 custom 基线;standard 只替换装配层并移除已由包维护的重复展示。
|
|
137
|
+
// 所有素材仍先完成校验,任何非法 layout 都不得用于拼接任意文件系统路径。
|
|
138
|
+
if (layout === 'standard') {
|
|
139
|
+
const overlay = join(packageRoot, 'scaffold', 'layouts', 'standard', template);
|
|
140
|
+
for (const [path, content] of collectTemplate(overlay)) files.set(path, content);
|
|
141
|
+
const owned = template === 'host' ? ['HostHeader.vue', 'HostMenu.vue', 'HostTabs.vue'] : ['Header.vue', 'Sidebar.vue', 'SidebarItem.vue'];
|
|
142
|
+
for (const name of owned) files.delete(join('src', 'layout', name));
|
|
143
|
+
}
|
|
134
144
|
const planned = new Map();
|
|
135
145
|
for (const [path, content] of files) {
|
|
136
146
|
const rendered = content.replace(/__[A-Z][A-Z0-9_]*?__/g, (placeholder) => {
|
|
@@ -154,7 +164,7 @@ export function createProject(options, { cwd = process.cwd(), packageRoot = PACK
|
|
|
154
164
|
mkdirSync(dirname(destination), { recursive: true });
|
|
155
165
|
writeFileSync(destination, content, { flag: 'wx' });
|
|
156
166
|
}
|
|
157
|
-
return { target, directory, template, appId, port };
|
|
167
|
+
return { target, directory, template, layout, appId, port };
|
|
158
168
|
}
|
|
159
169
|
|
|
160
170
|
/** 用户取消或输入流关闭时终止问答;在全部确认前不创建任何文件。 */
|
|
@@ -192,6 +202,12 @@ export async function completeOptions(initial, { interactive, ask, write = () =>
|
|
|
192
202
|
options.template = ({ 1: 'host', 2: 'vue' })[options.template] ?? options.template;
|
|
193
203
|
// 先校验显式参数,避免非法模板或标识被交互流程隐藏。
|
|
194
204
|
validateOptions(options, cwd);
|
|
205
|
+
if (options.layout === undefined) {
|
|
206
|
+
const answer = await inquire('布局模式 [1 standard 公共外壳 / 2 custom 本地布局]', 'standard', value => {
|
|
207
|
+
if (!['1', '2', 'standard', 'custom'].includes(value)) throw new Error('请选择 1/standard 或 2/custom。');
|
|
208
|
+
});
|
|
209
|
+
options.layout = ({ 1: 'standard', 2: 'custom' })[answer] ?? answer;
|
|
210
|
+
}
|
|
195
211
|
if (options.template === 'vue' && options.appId === undefined) {
|
|
196
212
|
options.appId = await inquire('应用 app-id', basename(resolve(cwd, options.directory)), (value) => {
|
|
197
213
|
if (!value || !NAME_PATTERN.test(value)) throw new Error('应用 ID 仅支持小写字母、数字和连字符。');
|
package/dist/core.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const u=require("./sdk.cjs");function Ge(Le,R={}){var Oe;const X=new Map,ye=new Map,z=e=>typeof e=="string"&&!!e.trim()&&!/[\u0000-\u001f\u007f]/.test(e),fe=e=>z(e)&&e.startsWith("/")&&!e.startsWith("//")&&!e.includes("\\"),pe=e=>e.split(/[?#]/,1)[0];for(const e of Le){let a=function(r){if(!Array.isArray(r))throw new Error("menus 必须为数组");return Object.freeze(r.map(i=>{if(!i||!z(i.menuId)||!z(i.name)||c.has(i.menuId))throw new Error("菜单标识/名称无效或菜单标识重复");if(c.add(i.menuId),i.children!==void 0){if(i.target!==void 0||!Array.isArray(i.children)||!i.children.length)throw new Error("菜单分组需要非空 children 且不能配置 target");return Object.freeze({menuId:i.menuId,name:i.name,children:a(i.children)})}if(!i.target||!z(i.target.tabId)||!fe(i.target.route))throw new Error("菜单叶节点需要有效 target");return I.add(i.menuId),b.push({pathPrefix:pe(i.target.route),menuId:i.menuId}),Object.freeze({menuId:i.menuId,name:i.name,target:Object.freeze({tabId:i.target.tabId,route:i.target.route})})}))};if(X.has(e.appId))throw new Error(`重复的 appId: ${e.appId}`);if(e.kind==="local"){if(e.entry!==void 0)throw new Error("本地应用不能配置 entry")}else{if(e.kind!==void 0&&e.kind!=="remote")throw new Error("未知应用类型");const r=new URL(e.entry);if(!["http:","https:"].includes(r.protocol))throw new Error("子应用地址必须使用 HTTP(S)")}const c=new Set,I=new Set,b=[],h=e.menus===void 0?void 0:a(e.menus);if(e.menuRoutes!==void 0&&!Array.isArray(e.menuRoutes))throw new Error("menuRoutes 必须为数组");const o=(Oe=e.menuRoutes)==null?void 0:Oe.map(r=>{if(!r||!fe(r.pathPrefix)||/[?#]/.test(r.pathPrefix)||!I.has(r.menuId))throw new Error("菜单归属规则需要有效路径与本应用可导航叶节点");return Object.freeze({pathPrefix:r.pathPrefix,menuId:r.menuId})});b.push(...(o??[]).map(r=>({...r})));const t=new Map;for(const r of b){if(r.pathPrefix=r.pathPrefix.replace(/\/+$/,"")||"/",t.has(r.pathPrefix)&&t.get(r.pathPrefix)!==r.menuId)throw new Error("菜单归属前缀冲突");t.set(r.pathPrefix,r.menuId)}ye.set(e.appId,{leaves:I,routes:b.sort((r,i)=>i.pathPrefix.length-r.pathPrefix.length)}),X.set(e.appId,Object.freeze({appId:e.appId,name:e.name,...e.kind==="local"?{kind:"local"}:{...e.kind?{kind:e.kind}:{},entry:e.entry},...h?{menus:h}:{},...o?{menuRoutes:Object.freeze(o)}:{}}))}const ae=u.createNotificationBus(),Re=Object.freeze({source:"host"}),de={on(e,a){return ae.on(e,({payload:c,metadata:I})=>U(()=>a(c,I)))},emit(e,a){ae.emit(e,{payload:a,metadata:Re})},clear:()=>ae.clear()},se=R.callTimeoutMs??3e4;if(!Number.isFinite(se)||se<=0||se>2147483647)throw new Error("callTimeoutMs 必须为不超过2147483647的正有限数");const Z=new Set;function we(e,a){if(!e||typeof e.authenticated!="boolean"||!Array.isArray(e.permissions)||!e.permissions.every(c=>typeof c=="string")||e.user!==void 0&&(!e.user||typeof e.user.id!="string"||e.user.name!==void 0&&typeof e.user.name!="string"))throw new Error("会话快照无效");return Object.freeze({authenticated:e.authenticated,permissions:Object.freeze([...e.permissions]),...e.user?{user:Object.freeze({id:e.user.id,...e.user.name!==void 0?{name:e.user.name}:{}})}:{},revision:a})}let K=we(R.session??{authenticated:!1,permissions:[]},0);function ze(e){D(),K=we(e,K.revision+1);const a=K;for(const c of[...Z]){if(a!==K)break;Z.has(c)&&U(()=>c(a))}return a}const ee=new Set,te=new Map,ve=new Map(Object.entries(R.capabilities??{}).filter(([,e])=>e!==void 0).map(([e,a])=>{const c=a;return[e,{handler:c.handler,apps:c.apps?new Set(c.apps):void 0}]})),V=new Map,B=new Map;let d={tabs:[],activeTabId:null},le=0,P=!1,G=0,Ee=Promise.resolve();const re=new Map,W=new Map,xe=e=>{var a;try{(a=R.onError)==null||a.call(R,e)}catch{}},U=e=>{try{e()}catch(a){xe(a)}},D=()=>{if(P)throw new Error("MicroHost 已销毁")},ie=()=>({tabs:[...d.tabs],activeTabId:d.activeTabId}),oe=()=>{G++;for(const e of[...ee])ee.has(e)&&U(()=>e(ie()))};function ke(e){return D(),ee.add(e),()=>{ee.delete(e)}}const be=R.canNavigate,ge=R.canOpen;function q(e){if(!ge)return;const a=Object.freeze({appId:e.appId,tabId:e.tabId,title:e.title,route:e.route,...e.activeMenuId!==void 0?{activeMenuId:e.activeMenuId}:{}});try{const c=ge(a);if(c===!0)return;c&&(typeof c=="object"||typeof c=="function")&&Promise.resolve(c).catch(()=>{})}catch{}throw new u.NavigationError("FORBIDDEN","无权访问该页面(权限策略必须同步返回 true)")}function ue(e,a=!1){var o;if(D(),!e||!z(e.title)||e.title.length>500)throw new Error("页签标题无效");if(!X.has(e.appId))throw new Error(`未知子应用: ${e.appId}`);if(!z(e.tabId)||!fe(e.route))throw new Error("需要 tabId 与应用内绝对路由");const c=ye.get(e.appId);if(e.activeMenuId!==void 0&&!c.leaves.has(e.activeMenuId))throw new Error("activeMenuId 必须指向本应用可导航叶节点");const I=pe(e.route),b=e.activeMenuId??((o=c.routes.find(t=>t.pathPrefix==="/"||I===t.pathPrefix||I.startsWith(`${t.pathPrefix}/`)))==null?void 0:o.menuId);let h=d.tabs.find(t=>t.tabId===e.tabId);if(h&&(h.appId!==e.appId||h.route!==e.route&&!a))throw new Error("tabId 已绑定到其他页面");return h&&h.route!==e.route&&(h=void 0),q(h??{...e,activeMenuId:b}),h||(h=Object.freeze({appId:e.appId,tabId:e.tabId,title:e.title,route:e.route,...b!==void 0?{activeMenuId:b}:{},key:`${e.tabId}:${++le}`})),h}function He(e){const a=ue(e);return d={tabs:d.tabs.includes(a)?d.tabs:[...d.tabs,a],activeTabId:a.tabId},oe(),a}function _e(e,a,c){if(D(),typeof a.title!="string"||!a.title.trim()||a.title.length>500||/[\u0000-\u001f\u007f]/.test(a.title))throw new Error("页签标题无效");const I=d.tabs.find(h=>h.tabId===e&&(!c||h.key===c));if(!I)return;const b=Object.freeze({...I,title:a.title});return d={...d,tabs:d.tabs.map(h=>h===I?b:h)},oe(),b}function Ce(e){D();const a=d.tabs.find(c=>c.tabId===e);return a?(q(a),d={...d,activeTabId:e},oe(),!0):!1}function Fe(e,a){var h;D();const c=d.tabs.findIndex(o=>o.tabId===e&&(!a||o.key===a));if(c<0)return!1;const I=d.tabs[c],b=d.tabs.filter(o=>o!==I);if(d={tabs:b,activeTabId:d.activeTabId===e?((h=b[Math.min(c,b.length-1)])==null?void 0:h.tabId)??null:d.activeTabId},me(I.key),!b.some(o=>o.appId===I.appId)){for(const o of[...B.get(I.appId)??[]])U(o);B.delete(I.appId),te.delete(I.appId)}return oe(),!0}function me(e){re.delete(e),W.delete(e);for(const a of[...V.get(e)??[]])U(a);V.delete(e)}function $e(e){return Y(e)}function Y(e,a,c,I=!1,b,h=!1){var o;try{if(P)throw new u.NavigationError("HOST_DISPOSED","MicroHost 已销毁");if(!e||!["open","activate","refresh","close","closeMany","back"].includes(e.type))throw new u.NavigationError("INVALID_OPERATION","导航命令无效");const t={...e,..."target"in e&&e.target?{target:{...e.target}}:{},..."anchor"in e&&e.anchor?{anchor:{...e.anchor}}:{}},r=n=>!!n&&z(n.tabId)&&(n.key===void 0||z(n.key));if(["activate","refresh","close"].includes(t.type)&&(!("target"in t)||!r(t.target)))throw new u.NavigationError("INVALID_OPERATION","导航需要合法目标引用");if("target"in t&&t.target!==void 0&&!r(t.target)||"anchor"in t&&t.anchor!==void 0&&!r(t.anchor))throw new u.NavigationError("INVALID_OPERATION","导航引用无效");if(t.type==="closeMany"&&t.exclude!==void 0){if(!Array.isArray(t.exclude)||!t.exclude.every(r))throw new u.NavigationError("INVALID_OPERATION","批量排除引用无效");t.exclude=Object.freeze(t.exclude.map(n=>{var m;return Object.freeze({tabId:n.tabId,...n.key!==void 0?{key:n.key}:{key:(m=d.tabs.find(v=>v.tabId===n.tabId))==null?void 0:m.key}})}))}if(t.type==="open"){const n=t.input;if(!n)throw new u.NavigationError("INVALID_OPERATION","导航目标无效");t.input=Object.freeze({appId:n.appId,tabId:n.tabId,title:n.title,route:n.route,...n.activeMenuId!==void 0?{activeMenuId:n.activeMenuId}:{}})}if("closeFallback"in t&&t.closeFallback!==void 0){if(!r(t.closeFallback))throw new u.NavigationError("INVALID_OPERATION","关闭回落引用无效");const n=t.closeFallback;t.closeFallback=Object.freeze({tabId:n.tabId,key:n.key??((o=d.tabs.find(m=>m.tabId===n.tabId))==null?void 0:o.key)})}"fallback"in t&&t.fallback&&(t.fallback=Object.freeze({...t.fallback})),"target"in t&&t.target&&Object.freeze(t.target),"anchor"in t&&t.anchor&&Object.freeze(t.anchor),Object.freeze(t);const i=n=>d.tabs.find(m=>n?m.tabId===n.tabId&&(!n.key||m.key===n.key):m.tabId===d.activeTabId),w="target"in t?i(t.target):void 0,E=t.type==="closeMany"&&t.scope!=="all"?t.anchor?i(t.anchor):c?d.tabs.find(n=>n.key===c):i():void 0,S=i(),f=t.type==="open"?d.tabs.find(n=>n.tabId===t.input.tabId):void 0,y=h&&c?d.tabs.find(n=>n.key===c):void 0,k=y&&a?Object.freeze({appId:a,tab:Object.freeze({...y})}):void 0,x=h&&t.type==="open"&&!!a&&t.input.appId!==a,j=()=>{if(!(!x||t.type!=="open")){try{const n=k&&(be==null?void 0:be(k,t.input));if(n===!0)return;n&&(typeof n=="object"||typeof n=="function")&&Promise.resolve(n).catch(()=>{})}catch{}throw new u.NavigationError("FORBIDDEN","无权跨应用导航(权限策略必须同步返回 true)")}},H=n=>!a||n.appId===a;if(w&&!H(w)||E&&!H(E)||t.type==="open"&&a&&t.input.appId!==a&&!x||t.type==="back"&&t.fallback&&a&&t.fallback.appId!==a)throw new u.NavigationError("FORBIDDEN","页面只能操作本应用");if(t.type==="closeMany"&&!["all","others","left","right"].includes(t.scope))throw new u.NavigationError("INVALID_OPERATION","批量范围无效");const _=E?d.tabs.indexOf(E):-1,J=t.type==="closeMany"?d.tabs.filter((n,m)=>{var v;return H(n)&&!((v=t.exclude)!=null&&v.some(F=>F.tabId===n.tabId&&F.key===n.key))&&(t.scope==="all"||_>=0&&(t.scope==="others"?n.key!==E.key:t.scope==="left"?m<_:m>_))}):[],g=t.type==="back"?w??(!("target"in t)||!t.target?S:void 0):w,C=async()=>{var Ne,Ae,Te,Pe,Me,De;const n=s=>({status:s,affected:[]});if(P)throw new u.NavigationError("HOST_DISPOSED","MicroHost 已销毁");if((Ne=t.signal)!=null&&Ne.aborted)return n("stale");if(c&&!I&&(b!=null&&b()||!d.tabs.some(s=>s.key===c)))throw new u.NavigationError("PAGE_RELEASED","页面上下文已释放");if(t.type==="open"&&(f?!d.tabs.some(s=>s.key===f.key):d.tabs.some(s=>s.tabId===t.input.tabId))||g&&!d.tabs.some(s=>s.key===g.key)||E&&!d.tabs.some(s=>s.key===E.key)||J.some(s=>!d.tabs.some(O=>O.key===s.key)))return n("stale");if(t.type!=="open"&&t.type!=="closeMany"&&!g)return n("noop");const m=G;if(j(),c&&!I&&(b!=null&&b()||!d.tabs.some(s=>s.key===c)))throw new u.NavigationError("PAGE_RELEASED","页面上下文已释放");if(G!==m||P||(Ae=t.signal)!=null&&Ae.aborted)return n("stale");const v=d,F=G;let p=[...d.tabs],l,M=[],$=d.activeTabId;try{if(t.type==="open"&&(l=ue(t.input,!0),f&&f.key!==l.key?(M=[f],p=p.map(s=>s.key===f.key?l:s)):p.some(s=>s.key===l.key)||p.push(l),$=l.tabId),t.type==="activate"&&(l=g,q(l),$=l.tabId),t.type==="refresh"&&(q(g),M=[g],l=Object.freeze({...g,key:`${g.tabId}:${++le}`}),p=p.map(s=>s.key===g.key?l:s)),(t.type==="close"||t.type==="back")&&(M=[g]),t.type==="closeMany"&&(M=J),M.length&&t.type!=="refresh"&&t.type!=="open"){const s=new Set(M.map(N=>N.key)),O=v.tabs.findIndex(N=>N.tabId===v.activeTabId);if(p=p.filter(N=>!s.has(N.key)),t.type==="back"){const N=W.get(g.key);l=p.find(T=>T.key===N&&H(T)),!l&&t.fallback&&(l=ue(t.fallback),s.has(l.key)&&(l=Object.freeze({...l,key:`${l.tabId}:${++le}`})),p.some(T=>T.key===l.key)||p.push(l));const A=v.tabs.filter(T=>H(T)),L=A.findIndex(T=>T.key===g.key);l??(l=A.slice(0,L).reverse().find(T=>p.some(ne=>ne.key===T.key))??A.slice(L+1).find(T=>p.some(ne=>ne.key===T.key))),l??(l=p[Math.min(O,p.length-1)]),l&&q(l),$=(l==null?void 0:l.tabId)??null}else if(M.some(N=>N.tabId===$)){const N="closeFallback"in t?t.closeFallback:void 0,A=N?p.find(L=>L.tabId===N.tabId&&L.key===N.key&&H(L)):void 0;A&&(q(A),l=A),$=((Te=A??p[Math.min(O,p.length-1)])==null?void 0:Te.tabId)??null}}}catch(s){throw s instanceof u.NavigationError?s:new u.NavigationError("INVALID_OPERATION","导航参数无效")}if(p.length===v.tabs.length&&p.every((s,O)=>s.key===v.tabs[O].key)&&$===v.activeTabId)return n("noop");const he=[...M],ce=v.tabs.find(s=>s.tabId===v.activeTabId);ce&&ce.tabId!==$&&!he.some(s=>s.key===ce.key)&&he.push(ce);for(const s of he)for(const O of[...re.get(s.key)??[]]){let N;const A=t.signal;let L=()=>{};try{const T=Promise.resolve(O({operation:t,tab:s})),ne=new Promise(Se=>{if(!A)return;const je=()=>Se();A.addEventListener("abort",je,{once:!0}),L=()=>A.removeEventListener("abort",je),A.aborted&&Se()});N=await Promise.race([T,ne])}catch{if(A!=null&&A.aborted)return n("stale");throw new u.NavigationError("GUARD_FAILED","离页守卫执行失败")}finally{L()}if(G!==F||P||(Pe=t.signal)!=null&&Pe.aborted)return n("stale");if(N===!1)return n("cancelled")}if(G!==F||P||(Me=t.signal)!=null&&Me.aborted)return n("stale");if(b!=null&&b())throw new u.NavigationError("PAGE_RELEASED","页面上下文已释放");if(j(),l&&q(l),b!=null&&b())throw new u.NavigationError("PAGE_RELEASED","页面上下文已释放");if(G!==F||P||(De=t.signal)!=null&&De.aborted)return n("stale");if(d={tabs:p,activeTabId:$},t.type==="open"&&l&&c&&!x&&l.key!==c&&!W.has(l.key)&&W.set(l.key,c),t.type==="refresh"&&l){const s=W.get(g.key);s&&W.set(l.key,s)}for(const s of M)me(s.key);for(const s of new Set(M.map(O=>O.appId)))if(!p.some(O=>O.appId===s)){for(const O of[...B.get(s)??[]])U(O);B.delete(s),te.delete(s)}const Ie=[...M,...p.filter(s=>!v.tabs.some(O=>O.key===s.key))];return!Ie.length&&l&&Ie.push(l),oe(),{status:"committed",affected:Ie.map(s=>({tabId:s.tabId,key:s.key})),...l?{tab:l}:{}}},Q=Ee.then(C);return Ee=Q.catch(()=>{}),Q}catch(t){return Promise.reject(t)}}function Ve(e){if(D(),!X.has(e))throw new Error(`未知子应用: ${e}`);const a=te.get(e);if(a)return a;let c=!0;const I=new Set([()=>{c=!1}]);B.set(e,I);async function b(o,t,r,i={}){const w=()=>{if(P)throw new u.HostCallError("HOST_DISPOSED","MicroHost 已销毁");if(r&&(r.released()||!d.tabs.some(j=>j.key===r.key)))throw new u.HostCallError("PAGE_RELEASED","页面上下文已释放");if(!c)throw new u.HostCallError("BRIDGE_RELEASED","应用 bridge 已释放")};w();const E=ve.get(o);if(!E)throw new u.HostCallError("UNKNOWN_CAPABILITY",`未注册宿主能力: ${o}`);if(E.apps&&!E.apps.has(e))throw new u.HostCallError("FORBIDDEN",`应用无权调用宿主能力: ${o}`);const S=i.timeoutMs??se;if(!Number.isFinite(S)||S<=0||S>2147483647)throw new TypeError("timeoutMs 必须为不超过2147483647的正有限数");const f=i.signal;if(f!=null&&f.aborted)throw new u.HostCallError("CANCELLED","宿主能力调用已取消");const y=new AbortController,k=r?d.tabs.find(j=>j.key===r.key):void 0,x=Object.freeze({appId:e,signal:y.signal,...k?{tab:Object.freeze({...k})}:{},isActive:()=>!y.signal.aborted&&!P&&c&&(!r||!r.released()&&d.tabs.some(j=>j.key===r.key))});return new Promise((j,H)=>{let _=!1,J;const g=()=>{J!==void 0&&clearTimeout(J),f==null||f.removeEventListener("abort",Q),I.delete(n),r==null||r.cleanups.delete(n)},C=v=>{_||(_=!0,g(),H(v),y.abort())},Q=()=>C(new u.HostCallError("CANCELLED","宿主能力调用已取消")),n=()=>{try{w()}catch(v){C(v)}};if(I.add(n),r==null||r.cleanups.add(n),f==null||f.addEventListener("abort",Q,{once:!0}),J=setTimeout(()=>C(new u.HostCallError("TIMEOUT","宿主能力调用超时")),S),f!=null&&f.aborted){Q();return}let m;try{m=E.handler(x,...t)}catch{C(new u.HostCallError("HANDLER_FAILED",`宿主能力执行失败: ${o}`));return}Promise.resolve(m).then(v=>{if(!_){try{w()}catch(F){C(F);return}_=!0,g(),j(v)}},()=>C(new u.HostCallError("HANDLER_FAILED",`宿主能力执行失败: ${o}`)))})}const h={appId:e,getState:ie,callHost(o,...t){return b(o,t)},callHostWithOptions(o,t,...r){return b(o,r,void 0,t)},protocol(){if(D(),!c)throw new Error("应用 bridge 已释放");return Object.freeze({version:"1.0",capabilities:Object.freeze([...ve].filter(([,o])=>!o.apps||o.apps.has(e)).map(([o])=>o)),features:Object.freeze(["call-options","session","event-metadata","navigate-to"])})},negotiate(o={}){const t=h.protocol(),r=Object.freeze((o.capabilities??[]).filter(i=>!t.capabilities.includes(i)));return Object.freeze({compatible:(!o.version||o.version===t.version)&&!r.length,protocol:t,missingCapabilities:r})},getSession(){if(D(),!c)throw new Error("应用 bridge 已释放");return K},subscribeSession(o){const t=h.getSession(),r=w=>o(w),i=()=>{Z.delete(r),I.delete(i)};return Z.add(r),I.add(i),U(()=>o(t)),i},subscribe(o){if(!c)throw new Error("应用 bridge 已释放");const t=ke(o),r=()=>{t(),I.delete(r)};return I.add(r),r},ready(o){const t=i=>{const w=i.tabs.find(E=>E.tabId===i.activeTabId&&E.appId===e);w&&o(w)},r=h.subscribe(t);try{t(ie())}catch(i){throw r(),i}return r},bindPage(o){const t=d.tabs.find(r=>r.tabId===o&&r.appId===e);if(!t)throw new Error("页面不属于该应用");return h.page(t.key)},page(o){if(D(),!c)throw new Error("应用 bridge 已释放");const t=d.tabs.find(f=>f.key===o&&f.appId===e);if(!t)throw new Error("页面上下文不存在或不属于该应用");let r=!1;const i=new Set,w=()=>{if(P)throw new u.NavigationError("HOST_DISPOSED","MicroHost 已销毁");if(r||!d.tabs.some(f=>f.key===o))throw new u.NavigationError("PAGE_RELEASED","页面上下文已释放")},E=()=>{var f;r=!0,i.forEach(y=>y()),i.clear(),(f=V.get(o))==null||f.delete(E)};let S=V.get(o);return S||V.set(o,S=new Set),S.add(E),{get tab(){return d.tabs.find(f=>f.key===o)??t},callHost(f,...y){return b(f,y,{key:o,released:()=>r,cleanups:i})},callHostWithOptions(f,y,...k){return b(f,k,{key:o,released:()=>r,cleanups:i},y)},async openTab(f){w();const y=await Y({type:"open",input:f},e,o,!1,()=>r);if(y.tab)return y.tab;const k=d.tabs.find(x=>x.tabId===f.tabId&&x.appId===e);if(y.status==="noop"&&k)return k;throw new u.NavigationError(y.status==="cancelled"?"CANCELLED":"STALE","导航未提交")},async navigateTo(f,y={}){return w(),Y({type:"open",input:f,signal:y.signal},e,o,!1,()=>r,!0)},async closeCurrent(){return P?!1:(await Y({type:"close",target:{tabId:t.tabId,key:o}},e,void 0,!0)).status==="committed"},beforeLeave(f){if(w(),typeof f!="function")throw new u.NavigationError("INVALID_OPERATION","离页守卫必须为函数");let y=re.get(o);y||re.set(o,y=new Set),y.add(f);const k=()=>{y.delete(f),i.delete(k)};return i.add(k),k},async refreshCurrent(){return w(),Y({type:"refresh",target:{tabId:t.tabId,key:o}},e,o,!1,()=>r)},async back(f){return w(),Y({type:"back",target:{tabId:t.tabId,key:o},fallback:f},e,o,!1,()=>r)},async operate(f){return w(),Y(f,e,o,!1,()=>r)},emit(f,y){w(),ae.emit(f,{payload:y,metadata:Object.freeze({source:"app",appId:e,tabKey:o})})},on(f,y){w();const k=de.on(f,y);return i.add(k),()=>{k(),i.delete(k)}},dispose:E}}};return te.set(e,h),h}function Be(){if(!P){P=!0;for(const e of V.values())[...e].forEach(a=>a());V.clear(),re.clear(),W.clear();for(const e of B.values())[...e].forEach(a=>a());B.clear(),ee.clear(),Z.clear(),te.clear(),de.clear(),d={tabs:[],activeTabId:null}}}return{apps:[...X.values()],getState:ie,subscribe:ke,openTab:He,activateTab:Ce,closeTab:Fe,updateTab:_e,updateSession:ze,operate:$e,bridge:Ve,bus:de,dispose:Be}}exports.createMicroHost=Ge;
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const u=require("./sdk.cjs");function Ge(Le,z={}){var Oe;const Q=new Map,Ie=new Map,R=e=>typeof e=="string"&&!!e.trim()&&!/[\u0000-\u001f\u007f]/.test(e),fe=e=>R(e)&&e.startsWith("/")&&!e.startsWith("//")&&!e.includes("\\"),pe=e=>e.split(/[?#]/,1)[0];for(const e of Le){let a=function(r){if(!Array.isArray(r))throw new Error("menus 必须为数组");return Object.freeze(r.map(s=>{if(!s||!R(s.menuId)||!R(s.name)||c.has(s.menuId))throw new Error("菜单标识/名称无效或菜单标识重复");if(c.add(s.menuId),s.icon!==void 0&&(typeof s.icon!="string"||!/^[A-Za-z][A-Za-z0-9-]{0,63}$/.test(s.icon)))throw new Error("菜单图标标识无效");if(s.children!==void 0){if(s.target!==void 0||!Array.isArray(s.children)||!s.children.length)throw new Error("菜单分组需要非空 children 且不能配置 target");return Object.freeze({menuId:s.menuId,name:s.name,...s.icon===void 0?{}:{icon:s.icon},children:a(s.children)})}if(!s.target||!R(s.target.tabId)||!fe(s.target.route))throw new Error("菜单叶节点需要有效 target");return y.add(s.menuId),b.push({pathPrefix:pe(s.target.route),menuId:s.menuId}),Object.freeze({menuId:s.menuId,name:s.name,...s.icon===void 0?{}:{icon:s.icon},target:Object.freeze({tabId:s.target.tabId,route:s.target.route})})}))};if(Q.has(e.appId))throw new Error(`重复的 appId: ${e.appId}`);if(e.kind==="local"){if(e.entry!==void 0)throw new Error("本地应用不能配置 entry")}else{if(e.kind!==void 0&&e.kind!=="remote")throw new Error("未知应用类型");const r=new URL(e.entry);if(!["http:","https:"].includes(r.protocol))throw new Error("子应用地址必须使用 HTTP(S)")}const c=new Set,y=new Set,b=[],h=e.menus===void 0?void 0:a(e.menus);if(e.menuRoutes!==void 0&&!Array.isArray(e.menuRoutes))throw new Error("menuRoutes 必须为数组");const o=(Oe=e.menuRoutes)==null?void 0:Oe.map(r=>{if(!r||!fe(r.pathPrefix)||/[?#]/.test(r.pathPrefix)||!y.has(r.menuId))throw new Error("菜单归属规则需要有效路径与本应用可导航叶节点");return Object.freeze({pathPrefix:r.pathPrefix,menuId:r.menuId})});b.push(...(o??[]).map(r=>({...r})));const t=new Map;for(const r of b){if(r.pathPrefix=r.pathPrefix.replace(/\/+$/,"")||"/",t.has(r.pathPrefix)&&t.get(r.pathPrefix)!==r.menuId)throw new Error("菜单归属前缀冲突");t.set(r.pathPrefix,r.menuId)}Ie.set(e.appId,{leaves:y,routes:b.sort((r,s)=>s.pathPrefix.length-r.pathPrefix.length)}),Q.set(e.appId,Object.freeze({appId:e.appId,name:e.name,...e.kind==="local"?{kind:"local"}:{...e.kind?{kind:e.kind}:{},entry:e.entry},...h?{menus:h}:{},...o?{menuRoutes:Object.freeze(o)}:{}}))}const ae=u.createNotificationBus(),ze=Object.freeze({source:"host"}),de={on(e,a){return ae.on(e,({payload:c,metadata:y})=>U(()=>a(c,y)))},emit(e,a){ae.emit(e,{payload:a,metadata:ze})},clear:()=>ae.clear()},se=z.callTimeoutMs??3e4;if(!Number.isFinite(se)||se<=0||se>2147483647)throw new Error("callTimeoutMs 必须为不超过2147483647的正有限数");const X=new Set;function we(e,a){if(!e||typeof e.authenticated!="boolean"||!Array.isArray(e.permissions)||!e.permissions.every(c=>typeof c=="string")||e.user!==void 0&&(!e.user||typeof e.user.id!="string"||e.user.name!==void 0&&typeof e.user.name!="string"))throw new Error("会话快照无效");return Object.freeze({authenticated:e.authenticated,permissions:Object.freeze([...e.permissions]),...e.user?{user:Object.freeze({id:e.user.id,...e.user.name!==void 0?{name:e.user.name}:{}})}:{},revision:a})}let K=we(z.session??{authenticated:!1,permissions:[]},0);function Re(e){D(),K=we(e,K.revision+1);const a=K;for(const c of[...X]){if(a!==K)break;X.has(c)&&U(()=>c(a))}return a}const ee=new Set,te=new Map,ve=new Map(Object.entries(z.capabilities??{}).filter(([,e])=>e!==void 0).map(([e,a])=>{const c=a;return[e,{handler:c.handler,apps:c.apps?new Set(c.apps):void 0}]})),V=new Map,B=new Map;let d={tabs:[],activeTabId:null},le=0,P=!1,G=0,Ee=Promise.resolve();const re=new Map,W=new Map,xe=e=>{var a;try{(a=z.onError)==null||a.call(z,e)}catch{}},U=e=>{try{e()}catch(a){xe(a)}},D=()=>{if(P)throw new Error("MicroHost 已销毁")},ie=()=>({tabs:[...d.tabs],activeTabId:d.activeTabId}),oe=()=>{G++;for(const e of[...ee])ee.has(e)&&U(()=>e(ie()))};function ke(e){return D(),ee.add(e),()=>{ee.delete(e)}}const be=z.canNavigate,ge=z.canOpen;function q(e){if(!ge)return;const a=Object.freeze({appId:e.appId,tabId:e.tabId,title:e.title,route:e.route,...e.activeMenuId!==void 0?{activeMenuId:e.activeMenuId}:{}});try{const c=ge(a);if(c===!0)return;c&&(typeof c=="object"||typeof c=="function")&&Promise.resolve(c).catch(()=>{})}catch{}throw new u.NavigationError("FORBIDDEN","无权访问该页面(权限策略必须同步返回 true)")}function ue(e,a=!1){var o;if(D(),!e||!R(e.title)||e.title.length>500)throw new Error("页签标题无效");if(!Q.has(e.appId))throw new Error(`未知子应用: ${e.appId}`);if(!R(e.tabId)||!fe(e.route))throw new Error("需要 tabId 与应用内绝对路由");const c=Ie.get(e.appId);if(e.activeMenuId!==void 0&&!c.leaves.has(e.activeMenuId))throw new Error("activeMenuId 必须指向本应用可导航叶节点");const y=pe(e.route),b=e.activeMenuId??((o=c.routes.find(t=>t.pathPrefix==="/"||y===t.pathPrefix||y.startsWith(`${t.pathPrefix}/`)))==null?void 0:o.menuId);let h=d.tabs.find(t=>t.tabId===e.tabId);if(h&&(h.appId!==e.appId||h.route!==e.route&&!a))throw new Error("tabId 已绑定到其他页面");return h&&h.route!==e.route&&(h=void 0),q(h??{...e,activeMenuId:b}),h||(h=Object.freeze({appId:e.appId,tabId:e.tabId,title:e.title,route:e.route,...b!==void 0?{activeMenuId:b}:{},key:`${e.tabId}:${++le}`})),h}function He(e){const a=ue(e);return d={tabs:d.tabs.includes(a)?d.tabs:[...d.tabs,a],activeTabId:a.tabId},oe(),a}function _e(e,a,c){if(D(),typeof a.title!="string"||!a.title.trim()||a.title.length>500||/[\u0000-\u001f\u007f]/.test(a.title))throw new Error("页签标题无效");const y=d.tabs.find(h=>h.tabId===e&&(!c||h.key===c));if(!y)return;const b=Object.freeze({...y,title:a.title});return d={...d,tabs:d.tabs.map(h=>h===y?b:h)},oe(),b}function Ce(e){D();const a=d.tabs.find(c=>c.tabId===e);return a?(q(a),d={...d,activeTabId:e},oe(),!0):!1}function Fe(e,a){var h;D();const c=d.tabs.findIndex(o=>o.tabId===e&&(!a||o.key===a));if(c<0)return!1;const y=d.tabs[c],b=d.tabs.filter(o=>o!==y);if(d={tabs:b,activeTabId:d.activeTabId===e?((h=b[Math.min(c,b.length-1)])==null?void 0:h.tabId)??null:d.activeTabId},me(y.key),!b.some(o=>o.appId===y.appId)){for(const o of[...B.get(y.appId)??[]])U(o);B.delete(y.appId),te.delete(y.appId)}return oe(),!0}function me(e){re.delete(e),W.delete(e);for(const a of[...V.get(e)??[]])U(a);V.delete(e)}function $e(e){return Z(e)}function Z(e,a,c,y=!1,b,h=!1){var o;try{if(P)throw new u.NavigationError("HOST_DISPOSED","MicroHost 已销毁");if(!e||!["open","activate","refresh","close","closeMany","back"].includes(e.type))throw new u.NavigationError("INVALID_OPERATION","导航命令无效");const t={...e,..."target"in e&&e.target?{target:{...e.target}}:{},..."anchor"in e&&e.anchor?{anchor:{...e.anchor}}:{}},r=n=>!!n&&R(n.tabId)&&(n.key===void 0||R(n.key));if(["activate","refresh","close"].includes(t.type)&&(!("target"in t)||!r(t.target)))throw new u.NavigationError("INVALID_OPERATION","导航需要合法目标引用");if("target"in t&&t.target!==void 0&&!r(t.target)||"anchor"in t&&t.anchor!==void 0&&!r(t.anchor))throw new u.NavigationError("INVALID_OPERATION","导航引用无效");if(t.type==="closeMany"&&t.exclude!==void 0){if(!Array.isArray(t.exclude)||!t.exclude.every(r))throw new u.NavigationError("INVALID_OPERATION","批量排除引用无效");t.exclude=Object.freeze(t.exclude.map(n=>{var m;return Object.freeze({tabId:n.tabId,...n.key!==void 0?{key:n.key}:{key:(m=d.tabs.find(v=>v.tabId===n.tabId))==null?void 0:m.key}})}))}if(t.type==="open"){const n=t.input;if(!n)throw new u.NavigationError("INVALID_OPERATION","导航目标无效");t.input=Object.freeze({appId:n.appId,tabId:n.tabId,title:n.title,route:n.route,...n.activeMenuId!==void 0?{activeMenuId:n.activeMenuId}:{}})}if("closeFallback"in t&&t.closeFallback!==void 0){if(!r(t.closeFallback))throw new u.NavigationError("INVALID_OPERATION","关闭回落引用无效");const n=t.closeFallback;t.closeFallback=Object.freeze({tabId:n.tabId,key:n.key??((o=d.tabs.find(m=>m.tabId===n.tabId))==null?void 0:o.key)})}"fallback"in t&&t.fallback&&(t.fallback=Object.freeze({...t.fallback})),"target"in t&&t.target&&Object.freeze(t.target),"anchor"in t&&t.anchor&&Object.freeze(t.anchor),Object.freeze(t);const s=n=>d.tabs.find(m=>n?m.tabId===n.tabId&&(!n.key||m.key===n.key):m.tabId===d.activeTabId),w="target"in t?s(t.target):void 0,E=t.type==="closeMany"&&t.scope!=="all"?t.anchor?s(t.anchor):c?d.tabs.find(n=>n.key===c):s():void 0,S=s(),f=t.type==="open"?d.tabs.find(n=>n.tabId===t.input.tabId):void 0,I=h&&c?d.tabs.find(n=>n.key===c):void 0,k=I&&a?Object.freeze({appId:a,tab:Object.freeze({...I})}):void 0,x=h&&t.type==="open"&&!!a&&t.input.appId!==a,j=()=>{if(!(!x||t.type!=="open")){try{const n=k&&(be==null?void 0:be(k,t.input));if(n===!0)return;n&&(typeof n=="object"||typeof n=="function")&&Promise.resolve(n).catch(()=>{})}catch{}throw new u.NavigationError("FORBIDDEN","无权跨应用导航(权限策略必须同步返回 true)")}},H=n=>!a||n.appId===a;if(w&&!H(w)||E&&!H(E)||t.type==="open"&&a&&t.input.appId!==a&&!x||t.type==="back"&&t.fallback&&a&&t.fallback.appId!==a)throw new u.NavigationError("FORBIDDEN","页面只能操作本应用");if(t.type==="closeMany"&&!["all","others","left","right"].includes(t.scope))throw new u.NavigationError("INVALID_OPERATION","批量范围无效");const _=E?d.tabs.indexOf(E):-1,Y=t.type==="closeMany"?d.tabs.filter((n,m)=>{var v;return H(n)&&!((v=t.exclude)!=null&&v.some(F=>F.tabId===n.tabId&&F.key===n.key))&&(t.scope==="all"||_>=0&&(t.scope==="others"?n.key!==E.key:t.scope==="left"?m<_:m>_))}):[],g=t.type==="back"?w??(!("target"in t)||!t.target?S:void 0):w,C=async()=>{var Ae,Ne,Te,Pe,Me,De;const n=i=>({status:i,affected:[]});if(P)throw new u.NavigationError("HOST_DISPOSED","MicroHost 已销毁");if((Ae=t.signal)!=null&&Ae.aborted)return n("stale");if(c&&!y&&(b!=null&&b()||!d.tabs.some(i=>i.key===c)))throw new u.NavigationError("PAGE_RELEASED","页面上下文已释放");if(t.type==="open"&&(f?!d.tabs.some(i=>i.key===f.key):d.tabs.some(i=>i.tabId===t.input.tabId))||g&&!d.tabs.some(i=>i.key===g.key)||E&&!d.tabs.some(i=>i.key===E.key)||Y.some(i=>!d.tabs.some(O=>O.key===i.key)))return n("stale");if(t.type!=="open"&&t.type!=="closeMany"&&!g)return n("noop");const m=G;if(j(),c&&!y&&(b!=null&&b()||!d.tabs.some(i=>i.key===c)))throw new u.NavigationError("PAGE_RELEASED","页面上下文已释放");if(G!==m||P||(Ne=t.signal)!=null&&Ne.aborted)return n("stale");const v=d,F=G;let p=[...d.tabs],l,M=[],$=d.activeTabId;try{if(t.type==="open"&&(l=ue(t.input,!0),f&&f.key!==l.key?(M=[f],p=p.map(i=>i.key===f.key?l:i)):p.some(i=>i.key===l.key)||p.push(l),$=l.tabId),t.type==="activate"&&(l=g,q(l),$=l.tabId),t.type==="refresh"&&(q(g),M=[g],l=Object.freeze({...g,key:`${g.tabId}:${++le}`}),p=p.map(i=>i.key===g.key?l:i)),(t.type==="close"||t.type==="back")&&(M=[g]),t.type==="closeMany"&&(M=Y),M.length&&t.type!=="refresh"&&t.type!=="open"){const i=new Set(M.map(A=>A.key)),O=v.tabs.findIndex(A=>A.tabId===v.activeTabId);if(p=p.filter(A=>!i.has(A.key)),t.type==="back"){const A=W.get(g.key);l=p.find(T=>T.key===A&&H(T)),!l&&t.fallback&&(l=ue(t.fallback),i.has(l.key)&&(l=Object.freeze({...l,key:`${l.tabId}:${++le}`})),p.some(T=>T.key===l.key)||p.push(l));const N=v.tabs.filter(T=>H(T)),L=N.findIndex(T=>T.key===g.key);l??(l=N.slice(0,L).reverse().find(T=>p.some(ne=>ne.key===T.key))??N.slice(L+1).find(T=>p.some(ne=>ne.key===T.key))),l??(l=p[Math.min(O,p.length-1)]),l&&q(l),$=(l==null?void 0:l.tabId)??null}else if(M.some(A=>A.tabId===$)){const A="closeFallback"in t?t.closeFallback:void 0,N=A?p.find(L=>L.tabId===A.tabId&&L.key===A.key&&H(L)):void 0;N&&(q(N),l=N),$=((Te=N??p[Math.min(O,p.length-1)])==null?void 0:Te.tabId)??null}}}catch(i){throw i instanceof u.NavigationError?i:new u.NavigationError("INVALID_OPERATION","导航参数无效")}if(p.length===v.tabs.length&&p.every((i,O)=>i.key===v.tabs[O].key)&&$===v.activeTabId)return n("noop");const he=[...M],ce=v.tabs.find(i=>i.tabId===v.activeTabId);ce&&ce.tabId!==$&&!he.some(i=>i.key===ce.key)&&he.push(ce);for(const i of he)for(const O of[...re.get(i.key)??[]]){let A;const N=t.signal;let L=()=>{};try{const T=Promise.resolve(O({operation:t,tab:i})),ne=new Promise(Se=>{if(!N)return;const je=()=>Se();N.addEventListener("abort",je,{once:!0}),L=()=>N.removeEventListener("abort",je),N.aborted&&Se()});A=await Promise.race([T,ne])}catch{if(N!=null&&N.aborted)return n("stale");throw new u.NavigationError("GUARD_FAILED","离页守卫执行失败")}finally{L()}if(G!==F||P||(Pe=t.signal)!=null&&Pe.aborted)return n("stale");if(A===!1)return n("cancelled")}if(G!==F||P||(Me=t.signal)!=null&&Me.aborted)return n("stale");if(b!=null&&b())throw new u.NavigationError("PAGE_RELEASED","页面上下文已释放");if(j(),l&&q(l),b!=null&&b())throw new u.NavigationError("PAGE_RELEASED","页面上下文已释放");if(G!==F||P||(De=t.signal)!=null&&De.aborted)return n("stale");if(d={tabs:p,activeTabId:$},t.type==="open"&&l&&c&&!x&&l.key!==c&&!W.has(l.key)&&W.set(l.key,c),t.type==="refresh"&&l){const i=W.get(g.key);i&&W.set(l.key,i)}for(const i of M)me(i.key);for(const i of new Set(M.map(O=>O.appId)))if(!p.some(O=>O.appId===i)){for(const O of[...B.get(i)??[]])U(O);B.delete(i),te.delete(i)}const ye=[...M,...p.filter(i=>!v.tabs.some(O=>O.key===i.key))];return!ye.length&&l&&ye.push(l),oe(),{status:"committed",affected:ye.map(i=>({tabId:i.tabId,key:i.key})),...l?{tab:l}:{}}},J=Ee.then(C);return Ee=J.catch(()=>{}),J}catch(t){return Promise.reject(t)}}function Ve(e){if(D(),!Q.has(e))throw new Error(`未知子应用: ${e}`);const a=te.get(e);if(a)return a;let c=!0;const y=new Set([()=>{c=!1}]);B.set(e,y);async function b(o,t,r,s={}){const w=()=>{if(P)throw new u.HostCallError("HOST_DISPOSED","MicroHost 已销毁");if(r&&(r.released()||!d.tabs.some(j=>j.key===r.key)))throw new u.HostCallError("PAGE_RELEASED","页面上下文已释放");if(!c)throw new u.HostCallError("BRIDGE_RELEASED","应用 bridge 已释放")};w();const E=ve.get(o);if(!E)throw new u.HostCallError("UNKNOWN_CAPABILITY",`未注册宿主能力: ${o}`);if(E.apps&&!E.apps.has(e))throw new u.HostCallError("FORBIDDEN",`应用无权调用宿主能力: ${o}`);const S=s.timeoutMs??se;if(!Number.isFinite(S)||S<=0||S>2147483647)throw new TypeError("timeoutMs 必须为不超过2147483647的正有限数");const f=s.signal;if(f!=null&&f.aborted)throw new u.HostCallError("CANCELLED","宿主能力调用已取消");const I=new AbortController,k=r?d.tabs.find(j=>j.key===r.key):void 0,x=Object.freeze({appId:e,signal:I.signal,...k?{tab:Object.freeze({...k})}:{},isActive:()=>!I.signal.aborted&&!P&&c&&(!r||!r.released()&&d.tabs.some(j=>j.key===r.key))});return new Promise((j,H)=>{let _=!1,Y;const g=()=>{Y!==void 0&&clearTimeout(Y),f==null||f.removeEventListener("abort",J),y.delete(n),r==null||r.cleanups.delete(n)},C=v=>{_||(_=!0,g(),H(v),I.abort())},J=()=>C(new u.HostCallError("CANCELLED","宿主能力调用已取消")),n=()=>{try{w()}catch(v){C(v)}};if(y.add(n),r==null||r.cleanups.add(n),f==null||f.addEventListener("abort",J,{once:!0}),Y=setTimeout(()=>C(new u.HostCallError("TIMEOUT","宿主能力调用超时")),S),f!=null&&f.aborted){J();return}let m;try{m=E.handler(x,...t)}catch{C(new u.HostCallError("HANDLER_FAILED",`宿主能力执行失败: ${o}`));return}Promise.resolve(m).then(v=>{if(!_){try{w()}catch(F){C(F);return}_=!0,g(),j(v)}},()=>C(new u.HostCallError("HANDLER_FAILED",`宿主能力执行失败: ${o}`)))})}const h={appId:e,getState:ie,callHost(o,...t){return b(o,t)},callHostWithOptions(o,t,...r){return b(o,r,void 0,t)},protocol(){if(D(),!c)throw new Error("应用 bridge 已释放");return Object.freeze({version:"1.0",capabilities:Object.freeze([...ve].filter(([,o])=>!o.apps||o.apps.has(e)).map(([o])=>o)),features:Object.freeze(["call-options","session","event-metadata","navigate-to"])})},negotiate(o={}){const t=h.protocol(),r=Object.freeze((o.capabilities??[]).filter(s=>!t.capabilities.includes(s)));return Object.freeze({compatible:(!o.version||o.version===t.version)&&!r.length,protocol:t,missingCapabilities:r})},getSession(){if(D(),!c)throw new Error("应用 bridge 已释放");return K},subscribeSession(o){const t=h.getSession(),r=w=>o(w),s=()=>{X.delete(r),y.delete(s)};return X.add(r),y.add(s),U(()=>o(t)),s},subscribe(o){if(!c)throw new Error("应用 bridge 已释放");const t=ke(o),r=()=>{t(),y.delete(r)};return y.add(r),r},ready(o){const t=s=>{const w=s.tabs.find(E=>E.tabId===s.activeTabId&&E.appId===e);w&&o(w)},r=h.subscribe(t);try{t(ie())}catch(s){throw r(),s}return r},bindPage(o){const t=d.tabs.find(r=>r.tabId===o&&r.appId===e);if(!t)throw new Error("页面不属于该应用");return h.page(t.key)},page(o){if(D(),!c)throw new Error("应用 bridge 已释放");const t=d.tabs.find(f=>f.key===o&&f.appId===e);if(!t)throw new Error("页面上下文不存在或不属于该应用");let r=!1;const s=new Set,w=()=>{if(P)throw new u.NavigationError("HOST_DISPOSED","MicroHost 已销毁");if(r||!d.tabs.some(f=>f.key===o))throw new u.NavigationError("PAGE_RELEASED","页面上下文已释放")},E=()=>{var f;r=!0,s.forEach(I=>I()),s.clear(),(f=V.get(o))==null||f.delete(E)};let S=V.get(o);return S||V.set(o,S=new Set),S.add(E),{get tab(){return d.tabs.find(f=>f.key===o)??t},callHost(f,...I){return b(f,I,{key:o,released:()=>r,cleanups:s})},callHostWithOptions(f,I,...k){return b(f,k,{key:o,released:()=>r,cleanups:s},I)},async openTab(f){w();const I=await Z({type:"open",input:f},e,o,!1,()=>r);if(I.tab)return I.tab;const k=d.tabs.find(x=>x.tabId===f.tabId&&x.appId===e);if(I.status==="noop"&&k)return k;throw new u.NavigationError(I.status==="cancelled"?"CANCELLED":"STALE","导航未提交")},async navigateTo(f,I={}){return w(),Z({type:"open",input:f,signal:I.signal},e,o,!1,()=>r,!0)},async closeCurrent(){return P?!1:(await Z({type:"close",target:{tabId:t.tabId,key:o}},e,void 0,!0)).status==="committed"},beforeLeave(f){if(w(),typeof f!="function")throw new u.NavigationError("INVALID_OPERATION","离页守卫必须为函数");let I=re.get(o);I||re.set(o,I=new Set),I.add(f);const k=()=>{I.delete(f),s.delete(k)};return s.add(k),k},async refreshCurrent(){return w(),Z({type:"refresh",target:{tabId:t.tabId,key:o}},e,o,!1,()=>r)},async back(f){return w(),Z({type:"back",target:{tabId:t.tabId,key:o},fallback:f},e,o,!1,()=>r)},async operate(f){return w(),Z(f,e,o,!1,()=>r)},emit(f,I){w(),ae.emit(f,{payload:I,metadata:Object.freeze({source:"app",appId:e,tabKey:o})})},on(f,I){w();const k=de.on(f,I);return s.add(k),()=>{k(),s.delete(k)}},dispose:E}}};return te.set(e,h),h}function Be(){if(!P){P=!0;for(const e of V.values())[...e].forEach(a=>a());V.clear(),re.clear(),W.clear();for(const e of B.values())[...e].forEach(a=>a());B.clear(),ee.clear(),X.clear(),te.clear(),de.clear(),d={tabs:[],activeTabId:null}}}return{apps:[...Q.values()],getState:ie,subscribe:ke,openTab:He,activateTab:Ce,closeTab:Fe,updateTab:_e,updateSession:Re,operate:$e,bridge:Ve,bus:de,dispose:Be}}exports.createMicroHost=Ge;
|