@axewo/fin-micro 1.0.0 → 2.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.
Files changed (68) hide show
  1. package/README.md +101 -3
  2. package/dist/browser.cjs +1 -1
  3. package/dist/browser.mjs +68 -66
  4. package/dist/core.cjs +1 -1
  5. package/dist/core.mjs +447 -433
  6. package/dist/index.cjs +1 -1
  7. package/dist/index.mjs +31 -24
  8. package/dist/{router-view-impl-C1KG6mxu.mjs → router-view-impl-B1NzCGKV.mjs} +18 -18
  9. package/dist/router-view-impl-BvOToV_Z.cjs +1 -0
  10. package/dist/runtime.cjs +1 -1
  11. package/dist/runtime.mjs +40 -39
  12. package/dist/sdk.cjs +1 -1
  13. package/dist/sdk.mjs +92 -56
  14. package/dist/types/browser/index.d.ts +4 -0
  15. package/dist/types/core/index.d.ts +1 -1
  16. package/dist/types/index.d.ts +2 -0
  17. package/dist/types/runtime/lifecycle.d.ts +1 -1
  18. package/dist/types/sdk/index.d.ts +63 -4
  19. package/dist/types/vue/index.d.ts +13 -2
  20. package/dist/types/vue/router-view-impl.d.ts +1 -1
  21. package/dist/vue.cjs +1 -1
  22. package/dist/vue.mjs +49 -35
  23. package/package.json +2 -2
  24. package/scaffold/base/package.json.template +5 -1
  25. package/scaffold/base/scripts/check-runtime.mjs +43 -0
  26. package/scaffold/base/src/locales/en.ts +12 -0
  27. package/scaffold/base/src/locales/index.ts +7 -1
  28. package/scaffold/base/src/locales/zh-CN.ts +12 -0
  29. package/scaffold/base/src/main.ts +13 -5
  30. package/scaffold/base/src/router/demo.ts +6 -0
  31. package/scaffold/base/src/router/index.ts +13 -0
  32. package/scaffold/base/src/stores/demo.ts +12 -0
  33. package/scaffold/base/src/stores/index.ts +11 -0
  34. package/scaffold/base/src/utils/appConfig.ts +49 -0
  35. package/scaffold/base/src/utils/errorUtils.ts +92 -0
  36. package/scaffold/base/src/utils/requestUtils.ts +32 -11
  37. package/scaffold/base/src/views/DemoView.vue +26 -0
  38. package/scaffold/host/.env.example +11 -3
  39. package/scaffold/host/src/App.vue +1 -13
  40. package/scaffold/host/src/bootstrap.ts +40 -8
  41. package/scaffold/host/src/hooks/usePlatformSession.ts +40 -8
  42. package/scaffold/host/src/layout/HostHeader.vue +65 -5
  43. package/scaffold/host/src/layout/HostLayout.vue +65 -21
  44. package/scaffold/host/src/layout/HostMenu.vue +1 -0
  45. package/scaffold/host/src/layout/HostTabs.vue +41 -8
  46. package/scaffold/host/src/layout/HostWorkspace.vue +19 -7
  47. package/scaffold/host/src/localPages.ts +45 -0
  48. package/scaffold/host/src/platform.ts +41 -10
  49. package/scaffold/host/src/router/guards.ts +128 -0
  50. package/scaffold/host/src/router/index.ts +25 -0
  51. package/scaffold/host/src/router/targets.ts +30 -0
  52. package/scaffold/host/src/router/workspace.ts +63 -0
  53. package/scaffold/host/src/styles/shell.css +761 -0
  54. package/scaffold/host/src/utils/request.ts +30 -3
  55. package/scaffold/host/src/views/HomeView.vue +19 -0
  56. package/scaffold/host/src/views/LoginRoute.vue +16 -0
  57. package/scaffold/host/src/views/LoginView.vue +59 -9
  58. package/scaffold/host/src/views/NotFoundView.vue +1 -0
  59. package/scaffold/host/src/views/ProfileView.vue +24 -0
  60. package/scaffold/host/src/views/WorkspaceRoute.vue +11 -0
  61. package/scaffold/vue/src/bootstrap.ts +18 -22
  62. package/scaffold/vue/src/bridge/auth.ts +23 -0
  63. package/scaffold/vue/src/bridge/session.ts +2 -2
  64. package/scaffold/vue/src/main.ts +5 -2
  65. package/scaffold/vue/src/router/index.ts +2 -0
  66. package/scaffold/vue/src/views/DetailView.vue +14 -5
  67. package/scaffold/vue/src/views/ListView.vue +9 -5
  68. package/dist/router-view-impl-CgLEalx4.cjs +0 -1
@@ -20,16 +20,26 @@ export interface MicroMenuRoute {
20
20
  /** 该应用内可导航叶节点的 menuId。 */
21
21
  menuId: string;
22
22
  }
23
- /** 宿主登记的应用元信息;entry 必须为 HTTP(S) 地址,菜单配置在创建宿主时递归复制冻结。 */
24
- export interface MicroApp {
23
+ /** 应用公共元信息;菜单配置在创建宿主时递归复制冻结。 */
24
+ interface MicroAppMetadata {
25
25
  appId: string;
26
26
  name: string;
27
- entry: string;
28
27
  /** 可选多级菜单;不配置时保持原有无菜单导航行为。 */
29
28
  menus?: readonly MicroMenu[];
30
29
  /** 隐藏页面的菜单归属规则;入口 target 路径也自动参与匹配。 */
31
30
  menuRoutes?: readonly MicroMenuRoute[];
32
31
  }
32
+ /**
33
+ * 明确区分远端与宿主本地应用;省略 kind 保持原有远端协议,entry 必须为 HTTP(S)。
34
+ * local 只登记导航身份,不具有执行地址;由宿主自己的 SFC 容器渲染,不能交给 Wujie。
35
+ */
36
+ export type MicroApp = MicroAppMetadata & ({
37
+ kind?: 'remote';
38
+ entry: string;
39
+ } | {
40
+ kind: 'local';
41
+ entry?: never;
42
+ });
33
43
  /** 打开页面的导航元信息;tabId 在宿主内唯一,route 为应用内绝对路径,不应存放敏感信息。 */
34
44
  export interface OpenTab {
35
45
  appId: string;
@@ -154,8 +164,12 @@ export type NavigationOperation =
154
164
  type: 'open';
155
165
  input: OpenTab;
156
166
  } | {
157
- type: 'activate' | 'refresh' | 'close';
167
+ type: 'activate' | 'refresh';
158
168
  target: TabRef;
169
+ } | {
170
+ type: 'close';
171
+ target: TabRef;
172
+ closeFallback?: TabRef;
159
173
  } | {
160
174
  type: 'closeMany';
161
175
  scope: 'all' | 'others' | 'left' | 'right';
@@ -166,6 +180,8 @@ export type NavigationOperation =
166
180
  * 排除项不参与关闭确认,不会扩大页面 SDK 的应用权限;省略时完全保持原有范围选择行为。
167
181
  */
168
182
  exclude?: readonly TabRef[];
183
+ /** 活动页被关闭时优先激活仍存活的指定生命周期;不存在时沿用相邻页回落,不创建页面。 */
184
+ closeFallback?: TabRef;
169
185
  } | {
170
186
  type: 'back';
171
187
  target?: TabRef;
@@ -326,6 +342,48 @@ export declare function createTokenCapability(options: {
326
342
  apps: readonly string[];
327
343
  handler(context: HostCallContext): Promise<TokenResult>;
328
344
  };
345
+ /** 单窗口宿主签发的不透明请求票据;只作内存关联,不含 Token,不得序列化或持久化。 */
346
+ export interface AuthRequestTicket {
347
+ readonly request: string;
348
+ }
349
+ /** 正式登录失效能力契约;与 getToken 同样必须显式注册并配置可信应用白名单。 */
350
+ export interface AuthCapabilities extends TokenCapabilities {
351
+ /** 请求发送前签发原对象票据;只关联宿主私有版本,不接受子应用自报版本或身份。 */
352
+ beginAuthRequest(): AuthRequestTicket;
353
+ /**
354
+ * 上报该请求的认证失效;首次有效上报同步撤销会话并返回 true。
355
+ * 伪造、复制、跨应用、已消费或旧版本票据返回 false;能力越权及生命周期释放仍由 callHost 拒绝。
356
+ * 本返回值不是业务请求结果,不得据此自动重发请求或恢复身份。
357
+ */
358
+ reportAuthFailure(ticket: AuthRequestTicket): boolean;
359
+ }
360
+ /**
361
+ * 创建会话失效协调能力,复用现有 callHost 白名单与生命周期,不建立事件总线。
362
+ * @param options apps 是可信应用白名单;getRevision 必须读取宿主私有版本,每次身份/凭据变化递增。
363
+ * invalidate 必须同步撤销身份并推进版本,不能等待 beforeLeave 或远程注销;抛错仍由 core 隔离。
364
+ * @returns 两个正式能力配置;票据同时绑定可信 context.appId、对象身份和私有版本,不接受子应用自报 revision。
365
+ * @remarks 仅支持同窗口对象桥,白名单不是 XSS 沙箱;后台仍须鉴权,不自动刷新 Token 或重放请求。
366
+ */
367
+ export declare function createAuthFailureCapabilities(options: {
368
+ apps: readonly string[];
369
+ getRevision(): number;
370
+ invalidate(): void;
371
+ }): {
372
+ beginAuthRequest: {
373
+ apps: readonly string[];
374
+ handler(context: HostCallContext): AuthRequestTicket;
375
+ };
376
+ reportAuthFailure: {
377
+ apps: readonly string[];
378
+ handler(context: HostCallContext, ticket: AuthRequestTicket): boolean;
379
+ };
380
+ };
381
+ /** 未登录或空权限输入一律拒绝;权限名区分大小写,不进行通配符或隐式角色映射。 */
382
+ export declare function hasPermission(session: SessionInput | null | undefined, permission: string): boolean;
383
+ /** 至少一个明确权限匹配才允许;空数组拒绝,不将空条件解释成公共访问。 */
384
+ export declare function hasAny(session: SessionInput | null | undefined, permissions: readonly string[]): boolean;
385
+ /** 所有明确权限均匹配才允许;空数组或包含空权限名均拒绝。 */
386
+ export declare function hasAll(session: SessionInput | null | undefined, permissions: readonly string[]): boolean;
329
387
  /** Wujie 注入约定;可显式传入测试窗口,SDK 本身不导入 Wujie。 */
330
388
  export interface MicroWindow extends Window {
331
389
  $wujie?: {
@@ -336,3 +394,4 @@ export interface MicroWindow extends Window {
336
394
  }
337
395
  /** 获取 target 上的桥;默认浏览器 window,独立运行或 Node/SSR 无 window 时返回 undefined,不创建宿主。 */
338
396
  export declare function getMicroBridge<C extends CapabilityContract<C> = HostCapabilities>(target?: MicroWindow): AppBridge<C> | undefined;
397
+ export {};
@@ -1,7 +1,18 @@
1
- import { PropType } from 'vue';
1
+ import { PropType, MaybeRefOrGetter } from 'vue';
2
+ import { SessionSnapshot, AppBridge, MicroTab, PageSDK, CapabilityContract } from '../sdk';
2
3
  import { MicroHost } from '../core';
3
4
  import { MicroRuntime } from '../runtime';
4
- import { AppBridge, MicroTab, PageSDK, CapabilityContract } from '../sdk';
5
+ /**
6
+ * 根据响应式公开会话消费权限;不读取 Token、不绕过核心准入或后台鉴权。
7
+ * @param session 当前 SessionSnapshot 的 ref/getter,未连接或未登录安全拒绝。
8
+ * @returns 三种判断函数;在 computed/模板中调用会追踪最新权限,can 用于直接生成权限计算值。
9
+ */
10
+ export declare function usePermission(session: MaybeRefOrGetter<SessionSnapshot | null | undefined>): {
11
+ hasPermission: (permission: string) => boolean;
12
+ hasAny: (permissions: readonly string[]) => boolean;
13
+ hasAll: (permissions: readonly string[]) => boolean;
14
+ can: (permission: MaybeRefOrGetter<string>) => import('vue').ComputedRef<boolean>;
15
+ };
5
16
  export { useMicroTab } from './tab-context';
6
17
  export { MicroRouteView, type MicroRouteErrorCode } from './router-view';
7
18
  /** 应用共享容器:appId 对应一个 runtime 实例;挂载失败显示错误,组件卸载触发异步 unmount。不是页级缓存组件,不应在每次切 tab 时销毁。 */
@@ -8,7 +8,7 @@ import { MicroRouteErrorCode } from './router-view';
8
8
  *
9
9
  * 这里只解析与加载页面,不启动导航事务、不执行 Vue Router 导航守卫、不修改宿主页签和权限。
10
10
  * redirect 不替代宿主 canOpen;业务必须保证重定向目标属于该应用已授权页面,鉴权仍由宿主/core 负责。
11
- * 页面定位等价于从 START_LOCATION 发起解析:相对 redirect 以应用根为基准,函数 redirect 的 from 为 START_LOCATION。
11
+ * 页面定位等价于从 START_LOCATION 发起解析:相对 redirect 以应用根为基准,函数 redirect 按公开契约只接收目标 route。
12
12
  * 建议 redirect 使用绝对 path 或 name;不会把其他页签的全局 currentRoute 当作本页的导航来源。
13
13
  * 页面跳转和离页确认应使用页面 SDK;不要通过 RouterLink/router.push 把全局路由当作宿主页签导航。
14
14
  * 懒加载无法取消网络请求;卸载后忽略迟到结果,不再写入组件状态。模块加载后的组件 setup/render 异常由 Vue 错误边界处理。
package/dist/vue.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("vue"),s=Symbol("micro-tab");function d(){const t=e.inject(s);if(!t)throw new Error("useMicroTab 必须在 MicroTabCache 内调用");return t}const m=e.defineComponent({name:"MicroRouteView",props:{name:{type:String,default:"default"}},slots:Object,setup(t,{slots:n}){const i=d(),a=e.shallowRef(),r=e.shallowRef(!1);let u=!1,c=!1;async function o(){if(!(u||c)){c=!0,r.value=!1;try{const l=await Promise.resolve().then(()=>require("./router-view-impl-CgLEalx4.cjs"));u||(a.value=l.MicroRouteViewImpl)}catch{u||(r.value=!0)}finally{c=!1}}}return e.onBeforeUnmount(()=>{u=!0}),o(),()=>{var l,f;return a.value?e.h(a.value,{name:t.name},n):r.value?((l=n.error)==null?void 0:l.call(n,{tab:i,code:"LOAD_FAILED",retry:o}))??e.h("div",{role:"alert","data-micro-route-error":"LOAD_FAILED"},[e.h("p","页面加载失败(LOAD_FAILED)"),e.h("button",{type:"button",onClick:o},"重新加载")]):((f=n.loading)==null?void 0:f.call(n,{tab:i}))??e.h("p",{role:"status"},"页面加载中…")}}}),h=e.defineComponent({name:"MicroAppContainer",props:{appId:{type:String,required:!0},runtime:{type:Object,required:!0},retryDisabled:Boolean},setup(t){const n=e.shallowRef(),i=e.shallowRef(""),a=e.shallowRef(!1);let r=!1;async function u(c=!1){if(!(r||a.value||!c&&t.retryDisabled)){a.value=!0,i.value="";try{await t.runtime.mount(t.appId,n.value)}catch(o){r||(i.value=String(o))}finally{r||(a.value=!1)}}}return e.onMounted(()=>{u(!0)}),e.onBeforeUnmount(()=>{r=!0,t.runtime.unmount(t.appId).catch(console.error)}),()=>e.h("section",{style:{height:"100%",minHeight:"420px"},"aria-busy":a.value},[i.value?e.h("div",{role:"alert"},[e.h("p",i.value),e.h("button",{type:"button",disabled:t.retryDisabled||a.value,onClick:()=>{u()}},"重新加载")]):null,a.value?e.h("p",{role:"status"},"子应用加载中…"):null,e.h("div",{ref:n,style:{height:"100%",minHeight:"420px"}})])}});function v(t){const n=e.shallowRef(t.getState()),i=t.subscribe(a=>{n.value=a});return e.onBeforeUnmount(i),n}const b=e.defineComponent({name:"MicroTabBoundary",props:{tab:{type:Object,required:!0},active:Boolean},setup(t,{slots:n}){const i=Object.freeze({...t.tab});e.provide(s,i);const a=e.defineComponent({name:"MicroTabContent",setup:()=>()=>{var r;return(r=n.default)==null?void 0:r.call(n,{tab:i})}});return()=>e.h("section",{hidden:!t.active,"data-micro-tab":i.key},[e.h(e.KeepAlive,null,{default:()=>t.active?e.h(a):null})])}}),p=e.defineComponent({name:"MicroTabCache",props:{tabs:{type:Array,required:!0},activeKey:String,max:{type:Number,default:20}},setup(t,{slots:n}){const i=e.shallowRef([]);return e.watch(()=>[t.tabs,t.activeKey,t.max],()=>{const a=new Set(t.tabs.map(o=>o.key));let r=i.value.filter(o=>a.has(o.key));const u=t.tabs.find(o=>o.key===t.activeKey);u&&(r=[...r.filter(o=>o.key!==u.key),u]);const c=Number.isFinite(t.max)?Math.max(1,Math.floor(t.max)):20;for(;r.length>c;){const o=r.findIndex(l=>l.key!==t.activeKey);if(o<0)break;r.splice(o,1)}i.value=r},{immediate:!0,flush:"sync"}),()=>i.value.map(a=>e.h(b,{key:a.key,tab:a,active:a.key===t.activeKey},n))}});function M(t,n,i=console.error){let a=!1,r,u=!1,c=0;async function o(){if(!u){u=!0;try{let f=-1;for(;!a&&r&&f!==c;){f=c;try{await n.replace(r.route,r)}catch(y){i(y)}}}finally{u=!1}}}const l=t.ready(f=>{r=f,c++,o()});return()=>{a=!0,l()}}function w(t,n){const i=t.page(n.key);return e.onBeforeUnmount(i.dispose),i}exports.MicroAppContainer=h;exports.MicroRouteView=m;exports.MicroTabCache=p;exports.connectMicroRouter=M;exports.useMicroHost=v;exports.useMicroPage=w;exports.useMicroTab=d;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("vue"),f=require("./sdk.cjs"),d=Symbol("micro-tab");function m(){const t=e.inject(d);if(!t)throw new Error("useMicroTab 必须在 MicroTabCache 内调用");return t}const y=e.defineComponent({name:"MicroRouteView",props:{name:{type:String,default:"default"}},slots:Object,setup(t,{slots:n}){const i=m(),a=e.shallowRef(),r=e.shallowRef(!1);let u=!1,c=!1;async function o(){if(!(u||c)){c=!0,r.value=!1;try{const l=await Promise.resolve().then(()=>require("./router-view-impl-BvOToV_Z.cjs"));u||(a.value=l.MicroRouteViewImpl)}catch{u||(r.value=!0)}finally{c=!1}}}return e.onBeforeUnmount(()=>{u=!0}),o(),()=>{var l,s;return a.value?e.h(a.value,{name:t.name},n):r.value?((l=n.error)==null?void 0:l.call(n,{tab:i,code:"LOAD_FAILED",retry:o}))??e.h("div",{role:"alert","data-micro-route-error":"LOAD_FAILED"},[e.h("p","页面加载失败(LOAD_FAILED)"),e.h("button",{type:"button",onClick:o},"重新加载")]):((s=n.loading)==null?void 0:s.call(n,{tab:i}))??e.h("p",{role:"status"},"页面加载中…")}}});function v(t){return{hasPermission:n=>f.hasPermission(e.toValue(t),n),hasAny:n=>f.hasAny(e.toValue(t),n),hasAll:n=>f.hasAll(e.toValue(t),n),can:n=>e.computed(()=>f.hasPermission(e.toValue(t),e.toValue(n)))}}const b=e.defineComponent({name:"MicroAppContainer",props:{appId:{type:String,required:!0},runtime:{type:Object,required:!0},retryDisabled:Boolean},setup(t){const n=e.shallowRef(),i=e.shallowRef(""),a=e.shallowRef(!1);let r=!1;async function u(c=!1){if(!(r||a.value||!c&&t.retryDisabled)){a.value=!0,i.value="";try{await t.runtime.mount(t.appId,n.value)}catch{r||(i.value="子应用加载失败,请重试或联系管理员")}finally{r||(a.value=!1)}}}return e.onMounted(()=>{u(!0)}),e.onBeforeUnmount(()=>{r=!0,t.runtime.unmount(t.appId).catch(()=>console.error("[fin-micro] 子应用资源释放失败"))}),()=>e.h("section",{style:{height:"100%",minHeight:"420px"},"aria-busy":a.value},[i.value?e.h("div",{role:"alert"},[e.h("p",i.value),e.h("button",{type:"button",disabled:t.retryDisabled||a.value,onClick:()=>{u()}},"重新加载")]):null,a.value?e.h("p",{role:"status"},"子应用加载中…"):null,e.h("div",{ref:n,style:{height:"100%",minHeight:"420px"}})])}});function p(t){const n=e.shallowRef(t.getState()),i=t.subscribe(a=>{n.value=a});return e.onBeforeUnmount(i),n}const M=e.defineComponent({name:"MicroTabBoundary",props:{tab:{type:Object,required:!0},active:Boolean},setup(t,{slots:n}){const i=Object.freeze({...t.tab});e.provide(d,i);const a=e.defineComponent({name:"MicroTabContent",setup:()=>()=>{var r;return(r=n.default)==null?void 0:r.call(n,{tab:i})}});return()=>e.h("section",{hidden:!t.active,"data-micro-tab":i.key},[e.h(e.KeepAlive,null,{default:()=>t.active?e.h(a):null})])}}),w=e.defineComponent({name:"MicroTabCache",props:{tabs:{type:Array,required:!0},activeKey:String,max:{type:Number,default:20}},setup(t,{slots:n}){const i=e.shallowRef([]);return e.watch(()=>[t.tabs,t.activeKey,t.max],()=>{const a=new Set(t.tabs.map(o=>o.key));let r=i.value.filter(o=>a.has(o.key));const u=t.tabs.find(o=>o.key===t.activeKey);u&&(r=[...r.filter(o=>o.key!==u.key),u]);const c=Number.isFinite(t.max)?Math.max(1,Math.floor(t.max)):20;for(;r.length>c;){const o=r.findIndex(l=>l.key!==t.activeKey);if(o<0)break;r.splice(o,1)}i.value=r},{immediate:!0,flush:"sync"}),()=>i.value.map(a=>e.h(M,{key:a.key,tab:a,active:a.key===t.activeKey},n))}});function g(t,n,i=()=>console.error("[fin-micro] 路由同步失败")){let a=!1,r,u=!1,c=0;async function o(){if(!u){u=!0;try{let s=-1;for(;!a&&r&&s!==c;){s=c;try{await n.replace(r.route,r)}catch(h){try{i(h)}catch{}}}}finally{u=!1}}}const l=t.ready(s=>{r=s,c++,o()});return()=>{a=!0,l()}}function k(t,n){const i=t.page(n.key);return e.onBeforeUnmount(i.dispose),i}exports.MicroAppContainer=b;exports.MicroRouteView=y;exports.MicroTabCache=w;exports.connectMicroRouter=g;exports.useMicroHost=p;exports.useMicroPage=k;exports.useMicroTab=m;exports.usePermission=v;
package/dist/vue.mjs CHANGED
@@ -1,22 +1,23 @@
1
- import { inject as b, defineComponent as s, shallowRef as d, onBeforeUnmount as y, h as u, onMounted as p, provide as h, KeepAlive as M, watch as g } from "vue";
2
- const m = Symbol("micro-tab");
3
- function k() {
4
- const e = b(m);
1
+ import { inject as p, defineComponent as y, shallowRef as d, onBeforeUnmount as m, h as u, onMounted as M, provide as g, KeepAlive as k, watch as A, computed as w, toValue as s } from "vue";
2
+ import { hasPermission as v, hasAll as x, hasAny as C } from "./sdk.mjs";
3
+ const b = Symbol("micro-tab");
4
+ function D() {
5
+ const e = p(b);
5
6
  if (!e) throw new Error("useMicroTab 必须在 MicroTabCache 内调用");
6
7
  return e;
7
8
  }
8
- const A = s({
9
+ const O = y({
9
10
  name: "MicroRouteView",
10
11
  props: { name: { type: String, default: "default" } },
11
12
  slots: Object,
12
13
  setup(e, { slots: t }) {
13
- const r = k(), a = d(), n = d(!1);
14
+ const r = D(), a = d(), n = d(!1);
14
15
  let o = !1, c = !1;
15
16
  async function i() {
16
17
  if (!(o || c)) {
17
18
  c = !0, n.value = !1;
18
19
  try {
19
- const l = await import("./router-view-impl-C1KG6mxu.mjs");
20
+ const l = await import("./router-view-impl-B1NzCGKV.mjs");
20
21
  o || (a.value = l.MicroRouteViewImpl);
21
22
  } catch {
22
23
  o || (n.value = !0);
@@ -25,7 +26,7 @@ const A = s({
25
26
  }
26
27
  }
27
28
  }
28
- return y(() => {
29
+ return m(() => {
29
30
  o = !0;
30
31
  }), i(), () => {
31
32
  var l, f;
@@ -35,7 +36,16 @@ const A = s({
35
36
  ]) : ((f = t.loading) == null ? void 0 : f.call(t, { tab: r })) ?? u("p", { role: "status" }, "页面加载中…");
36
37
  };
37
38
  }
38
- }), C = s({
39
+ });
40
+ function L(e) {
41
+ return {
42
+ hasPermission: (t) => v(s(e), t),
43
+ hasAny: (t) => C(s(e), t),
44
+ hasAll: (t) => x(s(e), t),
45
+ can: (t) => w(() => v(s(e), s(t)))
46
+ };
47
+ }
48
+ const S = y({
39
49
  name: "MicroAppContainer",
40
50
  props: {
41
51
  appId: { type: String, required: !0 },
@@ -50,17 +60,17 @@ const A = s({
50
60
  a.value = !0, r.value = "";
51
61
  try {
52
62
  await e.runtime.mount(e.appId, t.value);
53
- } catch (i) {
54
- n || (r.value = String(i));
63
+ } catch {
64
+ n || (r.value = "子应用加载失败,请重试或联系管理员");
55
65
  } finally {
56
66
  n || (a.value = !1);
57
67
  }
58
68
  }
59
69
  }
60
- return p(() => {
70
+ return M(() => {
61
71
  o(!0);
62
- }), y(() => {
63
- n = !0, e.runtime.unmount(e.appId).catch(console.error);
72
+ }), m(() => {
73
+ n = !0, e.runtime.unmount(e.appId).catch(() => console.error("[fin-micro] 子应用资源释放失败"));
64
74
  }), () => u("section", { style: { height: "100%", minHeight: "420px" }, "aria-busy": a.value }, [
65
75
  r.value ? u("div", { role: "alert" }, [
66
76
  u("p", r.value),
@@ -73,13 +83,13 @@ const A = s({
73
83
  ]);
74
84
  }
75
85
  });
76
- function D(e) {
86
+ function j(e) {
77
87
  const t = d(e.getState()), r = e.subscribe((a) => {
78
88
  t.value = a;
79
89
  });
80
- return y(r), t;
90
+ return m(r), t;
81
91
  }
82
- const w = s({
92
+ const T = y({
83
93
  name: "MicroTabBoundary",
84
94
  props: {
85
95
  tab: { type: Object, required: !0 },
@@ -87,16 +97,16 @@ const w = s({
87
97
  },
88
98
  setup(e, { slots: t }) {
89
99
  const r = Object.freeze({ ...e.tab });
90
- h(m, r);
91
- const a = s({ name: "MicroTabContent", setup: () => () => {
100
+ g(b, r);
101
+ const a = y({ name: "MicroTabContent", setup: () => () => {
92
102
  var n;
93
103
  return (n = t.default) == null ? void 0 : n.call(t, { tab: r });
94
104
  } });
95
105
  return () => u("section", { hidden: !e.active, "data-micro-tab": r.key }, [
96
- u(M, null, { default: () => e.active ? u(a) : null })
106
+ u(k, null, { default: () => e.active ? u(a) : null })
97
107
  ]);
98
108
  }
99
- }), T = s({
109
+ }), B = y({
100
110
  name: "MicroTabCache",
101
111
  props: {
102
112
  tabs: { type: Array, required: !0 },
@@ -105,7 +115,7 @@ const w = s({
105
115
  },
106
116
  setup(e, { slots: t }) {
107
117
  const r = d([]);
108
- return g(() => [e.tabs, e.activeKey, e.max], () => {
118
+ return A(() => [e.tabs, e.activeKey, e.max], () => {
109
119
  const a = new Set(e.tabs.map((i) => i.key));
110
120
  let n = r.value.filter((i) => a.has(i.key));
111
121
  const o = e.tabs.find((i) => i.key === e.activeKey);
@@ -117,14 +127,14 @@ const w = s({
117
127
  n.splice(i, 1);
118
128
  }
119
129
  r.value = n;
120
- }, { immediate: !0, flush: "sync" }), () => r.value.map((a) => u(w, {
130
+ }, { immediate: !0, flush: "sync" }), () => r.value.map((a) => u(T, {
121
131
  key: a.key,
122
132
  tab: a,
123
133
  active: a.key === e.activeKey
124
134
  }, t));
125
135
  }
126
136
  });
127
- function I(e, t, r = console.error) {
137
+ function R(e, t, r = () => console.error("[fin-micro] 路由同步失败")) {
128
138
  let a = !1, n, o = !1, c = 0;
129
139
  async function i() {
130
140
  if (!o) {
@@ -135,8 +145,11 @@ function I(e, t, r = console.error) {
135
145
  f = c;
136
146
  try {
137
147
  await t.replace(n.route, n);
138
- } catch (v) {
139
- r(v);
148
+ } catch (h) {
149
+ try {
150
+ r(h);
151
+ } catch {
152
+ }
140
153
  }
141
154
  }
142
155
  } finally {
@@ -151,16 +164,17 @@ function I(e, t, r = console.error) {
151
164
  a = !0, l();
152
165
  };
153
166
  }
154
- function K(e, t) {
167
+ function q(e, t) {
155
168
  const r = e.page(t.key);
156
- return y(r.dispose), r;
169
+ return m(r.dispose), r;
157
170
  }
158
171
  export {
159
- C as MicroAppContainer,
160
- A as MicroRouteView,
161
- T as MicroTabCache,
162
- I as connectMicroRouter,
163
- D as useMicroHost,
164
- K as useMicroPage,
165
- k as useMicroTab
172
+ S as MicroAppContainer,
173
+ O as MicroRouteView,
174
+ B as MicroTabCache,
175
+ R as connectMicroRouter,
176
+ j as useMicroHost,
177
+ q as useMicroPage,
178
+ D as useMicroTab,
179
+ L as usePermission
166
180
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axewo/fin-micro",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -86,7 +86,7 @@
86
86
  "license": "MIT",
87
87
  "scripts": {
88
88
  "build": "vite build",
89
- "test": "vitest run",
89
+ "test": "node ../../node_modules/vitest/vitest.mjs run",
90
90
  "typecheck": "vue-tsc --noEmit -p tsconfig.json",
91
91
  "test:cli": "node --test cli/create.test.mjs cli/scaffold.test.mjs",
92
92
  "check:package": "node scripts/check-package.mjs"
@@ -4,18 +4,22 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
+ "check:runtime": "node scripts/check-runtime.mjs",
8
+ "predev": "npm run check:runtime",
9
+ "prebuild": "npm run check:runtime",
7
10
  "dev": "vite",
8
11
  "build": "vue-tsc --noEmit && vite build",
9
12
  "preview": "vite preview",
10
13
  "typecheck": "vue-tsc --noEmit"
11
14
  },
12
15
  "dependencies": {
13
- "@axewo/fin-core": "2.1.0",
16
+ "@axewo/fin-core": "2.1.1",
14
17
  "@axewo/fin-micro": "__FIN_MICRO_VERSION__",
15
18
  "@axewo/fin-ui": "__FIN_UI_VERSION__",
16
19
  "@axewo/fin-editor": "2.0.1",
17
20
  "@element-plus/icons-vue": "2.3.2",
18
21
  "element-plus": "2.14.2",
22
+ "pinia": "2.3.1",
19
23
  "vue": "3.5.13",
20
24
  "vue-i18n": "^9.14.0",
21
25
  "vue-router": "4.5.0"
@@ -0,0 +1,43 @@
1
+ import { readdir, readFile } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ // 构建前检查安装包的实际导出,避免 Vite 链接失败被误认为登录或网络问题。
6
+ const imports = new Map()
7
+ let hostRouter = false
8
+ async function scan(directory) {
9
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
10
+ const file = join(directory, entry.name)
11
+ if (entry.isDirectory()) await scan(file)
12
+ else if (/\.(ts|vue)$/.test(entry.name)) {
13
+ const source = await readFile(file, 'utf8')
14
+ if (source.includes('connectWorkspaceRouter')) hostRouter = true
15
+ for (const match of source.matchAll(/import\s*\{([^}]+)\}\s*from\s*['"](@axewo\/[^'"]+)['"]/g)) {
16
+ const names = imports.get(match[2]) ?? new Set()
17
+ for (const part of match[1].split(',')) {
18
+ const name = part.trim()
19
+ if (name && !name.startsWith('type ')) names.add(name.split(/\s+as\s+/)[0])
20
+ }
21
+ imports.set(match[2], names)
22
+ }
23
+ }
24
+ }
25
+ }
26
+ export async function checkRuntime() {
27
+ await scan(fileURLToPath(new URL('../src', import.meta.url)))
28
+ const problems = []
29
+ for (const [specifier, names] of imports) {
30
+ try {
31
+ const module = await import(specifier)
32
+ for (const name of names) if (!(name in module)) problems.push(`${specifier} 缺少导出 ${name}`)
33
+ if (specifier === '@axewo/fin-micro' && hostRouter && module.hostRouterProtocol !== 1) {
34
+ problems.push('@axewo/fin-micro 缺少 hostRouterProtocol=1(真实 host 路由、local 页面和 url:false 连接器能力)')
35
+ }
36
+ } catch { problems.push(`${specifier} 无法加载,请检查安装版本及其依赖(不输出原始异常)`) }
37
+ }
38
+ if (problems.length) {
39
+ console.error('启动依赖能力检查失败:\n' + problems.join('\n') + '\n请安装与生成器配套构建的包,并重新运行 npm run check:runtime;不会自动改依赖。')
40
+ process.exitCode = 1
41
+ } else console.log('启动依赖能力检查通过')
42
+ }
43
+ await checkRuntime()
@@ -0,0 +1,12 @@
1
+ // 与中文保持相同的业务词条结构。
2
+ export default {
3
+ demo: {
4
+ title: 'Locale and state demo',
5
+ description: 'Switch language, increment or reset. State belongs only to this app.',
6
+ count: 'Count: {count}',
7
+ doubled: 'Double count: {count}',
8
+ increment: 'Increment',
9
+ reset: 'Reset',
10
+ switchLanguage: 'Switch language',
11
+ },
12
+ }
@@ -1,7 +1,13 @@
1
1
  import type { App } from 'vue'
2
2
  import { installFinI18n } from '@axewo/fin-ui'
3
+ import zhCN from './zh-CN'
4
+ import en from './en'
3
5
 
4
6
  /** 每次 createApp 后独立安装,不在模块级共享可变 i18n 实例。 */
5
7
  export function installAppLocale(app: App): void {
6
- installFinI18n(app, { locale: 'zh-CN', fallbackLocale: 'zh-CN', legacy: false })
8
+ // 工厂递归合并业务词条,不覆盖 fin-ui 内置语言包。
9
+ installFinI18n(app, {
10
+ locale: 'zh-CN', fallbackLocale: 'zh-CN', legacy: false,
11
+ messages: { 'zh-CN': zhCN, en },
12
+ })
7
13
  }
@@ -0,0 +1,12 @@
1
+ // 业务词条使用独立命名空间,与 fin-ui 内置 fin.* 词条合并。
2
+ export default {
3
+ demo: {
4
+ title: '国际化与状态示例',
5
+ description: '切换语言、增加计数或重置,状态仅属于当前应用。',
6
+ count: '当前计数:{count}',
7
+ doubled: '双倍计数:{count}',
8
+ increment: '增加',
9
+ reset: '重置',
10
+ switchLanguage: '切换语言',
11
+ },
12
+ }
@@ -1,14 +1,22 @@
1
1
  import 'element-plus/dist/index.css'
2
2
  import '@axewo/fin-ui/style.css'
3
3
  import './styles/index.css'
4
- import { mount, unmount } from './bootstrap'
4
+ import { createStartupLifecycle } from './utils/errorUtils'
5
+
6
+ // 动态导入使配置解析等模块求值错误也进入脱敏启动边界。
7
+ const { mount, unmount } = createStartupLifecycle(() => import('./bootstrap'))
5
8
 
6
9
  // 普通应用直接启动;Vue 子应用模板覆盖本入口,接入微前端生命周期。
7
10
  // fin-ui 公开根入口依赖 editor 与 icons,依赖已在公共 package.json 中显式声明。
8
11
  void mount()
9
- window.addEventListener('unload', unmount)
10
- if (import.meta.hot) import.meta.hot.dispose(() => {
12
+ function onPageHide(event: PageTransitionEvent) {
13
+ // BFCache 返回时继续使用同一应用;保留监听以处理之后的普通离开。
14
+ if (!event.persisted) dispose()
15
+ }
16
+ function dispose() {
11
17
  // 基座只清理自身实例,不注册 Wujie 子应用全局回调。
12
- window.removeEventListener('unload', unmount)
18
+ window.removeEventListener('pagehide', onPageHide)
13
19
  unmount()
14
- })
20
+ }
21
+ window.addEventListener('pagehide', onPageHide)
22
+ if (import.meta.hot) import.meta.hot.dispose(dispose)
@@ -0,0 +1,6 @@
1
+ import type { RouteRecordRaw } from 'vue-router'
2
+
3
+ // 仅定义本地页面,不授予宿主菜单/页签权限;host 默认不自动安装。
4
+ export const demoRoutes: RouteRecordRaw[] = [
5
+ { path: '/demo', name: 'demo', component: () => import('../views/DemoView.vue') },
6
+ ]
@@ -0,0 +1,13 @@
1
+ import { createMemoryHistory, createRouter, type RouteRecordRaw, type Router } from 'vue-router'
2
+
3
+ /**
4
+ * 每轮挂载创建独立路由,默认使用空路由表,业务可通过参数传入本地页面。
5
+ * 此通用工厂不拥有地址栏;host overlay 用真实 history 路由覆盖本文件,vue overlay 按嵌入模式选历史。
6
+ * 独立使用本工厂时须由调用方负责 URL 适配;本地路由不替代平台认证及 canOpen 检查。
7
+ * @param routes 本地页面路由表;省略时为空,不自动添加业务页面。
8
+ * @returns 独立 Router;调用方安装到 App,并在卸载时销毁 history。
9
+ * @throws 无效路由配置导致的 Vue Router 初始化异常原样抛出。
10
+ */
11
+ export function createAppRouter(routes: RouteRecordRaw[] = []): Router {
12
+ return createRouter({ history: createMemoryHistory(), routes })
13
+ }
@@ -0,0 +1,12 @@
1
+ import { computed, ref } from 'vue'
2
+ import { defineStore } from 'pinia'
3
+
4
+ // 只定义 store,不在模块顶层实例化;状态由当前应用安装的 Pinia 隔离。
5
+ export const useDemoStore = defineStore('demo', () => {
6
+ const count = ref(0)
7
+ const doubled = computed(() => count.value * 2)
8
+ function increment(): void { count.value++ }
9
+ // setup store 显式提供重置 action,不使用 options store 的默认 $reset。
10
+ function reset(): void { count.value = 0 }
11
+ return { count, doubled, increment, reset }
12
+ })
@@ -0,0 +1,11 @@
1
+ import { createPinia, type Pinia } from 'pinia'
2
+
3
+ /**
4
+ * 每个 Vue App、每轮挂载独立创建状态容器,禁止导出跨应用共享的模块级单例。
5
+ * @returns 未安装的独立 Pinia;调用方通过 app.use 安装,卸载时通过 disposePinia 释放。
6
+ * @throws 不额外包装底层初始化异常。
7
+ * @remarks 无参数;业务 store 需在安装后使用,组件外调用须显式传入所属 Pinia。
8
+ */
9
+ export function createAppPinia(): Pinia {
10
+ return createPinia()
11
+ }
@@ -0,0 +1,49 @@
1
+ /** 构建时公开配置;VITE_* 均会进入前端产物,禁止放入任何密码或 Token。 */
2
+ export interface AppConfig {
3
+ /** Axios API 目录;相对地址仅允许当前源的绝对路径。 */
4
+ readonly apiBase: string
5
+ /** 凭据允许发送的精确 API 目录,不从请求参数推断或扩大。 */
6
+ readonly trustedApiBase: string
7
+ /** 请求超时毫秒数,必须为正有限整数且不超过原生计时器上限。 */
8
+ readonly timeout: number
9
+ /** 是否允许跨源 Cookie;服务端仍须配置精确 CORS 与 CSRF 防护。 */
10
+ readonly withCredentials: boolean
11
+ }
12
+
13
+ /** 校验公开 HTTP(S) 地址;拒绝凭据、查询、片段、协议相对地址及编码路径歧义,不回显输入。 */
14
+ export function validateAddress(value: string, directory = true): string {
15
+ if (typeof value !== 'string' || !value || value.trim() !== value || /[\\\s\u0000-\u001f\u007f]/.test(value) || /%(?:2f|5c|2e)/i.test(value) || /(?:^|\/)\.{1,2}(?:\/|$)/.test(value)) throw new Error('应用地址配置无效')
16
+ const relative = value.startsWith('/') && !value.startsWith('//')
17
+ if (!relative && !/^https?:\/\//i.test(value)) throw new Error('应用地址必须为 HTTP(S) 或同源绝对路径')
18
+ const url = new URL(value, 'https://config.invalid')
19
+ if (url.username || url.password || /[?#]/.test(value) || (directory && !url.pathname.endsWith('/'))) throw new Error('应用地址必须为无凭据、查询和片段的目录')
20
+ // 只有本机开发地址允许 HTTP;生产网关和代码入口必须使用 HTTPS。
21
+ if (!relative && url.protocol !== 'https:' && !['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)) throw new Error('非本机应用地址必须使用 HTTPS')
22
+ return value
23
+ }
24
+
25
+ /** 将 Vite 环境转换成只读配置;非法值立即拒绝启动,默认值不扩大凭据目录。 */
26
+ export function resolveAppConfig(env: Record<string, unknown> = {}): AppConfig {
27
+ const apiBase = validateAddress((env.VITE_API_BASE ?? '/api/') as string)
28
+ const trustedApiBase = validateAddress((env.VITE_TRUSTED_API_BASE ?? apiBase) as string)
29
+ if (apiBase !== trustedApiBase) throw new Error('API 与可信凭据目录必须一致')
30
+ const rawTimeout = env.VITE_API_TIMEOUT ?? 30000
31
+ if ((typeof rawTimeout !== 'string' && typeof rawTimeout !== 'number') || !/^\d+$/.test(String(rawTimeout))) throw new Error('API 超时配置无效')
32
+ const timeout = Number(rawTimeout)
33
+ if (!Number.isInteger(timeout) || timeout <= 0 || timeout > 2147483647) throw new Error('API 超时配置无效')
34
+ const credentials = env.VITE_API_CREDENTIALS ?? 'false'
35
+ if (credentials !== 'true' && credentials !== 'false') throw new Error('API credentials 配置必须为 true 或 false')
36
+ return Object.freeze({ apiBase, trustedApiBase, timeout, withCredentials: credentials === 'true' })
37
+ }
38
+
39
+ /** 解析部署者控制的可信代码入口;只读取构建环境,后台 appIds/menus 不得覆盖 entry。 */
40
+ export function resolveTrustedApp(env: Record<string, unknown> = {}): { readonly appId: string; readonly entry: string } {
41
+ const appId = env.VITE_MICRO_APP_ID ?? 'business'
42
+ const entry = env.VITE_MICRO_APP_ENTRY ?? 'http://localhost:7101'
43
+ if (typeof appId !== 'string' || appId.length > 214 || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(appId)) throw new Error('可信应用 appId 配置无效')
44
+ if (typeof entry !== 'string' || !/^https?:\/\//i.test(entry)) throw new Error('可信应用 entry 必须为完整 HTTP(S) 地址')
45
+ return Object.freeze({ appId, entry: validateAddress(entry, false) })
46
+ }
47
+
48
+ /** 应用模块专属公开配置,不包含宿主认证状态;Node 测试环境使用相同默认值。 */
49
+ export const appConfig = resolveAppConfig(import.meta.env)