web-extend-plugin-vue2 0.2.5 → 0.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # web-extend-plugin-vue2
2
2
 
3
- 面向 **Vue 2.6+** 与 **Vue Router 3** 的 Web 插件运行时:浏览器中拉取**插件清单**、加载入口脚本、执行 `activator(hostApi)`,并提供 **`ExtensionPoint`**、菜单/路由注册表与受控 **`getBridge()`**。发布物为预构建的 **`dist/`**(CJS/ESM),类型见根目录 **`index.d.ts`**。
3
+ 面向 **Vue 2.6+** 与 **Vue Router 3** 的 Web 插件运行时:浏览器中拉取**插件清单**、加载入口脚本、执行 `activator(hostApi)`,并提供 **`ExtensionPoint`**、动态路由注册与受控 **`getBridge()`**;侧栏菜单由宿主根据路由 **`meta`** 自行推导(`buildMenuDescriptorsFromRoutes`)。发布物为预构建的 **`dist/`**(CJS/ESM),类型见根目录 **`index.d.ts`**。
4
4
 
5
5
  **Peer:`vue` ^2.6.14 \<3,`vue-router` ^3.5 \<4**(依赖 `router.addRoute`)。**
6
6
 
@@ -33,7 +33,7 @@ installWebExtendPluginVue2(Vue, router, {
33
33
  }).catch(console.warn)
34
34
  ```
35
35
 
36
- **Vue CLI + 已有 axios `request`**(开箱:`/plugin` 壳路由 + Layout 由框架注册;清单可走 Java,开发态可自动回退 `public/web-plugins/plugins.manifest.json`):
36
+ **Vue CLI + 已有 axios `request`**(清单可走 Java,开发态可自动回退 `public/web-plugins/plugins.manifest.json`;**不再**默认注册 `/plugin` 壳,需显式 `ensurePluginHostRoute: true` 且配置 `pluginRoutesParentName` + `hostLayoutComponent`):
37
37
 
38
38
  **更少样板(推荐)**:自动写入 `window.Vue`(IIFE 插件与 build-kit 约定)、合并 `hostContext`(`router` + 可选 `store`)、默认 `manifestListPath` 为 **`/frontend-plugins`**(与 `VUE_APP_BASE_API` 拼接;若后端为 `/api/frontend-plugins` 请在第 4 参数传 `manifestListPath: '/api/frontend-plugins'`)、`isDev` 与构建环境一致。按需关闭全局 Vue:`{ exposeGlobalVue: false }`。
39
39
 
@@ -49,15 +49,13 @@ installVueCliAxiosWebPlugins(
49
49
  {
50
50
  request,
51
51
  hostLayoutComponent: Layout,
52
- store,
53
- applyPluginMenuItems: ({ pluginId, items }) => {
54
- /* 并入宿主侧栏 */
55
- },
56
- revokePluginMenuItems: (pluginId) => {
57
- /* 撤销该插件菜单 */
58
- }
52
+ store
59
53
  },
60
- { manifestBase: process.env.VUE_APP_BASE_API }
54
+ {
55
+ manifestBase: process.env.VUE_APP_BASE_API,
56
+ pluginRoutesParentName: '__wepPluginHost',
57
+ ensurePluginHostRoute: true
58
+ }
61
59
  ).catch((e) => console.warn('[web-plugin] bootstrap failed', e))
62
60
  ```
63
61
 
@@ -83,11 +81,18 @@ import Layout from '@/layout'
83
81
  installWebExtendPluginVue2(
84
82
  Vue,
85
83
  router,
86
- createVueCliAxiosInstallOptions({ request }, { hostLayoutComponent: Layout })
84
+ createVueCliAxiosInstallOptions(
85
+ { request },
86
+ {
87
+ hostLayoutComponent: Layout,
88
+ pluginRoutesParentName: '__wepPluginHost',
89
+ ensurePluginHostRoute: true
90
+ }
91
+ )
87
92
  ).catch(console.warn)
88
93
  ```
89
94
 
90
- 默认插件父路由 **`name`** **`__wepPluginHost`**,路径 **`/plugin`**;插件内 `registerRoutes` 使用相对 `path`(如 `plugin-b-sample`)即可,全路径为 `/plugin/...`。自定义:`pluginRoutesParentName`、`pluginMountPath`。
95
+ 若启用壳路由:`pluginRoutesParentName`(如 **`__wepPluginHost`**)+ `pluginMountPath`(默认 **`/plugin`**)+ `ensurePluginHostRoute: true`。未启用壳时,`registerRoutes` `path` 为**绝对路径**或宿主在 `transformRoutes` 中自行加前缀。
91
96
 
92
97
  **静态清单模式**(无需 Java `frontend-plugins`,将聚合 JSON 放入宿主 `public` 等静态目录):
93
98
 
@@ -144,11 +149,11 @@ bootstrapPlugins(router, (id, r, kit) => createHostApi(id, r, kit), runtime).cat
144
149
  | `bridgeAllowedPathPrefixes` | `hostApi.getBridge().request(path)` 允许的 path 前缀 |
145
150
  | `allowedScriptHosts` | 允许加载插件脚本的主机名白名单 |
146
151
  | `isDev` / `webPluginDevOrigin` 等 | 开发态隐式 dev 入口与 SSE,见类型定义 |
147
- | `hostLayoutComponent` | 传入后自动注册 `/plugin` + Layout 壳(`addRoute`),无需在宿主路由表手写 `/plugin` |
148
- | `pluginMountPath` / `pluginHostRouteMeta` / `ensurePluginHostRoute` | 壳路径(默认 `/plugin`)、meta、是否自动注册(默认 true |
149
- | `pluginRoutesParentName` | 壳路由的 **name**,插件子项挂在其下;未传且传入 `hostLayoutComponent` 时默认为 **`__wepPluginHost`** |
152
+ | `hostLayoutComponent` | `ensurePluginHostRoute: true` 且配置了 `pluginRoutesParentName` 时用于自动注册壳路由的 Layout 组件 |
153
+ | `pluginMountPath` / `pluginHostRouteMeta` / `ensurePluginHostRoute` | 壳路径(默认 `/plugin`)、壳 meta、**仅在为 true 时**自动注册壳(默认 **false**) |
154
+ | `pluginRoutesParentName` | 命名父路由的 **name**;非空时 `registerRoutes` 子项通过 `addRoute(parentName, child)` 挂载 |
150
155
  | `adaptRouteDeclarations` / `transformRoutes` / `interceptRegisterRoutes` | 路由 PRD 适配与宿主扩展流水线 |
151
- | `applyPluginMenuItems` / `revokePluginMenuItems` | **菜单**:插件 `registerMenuItems` 仅触发回调,由宿主并入自己的侧栏/权限路由等数据结构;卸载时撤销 |
156
+ | `buildMenuDescriptorsFromRoutes`(具名导出 / `WebExtendPluginVue2.host`) | 由插件提交的 `RouteConfig` 树与 **`meta.title` / `meta.menuTitle`** 等推导菜单数据,供宿主写入 Vuex 等 |
152
157
  | `hostContext` | **依赖注入(推荐)**:普通对象,引导时做**浅拷贝并浅冻结**后挂到 **`hostApi.hostContext`**,插件只读使用 `store` / `router` / 宿主开放 API 等,不污染 `HostApi` 顶层方法名 |
153
158
  | `onBeforePluginActivate` / `onAfterPluginActivate` / `onPluginActivateError` | **生命周期**:激活前(可 `throw` 跳过该插件)、成功后、抛错后;便于埋点、门禁、监控,各宿主同一套签名即可接入 |
154
159
 
@@ -156,9 +161,10 @@ bootstrapPlugins(router, (id, r, kit) => createHostApi(id, r, kit), runtime).cat
156
161
 
157
162
  ### 宿主接入约定(跨项目统一)
158
163
 
159
- 1. **菜单**:实现 `applyPluginMenuItems` + `revokePluginMenuItems`,菜单字段→本地侧栏结构的映射留在宿主。
164
+ 1. **菜单**:插件在 `registerRoutes` **`meta`** 中写明 `title` / `order` / `icon` / `permission` / `hidden` 等;宿主在 `onAfterPluginActivate` 或统一收集阶段用 **`buildMenuDescriptorsFromRoutes(routes, pluginId)`** 生成侧栏数据(或由宿主在 `transformRoutes` 之后自行扫 `router.getRoutes()`)。**不再**使用 `registerMenuItems`。
160
165
  2. **宿主能力**:通过 **`hostContext`** 注入(如 `{ store, router, getDict }`),插件内使用 **`hostApi.hostContext.store`**,避免 `Vue.prototype` 魔法与命名冲突。
161
- 3. **拦截激活**:在 **`onBeforePluginActivate`** 中做权限/租户校验,不通过则 **`throw`**;错误用 **`onPluginActivateError`** 上报,勿在 error 钩子里再抛异常。
166
+ 3. **拦截激活**:在 **`onBeforePluginActivate`** 中做权限/租户校验,不通过则 **`throw`**;错误用 **`onPluginActivateError`** 上报,勿在 error 钩子里再抛异常。
167
+ 4. **卸载**:`disposeWebPlugin(pluginId)` 会 **`removeRoute`** 移除该插件登记的动态路由,并清理槽位、脚本与 teardown;侧栏需按 **`pluginId`** 或重建菜单树与路由对齐。
162
168
 
163
169
  ---
164
170
 
@@ -170,19 +176,30 @@ bootstrapPlugins(router, (id, r, kit) => createHostApi(id, r, kit), runtime).cat
170
176
  <ExtensionPoint point-id="app.toolbar.demo" />
171
177
  ```
172
178
 
173
- **侧栏菜单**(框架不维护菜单列表;`registries` 仅含扩展点 `slots`):
179
+ **侧栏菜单**(`registries` 仅含扩展点 `slots`;菜单与路由同源):
174
180
 
175
181
  ```js
176
- // resolveRuntimeOptions / install options 中提供:
177
- applyPluginMenuItems: ({ pluginId, items }) => {
178
- // items 映射为宿主侧栏数据结构(如 Vuex、与后端路由同形的树等)
179
- },
180
- revokePluginMenuItems: (pluginId) => {
181
- // 从宿主 state 移除该插件贡献的菜单项
182
- }
182
+ import { buildMenuDescriptorsFromRoutes } from 'web-extend-plugin-vue2'
183
+
184
+ // 插件 activator 内:
185
+ hostApi.registerRoutes([
186
+ {
187
+ path: 'hello',
188
+ component: Hello,
189
+ meta: { title: '插件 Hello', order: 10, pluginId: 'com.example' }
190
+ }
191
+ ])
192
+
193
+ // 宿主在 onAfterPluginActivate 或自行约定的收集点:
194
+ const items = buildMenuDescriptorsFromRoutes(
195
+ [
196
+ /* 与插件注册前相同的 RouteConfig 快照,或由宿主在 transformRoutes 中缓存 */
197
+ ],
198
+ pluginId
199
+ )
183
200
  ```
184
201
 
185
- 插件侧仍为 `hostApi.registerMenuItems([{ path, title, order, ... }])`;是否显示、图标、层级样式全部由宿主决定。
202
+ `disposeWebPlugin` 会移除对应动态路由;宿主从 state 中删除同一 `pluginId` 的菜单项即可。
186
203
 
187
204
  **宿主上下文(插件读宿主能力)**:
188
205
 
@@ -198,7 +215,7 @@ hostContext: {
198
215
 
199
216
  **路由扩展(宿主)**:在 `resolveRuntimeOptions` 里提供 `transformRoutes` 或 `adaptRouteDeclarations`,插件在 `activator` 第二个参数中取冻结的 **`pluginRecord`**(可含清单里的 `routeDeclarations`)。
200
217
 
201
- **卸载插件**:
218
+ **卸载插件**(含 `router.removeRoute` 批量撤销该插件登记的顶层路由名):
202
219
 
203
220
  ```js
204
221
  import { disposeWebPlugin } from 'web-extend-plugin-vue2'
package/dist/index.cjs CHANGED
@@ -344,15 +344,9 @@ function resolveRuntimeOptions$1(user = {}) {
344
344
  return String(DEF.devFallbackStaticManifestUrl).trim();
345
345
  })();
346
346
  const hostLayoutComponent = user.hostLayoutComponent;
347
- const pluginRoutesParentName = (() => {
348
- if (user.pluginRoutesParentName !== undefined) {
349
- return String(user.pluginRoutesParentName).trim();
350
- }
351
- if (hostLayoutComponent != null) {
352
- return String(DEF.pluginRoutesParentName).trim();
353
- }
354
- return '';
355
- })();
347
+ const pluginRoutesParentName = user.pluginRoutesParentName !== undefined && String(user.pluginRoutesParentName).trim() !== ''
348
+ ? String(user.pluginRoutesParentName).trim()
349
+ : '';
356
350
  const pluginMountRaw = user.pluginMountPath !== undefined && String(user.pluginMountPath).trim() !== ''
357
351
  ? String(user.pluginMountPath).trim()
358
352
  : String(resolveBundledEnv(EK.mountPath, '') || DEF.pluginMountPath).trim();
@@ -360,7 +354,7 @@ function resolveRuntimeOptions$1(user = {}) {
360
354
  const pluginHostRouteMeta = user.pluginHostRouteMeta !== undefined && user.pluginHostRouteMeta !== null
361
355
  ? user.pluginHostRouteMeta
362
356
  : undefined;
363
- const ensurePluginHostRoute = user.ensurePluginHostRoute !== false;
357
+ const ensurePluginHostRoute = user.ensurePluginHostRoute === true;
364
358
  const devManifestFallback = (() => {
365
359
  if (manifestMode === 'static') {
366
360
  return false;
@@ -423,12 +417,6 @@ function resolveRuntimeOptions$1(user = {}) {
423
417
  ...(typeof user.adaptRouteDeclarations === 'function'
424
418
  ? { adaptRouteDeclarations: user.adaptRouteDeclarations }
425
419
  : {}),
426
- ...(typeof user.applyPluginMenuItems === 'function'
427
- ? { applyPluginMenuItems: user.applyPluginMenuItems }
428
- : {}),
429
- ...(typeof user.revokePluginMenuItems === 'function'
430
- ? { revokePluginMenuItems: user.revokePluginMenuItems }
431
- : {}),
432
420
  ...(user.hostContext !== undefined &&
433
421
  user.hostContext !== null &&
434
422
  typeof user.hostContext === 'object' &&
@@ -3220,21 +3208,18 @@ var semverExports = requireSemver();
3220
3208
  var semver = /*@__PURE__*/getDefaultExportFromCjs(semverExports);
3221
3209
 
3222
3210
  /**
3223
- * `disposeWebPlugin` 使用的菜单撤销钩子:在 `bootstrapPlugins` 中用 `resolveRuntimeOptions` 结果注册。
3211
+ * 引导时注入的 router 引用,供 disposeWebPlugin 按插件批量 removeRoute。
3224
3212
  */
3225
- let revokePluginMenuItems;
3226
- function setRevokePluginMenuItems(fn) {
3227
- revokePluginMenuItems = fn;
3213
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3214
+ let bootstrapRouter;
3215
+ /** bootstrapPlugins 在浏览器环境调用 */
3216
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3217
+ function setPluginBootstrapRouter(router) {
3218
+ bootstrapRouter = router;
3228
3219
  }
3229
- function revokePluginMenusIfConfigured(pluginId) {
3230
- if (typeof revokePluginMenuItems === 'function') {
3231
- try {
3232
- revokePluginMenuItems(pluginId);
3233
- }
3234
- catch (e) {
3235
- console.warn('[wep] revokePluginMenuItems failed', pluginId, e);
3236
- }
3237
- }
3220
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3221
+ function getPluginBootstrapRouter() {
3222
+ return bootstrapRouter;
3238
3223
  }
3239
3224
 
3240
3225
  /**
@@ -3491,7 +3476,8 @@ async function fetchStaticManifestViaHttp(ctx) {
3491
3476
  }
3492
3477
 
3493
3478
  /**
3494
- * 开箱:在未手工配置时,注册 `/plugin` + 宿主 Layout 的命名父路由,供 `router.addRoute(parentName, child)` 挂载插件页。
3479
+ * `ensurePluginHostRoute === true` 且提供 `pluginRoutesParentName` + `hostLayoutComponent` 时,
3480
+ * 注册 `pluginMountPath` + Layout 的命名父路由,供 `router.addRoute(parentName, child)` 挂载插件页。
3495
3481
  */
3496
3482
  function routeNameExists(router, name) {
3497
3483
  if (!name) {
@@ -3518,7 +3504,7 @@ function walkRouteNames(routes, name) {
3518
3504
  return false;
3519
3505
  }
3520
3506
  function ensurePluginHostRoute$1(router, opts) {
3521
- if (opts.ensurePluginHostRoute === false) {
3507
+ if (opts.ensurePluginHostRoute !== true) {
3522
3508
  return;
3523
3509
  }
3524
3510
  if (!router || typeof router.addRoute !== 'function') {
@@ -3647,7 +3633,7 @@ createHostApiFactory, runtimeOptions) {
3647
3633
  }
3648
3634
  printRuntimeBannerOnce();
3649
3635
  const opts = resolveRuntimeOptions$1(runtimeOptions || {});
3650
- setRevokePluginMenuItems(typeof opts.revokePluginMenuItems === 'function' ? opts.revokePluginMenuItems : undefined);
3636
+ setPluginBootstrapRouter(router);
3651
3637
  ensurePluginHostRoute$1(router, opts);
3652
3638
  const base = String(opts.manifestBase).replace(/\/$/, '');
3653
3639
  const isStatic = opts.manifestMode === 'static';
@@ -3724,12 +3710,6 @@ createHostApiFactory, runtimeOptions) {
3724
3710
  : {}),
3725
3711
  ...(typeof opts.adaptRouteDeclarations === 'function'
3726
3712
  ? { adaptRouteDeclarations: opts.adaptRouteDeclarations }
3727
- : {}),
3728
- ...(typeof opts.applyPluginMenuItems === 'function'
3729
- ? { applyPluginMenuItems: opts.applyPluginMenuItems }
3730
- : {}),
3731
- ...(typeof opts.revokePluginMenuItems === 'function'
3732
- ? { revokePluginMenuItems: opts.revokePluginMenuItems }
3733
3713
  : {})
3734
3714
  };
3735
3715
  if (!manifestResult.ok) {
@@ -4019,7 +3999,7 @@ var manifestComposer = /*#__PURE__*/Object.freeze({
4019
3999
 
4020
4000
  /**
4021
4001
  * 宿主侧响应式注册表:扩展点槽位(供布局与 `ExtensionPoint` 消费)。
4022
- * 菜单数据由宿主在 `applyPluginMenuItems` 中自行并入其路由/菜单 state,框架不维护平行菜单列表。
4002
+ * 菜单由宿主从路由 `meta` 推导(见 `buildMenuDescriptorsFromRoutes`),框架不维护平行菜单列表。
4023
4003
  */
4024
4004
  const registries = Vue__default.default.observable({
4025
4005
  slots: {},
@@ -4082,6 +4062,47 @@ function createRequestBridge(config = {}) {
4082
4062
  };
4083
4063
  }
4084
4064
 
4065
+ /**
4066
+ * 记录各插件通过 registerRoutes 挂到 router 上的顶层 route name,便于 dispose 时 removeRoute。
4067
+ */
4068
+ const pluginIdToRouteNames = new Map();
4069
+ function recordPluginTopRouteNames(pluginId, names) {
4070
+ if (!pluginId || names.length === 0) {
4071
+ return;
4072
+ }
4073
+ const cur = pluginIdToRouteNames.get(pluginId) || [];
4074
+ pluginIdToRouteNames.set(pluginId, cur.concat(names));
4075
+ }
4076
+ /**
4077
+ * 从 matcher 移除该插件登记过的顶层路由(含其子树)。
4078
+ * 需 vue-router ≥3.5 的 removeRoute;否则静默跳过。
4079
+ */
4080
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4081
+ function removeRegisteredRoutesForPlugin(router, pluginId) {
4082
+ const names = pluginIdToRouteNames.get(pluginId);
4083
+ if (!names || names.length === 0) {
4084
+ return;
4085
+ }
4086
+ if (!router || typeof router.removeRoute !== 'function') {
4087
+ console.warn('[wep] router.removeRoute 不可用,跳过动态路由卸载', pluginId);
4088
+ pluginIdToRouteNames.delete(pluginId);
4089
+ return;
4090
+ }
4091
+ for (let i = names.length - 1; i >= 0; i--) {
4092
+ try {
4093
+ router.removeRoute(names[i]);
4094
+ }
4095
+ catch (e) {
4096
+ console.warn('[wep] removeRoute failed', names[i], e);
4097
+ }
4098
+ }
4099
+ pluginIdToRouteNames.delete(pluginId);
4100
+ }
4101
+ /** 测试或宿主高级场景:查询已登记 name(勿依赖顺序语义) */
4102
+ function getRegisteredTopRouteNamesForPlugin(pluginId) {
4103
+ return [...(pluginIdToRouteNames.get(pluginId) || [])];
4104
+ }
4105
+
4085
4106
  /**
4086
4107
  * 构造插件 `activator(hostApi)` 使用的宿主 API:路由、菜单、扩展点、资源与受控请求桥。
4087
4108
  */
@@ -4137,6 +4158,7 @@ function createHostApi(pluginId, router, hostKitOptions = {}) {
4137
4158
  if (typeof router.addRoute !== 'function') {
4138
4159
  throw new Error('[wep] vue-router 3.5+ 必需:请使用 router.addRoute(不再支持 addRoutes)');
4139
4160
  }
4161
+ recordPluginTopRouteNames(pluginId, wrapped.map((r) => String(r.name)));
4140
4162
  if (parentName) {
4141
4163
  for (const r of wrapped) {
4142
4164
  router.addRoute(parentName, r);
@@ -4216,16 +4238,6 @@ function createHostApi(pluginId, router, hostKitOptions = {}) {
4216
4238
  applyInternalRegister(configs);
4217
4239
  }
4218
4240
  },
4219
- registerMenuItems(items) {
4220
- const apply = hostKitOptions.applyPluginMenuItems;
4221
- if (typeof apply !== 'function') {
4222
- throw new Error('[wep] registerMenuItems 需要宿主在 resolveRuntimeOptions 中提供 applyPluginMenuItems,将菜单数据并入宿主侧栏/目录 state(框架不再维护 registries.menus)');
4223
- }
4224
- const list = Array.isArray(items) ? items : [];
4225
- const enriched = list.map((item) => ({ ...item, pluginId }));
4226
- enriched.sort((a, b) => (a.order != null ? Number(a.order) : 0) - (b.order != null ? Number(b.order) : 0));
4227
- apply({ pluginId, items: enriched });
4228
- },
4229
4241
  registerSlotComponents(pointId, components) {
4230
4242
  if (!pointId) {
4231
4243
  return;
@@ -4269,15 +4281,15 @@ function createHostApi(pluginId, router, hostKitOptions = {}) {
4269
4281
  }
4270
4282
 
4271
4283
  /**
4272
- * 卸载单个插件:执行 teardown、清理注册表与 activator、移除带 `data-plugin-asset` 的 DOM。
4273
- * 注意:Vue Router 3 无公开 `removeRoute`,动态路由通常需整页刷新或宿主自行维护。
4284
+ * 卸载单个插件:执行 teardown、按登记 name 批量 removeRoute、清理注册表与 activator、移除带 `data-plugin-asset` 的 DOM。
4285
+ * 依赖 vue-router 3.5 removeRoute;引导时由 bootstrapPlugins 注入 router。
4274
4286
  */
4275
4287
  function disposeWebPlugin(pluginId) {
4276
4288
  if (!pluginId || typeof pluginId !== 'string') {
4277
4289
  return;
4278
4290
  }
4279
4291
  runPluginTeardowns(pluginId);
4280
- revokePluginMenusIfConfigured(pluginId);
4292
+ removeRegisteredRoutesForPlugin(getPluginBootstrapRouter(), pluginId);
4281
4293
  const slots = registries.slots;
4282
4294
  for (const pointId of Object.keys(slots)) {
4283
4295
  const list = slots[pointId];
@@ -4305,6 +4317,75 @@ function disposeWebPlugin(pluginId) {
4305
4317
  }
4306
4318
  }
4307
4319
 
4320
+ /**
4321
+ * 从路由配置(含 meta)推导侧栏/目录用菜单描述,与 registerRoutes 入参同源。
4322
+ *
4323
+ * 约定:
4324
+ * - `meta.menu === false`:该节点及其子树不参与菜单
4325
+ * - 节点出现条件:`meta.title` 或 `meta.menuTitle` 非空字符串,或存在非空的子菜单列表
4326
+ * - `meta.order` 数字越小越靠前;缺省为 0
4327
+ * - 可选:`meta.icon`、`meta.permission`、`meta.hidden`、`meta.external`
4328
+ */
4329
+ function pickTitle(meta, route) {
4330
+ if (typeof meta.menuTitle === 'string' && meta.menuTitle) {
4331
+ return meta.menuTitle;
4332
+ }
4333
+ if (typeof meta.title === 'string' && meta.title) {
4334
+ return meta.title;
4335
+ }
4336
+ if (route.name != null && String(route.name)) {
4337
+ return String(route.name);
4338
+ }
4339
+ return String(route.path || '');
4340
+ }
4341
+ function sortDescriptors(items) {
4342
+ items.sort((a, b) => a.order - b.order || a.path.localeCompare(b.path));
4343
+ for (const x of items) {
4344
+ if (x.children && x.children.length) {
4345
+ sortDescriptors(x.children);
4346
+ }
4347
+ }
4348
+ }
4349
+ /**
4350
+ * 从插件侧 RouteConfig 树构建菜单描述(不含 component,便于并入宿主菜单 state)。
4351
+ */
4352
+ function buildMenuDescriptorsFromRoutes(routes, pluginId) {
4353
+ const out = [];
4354
+ function walk(route) {
4355
+ const meta = (route.meta || {});
4356
+ if (meta.menu === false) {
4357
+ return null;
4358
+ }
4359
+ const chIn = Array.isArray(route.children) ? route.children : [];
4360
+ const childMenus = chIn.map(walk).filter(Boolean);
4361
+ const hasTitle = typeof meta.title === 'string' || typeof meta.menuTitle === 'string';
4362
+ if (!hasTitle && childMenus.length === 0) {
4363
+ return null;
4364
+ }
4365
+ const item = {
4366
+ path: String(route.path || ''),
4367
+ name: route.name,
4368
+ title: pickTitle(meta, route),
4369
+ order: meta.order != null ? Number(meta.order) : 0,
4370
+ ...(pluginId ? { pluginId } : {}),
4371
+ ...(meta.icon !== undefined ? { icon: meta.icon } : {}),
4372
+ ...(meta.permission !== undefined ? { permission: meta.permission } : {}),
4373
+ ...(meta.hidden === true ? { hidden: true } : {}),
4374
+ ...(meta.external === true ? { external: true } : {}),
4375
+ ...(childMenus.length ? { children: childMenus } : {})
4376
+ };
4377
+ return item;
4378
+ }
4379
+ for (const r of routes) {
4380
+ const m = walk(r);
4381
+ if (m) {
4382
+ out.push(m);
4383
+ }
4384
+ }
4385
+ sortDescriptors(out);
4386
+ return out;
4387
+ }
4388
+
4308
4389
  /**
4309
4390
  * 布局中的扩展点占位;插件通过 `hostApi.registerSlotComponents(pointId, ...)` 注入组件。
4310
4391
  * 样式由宿主全局 CSS 覆盖 `.extension-point` / `.plugin-point-error`。
@@ -4475,7 +4556,7 @@ function createVueCliAxiosInstallOptions(deps, extra = {}) {
4475
4556
  function createVueCliAxiosQuickInstallOptions(
4476
4557
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
4477
4558
  router, deps, extra = {}) {
4478
- const { request, hostLayoutComponent, store, hostContext: depHostContext, applyPluginMenuItems, revokePluginMenuItems } = deps;
4559
+ const { request, hostLayoutComponent, store, hostContext: depHostContext } = deps;
4479
4560
  const { hostContext: extraHostContext, isDev: extraIsDev, manifestListPath: extraListPath, ...restExtra } = extra;
4480
4561
  const hostContext = {
4481
4562
  router,
@@ -4489,8 +4570,6 @@ router, deps, extra = {}) {
4489
4570
  return createVueCliAxiosInstallOptions({ request }, {
4490
4571
  hostLayoutComponent,
4491
4572
  hostContext,
4492
- applyPluginMenuItems,
4493
- revokePluginMenuItems,
4494
4573
  isDev: extraIsDev !== undefined ? Boolean(extraIsDev) : resolveBundledIsDev(),
4495
4574
  manifestListPath,
4496
4575
  ...restExtra
@@ -4511,7 +4590,8 @@ const presetVueCliAxios = Object.freeze({
4511
4590
  */
4512
4591
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
4513
4592
  function installVueCliAxiosWebPlugins(Vue, router, deps, extra) {
4514
- const exposeGlobalVue = extra?.exposeGlobalVue !== false;
4593
+ /** 避免输出 `?.`:Vue CLI 4 / Webpack 4 默认不转译 node_modules */
4594
+ const exposeGlobalVue = extra == null || extra.exposeGlobalVue !== false;
4515
4595
  if (exposeGlobalVue && typeof window !== 'undefined') {
4516
4596
  window.Vue = Vue;
4517
4597
  }
@@ -4540,7 +4620,9 @@ const WebExtendPluginVue2 = Object.freeze({
4540
4620
  createHostApi,
4541
4621
  disposeWebPlugin,
4542
4622
  createRequestBridge,
4543
- registries
4623
+ registries,
4624
+ buildMenuDescriptorsFromRoutes,
4625
+ getRegisteredTopRouteNamesForPlugin
4544
4626
  }),
4545
4627
  config: Object.freeze({
4546
4628
  defaultWebExtendPluginRuntime,
@@ -4568,6 +4650,7 @@ exports.HOST_PLUGIN_API_VERSION = HOST_PLUGIN_API_VERSION;
4568
4650
  exports.RUNTIME_CONSOLE_LABEL = RUNTIME_CONSOLE_LABEL;
4569
4651
  exports.WebExtendPluginVue2 = WebExtendPluginVue2;
4570
4652
  exports.bootstrapPlugins = bootstrapPlugins;
4653
+ exports.buildMenuDescriptorsFromRoutes = buildMenuDescriptorsFromRoutes;
4571
4654
  exports.composeManifestFetch = composeManifestFetch;
4572
4655
  exports.createHostApi = createHostApi;
4573
4656
  exports.createRequestBridge = createRequestBridge;
@@ -4580,6 +4663,7 @@ exports.defaultVueCliJavaManifestListPath = defaultVueCliJavaManifestListPath;
4580
4663
  exports.defaultWebExtendPluginRuntime = defaultWebExtendPluginRuntime;
4581
4664
  exports.disposeWebPlugin = disposeWebPlugin;
4582
4665
  exports.ensurePluginHostRoute = ensurePluginHostRoute;
4666
+ exports.getRegisteredTopRouteNamesForPlugin = getRegisteredTopRouteNamesForPlugin;
4583
4667
  exports.installVueCliAxiosWebPlugins = installVueCliAxiosWebPlugins;
4584
4668
  exports.installWebExtendPluginVue2 = installWebExtendPluginVue2;
4585
4669
  exports.manifestFetchCacheMiddleware = manifestFetchCacheMiddleware;