web-extend-plugin-vue2 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1307 @@
1
+ 'use strict';
2
+
3
+ var semver = require('semver');
4
+ var Vue = require('vue');
5
+
6
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
+
8
+ var Vue__default = /*#__PURE__*/_interopDefault(Vue);
9
+
10
+ /**
11
+ * 宿主可覆盖的默认运行时配置(路径、白名单、超时等)。
12
+ * 使用方式:`resolveRuntimeOptions({ ...defaultWebExtendPluginRuntime, manifestListPath: '/api/my-plugins' })`
13
+ * 或只传需要改动的字段:`resolveRuntimeOptions({ manifestListPath: '/api/my-plugins' })`。
14
+ *
15
+ * @module default-runtime-config
16
+ */
17
+
18
+ /**
19
+ * @typedef {typeof defaultWebExtendPluginRuntime} WebExtendPluginDefaultRuntime
20
+ */
21
+
22
+ const defaultWebExtendPluginRuntime = {
23
+ /** 清单 HTTP 服务前缀(与后端 context-path 对齐),不含尾部 `/` */
24
+ manifestBase: '/fp-api',
25
+
26
+ /**
27
+ * 拉取插件清单的 **路径段**(以 `/` 开头),拼在 `manifestBase` 之后。
28
+ * 完整 URL:`{manifestBase}{manifestListPath}` → 默认 `/fp-api/api/frontend-plugins`
29
+ */
30
+ manifestListPath: '/api/frontend-plugins',
31
+
32
+ /** 清单 `fetch` 的 `credentials`,需 Cookie 会话时用 `include` */
33
+ manifestFetchCredentials: 'include',
34
+
35
+ /** 插件 dev 服务存活探测路径(拼在 `webPluginDevOrigin` 后) */
36
+ devPingPath: '/__web_plugin_dev_ping',
37
+
38
+ /** 插件 dev 热更新 SSE 路径(拼在插件 dev 的 origin 后) */
39
+ devReloadSsePath: '/__web_plugin_reload_stream',
40
+
41
+ /** 隐式 dev 映射里,每个 id 对应的入口路径(相对插件 dev origin) */
42
+ webPluginDevEntryPath: '/src/plugin-entry.js',
43
+
44
+ /** `fetch(devPingUrl)` 超时毫秒数 */
45
+ devPingTimeoutMs: 500,
46
+
47
+ /**
48
+ * **隐式 dev 映射**(可选):开发模式下若 `webPluginDevOrigin` 上 ping 成功,运行时会为若干插件 id 自动生成
49
+ * `{ id → origin + webPluginDevEntryPath }`,从而用 dev 服务入口替代清单里的 dist,便于热更新联调。
50
+ *
51
+ * 插件 id 来源优先级:`webPluginDevIds`(或 `VITE_WEB_PLUGIN_DEV_IDS`)→ 本字段。**默认 `[]`**,
52
+ * 避免把仓库示例 id 写进通用宿主;联调时在 `.env` 里设 `VITE_WEB_PLUGIN_DEV_IDS=com.xxx,com.yyy`,
53
+ * 或 `resolveRuntimeOptions({ defaultImplicitDevPluginIds: ['com.xxx'] })` 即可。
54
+ */
55
+ defaultImplicitDevPluginIds: [],
56
+
57
+ /**
58
+ * 允许通过 `<script>` / 动态 `import()` 加载的插件脚本所在主机名(小写),防误配公网 URL。
59
+ */
60
+ allowedScriptHosts: ['localhost', '127.0.0.1', '::1'],
61
+
62
+ /**
63
+ * `hostApi.getBridge().request(path)` 允许的 path 前缀;须以 `/` 开头。
64
+ * 需与后端实际 API 前缀一致。
65
+ */
66
+ bridgeAllowedPathPrefixes: ['/api/']
67
+ };
68
+
69
+ /**
70
+ * 不依赖 `import.meta`,兼容 Webpack 4/5、Vue CLI、Vite、Rspack 等宿主。
71
+ * - Webpack / Vue CLI:依赖构建时注入的 `process.env`(如 `VUE_APP_*`、`DefinePlugin` 注入的 `VITE_*`)。
72
+ * - Vite:在入口调用 `setWebExtendPluginEnv(import.meta.env)`,或 `installWebExtendPluginVue2(..., { env: import.meta.env })`。
73
+ * - 也可在入口前设置 `globalThis.__WEP_ENV__ = import.meta.env`(与 setWebExtendPluginEnv 二选一即可)。
74
+ *
75
+ * @module bundled-env
76
+ */
77
+
78
+ /** @type {Record<string, unknown> | null} */
79
+ let _explicitEnv = null;
80
+
81
+ /**
82
+ * 显式注入与 `import.meta.env` 同形态的对象(推荐 Vite 宿主在入口调用一次)。
83
+ * @param {Record<string, unknown> | null | undefined} env
84
+ */
85
+ function setWebExtendPluginEnv(env) {
86
+ _explicitEnv = env && typeof env === 'object' ? env : null;
87
+ }
88
+
89
+ /**
90
+ * @returns {Record<string, unknown> | null}
91
+ */
92
+ function getInjectedEnvObject() {
93
+ if (_explicitEnv) {
94
+ return _explicitEnv
95
+ }
96
+ try {
97
+ const g = typeof globalThis !== 'undefined' ? globalThis : undefined;
98
+ if (g && g.__WEP_ENV__ && typeof g.__WEP_ENV__ === 'object') {
99
+ return g.__WEP_ENV__
100
+ }
101
+ } catch (_) {}
102
+ return null
103
+ }
104
+
105
+ /**
106
+ * 从注入环境读取字符串配置键(`VITE_*` / `PLUGIN_*` 等)。
107
+ * @param {string} key
108
+ * @returns {string|undefined}
109
+ */
110
+ function readInjectedEnvKey(key) {
111
+ const o = getInjectedEnvObject();
112
+ if (!o || !(key in o)) {
113
+ return undefined
114
+ }
115
+ const v = o[key];
116
+ if (v === undefined || v === '') {
117
+ return undefined
118
+ }
119
+ return String(v)
120
+ }
121
+
122
+ /**
123
+ * @returns {boolean}
124
+ */
125
+ function readInjectedEnvDev() {
126
+ const o = getInjectedEnvObject();
127
+ return !!(o && o.DEV === true)
128
+ }
129
+
130
+ /**
131
+ * 与 `plugin-web-starter`(WebPluginsResponse)返回的 `hostPluginApiVersion` 保持一致,用于契约校验。
132
+ * @type {string}
133
+ */
134
+ const HOST_PLUGIN_API_VERSION = '1.0.0';
135
+
136
+ /**
137
+ * 宿主侧插件引导:拉取清单、dev 映射、加载入口脚本、调用 activator。
138
+ * 路径与白名单等默认值见 `defaultWebExtendPluginRuntime`,可通过 `resolveRuntimeOptions` / 环境变量覆盖。
139
+ *
140
+ * **Webpack 宿主**:用 `DefinePlugin` 注入 `process.env.VITE_*` 或 **`PLUGIN_*`**(等价键),或 `resolveRuntimeOptions` 显式传参。
141
+ * **Vite 宿主**:入口调用 `setWebExtendPluginEnv(import.meta.env)`,或 `installWebExtendPluginVue2(..., { env: import.meta.env })`。
142
+ *
143
+ * @module PluginRuntime
144
+ */
145
+
146
+ const DEF = defaultWebExtendPluginRuntime;
147
+
148
+ /**
149
+ * @typedef {object} WebExtendPluginRuntimeOptions
150
+ * @property {string} [manifestBase] 清单服务 URL 前缀
151
+ * @property {string} [manifestListPath] 清单接口路径(以 `/` 开头),拼在 manifestBase 后
152
+ * @property {RequestCredentials} [manifestFetchCredentials] 清单 fetch 的 credentials
153
+ * @property {boolean} [isDev] 开发模式
154
+ * @property {string} [webPluginDevOrigin] 插件 dev origin
155
+ * @property {string} [webPluginDevIds] 逗号分隔 id,隐式 dev 映射
156
+ * @property {string} [webPluginDevMapJson] 显式 dev 映射 JSON
157
+ * @property {string} [webPluginDevEntryPath] 隐式 dev 入口路径(相对插件 dev origin)
158
+ * @property {string} [devPingPath] dev 存活探测路径
159
+ * @property {string} [devReloadSsePath] dev 热更新 SSE 路径
160
+ * @property {number} [devPingTimeoutMs] 探测超时
161
+ * @property {string[]} [defaultImplicitDevPluginIds] 无 `webPluginDevIds`/env 时用于隐式 dev 的 id;包内默认 `[]`
162
+ * @property {string[]} [allowedScriptHosts] 允许加载脚本的主机名
163
+ * @property {string[]} [bridgeAllowedPathPrefixes] bridge.request 白名单前缀
164
+ * @property {boolean} [bootstrapSummary] bootstrap 结束是否打印摘要
165
+ */
166
+
167
+ /**
168
+ * 从 Webpack `DefinePlugin` 等注入的 `process.env` 读取。
169
+ * @param {string} key
170
+ * @returns {string|undefined}
171
+ */
172
+ function readProcessEnv(key) {
173
+ try {
174
+ if (typeof process !== 'undefined' && process.env && key in process.env) {
175
+ const v = process.env[key];
176
+ if (v !== undefined && v !== '') {
177
+ return String(v)
178
+ }
179
+ }
180
+ } catch (_) {}
181
+ return undefined
182
+ }
183
+
184
+ /**
185
+ * `VITE_*` 的并列命名:同值可读 `PLUGIN_*`(`VITE_WEB_PLUGIN_X` → `PLUGIN_WEB_PLUGIN_X`)。
186
+ * Vite 需在 `defineConfig({ envPrefix: ['VITE_', 'PLUGIN_'] })` 中暴露 `PLUGIN_`;Webpack 用 DefinePlugin 注入即可。
187
+ * @param {string} viteStyleKey 以 `VITE_` 开头的键名
188
+ * @returns {string|null}
189
+ */
190
+ function viteKeyToPluginAlternate(viteStyleKey) {
191
+ if (typeof viteStyleKey !== 'string' || !viteStyleKey.startsWith('VITE_')) {
192
+ return null
193
+ }
194
+ return `PLUGIN_${viteStyleKey.slice(5)}`
195
+ }
196
+
197
+ /**
198
+ * 先读注入环境(`setWebExtendPluginEnv` / `__WEP_ENV__`)中的 `VITE_*` 与并列 `PLUGIN_*`,再读 `process.env`,最后 `fallback`。
199
+ * @param {string} key 仍以 `VITE_*` 为逻辑名(与文档一致)
200
+ * @param {string} [fallback='']
201
+ */
202
+ function resolveBundledEnv(key, fallback = '') {
203
+ const alt = viteKeyToPluginAlternate(key);
204
+ const fromInjected =
205
+ readInjectedEnvKey(key) ?? (alt ? readInjectedEnvKey(alt) : undefined);
206
+ const fromProcess =
207
+ readProcessEnv(key) ?? (alt ? readProcessEnv(alt) : undefined);
208
+ return fromInjected ?? fromProcess ?? fallback
209
+ }
210
+
211
+ /**
212
+ * @returns {boolean}
213
+ */
214
+ function resolveBundledIsDev() {
215
+ try {
216
+ if (readInjectedEnvDev()) {
217
+ return true
218
+ }
219
+ } catch (_) {}
220
+ try {
221
+ if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'development') {
222
+ return true
223
+ }
224
+ } catch (_) {}
225
+ return false
226
+ }
227
+
228
+ /**
229
+ * @param {string} p
230
+ */
231
+ function ensureLeadingPath(p) {
232
+ const t = String(p || '').trim();
233
+ if (!t) {
234
+ return '/'
235
+ }
236
+ return t.startsWith('/') ? t : `/${t}`
237
+ }
238
+
239
+ /**
240
+ * 解析 `include` | `omit` | `same-origin`,非法时回退默认值。
241
+ * @param {string|undefined} userVal
242
+ * @param {string} envKey
243
+ * @param {RequestCredentials} fallback
244
+ */
245
+ function resolveManifestCredentials(userVal, envKey, fallback) {
246
+ if (userVal !== undefined && userVal !== '') {
247
+ const s = String(userVal);
248
+ if (s === 'include' || s === 'omit' || s === 'same-origin') {
249
+ return s
250
+ }
251
+ }
252
+ const e = resolveBundledEnv(envKey, '');
253
+ if (e === 'include' || e === 'omit' || e === 'same-origin') {
254
+ return e
255
+ }
256
+ return fallback
257
+ }
258
+
259
+ /**
260
+ * @param {number|undefined} userVal
261
+ * @param {string} envKey
262
+ * @param {number} fallback
263
+ */
264
+ function resolvePositiveInt(userVal, envKey, fallback) {
265
+ if (typeof userVal === 'number' && Number.isFinite(userVal) && userVal > 0) {
266
+ return Math.floor(userVal)
267
+ }
268
+ const raw = resolveBundledEnv(envKey, '');
269
+ const n = raw ? parseInt(raw, 10) : NaN;
270
+ if (Number.isFinite(n) && n > 0) {
271
+ return n
272
+ }
273
+ return fallback
274
+ }
275
+
276
+ /**
277
+ * 合并用户、环境变量与 `defaultWebExtendPluginRuntime`,得到完整运行时选项(宿主可只传需要覆盖的字段)。
278
+ * @param {WebExtendPluginRuntimeOptions} [user]
279
+ * @returns {object}
280
+ */
281
+ function resolveRuntimeOptions(user = {}) {
282
+ const manifestBaseRaw =
283
+ user.manifestBase !== undefined && user.manifestBase !== ''
284
+ ? String(user.manifestBase)
285
+ : resolveBundledEnv('VITE_FRONTEND_PLUGIN_BASE', DEF.manifestBase) || DEF.manifestBase;
286
+
287
+ const manifestListPath = ensureLeadingPath(
288
+ user.manifestListPath !== undefined && user.manifestListPath !== ''
289
+ ? user.manifestListPath
290
+ : resolveBundledEnv('VITE_WEB_PLUGIN_MANIFEST_PATH', DEF.manifestListPath)
291
+ );
292
+
293
+ const defaultImplicitDevPluginIds =
294
+ Array.isArray(user.defaultImplicitDevPluginIds)
295
+ ? user.defaultImplicitDevPluginIds.map(String).filter(Boolean)
296
+ : (() => {
297
+ const e = resolveBundledEnv('VITE_WEB_PLUGIN_IMPLICIT_DEV_IDS', '');
298
+ if (e) {
299
+ return e
300
+ .split(',')
301
+ .map((s) => s.trim())
302
+ .filter(Boolean)
303
+ }
304
+ return [...DEF.defaultImplicitDevPluginIds]
305
+ })();
306
+
307
+ const allowedScriptHosts =
308
+ Array.isArray(user.allowedScriptHosts) && user.allowedScriptHosts.length > 0
309
+ ? user.allowedScriptHosts.map((h) => normalizeHost(String(h))).filter(Boolean)
310
+ : (() => {
311
+ const e = resolveBundledEnv('VITE_WEB_PLUGIN_ALLOWED_SCRIPT_HOSTS', '');
312
+ if (e) {
313
+ return e
314
+ .split(',')
315
+ .map((s) => normalizeHost(s.trim()))
316
+ .filter(Boolean)
317
+ }
318
+ return [...DEF.allowedScriptHosts]
319
+ })();
320
+
321
+ const bridgeAllowedPathPrefixes =
322
+ Array.isArray(user.bridgeAllowedPathPrefixes) && user.bridgeAllowedPathPrefixes.length > 0
323
+ ? user.bridgeAllowedPathPrefixes.map((p) => ensureLeadingPath(p)).filter(Boolean)
324
+ : (() => {
325
+ const e = resolveBundledEnv('VITE_WEB_PLUGIN_BRIDGE_PREFIXES', '');
326
+ if (e) {
327
+ return e
328
+ .split(',')
329
+ .map((s) => ensureLeadingPath(s.trim()))
330
+ .filter(Boolean)
331
+ }
332
+ return [...DEF.bridgeAllowedPathPrefixes]
333
+ })();
334
+
335
+ return {
336
+ manifestBase: manifestBaseRaw.replace(/\/$/, '') || DEF.manifestBase.replace(/\/$/, ''),
337
+ manifestListPath,
338
+ manifestFetchCredentials: resolveManifestCredentials(
339
+ user.manifestFetchCredentials,
340
+ 'VITE_WEB_PLUGIN_MANIFEST_CREDENTIALS',
341
+ DEF.manifestFetchCredentials
342
+ ),
343
+ isDev: user.isDev !== undefined ? user.isDev : resolveBundledIsDev(),
344
+ webPluginDevOrigin:
345
+ user.webPluginDevOrigin !== undefined ? user.webPluginDevOrigin : resolveBundledEnv('VITE_WEB_PLUGIN_DEV_ORIGIN', ''),
346
+ webPluginDevIds:
347
+ user.webPluginDevIds !== undefined ? user.webPluginDevIds : resolveBundledEnv('VITE_WEB_PLUGIN_DEV_IDS', ''),
348
+ webPluginDevMapJson:
349
+ user.webPluginDevMapJson !== undefined
350
+ ? user.webPluginDevMapJson
351
+ : resolveBundledEnv('VITE_WEB_PLUGIN_DEV_MAP', ''),
352
+ webPluginDevEntryPath: ensureLeadingPath(
353
+ user.webPluginDevEntryPath !== undefined && user.webPluginDevEntryPath !== ''
354
+ ? user.webPluginDevEntryPath
355
+ : resolveBundledEnv('VITE_WEB_PLUGIN_DEV_ENTRY', DEF.webPluginDevEntryPath)
356
+ ),
357
+ devPingPath: ensureLeadingPath(
358
+ user.devPingPath !== undefined && user.devPingPath !== ''
359
+ ? user.devPingPath
360
+ : resolveBundledEnv('VITE_WEB_PLUGIN_DEV_PING_PATH', DEF.devPingPath)
361
+ ),
362
+ devReloadSsePath: ensureLeadingPath(
363
+ user.devReloadSsePath !== undefined && user.devReloadSsePath !== ''
364
+ ? user.devReloadSsePath
365
+ : resolveBundledEnv('VITE_WEB_PLUGIN_DEV_SSE_PATH', DEF.devReloadSsePath)
366
+ ),
367
+ devPingTimeoutMs: resolvePositiveInt(user.devPingTimeoutMs, 'VITE_WEB_PLUGIN_DEV_PING_TIMEOUT_MS', DEF.devPingTimeoutMs),
368
+ defaultImplicitDevPluginIds,
369
+ allowedScriptHosts,
370
+ bridgeAllowedPathPrefixes,
371
+ bootstrapSummary: user.bootstrapSummary
372
+ }
373
+ }
374
+
375
+ /**
376
+ * @param {ReturnType<typeof resolveRuntimeOptions>} opts
377
+ */
378
+ function shouldShowBootstrapSummary(opts) {
379
+ if (opts.bootstrapSummary === true) {
380
+ return true
381
+ }
382
+ if (opts.bootstrapSummary === false) {
383
+ return false
384
+ }
385
+ const env = resolveBundledEnv('VITE_PLUGINS_BOOTSTRAP_SUMMARY', '');
386
+ if (env === '0' || env === 'false') {
387
+ return false
388
+ }
389
+ if (env === '1' || env === 'true') {
390
+ return true
391
+ }
392
+ return resolveBundledIsDev()
393
+ }
394
+
395
+ /**
396
+ * @param {string} hostname
397
+ */
398
+ function normalizeHost(hostname) {
399
+ if (!hostname) {
400
+ return ''
401
+ }
402
+ const h = hostname.toLowerCase();
403
+ if (h.startsWith('[') && h.endsWith(']')) {
404
+ return h.slice(1, -1)
405
+ }
406
+ return h
407
+ }
408
+
409
+ /**
410
+ * @param {string[]} hostnames
411
+ * @returns {Set<string>}
412
+ */
413
+ function buildAllowedScriptHostsSet(hostnames) {
414
+ const s = new Set();
415
+ for (const h of hostnames) {
416
+ const n = normalizeHost(h);
417
+ if (n) {
418
+ s.add(n);
419
+ }
420
+ }
421
+ return s
422
+ }
423
+
424
+ /**
425
+ * @param {string} url
426
+ * @param {Set<string>} hostSet
427
+ */
428
+ function isScriptHostAllowed(url, hostSet) {
429
+ if (typeof window === 'undefined') {
430
+ return false
431
+ }
432
+ try {
433
+ const u = new URL(url, window.location.origin);
434
+ const h = normalizeHost(u.hostname);
435
+ return hostSet.has(h)
436
+ } catch {
437
+ return false
438
+ }
439
+ }
440
+
441
+ /**
442
+ * @param {ReturnType<typeof resolveRuntimeOptions>} opts
443
+ */
444
+ function parseWebPluginDevMapExplicit(opts) {
445
+ if (!opts.isDev) {
446
+ return null
447
+ }
448
+ const raw = opts.webPluginDevMapJson;
449
+ if (raw === undefined || raw === null || String(raw).trim() === '') {
450
+ return null
451
+ }
452
+ try {
453
+ const map = JSON.parse(String(raw));
454
+ return map && typeof map === 'object' ? map : null
455
+ } catch {
456
+ console.warn('[plugins] webPluginDevMapJson / VITE_WEB_PLUGIN_DEV_MAP is not valid JSON');
457
+ return null
458
+ }
459
+ }
460
+
461
+ /**
462
+ * @param {ReturnType<typeof resolveRuntimeOptions>} opts
463
+ * @param {Set<string>} hostSet
464
+ */
465
+ async function buildImplicitWebPluginDevMap(opts, hostSet) {
466
+ if (!opts.isDev) {
467
+ return {}
468
+ }
469
+ const origin =
470
+ opts.webPluginDevOrigin === undefined || opts.webPluginDevOrigin === null
471
+ ? ''
472
+ : String(opts.webPluginDevOrigin).trim();
473
+ if (!origin) {
474
+ return {}
475
+ }
476
+ if (!isScriptHostAllowed(`${origin}/`, hostSet)) {
477
+ return {}
478
+ }
479
+
480
+ const idsRaw = opts.webPluginDevIds;
481
+ const ids =
482
+ idsRaw !== undefined && idsRaw !== null && String(idsRaw).trim() !== ''
483
+ ? String(idsRaw)
484
+ .split(',')
485
+ .map((s) => s.trim())
486
+ .filter(Boolean)
487
+ : [...opts.defaultImplicitDevPluginIds];
488
+
489
+ if (ids.length === 0) {
490
+ return {}
491
+ }
492
+
493
+ const base = origin.replace(/\/$/, '');
494
+ const pingUrl = `${base}${opts.devPingPath}`;
495
+ try {
496
+ const ctrl = new AbortController();
497
+ const timer = setTimeout(() => ctrl.abort(), opts.devPingTimeoutMs);
498
+ const r = await fetch(pingUrl, {
499
+ mode: 'cors',
500
+ cache: 'no-store',
501
+ signal: ctrl.signal
502
+ });
503
+ clearTimeout(timer);
504
+ if (!r.ok) {
505
+ return {}
506
+ }
507
+ const body = (await r.text()).trim();
508
+ if (body !== 'ok') {
509
+ return {}
510
+ }
511
+ } catch {
512
+ return {}
513
+ }
514
+
515
+ const pathPart = opts.webPluginDevEntryPath;
516
+ const map = {};
517
+ for (const id of ids) {
518
+ map[id] = `${base}${pathPart}`;
519
+ }
520
+ if (ids.length) {
521
+ console.info(
522
+ '[plugins] 已检测到插件 dev 服务(',
523
+ base,
524
+ '),下列 id 将加载隐式 dev 入口(',
525
+ pathPart,
526
+ ')而非清单 dist:',
527
+ ids.join(', ')
528
+ );
529
+ }
530
+ return map
531
+ }
532
+
533
+ /**
534
+ * @param {Record<string, string>} implicit
535
+ * @param {Record<string, string>|null} explicit
536
+ */
537
+ function mergeDevMaps(implicit, explicit) {
538
+ const i = implicit && typeof implicit === 'object' ? implicit : {};
539
+ const e = explicit && typeof explicit === 'object' ? explicit : {};
540
+ return { ...i, ...e }
541
+ }
542
+
543
+ /** @type {Map<string, EventSource>} */
544
+ const pluginDevEventSources = new Map();
545
+
546
+ let pluginDevBeforeUnloadRegistered = false;
547
+
548
+ function closeAllPluginDevEventSources() {
549
+ for (const es of pluginDevEventSources.values()) {
550
+ try {
551
+ es.close();
552
+ } catch (_) {}
553
+ }
554
+ pluginDevEventSources.clear();
555
+ }
556
+
557
+ function ensurePluginDevBeforeUnload() {
558
+ if (pluginDevBeforeUnloadRegistered || typeof window === 'undefined') {
559
+ return
560
+ }
561
+ pluginDevBeforeUnloadRegistered = true;
562
+ window.addEventListener('beforeunload', closeAllPluginDevEventSources);
563
+ }
564
+
565
+ /**
566
+ * @param {string} origin
567
+ * @param {Set<string>} hostSet
568
+ */
569
+ function isDevOriginAllowedForSse(origin, hostSet) {
570
+ try {
571
+ const u = new URL(origin);
572
+ return hostSet.has(normalizeHost(u.hostname))
573
+ } catch {
574
+ return false
575
+ }
576
+ }
577
+
578
+ /**
579
+ * @param {string} origin
580
+ * @param {boolean} isDev
581
+ * @param {Set<string>} hostSet
582
+ * @param {string} ssePath
583
+ */
584
+ function startPluginDevReloadSse(origin, isDev, hostSet, ssePath) {
585
+ if (!isDev || pluginDevEventSources.has(origin)) {
586
+ return
587
+ }
588
+ if (!isDevOriginAllowedForSse(origin, hostSet)) {
589
+ return
590
+ }
591
+ ensurePluginDevBeforeUnload();
592
+ const base = origin.replace(/\/$/, '');
593
+ const url = `${base}${ssePath}`;
594
+ try {
595
+ const es = new EventSource(url);
596
+ pluginDevEventSources.set(origin, es);
597
+ es.addEventListener('reload', () => {
598
+ window.location.reload();
599
+ });
600
+ es.onopen = () => {
601
+ console.info('[plugins] plugin dev reload SSE:', url);
602
+ };
603
+ } catch (e) {
604
+ console.warn('[plugins] EventSource failed', url, e);
605
+ }
606
+ }
607
+
608
+ /**
609
+ * @param {Record<string, string>|null|undefined} devMap
610
+ * @param {boolean} isDev
611
+ * @param {Set<string>} hostSet
612
+ * @param {string} ssePath
613
+ */
614
+ function startPluginDevSseForMap(devMap, isDev, hostSet, ssePath) {
615
+ if (!isDev || !devMap || typeof window === 'undefined') {
616
+ return
617
+ }
618
+ const origins = new Set();
619
+ for (const entry of Object.values(devMap)) {
620
+ if (typeof entry !== 'string') {
621
+ continue
622
+ }
623
+ const t = entry.trim();
624
+ if (!t) {
625
+ continue
626
+ }
627
+ try {
628
+ origins.add(new URL(t, window.location.href).origin);
629
+ } catch {
630
+ /* skip */
631
+ }
632
+ }
633
+ for (const o of origins) {
634
+ startPluginDevReloadSse(o, isDev, hostSet, ssePath);
635
+ }
636
+ }
637
+
638
+ const loadScriptMemo = new Map();
639
+
640
+ function loadScript(src) {
641
+ if (typeof document === 'undefined') {
642
+ return Promise.reject(new Error('loadScript: no document'))
643
+ }
644
+ if (loadScriptMemo.has(src)) {
645
+ return loadScriptMemo.get(src)
646
+ }
647
+ const p = new Promise((resolve, reject) => {
648
+ const scripts = document.getElementsByTagName('script');
649
+ for (let i = 0; i < scripts.length; i++) {
650
+ const el = scripts[i];
651
+ if (el.src === src) {
652
+ if (el.getAttribute('data-wep-loaded') === 'true') {
653
+ resolve();
654
+ return
655
+ }
656
+ el.addEventListener(
657
+ 'load',
658
+ () => {
659
+ el.setAttribute('data-wep-loaded', 'true');
660
+ resolve();
661
+ },
662
+ { once: true }
663
+ );
664
+ el.addEventListener('error', () => reject(new Error('loadScript failed: ' + src)), { once: true });
665
+ return
666
+ }
667
+ }
668
+ const s = document.createElement('script');
669
+ s.async = true;
670
+ s.src = src;
671
+ s.onload = () => {
672
+ s.setAttribute('data-wep-loaded', 'true');
673
+ resolve();
674
+ };
675
+ s.onerror = () => reject(new Error('loadScript failed: ' + src));
676
+ document.head.appendChild(s);
677
+ });
678
+ loadScriptMemo.set(src, p);
679
+ p.catch(() => loadScriptMemo.delete(src));
680
+ return p
681
+ }
682
+
683
+ /**
684
+ * @param {{ id: string }} p
685
+ * @param {string} [entryUrl]
686
+ * @param {Record<string, string>|null|undefined} devMap
687
+ * @param {Set<string>} hostSet
688
+ */
689
+ async function loadPluginEntry(p, entryUrl, devMap, hostSet) {
690
+ const devEntry = devMap && typeof devMap[p.id] === 'string' ? devMap[p.id].trim() : '';
691
+ if (devEntry) {
692
+ if (!isScriptHostAllowed(devEntry, hostSet)) {
693
+ console.warn('[plugins] dev entry URL not allowed', p.id, devEntry);
694
+ return
695
+ }
696
+ try {
697
+ await import(
698
+ /* webpackIgnore: true */
699
+ /* @vite-ignore */
700
+ devEntry
701
+ );
702
+ } catch (e) {
703
+ console.warn('[plugins] dev module import failed, try manifest entryUrl', p.id, e);
704
+ if (entryUrl && isScriptHostAllowed(entryUrl, hostSet)) {
705
+ await loadScript(entryUrl);
706
+ }
707
+ return
708
+ }
709
+ return
710
+ }
711
+ if (!entryUrl || !isScriptHostAllowed(entryUrl, hostSet)) {
712
+ console.warn('[plugins] skip (entryUrl not allowed)', p.id, entryUrl);
713
+ return
714
+ }
715
+ await loadScript(entryUrl);
716
+ }
717
+
718
+ /**
719
+ * @param {import('vue-router').default} router
720
+ * @param {(pluginId: string, router: import('vue-router').default, hostKit?: { bridgeAllowedPathPrefixes: string[] }) => object} createHostApiFactory
721
+ * 始终传入三个参数;单参工厂 `(id) => createHostApi(id, router)` 仍可用,后两个实参被忽略。
722
+ * @param {WebExtendPluginRuntimeOptions} [runtimeOptions]
723
+ */
724
+ async function bootstrapPlugins(router, createHostApiFactory, runtimeOptions) {
725
+ if (typeof window === 'undefined') {
726
+ console.warn('[plugins] bootstrapPlugins skipped: requires browser (window)');
727
+ return
728
+ }
729
+ const opts = resolveRuntimeOptions(runtimeOptions || {});
730
+ const base = String(opts.manifestBase).replace(/\/$/, '');
731
+ const manifestUrl = `${base}${opts.manifestListPath}`;
732
+ const hostSet = buildAllowedScriptHostsSet(opts.allowedScriptHosts);
733
+ const explicit = parseWebPluginDevMapExplicit(opts);
734
+
735
+ const [manifestResult, implicit] = await Promise.all([
736
+ (async () => {
737
+ try {
738
+ const res = await fetch(manifestUrl, { credentials: opts.manifestFetchCredentials });
739
+ if (!res.ok) {
740
+ return { ok: false, status: res.status, data: null }
741
+ }
742
+ const data = await res.json();
743
+ return { ok: true, data }
744
+ } catch (e) {
745
+ return { ok: false, error: e, data: null }
746
+ }
747
+ })(),
748
+ buildImplicitWebPluginDevMap(opts, hostSet)
749
+ ]);
750
+
751
+ const devMap = mergeDevMaps(implicit, explicit);
752
+ startPluginDevSseForMap(devMap, opts.isDev, hostSet, opts.devReloadSsePath);
753
+
754
+ const hostKit = { bridgeAllowedPathPrefixes: opts.bridgeAllowedPathPrefixes };
755
+
756
+ if (!manifestResult.ok) {
757
+ if (manifestResult.error) {
758
+ console.warn('[plugins] fetch manifest failed', manifestResult.error);
759
+ } else {
760
+ console.warn('[plugins] manifest HTTP', manifestResult.status, manifestUrl);
761
+ }
762
+ if (shouldShowBootstrapSummary(opts)) {
763
+ console.info('[plugins] bootstrap_summary', { ok: false, reason: 'manifest_fetch' });
764
+ }
765
+ return
766
+ }
767
+ /** @type {{ hostPluginApiVersion?: string, plugins?: object[] }} */
768
+ const data = manifestResult.data;
769
+ if (!data) {
770
+ if (shouldShowBootstrapSummary(opts)) {
771
+ console.info('[plugins] bootstrap_summary', { ok: false, reason: 'manifest_empty_body' });
772
+ }
773
+ return
774
+ }
775
+
776
+ const apiVer = data.hostPluginApiVersion;
777
+ if (apiVer) {
778
+ const coerced = semver.coerce(apiVer);
779
+ const maj = coerced ? coerced.major : 0;
780
+ const range = `^${maj}.0.0`;
781
+ if (!semver.satisfies(HOST_PLUGIN_API_VERSION, range, { includePrerelease: true })) {
782
+ console.warn(
783
+ '[plugins] host API version mismatch: host implements',
784
+ HOST_PLUGIN_API_VERSION,
785
+ 'server declares',
786
+ apiVer
787
+ );
788
+ }
789
+ }
790
+
791
+ window.__PLUGIN_ACTIVATORS__ = window.__PLUGIN_ACTIVATORS__ || {};
792
+
793
+ const plugins = data.plugins || [];
794
+ if (plugins.length === 0) {
795
+ console.info(
796
+ '[plugins] 清单为空。请检查:① 后端清单服务(plugin-web-starter)是否已接入;② web-plugin.web-plugins-dir 是否指向含各插件子目录及 manifest.json 的路径;③ 浏览器直接访问',
797
+ manifestUrl,
798
+ '是否返回 plugins 条目。'
799
+ );
800
+ }
801
+
802
+ const summary = {
803
+ manifestCount: plugins.length,
804
+ activated: 0,
805
+ skipEngines: 0,
806
+ skipLoad: 0,
807
+ skipNoActivator: 0,
808
+ activateFail: 0
809
+ };
810
+
811
+ for (const p of plugins) {
812
+ const range = p.engines && p.engines.host;
813
+ if (range && !semver.satisfies(HOST_PLUGIN_API_VERSION, range, { includePrerelease: true })) {
814
+ console.warn('[plugins] skip (engines.host)', p.id, range);
815
+ summary.skipEngines++;
816
+ continue
817
+ }
818
+ const entryUrl = p.entryUrl;
819
+ try {
820
+ await loadPluginEntry(p, entryUrl, devMap, hostSet);
821
+ } catch (e) {
822
+ console.warn('[plugins] script load failed', p.id, e);
823
+ summary.skipLoad++;
824
+ continue
825
+ }
826
+ const activator = window.__PLUGIN_ACTIVATORS__[p.id];
827
+ if (typeof activator !== 'function') {
828
+ console.warn('[plugins] no activator for', p.id);
829
+ summary.skipNoActivator++;
830
+ continue
831
+ }
832
+ const hostApi = createHostApiFactory(p.id, router, hostKit);
833
+ try {
834
+ activator(hostApi);
835
+ summary.activated++;
836
+ } catch (e) {
837
+ console.error('[plugins] activate failed', p.id, e);
838
+ summary.activateFail++;
839
+ }
840
+ }
841
+
842
+ if (shouldShowBootstrapSummary(opts)) {
843
+ console.info('[plugins] bootstrap_summary', { ok: true, ...summary });
844
+ }
845
+ }
846
+
847
+ /**
848
+ * 插件通过宿主访问后端的受控通道:仅允许配置的前缀路径,默认 `/api/`;强制默认 `same-origin` 携带 Cookie。
849
+ * 前缀列表由 `createRequestBridge({ allowedPathPrefixes })` 传入,与 `defaultWebExtendPluginRuntime.bridgeAllowedPathPrefixes` 对齐。
850
+ *
851
+ * @module bridge
852
+ */
853
+
854
+ /**
855
+ * @param {string} p
856
+ */
857
+ function ensureLeadingSlash(p) {
858
+ const t = String(p || '').trim();
859
+ if (!t) {
860
+ return '/'
861
+ }
862
+ return t.startsWith('/') ? t : `/${t}`
863
+ }
864
+
865
+ /**
866
+ * @param {{ allowedPathPrefixes?: string[] }} [config]
867
+ */
868
+ function createRequestBridge(config = {}) {
869
+ const raw =
870
+ Array.isArray(config.allowedPathPrefixes) && config.allowedPathPrefixes.length > 0
871
+ ? config.allowedPathPrefixes
872
+ : defaultWebExtendPluginRuntime.bridgeAllowedPathPrefixes;
873
+ const allowedPathPrefixes = raw.map((p) => ensureLeadingSlash(p));
874
+
875
+ return {
876
+ /**
877
+ * 发起受控 `fetch`。
878
+ * @param {string} path 必须以 `/` 开头,且匹配某一 `allowedPathPrefixes` 前缀
879
+ * @param {RequestInit} [init] 会与默认 `{ credentials: 'same-origin' }` 合并(后者可被覆盖)
880
+ */
881
+ async request(path, init = {}) {
882
+ if (typeof path !== 'string' || !path.startsWith('/')) {
883
+ throw new Error('[bridge] path must be a string starting with /')
884
+ }
885
+ const allowed = allowedPathPrefixes.some((p) => path.startsWith(p));
886
+ if (!allowed) {
887
+ throw new Error('[bridge] path not allowed: ' + path)
888
+ }
889
+ return fetch(path, {
890
+ credentials: 'same-origin',
891
+ ...init
892
+ })
893
+ }
894
+ }
895
+ }
896
+
897
+ /**
898
+ * 宿主全局响应式注册表:菜单与扩展点槽位,供布局与 `ExtensionPoint` 订阅。
899
+ *
900
+ * @module registries
901
+ */
902
+
903
+ /**
904
+ * @type {{
905
+ * menus: object[],
906
+ * slots: Record<string, Array<{ pluginId: string, component: import('vue').Component, priority: number, key: string }>>
907
+ * }}
908
+ */
909
+ const registries = Vue__default.default.observable({
910
+ menus: [],
911
+ /** 扩展点 id → 已注册组件列表(内容区 / 工具栏等共用模型) */
912
+ slots: {},
913
+ /**
914
+ * 每次变更 slots 时递增,供 ExtensionPoint 计算属性显式依赖。
915
+ * Vue 2 对「先访问不存在的 slots[key]、后 Vue.set 补 key」的依赖收集不可靠,会导致扩展点不刷新。
916
+ */
917
+ slotRevision: 0
918
+ });
919
+
920
+ /**
921
+ * 按 `pluginId` 收集 `onTeardown` 回调,供 `disposeWebPlugin` 统一执行。
922
+ *
923
+ * @module teardown-registry
924
+ */
925
+
926
+ /** @type {Map<string, Function[]>} */
927
+ const byPlugin = new Map();
928
+
929
+ /**
930
+ * 登记插件卸载时要执行的同步回调(建议只做解绑、清定时器等轻量逻辑)。
931
+ * @param {string} pluginId
932
+ * @param {() => void} fn
933
+ */
934
+ function registerPluginTeardown(pluginId, fn) {
935
+ if (typeof fn !== 'function') {
936
+ return
937
+ }
938
+ let arr = byPlugin.get(pluginId);
939
+ if (!arr) {
940
+ arr = [];
941
+ byPlugin.set(pluginId, arr);
942
+ }
943
+ arr.push(fn);
944
+ }
945
+
946
+ /**
947
+ * 执行并清空该插件已登记的全部 teardown;调用后 Map 中不再保留该 id。
948
+ * @param {string} pluginId
949
+ */
950
+ function runPluginTeardowns(pluginId) {
951
+ const arr = byPlugin.get(pluginId);
952
+ if (!arr) {
953
+ return
954
+ }
955
+ byPlugin.delete(pluginId);
956
+ for (const fn of arr) {
957
+ try {
958
+ fn();
959
+ } catch (e) {
960
+ console.warn('[plugins] teardown failed', pluginId, e);
961
+ }
962
+ }
963
+ }
964
+
965
+ /**
966
+ * 构造供插件 activator 调用的宿主 API(路由、菜单、扩展点、资源注入等)。
967
+ * 与打包工具无关;Webpack 宿主需已配置 `vue-loader` 以编译本包内的 `.vue` 依赖。
968
+ *
969
+ * @module createHostApi
970
+ */
971
+
972
+ /** 扩展点列表项 key 递增,避免用 Date.now() 导致列表重排时误卸载组件 */
973
+ let slotItemKeySeq = 0;
974
+ /** 无 name 的动态路由合成名递增,避免多次 registerRoutes 重名 */
975
+ let routeSynthSeq = 0;
976
+
977
+ /**
978
+ * @typedef {object} RegisterSlotEntry
979
+ * @property {import('vue').Component} component
980
+ * @property {number} [priority]
981
+ */
982
+
983
+ /**
984
+ * @typedef {object} HostApi
985
+ * @property {string} hostPluginApiVersion 宿主实现的协议版本
986
+ * @property {(routes: import('vue-router').RouteConfig[]) => void} registerRoutes 注册路由;无 `name` 时自动生成稳定合成名;优先 `router.addRoute`
987
+ * @property {(items: object[]) => void} registerMenuItems 注册菜单并按 `order` 排序
988
+ * @property {(pointId: string, components: RegisterSlotEntry[]) => void} registerSlotComponents 向扩展点挂载组件
989
+ * @property {(urls?: string[]) => void} registerStylesheetUrls 注入 `link[rel=stylesheet]`,带 `data-plugin-asset`
990
+ * @property {(urls?: string[]) => void} registerScriptUrls 顺序注入外链脚本
991
+ * @property {() => void} registerSanitizedHtmlSnippet MVP 未实现,调用即抛错
992
+ * @property {() => ReturnType<typeof createRequestBridge>} getBridge 受控 `fetch` 代理
993
+ * @property {(pluginId: string, fn: () => void) => void} onTeardown 注册卸载回调;由 `disposeWebPlugin(pluginId)` 触发
994
+ */
995
+
996
+ /**
997
+ * @typedef {object} HostKitOptions
998
+ * @property {string[]} [bridgeAllowedPathPrefixes] 覆盖 `getBridge().request` 允许的 URL 前缀;默认见 `defaultWebExtendPluginRuntime.bridgeAllowedPathPrefixes`
999
+ */
1000
+
1001
+ /**
1002
+ * 创建单个插件在宿主侧的 API 句柄,传入插件 `activator(hostApi)`。
1003
+ *
1004
+ * `bootstrapPlugins` 始终以 `(pluginId, router, hostKitOptions)` 调用工厂;请使用 `(id, r, kit) => createHostApi(id, r, kit)` 传入 `bridgeAllowedPathPrefixes`。
1005
+ * 单参工厂 `(id) => createHostApi(id, router)` 仍可用(忽略后两个实参),此时 bridge 仅用包内默认前缀。
1006
+ *
1007
+ * @param {string} pluginId 与 manifest.id 一致
1008
+ * @param {import('vue-router').default} router 宿主 Vue Router 实例(vue-router@3)
1009
+ * @param {HostKitOptions} [hostKitOptions]
1010
+ * @returns {HostApi}
1011
+ */
1012
+ function createHostApi(pluginId, router, hostKitOptions = {}) {
1013
+ const bridgePrefixes =
1014
+ Array.isArray(hostKitOptions.bridgeAllowedPathPrefixes) &&
1015
+ hostKitOptions.bridgeAllowedPathPrefixes.length > 0
1016
+ ? hostKitOptions.bridgeAllowedPathPrefixes
1017
+ : defaultWebExtendPluginRuntime.bridgeAllowedPathPrefixes;
1018
+ const bridge = createRequestBridge({ allowedPathPrefixes: bridgePrefixes });
1019
+
1020
+ /**
1021
+ * 注入样式表;`disposeWebPlugin` 会按 `data-plugin-asset` 移除对应节点。
1022
+ * @param {string} href
1023
+ */
1024
+ function injectStylesheet(href) {
1025
+ const link = document.createElement('link');
1026
+ link.rel = 'stylesheet';
1027
+ link.href = href;
1028
+ link.setAttribute('data-plugin-asset', pluginId);
1029
+ document.head.appendChild(link);
1030
+ }
1031
+
1032
+ /**
1033
+ * 注入外链脚本(用于插件额外资源,非清单主入口)。
1034
+ * @param {string} src
1035
+ * @returns {Promise<void>}
1036
+ */
1037
+ function injectScript(src) {
1038
+ return new Promise((resolve, reject) => {
1039
+ const s = document.createElement('script');
1040
+ s.async = true;
1041
+ s.src = src;
1042
+ s.setAttribute('data-plugin-asset', pluginId);
1043
+ s.onload = () => resolve();
1044
+ s.onerror = () => reject(new Error('script failed: ' + src));
1045
+ document.head.appendChild(s);
1046
+ })
1047
+ }
1048
+
1049
+ return {
1050
+ hostPluginApiVersion: HOST_PLUGIN_API_VERSION,
1051
+
1052
+ /**
1053
+ * 动态注册路由。Vue Router 3.5+ 推荐 `addRoute`;若不存在则回退已弃用的 `addRoutes`。
1054
+ * @param {import('vue-router').RouteConfig[]} routes
1055
+ */
1056
+ registerRoutes(routes) {
1057
+ const wrapped = routes.map((r) => ({
1058
+ ...r,
1059
+ name: r.name || `__wep_${pluginId}_${routeSynthSeq++}`,
1060
+ meta: { ...(r.meta || {}), pluginId }
1061
+ }));
1062
+ if (typeof router.addRoute === 'function') {
1063
+ for (const r of wrapped) {
1064
+ router.addRoute(r);
1065
+ }
1066
+ } else {
1067
+ router.addRoutes(wrapped);
1068
+ }
1069
+ },
1070
+
1071
+ /**
1072
+ * 写入全局菜单注册表(响应式);按 `order` 升序排列。
1073
+ * @param {object[]} items
1074
+ */
1075
+ registerMenuItems(items) {
1076
+ for (const item of items) {
1077
+ registries.menus.push({ ...item, pluginId });
1078
+ }
1079
+ registries.menus.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
1080
+ },
1081
+
1082
+ /**
1083
+ * 向指定扩展点 id 注册 Vue 组件;`ExtensionPoint` 按 `priority` 降序渲染。
1084
+ * @param {string} pointId
1085
+ * @param {RegisterSlotEntry[]} components
1086
+ */
1087
+ registerSlotComponents(pointId, components) {
1088
+ if (!pointId) {
1089
+ return
1090
+ }
1091
+ if (!registries.slots[pointId]) {
1092
+ Vue__default.default.set(registries.slots, pointId, []);
1093
+ }
1094
+ const list = registries.slots[pointId];
1095
+ for (const c of components) {
1096
+ list.push({
1097
+ pluginId,
1098
+ component: c.component,
1099
+ priority: c.priority ?? 0,
1100
+ key: `${pluginId}-${pointId}-${++slotItemKeySeq}`
1101
+ });
1102
+ }
1103
+ list.sort((a, b) => b.priority - a.priority);
1104
+ registries.slotRevision++;
1105
+ },
1106
+
1107
+ /**
1108
+ * @param {string[]|undefined} urls
1109
+ */
1110
+ registerStylesheetUrls(urls) {
1111
+ for (const u of urls || []) {
1112
+ if (typeof u === 'string' && u) {
1113
+ injectStylesheet(u);
1114
+ }
1115
+ }
1116
+ },
1117
+
1118
+ /**
1119
+ * 串行加载多个脚本,失败仅告警不中断宿主。
1120
+ * @param {string[]|undefined} urls
1121
+ */
1122
+ registerScriptUrls(urls) {
1123
+ const chain = (urls || []).filter((u) => typeof u === 'string' && u).reduce(
1124
+ (p, u) => p.then(() => injectScript(u)),
1125
+ Promise.resolve()
1126
+ );
1127
+ chain.catch((e) => console.warn('[plugins] registerScriptUrls', pluginId, e));
1128
+ },
1129
+
1130
+ registerSanitizedHtmlSnippet() {
1131
+ throw new Error('registerSanitizedHtmlSnippet is not enabled in MVP')
1132
+ },
1133
+
1134
+ getBridge: () => bridge,
1135
+
1136
+ /**
1137
+ * 插件卸载前清理逻辑;第一个参数为预留与协议对齐,实际以创建 API 时的 `pluginId` 为准。
1138
+ * @param {string} _pluginId 预留,与 manifest.id 一致时可传入
1139
+ * @param {() => void} fn
1140
+ */
1141
+ onTeardown(_pluginId, fn) {
1142
+ if (typeof fn === 'function') {
1143
+ registerPluginTeardown(pluginId, fn);
1144
+ }
1145
+ }
1146
+ }
1147
+ }
1148
+
1149
+ /**
1150
+ * 单插件卸载:与 `bootstrapPlugins` / `createHostApi` 对称,清理注册表与 DOM 副作用。
1151
+ *
1152
+ * @module dispose-plugin
1153
+ */
1154
+
1155
+ /**
1156
+ * 卸载指定 id 的插件:依次执行 teardown、移除菜单与扩展点条目、删除 activator、移除带 `data-plugin-asset` 的节点。
1157
+ *
1158
+ * **路由**:Vue Router 3 无公开 `removeRoute`,此处不改动 matcher;动态路由需整页刷新或自行维护路由表。
1159
+ *
1160
+ * @param {string} pluginId 与 manifest.id 一致
1161
+ */
1162
+ function disposeWebPlugin(pluginId) {
1163
+ if (!pluginId || typeof pluginId !== 'string') {
1164
+ return
1165
+ }
1166
+
1167
+ runPluginTeardowns(pluginId);
1168
+
1169
+ for (let i = registries.menus.length - 1; i >= 0; i--) {
1170
+ if (registries.menus[i].pluginId === pluginId) {
1171
+ registries.menus.splice(i, 1);
1172
+ }
1173
+ }
1174
+
1175
+ const slots = registries.slots;
1176
+ for (const pointId of Object.keys(slots)) {
1177
+ const list = slots[pointId];
1178
+ if (!Array.isArray(list)) {
1179
+ continue
1180
+ }
1181
+ const next = list.filter((x) => x.pluginId !== pluginId);
1182
+ if (next.length === 0) {
1183
+ Vue__default.default.delete(slots, pointId);
1184
+ } else if (next.length !== list.length) {
1185
+ Vue__default.default.set(slots, pointId, next);
1186
+ }
1187
+ }
1188
+ registries.slotRevision++;
1189
+
1190
+ if (typeof window !== 'undefined' && window.__PLUGIN_ACTIVATORS__) {
1191
+ delete window.__PLUGIN_ACTIVATORS__[pluginId];
1192
+ }
1193
+
1194
+ if (typeof document !== 'undefined') {
1195
+ document.querySelectorAll('[data-plugin-asset]').forEach((el) => {
1196
+ if (el.getAttribute('data-plugin-asset') === pluginId) {
1197
+ el.remove();
1198
+ }
1199
+ });
1200
+ }
1201
+ }
1202
+
1203
+ /**
1204
+ * 在宿主布局中声明扩展点;插件通过 `hostApi.registerSlotComponents(pointId, ...)` 注入组件。
1205
+ * 使用纯 render 函数,便于 Rollup 发布 dist,宿主无需再转译 .vue。
1206
+ */
1207
+
1208
+ const SlotErrorBoundary = {
1209
+ name: 'SlotErrorBoundary',
1210
+ props: { label: String },
1211
+ data() {
1212
+ return { error: null }
1213
+ },
1214
+ errorCaptured(err) {
1215
+ this.error = err && err.message ? err.message : String(err);
1216
+ console.error('[ExtensionPoint] render error in', this.label, err);
1217
+ return false
1218
+ },
1219
+ render(h) {
1220
+ if (this.error) {
1221
+ return h(
1222
+ 'div',
1223
+ { class: 'plugin-point-error', style: { color: '#c00', fontSize: '12px' } },
1224
+ `[插件 ${this.label}] 渲染失败`
1225
+ )
1226
+ }
1227
+ const d = this.$slots.default;
1228
+ return d && d[0] ? d[0] : h('span')
1229
+ }
1230
+ };
1231
+
1232
+ var ExtensionPoint = {
1233
+ name: 'ExtensionPoint',
1234
+ components: { SlotErrorBoundary },
1235
+ props: {
1236
+ pointId: { type: String, required: true },
1237
+ slotProps: { type: Object, default: () => ({}) }
1238
+ },
1239
+ computed: {
1240
+ items() {
1241
+ void registries.slotRevision;
1242
+ return registries.slots[this.pointId] || []
1243
+ },
1244
+ forwardProps() {
1245
+ return this.slotProps || {}
1246
+ }
1247
+ },
1248
+ render(h) {
1249
+ return h(
1250
+ 'div',
1251
+ {
1252
+ class: 'extension-point',
1253
+ style: { minHeight: '8px' },
1254
+ attrs: { 'data-point-id': this.pointId }
1255
+ },
1256
+ this.items.map((item) =>
1257
+ h(
1258
+ SlotErrorBoundary,
1259
+ {
1260
+ key: item.key,
1261
+ props: { label: item.pluginId }
1262
+ },
1263
+ [h(item.component, { props: this.forwardProps })]
1264
+ )
1265
+ )
1266
+ )
1267
+ }
1268
+ };
1269
+
1270
+ /**
1271
+ * 一键接入:注册 `ExtensionPoint` 并执行 `bootstrapPlugins`。
1272
+ * @module install
1273
+ */
1274
+
1275
+ /**
1276
+ * 注册全局组件 `ExtensionPoint` 并异步引导插件清单。
1277
+ *
1278
+ * @param {*} Vue
1279
+ * @param {*} router vue-router 实例
1280
+ * @param {Record<string, unknown>} [options] 传给 `resolveRuntimeOptions` 的字段;可含 `env`(Vite 传入 `import.meta.env`)以读取 `VITE_*`。
1281
+ * @returns {Promise<void>}
1282
+ */
1283
+ function installWebExtendPluginVue2(Vue, router, options) {
1284
+ const opts = options || {};
1285
+ const { env: injectedEnv, ...runtimeUser } = opts;
1286
+ if (injectedEnv && typeof injectedEnv === 'object') {
1287
+ setWebExtendPluginEnv(injectedEnv);
1288
+ }
1289
+ if (Vue && ExtensionPoint) {
1290
+ Vue.component('ExtensionPoint', ExtensionPoint);
1291
+ }
1292
+ const runtime = resolveRuntimeOptions(runtimeUser);
1293
+ return bootstrapPlugins(router, (id, r, kit) => createHostApi(id, r, kit), runtime)
1294
+ }
1295
+
1296
+ exports.ExtensionPoint = ExtensionPoint;
1297
+ exports.HOST_PLUGIN_API_VERSION = HOST_PLUGIN_API_VERSION;
1298
+ exports.bootstrapPlugins = bootstrapPlugins;
1299
+ exports.createHostApi = createHostApi;
1300
+ exports.createRequestBridge = createRequestBridge;
1301
+ exports.defaultWebExtendPluginRuntime = defaultWebExtendPluginRuntime;
1302
+ exports.disposeWebPlugin = disposeWebPlugin;
1303
+ exports.installWebExtendPluginVue2 = installWebExtendPluginVue2;
1304
+ exports.registries = registries;
1305
+ exports.resolveRuntimeOptions = resolveRuntimeOptions;
1306
+ exports.setWebExtendPluginEnv = setWebExtendPluginEnv;
1307
+ //# sourceMappingURL=index.cjs.map