web-extend-plugin-vue2 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -200
- package/dist/index.cjs +468 -406
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +467 -399
- package/dist/index.mjs.map +1 -1
- package/index.d.ts +18 -180
- package/package.json +3 -6
package/dist/index.mjs
CHANGED
|
@@ -1,23 +1,15 @@
|
|
|
1
1
|
import Vue from 'vue';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
10
|
-
vueRouter: '3.5.
|
|
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
|
|
3183
|
+
* 引导时注入的 router 引用,供 disposeWebPlugin 卸载插件动态路由。
|
|
3206
3184
|
*/
|
|
3207
3185
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3208
3186
|
let bootstrapRouter;
|
|
@@ -3216,6 +3194,206 @@ 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
|
+
* 宿主侧响应式注册表:扩展点槽位(供布局与 `ExtensionPoint` 消费)。
|
|
3259
|
+
* 菜单/侧栏树完全由宿主基于路由快照自行映射,框架不维护平行菜单列表。
|
|
3260
|
+
*/
|
|
3261
|
+
const registries = Vue.observable({
|
|
3262
|
+
slots: {},
|
|
3263
|
+
slotRevision: 0
|
|
3264
|
+
});
|
|
3265
|
+
|
|
3266
|
+
/**
|
|
3267
|
+
* 按插件 id 收集 `onTeardown` 回调,供 `disposeWebPlugin` 统一执行。
|
|
3268
|
+
*/
|
|
3269
|
+
const _byPlugin = new Map();
|
|
3270
|
+
function registerPluginTeardown(pluginId, fn) {
|
|
3271
|
+
if (typeof fn !== 'function') {
|
|
3272
|
+
return;
|
|
3273
|
+
}
|
|
3274
|
+
let list = _byPlugin.get(pluginId);
|
|
3275
|
+
if (!list) {
|
|
3276
|
+
list = [];
|
|
3277
|
+
_byPlugin.set(pluginId, list);
|
|
3278
|
+
}
|
|
3279
|
+
list.push(fn);
|
|
3280
|
+
}
|
|
3281
|
+
function runPluginTeardowns(pluginId) {
|
|
3282
|
+
const list = _byPlugin.get(pluginId);
|
|
3283
|
+
if (!list) {
|
|
3284
|
+
return;
|
|
3285
|
+
}
|
|
3286
|
+
_byPlugin.delete(pluginId);
|
|
3287
|
+
for (const fn of list) {
|
|
3288
|
+
try {
|
|
3289
|
+
fn();
|
|
3290
|
+
}
|
|
3291
|
+
catch (e) {
|
|
3292
|
+
console.warn('[wep] teardown failed', pluginId, e);
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
/**
|
|
3298
|
+
* 动态加载脚本(去重与并发合并)。
|
|
3299
|
+
*/
|
|
3300
|
+
const loadScriptMemo = new Map();
|
|
3301
|
+
function markScriptOwnership(script, pluginId) {
|
|
3302
|
+
if (pluginId) {
|
|
3303
|
+
script.setAttribute('data-plugin-asset', pluginId);
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
function clearLoadedScriptMemo(src) {
|
|
3307
|
+
if (typeof src === 'string' && src) {
|
|
3308
|
+
loadScriptMemo.delete(src);
|
|
3309
|
+
return;
|
|
3310
|
+
}
|
|
3311
|
+
loadScriptMemo.clear();
|
|
3312
|
+
}
|
|
3313
|
+
function loadScript(src, pluginId) {
|
|
3314
|
+
if (typeof document === 'undefined') {
|
|
3315
|
+
return Promise.reject(new Error('loadScript: no document'));
|
|
3316
|
+
}
|
|
3317
|
+
if (loadScriptMemo.has(src)) {
|
|
3318
|
+
return loadScriptMemo.get(src);
|
|
3319
|
+
}
|
|
3320
|
+
const p = new Promise((resolve, reject) => {
|
|
3321
|
+
const scripts = document.getElementsByTagName('script');
|
|
3322
|
+
for (let i = 0; i < scripts.length; i++) {
|
|
3323
|
+
const el = scripts[i];
|
|
3324
|
+
if (el.src === src) {
|
|
3325
|
+
markScriptOwnership(el, pluginId);
|
|
3326
|
+
if (el.getAttribute('data-wep-loaded') === 'true') {
|
|
3327
|
+
resolve();
|
|
3328
|
+
return;
|
|
3329
|
+
}
|
|
3330
|
+
el.addEventListener('load', () => {
|
|
3331
|
+
el.setAttribute('data-wep-loaded', 'true');
|
|
3332
|
+
resolve();
|
|
3333
|
+
}, { once: true });
|
|
3334
|
+
el.addEventListener('error', () => reject(new Error('loadScript failed: ' + src)), { once: true });
|
|
3335
|
+
return;
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
const s = document.createElement('script');
|
|
3339
|
+
s.async = true;
|
|
3340
|
+
s.src = src;
|
|
3341
|
+
markScriptOwnership(s, pluginId);
|
|
3342
|
+
s.onload = () => {
|
|
3343
|
+
s.setAttribute('data-wep-loaded', 'true');
|
|
3344
|
+
resolve();
|
|
3345
|
+
};
|
|
3346
|
+
s.onerror = () => reject(new Error('loadScript failed: ' + src));
|
|
3347
|
+
document.head.appendChild(s);
|
|
3348
|
+
});
|
|
3349
|
+
loadScriptMemo.set(src, p);
|
|
3350
|
+
p.catch(() => loadScriptMemo.delete(src));
|
|
3351
|
+
return p;
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
/**
|
|
3355
|
+
* 卸载单个插件:执行 teardown、移除该插件登记的动态路由、清理注册表与 activator、移除带 `data-plugin-asset` 的 DOM。
|
|
3356
|
+
* 优先使用 `addRoute()` 返回的 disposer;必要时回退 `router.removeRoute(name)`。
|
|
3357
|
+
*/
|
|
3358
|
+
function disposeWebPlugin(pluginId) {
|
|
3359
|
+
if (!pluginId || typeof pluginId !== 'string') {
|
|
3360
|
+
return;
|
|
3361
|
+
}
|
|
3362
|
+
runPluginTeardowns(pluginId);
|
|
3363
|
+
removeRegisteredRoutesForPlugin(getPluginBootstrapRouter(), pluginId);
|
|
3364
|
+
const slots = registries.slots;
|
|
3365
|
+
for (const pointId of Object.keys(slots)) {
|
|
3366
|
+
const list = slots[pointId];
|
|
3367
|
+
if (!Array.isArray(list)) {
|
|
3368
|
+
continue;
|
|
3369
|
+
}
|
|
3370
|
+
const next = list.filter((x) => x.pluginId !== pluginId);
|
|
3371
|
+
if (next.length === 0) {
|
|
3372
|
+
Vue.delete(slots, pointId);
|
|
3373
|
+
}
|
|
3374
|
+
else if (next.length !== list.length) {
|
|
3375
|
+
Vue.set(slots, pointId, next);
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
registries.slotRevision++;
|
|
3379
|
+
if (typeof window !== 'undefined' && window.__PLUGIN_ACTIVATORS__) {
|
|
3380
|
+
delete window.__PLUGIN_ACTIVATORS__[pluginId];
|
|
3381
|
+
}
|
|
3382
|
+
if (typeof document !== 'undefined') {
|
|
3383
|
+
document.querySelectorAll('[data-plugin-asset]').forEach((el) => {
|
|
3384
|
+
if (el.getAttribute('data-plugin-asset') === pluginId) {
|
|
3385
|
+
if (el.tagName === 'SCRIPT') {
|
|
3386
|
+
const src = el.src;
|
|
3387
|
+
if (src) {
|
|
3388
|
+
clearLoadedScriptMemo(src);
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
el.remove();
|
|
3392
|
+
}
|
|
3393
|
+
});
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3219
3397
|
/**
|
|
3220
3398
|
* 开发模式插件 URL 映射(显式 JSON + 隐式 dev 探测)。
|
|
3221
3399
|
*/
|
|
@@ -3378,49 +3556,6 @@ function startPluginDevSseForMap(devMap, isDev, hostSet, ssePath) {
|
|
|
3378
3556
|
}
|
|
3379
3557
|
}
|
|
3380
3558
|
|
|
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
3559
|
let _printed = false;
|
|
3425
3560
|
/** 在首次引导插件时打印一行运行时标识(非大块 ASCII art)。 */
|
|
3426
3561
|
function printRuntimeBannerOnce() {
|
|
@@ -3569,6 +3704,29 @@ function resolveStaticManifestUrlForFetch(url, origin) {
|
|
|
3569
3704
|
}
|
|
3570
3705
|
}
|
|
3571
3706
|
|
|
3707
|
+
/**
|
|
3708
|
+
* 记录当前已成功激活的插件 id,供宿主做只读观测。
|
|
3709
|
+
*/
|
|
3710
|
+
const activatedPluginIds = new Set();
|
|
3711
|
+
function clearActivatedPluginIds() {
|
|
3712
|
+
activatedPluginIds.clear();
|
|
3713
|
+
}
|
|
3714
|
+
function markPluginActivated(pluginId) {
|
|
3715
|
+
if (!pluginId) {
|
|
3716
|
+
return;
|
|
3717
|
+
}
|
|
3718
|
+
activatedPluginIds.add(pluginId);
|
|
3719
|
+
}
|
|
3720
|
+
function markPluginDeactivated(pluginId) {
|
|
3721
|
+
if (!pluginId) {
|
|
3722
|
+
return;
|
|
3723
|
+
}
|
|
3724
|
+
activatedPluginIds.delete(pluginId);
|
|
3725
|
+
}
|
|
3726
|
+
function getActivatedPluginIds$1() {
|
|
3727
|
+
return [...activatedPluginIds];
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3572
3730
|
/**
|
|
3573
3731
|
* 拉取插件清单、加载入口脚本并调用各插件 `activator`。
|
|
3574
3732
|
*/
|
|
@@ -3604,7 +3762,7 @@ async function loadPluginEntry(p, entryUrl, devMap, hostSet) {
|
|
|
3604
3762
|
catch (e) {
|
|
3605
3763
|
console.warn('[wep] dev import failed, trying manifest entryUrl', p.id, e);
|
|
3606
3764
|
if (entryUrl && isScriptHostAllowed(entryUrl, hostSet)) {
|
|
3607
|
-
await loadScript(entryUrl);
|
|
3765
|
+
await loadScript(entryUrl, p.id);
|
|
3608
3766
|
}
|
|
3609
3767
|
return;
|
|
3610
3768
|
}
|
|
@@ -3614,7 +3772,40 @@ async function loadPluginEntry(p, entryUrl, devMap, hostSet) {
|
|
|
3614
3772
|
console.warn('[wep] skip (entryUrl not allowed)', p.id, entryUrl);
|
|
3615
3773
|
return;
|
|
3616
3774
|
}
|
|
3617
|
-
await loadScript(entryUrl);
|
|
3775
|
+
await loadScript(entryUrl, p.id);
|
|
3776
|
+
}
|
|
3777
|
+
function coercePriority(value) {
|
|
3778
|
+
if (value === null || value === undefined) {
|
|
3779
|
+
return null;
|
|
3780
|
+
}
|
|
3781
|
+
const n = Number(value);
|
|
3782
|
+
return Number.isFinite(n) ? n : null;
|
|
3783
|
+
}
|
|
3784
|
+
function sortByPriority(plugins) {
|
|
3785
|
+
return plugins
|
|
3786
|
+
.map((entry, index) => ({
|
|
3787
|
+
entry,
|
|
3788
|
+
priority: coercePriority(entry.priority),
|
|
3789
|
+
index
|
|
3790
|
+
}))
|
|
3791
|
+
.sort((a, b) => {
|
|
3792
|
+
const aHas = a.priority !== null;
|
|
3793
|
+
const bHas = b.priority !== null;
|
|
3794
|
+
if (!aHas && !bHas) {
|
|
3795
|
+
return a.index - b.index;
|
|
3796
|
+
}
|
|
3797
|
+
if (!aHas) {
|
|
3798
|
+
return 1;
|
|
3799
|
+
}
|
|
3800
|
+
if (!bHas) {
|
|
3801
|
+
return -1;
|
|
3802
|
+
}
|
|
3803
|
+
if (a.priority !== b.priority) {
|
|
3804
|
+
return a.priority - b.priority;
|
|
3805
|
+
}
|
|
3806
|
+
return a.index - b.index;
|
|
3807
|
+
})
|
|
3808
|
+
.map((decorated) => decorated.entry);
|
|
3618
3809
|
}
|
|
3619
3810
|
async function bootstrapPlugins$1(
|
|
3620
3811
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -3626,6 +3817,7 @@ createHostApiFactory, runtimeOptions) {
|
|
|
3626
3817
|
return;
|
|
3627
3818
|
}
|
|
3628
3819
|
printRuntimeBannerOnce();
|
|
3820
|
+
clearActivatedPluginIds();
|
|
3629
3821
|
const opts = resolveRuntimeOptions$1(runtimeOptions || {});
|
|
3630
3822
|
setPluginBootstrapRouter(router);
|
|
3631
3823
|
ensurePluginHostRoute$1(router, opts);
|
|
@@ -3704,6 +3896,9 @@ createHostApiFactory, runtimeOptions) {
|
|
|
3704
3896
|
: {}),
|
|
3705
3897
|
...(typeof opts.adaptRouteDeclarations === 'function'
|
|
3706
3898
|
? { adaptRouteDeclarations: opts.adaptRouteDeclarations }
|
|
3899
|
+
: {}),
|
|
3900
|
+
...(typeof opts.onPluginRoutesContributed === 'function'
|
|
3901
|
+
? { onPluginRoutesContributed: opts.onPluginRoutesContributed }
|
|
3707
3902
|
: {})
|
|
3708
3903
|
};
|
|
3709
3904
|
if (!manifestResult.ok) {
|
|
@@ -3732,22 +3927,30 @@ createHostApiFactory, runtimeOptions) {
|
|
|
3732
3927
|
const maj = coerced ? coerced.major : 0;
|
|
3733
3928
|
const range = `^${maj}.0.0`;
|
|
3734
3929
|
if (!semver.satisfies(HOST_PLUGIN_API_VERSION, range, { includePrerelease: true })) {
|
|
3735
|
-
console.warn('[wep] host API version mismatch', {
|
|
3930
|
+
console.warn('[wep] manifest host API version mismatch; skip bootstrap', {
|
|
3736
3931
|
host: HOST_PLUGIN_API_VERSION,
|
|
3737
3932
|
manifest: apiVer
|
|
3738
3933
|
});
|
|
3934
|
+
if (shouldShowBootstrapSummary(opts)) {
|
|
3935
|
+
console.info('[wep] bootstrap_summary', {
|
|
3936
|
+
ok: false,
|
|
3937
|
+
reason: 'manifest_host_api_version_mismatch'
|
|
3938
|
+
});
|
|
3939
|
+
}
|
|
3940
|
+
return;
|
|
3739
3941
|
}
|
|
3740
3942
|
}
|
|
3741
3943
|
window.__PLUGIN_ACTIVATORS__ = window.__PLUGIN_ACTIVATORS__ || {};
|
|
3742
|
-
const
|
|
3944
|
+
const originalPlugins = data.plugins || [];
|
|
3945
|
+
const plugins = sortByPriority(originalPlugins);
|
|
3743
3946
|
if (plugins.length === 0) {
|
|
3744
3947
|
const hint = isStatic ? 'check static JSON file and plugins[]' : 'check backend and URL';
|
|
3745
3948
|
console.info('[wep] empty plugin manifest — ' + hint, manifestUrlUsed);
|
|
3746
3949
|
}
|
|
3747
3950
|
const summary = {
|
|
3748
|
-
manifestCount:
|
|
3951
|
+
manifestCount: originalPlugins.length,
|
|
3749
3952
|
activated: 0,
|
|
3750
|
-
|
|
3953
|
+
skipApiVersion: 0,
|
|
3751
3954
|
skipLoad: 0,
|
|
3752
3955
|
skipNoActivator: 0,
|
|
3753
3956
|
activateFail: 0
|
|
@@ -3755,8 +3958,10 @@ createHostApiFactory, runtimeOptions) {
|
|
|
3755
3958
|
for (const p of plugins) {
|
|
3756
3959
|
const range = p.engines && p.engines.host;
|
|
3757
3960
|
if (range && !semver.satisfies(HOST_PLUGIN_API_VERSION, range, { includePrerelease: true })) {
|
|
3758
|
-
console.warn('[wep] skip plugin (engines.host)', p.id, range
|
|
3759
|
-
|
|
3961
|
+
console.warn('[wep] skip plugin (engines.host)', p.id, range, {
|
|
3962
|
+
host: HOST_PLUGIN_API_VERSION
|
|
3963
|
+
});
|
|
3964
|
+
summary.skipApiVersion++;
|
|
3760
3965
|
continue;
|
|
3761
3966
|
}
|
|
3762
3967
|
const entryUrl = p.entryUrl;
|
|
@@ -3792,6 +3997,11 @@ createHostApiFactory, runtimeOptions) {
|
|
|
3792
3997
|
const hostApi = createHostApiFactory(p.id, router, hostKit);
|
|
3793
3998
|
try {
|
|
3794
3999
|
await Promise.resolve(activator(hostApi, { pluginRecord }));
|
|
4000
|
+
markPluginActivated(p.id);
|
|
4001
|
+
const teardownCapableHostApi = hostApi;
|
|
4002
|
+
if (typeof teardownCapableHostApi.onTeardown === 'function') {
|
|
4003
|
+
teardownCapableHostApi.onTeardown(p.id, () => markPluginDeactivated(p.id));
|
|
4004
|
+
}
|
|
3795
4005
|
summary.activated++;
|
|
3796
4006
|
if (typeof opts.onAfterPluginActivate === 'function') {
|
|
3797
4007
|
await Promise.resolve(opts.onAfterPluginActivate({
|
|
@@ -3804,6 +4014,12 @@ createHostApiFactory, runtimeOptions) {
|
|
|
3804
4014
|
}
|
|
3805
4015
|
catch (e) {
|
|
3806
4016
|
console.error('[wep] activate failed', p.id, e);
|
|
4017
|
+
try {
|
|
4018
|
+
disposeWebPlugin(p.id);
|
|
4019
|
+
}
|
|
4020
|
+
catch (disposeErr) {
|
|
4021
|
+
console.warn('[wep] rollback failed after activation error', p.id, disposeErr);
|
|
4022
|
+
}
|
|
3807
4023
|
summary.activateFail++;
|
|
3808
4024
|
if (typeof opts.onPluginActivateError === 'function') {
|
|
3809
4025
|
try {
|
|
@@ -3834,6 +4050,7 @@ var pluginRuntime = /*#__PURE__*/Object.freeze({
|
|
|
3834
4050
|
bootstrapPlugins: bootstrapPlugins$1,
|
|
3835
4051
|
defaultFetchWebPluginManifest: defaultFetchWebPluginManifest$1,
|
|
3836
4052
|
ensurePluginHostRoute: ensurePluginHostRoute$1,
|
|
4053
|
+
getActivatedPluginIds: getActivatedPluginIds$1,
|
|
3837
4054
|
resolveRuntimeOptions: resolveRuntimeOptions$1
|
|
3838
4055
|
});
|
|
3839
4056
|
|
|
@@ -3992,48 +4209,40 @@ var manifestComposer = /*#__PURE__*/Object.freeze({
|
|
|
3992
4209
|
});
|
|
3993
4210
|
|
|
3994
4211
|
/**
|
|
3995
|
-
*
|
|
3996
|
-
* 菜单由宿主从路由 `meta` 推导(见 `buildMenuDescriptorsFromRoutes`),框架不维护平行菜单列表。
|
|
3997
|
-
*/
|
|
3998
|
-
const registries = Vue.observable({
|
|
3999
|
-
slots: {},
|
|
4000
|
-
slotRevision: 0
|
|
4001
|
-
});
|
|
4002
|
-
|
|
4003
|
-
/**
|
|
4004
|
-
* 按插件 id 收集 `onTeardown` 回调,供 `disposeWebPlugin` 统一执行。
|
|
4212
|
+
* 插件访问后端的受控通道:`fetch` 仅允许落在配置的路径前缀下(默认 `/api/`),默认 `credentials: 'same-origin'`。
|
|
4005
4213
|
*/
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
return;
|
|
4214
|
+
function normalizeBridgePath(input) {
|
|
4215
|
+
if (typeof window === 'undefined') {
|
|
4216
|
+
throw new Error('[wep:bridge] window is unavailable');
|
|
4010
4217
|
}
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
list = [];
|
|
4014
|
-
_byPlugin.set(pluginId, list);
|
|
4218
|
+
if (typeof input !== 'string' || !input.startsWith('/')) {
|
|
4219
|
+
throw new Error('[wep:bridge] path must start with /');
|
|
4015
4220
|
}
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
function runPluginTeardowns(pluginId) {
|
|
4019
|
-
const list = _byPlugin.get(pluginId);
|
|
4020
|
-
if (!list) {
|
|
4021
|
-
return;
|
|
4221
|
+
if (input.includes('\\')) {
|
|
4222
|
+
throw new Error('[wep:bridge] path must not contain backslashes');
|
|
4022
4223
|
}
|
|
4023
|
-
|
|
4024
|
-
|
|
4224
|
+
const url = new URL(input, window.location.origin);
|
|
4225
|
+
if (url.origin !== window.location.origin) {
|
|
4226
|
+
throw new Error('[wep:bridge] cross-origin path is not allowed');
|
|
4227
|
+
}
|
|
4228
|
+
const normalized = url.pathname;
|
|
4229
|
+
const decodedPath = (() => {
|
|
4025
4230
|
try {
|
|
4026
|
-
|
|
4231
|
+
return decodeURIComponent(normalized);
|
|
4027
4232
|
}
|
|
4028
|
-
catch
|
|
4029
|
-
|
|
4233
|
+
catch {
|
|
4234
|
+
return normalized;
|
|
4030
4235
|
}
|
|
4031
|
-
}
|
|
4236
|
+
})();
|
|
4237
|
+
const hasTraversalSegment = decodedPath
|
|
4238
|
+
.split('/')
|
|
4239
|
+
.filter(Boolean)
|
|
4240
|
+
.some((segment) => segment === '.' || segment === '..');
|
|
4241
|
+
if (hasTraversalSegment) {
|
|
4242
|
+
throw new Error('[wep:bridge] path traversal is not allowed');
|
|
4243
|
+
}
|
|
4244
|
+
return normalized + url.search;
|
|
4032
4245
|
}
|
|
4033
|
-
|
|
4034
|
-
/**
|
|
4035
|
-
* 插件访问后端的受控通道:`fetch` 仅允许落在配置的路径前缀下(默认 `/api/`),默认 `credentials: 'same-origin'`。
|
|
4036
|
-
*/
|
|
4037
4246
|
function createRequestBridge(config = {}) {
|
|
4038
4247
|
const raw = Array.isArray(config.allowedPathPrefixes) && config.allowedPathPrefixes.length > 0
|
|
4039
4248
|
? config.allowedPathPrefixes
|
|
@@ -4041,14 +4250,13 @@ function createRequestBridge(config = {}) {
|
|
|
4041
4250
|
const allowedPathPrefixes = raw.map((p) => ensureLeadingPath(p));
|
|
4042
4251
|
return {
|
|
4043
4252
|
async request(path, init = {}) {
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
}
|
|
4047
|
-
const allowed = allowedPathPrefixes.some((p) => path.startsWith(p));
|
|
4253
|
+
const normalizedPath = normalizeBridgePath(path);
|
|
4254
|
+
const pathnameOnly = normalizedPath.split('?')[0];
|
|
4255
|
+
const allowed = allowedPathPrefixes.some((p) => pathnameOnly === p || pathnameOnly.startsWith(`${p.replace(/\/$/, '')}/`));
|
|
4048
4256
|
if (!allowed) {
|
|
4049
|
-
throw new Error('[wep:bridge] path not allowed: ' +
|
|
4257
|
+
throw new Error('[wep:bridge] path not allowed: ' + normalizedPath);
|
|
4050
4258
|
}
|
|
4051
|
-
return fetch(
|
|
4259
|
+
return fetch(normalizedPath, {
|
|
4052
4260
|
credentials: 'same-origin',
|
|
4053
4261
|
...init
|
|
4054
4262
|
});
|
|
@@ -4057,51 +4265,83 @@ function createRequestBridge(config = {}) {
|
|
|
4057
4265
|
}
|
|
4058
4266
|
|
|
4059
4267
|
/**
|
|
4060
|
-
*
|
|
4268
|
+
* 按插件记录已贡献路由的可序列化快照,避免宿主依赖 router 内部 matcher 形态。
|
|
4061
4269
|
*/
|
|
4062
|
-
const
|
|
4063
|
-
function
|
|
4064
|
-
if (
|
|
4065
|
-
return;
|
|
4270
|
+
const contributedRoutesByPlugin = new Map();
|
|
4271
|
+
function sanitizeValue(value) {
|
|
4272
|
+
if (value == null) {
|
|
4273
|
+
return value;
|
|
4066
4274
|
}
|
|
4067
|
-
|
|
4068
|
-
|
|
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;
|
|
4275
|
+
if (typeof value === 'function') {
|
|
4276
|
+
return undefined;
|
|
4079
4277
|
}
|
|
4080
|
-
if (
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4278
|
+
if (Array.isArray(value)) {
|
|
4279
|
+
return value
|
|
4280
|
+
.map((item) => sanitizeValue(item))
|
|
4281
|
+
.filter((item) => item !== undefined);
|
|
4084
4282
|
}
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4283
|
+
if (typeof value !== 'object') {
|
|
4284
|
+
return value;
|
|
4285
|
+
}
|
|
4286
|
+
const out = {};
|
|
4287
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
4288
|
+
if (key === 'component' || key === 'components') {
|
|
4289
|
+
continue;
|
|
4088
4290
|
}
|
|
4089
|
-
|
|
4090
|
-
|
|
4291
|
+
const sanitized = sanitizeValue(raw);
|
|
4292
|
+
if (sanitized !== undefined) {
|
|
4293
|
+
out[key] = sanitized;
|
|
4091
4294
|
}
|
|
4092
4295
|
}
|
|
4093
|
-
|
|
4296
|
+
return out;
|
|
4094
4297
|
}
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4298
|
+
function sanitizeContributedRoutes(routes) {
|
|
4299
|
+
return routes
|
|
4300
|
+
.map((route) => sanitizeValue(route))
|
|
4301
|
+
.filter((route) => !!route && typeof route === 'object');
|
|
4302
|
+
}
|
|
4303
|
+
function recordContributedRoutesForPlugin(pluginId, routes) {
|
|
4304
|
+
const sanitized = sanitizeContributedRoutes(routes);
|
|
4305
|
+
const current = contributedRoutesByPlugin.get(pluginId) || [];
|
|
4306
|
+
const next = current.concat(sanitized);
|
|
4307
|
+
contributedRoutesByPlugin.set(pluginId, next);
|
|
4308
|
+
return next.map((route) => sanitizeValue(route));
|
|
4309
|
+
}
|
|
4310
|
+
function getContributedRoutesForPlugin(pluginId) {
|
|
4311
|
+
const routes = contributedRoutesByPlugin.get(pluginId) || [];
|
|
4312
|
+
return routes.map((route) => sanitizeValue(route));
|
|
4313
|
+
}
|
|
4314
|
+
function clearContributedRoutesForPlugin(pluginId) {
|
|
4315
|
+
contributedRoutesByPlugin.delete(pluginId);
|
|
4098
4316
|
}
|
|
4099
4317
|
|
|
4100
4318
|
/**
|
|
4101
|
-
* 构造插件 `activator(hostApi)` 使用的宿主 API
|
|
4319
|
+
* 构造插件 `activator(hostApi)` 使用的宿主 API:路由、扩展点、资源与受控请求桥。
|
|
4102
4320
|
*/
|
|
4103
4321
|
let slotItemKeySeq = 0;
|
|
4104
4322
|
let routeSynthSeq = 0;
|
|
4323
|
+
function decorateRouteTreeWithPluginMeta(pluginId, route) {
|
|
4324
|
+
const meta = route.meta && typeof route.meta === 'object' && !Array.isArray(route.meta)
|
|
4325
|
+
? route.meta
|
|
4326
|
+
: {};
|
|
4327
|
+
const wepMeta = meta.__wep && typeof meta.__wep === 'object' && !Array.isArray(meta.__wep)
|
|
4328
|
+
? meta.__wep
|
|
4329
|
+
: {};
|
|
4330
|
+
const nextChildren = Array.isArray(route.children)
|
|
4331
|
+
? route.children.map((child) => decorateRouteTreeWithPluginMeta(pluginId, child))
|
|
4332
|
+
: route.children;
|
|
4333
|
+
return {
|
|
4334
|
+
...route,
|
|
4335
|
+
meta: {
|
|
4336
|
+
...meta,
|
|
4337
|
+
__wep: {
|
|
4338
|
+
...wepMeta,
|
|
4339
|
+
pluginId
|
|
4340
|
+
}
|
|
4341
|
+
},
|
|
4342
|
+
...(nextChildren ? { children: nextChildren } : {})
|
|
4343
|
+
};
|
|
4344
|
+
}
|
|
4105
4345
|
function analyzeRouteInputTree(nodes) {
|
|
4106
4346
|
let hasDecl = false;
|
|
4107
4347
|
let hasCfg = false;
|
|
@@ -4143,26 +4383,71 @@ function createHostApi(pluginId, router, hostKitOptions = {}) {
|
|
|
4143
4383
|
const parentName = typeof hostKitOptions.pluginRoutesParentName === 'string'
|
|
4144
4384
|
? hostKitOptions.pluginRoutesParentName.trim()
|
|
4145
4385
|
: '';
|
|
4386
|
+
function rollbackRegisteredTopRoutes(routes) {
|
|
4387
|
+
for (let i = routes.length - 1; i >= 0; i--) {
|
|
4388
|
+
const route = routes[i];
|
|
4389
|
+
if (typeof route.dispose === 'function') {
|
|
4390
|
+
try {
|
|
4391
|
+
route.dispose();
|
|
4392
|
+
continue;
|
|
4393
|
+
}
|
|
4394
|
+
catch (e) {
|
|
4395
|
+
console.warn('[wep] rollback route disposer failed', route.name, e);
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
if (typeof router.removeRoute === 'function') {
|
|
4399
|
+
try {
|
|
4400
|
+
router.removeRoute(route.name);
|
|
4401
|
+
}
|
|
4402
|
+
catch (e) {
|
|
4403
|
+
console.warn('[wep] rollback removeRoute failed', route.name, e);
|
|
4404
|
+
}
|
|
4405
|
+
}
|
|
4406
|
+
}
|
|
4407
|
+
}
|
|
4146
4408
|
function applyInternalRegister(rawRouteConfigs) {
|
|
4147
4409
|
const wrapped = rawRouteConfigs.map((r) => ({
|
|
4148
|
-
...r,
|
|
4149
|
-
name: r.name || `${routeSynthNamePrefix}${pluginId}_${routeSynthSeq++}
|
|
4150
|
-
meta: { ...(r.meta || {}), pluginId }
|
|
4410
|
+
...decorateRouteTreeWithPluginMeta(pluginId, r),
|
|
4411
|
+
name: r.name || `${routeSynthNamePrefix}${pluginId}_${routeSynthSeq++}`
|
|
4151
4412
|
}));
|
|
4152
4413
|
if (typeof router.addRoute !== 'function') {
|
|
4153
4414
|
throw new Error('[wep] vue-router 3.5+ 必需:请使用 router.addRoute(不再支持 addRoutes)');
|
|
4154
4415
|
}
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4416
|
+
const registeredTopRoutes = [];
|
|
4417
|
+
try {
|
|
4418
|
+
if (parentName) {
|
|
4419
|
+
for (const r of wrapped) {
|
|
4420
|
+
const dispose = router.addRoute(parentName, r);
|
|
4421
|
+
registeredTopRoutes.push({
|
|
4422
|
+
name: String(r.name),
|
|
4423
|
+
dispose: typeof dispose === 'function' ? dispose : undefined
|
|
4424
|
+
});
|
|
4425
|
+
}
|
|
4159
4426
|
}
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4427
|
+
else {
|
|
4428
|
+
for (const r of wrapped) {
|
|
4429
|
+
const dispose = router.addRoute(r);
|
|
4430
|
+
registeredTopRoutes.push({
|
|
4431
|
+
name: String(r.name),
|
|
4432
|
+
dispose: typeof dispose === 'function' ? dispose : undefined
|
|
4433
|
+
});
|
|
4434
|
+
}
|
|
4164
4435
|
}
|
|
4165
4436
|
}
|
|
4437
|
+
catch (e) {
|
|
4438
|
+
rollbackRegisteredTopRoutes(registeredTopRoutes);
|
|
4439
|
+
throw e;
|
|
4440
|
+
}
|
|
4441
|
+
recordPluginTopRoutes(pluginId, registeredTopRoutes);
|
|
4442
|
+
const contributedRoutes = recordContributedRoutesForPlugin(pluginId, wrapped);
|
|
4443
|
+
if (contributedRoutes.length > 0 && typeof hostKitOptions.onPluginRoutesContributed === 'function') {
|
|
4444
|
+
hostKitOptions.onPluginRoutesContributed({
|
|
4445
|
+
pluginId,
|
|
4446
|
+
router,
|
|
4447
|
+
routes: wrapped,
|
|
4448
|
+
contributedRoutes
|
|
4449
|
+
});
|
|
4450
|
+
}
|
|
4166
4451
|
}
|
|
4167
4452
|
function injectStylesheet(href) {
|
|
4168
4453
|
const link = document.createElement('link');
|
|
@@ -4185,6 +4470,9 @@ function createHostApi(pluginId, router, hostKitOptions = {}) {
|
|
|
4185
4470
|
const hostContext = hostKitOptions.hostContext != null && typeof hostKitOptions.hostContext === 'object'
|
|
4186
4471
|
? hostKitOptions.hostContext
|
|
4187
4472
|
: Object.freeze({});
|
|
4473
|
+
registerPluginTeardown(pluginId, () => {
|
|
4474
|
+
clearContributedRoutesForPlugin(pluginId);
|
|
4475
|
+
});
|
|
4188
4476
|
return {
|
|
4189
4477
|
hostPluginApiVersion: HOST_PLUGIN_API_VERSION,
|
|
4190
4478
|
/** 宿主注入的只读依赖(store、router、开放能力等),见 `resolveRuntimeOptions.hostContext` */
|
|
@@ -4265,6 +4553,7 @@ function createHostApi(pluginId, router, hostKitOptions = {}) {
|
|
|
4265
4553
|
registerSanitizedHtmlSnippet() {
|
|
4266
4554
|
throw new Error('registerSanitizedHtmlSnippet is not enabled');
|
|
4267
4555
|
},
|
|
4556
|
+
getContributedRoutes: () => getContributedRoutesForPlugin(pluginId),
|
|
4268
4557
|
getBridge: () => bridge,
|
|
4269
4558
|
onTeardown(_pluginId, fn) {
|
|
4270
4559
|
if (typeof fn === 'function') {
|
|
@@ -4274,112 +4563,6 @@ function createHostApi(pluginId, router, hostKitOptions = {}) {
|
|
|
4274
4563
|
};
|
|
4275
4564
|
}
|
|
4276
4565
|
|
|
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();
|
|
4309
|
-
}
|
|
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);
|
|
4377
|
-
}
|
|
4378
|
-
}
|
|
4379
|
-
sortDescriptors(out);
|
|
4380
|
-
return out;
|
|
4381
|
-
}
|
|
4382
|
-
|
|
4383
4566
|
/**
|
|
4384
4567
|
* 布局中的扩展点占位;插件通过 `hostApi.registerSlotComponents(pointId, ...)` 注入组件。
|
|
4385
4568
|
* 样式由宿主全局 CSS 覆盖 `.extension-point` / `.plugin-point-error`。
|
|
@@ -4432,31 +4615,15 @@ var ExtensionPoint = Vue.extend({
|
|
|
4432
4615
|
}
|
|
4433
4616
|
});
|
|
4434
4617
|
|
|
4435
|
-
/**
|
|
4436
|
-
* 注册全局 `ExtensionPoint` 并异步拉取清单、激活插件。
|
|
4437
|
-
*/
|
|
4438
|
-
/**
|
|
4439
|
-
* @param Vue Vue 构造函数
|
|
4440
|
-
* @param router vue-router 实例
|
|
4441
|
-
* @param options 传给 `resolveRuntimeOptions`;可含 `env` 以读取 `VITE_*`
|
|
4442
|
-
*/
|
|
4443
4618
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4444
4619
|
function installWebExtendPluginVue2(Vue, router, options) {
|
|
4445
|
-
const opts = options || {};
|
|
4446
|
-
const { env: injectedEnv, ...runtimeUser } = opts;
|
|
4447
|
-
if (injectedEnv && typeof injectedEnv === 'object') {
|
|
4448
|
-
setWebExtendPluginEnv(injectedEnv);
|
|
4449
|
-
}
|
|
4450
4620
|
if (Vue && ExtensionPoint) {
|
|
4451
4621
|
Vue.component('ExtensionPoint', ExtensionPoint);
|
|
4452
4622
|
}
|
|
4453
|
-
const runtime = resolveRuntimeOptions$1(
|
|
4623
|
+
const runtime = resolveRuntimeOptions$1(options || {});
|
|
4454
4624
|
return bootstrapPlugins$1(router, (id, r, kit) => createHostApi(id, r, kit || {}), runtime);
|
|
4455
4625
|
}
|
|
4456
4626
|
|
|
4457
|
-
/**
|
|
4458
|
-
* Vue CLI + 统一 axios(如 RuoYi `utils/request`)场景的 `install` 预设。
|
|
4459
|
-
*/
|
|
4460
4627
|
function resolveManifestPathUnderApiBase(manifestUrl, apiBase) {
|
|
4461
4628
|
const base = String(apiBase !== undefined
|
|
4462
4629
|
? apiBase
|
|
@@ -4466,18 +4633,13 @@ function resolveManifestPathUnderApiBase(manifestUrl, apiBase) {
|
|
|
4466
4633
|
if (typeof window === 'undefined') {
|
|
4467
4634
|
return '/api/frontend-plugins';
|
|
4468
4635
|
}
|
|
4469
|
-
const
|
|
4470
|
-
let path =
|
|
4636
|
+
const url = new URL(manifestUrl, window.location.origin);
|
|
4637
|
+
let path = url.pathname + url.search;
|
|
4471
4638
|
if (base && path.startsWith(base)) {
|
|
4472
4639
|
path = path.slice(base.length) || '/';
|
|
4473
4640
|
}
|
|
4474
4641
|
return path;
|
|
4475
4642
|
}
|
|
4476
|
-
/**
|
|
4477
|
-
* 常见 Java 清单路径:与 `VUE_APP_BASE_API` 拼接为 `${base}/frontend-plugins`。
|
|
4478
|
-
* 若后端使用 `/api/frontend-plugins` 段,请在 extra 中显式传入 `manifestListPath`。
|
|
4479
|
-
*/
|
|
4480
|
-
const defaultVueCliJavaManifestListPath = '/frontend-plugins';
|
|
4481
4643
|
function bridgePrefixesFromVueCliEnv() {
|
|
4482
4644
|
const base = (typeof process !== 'undefined' && process.env && process.env.VUE_APP_BASE_API
|
|
4483
4645
|
? String(process.env.VUE_APP_BASE_API)
|
|
@@ -4498,13 +4660,10 @@ function createVueCliAxiosInstallOptions(deps, extra = {}) {
|
|
|
4498
4660
|
? String(extra.manifestBase).replace(/\/$/, '')
|
|
4499
4661
|
: '';
|
|
4500
4662
|
const stripBase = userBase || envBase;
|
|
4501
|
-
const manifestMode = resolveManifestModeFromInputs(extraManifestMode);
|
|
4502
|
-
const staticManifestUrl = resolveStaticManifestUrlFromInputs(extraStaticManifestUrl);
|
|
4503
4663
|
const fetchManifestApi = async (ctx) => {
|
|
4504
4664
|
try {
|
|
4505
|
-
const url = resolveManifestPathUnderApiBase(ctx.manifestUrl, stripBase);
|
|
4506
4665
|
const body = await request({
|
|
4507
|
-
url,
|
|
4666
|
+
url: resolveManifestPathUnderApiBase(ctx.manifestUrl, stripBase),
|
|
4508
4667
|
method: 'get'
|
|
4509
4668
|
});
|
|
4510
4669
|
const data = unwrapNestedManifestBody(body);
|
|
@@ -4517,17 +4676,18 @@ function createVueCliAxiosInstallOptions(deps, extra = {}) {
|
|
|
4517
4676
|
}
|
|
4518
4677
|
return { ok: true, data };
|
|
4519
4678
|
}
|
|
4520
|
-
catch (
|
|
4521
|
-
return { ok: false, error
|
|
4679
|
+
catch (error) {
|
|
4680
|
+
return { ok: false, error, data: null };
|
|
4522
4681
|
}
|
|
4523
4682
|
};
|
|
4524
|
-
const
|
|
4683
|
+
const manifestMode = resolveManifestModeFromInputs(extraManifestMode);
|
|
4684
|
+
const staticManifestUrl = resolveStaticManifestUrlFromInputs(extraStaticManifestUrl);
|
|
4525
4685
|
const fetchManifest = typeof userFetchManifest === 'function'
|
|
4526
4686
|
? userFetchManifest
|
|
4527
4687
|
: manifestMode === 'static'
|
|
4528
|
-
?
|
|
4688
|
+
? fetchStaticManifestViaHttp
|
|
4529
4689
|
: fetchManifestApi;
|
|
4530
|
-
const
|
|
4690
|
+
const options = {
|
|
4531
4691
|
manifestBase: stripBase || undefined,
|
|
4532
4692
|
bridgeAllowedPathPrefixes: bridgePrefixesFromVueCliEnv(),
|
|
4533
4693
|
manifestMode,
|
|
@@ -4538,106 +4698,14 @@ function createVueCliAxiosInstallOptions(deps, extra = {}) {
|
|
|
4538
4698
|
const listPath = typeof process !== 'undefined' &&
|
|
4539
4699
|
process.env &&
|
|
4540
4700
|
process.env[webExtendPluginEnvKeys.manifestPathAlt];
|
|
4541
|
-
if (listPath &&
|
|
4542
|
-
|
|
4701
|
+
if (listPath && options.manifestListPath === undefined && extra.manifestListPath === undefined) {
|
|
4702
|
+
options.manifestListPath = String(process.env[webExtendPluginEnvKeys.manifestPathAlt]);
|
|
4543
4703
|
}
|
|
4544
|
-
return
|
|
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
|
-
});
|
|
4571
|
-
}
|
|
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
|
-
|
|
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);
|
|
4704
|
+
return options;
|
|
4595
4705
|
}
|
|
4596
4706
|
|
|
4597
|
-
|
|
4598
|
-
* 包对外稳定导出与 `WebExtendPluginVue2` 命名空间。
|
|
4599
|
-
*/
|
|
4600
|
-
const { bootstrapPlugins, defaultFetchWebPluginManifest, resolveRuntimeOptions, ensurePluginHostRoute } = pluginRuntime;
|
|
4707
|
+
const { bootstrapPlugins, defaultFetchWebPluginManifest, resolveRuntimeOptions, ensurePluginHostRoute, getActivatedPluginIds } = pluginRuntime;
|
|
4601
4708
|
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
4709
|
|
|
4642
|
-
export { ExtensionPoint, HOST_PLUGIN_API_VERSION, RUNTIME_CONSOLE_LABEL,
|
|
4710
|
+
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
4711
|
//# sourceMappingURL=index.mjs.map
|