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