web-extend-plugin-vue2 0.3.0 → 0.3.2

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/dist/index.cjs CHANGED
@@ -7,23 +7,15 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
7
  var Vue__default = /*#__PURE__*/_interopDefault(Vue);
8
8
 
9
9
  /**
10
- * 对外配置单一入口:`resolveRuntimeOptions` / 文档 / 其它模块的默认值与环境键名均由此引用。
11
- * 宿主通过 `resolveRuntimeOptions(partial)` 覆盖的对象形状见 `defaultWebExtendPluginRuntime`。
10
+ * Public constants and runtime defaults consumed by resolveRuntimeOptions and
11
+ * other runtime modules.
12
12
  */
13
- /** 与 `package.json` 的 peer 下限一致;`npm run test:peer-min` / CI matrix 须与此保持同步 */
14
13
  const peerMinimumVersions = {
15
- vue: '2.6.14',
16
- vueRouter: '3.5.4'
14
+ vue: '2.6.0',
15
+ vueRouter: '3.5.0'
17
16
  };
18
- // ---------------------------------------------------------------------------
19
- // 协议与品牌(发布契约;与清单 `hostPluginApiVersion` 对齐 semver 主版本,一般不随宿主覆盖)
20
- // ---------------------------------------------------------------------------
21
17
  const HOST_PLUGIN_API_VERSION = '1.0.0';
22
- /** 控制台日志与首次引导横幅使用的短名称 */
23
18
  const RUNTIME_CONSOLE_LABEL = 'web-extend-plugin-vue2';
24
- // ---------------------------------------------------------------------------
25
- // 与 `build-env` / `resolveBundledEnv` 配套的键名(单一事实来源,便于检索与文档)
26
- // ---------------------------------------------------------------------------
27
19
  const webExtendPluginEnvKeys = {
28
20
  manifestBase: 'VITE_FRONTEND_PLUGIN_BASE',
29
21
  manifestListPath: 'VITE_WEB_PLUGIN_MANIFEST_PATH',
@@ -44,27 +36,14 @@ const webExtendPluginEnvKeys = {
44
36
  webPluginDevIds: 'VITE_WEB_PLUGIN_DEV_IDS',
45
37
  webPluginDevMap: 'VITE_WEB_PLUGIN_DEV_MAP',
46
38
  pluginsBootstrapSummary: 'VITE_PLUGINS_BOOTSTRAP_SUMMARY',
47
- /** 清单路径备选:部分宿主单独使用 */
48
39
  manifestPathAlt: 'VUE_APP_WEB_PLUGIN_MANIFEST_PATH'
49
40
  };
50
- // ---------------------------------------------------------------------------
51
- // `manifestMode` 未配置且环境未指定时的默认值
52
- // ---------------------------------------------------------------------------
53
41
  const defaultManifestMode = 'api';
54
- // ---------------------------------------------------------------------------
55
- // `manifest-fetch-composer` 中间件默认(中间件 options 可逐项覆盖)
56
- // ---------------------------------------------------------------------------
57
42
  const defaultManifestFetchCache = {
58
43
  storageKeyPrefix: 'wep.manifestFetch.v1',
59
44
  maxEntries: 50
60
45
  };
61
- // ---------------------------------------------------------------------------
62
- // 路由合成名前缀(`createHostApi` 为无 name 的路由生成 `__wep_${pluginId}_${seq}`)
63
- // ---------------------------------------------------------------------------
64
46
  const routeSynthNamePrefix = '__wep_';
65
- // ---------------------------------------------------------------------------
66
- // 宿主可通过 `resolveRuntimeOptions` 覆盖的运行时默认值(与 README / index.d.ts 描述一致)
67
- // ---------------------------------------------------------------------------
68
47
  const defaultWebExtendPluginRuntime = {
69
48
  manifestBase: '/fp-api',
70
49
  manifestListPath: '/api/frontend-plugins',
@@ -76,11 +55,7 @@ const defaultWebExtendPluginRuntime = {
76
55
  defaultImplicitDevPluginIds: [],
77
56
  allowedScriptHosts: ['localhost', '127.0.0.1', '::1'],
78
57
  bridgeAllowedPathPrefixes: ['/api/'],
79
- /** 与 `hostLayoutComponent` 同时使用时默认父路由 name */
80
- pluginRoutesParentName: '__wepPluginHost',
81
- /** 插件壳路径(与菜单、ensurePluginHostRoute 一致) */
82
58
  pluginMountPath: '/plugin',
83
- /** `manifestMode=api` 且开发环境下,API 失败或 plugins 为空时尝试的静态 JSON(可放于 `public/web-plugins/`) */
84
59
  devFallbackStaticManifestUrl: '/web-plugins/plugins.manifest.json'
85
60
  };
86
61
 
@@ -417,6 +392,9 @@ function resolveRuntimeOptions$1(user = {}) {
417
392
  ...(typeof user.adaptRouteDeclarations === 'function'
418
393
  ? { adaptRouteDeclarations: user.adaptRouteDeclarations }
419
394
  : {}),
395
+ ...(typeof user.onPluginRoutesContributed === 'function'
396
+ ? { onPluginRoutesContributed: user.onPluginRoutesContributed }
397
+ : {}),
420
398
  ...(user.hostContext !== undefined &&
421
399
  user.hostContext !== null &&
422
400
  typeof user.hostContext === 'object' &&
@@ -3208,7 +3186,7 @@ var semverExports = requireSemver();
3208
3186
  var semver = /*@__PURE__*/getDefaultExportFromCjs(semverExports);
3209
3187
 
3210
3188
  /**
3211
- * 引导时注入的 router 引用,供 disposeWebPlugin 按插件批量 removeRoute。
3189
+ * 引导时注入的 router 引用,供 disposeWebPlugin 卸载插件动态路由。
3212
3190
  */
3213
3191
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3214
3192
  let bootstrapRouter;
@@ -3222,6 +3200,223 @@ function getPluginBootstrapRouter() {
3222
3200
  return bootstrapRouter;
3223
3201
  }
3224
3202
 
3203
+ /**
3204
+ * 记录各插件通过 registerRoutes 挂到 router 上的顶层路由,
3205
+ * 优先使用官方 `addRoute()` 返回的 disposer 做卸载;仅在 disposer 不可用时回退到 name。
3206
+ */
3207
+ const pluginIdToTopRoutes = new Map();
3208
+ function recordPluginTopRoutes(pluginId, routes) {
3209
+ if (!pluginId || routes.length === 0) {
3210
+ return;
3211
+ }
3212
+ const normalized = routes.filter((route) => route && route.name);
3213
+ if (normalized.length === 0) {
3214
+ return;
3215
+ }
3216
+ const cur = pluginIdToTopRoutes.get(pluginId) || [];
3217
+ pluginIdToTopRoutes.set(pluginId, cur.concat(normalized));
3218
+ }
3219
+ function tryRemoveRouteByName(
3220
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3221
+ router, pluginId, name) {
3222
+ if (!router || typeof router.removeRoute !== 'function') {
3223
+ console.warn('[wep] route disposer unavailable,且 router.removeRoute 不可用,动态路由可能残留', pluginId, name);
3224
+ return;
3225
+ }
3226
+ try {
3227
+ router.removeRoute(name);
3228
+ }
3229
+ catch (e) {
3230
+ console.warn('[wep] removeRoute fallback failed', name, e);
3231
+ }
3232
+ }
3233
+ /**
3234
+ * 从 matcher 移除该插件登记过的顶层路由(含其子树)。
3235
+ * 优先走 `router.addRoute()` 返回的 disposer;仅在 disposer 不可用时回退 `router.removeRoute(name)`。
3236
+ */
3237
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3238
+ function removeRegisteredRoutesForPlugin(router, pluginId) {
3239
+ const routes = pluginIdToTopRoutes.get(pluginId);
3240
+ if (!routes || routes.length === 0) {
3241
+ return;
3242
+ }
3243
+ for (let i = routes.length - 1; i >= 0; i--) {
3244
+ const route = routes[i];
3245
+ if (typeof route.dispose === 'function') {
3246
+ try {
3247
+ route.dispose();
3248
+ continue;
3249
+ }
3250
+ catch (e) {
3251
+ console.warn('[wep] route disposer failed', route.name, e);
3252
+ }
3253
+ }
3254
+ tryRemoveRouteByName(router, pluginId, route.name);
3255
+ }
3256
+ pluginIdToTopRoutes.delete(pluginId);
3257
+ }
3258
+ /** 调试或宿主高级场景:查询已登记 name(勿依赖顺序语义) */
3259
+ function getRegisteredTopRouteNamesForPlugin(pluginId) {
3260
+ return (pluginIdToTopRoutes.get(pluginId) || []).map((route) => route.name);
3261
+ }
3262
+
3263
+ /**
3264
+ * Host-side reactive registries for extension-point slot components.
3265
+ * Menus/sidebar mapping stays on the host and is not duplicated here.
3266
+ */
3267
+ const registries = {
3268
+ slots: {},
3269
+ slotRevision: 0
3270
+ };
3271
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3272
+ function ensureRegistriesReactive(VueLike) {
3273
+ if (!VueLike || typeof VueLike.observable !== 'function') {
3274
+ return registries;
3275
+ }
3276
+ if (registries.__wepObservedBy__ === VueLike) {
3277
+ return registries;
3278
+ }
3279
+ VueLike.observable(registries);
3280
+ Object.defineProperty(registries, '__wepObservedBy__', {
3281
+ value: VueLike,
3282
+ configurable: true,
3283
+ enumerable: false,
3284
+ writable: true
3285
+ });
3286
+ return registries;
3287
+ }
3288
+
3289
+ /**
3290
+ * 按插件 id 收集 `onTeardown` 回调,供 `disposeWebPlugin` 统一执行。
3291
+ */
3292
+ const _byPlugin = new Map();
3293
+ function registerPluginTeardown(pluginId, fn) {
3294
+ if (typeof fn !== 'function') {
3295
+ return;
3296
+ }
3297
+ let list = _byPlugin.get(pluginId);
3298
+ if (!list) {
3299
+ list = [];
3300
+ _byPlugin.set(pluginId, list);
3301
+ }
3302
+ list.push(fn);
3303
+ }
3304
+ function runPluginTeardowns(pluginId) {
3305
+ const list = _byPlugin.get(pluginId);
3306
+ if (!list) {
3307
+ return;
3308
+ }
3309
+ _byPlugin.delete(pluginId);
3310
+ for (const fn of list) {
3311
+ try {
3312
+ fn();
3313
+ }
3314
+ catch (e) {
3315
+ console.warn('[wep] teardown failed', pluginId, e);
3316
+ }
3317
+ }
3318
+ }
3319
+
3320
+ /**
3321
+ * 动态加载脚本(去重与并发合并)。
3322
+ */
3323
+ const loadScriptMemo = new Map();
3324
+ function markScriptOwnership(script, pluginId) {
3325
+ if (pluginId) {
3326
+ script.setAttribute('data-plugin-asset', pluginId);
3327
+ }
3328
+ }
3329
+ function clearLoadedScriptMemo(src) {
3330
+ if (typeof src === 'string' && src) {
3331
+ loadScriptMemo.delete(src);
3332
+ return;
3333
+ }
3334
+ loadScriptMemo.clear();
3335
+ }
3336
+ function loadScript(src, pluginId) {
3337
+ if (typeof document === 'undefined') {
3338
+ return Promise.reject(new Error('loadScript: no document'));
3339
+ }
3340
+ if (loadScriptMemo.has(src)) {
3341
+ return loadScriptMemo.get(src);
3342
+ }
3343
+ const p = new Promise((resolve, reject) => {
3344
+ const scripts = document.getElementsByTagName('script');
3345
+ for (let i = 0; i < scripts.length; i++) {
3346
+ const el = scripts[i];
3347
+ if (el.src === src) {
3348
+ markScriptOwnership(el, pluginId);
3349
+ if (el.getAttribute('data-wep-loaded') === 'true') {
3350
+ resolve();
3351
+ return;
3352
+ }
3353
+ el.addEventListener('load', () => {
3354
+ el.setAttribute('data-wep-loaded', 'true');
3355
+ resolve();
3356
+ }, { once: true });
3357
+ el.addEventListener('error', () => reject(new Error('loadScript failed: ' + src)), { once: true });
3358
+ return;
3359
+ }
3360
+ }
3361
+ const s = document.createElement('script');
3362
+ s.async = true;
3363
+ s.src = src;
3364
+ markScriptOwnership(s, pluginId);
3365
+ s.onload = () => {
3366
+ s.setAttribute('data-wep-loaded', 'true');
3367
+ resolve();
3368
+ };
3369
+ s.onerror = () => reject(new Error('loadScript failed: ' + src));
3370
+ document.head.appendChild(s);
3371
+ });
3372
+ loadScriptMemo.set(src, p);
3373
+ p.catch(() => loadScriptMemo.delete(src));
3374
+ return p;
3375
+ }
3376
+
3377
+ /**
3378
+ * 卸载单个插件:执行 teardown、移除该插件登记的动态路由、清理注册表与 activator、移除带 `data-plugin-asset` 的 DOM。
3379
+ * 优先使用 `addRoute()` 返回的 disposer;必要时回退 `router.removeRoute(name)`。
3380
+ */
3381
+ function disposeWebPlugin(pluginId) {
3382
+ if (!pluginId || typeof pluginId !== 'string') {
3383
+ return;
3384
+ }
3385
+ runPluginTeardowns(pluginId);
3386
+ removeRegisteredRoutesForPlugin(getPluginBootstrapRouter(), pluginId);
3387
+ const slots = registries.slots;
3388
+ for (const pointId of Object.keys(slots)) {
3389
+ const list = slots[pointId];
3390
+ if (!Array.isArray(list)) {
3391
+ continue;
3392
+ }
3393
+ const next = list.filter((x) => x.pluginId !== pluginId);
3394
+ if (next.length === 0) {
3395
+ Vue__default.default.delete(slots, pointId);
3396
+ }
3397
+ else if (next.length !== list.length) {
3398
+ Vue__default.default.set(slots, pointId, next);
3399
+ }
3400
+ }
3401
+ registries.slotRevision++;
3402
+ if (typeof window !== 'undefined' && window.__PLUGIN_ACTIVATORS__) {
3403
+ delete window.__PLUGIN_ACTIVATORS__[pluginId];
3404
+ }
3405
+ if (typeof document !== 'undefined') {
3406
+ document.querySelectorAll('[data-plugin-asset]').forEach((el) => {
3407
+ if (el.getAttribute('data-plugin-asset') === pluginId) {
3408
+ if (el.tagName === 'SCRIPT') {
3409
+ const src = el.src;
3410
+ if (src) {
3411
+ clearLoadedScriptMemo(src);
3412
+ }
3413
+ }
3414
+ el.remove();
3415
+ }
3416
+ });
3417
+ }
3418
+ }
3419
+
3225
3420
  /**
3226
3421
  * 开发模式插件 URL 映射(显式 JSON + 隐式 dev 探测)。
3227
3422
  */
@@ -3384,49 +3579,6 @@ function startPluginDevSseForMap(devMap, isDev, hostSet, ssePath) {
3384
3579
  }
3385
3580
  }
3386
3581
 
3387
- /**
3388
- * 动态加载脚本(去重与并发合并)。
3389
- */
3390
- const loadScriptMemo = new Map();
3391
- function loadScript(src) {
3392
- if (typeof document === 'undefined') {
3393
- return Promise.reject(new Error('loadScript: no document'));
3394
- }
3395
- if (loadScriptMemo.has(src)) {
3396
- return loadScriptMemo.get(src);
3397
- }
3398
- const p = new Promise((resolve, reject) => {
3399
- const scripts = document.getElementsByTagName('script');
3400
- for (let i = 0; i < scripts.length; i++) {
3401
- const el = scripts[i];
3402
- if (el.src === src) {
3403
- if (el.getAttribute('data-wep-loaded') === 'true') {
3404
- resolve();
3405
- return;
3406
- }
3407
- el.addEventListener('load', () => {
3408
- el.setAttribute('data-wep-loaded', 'true');
3409
- resolve();
3410
- }, { once: true });
3411
- el.addEventListener('error', () => reject(new Error('loadScript failed: ' + src)), { once: true });
3412
- return;
3413
- }
3414
- }
3415
- const s = document.createElement('script');
3416
- s.async = true;
3417
- s.src = src;
3418
- s.onload = () => {
3419
- s.setAttribute('data-wep-loaded', 'true');
3420
- resolve();
3421
- };
3422
- s.onerror = () => reject(new Error('loadScript failed: ' + src));
3423
- document.head.appendChild(s);
3424
- });
3425
- loadScriptMemo.set(src, p);
3426
- p.catch(() => loadScriptMemo.delete(src));
3427
- return p;
3428
- }
3429
-
3430
3582
  let _printed = false;
3431
3583
  /** 在首次引导插件时打印一行运行时标识(非大块 ASCII art)。 */
3432
3584
  function printRuntimeBannerOnce() {
@@ -3575,6 +3727,29 @@ function resolveStaticManifestUrlForFetch(url, origin) {
3575
3727
  }
3576
3728
  }
3577
3729
 
3730
+ /**
3731
+ * 记录当前已成功激活的插件 id,供宿主做只读观测。
3732
+ */
3733
+ const activatedPluginIds = new Set();
3734
+ function clearActivatedPluginIds() {
3735
+ activatedPluginIds.clear();
3736
+ }
3737
+ function markPluginActivated(pluginId) {
3738
+ if (!pluginId) {
3739
+ return;
3740
+ }
3741
+ activatedPluginIds.add(pluginId);
3742
+ }
3743
+ function markPluginDeactivated(pluginId) {
3744
+ if (!pluginId) {
3745
+ return;
3746
+ }
3747
+ activatedPluginIds.delete(pluginId);
3748
+ }
3749
+ function getActivatedPluginIds$1() {
3750
+ return [...activatedPluginIds];
3751
+ }
3752
+
3578
3753
  /**
3579
3754
  * 拉取插件清单、加载入口脚本并调用各插件 `activator`。
3580
3755
  */
@@ -3610,7 +3785,7 @@ async function loadPluginEntry(p, entryUrl, devMap, hostSet) {
3610
3785
  catch (e) {
3611
3786
  console.warn('[wep] dev import failed, trying manifest entryUrl', p.id, e);
3612
3787
  if (entryUrl && isScriptHostAllowed(entryUrl, hostSet)) {
3613
- await loadScript(entryUrl);
3788
+ await loadScript(entryUrl, p.id);
3614
3789
  }
3615
3790
  return;
3616
3791
  }
@@ -3620,7 +3795,40 @@ async function loadPluginEntry(p, entryUrl, devMap, hostSet) {
3620
3795
  console.warn('[wep] skip (entryUrl not allowed)', p.id, entryUrl);
3621
3796
  return;
3622
3797
  }
3623
- await loadScript(entryUrl);
3798
+ await loadScript(entryUrl, p.id);
3799
+ }
3800
+ function coercePriority(value) {
3801
+ if (value === null || value === undefined) {
3802
+ return null;
3803
+ }
3804
+ const n = Number(value);
3805
+ return Number.isFinite(n) ? n : null;
3806
+ }
3807
+ function sortByPriority(plugins) {
3808
+ return plugins
3809
+ .map((entry, index) => ({
3810
+ entry,
3811
+ priority: coercePriority(entry.priority),
3812
+ index
3813
+ }))
3814
+ .sort((a, b) => {
3815
+ const aHas = a.priority !== null;
3816
+ const bHas = b.priority !== null;
3817
+ if (!aHas && !bHas) {
3818
+ return a.index - b.index;
3819
+ }
3820
+ if (!aHas) {
3821
+ return 1;
3822
+ }
3823
+ if (!bHas) {
3824
+ return -1;
3825
+ }
3826
+ if (a.priority !== b.priority) {
3827
+ return a.priority - b.priority;
3828
+ }
3829
+ return a.index - b.index;
3830
+ })
3831
+ .map((decorated) => decorated.entry);
3624
3832
  }
3625
3833
  async function bootstrapPlugins$1(
3626
3834
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -3632,6 +3840,7 @@ createHostApiFactory, runtimeOptions) {
3632
3840
  return;
3633
3841
  }
3634
3842
  printRuntimeBannerOnce();
3843
+ clearActivatedPluginIds();
3635
3844
  const opts = resolveRuntimeOptions$1(runtimeOptions || {});
3636
3845
  setPluginBootstrapRouter(router);
3637
3846
  ensurePluginHostRoute$1(router, opts);
@@ -3710,6 +3919,9 @@ createHostApiFactory, runtimeOptions) {
3710
3919
  : {}),
3711
3920
  ...(typeof opts.adaptRouteDeclarations === 'function'
3712
3921
  ? { adaptRouteDeclarations: opts.adaptRouteDeclarations }
3922
+ : {}),
3923
+ ...(typeof opts.onPluginRoutesContributed === 'function'
3924
+ ? { onPluginRoutesContributed: opts.onPluginRoutesContributed }
3713
3925
  : {})
3714
3926
  };
3715
3927
  if (!manifestResult.ok) {
@@ -3738,22 +3950,30 @@ createHostApiFactory, runtimeOptions) {
3738
3950
  const maj = coerced ? coerced.major : 0;
3739
3951
  const range = `^${maj}.0.0`;
3740
3952
  if (!semver.satisfies(HOST_PLUGIN_API_VERSION, range, { includePrerelease: true })) {
3741
- console.warn('[wep] host API version mismatch', {
3953
+ console.warn('[wep] manifest host API version mismatch; skip bootstrap', {
3742
3954
  host: HOST_PLUGIN_API_VERSION,
3743
3955
  manifest: apiVer
3744
3956
  });
3957
+ if (shouldShowBootstrapSummary(opts)) {
3958
+ console.info('[wep] bootstrap_summary', {
3959
+ ok: false,
3960
+ reason: 'manifest_host_api_version_mismatch'
3961
+ });
3962
+ }
3963
+ return;
3745
3964
  }
3746
3965
  }
3747
3966
  window.__PLUGIN_ACTIVATORS__ = window.__PLUGIN_ACTIVATORS__ || {};
3748
- const plugins = data.plugins || [];
3967
+ const originalPlugins = data.plugins || [];
3968
+ const plugins = sortByPriority(originalPlugins);
3749
3969
  if (plugins.length === 0) {
3750
3970
  const hint = isStatic ? 'check static JSON file and plugins[]' : 'check backend and URL';
3751
3971
  console.info('[wep] empty plugin manifest — ' + hint, manifestUrlUsed);
3752
3972
  }
3753
3973
  const summary = {
3754
- manifestCount: plugins.length,
3974
+ manifestCount: originalPlugins.length,
3755
3975
  activated: 0,
3756
- skipEngines: 0,
3976
+ skipApiVersion: 0,
3757
3977
  skipLoad: 0,
3758
3978
  skipNoActivator: 0,
3759
3979
  activateFail: 0
@@ -3761,8 +3981,10 @@ createHostApiFactory, runtimeOptions) {
3761
3981
  for (const p of plugins) {
3762
3982
  const range = p.engines && p.engines.host;
3763
3983
  if (range && !semver.satisfies(HOST_PLUGIN_API_VERSION, range, { includePrerelease: true })) {
3764
- console.warn('[wep] skip plugin (engines.host)', p.id, range);
3765
- summary.skipEngines++;
3984
+ console.warn('[wep] skip plugin (engines.host)', p.id, range, {
3985
+ host: HOST_PLUGIN_API_VERSION
3986
+ });
3987
+ summary.skipApiVersion++;
3766
3988
  continue;
3767
3989
  }
3768
3990
  const entryUrl = p.entryUrl;
@@ -3798,6 +4020,11 @@ createHostApiFactory, runtimeOptions) {
3798
4020
  const hostApi = createHostApiFactory(p.id, router, hostKit);
3799
4021
  try {
3800
4022
  await Promise.resolve(activator(hostApi, { pluginRecord }));
4023
+ markPluginActivated(p.id);
4024
+ const teardownCapableHostApi = hostApi;
4025
+ if (typeof teardownCapableHostApi.onTeardown === 'function') {
4026
+ teardownCapableHostApi.onTeardown(p.id, () => markPluginDeactivated(p.id));
4027
+ }
3801
4028
  summary.activated++;
3802
4029
  if (typeof opts.onAfterPluginActivate === 'function') {
3803
4030
  await Promise.resolve(opts.onAfterPluginActivate({
@@ -3810,6 +4037,12 @@ createHostApiFactory, runtimeOptions) {
3810
4037
  }
3811
4038
  catch (e) {
3812
4039
  console.error('[wep] activate failed', p.id, e);
4040
+ try {
4041
+ disposeWebPlugin(p.id);
4042
+ }
4043
+ catch (disposeErr) {
4044
+ console.warn('[wep] rollback failed after activation error', p.id, disposeErr);
4045
+ }
3813
4046
  summary.activateFail++;
3814
4047
  if (typeof opts.onPluginActivateError === 'function') {
3815
4048
  try {
@@ -3840,6 +4073,7 @@ var pluginRuntime = /*#__PURE__*/Object.freeze({
3840
4073
  bootstrapPlugins: bootstrapPlugins$1,
3841
4074
  defaultFetchWebPluginManifest: defaultFetchWebPluginManifest$1,
3842
4075
  ensurePluginHostRoute: ensurePluginHostRoute$1,
4076
+ getActivatedPluginIds: getActivatedPluginIds$1,
3843
4077
  resolveRuntimeOptions: resolveRuntimeOptions$1
3844
4078
  });
3845
4079
 
@@ -3998,48 +4232,40 @@ var manifestComposer = /*#__PURE__*/Object.freeze({
3998
4232
  });
3999
4233
 
4000
4234
  /**
4001
- * 宿主侧响应式注册表:扩展点槽位(供布局与 `ExtensionPoint` 消费)。
4002
- * 菜单由宿主从路由 `meta` 推导(见 `buildMenuDescriptorsFromRoutes`),框架不维护平行菜单列表。
4003
- */
4004
- const registries = Vue__default.default.observable({
4005
- slots: {},
4006
- slotRevision: 0
4007
- });
4008
-
4009
- /**
4010
- * 按插件 id 收集 `onTeardown` 回调,供 `disposeWebPlugin` 统一执行。
4235
+ * 插件访问后端的受控通道:`fetch` 仅允许落在配置的路径前缀下(默认 `/api/`),默认 `credentials: 'same-origin'`。
4011
4236
  */
4012
- const _byPlugin = new Map();
4013
- function registerPluginTeardown(pluginId, fn) {
4014
- if (typeof fn !== 'function') {
4015
- return;
4237
+ function normalizeBridgePath(input) {
4238
+ if (typeof window === 'undefined') {
4239
+ throw new Error('[wep:bridge] window is unavailable');
4016
4240
  }
4017
- let list = _byPlugin.get(pluginId);
4018
- if (!list) {
4019
- list = [];
4020
- _byPlugin.set(pluginId, list);
4241
+ if (typeof input !== 'string' || !input.startsWith('/')) {
4242
+ throw new Error('[wep:bridge] path must start with /');
4021
4243
  }
4022
- list.push(fn);
4023
- }
4024
- function runPluginTeardowns(pluginId) {
4025
- const list = _byPlugin.get(pluginId);
4026
- if (!list) {
4027
- return;
4244
+ if (input.includes('\\')) {
4245
+ throw new Error('[wep:bridge] path must not contain backslashes');
4028
4246
  }
4029
- _byPlugin.delete(pluginId);
4030
- for (const fn of list) {
4247
+ const url = new URL(input, window.location.origin);
4248
+ if (url.origin !== window.location.origin) {
4249
+ throw new Error('[wep:bridge] cross-origin path is not allowed');
4250
+ }
4251
+ const normalized = url.pathname;
4252
+ const decodedPath = (() => {
4031
4253
  try {
4032
- fn();
4254
+ return decodeURIComponent(normalized);
4033
4255
  }
4034
- catch (e) {
4035
- console.warn('[wep] teardown failed', pluginId, e);
4256
+ catch {
4257
+ return normalized;
4036
4258
  }
4037
- }
4259
+ })();
4260
+ const hasTraversalSegment = decodedPath
4261
+ .split('/')
4262
+ .filter(Boolean)
4263
+ .some((segment) => segment === '.' || segment === '..');
4264
+ if (hasTraversalSegment) {
4265
+ throw new Error('[wep:bridge] path traversal is not allowed');
4266
+ }
4267
+ return normalized + url.search;
4038
4268
  }
4039
-
4040
- /**
4041
- * 插件访问后端的受控通道:`fetch` 仅允许落在配置的路径前缀下(默认 `/api/`),默认 `credentials: 'same-origin'`。
4042
- */
4043
4269
  function createRequestBridge(config = {}) {
4044
4270
  const raw = Array.isArray(config.allowedPathPrefixes) && config.allowedPathPrefixes.length > 0
4045
4271
  ? config.allowedPathPrefixes
@@ -4047,14 +4273,13 @@ function createRequestBridge(config = {}) {
4047
4273
  const allowedPathPrefixes = raw.map((p) => ensureLeadingPath(p));
4048
4274
  return {
4049
4275
  async request(path, init = {}) {
4050
- if (typeof path !== 'string' || !path.startsWith('/')) {
4051
- throw new Error('[wep:bridge] path must start with /');
4052
- }
4053
- const allowed = allowedPathPrefixes.some((p) => path.startsWith(p));
4276
+ const normalizedPath = normalizeBridgePath(path);
4277
+ const pathnameOnly = normalizedPath.split('?')[0];
4278
+ const allowed = allowedPathPrefixes.some((p) => pathnameOnly === p || pathnameOnly.startsWith(`${p.replace(/\/$/, '')}/`));
4054
4279
  if (!allowed) {
4055
- throw new Error('[wep:bridge] path not allowed: ' + path);
4280
+ throw new Error('[wep:bridge] path not allowed: ' + normalizedPath);
4056
4281
  }
4057
- return fetch(path, {
4282
+ return fetch(normalizedPath, {
4058
4283
  credentials: 'same-origin',
4059
4284
  ...init
4060
4285
  });
@@ -4063,51 +4288,83 @@ function createRequestBridge(config = {}) {
4063
4288
  }
4064
4289
 
4065
4290
  /**
4066
- * 记录各插件通过 registerRoutes 挂到 router 上的顶层 route name,便于 dispose 时 removeRoute。
4291
+ * 按插件记录已贡献路由的可序列化快照,避免宿主依赖 router 内部 matcher 形态。
4067
4292
  */
4068
- const pluginIdToRouteNames = new Map();
4069
- function recordPluginTopRouteNames(pluginId, names) {
4070
- if (!pluginId || names.length === 0) {
4071
- return;
4293
+ const contributedRoutesByPlugin = new Map();
4294
+ function sanitizeValue(value) {
4295
+ if (value == null) {
4296
+ return value;
4072
4297
  }
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;
4298
+ if (typeof value === 'function') {
4299
+ return undefined;
4085
4300
  }
4086
- if (!router || typeof router.removeRoute !== 'function') {
4087
- console.warn('[wep] router.removeRoute 不可用,跳过动态路由卸载', pluginId);
4088
- pluginIdToRouteNames.delete(pluginId);
4089
- return;
4301
+ if (Array.isArray(value)) {
4302
+ return value
4303
+ .map((item) => sanitizeValue(item))
4304
+ .filter((item) => item !== undefined);
4090
4305
  }
4091
- for (let i = names.length - 1; i >= 0; i--) {
4092
- try {
4093
- router.removeRoute(names[i]);
4306
+ if (typeof value !== 'object') {
4307
+ return value;
4308
+ }
4309
+ const out = {};
4310
+ for (const [key, raw] of Object.entries(value)) {
4311
+ if (key === 'component' || key === 'components') {
4312
+ continue;
4094
4313
  }
4095
- catch (e) {
4096
- console.warn('[wep] removeRoute failed', names[i], e);
4314
+ const sanitized = sanitizeValue(raw);
4315
+ if (sanitized !== undefined) {
4316
+ out[key] = sanitized;
4097
4317
  }
4098
4318
  }
4099
- pluginIdToRouteNames.delete(pluginId);
4319
+ return out;
4100
4320
  }
4101
- /** 测试或宿主高级场景:查询已登记 name(勿依赖顺序语义) */
4102
- function getRegisteredTopRouteNamesForPlugin(pluginId) {
4103
- return [...(pluginIdToRouteNames.get(pluginId) || [])];
4321
+ function sanitizeContributedRoutes(routes) {
4322
+ return routes
4323
+ .map((route) => sanitizeValue(route))
4324
+ .filter((route) => !!route && typeof route === 'object');
4325
+ }
4326
+ function recordContributedRoutesForPlugin(pluginId, routes) {
4327
+ const sanitized = sanitizeContributedRoutes(routes);
4328
+ const current = contributedRoutesByPlugin.get(pluginId) || [];
4329
+ const next = current.concat(sanitized);
4330
+ contributedRoutesByPlugin.set(pluginId, next);
4331
+ return next.map((route) => sanitizeValue(route));
4332
+ }
4333
+ function getContributedRoutesForPlugin(pluginId) {
4334
+ const routes = contributedRoutesByPlugin.get(pluginId) || [];
4335
+ return routes.map((route) => sanitizeValue(route));
4336
+ }
4337
+ function clearContributedRoutesForPlugin(pluginId) {
4338
+ contributedRoutesByPlugin.delete(pluginId);
4104
4339
  }
4105
4340
 
4106
4341
  /**
4107
- * 构造插件 `activator(hostApi)` 使用的宿主 API:路由、菜单、扩展点、资源与受控请求桥。
4342
+ * 构造插件 `activator(hostApi)` 使用的宿主 API:路由、扩展点、资源与受控请求桥。
4108
4343
  */
4109
4344
  let slotItemKeySeq = 0;
4110
4345
  let routeSynthSeq = 0;
4346
+ function decorateRouteTreeWithPluginMeta(pluginId, route) {
4347
+ const meta = route.meta && typeof route.meta === 'object' && !Array.isArray(route.meta)
4348
+ ? route.meta
4349
+ : {};
4350
+ const wepMeta = meta.__wep && typeof meta.__wep === 'object' && !Array.isArray(meta.__wep)
4351
+ ? meta.__wep
4352
+ : {};
4353
+ const nextChildren = Array.isArray(route.children)
4354
+ ? route.children.map((child) => decorateRouteTreeWithPluginMeta(pluginId, child))
4355
+ : route.children;
4356
+ return {
4357
+ ...route,
4358
+ meta: {
4359
+ ...meta,
4360
+ __wep: {
4361
+ ...wepMeta,
4362
+ pluginId
4363
+ }
4364
+ },
4365
+ ...(nextChildren ? { children: nextChildren } : {})
4366
+ };
4367
+ }
4111
4368
  function analyzeRouteInputTree(nodes) {
4112
4369
  let hasDecl = false;
4113
4370
  let hasCfg = false;
@@ -4149,26 +4406,71 @@ function createHostApi(pluginId, router, hostKitOptions = {}) {
4149
4406
  const parentName = typeof hostKitOptions.pluginRoutesParentName === 'string'
4150
4407
  ? hostKitOptions.pluginRoutesParentName.trim()
4151
4408
  : '';
4409
+ function rollbackRegisteredTopRoutes(routes) {
4410
+ for (let i = routes.length - 1; i >= 0; i--) {
4411
+ const route = routes[i];
4412
+ if (typeof route.dispose === 'function') {
4413
+ try {
4414
+ route.dispose();
4415
+ continue;
4416
+ }
4417
+ catch (e) {
4418
+ console.warn('[wep] rollback route disposer failed', route.name, e);
4419
+ }
4420
+ }
4421
+ if (typeof router.removeRoute === 'function') {
4422
+ try {
4423
+ router.removeRoute(route.name);
4424
+ }
4425
+ catch (e) {
4426
+ console.warn('[wep] rollback removeRoute failed', route.name, e);
4427
+ }
4428
+ }
4429
+ }
4430
+ }
4152
4431
  function applyInternalRegister(rawRouteConfigs) {
4153
4432
  const wrapped = rawRouteConfigs.map((r) => ({
4154
- ...r,
4155
- name: r.name || `${routeSynthNamePrefix}${pluginId}_${routeSynthSeq++}`,
4156
- meta: { ...(r.meta || {}), pluginId }
4433
+ ...decorateRouteTreeWithPluginMeta(pluginId, r),
4434
+ name: r.name || `${routeSynthNamePrefix}${pluginId}_${routeSynthSeq++}`
4157
4435
  }));
4158
4436
  if (typeof router.addRoute !== 'function') {
4159
4437
  throw new Error('[wep] vue-router 3.5+ 必需:请使用 router.addRoute(不再支持 addRoutes)');
4160
4438
  }
4161
- recordPluginTopRouteNames(pluginId, wrapped.map((r) => String(r.name)));
4162
- if (parentName) {
4163
- for (const r of wrapped) {
4164
- router.addRoute(parentName, r);
4439
+ const registeredTopRoutes = [];
4440
+ try {
4441
+ if (parentName) {
4442
+ for (const r of wrapped) {
4443
+ const dispose = router.addRoute(parentName, r);
4444
+ registeredTopRoutes.push({
4445
+ name: String(r.name),
4446
+ dispose: typeof dispose === 'function' ? dispose : undefined
4447
+ });
4448
+ }
4165
4449
  }
4166
- }
4167
- else {
4168
- for (const r of wrapped) {
4169
- router.addRoute(r);
4450
+ else {
4451
+ for (const r of wrapped) {
4452
+ const dispose = router.addRoute(r);
4453
+ registeredTopRoutes.push({
4454
+ name: String(r.name),
4455
+ dispose: typeof dispose === 'function' ? dispose : undefined
4456
+ });
4457
+ }
4170
4458
  }
4171
4459
  }
4460
+ catch (e) {
4461
+ rollbackRegisteredTopRoutes(registeredTopRoutes);
4462
+ throw e;
4463
+ }
4464
+ recordPluginTopRoutes(pluginId, registeredTopRoutes);
4465
+ const contributedRoutes = recordContributedRoutesForPlugin(pluginId, wrapped);
4466
+ if (contributedRoutes.length > 0 && typeof hostKitOptions.onPluginRoutesContributed === 'function') {
4467
+ hostKitOptions.onPluginRoutesContributed({
4468
+ pluginId,
4469
+ router,
4470
+ routes: wrapped,
4471
+ contributedRoutes
4472
+ });
4473
+ }
4172
4474
  }
4173
4475
  function injectStylesheet(href) {
4174
4476
  const link = document.createElement('link');
@@ -4191,6 +4493,12 @@ function createHostApi(pluginId, router, hostKitOptions = {}) {
4191
4493
  const hostContext = hostKitOptions.hostContext != null && typeof hostKitOptions.hostContext === 'object'
4192
4494
  ? hostKitOptions.hostContext
4193
4495
  : Object.freeze({});
4496
+ const hostVue = hostContext && hostContext.Vue;
4497
+ const VueRuntime = hostVue || Vue__default.default;
4498
+ ensureRegistriesReactive(VueRuntime);
4499
+ registerPluginTeardown(pluginId, () => {
4500
+ clearContributedRoutesForPlugin(pluginId);
4501
+ });
4194
4502
  return {
4195
4503
  hostPluginApiVersion: HOST_PLUGIN_API_VERSION,
4196
4504
  /** 宿主注入的只读依赖(store、router、开放能力等),见 `resolveRuntimeOptions.hostContext` */
@@ -4243,7 +4551,7 @@ function createHostApi(pluginId, router, hostKitOptions = {}) {
4243
4551
  return;
4244
4552
  }
4245
4553
  if (!registries.slots[pointId]) {
4246
- Vue__default.default.set(registries.slots, pointId, []);
4554
+ VueRuntime.set(registries.slots, pointId, []);
4247
4555
  }
4248
4556
  const list = registries.slots[pointId];
4249
4557
  for (const c of components) {
@@ -4271,6 +4579,7 @@ function createHostApi(pluginId, router, hostKitOptions = {}) {
4271
4579
  registerSanitizedHtmlSnippet() {
4272
4580
  throw new Error('registerSanitizedHtmlSnippet is not enabled');
4273
4581
  },
4582
+ getContributedRoutes: () => getContributedRoutesForPlugin(pluginId),
4274
4583
  getBridge: () => bridge,
4275
4584
  onTeardown(_pluginId, fn) {
4276
4585
  if (typeof fn === 'function') {
@@ -4280,189 +4589,69 @@ function createHostApi(pluginId, router, hostKitOptions = {}) {
4280
4589
  };
4281
4590
  }
4282
4591
 
4283
- /**
4284
- * 卸载单个插件:执行 teardown、按登记 name 批量 removeRoute、清理注册表与 activator、移除带 `data-plugin-asset` 的 DOM。
4285
- * 依赖 vue-router ≥3.5 的 removeRoute;引导时由 bootstrapPlugins 注入 router。
4286
- */
4287
- function disposeWebPlugin(pluginId) {
4288
- if (!pluginId || typeof pluginId !== 'string') {
4289
- return;
4290
- }
4291
- runPluginTeardowns(pluginId);
4292
- removeRegisteredRoutesForPlugin(getPluginBootstrapRouter(), pluginId);
4293
- const slots = registries.slots;
4294
- for (const pointId of Object.keys(slots)) {
4295
- const list = slots[pointId];
4296
- if (!Array.isArray(list)) {
4297
- continue;
4298
- }
4299
- const next = list.filter((x) => x.pluginId !== pluginId);
4300
- if (next.length === 0) {
4301
- Vue__default.default.delete(slots, pointId);
4302
- }
4303
- else if (next.length !== list.length) {
4304
- Vue__default.default.set(slots, pointId, next);
4305
- }
4306
- }
4307
- registries.slotRevision++;
4308
- if (typeof window !== 'undefined' && window.__PLUGIN_ACTIVATORS__) {
4309
- delete window.__PLUGIN_ACTIVATORS__[pluginId];
4310
- }
4311
- if (typeof document !== 'undefined') {
4312
- document.querySelectorAll('[data-plugin-asset]').forEach((el) => {
4313
- if (el.getAttribute('data-plugin-asset') === pluginId) {
4314
- el.remove();
4592
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4593
+ function createExtensionPointComponent(VueLike = Vue__default.default) {
4594
+ ensureRegistriesReactive(VueLike);
4595
+ const SlotErrorBoundary = VueLike.extend({
4596
+ name: 'SlotErrorBoundary',
4597
+ props: { label: String },
4598
+ data() {
4599
+ return { error: null };
4600
+ },
4601
+ errorCaptured(err) {
4602
+ this.error = err instanceof Error && err.message ? err.message : String(err);
4603
+ console.error('[wep:ExtensionPoint]', this.label, err);
4604
+ return false;
4605
+ },
4606
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4607
+ render(h) {
4608
+ if (this.error) {
4609
+ return h('div', { class: 'plugin-point-error' }, `[插件 ${this.label}] 渲染失败`);
4315
4610
  }
4316
- });
4317
- }
4318
- }
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);
4611
+ const d = this.$slots.default;
4612
+ return d && d[0] ? d[0] : h('span');
4383
4613
  }
4384
- }
4385
- sortDescriptors(out);
4386
- return out;
4387
- }
4388
-
4389
- /**
4390
- * 布局中的扩展点占位;插件通过 `hostApi.registerSlotComponents(pointId, ...)` 注入组件。
4391
- * 样式由宿主全局 CSS 覆盖 `.extension-point` / `.plugin-point-error`。
4392
- */
4393
- const SlotErrorBoundary = Vue__default.default.extend({
4394
- name: 'SlotErrorBoundary',
4395
- props: { label: String },
4396
- data() {
4397
- return { error: null };
4398
- },
4399
- errorCaptured(err) {
4400
- this.error = err instanceof Error && err.message ? err.message : String(err);
4401
- console.error('[wep:ExtensionPoint]', this.label, err);
4402
- return false;
4403
- },
4404
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
4405
- render(h) {
4406
- if (this.error) {
4407
- return h('div', { class: 'plugin-point-error' }, `[插件 ${this.label}] 渲染失败`);
4408
- }
4409
- const d = this.$slots.default;
4410
- return d && d[0] ? d[0] : h('span');
4411
- }
4412
- });
4413
- var ExtensionPoint = Vue__default.default.extend({
4414
- name: 'ExtensionPoint',
4415
- components: { SlotErrorBoundary },
4416
- props: {
4417
- pointId: { type: String, required: true },
4418
- slotProps: { type: Object, default: () => ({}) }
4419
- },
4420
- computed: {
4421
- items() {
4422
- void registries.slotRevision;
4423
- return registries.slots[this.pointId] || [];
4614
+ });
4615
+ return VueLike.extend({
4616
+ name: 'ExtensionPoint',
4617
+ components: { SlotErrorBoundary },
4618
+ props: {
4619
+ pointId: { type: String, required: true },
4620
+ slotProps: { type: Object, default: () => ({}) }
4424
4621
  },
4425
- forwardProps() {
4426
- return this.slotProps || {};
4622
+ computed: {
4623
+ items() {
4624
+ void registries.slotRevision;
4625
+ return registries.slots[this.pointId] || [];
4626
+ },
4627
+ forwardProps() {
4628
+ return this.slotProps || {};
4629
+ }
4630
+ },
4631
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4632
+ render(h) {
4633
+ return h('div', {
4634
+ class: 'extension-point',
4635
+ attrs: { 'data-point-id': this.pointId }
4636
+ }, this.items.map((item) => h(SlotErrorBoundary, {
4637
+ key: item.key,
4638
+ props: { label: item.pluginId }
4639
+ }, [h(item.component, { props: this.forwardProps })])));
4427
4640
  }
4428
- },
4429
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
4430
- render(h) {
4431
- return h('div', {
4432
- class: 'extension-point',
4433
- attrs: { 'data-point-id': this.pointId }
4434
- }, this.items.map((item) => h(SlotErrorBoundary, {
4435
- key: item.key,
4436
- props: { label: item.pluginId }
4437
- }, [h(item.component, { props: this.forwardProps })])));
4438
- }
4439
- });
4641
+ });
4642
+ }
4643
+ var ExtensionPoint = createExtensionPointComponent();
4440
4644
 
4441
- /**
4442
- * 注册全局 `ExtensionPoint` 并异步拉取清单、激活插件。
4443
- */
4444
- /**
4445
- * @param Vue Vue 构造函数
4446
- * @param router vue-router 实例
4447
- * @param options 传给 `resolveRuntimeOptions`;可含 `env` 以读取 `VITE_*`
4448
- */
4449
4645
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
4450
4646
  function installWebExtendPluginVue2(Vue, router, options) {
4451
- const opts = options || {};
4452
- const { env: injectedEnv, ...runtimeUser } = opts;
4453
- if (injectedEnv && typeof injectedEnv === 'object') {
4454
- setWebExtendPluginEnv(injectedEnv);
4647
+ if (Vue) {
4648
+ ensureRegistriesReactive(Vue);
4649
+ Vue.component('ExtensionPoint', createExtensionPointComponent(Vue));
4455
4650
  }
4456
- if (Vue && ExtensionPoint) {
4457
- Vue.component('ExtensionPoint', ExtensionPoint);
4458
- }
4459
- const runtime = resolveRuntimeOptions$1(runtimeUser);
4651
+ const runtime = resolveRuntimeOptions$1(options || {});
4460
4652
  return bootstrapPlugins$1(router, (id, r, kit) => createHostApi(id, r, kit || {}), runtime);
4461
4653
  }
4462
4654
 
4463
- /**
4464
- * Vue CLI + 统一 axios(如 RuoYi `utils/request`)场景的 `install` 预设。
4465
- */
4466
4655
  function resolveManifestPathUnderApiBase(manifestUrl, apiBase) {
4467
4656
  const base = String(apiBase !== undefined
4468
4657
  ? apiBase
@@ -4472,18 +4661,13 @@ function resolveManifestPathUnderApiBase(manifestUrl, apiBase) {
4472
4661
  if (typeof window === 'undefined') {
4473
4662
  return '/api/frontend-plugins';
4474
4663
  }
4475
- const u = new URL(manifestUrl, window.location.origin);
4476
- let path = u.pathname + u.search;
4664
+ const url = new URL(manifestUrl, window.location.origin);
4665
+ let path = url.pathname + url.search;
4477
4666
  if (base && path.startsWith(base)) {
4478
4667
  path = path.slice(base.length) || '/';
4479
4668
  }
4480
4669
  return path;
4481
4670
  }
4482
- /**
4483
- * 常见 Java 清单路径:与 `VUE_APP_BASE_API` 拼接为 `${base}/frontend-plugins`。
4484
- * 若后端使用 `/api/frontend-plugins` 段,请在 extra 中显式传入 `manifestListPath`。
4485
- */
4486
- const defaultVueCliJavaManifestListPath = '/frontend-plugins';
4487
4671
  function bridgePrefixesFromVueCliEnv() {
4488
4672
  const base = (typeof process !== 'undefined' && process.env && process.env.VUE_APP_BASE_API
4489
4673
  ? String(process.env.VUE_APP_BASE_API)
@@ -4504,13 +4688,10 @@ function createVueCliAxiosInstallOptions(deps, extra = {}) {
4504
4688
  ? String(extra.manifestBase).replace(/\/$/, '')
4505
4689
  : '';
4506
4690
  const stripBase = userBase || envBase;
4507
- const manifestMode = resolveManifestModeFromInputs(extraManifestMode);
4508
- const staticManifestUrl = resolveStaticManifestUrlFromInputs(extraStaticManifestUrl);
4509
4691
  const fetchManifestApi = async (ctx) => {
4510
4692
  try {
4511
- const url = resolveManifestPathUnderApiBase(ctx.manifestUrl, stripBase);
4512
4693
  const body = await request({
4513
- url,
4694
+ url: resolveManifestPathUnderApiBase(ctx.manifestUrl, stripBase),
4514
4695
  method: 'get'
4515
4696
  });
4516
4697
  const data = unwrapNestedManifestBody(body);
@@ -4523,17 +4704,18 @@ function createVueCliAxiosInstallOptions(deps, extra = {}) {
4523
4704
  }
4524
4705
  return { ok: true, data };
4525
4706
  }
4526
- catch (e) {
4527
- return { ok: false, error: e, data: null };
4707
+ catch (error) {
4708
+ return { ok: false, error, data: null };
4528
4709
  }
4529
4710
  };
4530
- const fetchManifestStatic = (ctx) => fetchStaticManifestViaHttp(ctx);
4711
+ const manifestMode = resolveManifestModeFromInputs(extraManifestMode);
4712
+ const staticManifestUrl = resolveStaticManifestUrlFromInputs(extraStaticManifestUrl);
4531
4713
  const fetchManifest = typeof userFetchManifest === 'function'
4532
4714
  ? userFetchManifest
4533
4715
  : manifestMode === 'static'
4534
- ? fetchManifestStatic
4716
+ ? fetchStaticManifestViaHttp
4535
4717
  : fetchManifestApi;
4536
- const opts = {
4718
+ const options = {
4537
4719
  manifestBase: stripBase || undefined,
4538
4720
  bridgeAllowedPathPrefixes: bridgePrefixesFromVueCliEnv(),
4539
4721
  manifestMode,
@@ -4544,137 +4726,39 @@ function createVueCliAxiosInstallOptions(deps, extra = {}) {
4544
4726
  const listPath = typeof process !== 'undefined' &&
4545
4727
  process.env &&
4546
4728
  process.env[webExtendPluginEnvKeys.manifestPathAlt];
4547
- if (listPath && opts.manifestListPath === undefined && extra.manifestListPath === undefined) {
4548
- opts.manifestListPath = String(process.env[webExtendPluginEnvKeys.manifestPathAlt]);
4729
+ if (listPath && options.manifestListPath === undefined && extra.manifestListPath === undefined) {
4730
+ options.manifestListPath = String(process.env[webExtendPluginEnvKeys.manifestPathAlt]);
4549
4731
  }
4550
- return opts;
4551
- }
4552
- /**
4553
- * 少样板接入:`hostContext` 自动含 `router`(及可选 `store`)、`isDev` 与常见 `manifestListPath` 默认值。
4554
- * 更多运行时字段仍可通过 `extra` 覆盖。
4555
- */
4556
- function createVueCliAxiosQuickInstallOptions(
4557
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
4558
- router, deps, extra = {}) {
4559
- const { request, hostLayoutComponent, store, hostContext: depHostContext } = deps;
4560
- const { hostContext: extraHostContext, isDev: extraIsDev, manifestListPath: extraListPath, ...restExtra } = extra;
4561
- const hostContext = {
4562
- router,
4563
- ...(store !== undefined ? { store } : {}),
4564
- ...(depHostContext && typeof depHostContext === 'object' ? depHostContext : {}),
4565
- ...(extraHostContext && typeof extraHostContext === 'object' ? extraHostContext : {})
4566
- };
4567
- const manifestListPath = extraListPath !== undefined && String(extraListPath).trim() !== ''
4568
- ? String(extraListPath)
4569
- : defaultVueCliJavaManifestListPath;
4570
- return createVueCliAxiosInstallOptions({ request }, {
4571
- hostLayoutComponent,
4572
- hostContext,
4573
- isDev: extraIsDev !== undefined ? Boolean(extraIsDev) : resolveBundledIsDev(),
4574
- manifestListPath,
4575
- ...restExtra
4576
- });
4732
+ return options;
4577
4733
  }
4578
- const presetVueCliAxios = Object.freeze({
4579
- id: 'vue-cli-axios',
4580
- description: 'Vue CLI + axios request for API manifest; optional manifestMode=static uses fetch',
4581
- createInstallOptions: createVueCliAxiosInstallOptions,
4582
- createQuickInstallOptions: createVueCliAxiosQuickInstallOptions,
4583
- defaultJavaManifestListPath: defaultVueCliJavaManifestListPath,
4584
- manifestPathForApiBase: resolveManifestPathUnderApiBase,
4585
- unwrapManifestBody: unwrapNestedManifestBody
4586
- });
4587
4734
 
4588
- /**
4589
- * Vue CLI + axios 场景的一键安装:合并 hostContext、默认清单路径与 IIFE 全局 Vue。
4590
- */
4591
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
4592
- function installVueCliAxiosWebPlugins(Vue, router, deps, extra) {
4593
- /** 避免输出 `?.`:Vue CLI 4 / Webpack 4 默认不转译 node_modules */
4594
- const exposeGlobalVue = extra == null || extra.exposeGlobalVue !== false;
4595
- if (exposeGlobalVue && typeof window !== 'undefined') {
4596
- window.Vue = Vue;
4597
- }
4598
- const { exposeGlobalVue: _skip, ...restExtra } = extra || {};
4599
- const opts = createVueCliAxiosQuickInstallOptions(router, deps, restExtra);
4600
- return installWebExtendPluginVue2(Vue, router, opts);
4601
- }
4602
-
4603
- /**
4604
- * 包对外稳定导出与 `WebExtendPluginVue2` 命名空间。
4605
- */
4606
- const { bootstrapPlugins, defaultFetchWebPluginManifest, resolveRuntimeOptions, ensurePluginHostRoute } = pluginRuntime;
4735
+ const { bootstrapPlugins, defaultFetchWebPluginManifest, resolveRuntimeOptions, ensurePluginHostRoute, getActivatedPluginIds } = pluginRuntime;
4607
4736
  const { composeManifestFetch, manifestFetchCacheMiddleware, wrapManifestFetchWithCache } = manifestComposer;
4608
- const WebExtendPluginVue2 = Object.freeze({
4609
- install: installWebExtendPluginVue2,
4610
- runtime: Object.freeze({
4611
- bootstrapPlugins: bootstrapPlugins$1,
4612
- resolveRuntimeOptions: resolveRuntimeOptions$1,
4613
- ensurePluginHostRoute: ensurePluginHostRoute$1,
4614
- defaultFetchWebPluginManifest: defaultFetchWebPluginManifest$1,
4615
- composeManifestFetch: composeManifestFetch$1,
4616
- manifestFetchCacheMiddleware: manifestFetchCacheMiddleware$1,
4617
- wrapManifestFetchWithCache: wrapManifestFetchWithCache$1
4618
- }),
4619
- host: Object.freeze({
4620
- createHostApi,
4621
- disposeWebPlugin,
4622
- createRequestBridge,
4623
- registries,
4624
- buildMenuDescriptorsFromRoutes,
4625
- getRegisteredTopRouteNamesForPlugin
4626
- }),
4627
- config: Object.freeze({
4628
- defaultWebExtendPluginRuntime,
4629
- setWebExtendPluginEnv,
4630
- webExtendPluginEnvKeys,
4631
- defaultManifestFetchCache,
4632
- defaultManifestMode,
4633
- routeSynthNamePrefix,
4634
- peerMinimumVersions
4635
- }),
4636
- constants: Object.freeze({
4637
- HOST_PLUGIN_API_VERSION,
4638
- RUNTIME_CONSOLE_LABEL
4639
- }),
4640
- components: Object.freeze({
4641
- ExtensionPoint
4642
- }),
4643
- presets: Object.freeze({
4644
- vueCliAxios: presetVueCliAxios
4645
- })
4646
- });
4647
4737
 
4648
4738
  exports.ExtensionPoint = ExtensionPoint;
4649
4739
  exports.HOST_PLUGIN_API_VERSION = HOST_PLUGIN_API_VERSION;
4650
4740
  exports.RUNTIME_CONSOLE_LABEL = RUNTIME_CONSOLE_LABEL;
4651
- exports.WebExtendPluginVue2 = WebExtendPluginVue2;
4652
4741
  exports.bootstrapPlugins = bootstrapPlugins;
4653
- exports.buildMenuDescriptorsFromRoutes = buildMenuDescriptorsFromRoutes;
4654
4742
  exports.composeManifestFetch = composeManifestFetch;
4655
4743
  exports.createHostApi = createHostApi;
4656
4744
  exports.createRequestBridge = createRequestBridge;
4657
4745
  exports.createVueCliAxiosInstallOptions = createVueCliAxiosInstallOptions;
4658
- exports.createVueCliAxiosQuickInstallOptions = createVueCliAxiosQuickInstallOptions;
4659
4746
  exports.defaultFetchWebPluginManifest = defaultFetchWebPluginManifest;
4660
4747
  exports.defaultManifestFetchCache = defaultManifestFetchCache;
4661
4748
  exports.defaultManifestMode = defaultManifestMode;
4662
- exports.defaultVueCliJavaManifestListPath = defaultVueCliJavaManifestListPath;
4663
4749
  exports.defaultWebExtendPluginRuntime = defaultWebExtendPluginRuntime;
4664
4750
  exports.disposeWebPlugin = disposeWebPlugin;
4665
4751
  exports.ensurePluginHostRoute = ensurePluginHostRoute;
4752
+ exports.getActivatedPluginIds = getActivatedPluginIds;
4753
+ exports.getContributedRoutesForPlugin = getContributedRoutesForPlugin;
4666
4754
  exports.getRegisteredTopRouteNamesForPlugin = getRegisteredTopRouteNamesForPlugin;
4667
- exports.installVueCliAxiosWebPlugins = installVueCliAxiosWebPlugins;
4668
4755
  exports.installWebExtendPluginVue2 = installWebExtendPluginVue2;
4669
4756
  exports.manifestFetchCacheMiddleware = manifestFetchCacheMiddleware;
4670
4757
  exports.peerMinimumVersions = peerMinimumVersions;
4671
- exports.presetVueCliAxios = presetVueCliAxios;
4672
4758
  exports.registries = registries;
4673
- exports.resolveManifestPathUnderApiBase = resolveManifestPathUnderApiBase;
4674
4759
  exports.resolveRuntimeOptions = resolveRuntimeOptions;
4675
4760
  exports.routeSynthNamePrefix = routeSynthNamePrefix;
4676
4761
  exports.setWebExtendPluginEnv = setWebExtendPluginEnv;
4677
- exports.unwrapNestedManifestBody = unwrapNestedManifestBody;
4678
4762
  exports.webExtendPluginEnvKeys = webExtendPluginEnvKeys;
4679
4763
  exports.wrapManifestFetchWithCache = wrapManifestFetchWithCache;
4680
4764
  //# sourceMappingURL=index.cjs.map