@axewo/fin-micro 0.0.0-stage → 1.0.0
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 +125 -2
- package/cli/index.mjs +240 -0
- package/dist/browser.cjs +1 -0
- package/dist/browser.mjs +167 -0
- package/dist/core.cjs +1 -0
- package/dist/core.mjs +640 -0
- package/dist/iframe.cjs +1 -0
- package/dist/iframe.mjs +146 -0
- package/dist/index.cjs +1 -0
- package/dist/index.mjs +28 -0
- package/dist/router-view-impl-C1KG6mxu.mjs +76 -0
- package/dist/router-view-impl-CgLEalx4.cjs +1 -0
- package/dist/runtime.cjs +1 -0
- package/dist/runtime.mjs +148 -0
- package/dist/sdk.cjs +1 -0
- package/dist/sdk.mjs +82 -0
- package/dist/types/browser/index.d.ts +42 -0
- package/dist/types/core/index.d.ts +63 -0
- package/dist/types/iframe/index.d.ts +77 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/runtime/index.d.ts +21 -0
- package/dist/types/runtime/lifecycle.d.ts +23 -0
- package/dist/types/sdk/index.d.ts +338 -0
- package/dist/types/vue/index.d.ts +91 -0
- package/dist/types/vue/router-view-impl.d.ts +41 -0
- package/dist/types/vue/router-view.d.ts +42 -0
- package/dist/types/vue/tab-context.d.ts +6 -0
- package/dist/vue.cjs +1 -0
- package/dist/vue.mjs +166 -0
- package/package.json +91 -3
- package/scaffold/base/index.html +12 -0
- package/scaffold/base/package.json.template +29 -0
- package/scaffold/base/src/components/.gitkeep +1 -0
- package/scaffold/base/src/components/AppProviders.vue +25 -0
- package/scaffold/base/src/hooks/.gitkeep +1 -0
- package/scaffold/base/src/layout/.gitkeep +1 -0
- package/scaffold/base/src/locales/.gitkeep +1 -0
- package/scaffold/base/src/locales/index.ts +7 -0
- package/scaffold/base/src/main.ts +14 -0
- package/scaffold/base/src/router/.gitkeep +1 -0
- package/scaffold/base/src/stores/.gitkeep +1 -0
- package/scaffold/base/src/styles/index.css +18 -0
- package/scaffold/base/src/types/.gitkeep +1 -0
- package/scaffold/base/src/utils/.gitkeep +1 -0
- package/scaffold/base/src/utils/requestUtils.ts +123 -0
- package/scaffold/base/src/views/.gitkeep +1 -0
- package/scaffold/base/tsconfig.json +16 -0
- package/scaffold/base/vite.config.ts +8 -0
- package/scaffold/host/.env.example +5 -0
- package/scaffold/host/src/App.vue +16 -0
- package/scaffold/host/src/bootstrap.ts +18 -0
- package/scaffold/host/src/hooks/usePlatformSession.ts +59 -0
- package/scaffold/host/src/layout/HostHeader.vue +9 -0
- package/scaffold/host/src/layout/HostLayout.vue +124 -0
- package/scaffold/host/src/layout/HostMenu.vue +14 -0
- package/scaffold/host/src/layout/HostTabs.vue +14 -0
- package/scaffold/host/src/layout/HostWorkspace.vue +14 -0
- package/scaffold/host/src/platform.ts +208 -0
- package/scaffold/host/src/types/platform.ts +12 -0
- package/scaffold/host/src/utils/request.ts +15 -0
- package/scaffold/host/src/views/LoginView.vue +38 -0
- package/scaffold/vue/src/App.vue +24 -0
- package/scaffold/vue/src/bootstrap.ts +71 -0
- package/scaffold/vue/src/bridge/session.ts +16 -0
- package/scaffold/vue/src/main.ts +10 -0
- package/scaffold/vue/src/router/index.ts +14 -0
- package/scaffold/vue/src/views/DetailView.vue +48 -0
- package/scaffold/vue/src/views/ListView.vue +51 -0
- package/scaffold/vue/src/views/NotFoundView.vue +1 -0
package/README.md
CHANGED
|
@@ -1,3 +1,126 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @axewo/fin-micro
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
统一页签式微前端库:无框架 core/SDK、Wujie 应用运行时、Vue 页级缓存适配、浏览器恢复和独立 iframe RPC。
|
|
4
|
+
|
|
5
|
+
## 完整使用手册
|
|
6
|
+
|
|
7
|
+
详细文档已接入仓库 VitePress 文档站的“微前端”栏目:
|
|
8
|
+
|
|
9
|
+
- [总览与设计边界](../../apps/docs/micro/index.md)
|
|
10
|
+
- [快速开始与项目结构](../../apps/docs/micro/getting-started.md)
|
|
11
|
+
- [路由参数与跨应用导航](../../apps/docs/micro/navigation.md)
|
|
12
|
+
- [页面缓存与浏览器恢复](../../apps/docs/micro/cache-and-browser.md)
|
|
13
|
+
- [宿主通信、会话与认证](../../apps/docs/micro/communication.md)
|
|
14
|
+
- [独立 iframe RPC](../../apps/docs/micro/iframe.md)
|
|
15
|
+
- [API 索引与排错验收](../../apps/docs/micro/reference.md)
|
|
16
|
+
|
|
17
|
+
在仓库根执行:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
pnpm --filter @axewo/fin-micro build
|
|
21
|
+
pnpm micro:dev
|
|
22
|
+
pnpm --filter @axewo/fin-docs dev
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
文档路径为 `/micro/`。模板位于 `apps/micro-host` 和 `apps/micro-vue`,首次接入建议先运行模板,再替换业务视图与权限规则。
|
|
26
|
+
|
|
27
|
+
## 按需获取当前 Token
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { getToken } from '@axewo/fin-micro/sdk'
|
|
31
|
+
const token = await getToken({ bridge, signal, timeoutMs: 5000 })
|
|
32
|
+
// 已登录 Cookie 模式返回 null;未登录、无桥、会话已变化均有独立错误码。
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
基座通过 `createTokenCapability({ apps, getSession, provider })` 显式授权可信应用,每次读取私有凭据,异步后复查会话 revision 与调用存活。Token 不进入公开会话或事件广播。CLI host 模板默认保留 HttpOnly Cookie,后台 login/session 可选返回 `accessToken`;退出、初始化失败及卸载清除内存。base 请求只在安装 provider 后动态注入,vue bootstrap 在嵌入挂载时安装、卸载时取消并还原;独立运行或 host 不隐式读取 bridge。默认仅可信 `/api/` 请求可携带 Token,绝对 URL 和被覆盖的 baseURL 不自动注入。详见[认证接线与安全边界](../../apps/docs/micro/communication.md)。
|
|
36
|
+
|
|
37
|
+
## 子应用生命周期与环境判断
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { isMicroFrontend, setupWujieLifecycle } from '@axewo/fin-micro/runtime'
|
|
41
|
+
import { mount, unmount } from './bootstrap'
|
|
42
|
+
|
|
43
|
+
// 仅检测当前 Wujie 环境,不代表 bridge 连接、身份或权限。
|
|
44
|
+
console.log(isMicroFrontend())
|
|
45
|
+
const dispose = setupWujieLifecycle({ mount, unmount })
|
|
46
|
+
if (import.meta.hot) import.meta.hot.dispose(dispose)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`setupWujieLifecycle` 统一注册 Wujie 回调、通知嵌入挂载、独立启动、unload 和幂等清理;重复注册会释放旧注册。`dispose()` 返回 Promise,可等待清理完成。错误会报告至 console.error,显式调用生命周期回调可接收失败。bootstrap 自身仍须在 unmount 时使异步初始化失效,封装不能撤销回调内部副作用。SSR 检测为 false,注册不启动应用。环境检测不会把普通 iframe 或自行注册的挂载函数误认为微前端,不探测其他未适配的微前端框架。
|
|
50
|
+
|
|
51
|
+
基座使用普通启动入口,只有 vue 子应用模板接入上述封装;不再在 base 中注册子应用专属全局回调。
|
|
52
|
+
|
|
53
|
+
## 单包创建项目(本地首版)
|
|
54
|
+
|
|
55
|
+
运行库与创建命令使用同一个包,不另发 create-fin-micro。包发布后可执行以下命令;当前开发不代表已经发布至 npm:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
# 交互式:依次输入目录、选择 host/vue、输入子应用 app-id 和端口
|
|
59
|
+
npx @axewo/fin-micro@latest create
|
|
60
|
+
|
|
61
|
+
# 参数式:适合脚本和 CI,不触发问答
|
|
62
|
+
npx @axewo/fin-micro create my-host --template host
|
|
63
|
+
npx @axewo/fin-micro create order-app --template vue --app-id order --port 7101
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
仓库内无需发布即可验证:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
node packages/fin-micro/cli/index.mjs create ./my-host --template host
|
|
70
|
+
node packages/fin-micro/cli/index.mjs create ./order-app --template vue --app-id order
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`host` 默认端口 7100,`vue` 默认端口 7101;项目名与 appId 只接受小写字母、数字和连字符。子应用示例建议指定 `--app-id business`,与基座默认登记保持一致;或调整基座的环境变量。目标目录必须不存在,父目录必须已存在;命令不覆盖文件、不自动安装依赖、不自动注册远端应用、不执行发布。进入生成项目后运行 `pnpm install`、`pnpm dev` 和 `pnpm build`。生成源码属于项目,CLI 不升级或覆盖既有源码。
|
|
74
|
+
|
|
75
|
+
基座联调时,在 `.env.local` 显式配置 `VITE_AUTH_DEMO=true` 才能启用开发演示身份;正式模式请求 `/api/session`、`/api/login`、`/api/menus`、`/api/logout`,协议与替换点见快速开始章节。后台菜单只提供授权路由,应用执行地址来自可信本地注册表。
|
|
76
|
+
|
|
77
|
+
`scaffold/base` 保存公共骨架,`scaffold/host`、`scaffold/vue` 按类型覆盖差异文件;两类模板统一接入 fin-ui。基座仅包含登录、公开用户信息、菜单与微前端平台编排,不包含系统管理业务。认证示例不是生产认证,需替换接口适配并补齐服务端鉴权;应用入口、appId、菜单权限需按实际配置。
|
|
78
|
+
|
|
79
|
+
运行库通过 npm 依赖导入,不复制 core 源码。公开包使用 files 白名单,仅交付运行库 JS、类型声明、创建命令与可公开的骨架源码,不交付运行库 src 或 sourcemap。编译 JavaScript 仍然可被分析,不应含密钥或保密逻辑。库升级需回归、构建、部署,无需重新创建项目;模板更新不会自动迁移生成项目。
|
|
80
|
+
|
|
81
|
+
生成项目的 `src` 统一具有 `stores/router/layout/locales/hooks/utils/types/components/views/styles`。布局放 layout,页面放 views,公共样式放 styles;未启用的状态管理仅保留目录,不新增 plugins 目录。公共 `components/AppProviders.vue` 使用 `FConfigProvider` 与 `ElConfigProvider` 包裹 host/vue 全树,以 `useFinLocale()` 的全局 locale 响应式选择 fin-ui 的 `zhCN/en` 和 Element Plus 语言包,并同步 size(默认 default)。`locales/index.ts` 通过 fin-ui 重导出的 `installFinI18n` 在每次 createApp 后、mount 前独立安装,默认 zh-CN;不共享模块级 i18n 实例,`config.locale` 仅是语言包数据,不负责安装插件。模板显式安装 fin-locale 要求的 peer 依赖 `vue-i18n ^9.14.0`;业务子组件可用 `useFinLocale().locale.value = 'en'` 切换当前应用语言,不自动跨应用广播或翻译硬编码业务文案。公共 `base/src/utils/requestUtils.ts` 同时生成到基座与 Vue 子应用,通过 `@axewo/fin-core/http/axios` 为各项目创建独立 `AxiosHttpClient`,提供请求/响应拦截器的应用级替换点。基座平台适配使用专属 `utils/request.ts` 的独立 Axios 实例,配置原生 timeout/withCredentials;平台显式处理 Axios 401,并以局部 Token 请求菜单/注销。子应用不生成该平台实例。业务中使用 `import { http } from '../utils/requestUtils'`(未配置别名时使用相对路径),调用 `http.get/post`;返回整个响应体,业务 code/data 不自动解包。子应用拦截器通过 `http.interceptors` 配置,响应拦截器需保留 AxiosResponse,Token/401策略由业务明确接入。依赖含该Axios入口的 fin-core 2.1.0 源码版本,公开安装前需先发布该依赖;此前旧2.1.0候选tgz不包含Axios入口,不能用于本次模板。本地联合安装前需重新构建、打包 core 与 micro,旧候选包不能代表本次源码。
|
|
82
|
+
|
|
83
|
+
地址栏修改仍经过基座 canOpen;子应用本地路由不是权限表。独立子应用当前为演示,直接访问路由可以显示示例页;生产独立运行需接身份/权限检查或禁用独立业务模式。前端检查不能代替后台 API 的登录、操作权限与资源归属验证;隐藏页面可有显式授权,不能只以菜单是否展示判断访问权限。
|
|
84
|
+
|
|
85
|
+
## 包入口
|
|
86
|
+
|
|
87
|
+
| 入口 | 用途 |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `/sdk` | 框架无关类型、桥获取、调用/导航契约 |
|
|
90
|
+
| `/core` | 应用登记、宿主状态、导航事务与能力白名单 |
|
|
91
|
+
| `/runtime` | Wujie 单 appId 单实例挂载和清理 |
|
|
92
|
+
| `/vue` | Vue 容器、路由连接、页签缓存与页面 SDK |
|
|
93
|
+
| `/browser` | 基座 URL 和公开导航元信息恢复 |
|
|
94
|
+
| `/iframe` | 显式窗口来源校验的独立 RPC |
|
|
95
|
+
|
|
96
|
+
非 Vue 应用使用独立子入口;根入口包含 Vue 适配。
|
|
97
|
+
|
|
98
|
+
Vue 子应用在本地维护正常 Vue Router 路由表,基座只传递 appId 和应用内 route,不传组件文件路径。缓存内容直接使用 `MicroRouteView`,不必复制路由解析器:
|
|
99
|
+
|
|
100
|
+
```vue
|
|
101
|
+
<MicroTabCache :tabs="tabs" :active-key="activeKey">
|
|
102
|
+
<MicroRouteView />
|
|
103
|
+
</MicroTabCache>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`MicroRouteView` 需要应用安装 Vue Router `^4.5.0` 并调用 `app.use(router)`;提供固定页面路由上下文、懒加载、路由记录 redirect、可见错误及重试。它不执行全局导航守卫、不修改宿主页签;嵌入模式不要同时对每个页签调用共享 router.replace,跳转和离页确认仍使用 PageSDK。详细示例与安全边界见缓存章节。
|
|
107
|
+
|
|
108
|
+
## 使用前必须知道
|
|
109
|
+
|
|
110
|
+
- 同 appId 只运行一个实例,应用内通过不同 tabId 打开多个路由页;key 代表具体页面生命周期。
|
|
111
|
+
- `openTab/operate` 保持应用隔离;跨应用打开使用 `page.navigateTo`,由宿主 `canNavigate` 显式授权并继续检查 `canOpen`。
|
|
112
|
+
- 导航 committed 不代表资源加载完成;运行时错误与重试另行处理。
|
|
113
|
+
- 公开会话不带 Token,宿主能力使用显式白名单;同窗口桥不是恶意代码安全沙箱。
|
|
114
|
+
- 超时/取消不回滚业务副作用;缓存和 sessionStorage 导航恢复不代替草稿持久化。
|
|
115
|
+
- iframe RPC 不是完整 AppBridge、守卫或缓存的透明代理。
|
|
116
|
+
|
|
117
|
+
## 验证
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
pnpm --filter @axewo/fin-micro typecheck
|
|
121
|
+
pnpm --filter @axewo/fin-micro test
|
|
122
|
+
pnpm --filter @axewo/fin-micro build
|
|
123
|
+
pnpm --filter @axewo/fin-docs build
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
真实浏览器部署验收不能由 happy-dom 单元测试替代,具体检查清单见手册。
|
package/cli/index.mjs
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 离线脚手架入口:只读取随包发布的模板,不安装依赖、不请求网络。
|
|
4
|
+
* 运行时入口不应导入本文件,只有命令行和测试使用这里的导出。
|
|
5
|
+
*/
|
|
6
|
+
import { lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { createInterface } from 'node:readline/promises';
|
|
10
|
+
|
|
11
|
+
const PACKAGE_ROOT = fileURLToPath(new URL('../', import.meta.url));
|
|
12
|
+
const NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,213}$/;
|
|
13
|
+
const HELP = `fin-micro 离线项目创建工具
|
|
14
|
+
|
|
15
|
+
用法:
|
|
16
|
+
fin-micro create
|
|
17
|
+
fin-micro create <directory> --template host|vue [--app-id <id>] [--port <port>]
|
|
18
|
+
fin-micro --help
|
|
19
|
+
|
|
20
|
+
说明:
|
|
21
|
+
目标目录必须不存在,父目录必须真实存在。
|
|
22
|
+
目录名和应用 ID 仅支持小写字母、数字与连字符,首字符为字母或数字。
|
|
23
|
+
host 默认端口 7100,vue 默认端口 7101;端口范围为 1–65535。
|
|
24
|
+
不自动安装依赖,不访问网络。
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
/** 检查路径本身是否存在;悬空符号链接同样视为已存在。 */
|
|
28
|
+
function exists(path) {
|
|
29
|
+
try {
|
|
30
|
+
lstatSync(path);
|
|
31
|
+
return true;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error.code === 'ENOENT') return false;
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 解析有限命令集,不产生文件系统写入。
|
|
40
|
+
* @param {string[]} args 不含 node 和脚本路径的命令参数。
|
|
41
|
+
* @param {{ partial?: boolean }} config partial 仅允许缺少目录/模板,供后续交互补全;不放宽选项语法。
|
|
42
|
+
* @returns {object} 创建选项或 { help: true };默认值及路径安全在创建前统一校验。
|
|
43
|
+
* @throws {Error} 未知命令/选项、重复选项、缺值或严格模式缺少必要参数。
|
|
44
|
+
*/
|
|
45
|
+
export function parseArgs(args, { partial = false } = {}) {
|
|
46
|
+
if (args.length === 1 && (args[0] === '--help' || args[0] === '-h')) return { help: true };
|
|
47
|
+
if (args[0] !== 'create') throw new Error('请使用 create 命令,或通过 --help 查看用法。');
|
|
48
|
+
if (args.length === 2 && (args[1] === '--help' || args[1] === '-h')) return { help: true };
|
|
49
|
+
const directory = args[1] && !args[1].startsWith('-') ? args[1] : undefined;
|
|
50
|
+
if (!directory && !partial) throw new Error('请指定目标目录。');
|
|
51
|
+
const options = directory ? { directory } : {};
|
|
52
|
+
const names = { '--template': 'template', '--app-id': 'appId', '--port': 'port' };
|
|
53
|
+
for (let index = directory ? 2 : 1; index < args.length; index += 2) {
|
|
54
|
+
const option = args[index];
|
|
55
|
+
if (!Object.hasOwn(names, option)) throw new Error(`未知选项:${option}`);
|
|
56
|
+
const key = names[option];
|
|
57
|
+
if (Object.hasOwn(options, key)) throw new Error(`选项不可重复:${option}`);
|
|
58
|
+
const value = args[index + 1];
|
|
59
|
+
if (!value || value.startsWith('--')) throw new Error(`选项缺少值:${option}`);
|
|
60
|
+
options[key] = value;
|
|
61
|
+
}
|
|
62
|
+
if (!options.template && !partial) throw new Error('必须指定 --template host 或 --template vue。');
|
|
63
|
+
return options;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 递归读取可信模板目录,拒绝符号链接及非常规文件,避免越界读取。 */
|
|
67
|
+
function collectTemplate(root, current = root, files = new Map()) {
|
|
68
|
+
const stat = lstatSync(current);
|
|
69
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
70
|
+
throw new Error(`模板目录无效:${current}`);
|
|
71
|
+
}
|
|
72
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
73
|
+
const source = join(current, entry.name);
|
|
74
|
+
if (entry.isSymbolicLink()) throw new Error(`模板不可包含符号链接:${source}`);
|
|
75
|
+
if (entry.isDirectory()) collectTemplate(root, source, files);
|
|
76
|
+
else if (entry.isFile()) {
|
|
77
|
+
const target = relative(root, source).split(sep).map((part) => part === 'package.json.template' ? 'package.json' : part).join(sep);
|
|
78
|
+
if (files.has(target)) throw new Error(`同一模板含重复输出文件:${target}`);
|
|
79
|
+
files.set(target, readFileSync(source, 'utf8'));
|
|
80
|
+
} else throw new Error(`模板包含不支持的文件:${source}`);
|
|
81
|
+
}
|
|
82
|
+
return files;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 校验选项和真实父路径,不创建文件;交互重输与最终写入前复核共用此边界。 */
|
|
86
|
+
function validateOptions(options, cwd) {
|
|
87
|
+
const { directory, template } = options;
|
|
88
|
+
if (template !== 'host' && template !== 'vue') throw new Error('模板只能是 host 或 vue。');
|
|
89
|
+
if (typeof directory !== 'string' || !directory || directory.endsWith('/') || directory.endsWith('\\')) {
|
|
90
|
+
throw new Error('目标目录必须包含有效目录名,且不能以分隔符结尾。');
|
|
91
|
+
}
|
|
92
|
+
const target = resolve(cwd, directory);
|
|
93
|
+
const projectName = basename(target);
|
|
94
|
+
if (!NAME_PATTERN.test(projectName)) throw new Error('目录名无效:仅支持小写字母、数字和连字符,长度不超过 214。');
|
|
95
|
+
const appId = options.appId ?? projectName;
|
|
96
|
+
if (!NAME_PATTERN.test(appId)) throw new Error('应用 ID 无效:仅支持小写字母、数字和连字符,长度不超过 214。');
|
|
97
|
+
const port = String(options.port ?? (template === 'host' ? 7100 : 7101));
|
|
98
|
+
if (!/^[1-9][0-9]{0,4}$/.test(port) || Number(port) > 65535) throw new Error('端口必须是 1–65535 的十进制整数。');
|
|
99
|
+
if (exists(target)) throw new Error(`目标已存在,拒绝覆盖:${target}`);
|
|
100
|
+
const parent = dirname(target);
|
|
101
|
+
if (!exists(parent) || !lstatSync(parent).isDirectory()) {
|
|
102
|
+
throw new Error('目标的父路径必须是真实存在的目录,且不能通过符号链接访问。');
|
|
103
|
+
}
|
|
104
|
+
// Windows 路径比较忽略大小写,但仍拒绝 junction 或其他符号链接造成的路径跳转。
|
|
105
|
+
const canonicalParent = realpathSync(parent);
|
|
106
|
+
const sameParent = process.platform === 'win32'
|
|
107
|
+
? canonicalParent.toLowerCase() === resolve(parent).toLowerCase()
|
|
108
|
+
: canonicalParent === resolve(parent);
|
|
109
|
+
if (!sameParent) throw new Error('目标的父路径必须是真实存在的目录,且不能通过符号链接访问。');
|
|
110
|
+
return { target, directory, template, projectName, appId, port };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 组合公共骨架和差异模板,替换占位符后排他写入新目录;不会安装依赖或访问网络。
|
|
115
|
+
* @param {object} options 目录、host/vue模板及可选appId/端口。
|
|
116
|
+
* @param {object} environment cwd为已有真实目录,packageRoot仅用于测试注入可信素材。
|
|
117
|
+
* @returns {object} 实际目标路径及归一化配置。
|
|
118
|
+
* @throws {Error} 参数无效、目录已存在、模板冲突/非法占位符或文件系统错误。
|
|
119
|
+
* 可预见错误在写入前失败;磁盘故障或外部并发篡改仍可能留下部分新文件,不递归删除以免误删用户内容。
|
|
120
|
+
*/
|
|
121
|
+
export function createProject(options, { cwd = process.cwd(), packageRoot = PACKAGE_ROOT } = {}) {
|
|
122
|
+
const { target, directory, template, projectName, appId, port } = validateOptions(options, cwd);
|
|
123
|
+
const metadata = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'));
|
|
124
|
+
if (typeof metadata.version !== 'string' || !metadata.version) throw new Error('当前包缺少有效版本号。');
|
|
125
|
+
const replacements = {
|
|
126
|
+
__PROJECT_NAME__: projectName,
|
|
127
|
+
__APP_ID__: appId,
|
|
128
|
+
__PORT__: port,
|
|
129
|
+
__FIN_MICRO_VERSION__: metadata.version,
|
|
130
|
+
__FIN_UI_VERSION__: '2.0.12',
|
|
131
|
+
};
|
|
132
|
+
const files = collectTemplate(join(packageRoot, 'scaffold', 'base'));
|
|
133
|
+
for (const [path, content] of collectTemplate(join(packageRoot, 'scaffold', template))) files.set(path, content);
|
|
134
|
+
const planned = new Map();
|
|
135
|
+
for (const [path, content] of files) {
|
|
136
|
+
const rendered = content.replace(/__[A-Z][A-Z0-9_]*?__/g, (placeholder) => {
|
|
137
|
+
if (!Object.hasOwn(replacements, placeholder)) throw new Error(`模板包含未知占位符:${placeholder}`);
|
|
138
|
+
return replacements[placeholder];
|
|
139
|
+
});
|
|
140
|
+
planned.set(path, rendered);
|
|
141
|
+
}
|
|
142
|
+
if (!planned.has('package.json')) throw new Error('模板缺少 package.json.template。');
|
|
143
|
+
JSON.parse(planned.get('package.json'));
|
|
144
|
+
// 检测文件与目录冲突,确保任何可预见的素材错误都在写入前失败。
|
|
145
|
+
for (const path of planned.keys()) {
|
|
146
|
+
for (let parentPath = dirname(path); parentPath !== '.'; parentPath = dirname(parentPath)) {
|
|
147
|
+
if (planned.has(parentPath)) throw new Error(`模板文件与目录冲突:${parentPath}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// 不递归创建根目录,利用 mkdir 的排他语义再次阻止并发创建后的覆盖。
|
|
151
|
+
mkdirSync(target);
|
|
152
|
+
for (const [path, content] of planned) {
|
|
153
|
+
const destination = join(target, path);
|
|
154
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
155
|
+
writeFileSync(destination, content, { flag: 'wx' });
|
|
156
|
+
}
|
|
157
|
+
return { target, directory, template, appId, port };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** 用户取消或输入流关闭时终止问答;在全部确认前不创建任何文件。 */
|
|
161
|
+
export class PromptCancelled extends Error {
|
|
162
|
+
constructor() { super('已取消创建,未写入项目文件。'); }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 补全缺少的选项;ask 为可注入的异步问答函数,便于无终端测试。
|
|
167
|
+
* 已给出的选项不会静默修改;完整参数命令保持原有默认值及非交互行为。
|
|
168
|
+
* @param {object} initial parseArgs 的部分选项,不应含未知选项。
|
|
169
|
+
* @param {object} environment interactive指定真实交互环境,ask返回输入文本/null取消,write仅报告校验失败。
|
|
170
|
+
* @returns {Promise<object>} 补全后的创建选项;本函数不写文件,createProject仍需最终校验。
|
|
171
|
+
* @throws {PromptCancelled|Error} 取消、非交互缺项、显式参数无效或问答失败。
|
|
172
|
+
*/
|
|
173
|
+
export async function completeOptions(initial, { interactive, ask, write = () => {}, cwd = process.cwd() }) {
|
|
174
|
+
if (initial.directory && initial.template) return initial;
|
|
175
|
+
if (!interactive) throw new Error('非交互环境请提供项目目录及 --template host|vue;可通过 --help 查看用法。');
|
|
176
|
+
const options = { ...initial };
|
|
177
|
+
const inquire = async (label, fallback, validate) => {
|
|
178
|
+
for (;;) {
|
|
179
|
+
const answer = await ask(`${label}${fallback ? ` (${fallback})` : ''}:`);
|
|
180
|
+
if (answer === undefined || answer === null) throw new PromptCancelled();
|
|
181
|
+
const value = answer.trim() || fallback;
|
|
182
|
+
try { validate(value); return value; }
|
|
183
|
+
catch (error) { write(`${error.message}\n`); }
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
if (!options.directory) options.directory = await inquire('项目目录', undefined, (directory) => {
|
|
187
|
+
validateOptions({ directory, template: 'host' }, cwd);
|
|
188
|
+
});
|
|
189
|
+
if (!options.template) options.template = await inquire('项目类型 [1 host 基座 / 2 vue 子应用,输入序号或名称]', 'vue', (value) => {
|
|
190
|
+
if (!['1', '2', 'host', 'vue'].includes(value)) throw new Error('请选择 1/host 或 2/vue。');
|
|
191
|
+
});
|
|
192
|
+
options.template = ({ 1: 'host', 2: 'vue' })[options.template] ?? options.template;
|
|
193
|
+
// 先校验显式参数,避免非法模板或标识被交互流程隐藏。
|
|
194
|
+
validateOptions(options, cwd);
|
|
195
|
+
if (options.template === 'vue' && options.appId === undefined) {
|
|
196
|
+
options.appId = await inquire('应用 app-id', basename(resolve(cwd, options.directory)), (value) => {
|
|
197
|
+
if (!value || !NAME_PATTERN.test(value)) throw new Error('应用 ID 仅支持小写字母、数字和连字符。');
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (options.port === undefined) options.port = await inquire('开发端口', options.template === 'host' ? '7100' : '7101', (port) => {
|
|
201
|
+
validateOptions({ ...options, port }, cwd);
|
|
202
|
+
});
|
|
203
|
+
return options;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** 输出后续运行指引;仅真实终端启用问答,关闭、EOF和Ctrl+C均在写文件前结束。 */
|
|
207
|
+
export async function main(args = process.argv.slice(2)) {
|
|
208
|
+
let terminal;
|
|
209
|
+
try {
|
|
210
|
+
let options = parseArgs(args, { partial: true });
|
|
211
|
+
if (options.help) {
|
|
212
|
+
process.stdout.write(HELP);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
216
|
+
if (interactive && (!options.directory || !options.template)) {
|
|
217
|
+
terminal = createInterface({ input: process.stdin, output: process.stdout });
|
|
218
|
+
terminal.on('SIGINT', () => terminal.close());
|
|
219
|
+
}
|
|
220
|
+
options = await completeOptions(options, {
|
|
221
|
+
interactive,
|
|
222
|
+
write: (message) => process.stdout.write(message),
|
|
223
|
+
ask: (label) => new Promise((resolveAnswer, reject) => {
|
|
224
|
+
const cancel = () => reject(new PromptCancelled());
|
|
225
|
+
terminal.once('close', cancel);
|
|
226
|
+
terminal.question(label).then(resolveAnswer, () => reject(new PromptCancelled())).finally(() => terminal.removeListener('close', cancel));
|
|
227
|
+
}),
|
|
228
|
+
});
|
|
229
|
+
const result = createProject(options);
|
|
230
|
+
process.stdout.write(`项目已创建。运行指引:\n cd ${JSON.stringify(result.target)}\n npm install\n npm run dev\n`);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
process.stderr.write(error instanceof PromptCancelled ? `${error.message}\n` : `创建失败:${error.message}\n`);
|
|
233
|
+
process.exitCode = error instanceof PromptCancelled ? 130 : 1;
|
|
234
|
+
} finally {
|
|
235
|
+
terminal?.close();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 被测试导入时不执行命令,避免导入产生文件系统副作用。
|
|
240
|
+
if (process.argv[1] && realpathSync(resolve(process.argv[1])) === realpathSync(fileURLToPath(import.meta.url))) main();
|
package/dist/browser.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function H(o){return`#/apps/${encodeURIComponent(o.appId)}/${encodeURIComponent(o.tabId)}?route=${encodeURIComponent(o.route)}`}function U(o){if(!o||o==="#/"||o==="#")return null;const p=/^#\/apps\/([^/?#]+)\/([^/?#]+)\?route=([^&#]*)$/.exec(o);if(!p)throw new Error("无效的微应用 URL");const[,r,v,h]=p;return{appId:decodeURIComponent(r),tabId:decodeURIComponent(v),route:decodeURIComponent(h)}}function P(o,p={}){const r=p.window??window,v=p.storageKey??"fin-micro:tabs:v1",h="fin-micro:navigation:v1",L=`${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`,f=t=>{var e;try{(e=p.onError)==null||e.call(p,t instanceof Error?t:new Error(String(t)))}catch{}},A=new Set(o.apps.map(t=>t.appId)),w=(t,e)=>typeof t=="string"&&t.length>0&&t.length<=e&&!/[\u0000-\u001f\u007f]/.test(t);function m(t){if(!t||typeof t!="object")return!1;const e=t;return w(e.appId,200)&&A.has(e.appId)&&w(e.tabId,500)&&w(e.title,500)&&w(e.route,8192)&&e.route.startsWith("/")&&!e.route.startsWith("//")&&!e.route.includes("\\")}const M=t=>({appId:t.appId,tabId:t.tabId,title:t.title,route:t.route,...t.activeMenuId!==void 0?{activeMenuId:t.activeMenuId}:{}});function $(t){try{r.sessionStorage.setItem(v,JSON.stringify({version:1,tabs:t.tabs.map(M),activeTabId:t.activeTabId}))}catch(e){f(e)}}function k(t,e=!1){try{const n=t.tabs.find(R=>R.tabId===t.activeTabId),s=n?H(n):"#/",a=r.history.state,c=a==null?void 0:a[h],d=r.location.hash===s;if(d&&(c==null?void 0:c.session)===L&&(c==null?void 0:c.key)===((n==null?void 0:n.key)??null))return;const i=`${r.location.pathname}${r.location.search}${s}`,b={...a&&typeof a=="object"?a:{},[h]:{session:L,key:(n==null?void 0:n.key)??null}};e&&!d?r.history.pushState(b,"",i):r.history.replaceState(b,"",i)}catch(n){f(n)}}function K(t){const e=o.getState().tabs.find(s=>s.tabId===t.tabId),n={...t,title:(e==null?void 0:e.title)??`${t.appId} · ${t.route}`.slice(0,500)};if(!m(n))throw new Error("URL 包含未知应用或无效页签/路由");o.openTab(n)}let u=null;try{u=U(r.location.hash)}catch(t){f(t)}try{const t=r.sessionStorage.getItem(v);if(t){const e=JSON.parse(t);if(!e||typeof e!="object")throw new Error("无效的页签存储");const n=e;if(n.version!==1||!Array.isArray(n.tabs)||n.tabs.length>100)throw new Error("不支持的页签存储版本或格式");const s=new Set(o.getState().tabs.map(a=>a.tabId));for(const a of n.tabs)if(!(!m(a)||s.has(a.tabId))&&!(u&&m({...u,title:"深链恢复"})&&u.tabId===a.tabId&&(u.appId!==a.appId||u.route!==a.route)))try{o.openTab(M(a)),s.add(a.tabId)}catch(c){f(c)}typeof n.activeTabId=="string"&&o.activateTab(n.activeTabId)}}catch(t){f(t)}try{u&&K(u)}catch(t){f(t)}let l=o.getState();k(l),$(l);let E=!1,y=!1;const O=o.subscribe(t=>{const e=l.tabs.some(n=>!t.tabs.some(s=>s.key===n.key));l=t,$(t),E||k(t,!e)});let g=0,I,S;function T(t){var c;if(y)return;const e=++g;I==null||I.abort();const n=new AbortController;I=n;const s=r.location.hash,a=(c=r.history.state)==null?void 0:c[h];E=!0,(async()=>{await Promise.resolve();try{if(y||e!==g)return;const d=U(s);if(!d)return;const i=o.getState().tabs.find(R=>R.tabId===d.tabId),b={...d,title:(i==null?void 0:i.title)??`${d.appId} · ${d.route}`.slice(0,500)};if(!m(b))throw new Error("URL 包含未知应用或无效页签/路由");if(t){if((a==null?void 0:a.session)!==L||!i||a.key!==i.key||i.appId!==b.appId||i.route!==b.route)return;await o.operate({type:"activate",target:{tabId:i.tabId,key:i.key},signal:n.signal})}else await o.operate({type:"open",input:b,signal:n.signal})}catch(d){!y&&e===g&&f(d)}finally{!y&&e===g&&(E=!1,l=o.getState(),k(l),$(l))}})()}const C=()=>{S=r.location.hash,T(!0)},j=t=>{const e=t.newURL,n=e?new URL(e).hash:r.location.hash;if(S===n){S=void 0;return}S=void 0,T(!1)};return r.addEventListener("popstate",C),r.addEventListener("hashchange",j),{dispose(){y||(y=!0,g++,I==null||I.abort(),O(),r.removeEventListener("popstate",C),r.removeEventListener("hashchange",j))}}}exports.connectMicroBrowser=P;exports.decodeMicroLocation=U;exports.encodeMicroLocation=H;
|
package/dist/browser.mjs
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
function x(o) {
|
|
2
|
+
return `#/apps/${encodeURIComponent(o.appId)}/${encodeURIComponent(o.tabId)}?route=${encodeURIComponent(o.route)}`;
|
|
3
|
+
}
|
|
4
|
+
function H(o) {
|
|
5
|
+
if (!o || o === "#/" || o === "#") return null;
|
|
6
|
+
const p = /^#\/apps\/([^/?#]+)\/([^/?#]+)\?route=([^&#]*)$/.exec(o);
|
|
7
|
+
if (!p) throw new Error("无效的微应用 URL");
|
|
8
|
+
const [, r, v, y] = p;
|
|
9
|
+
return {
|
|
10
|
+
appId: decodeURIComponent(r),
|
|
11
|
+
tabId: decodeURIComponent(v),
|
|
12
|
+
route: decodeURIComponent(y)
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function J(o, p = {}) {
|
|
16
|
+
const r = p.window ?? window, v = p.storageKey ?? "fin-micro:tabs:v1", y = "fin-micro:navigation:v1", L = `${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`, u = (t) => {
|
|
17
|
+
var e;
|
|
18
|
+
try {
|
|
19
|
+
(e = p.onError) == null || e.call(p, t instanceof Error ? t : new Error(String(t)));
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
}, j = new Set(o.apps.map((t) => t.appId)), w = (t, e) => typeof t == "string" && t.length > 0 && t.length <= e && !/[\u0000-\u001f\u007f]/.test(t);
|
|
23
|
+
function m(t) {
|
|
24
|
+
if (!t || typeof t != "object") return !1;
|
|
25
|
+
const e = t;
|
|
26
|
+
return w(e.appId, 200) && j.has(e.appId) && w(e.tabId, 500) && w(e.title, 500) && w(e.route, 8192) && e.route.startsWith("/") && !e.route.startsWith("//") && !e.route.includes("\\");
|
|
27
|
+
}
|
|
28
|
+
const U = (t) => ({
|
|
29
|
+
appId: t.appId,
|
|
30
|
+
tabId: t.tabId,
|
|
31
|
+
title: t.title,
|
|
32
|
+
route: t.route,
|
|
33
|
+
...t.activeMenuId !== void 0 ? { activeMenuId: t.activeMenuId } : {}
|
|
34
|
+
});
|
|
35
|
+
function $(t) {
|
|
36
|
+
try {
|
|
37
|
+
r.sessionStorage.setItem(
|
|
38
|
+
v,
|
|
39
|
+
JSON.stringify({
|
|
40
|
+
version: 1,
|
|
41
|
+
tabs: t.tabs.map(U),
|
|
42
|
+
activeTabId: t.activeTabId
|
|
43
|
+
})
|
|
44
|
+
);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
u(e);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function k(t, e = !1) {
|
|
50
|
+
try {
|
|
51
|
+
const n = t.tabs.find((R) => R.tabId === t.activeTabId), s = n ? x(n) : "#/", a = r.history.state, c = a == null ? void 0 : a[y], d = r.location.hash === s;
|
|
52
|
+
if (d && (c == null ? void 0 : c.session) === L && (c == null ? void 0 : c.key) === ((n == null ? void 0 : n.key) ?? null)) return;
|
|
53
|
+
const i = `${r.location.pathname}${r.location.search}${s}`, b = {
|
|
54
|
+
...a && typeof a == "object" ? a : {},
|
|
55
|
+
[y]: { session: L, key: (n == null ? void 0 : n.key) ?? null }
|
|
56
|
+
};
|
|
57
|
+
e && !d ? r.history.pushState(b, "", i) : r.history.replaceState(b, "", i);
|
|
58
|
+
} catch (n) {
|
|
59
|
+
u(n);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function A(t) {
|
|
63
|
+
const e = o.getState().tabs.find((s) => s.tabId === t.tabId), n = {
|
|
64
|
+
...t,
|
|
65
|
+
title: (e == null ? void 0 : e.title) ?? `${t.appId} · ${t.route}`.slice(0, 500)
|
|
66
|
+
};
|
|
67
|
+
if (!m(n)) throw new Error("URL 包含未知应用或无效页签/路由");
|
|
68
|
+
o.openTab(n);
|
|
69
|
+
}
|
|
70
|
+
let f = null;
|
|
71
|
+
try {
|
|
72
|
+
f = H(r.location.hash);
|
|
73
|
+
} catch (t) {
|
|
74
|
+
u(t);
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const t = r.sessionStorage.getItem(v);
|
|
78
|
+
if (t) {
|
|
79
|
+
const e = JSON.parse(t);
|
|
80
|
+
if (!e || typeof e != "object") throw new Error("无效的页签存储");
|
|
81
|
+
const n = e;
|
|
82
|
+
if (n.version !== 1 || !Array.isArray(n.tabs) || n.tabs.length > 100)
|
|
83
|
+
throw new Error("不支持的页签存储版本或格式");
|
|
84
|
+
const s = new Set(o.getState().tabs.map((a) => a.tabId));
|
|
85
|
+
for (const a of n.tabs)
|
|
86
|
+
if (!(!m(a) || s.has(a.tabId)) && !(f && m({ ...f, title: "深链恢复" }) && f.tabId === a.tabId && (f.appId !== a.appId || f.route !== a.route)))
|
|
87
|
+
try {
|
|
88
|
+
o.openTab(U(a)), s.add(a.tabId);
|
|
89
|
+
} catch (c) {
|
|
90
|
+
u(c);
|
|
91
|
+
}
|
|
92
|
+
typeof n.activeTabId == "string" && o.activateTab(n.activeTabId);
|
|
93
|
+
}
|
|
94
|
+
} catch (t) {
|
|
95
|
+
u(t);
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
f && A(f);
|
|
99
|
+
} catch (t) {
|
|
100
|
+
u(t);
|
|
101
|
+
}
|
|
102
|
+
let I = o.getState();
|
|
103
|
+
k(I), $(I);
|
|
104
|
+
let E = !1, h = !1;
|
|
105
|
+
const K = o.subscribe((t) => {
|
|
106
|
+
const e = I.tabs.some((n) => !t.tabs.some((s) => s.key === n.key));
|
|
107
|
+
I = t, $(t), E || k(t, !e);
|
|
108
|
+
});
|
|
109
|
+
let g = 0, l, S;
|
|
110
|
+
function T(t) {
|
|
111
|
+
var c;
|
|
112
|
+
if (h) return;
|
|
113
|
+
const e = ++g;
|
|
114
|
+
l == null || l.abort();
|
|
115
|
+
const n = new AbortController();
|
|
116
|
+
l = n;
|
|
117
|
+
const s = r.location.hash, a = (c = r.history.state) == null ? void 0 : c[y];
|
|
118
|
+
E = !0, (async () => {
|
|
119
|
+
await Promise.resolve();
|
|
120
|
+
try {
|
|
121
|
+
if (h || e !== g) return;
|
|
122
|
+
const d = H(s);
|
|
123
|
+
if (!d) return;
|
|
124
|
+
const i = o.getState().tabs.find((R) => R.tabId === d.tabId), b = {
|
|
125
|
+
...d,
|
|
126
|
+
title: (i == null ? void 0 : i.title) ?? `${d.appId} · ${d.route}`.slice(0, 500)
|
|
127
|
+
};
|
|
128
|
+
if (!m(b)) throw new Error("URL 包含未知应用或无效页签/路由");
|
|
129
|
+
if (t) {
|
|
130
|
+
if ((a == null ? void 0 : a.session) !== L || !i || a.key !== i.key || i.appId !== b.appId || i.route !== b.route)
|
|
131
|
+
return;
|
|
132
|
+
await o.operate({
|
|
133
|
+
type: "activate",
|
|
134
|
+
target: { tabId: i.tabId, key: i.key },
|
|
135
|
+
signal: n.signal
|
|
136
|
+
});
|
|
137
|
+
} else
|
|
138
|
+
await o.operate({ type: "open", input: b, signal: n.signal });
|
|
139
|
+
} catch (d) {
|
|
140
|
+
!h && e === g && u(d);
|
|
141
|
+
} finally {
|
|
142
|
+
!h && e === g && (E = !1, I = o.getState(), k(I), $(I));
|
|
143
|
+
}
|
|
144
|
+
})();
|
|
145
|
+
}
|
|
146
|
+
const C = () => {
|
|
147
|
+
S = r.location.hash, T(!0);
|
|
148
|
+
}, M = (t) => {
|
|
149
|
+
const e = t.newURL, n = e ? new URL(e).hash : r.location.hash;
|
|
150
|
+
if (S === n) {
|
|
151
|
+
S = void 0;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
S = void 0, T(!1);
|
|
155
|
+
};
|
|
156
|
+
return r.addEventListener("popstate", C), r.addEventListener("hashchange", M), {
|
|
157
|
+
/** 幂等移除本连接器的宿主订阅和浏览器事件;不清空存储、不关闭页签、不销毁宿主。 */
|
|
158
|
+
dispose() {
|
|
159
|
+
h || (h = !0, g++, l == null || l.abort(), K(), r.removeEventListener("popstate", C), r.removeEventListener("hashchange", M));
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
export {
|
|
164
|
+
J as connectMicroBrowser,
|
|
165
|
+
H as decodeMicroLocation,
|
|
166
|
+
x as encodeMicroLocation
|
|
167
|
+
};
|
package/dist/core.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const y=require("./sdk.cjs");function Ge(je,z={}){var me;const q=new Map,ye=new Map,R=t=>typeof t=="string"&&!!t.trim()&&!/[\u0000-\u001f\u007f]/.test(t),ce=t=>R(t)&&t.startsWith("/")&&!t.startsWith("//")&&!t.includes("\\"),pe=t=>t.split(/[?#]/,1)[0];for(const t of je){let r=function(c){if(!Array.isArray(c))throw new Error("menus 必须为数组");return Object.freeze(c.map(i=>{if(!i||!R(i.menuId)||!R(i.name)||p.has(i.menuId))throw new Error("菜单标识/名称无效或菜单标识重复");if(p.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:r(i.children)})}if(!i.target||!R(i.target.tabId)||!ce(i.target.route))throw new Error("菜单叶节点需要有效 target");return h.add(i.menuId),u.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(q.has(t.appId))throw new Error(`重复的 appId: ${t.appId}`);const s=new URL(t.entry);if(!["http:","https:"].includes(s.protocol))throw new Error("子应用地址必须使用 HTTP(S)");const p=new Set,h=new Set,u=[],e=t.menus===void 0?void 0:r(t.menus);if(t.menuRoutes!==void 0&&!Array.isArray(t.menuRoutes))throw new Error("menuRoutes 必须为数组");const l=(me=t.menuRoutes)==null?void 0:me.map(c=>{if(!c||!ce(c.pathPrefix)||/[?#]/.test(c.pathPrefix)||!h.has(c.menuId))throw new Error("菜单归属规则需要有效路径与本应用可导航叶节点");return Object.freeze({pathPrefix:c.pathPrefix,menuId:c.menuId})});u.push(...(l??[]).map(c=>({...c})));const a=new Map;for(const c of u){if(c.pathPrefix=c.pathPrefix.replace(/\/+$/,"")||"/",a.has(c.pathPrefix)&&a.get(c.pathPrefix)!==c.menuId)throw new Error("菜单归属前缀冲突");a.set(c.pathPrefix,c.menuId)}ye.set(t.appId,{leaves:h,routes:u.sort((c,i)=>i.pathPrefix.length-c.pathPrefix.length)}),q.set(t.appId,Object.freeze({appId:t.appId,name:t.name,entry:t.entry,...e?{menus:e}:{},...l?{menuRoutes:Object.freeze(l)}:{}}))}const oe=y.createNotificationBus(),Le=Object.freeze({source:"host"}),fe={on(t,r){return oe.on(t,({payload:s,metadata:p})=>B(()=>r(s,p)))},emit(t,r){oe.emit(t,{payload:r,metadata:Le})},clear:()=>oe.clear()},ne=z.callTimeoutMs??3e4;if(!Number.isFinite(ne)||ne<=0||ne>2147483647)throw new Error("callTimeoutMs 必须为不超过2147483647的正有限数");const Y=new Set;function Ie(t,r){if(!t||typeof t.authenticated!="boolean"||!Array.isArray(t.permissions)||!t.permissions.every(s=>typeof s=="string")||t.user!==void 0&&(!t.user||typeof t.user.id!="string"||t.user.name!==void 0&&typeof t.user.name!="string"))throw new Error("会话快照无效");return Object.freeze({authenticated:t.authenticated,permissions:Object.freeze([...t.permissions]),...t.user?{user:Object.freeze({id:t.user.id,...t.user.name!==void 0?{name:t.user.name}:{}})}:{},revision:r})}let J=Ie(z.session??{authenticated:!1,permissions:[]},0);function xe(t){L(),J=Ie(t,J.revision+1);const r=J;for(const s of[...Y]){if(r!==J)break;Y.has(s)&&B(()=>s(r))}return r}const Q=new Set,X=new Map,we=new Map(Object.entries(z.capabilities??{}).filter(([,t])=>t!==void 0).map(([t,r])=>{const s=r;return[t,{handler:s.handler,apps:s.apps?new Set(s.apps):void 0}]})),C=new Map,_=new Map;let f={tabs:[],activeTabId:null},de=0,N=!1,$=0,ve=Promise.resolve();const Z=new Map,F=new Map,ze=t=>{var r;try{(r=z.onError)==null||r.call(z,t)}catch{}},B=t=>{try{t()}catch(r){ze(r)}},L=()=>{if(N)throw new Error("MicroHost 已销毁")},ae=()=>({tabs:[...f.tabs],activeTabId:f.activeTabId}),K=()=>{$++;for(const t of[...Q])Q.has(t)&&B(()=>t(ae()))};function Ee(t){return L(),Q.add(t),()=>{Q.delete(t)}}const le=z.canNavigate,ge=z.canOpen;function U(t){if(!ge)return;const r=Object.freeze({appId:t.appId,tabId:t.tabId,title:t.title,route:t.route,...t.activeMenuId!==void 0?{activeMenuId:t.activeMenuId}:{}});try{const s=ge(r);if(s===!0)return;s&&(typeof s=="object"||typeof s=="function")&&Promise.resolve(s).catch(()=>{})}catch{}throw new y.NavigationError("FORBIDDEN","无权访问该页面(权限策略必须同步返回 true)")}function be(t,r=!1){var e;if(L(),!t||!R(t.title)||t.title.length>500)throw new Error("页签标题无效");if(!q.has(t.appId))throw new Error(`未知子应用: ${t.appId}`);if(!R(t.tabId)||!ce(t.route))throw new Error("需要 tabId 与应用内绝对路由");const s=ye.get(t.appId);if(t.activeMenuId!==void 0&&!s.leaves.has(t.activeMenuId))throw new Error("activeMenuId 必须指向本应用可导航叶节点");const p=pe(t.route),h=t.activeMenuId??((e=s.routes.find(l=>l.pathPrefix==="/"||p===l.pathPrefix||p.startsWith(`${l.pathPrefix}/`)))==null?void 0:e.menuId);let u=f.tabs.find(l=>l.tabId===t.tabId);if(u&&(u.appId!==t.appId||u.route!==t.route&&!r))throw new Error("tabId 已绑定到其他页面");return u&&u.route!==t.route&&(u=void 0),U(u??{...t,activeMenuId:h}),u||(u=Object.freeze({appId:t.appId,tabId:t.tabId,title:t.title,route:t.route,...h!==void 0?{activeMenuId:h}:{},key:`${t.tabId}:${++de}`})),u}function Re(t){const r=be(t);return f={tabs:f.tabs.includes(r)?f.tabs:[...f.tabs,r],activeTabId:r.tabId},K(),r}function He(t,r,s){if(L(),typeof r.title!="string"||!r.title.trim()||r.title.length>500||/[\u0000-\u001f\u007f]/.test(r.title))throw new Error("页签标题无效");const p=f.tabs.find(u=>u.tabId===t&&(!s||u.key===s));if(!p)return;const h=Object.freeze({...p,title:r.title});return f={...f,tabs:f.tabs.map(u=>u===p?h:u)},K(),h}function Ce(t){L();const r=f.tabs.find(s=>s.tabId===t);return r?(U(r),f={...f,activeTabId:t},K(),!0):!1}function _e(t,r){var u;L();const s=f.tabs.findIndex(e=>e.tabId===t&&(!r||e.key===r));if(s<0)return!1;const p=f.tabs[s],h=f.tabs.filter(e=>e!==p);if(f={tabs:h,activeTabId:f.activeTabId===t?((u=h[Math.min(s,h.length-1)])==null?void 0:u.tabId)??null:f.activeTabId},ke(p.key),!h.some(e=>e.appId===p.appId)){for(const e of[..._.get(p.appId)??[]])B(e);_.delete(p.appId),X.delete(p.appId)}return K(),!0}function ke(t){Z.delete(t),F.delete(t);for(const r of[...C.get(t)??[]])B(r);C.delete(t)}function $e(t){return G(t)}function G(t,r,s,p=!1,h,u=!1){try{if(N)throw new y.NavigationError("HOST_DISPOSED","MicroHost 已销毁");if(!t||!["open","activate","refresh","close","closeMany","back"].includes(t.type))throw new y.NavigationError("INVALID_OPERATION","导航命令无效");const e={...t,..."target"in t&&t.target?{target:{...t.target}}:{},..."anchor"in t&&t.anchor?{anchor:{...t.anchor}}:{}},l=o=>!!o&&R(o.tabId)&&(o.key===void 0||R(o.key));if(["activate","refresh","close"].includes(e.type)&&(!("target"in e)||!l(e.target)))throw new y.NavigationError("INVALID_OPERATION","导航需要合法目标引用");if("target"in e&&e.target!==void 0&&!l(e.target)||"anchor"in e&&e.anchor!==void 0&&!l(e.anchor))throw new y.NavigationError("INVALID_OPERATION","导航引用无效");if(e.type==="closeMany"&&e.exclude!==void 0){if(!Array.isArray(e.exclude)||!e.exclude.every(l))throw new y.NavigationError("INVALID_OPERATION","批量排除引用无效");e.exclude=Object.freeze(e.exclude.map(o=>{var k;return Object.freeze({tabId:o.tabId,...o.key!==void 0?{key:o.key}:{key:(k=f.tabs.find(g=>g.tabId===o.tabId))==null?void 0:k.key}})}))}if(e.type==="open"){const o=e.input;if(!o)throw new y.NavigationError("INVALID_OPERATION","导航目标无效");e.input=Object.freeze({appId:o.appId,tabId:o.tabId,title:o.title,route:o.route,...o.activeMenuId!==void 0?{activeMenuId:o.activeMenuId}:{}})}"fallback"in e&&e.fallback&&(e.fallback=Object.freeze({...e.fallback})),"target"in e&&e.target&&Object.freeze(e.target),"anchor"in e&&e.anchor&&Object.freeze(e.anchor),Object.freeze(e);const a=o=>f.tabs.find(k=>o?k.tabId===o.tabId&&(!o.key||k.key===o.key):k.tabId===f.activeTabId),c="target"in e?a(e.target):void 0,i=e.type==="closeMany"&&e.scope!=="all"?e.anchor?a(e.anchor):s?f.tabs.find(o=>o.key===s):a():void 0,T=a(),O=e.type==="open"?f.tabs.find(o=>o.tabId===e.input.tabId):void 0,d=u&&s?f.tabs.find(o=>o.key===s):void 0,I=d&&r?Object.freeze({appId:r,tab:Object.freeze({...d})}):void 0,v=u&&e.type==="open"&&!!r&&e.input.appId!==r,V=()=>{if(!(!v||e.type!=="open")){try{const o=I&&(le==null?void 0:le(I,e.input));if(o===!0)return;o&&(typeof o=="object"||typeof o=="function")&&Promise.resolve(o).catch(()=>{})}catch{}throw new y.NavigationError("FORBIDDEN","无权跨应用导航(权限策略必须同步返回 true)")}},P=o=>!r||o.appId===r;if(c&&!P(c)||i&&!P(i)||e.type==="open"&&r&&e.input.appId!==r&&!v||e.type==="back"&&e.fallback&&r&&e.fallback.appId!==r)throw new y.NavigationError("FORBIDDEN","页面只能操作本应用");if(e.type==="closeMany"&&!["all","others","left","right"].includes(e.scope))throw new y.NavigationError("INVALID_OPERATION","批量范围无效");const ee=i?f.tabs.indexOf(i):-1,W=e.type==="closeMany"?f.tabs.filter((o,k)=>{var g;return P(o)&&!((g=e.exclude)!=null&&g.some(M=>M.tabId===o.tabId&&M.key===o.key))&&(e.scope==="all"||ee>=0&&(e.scope==="others"?o.key!==i.key:e.scope==="left"?k<ee:k>ee))}):[],E=e.type==="back"?c??(!("target"in e)||!e.target?T:void 0):c,se=async()=>{var Oe,Ae,Ne,Te,Pe,Me;const o=n=>({status:n,affected:[]});if(N)throw new y.NavigationError("HOST_DISPOSED","MicroHost 已销毁");if((Oe=e.signal)!=null&&Oe.aborted)return o("stale");if(s&&!p&&(h!=null&&h()||!f.tabs.some(n=>n.key===s)))throw new y.NavigationError("PAGE_RELEASED","页面上下文已释放");if(e.type==="open"&&(O?!f.tabs.some(n=>n.key===O.key):f.tabs.some(n=>n.tabId===e.input.tabId))||E&&!f.tabs.some(n=>n.key===E.key)||i&&!f.tabs.some(n=>n.key===i.key)||W.some(n=>!f.tabs.some(m=>m.key===n.key)))return o("stale");if(e.type!=="open"&&e.type!=="closeMany"&&!E)return o("noop");const k=$;if(V(),s&&!p&&(h!=null&&h()||!f.tabs.some(n=>n.key===s)))throw new y.NavigationError("PAGE_RELEASED","页面上下文已释放");if($!==k||N||(Ae=e.signal)!=null&&Ae.aborted)return o("stale");const g=f,M=$;let w=[...f.tabs],b,S=[],H=f.activeTabId;try{if(e.type==="open"&&(b=be(e.input,!0),O&&O.key!==b.key?(S=[O],w=w.map(n=>n.key===O.key?b:n)):w.some(n=>n.key===b.key)||w.push(b),H=b.tabId),e.type==="activate"&&(b=E,U(b),H=b.tabId),e.type==="refresh"&&(U(E),S=[E],b=Object.freeze({...E,key:`${E.tabId}:${++de}`}),w=w.map(n=>n.key===E.key?b:n)),(e.type==="close"||e.type==="back")&&(S=[E]),e.type==="closeMany"&&(S=W),S.length&&e.type!=="refresh"&&e.type!=="open"){const n=new Set(S.map(D=>D.key)),m=g.tabs.findIndex(D=>D.tabId===g.activeTabId);if(w=w.filter(D=>!n.has(D.key)),e.type==="back"){const D=F.get(E.key);b=w.find(A=>A.key===D&&P(A)),!b&&e.fallback&&(b=be(e.fallback),n.has(b.key)&&(b=Object.freeze({...b,key:`${b.tabId}:${++de}`})),w.some(A=>A.key===b.key)||w.push(b));const j=g.tabs.filter(A=>P(A)),te=j.findIndex(A=>A.key===E.key);b??(b=j.slice(0,te).reverse().find(A=>w.some(re=>re.key===A.key))??j.slice(te+1).find(A=>w.some(re=>re.key===A.key))),b??(b=w[Math.min(m,w.length-1)]),b&&U(b),H=(b==null?void 0:b.tabId)??null}else S.some(D=>D.tabId===H)&&(H=((Ne=w[Math.min(m,w.length-1)])==null?void 0:Ne.tabId)??null)}}catch(n){throw n instanceof y.NavigationError?n:new y.NavigationError("INVALID_OPERATION","导航参数无效")}if(w.length===g.tabs.length&&w.every((n,m)=>n.key===g.tabs[m].key)&&H===g.activeTabId)return o("noop");const ue=[...S],ie=g.tabs.find(n=>n.tabId===g.activeTabId);ie&&ie.tabId!==H&&!ue.some(n=>n.key===ie.key)&&ue.push(ie);for(const n of ue)for(const m of[...Z.get(n.key)??[]]){let D;const j=e.signal;let te=()=>{};try{const A=Promise.resolve(m({operation:e,tab:n})),re=new Promise(De=>{if(!j)return;const Se=()=>De();j.addEventListener("abort",Se,{once:!0}),te=()=>j.removeEventListener("abort",Se),j.aborted&&De()});D=await Promise.race([A,re])}catch{if(j!=null&&j.aborted)return o("stale");throw new y.NavigationError("GUARD_FAILED","离页守卫执行失败")}finally{te()}if($!==M||N||(Te=e.signal)!=null&&Te.aborted)return o("stale");if(D===!1)return o("cancelled")}if($!==M||N||(Pe=e.signal)!=null&&Pe.aborted)return o("stale");if(h!=null&&h())throw new y.NavigationError("PAGE_RELEASED","页面上下文已释放");if(V(),b&&U(b),h!=null&&h())throw new y.NavigationError("PAGE_RELEASED","页面上下文已释放");if($!==M||N||(Me=e.signal)!=null&&Me.aborted)return o("stale");if(f={tabs:w,activeTabId:H},e.type==="open"&&b&&s&&!v&&b.key!==s&&!F.has(b.key)&&F.set(b.key,s),e.type==="refresh"&&b){const n=F.get(E.key);n&&F.set(b.key,n)}for(const n of S)ke(n.key);for(const n of new Set(S.map(m=>m.appId)))if(!w.some(m=>m.appId===n)){for(const m of[..._.get(n)??[]])B(m);_.delete(n),X.delete(n)}const he=[...S,...w.filter(n=>!g.tabs.some(m=>m.key===n.key))];return!he.length&&b&&he.push(b),K(),{status:"committed",affected:he.map(n=>({tabId:n.tabId,key:n.key})),...b?{tab:b}:{}}},x=ve.then(se);return ve=x.catch(()=>{}),x}catch(e){return Promise.reject(e)}}function Fe(t){if(L(),!q.has(t))throw new Error(`未知子应用: ${t}`);const r=X.get(t);if(r)return r;let s=!0;const p=new Set([()=>{s=!1}]);_.set(t,p);async function h(e,l,a,c={}){const i=()=>{if(N)throw new y.HostCallError("HOST_DISPOSED","MicroHost 已销毁");if(a&&(a.released()||!f.tabs.some(P=>P.key===a.key)))throw new y.HostCallError("PAGE_RELEASED","页面上下文已释放");if(!s)throw new y.HostCallError("BRIDGE_RELEASED","应用 bridge 已释放")};i();const T=we.get(e);if(!T)throw new y.HostCallError("UNKNOWN_CAPABILITY",`未注册宿主能力: ${e}`);if(T.apps&&!T.apps.has(t))throw new y.HostCallError("FORBIDDEN",`应用无权调用宿主能力: ${e}`);const O=c.timeoutMs??ne;if(!Number.isFinite(O)||O<=0||O>2147483647)throw new TypeError("timeoutMs 必须为不超过2147483647的正有限数");const d=c.signal;if(d!=null&&d.aborted)throw new y.HostCallError("CANCELLED","宿主能力调用已取消");const I=new AbortController,v=a?f.tabs.find(P=>P.key===a.key):void 0,V=Object.freeze({appId:t,signal:I.signal,...v?{tab:Object.freeze({...v})}:{},isActive:()=>!I.signal.aborted&&!N&&s&&(!a||!a.released()&&f.tabs.some(P=>P.key===a.key))});return new Promise((P,ee)=>{let W=!1,E;const se=()=>{E!==void 0&&clearTimeout(E),d==null||d.removeEventListener("abort",o),p.delete(k),a==null||a.cleanups.delete(k)},x=M=>{W||(W=!0,se(),ee(M),I.abort())},o=()=>x(new y.HostCallError("CANCELLED","宿主能力调用已取消")),k=()=>{try{i()}catch(M){x(M)}};if(p.add(k),a==null||a.cleanups.add(k),d==null||d.addEventListener("abort",o,{once:!0}),E=setTimeout(()=>x(new y.HostCallError("TIMEOUT","宿主能力调用超时")),O),d!=null&&d.aborted){o();return}let g;try{g=T.handler(V,...l)}catch{x(new y.HostCallError("HANDLER_FAILED",`宿主能力执行失败: ${e}`));return}Promise.resolve(g).then(M=>{if(!W){try{i()}catch(w){x(w);return}W=!0,se(),P(M)}},()=>x(new y.HostCallError("HANDLER_FAILED",`宿主能力执行失败: ${e}`)))})}const u={appId:t,getState:ae,callHost(e,...l){return h(e,l)},callHostWithOptions(e,l,...a){return h(e,a,void 0,l)},protocol(){if(L(),!s)throw new Error("应用 bridge 已释放");return Object.freeze({version:"1.0",capabilities:Object.freeze([...we].filter(([,e])=>!e.apps||e.apps.has(t)).map(([e])=>e)),features:Object.freeze(["call-options","session","event-metadata","navigate-to"])})},negotiate(e={}){const l=u.protocol(),a=Object.freeze((e.capabilities??[]).filter(c=>!l.capabilities.includes(c)));return Object.freeze({compatible:(!e.version||e.version===l.version)&&!a.length,protocol:l,missingCapabilities:a})},getSession(){if(L(),!s)throw new Error("应用 bridge 已释放");return J},subscribeSession(e){const l=u.getSession(),a=i=>e(i),c=()=>{Y.delete(a),p.delete(c)};return Y.add(a),p.add(c),B(()=>e(l)),c},subscribe(e){if(!s)throw new Error("应用 bridge 已释放");const l=Ee(e),a=()=>{l(),p.delete(a)};return p.add(a),a},ready(e){const l=c=>{const i=c.tabs.find(T=>T.tabId===c.activeTabId&&T.appId===t);i&&e(i)},a=u.subscribe(l);try{l(ae())}catch(c){throw a(),c}return a},bindPage(e){const l=f.tabs.find(a=>a.tabId===e&&a.appId===t);if(!l)throw new Error("页面不属于该应用");return u.page(l.key)},page(e){if(L(),!s)throw new Error("应用 bridge 已释放");const l=f.tabs.find(d=>d.key===e&&d.appId===t);if(!l)throw new Error("页面上下文不存在或不属于该应用");let a=!1;const c=new Set,i=()=>{if(N)throw new y.NavigationError("HOST_DISPOSED","MicroHost 已销毁");if(a||!f.tabs.some(d=>d.key===e))throw new y.NavigationError("PAGE_RELEASED","页面上下文已释放")},T=()=>{var d;a=!0,c.forEach(I=>I()),c.clear(),(d=C.get(e))==null||d.delete(T)};let O=C.get(e);return O||C.set(e,O=new Set),O.add(T),{get tab(){return f.tabs.find(d=>d.key===e)??l},callHost(d,...I){return h(d,I,{key:e,released:()=>a,cleanups:c})},callHostWithOptions(d,I,...v){return h(d,v,{key:e,released:()=>a,cleanups:c},I)},async openTab(d){i();const I=await G({type:"open",input:d},t,e,!1,()=>a);if(I.tab)return I.tab;const v=f.tabs.find(V=>V.tabId===d.tabId&&V.appId===t);if(I.status==="noop"&&v)return v;throw new y.NavigationError(I.status==="cancelled"?"CANCELLED":"STALE","导航未提交")},async navigateTo(d,I={}){return i(),G({type:"open",input:d,signal:I.signal},t,e,!1,()=>a,!0)},async closeCurrent(){return N?!1:(await G({type:"close",target:{tabId:l.tabId,key:e}},t,void 0,!0)).status==="committed"},beforeLeave(d){if(i(),typeof d!="function")throw new y.NavigationError("INVALID_OPERATION","离页守卫必须为函数");let I=Z.get(e);I||Z.set(e,I=new Set),I.add(d);const v=()=>{I.delete(d),c.delete(v)};return c.add(v),v},async refreshCurrent(){return i(),G({type:"refresh",target:{tabId:l.tabId,key:e}},t,e,!1,()=>a)},async back(d){return i(),G({type:"back",target:{tabId:l.tabId,key:e},fallback:d},t,e,!1,()=>a)},async operate(d){return i(),G(d,t,e,!1,()=>a)},emit(d,I){i(),oe.emit(d,{payload:I,metadata:Object.freeze({source:"app",appId:t,tabKey:e})})},on(d,I){i();const v=fe.on(d,I);return c.add(v),()=>{v(),c.delete(v)}},dispose:T}}};return X.set(t,u),u}function Be(){if(!N){N=!0;for(const t of C.values())[...t].forEach(r=>r());C.clear(),Z.clear(),F.clear();for(const t of _.values())[...t].forEach(r=>r());_.clear(),Q.clear(),Y.clear(),X.clear(),fe.clear(),f={tabs:[],activeTabId:null}}}return{apps:[...q.values()],getState:ae,subscribe:Ee,openTab:Re,activateTab:Ce,closeTab:_e,updateTab:He,updateSession:xe,operate:$e,bridge:Fe,bus:fe,dispose:Be}}exports.createMicroHost=Ge;
|