web-extend-plugin-vue2 0.2.1 → 0.2.3

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,64 +1,380 @@
1
1
  import Vue from 'vue';
2
2
 
3
3
  /**
4
- * 宿主可覆盖的默认运行时配置(路径、白名单、超时等)。
5
- * 使用方式:`resolveRuntimeOptions({ ...defaultWebExtendPluginRuntime, manifestListPath: '/api/my-plugins' })`
6
- * 或只传需要改动的字段:`resolveRuntimeOptions({ manifestListPath: '/api/my-plugins' })`。
7
- *
8
- * @module default-runtime-config
4
+ * `resolveRuntimeOptions` 的默认值来源;宿主可只覆盖部分字段。
9
5
  */
10
-
11
- /**
12
- * @typedef {typeof defaultWebExtendPluginRuntime} WebExtendPluginDefaultRuntime
13
- */
14
-
15
6
  const defaultWebExtendPluginRuntime = {
16
- /** 清单 HTTP 服务前缀(与后端 context-path 对齐),不含尾部 `/` */
17
7
  manifestBase: '/fp-api',
18
-
19
- /**
20
- * 拉取插件清单的 **路径段**(以 `/` 开头),拼在 `manifestBase` 之后。
21
- * 完整 URL:`{manifestBase}{manifestListPath}` → 默认 `/fp-api/api/frontend-plugins`
22
- */
23
8
  manifestListPath: '/api/frontend-plugins',
24
-
25
- /** 清单 `fetch` 的 `credentials`,需 Cookie 会话时用 `include` */
26
9
  manifestFetchCredentials: 'include',
27
-
28
- /** 插件 dev 服务存活探测路径(拼在 `webPluginDevOrigin` 后) */
29
10
  devPingPath: '/__web_plugin_dev_ping',
30
-
31
- /** 插件 dev 热更新 SSE 路径(拼在插件 dev 的 origin 后) */
32
11
  devReloadSsePath: '/__web_plugin_reload_stream',
33
-
34
- /** 隐式 dev 映射里,每个 id 对应的入口路径(相对插件 dev origin) */
35
12
  webPluginDevEntryPath: '/src/plugin-entry.js',
36
-
37
- /** `fetch(devPingUrl)` 超时毫秒数 */
38
13
  devPingTimeoutMs: 500,
39
-
40
- /**
41
- * **隐式 dev 映射**(可选):开发模式下若 `webPluginDevOrigin` 上 ping 成功,运行时会为若干插件 id 自动生成
42
- * `{ id → origin + webPluginDevEntryPath }`,从而用 dev 服务入口替代清单里的 dist,便于热更新联调。
43
- *
44
- * 插件 id 来源优先级:`webPluginDevIds`(或 `VITE_WEB_PLUGIN_DEV_IDS`)→ 本字段。**默认 `[]`**,
45
- * 避免把仓库示例 id 写进通用宿主;联调时在 `.env` 里设 `VITE_WEB_PLUGIN_DEV_IDS=com.xxx,com.yyy`,
46
- * 或 `resolveRuntimeOptions({ defaultImplicitDevPluginIds: ['com.xxx'] })` 即可。
47
- */
48
14
  defaultImplicitDevPluginIds: [],
49
-
50
- /**
51
- * 允许通过 `<script>` / 动态 `import()` 加载的插件脚本所在主机名(小写),防误配公网 URL。
52
- */
53
15
  allowedScriptHosts: ['localhost', '127.0.0.1', '::1'],
54
-
55
- /**
56
- * `hostApi.getBridge().request(path)` 允许的 path 前缀;须以 `/` 开头。
57
- * 需与后端实际 API 前缀一致。
58
- */
59
16
  bridgeAllowedPathPrefixes: ['/api/']
60
17
  };
61
18
 
19
+ /**
20
+ * 与具体打包器解耦的运行时环境读取:优先显式注入,其次 `globalThis.__WEP_ENV__`,再读 `process.env`。
21
+ * Vite 宿主建议在入口调用 `setWebExtendPluginEnv(import.meta.env)`。
22
+ */
23
+
24
+ /** @type {Record<string, unknown> | null} */
25
+ let _injected = null;
26
+
27
+ /**
28
+ * 注入与 `import.meta.env` 同形态的对象(键如 `VITE_*`、`DEV`)。
29
+ * @param {Record<string, unknown> | null | undefined} env
30
+ */
31
+ function setWebExtendPluginEnv(env) {
32
+ _injected = env && typeof env === 'object' ? env : null;
33
+ }
34
+
35
+ /**
36
+ * @returns {Record<string, unknown> | null}
37
+ */
38
+ function getEnvObject() {
39
+ if (_injected) {
40
+ return _injected
41
+ }
42
+ try {
43
+ const g = typeof globalThis !== 'undefined' ? globalThis : undefined;
44
+ if (g && g.__WEP_ENV__ && typeof g.__WEP_ENV__ === 'object') {
45
+ return g.__WEP_ENV__
46
+ }
47
+ } catch (_) {
48
+ /* ignore */
49
+ }
50
+ return null
51
+ }
52
+
53
+ /**
54
+ * 读取注入环境中的字符串配置(`VITE_*` / `PLUGIN_*` 等价键由调用方传入)。
55
+ * @param {string} key
56
+ * @returns {string|undefined}
57
+ */
58
+ function readInjectedEnvKey(key) {
59
+ const o = getEnvObject();
60
+ if (!o || !(key in o)) {
61
+ return undefined
62
+ }
63
+ const v = o[key];
64
+ if (v === undefined || v === '') {
65
+ return undefined
66
+ }
67
+ return String(v)
68
+ }
69
+
70
+ /** @returns {boolean} */
71
+ function readInjectedEnvDev() {
72
+ const o = getEnvObject();
73
+ return !!(o && o.DEV === true)
74
+ }
75
+
76
+ /**
77
+ * 从注入环境与 `process.env` 解析 `VITE_*` / `PLUGIN_*` 键。
78
+ */
79
+
80
+ /**
81
+ * @param {string} key
82
+ * @returns {string|undefined}
83
+ */
84
+ function readProcessEnv(key) {
85
+ try {
86
+ if (typeof process !== 'undefined' && process.env && key in process.env) {
87
+ const v = process.env[key];
88
+ if (v !== undefined && v !== '') {
89
+ return String(v)
90
+ }
91
+ }
92
+ } catch (_) {
93
+ /* ignore */
94
+ }
95
+ return undefined
96
+ }
97
+
98
+ /**
99
+ * @param {string} viteStyleKey
100
+ * @returns {string|null}
101
+ */
102
+ function viteKeyToPluginAlternate(viteStyleKey) {
103
+ if (typeof viteStyleKey !== 'string' || !viteStyleKey.startsWith('VITE_')) {
104
+ return null
105
+ }
106
+ return `PLUGIN_${viteStyleKey.slice(5)}`
107
+ }
108
+
109
+ /**
110
+ * @param {string} key
111
+ * @param {string} [fallback='']
112
+ */
113
+ function resolveBundledEnv(key, fallback = '') {
114
+ const alt = viteKeyToPluginAlternate(key);
115
+ const inj = readInjectedEnvKey(key);
116
+ const fromInjected =
117
+ inj === undefined || inj === null ? (alt ? readInjectedEnvKey(alt) : undefined) : inj;
118
+ const proc = readProcessEnv(key);
119
+ const fromProcess =
120
+ proc === undefined || proc === null ? (alt ? readProcessEnv(alt) : undefined) : proc;
121
+ const first = fromInjected === undefined || fromInjected === null ? fromProcess : fromInjected;
122
+ return first === undefined || first === null ? fallback : first
123
+ }
124
+
125
+ /** @returns {boolean} */
126
+ function resolveBundledIsDev() {
127
+ try {
128
+ if (readInjectedEnvDev()) {
129
+ return true
130
+ }
131
+ } catch (_) {
132
+ /* ignore */
133
+ }
134
+ try {
135
+ if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'development') {
136
+ return true
137
+ }
138
+ } catch (_) {
139
+ /* ignore */
140
+ }
141
+ return false
142
+ }
143
+
144
+ /**
145
+ * 路径与脚本主机名校验工具。
146
+ * @module runtime/path-host-utils
147
+ */
148
+
149
+ /**
150
+ * @param {string} p
151
+ */
152
+ function ensureLeadingPath(p) {
153
+ const t = String(p || '').trim();
154
+ if (!t) {
155
+ return '/'
156
+ }
157
+ return t.startsWith('/') ? t : `/${t}`
158
+ }
159
+
160
+ /**
161
+ * @param {string} hostname
162
+ */
163
+ function normalizeHost(hostname) {
164
+ if (!hostname) {
165
+ return ''
166
+ }
167
+ const h = hostname.toLowerCase();
168
+ if (h.startsWith('[') && h.endsWith(']')) {
169
+ return h.slice(1, -1)
170
+ }
171
+ return h
172
+ }
173
+
174
+ /**
175
+ * @param {string[]} hostnames
176
+ * @returns {Set<string>}
177
+ */
178
+ function buildAllowedScriptHostsSet(hostnames) {
179
+ const s = new Set();
180
+ for (const h of hostnames) {
181
+ const n = normalizeHost(h);
182
+ if (n) {
183
+ s.add(n);
184
+ }
185
+ }
186
+ return s
187
+ }
188
+
189
+ /**
190
+ * @param {string} url
191
+ * @param {Set<string>} hostSet
192
+ */
193
+ function isScriptHostAllowed(url, hostSet) {
194
+ if (typeof window === 'undefined') {
195
+ return false
196
+ }
197
+ try {
198
+ const u = new URL(url, window.location.origin);
199
+ const h = normalizeHost(u.hostname);
200
+ return hostSet.has(h)
201
+ } catch {
202
+ return false
203
+ }
204
+ }
205
+
206
+ /**
207
+ * 合并用户、环境与默认配置得到运行时选项。
208
+ * @module runtime/resolve-runtime-options
209
+ */
210
+
211
+
212
+ const DEF = defaultWebExtendPluginRuntime;
213
+
214
+ /**
215
+ * @param {string|undefined} userVal
216
+ * @param {string} envKey
217
+ * @param {RequestCredentials} fallback
218
+ */
219
+ function resolveManifestCredentials(userVal, envKey, fallback) {
220
+ if (userVal !== undefined && userVal !== '') {
221
+ const s = String(userVal);
222
+ if (s === 'include' || s === 'omit' || s === 'same-origin') {
223
+ return s
224
+ }
225
+ }
226
+ const e = resolveBundledEnv(envKey, '');
227
+ if (e === 'include' || e === 'omit' || e === 'same-origin') {
228
+ return e
229
+ }
230
+ return fallback
231
+ }
232
+
233
+ /**
234
+ * @param {number|undefined} userVal
235
+ * @param {string} envKey
236
+ * @param {number} fallback
237
+ */
238
+ function resolvePositiveInt(userVal, envKey, fallback) {
239
+ if (typeof userVal === 'number' && Number.isFinite(userVal) && userVal > 0) {
240
+ return Math.floor(userVal)
241
+ }
242
+ const raw = resolveBundledEnv(envKey, '');
243
+ const n = raw ? parseInt(raw, 10) : NaN;
244
+ if (Number.isFinite(n) && n > 0) {
245
+ return n
246
+ }
247
+ return fallback
248
+ }
249
+
250
+ /**
251
+ * 合并用户、环境变量与 `defaultWebExtendPluginRuntime`,得到完整运行时选项(宿主可只传需要覆盖的字段)。
252
+ * @param {WebExtendPluginRuntimeOptions} [user]
253
+ * @returns {object}
254
+ */
255
+ function resolveRuntimeOptions$1(user = {}) {
256
+ const manifestBaseRaw =
257
+ user.manifestBase !== undefined && user.manifestBase !== ''
258
+ ? String(user.manifestBase)
259
+ : resolveBundledEnv('VITE_FRONTEND_PLUGIN_BASE', DEF.manifestBase) || DEF.manifestBase;
260
+
261
+ const manifestListPath = ensureLeadingPath(
262
+ user.manifestListPath !== undefined && user.manifestListPath !== ''
263
+ ? user.manifestListPath
264
+ : resolveBundledEnv('VITE_WEB_PLUGIN_MANIFEST_PATH', DEF.manifestListPath)
265
+ );
266
+
267
+ const defaultImplicitDevPluginIds = Array.isArray(user.defaultImplicitDevPluginIds)
268
+ ? user.defaultImplicitDevPluginIds.map(String).filter(Boolean)
269
+ : (() => {
270
+ const e = resolveBundledEnv('VITE_WEB_PLUGIN_IMPLICIT_DEV_IDS', '');
271
+ if (e) {
272
+ return e
273
+ .split(',')
274
+ .map((s) => s.trim())
275
+ .filter(Boolean)
276
+ }
277
+ return [...DEF.defaultImplicitDevPluginIds]
278
+ })();
279
+
280
+ const allowedScriptHosts =
281
+ Array.isArray(user.allowedScriptHosts) && user.allowedScriptHosts.length > 0
282
+ ? user.allowedScriptHosts.map((h) => normalizeHost(String(h))).filter(Boolean)
283
+ : (() => {
284
+ const e = resolveBundledEnv('VITE_WEB_PLUGIN_ALLOWED_SCRIPT_HOSTS', '');
285
+ if (e) {
286
+ return e
287
+ .split(',')
288
+ .map((s) => normalizeHost(s.trim()))
289
+ .filter(Boolean)
290
+ }
291
+ return [...DEF.allowedScriptHosts]
292
+ })();
293
+
294
+ const bridgeAllowedPathPrefixes =
295
+ Array.isArray(user.bridgeAllowedPathPrefixes) && user.bridgeAllowedPathPrefixes.length > 0
296
+ ? user.bridgeAllowedPathPrefixes.map((p) => ensureLeadingPath(p)).filter(Boolean)
297
+ : (() => {
298
+ const e = resolveBundledEnv('VITE_WEB_PLUGIN_BRIDGE_PREFIXES', '');
299
+ if (e) {
300
+ return e
301
+ .split(',')
302
+ .map((s) => ensureLeadingPath(s.trim()))
303
+ .filter(Boolean)
304
+ }
305
+ return [...DEF.bridgeAllowedPathPrefixes]
306
+ })();
307
+
308
+ return {
309
+ manifestBase: manifestBaseRaw.replace(/\/$/, '') || DEF.manifestBase.replace(/\/$/, ''),
310
+ manifestListPath,
311
+ manifestFetchCredentials: resolveManifestCredentials(
312
+ user.manifestFetchCredentials,
313
+ 'VITE_WEB_PLUGIN_MANIFEST_CREDENTIALS',
314
+ DEF.manifestFetchCredentials
315
+ ),
316
+ isDev: user.isDev !== undefined ? user.isDev : resolveBundledIsDev(),
317
+ webPluginDevOrigin:
318
+ user.webPluginDevOrigin !== undefined ? user.webPluginDevOrigin : resolveBundledEnv('VITE_WEB_PLUGIN_DEV_ORIGIN', ''),
319
+ webPluginDevIds:
320
+ user.webPluginDevIds !== undefined ? user.webPluginDevIds : resolveBundledEnv('VITE_WEB_PLUGIN_DEV_IDS', ''),
321
+ webPluginDevMapJson:
322
+ user.webPluginDevMapJson !== undefined
323
+ ? user.webPluginDevMapJson
324
+ : resolveBundledEnv('VITE_WEB_PLUGIN_DEV_MAP', ''),
325
+ webPluginDevEntryPath: ensureLeadingPath(
326
+ user.webPluginDevEntryPath !== undefined && user.webPluginDevEntryPath !== ''
327
+ ? user.webPluginDevEntryPath
328
+ : resolveBundledEnv('VITE_WEB_PLUGIN_DEV_ENTRY', DEF.webPluginDevEntryPath)
329
+ ),
330
+ devPingPath: ensureLeadingPath(
331
+ user.devPingPath !== undefined && user.devPingPath !== ''
332
+ ? user.devPingPath
333
+ : resolveBundledEnv('VITE_WEB_PLUGIN_DEV_PING_PATH', DEF.devPingPath)
334
+ ),
335
+ devReloadSsePath: ensureLeadingPath(
336
+ user.devReloadSsePath !== undefined && user.devReloadSsePath !== ''
337
+ ? user.devReloadSsePath
338
+ : resolveBundledEnv('VITE_WEB_PLUGIN_DEV_SSE_PATH', DEF.devReloadSsePath)
339
+ ),
340
+ devPingTimeoutMs: resolvePositiveInt(user.devPingTimeoutMs, 'VITE_WEB_PLUGIN_DEV_PING_TIMEOUT_MS', DEF.devPingTimeoutMs),
341
+ defaultImplicitDevPluginIds,
342
+ allowedScriptHosts,
343
+ bridgeAllowedPathPrefixes,
344
+ bootstrapSummary: user.bootstrapSummary,
345
+ pluginRoutesParentName:
346
+ user.pluginRoutesParentName !== undefined && String(user.pluginRoutesParentName).trim() !== ''
347
+ ? String(user.pluginRoutesParentName).trim()
348
+ : '',
349
+ ...(typeof user.fetchManifest === 'function' ? { fetchManifest: user.fetchManifest } : {}),
350
+ ...(typeof user.transformRoutes === 'function' ? { transformRoutes: user.transformRoutes } : {}),
351
+ ...(typeof user.interceptRegisterRoutes === 'function'
352
+ ? { interceptRegisterRoutes: user.interceptRegisterRoutes }
353
+ : {}),
354
+ ...(typeof user.adaptRouteDeclarations === 'function'
355
+ ? { adaptRouteDeclarations: user.adaptRouteDeclarations }
356
+ : {})
357
+ }
358
+ }
359
+
360
+ /**
361
+ * 未配置 `fetchManifest` 时使用的清单 `fetch` 实现。
362
+ * @param {{ manifestUrl: string, credentials: RequestCredentials }} ctx
363
+ */
364
+ async function defaultFetchWebPluginManifest$1(ctx) {
365
+ const { manifestUrl, credentials } = ctx;
366
+ try {
367
+ const res = await fetch(manifestUrl, { credentials });
368
+ if (!res.ok) {
369
+ return { ok: false, status: res.status, data: null }
370
+ }
371
+ const data = await res.json();
372
+ return { ok: true, data }
373
+ } catch (e) {
374
+ return { ok: false, error: e, data: null }
375
+ }
376
+ }
377
+
62
378
  function getDefaultExportFromCjs (x) {
63
379
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
64
380
  }
@@ -2784,317 +3100,292 @@ function requireSemver () {
2784
3100
  var semverExports = requireSemver();
2785
3101
  var semver = /*@__PURE__*/getDefaultExportFromCjs(semverExports);
2786
3102
 
2787
- /**
2788
- * 不依赖 `import.meta`,兼容 Webpack 4/5、Vue CLI、Vite、Rspack 等宿主。
2789
- * - Webpack / Vue CLI:依赖构建时注入的 `process.env`(如 `VUE_APP_*`、`DefinePlugin` 注入的 `VITE_*`)。
2790
- * - Vite:在入口调用 `setWebExtendPluginEnv(import.meta.env)`,或 `installWebExtendPluginVue2(..., { env: import.meta.env })`。
2791
- * - 也可在入口前设置 `globalThis.__WEP_ENV__ = import.meta.env`(与 setWebExtendPluginEnv 二选一即可)。
2792
- *
2793
- * @module bundled-env
2794
- */
2795
-
2796
- /** @type {Record<string, unknown> | null} */
2797
- let _explicitEnv = null;
3103
+ /** 与清单服务 `hostPluginApiVersion` 对齐,用于 `semver` 校验。 */
3104
+ const HOST_PLUGIN_API_VERSION = '1.0.0';
3105
+
3106
+ /** 控制台日志与横幅使用的短名称。 */
3107
+ const RUNTIME_CONSOLE_LABEL = 'web-extend-plugin-vue2';
2798
3108
 
2799
3109
  /**
2800
- * 显式注入与 `import.meta.env` 同形态的对象(推荐 Vite 宿主在入口调用一次)。
2801
- * @param {Record<string, unknown> | null | undefined} env
3110
+ * 开发模式插件 URL 映射(显式 JSON + 隐式 dev 探测)。
3111
+ * @module runtime/dev-map
2802
3112
  */
2803
- function setWebExtendPluginEnv(env) {
2804
- _explicitEnv = env && typeof env === 'object' ? env : null;
2805
- }
2806
3113
 
2807
3114
  /**
2808
- * @returns {Record<string, unknown> | null}
3115
+ * @param {{ isDev: boolean, webPluginDevMapJson?: string|null }} opts
2809
3116
  */
2810
- function getInjectedEnvObject() {
2811
- if (_explicitEnv) {
2812
- return _explicitEnv
3117
+ function parseWebPluginDevMapExplicit(opts) {
3118
+ if (!opts.isDev) {
3119
+ return null
2813
3120
  }
2814
- try {
2815
- const g = typeof globalThis !== 'undefined' ? globalThis : undefined;
2816
- if (g && g.__WEP_ENV__ && typeof g.__WEP_ENV__ === 'object') {
2817
- return g.__WEP_ENV__
2818
- }
2819
- } catch (_) {}
2820
- return null
2821
- }
2822
-
2823
- /**
2824
- * 从注入环境读取字符串配置键(`VITE_*` / `PLUGIN_*` 等)。
2825
- * @param {string} key
2826
- * @returns {string|undefined}
2827
- */
2828
- function readInjectedEnvKey(key) {
2829
- const o = getInjectedEnvObject();
2830
- if (!o || !(key in o)) {
2831
- return undefined
3121
+ const raw = opts.webPluginDevMapJson;
3122
+ if (raw === undefined || raw === null || String(raw).trim() === '') {
3123
+ return null
2832
3124
  }
2833
- const v = o[key];
2834
- if (v === undefined || v === '') {
2835
- return undefined
3125
+ try {
3126
+ const map = JSON.parse(String(raw));
3127
+ return map && typeof map === 'object' ? map : null
3128
+ } catch {
3129
+ console.warn('[wep] invalid webPluginDevMapJson / VITE_WEB_PLUGIN_DEV_MAP');
3130
+ return null
2836
3131
  }
2837
- return String(v)
2838
3132
  }
2839
3133
 
2840
3134
  /**
2841
- * @returns {boolean}
3135
+ * @param {Record<string, string>} implicit
3136
+ * @param {Record<string, string>|null} explicit
2842
3137
  */
2843
- function readInjectedEnvDev() {
2844
- const o = getInjectedEnvObject();
2845
- return !!(o && o.DEV === true)
3138
+ function mergeDevMaps(implicit, explicit) {
3139
+ const i = implicit && typeof implicit === 'object' ? implicit : {};
3140
+ const e = explicit && typeof explicit === 'object' ? explicit : {};
3141
+ return { ...i, ...e }
2846
3142
  }
2847
3143
 
2848
- /**
2849
- * 与 `plugin-web-starter`(WebPluginsResponse)返回的 `hostPluginApiVersion` 保持一致,用于契约校验。
2850
- * @type {string}
2851
- */
2852
- const HOST_PLUGIN_API_VERSION = '1.0.0';
2853
-
2854
3144
  /**
2855
- * 宿主侧插件引导:拉取清单、dev 映射、加载入口脚本、调用 activator。
2856
- * 路径与白名单等默认值见 `defaultWebExtendPluginRuntime`,可通过 `resolveRuntimeOptions` / 环境变量覆盖。
2857
- *
2858
- * **Webpack 宿主**:用 `DefinePlugin` 注入 `process.env.VITE_*` 或 **`PLUGIN_*`**(等价键),或 `resolveRuntimeOptions` 显式传参。
2859
- * **Vite 宿主**:入口调用 `setWebExtendPluginEnv(import.meta.env)`,或 `installWebExtendPluginVue2(..., { env: import.meta.env })`。
2860
- *
2861
- * @module PluginRuntime
3145
+ * @param {object} opts
3146
+ * @param {boolean} opts.isDev
3147
+ * @param {string|undefined|null} opts.webPluginDevOrigin
3148
+ * @param {string|undefined|null} opts.webPluginDevIds
3149
+ * @param {string[]} opts.defaultImplicitDevPluginIds
3150
+ * @param {string} opts.devPingPath
3151
+ * @param {number} opts.devPingTimeoutMs
3152
+ * @param {string} opts.webPluginDevEntryPath
3153
+ * @param {Set<string>} hostSet
2862
3154
  */
3155
+ async function buildImplicitWebPluginDevMap(opts, hostSet) {
3156
+ if (!opts.isDev) {
3157
+ return {}
3158
+ }
3159
+ const origin =
3160
+ opts.webPluginDevOrigin === undefined || opts.webPluginDevOrigin === null
3161
+ ? ''
3162
+ : String(opts.webPluginDevOrigin).trim();
3163
+ if (!origin) {
3164
+ return {}
3165
+ }
3166
+ if (!isScriptHostAllowed(`${origin}/`, hostSet)) {
3167
+ return {}
3168
+ }
2863
3169
 
2864
- const DEF = defaultWebExtendPluginRuntime;
3170
+ const idsRaw = opts.webPluginDevIds;
3171
+ const ids =
3172
+ idsRaw !== undefined && idsRaw !== null && String(idsRaw).trim() !== ''
3173
+ ? String(idsRaw)
3174
+ .split(',')
3175
+ .map((s) => s.trim())
3176
+ .filter(Boolean)
3177
+ : [...opts.defaultImplicitDevPluginIds];
2865
3178
 
2866
- /**
2867
- * @typedef {object} WebExtendPluginRuntimeOptions
2868
- * @property {string} [manifestBase] 清单服务 URL 前缀
2869
- * @property {string} [manifestListPath] 清单接口路径(以 `/` 开头),拼在 manifestBase 后
2870
- * @property {RequestCredentials} [manifestFetchCredentials] 清单 fetch 的 credentials
2871
- * @property {boolean} [isDev] 开发模式
2872
- * @property {string} [webPluginDevOrigin] 插件 dev origin
2873
- * @property {string} [webPluginDevIds] 逗号分隔 id,隐式 dev 映射
2874
- * @property {string} [webPluginDevMapJson] 显式 dev 映射 JSON
2875
- * @property {string} [webPluginDevEntryPath] 隐式 dev 入口路径(相对插件 dev origin)
2876
- * @property {string} [devPingPath] dev 存活探测路径
2877
- * @property {string} [devReloadSsePath] dev 热更新 SSE 路径
2878
- * @property {number} [devPingTimeoutMs] 探测超时
2879
- * @property {string[]} [defaultImplicitDevPluginIds] 无 `webPluginDevIds`/env 时用于隐式 dev 的 id;包内默认 `[]`
2880
- * @property {string[]} [allowedScriptHosts] 允许加载脚本的主机名
2881
- * @property {string[]} [bridgeAllowedPathPrefixes] bridge.request 白名单前缀
2882
- * @property {boolean} [bootstrapSummary] bootstrap 结束是否打印摘要
2883
- */
3179
+ if (ids.length === 0) {
3180
+ return {}
3181
+ }
2884
3182
 
2885
- /**
2886
- * Webpack `DefinePlugin` 等注入的 `process.env` 读取。
2887
- * @param {string} key
2888
- * @returns {string|undefined}
2889
- */
2890
- function readProcessEnv(key) {
3183
+ const base = origin.replace(/\/$/, '');
3184
+ const pingUrl = `${base}${opts.devPingPath}`;
2891
3185
  try {
2892
- if (typeof process !== 'undefined' && process.env && key in process.env) {
2893
- const v = process.env[key];
2894
- if (v !== undefined && v !== '') {
2895
- return String(v)
2896
- }
3186
+ const ctrl = new AbortController();
3187
+ const timer = setTimeout(() => ctrl.abort(), opts.devPingTimeoutMs);
3188
+ const r = await fetch(pingUrl, {
3189
+ mode: 'cors',
3190
+ cache: 'no-store',
3191
+ signal: ctrl.signal
3192
+ });
3193
+ clearTimeout(timer);
3194
+ if (!r.ok) {
3195
+ return {}
2897
3196
  }
2898
- } catch (_) {}
2899
- return undefined
3197
+ const body = (await r.text()).trim();
3198
+ if (body !== 'ok') {
3199
+ return {}
3200
+ }
3201
+ } catch {
3202
+ return {}
3203
+ }
3204
+
3205
+ const pathPart = opts.webPluginDevEntryPath;
3206
+ const map = {};
3207
+ for (const id of ids) {
3208
+ map[id] = `${base}${pathPart}`;
3209
+ }
3210
+ if (ids.length) {
3211
+ console.info('[wep] plugin dev server', base, '→ implicit entries', pathPart, ids.join(', '));
3212
+ }
3213
+ return map
2900
3214
  }
2901
3215
 
2902
3216
  /**
2903
- * `VITE_*` 的并列命名:同值可读 `PLUGIN_*`(`VITE_WEB_PLUGIN_X` `PLUGIN_WEB_PLUGIN_X`)。
2904
- * Vite 需在 `defineConfig({ envPrefix: ['VITE_', 'PLUGIN_'] })` 中暴露 `PLUGIN_`;Webpack 用 DefinePlugin 注入即可。
2905
- * @param {string} viteStyleKey 以 `VITE_` 开头的键名
2906
- * @returns {string|null}
3217
+ * 开发模式下插件热更新 SSE(按 dev 映射中的 origin 连接)。
3218
+ * @module runtime/dev-reload-sse
2907
3219
  */
2908
- function viteKeyToPluginAlternate(viteStyleKey) {
2909
- if (typeof viteStyleKey !== 'string' || !viteStyleKey.startsWith('VITE_')) {
2910
- return null
3220
+
3221
+ /** @type {Map<string, EventSource>} */
3222
+ const pluginDevEventSources = new Map();
3223
+
3224
+ let pluginDevBeforeUnloadRegistered = false;
3225
+
3226
+ function closeAllPluginDevEventSources() {
3227
+ for (const es of pluginDevEventSources.values()) {
3228
+ try {
3229
+ es.close();
3230
+ } catch (_) {}
2911
3231
  }
2912
- return `PLUGIN_${viteStyleKey.slice(5)}`
3232
+ pluginDevEventSources.clear();
2913
3233
  }
2914
3234
 
2915
- /**
2916
- * 先读注入环境(`setWebExtendPluginEnv` / `__WEP_ENV__`)中的 `VITE_*` 与并列 `PLUGIN_*`,再读 `process.env`,最后 `fallback`。
2917
- * @param {string} key 仍以 `VITE_*` 为逻辑名(与文档一致)
2918
- * @param {string} [fallback='']
2919
- */
2920
- function resolveBundledEnv(key, fallback = '') {
2921
- const alt = viteKeyToPluginAlternate(key);
2922
- const inj = readInjectedEnvKey(key);
2923
- const fromInjected =
2924
- inj === undefined || inj === null ? (alt ? readInjectedEnvKey(alt) : undefined) : inj;
2925
- const proc = readProcessEnv(key);
2926
- const fromProcess =
2927
- proc === undefined || proc === null ? (alt ? readProcessEnv(alt) : undefined) : proc;
2928
- const first = fromInjected === undefined || fromInjected === null ? fromProcess : fromInjected;
2929
- return first === undefined || first === null ? fallback : first
3235
+ function ensurePluginDevBeforeUnload() {
3236
+ if (pluginDevBeforeUnloadRegistered || typeof window === 'undefined') {
3237
+ return
3238
+ }
3239
+ pluginDevBeforeUnloadRegistered = true;
3240
+ window.addEventListener('beforeunload', closeAllPluginDevEventSources);
2930
3241
  }
2931
3242
 
2932
3243
  /**
2933
- * @returns {boolean}
3244
+ * @param {string} origin
3245
+ * @param {Set<string>} hostSet
2934
3246
  */
2935
- function resolveBundledIsDev() {
2936
- try {
2937
- if (readInjectedEnvDev()) {
2938
- return true
2939
- }
2940
- } catch (_) {}
3247
+ function isDevOriginAllowedForSse(origin, hostSet) {
2941
3248
  try {
2942
- if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'development') {
2943
- return true
2944
- }
2945
- } catch (_) {}
2946
- return false
3249
+ const u = new URL(origin);
3250
+ return hostSet.has(normalizeHost(u.hostname))
3251
+ } catch {
3252
+ return false
3253
+ }
2947
3254
  }
2948
3255
 
2949
3256
  /**
2950
- * @param {string} p
3257
+ * @param {string} origin
3258
+ * @param {boolean} isDev
3259
+ * @param {Set<string>} hostSet
3260
+ * @param {string} ssePath
2951
3261
  */
2952
- function ensureLeadingPath(p) {
2953
- const t = String(p || '').trim();
2954
- if (!t) {
2955
- return '/'
3262
+ function startPluginDevReloadSse(origin, isDev, hostSet, ssePath) {
3263
+ if (!isDev || pluginDevEventSources.has(origin)) {
3264
+ return
3265
+ }
3266
+ if (!isDevOriginAllowedForSse(origin, hostSet)) {
3267
+ return
3268
+ }
3269
+ ensurePluginDevBeforeUnload();
3270
+ const base = origin.replace(/\/$/, '');
3271
+ const url = `${base}${ssePath}`;
3272
+ try {
3273
+ const es = new EventSource(url);
3274
+ pluginDevEventSources.set(origin, es);
3275
+ es.addEventListener('reload', () => {
3276
+ window.location.reload();
3277
+ });
3278
+ es.onopen = () => {
3279
+ console.info('[wep] dev reload SSE', url);
3280
+ };
3281
+ } catch (e) {
3282
+ console.warn('[wep] EventSource failed', url, e);
2956
3283
  }
2957
- return t.startsWith('/') ? t : `/${t}`
2958
3284
  }
2959
3285
 
2960
3286
  /**
2961
- * 解析 `include` | `omit` | `same-origin`,非法时回退默认值。
2962
- * @param {string|undefined} userVal
2963
- * @param {string} envKey
2964
- * @param {RequestCredentials} fallback
3287
+ * @param {Record<string, string>|null|undefined} devMap
3288
+ * @param {boolean} isDev
3289
+ * @param {Set<string>} hostSet
3290
+ * @param {string} ssePath
2965
3291
  */
2966
- function resolveManifestCredentials(userVal, envKey, fallback) {
2967
- if (userVal !== undefined && userVal !== '') {
2968
- const s = String(userVal);
2969
- if (s === 'include' || s === 'omit' || s === 'same-origin') {
2970
- return s
3292
+ function startPluginDevSseForMap(devMap, isDev, hostSet, ssePath) {
3293
+ if (!isDev || !devMap || typeof window === 'undefined') {
3294
+ return
3295
+ }
3296
+ const origins = new Set();
3297
+ for (const entry of Object.values(devMap)) {
3298
+ if (typeof entry !== 'string') {
3299
+ continue
3300
+ }
3301
+ const t = entry.trim();
3302
+ if (!t) {
3303
+ continue
3304
+ }
3305
+ try {
3306
+ origins.add(new URL(t, window.location.href).origin);
3307
+ } catch {
3308
+ /* skip */
2971
3309
  }
2972
3310
  }
2973
- const e = resolveBundledEnv(envKey, '');
2974
- if (e === 'include' || e === 'omit' || e === 'same-origin') {
2975
- return e
3311
+ for (const o of origins) {
3312
+ startPluginDevReloadSse(o, isDev, hostSet, ssePath);
2976
3313
  }
2977
- return fallback
2978
3314
  }
2979
3315
 
2980
3316
  /**
2981
- * @param {number|undefined} userVal
2982
- * @param {string} envKey
2983
- * @param {number} fallback
3317
+ * 动态加载脚本(去重与并发合并)。
3318
+ * @module runtime/load-script
2984
3319
  */
2985
- function resolvePositiveInt(userVal, envKey, fallback) {
2986
- if (typeof userVal === 'number' && Number.isFinite(userVal) && userVal > 0) {
2987
- return Math.floor(userVal)
3320
+
3321
+ const loadScriptMemo = new Map();
3322
+
3323
+ /**
3324
+ * @param {string} src
3325
+ * @returns {Promise<void>}
3326
+ */
3327
+ function loadScript(src) {
3328
+ if (typeof document === 'undefined') {
3329
+ return Promise.reject(new Error('loadScript: no document'))
3330
+ }
3331
+ if (loadScriptMemo.has(src)) {
3332
+ return loadScriptMemo.get(src)
3333
+ }
3334
+ const p = new Promise((resolve, reject) => {
3335
+ const scripts = document.getElementsByTagName('script');
3336
+ for (let i = 0; i < scripts.length; i++) {
3337
+ const el = scripts[i];
3338
+ if (el.src === src) {
3339
+ if (el.getAttribute('data-wep-loaded') === 'true') {
3340
+ resolve();
3341
+ return
3342
+ }
3343
+ el.addEventListener(
3344
+ 'load',
3345
+ () => {
3346
+ el.setAttribute('data-wep-loaded', 'true');
3347
+ resolve();
3348
+ },
3349
+ { once: true }
3350
+ );
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
+ s.onload = () => {
3359
+ s.setAttribute('data-wep-loaded', 'true');
3360
+ resolve();
3361
+ };
3362
+ s.onerror = () => reject(new Error('loadScript failed: ' + src));
3363
+ document.head.appendChild(s);
3364
+ });
3365
+ loadScriptMemo.set(src, p);
3366
+ p.catch(() => loadScriptMemo.delete(src));
3367
+ return p
3368
+ }
3369
+
3370
+ let _printed = false;
3371
+
3372
+ /** 在首次引导插件时打印一行运行时标识(非大块 ASCII art)。 */
3373
+ function printRuntimeBannerOnce() {
3374
+ if (_printed) {
3375
+ return
2988
3376
  }
2989
- const raw = resolveBundledEnv(envKey, '');
2990
- const n = raw ? parseInt(raw, 10) : NaN;
2991
- if (Number.isFinite(n) && n > 0) {
2992
- return n
3377
+ _printed = true;
3378
+ if (typeof console !== 'undefined' && typeof console.info === 'function') {
3379
+ console.info(`[wep] ${RUNTIME_CONSOLE_LABEL} · host API ${HOST_PLUGIN_API_VERSION}`);
2993
3380
  }
2994
- return fallback
2995
3381
  }
2996
3382
 
2997
3383
  /**
2998
- * 合并用户、环境变量与 `defaultWebExtendPluginRuntime`,得到完整运行时选项(宿主可只传需要覆盖的字段)。
2999
- * @param {WebExtendPluginRuntimeOptions} [user]
3000
- * @returns {object}
3384
+ * 拉取插件清单、加载入口脚本并调用各插件 `activator`。
3001
3385
  */
3002
- function resolveRuntimeOptions(user = {}) {
3003
- const manifestBaseRaw =
3004
- user.manifestBase !== undefined && user.manifestBase !== ''
3005
- ? String(user.manifestBase)
3006
- : resolveBundledEnv('VITE_FRONTEND_PLUGIN_BASE', DEF.manifestBase) || DEF.manifestBase;
3007
-
3008
- const manifestListPath = ensureLeadingPath(
3009
- user.manifestListPath !== undefined && user.manifestListPath !== ''
3010
- ? user.manifestListPath
3011
- : resolveBundledEnv('VITE_WEB_PLUGIN_MANIFEST_PATH', DEF.manifestListPath)
3012
- );
3013
-
3014
- const defaultImplicitDevPluginIds =
3015
- Array.isArray(user.defaultImplicitDevPluginIds)
3016
- ? user.defaultImplicitDevPluginIds.map(String).filter(Boolean)
3017
- : (() => {
3018
- const e = resolveBundledEnv('VITE_WEB_PLUGIN_IMPLICIT_DEV_IDS', '');
3019
- if (e) {
3020
- return e
3021
- .split(',')
3022
- .map((s) => s.trim())
3023
- .filter(Boolean)
3024
- }
3025
- return [...DEF.defaultImplicitDevPluginIds]
3026
- })();
3027
-
3028
- const allowedScriptHosts =
3029
- Array.isArray(user.allowedScriptHosts) && user.allowedScriptHosts.length > 0
3030
- ? user.allowedScriptHosts.map((h) => normalizeHost(String(h))).filter(Boolean)
3031
- : (() => {
3032
- const e = resolveBundledEnv('VITE_WEB_PLUGIN_ALLOWED_SCRIPT_HOSTS', '');
3033
- if (e) {
3034
- return e
3035
- .split(',')
3036
- .map((s) => normalizeHost(s.trim()))
3037
- .filter(Boolean)
3038
- }
3039
- return [...DEF.allowedScriptHosts]
3040
- })();
3041
-
3042
- const bridgeAllowedPathPrefixes =
3043
- Array.isArray(user.bridgeAllowedPathPrefixes) && user.bridgeAllowedPathPrefixes.length > 0
3044
- ? user.bridgeAllowedPathPrefixes.map((p) => ensureLeadingPath(p)).filter(Boolean)
3045
- : (() => {
3046
- const e = resolveBundledEnv('VITE_WEB_PLUGIN_BRIDGE_PREFIXES', '');
3047
- if (e) {
3048
- return e
3049
- .split(',')
3050
- .map((s) => ensureLeadingPath(s.trim()))
3051
- .filter(Boolean)
3052
- }
3053
- return [...DEF.bridgeAllowedPathPrefixes]
3054
- })();
3055
-
3056
- return {
3057
- manifestBase: manifestBaseRaw.replace(/\/$/, '') || DEF.manifestBase.replace(/\/$/, ''),
3058
- manifestListPath,
3059
- manifestFetchCredentials: resolveManifestCredentials(
3060
- user.manifestFetchCredentials,
3061
- 'VITE_WEB_PLUGIN_MANIFEST_CREDENTIALS',
3062
- DEF.manifestFetchCredentials
3063
- ),
3064
- isDev: user.isDev !== undefined ? user.isDev : resolveBundledIsDev(),
3065
- webPluginDevOrigin:
3066
- user.webPluginDevOrigin !== undefined ? user.webPluginDevOrigin : resolveBundledEnv('VITE_WEB_PLUGIN_DEV_ORIGIN', ''),
3067
- webPluginDevIds:
3068
- user.webPluginDevIds !== undefined ? user.webPluginDevIds : resolveBundledEnv('VITE_WEB_PLUGIN_DEV_IDS', ''),
3069
- webPluginDevMapJson:
3070
- user.webPluginDevMapJson !== undefined
3071
- ? user.webPluginDevMapJson
3072
- : resolveBundledEnv('VITE_WEB_PLUGIN_DEV_MAP', ''),
3073
- webPluginDevEntryPath: ensureLeadingPath(
3074
- user.webPluginDevEntryPath !== undefined && user.webPluginDevEntryPath !== ''
3075
- ? user.webPluginDevEntryPath
3076
- : resolveBundledEnv('VITE_WEB_PLUGIN_DEV_ENTRY', DEF.webPluginDevEntryPath)
3077
- ),
3078
- devPingPath: ensureLeadingPath(
3079
- user.devPingPath !== undefined && user.devPingPath !== ''
3080
- ? user.devPingPath
3081
- : resolveBundledEnv('VITE_WEB_PLUGIN_DEV_PING_PATH', DEF.devPingPath)
3082
- ),
3083
- devReloadSsePath: ensureLeadingPath(
3084
- user.devReloadSsePath !== undefined && user.devReloadSsePath !== ''
3085
- ? user.devReloadSsePath
3086
- : resolveBundledEnv('VITE_WEB_PLUGIN_DEV_SSE_PATH', DEF.devReloadSsePath)
3087
- ),
3088
- devPingTimeoutMs: resolvePositiveInt(user.devPingTimeoutMs, 'VITE_WEB_PLUGIN_DEV_PING_TIMEOUT_MS', DEF.devPingTimeoutMs),
3089
- defaultImplicitDevPluginIds,
3090
- allowedScriptHosts,
3091
- bridgeAllowedPathPrefixes,
3092
- bootstrapSummary: user.bootstrapSummary
3093
- }
3094
- }
3095
3386
 
3096
3387
  /**
3097
- * @param {ReturnType<typeof resolveRuntimeOptions>} opts
3388
+ * @param {import('./resolve-runtime-options.js').WebExtendPluginRuntimeOptions|object} opts
3098
3389
  */
3099
3390
  function shouldShowBootstrapSummary(opts) {
3100
3391
  if (opts.bootstrapSummary === true) {
@@ -3114,818 +3405,767 @@ function shouldShowBootstrapSummary(opts) {
3114
3405
  }
3115
3406
 
3116
3407
  /**
3117
- * @param {string} hostname
3408
+ * @param {{ id: string }} p
3409
+ * @param {string} [entryUrl]
3410
+ * @param {Record<string, string>|null|undefined} devMap
3411
+ * @param {Set<string>} hostSet
3118
3412
  */
3119
- function normalizeHost(hostname) {
3120
- if (!hostname) {
3121
- return ''
3413
+ async function loadPluginEntry(p, entryUrl, devMap, hostSet) {
3414
+ const devEntry = devMap && typeof devMap[p.id] === 'string' ? devMap[p.id].trim() : '';
3415
+ if (devEntry) {
3416
+ if (!isScriptHostAllowed(devEntry, hostSet)) {
3417
+ console.warn('[wep] dev entry URL not allowed', p.id, devEntry);
3418
+ return
3419
+ }
3420
+ try {
3421
+ await import(
3422
+ /* webpackIgnore: true */
3423
+ /* @vite-ignore */
3424
+ devEntry
3425
+ );
3426
+ } catch (e) {
3427
+ console.warn('[wep] dev import failed, trying manifest entryUrl', p.id, e);
3428
+ if (entryUrl && isScriptHostAllowed(entryUrl, hostSet)) {
3429
+ await loadScript(entryUrl);
3430
+ }
3431
+ return
3432
+ }
3433
+ return
3122
3434
  }
3123
- const h = hostname.toLowerCase();
3124
- if (h.startsWith('[') && h.endsWith(']')) {
3125
- return h.slice(1, -1)
3435
+ if (!entryUrl || !isScriptHostAllowed(entryUrl, hostSet)) {
3436
+ console.warn('[wep] skip (entryUrl not allowed)', p.id, entryUrl);
3437
+ return
3126
3438
  }
3127
- return h
3439
+ await loadScript(entryUrl);
3128
3440
  }
3129
3441
 
3130
3442
  /**
3131
- * @param {string[]} hostnames
3132
- * @returns {Set<string>}
3443
+ * @param {import('vue-router').default} router
3444
+ * @param {(pluginId: string, router: import('vue-router').default, hostKit?: object) => object} createHostApiFactory
3445
+ * @param {import('./resolve-runtime-options.js').WebExtendPluginRuntimeOptions} [runtimeOptions]
3133
3446
  */
3134
- function buildAllowedScriptHostsSet(hostnames) {
3135
- const s = new Set();
3136
- for (const h of hostnames) {
3137
- const n = normalizeHost(h);
3138
- if (n) {
3139
- s.add(n);
3447
+ async function bootstrapPlugins$1(router, createHostApiFactory, runtimeOptions) {
3448
+ if (typeof window === 'undefined') {
3449
+ console.warn('[wep] bootstrapPlugins skipped (no window)');
3450
+ return
3451
+ }
3452
+ printRuntimeBannerOnce();
3453
+ const opts = resolveRuntimeOptions$1(runtimeOptions || {});
3454
+ const base = String(opts.manifestBase).replace(/\/$/, '');
3455
+ const manifestUrl = `${base}${opts.manifestListPath}`;
3456
+ const hostSet = buildAllowedScriptHostsSet(opts.allowedScriptHosts);
3457
+ const explicit = parseWebPluginDevMapExplicit(opts);
3458
+
3459
+ const manifestCtx = {
3460
+ manifestUrl,
3461
+ credentials: opts.manifestFetchCredentials
3462
+ };
3463
+ const [manifestResult, implicit] = await Promise.all([
3464
+ (async () => {
3465
+ try {
3466
+ if (typeof opts.fetchManifest === 'function') {
3467
+ return await opts.fetchManifest(manifestCtx)
3468
+ }
3469
+ return await defaultFetchWebPluginManifest$1(manifestCtx)
3470
+ } catch (e) {
3471
+ return { ok: false, error: e, data: null }
3472
+ }
3473
+ })(),
3474
+ buildImplicitWebPluginDevMap(opts, hostSet)
3475
+ ]);
3476
+
3477
+ const devMap = mergeDevMaps(implicit, explicit);
3478
+ startPluginDevSseForMap(devMap, opts.isDev, hostSet, opts.devReloadSsePath);
3479
+
3480
+ const hostKit = {
3481
+ bridgeAllowedPathPrefixes: opts.bridgeAllowedPathPrefixes,
3482
+ ...(opts.pluginRoutesParentName
3483
+ ? { pluginRoutesParentName: opts.pluginRoutesParentName }
3484
+ : {}),
3485
+ ...(typeof opts.transformRoutes === 'function' ? { transformRoutes: opts.transformRoutes } : {}),
3486
+ ...(typeof opts.interceptRegisterRoutes === 'function'
3487
+ ? { interceptRegisterRoutes: opts.interceptRegisterRoutes }
3488
+ : {}),
3489
+ ...(typeof opts.adaptRouteDeclarations === 'function'
3490
+ ? { adaptRouteDeclarations: opts.adaptRouteDeclarations }
3491
+ : {})
3492
+ };
3493
+
3494
+ if (!manifestResult.ok) {
3495
+ if (manifestResult.error) {
3496
+ console.warn('[wep] fetch manifest failed', manifestResult.error);
3497
+ } else {
3498
+ console.warn('[wep] manifest HTTP', manifestResult.status, manifestUrl);
3140
3499
  }
3500
+ if (shouldShowBootstrapSummary(opts)) {
3501
+ console.info('[wep] bootstrap_summary', { ok: false, reason: 'manifest_fetch' });
3502
+ }
3503
+ return
3504
+ }
3505
+ /** @type {{ hostPluginApiVersion?: string, plugins?: object[] }} */
3506
+ const data = manifestResult.data;
3507
+ if (!data) {
3508
+ if (shouldShowBootstrapSummary(opts)) {
3509
+ console.info('[wep] bootstrap_summary', { ok: false, reason: 'manifest_empty_body' });
3510
+ }
3511
+ return
3141
3512
  }
3142
- return s
3143
- }
3144
3513
 
3145
- /**
3146
- * @param {string} url
3147
- * @param {Set<string>} hostSet
3148
- */
3149
- function isScriptHostAllowed(url, hostSet) {
3150
- if (typeof window === 'undefined') {
3151
- return false
3514
+ const apiVer = data.hostPluginApiVersion;
3515
+ if (apiVer) {
3516
+ const coerced = semver.coerce(apiVer);
3517
+ const maj = coerced ? coerced.major : 0;
3518
+ const range = `^${maj}.0.0`;
3519
+ if (!semver.satisfies(HOST_PLUGIN_API_VERSION, range, { includePrerelease: true })) {
3520
+ console.warn('[wep] host API version mismatch', {
3521
+ host: HOST_PLUGIN_API_VERSION,
3522
+ manifest: apiVer
3523
+ });
3524
+ }
3152
3525
  }
3153
- try {
3154
- const u = new URL(url, window.location.origin);
3155
- const h = normalizeHost(u.hostname);
3156
- return hostSet.has(h)
3157
- } catch {
3158
- return false
3526
+
3527
+ window.__PLUGIN_ACTIVATORS__ = window.__PLUGIN_ACTIVATORS__ || {};
3528
+
3529
+ const plugins = data.plugins || [];
3530
+ if (plugins.length === 0) {
3531
+ console.info('[wep] empty plugin manifest — check backend and URL', manifestUrl);
3532
+ }
3533
+
3534
+ const summary = {
3535
+ manifestCount: plugins.length,
3536
+ activated: 0,
3537
+ skipEngines: 0,
3538
+ skipLoad: 0,
3539
+ skipNoActivator: 0,
3540
+ activateFail: 0
3541
+ };
3542
+
3543
+ for (const p of plugins) {
3544
+ const range = p.engines && p.engines.host;
3545
+ if (range && !semver.satisfies(HOST_PLUGIN_API_VERSION, range, { includePrerelease: true })) {
3546
+ console.warn('[wep] skip plugin (engines.host)', p.id, range);
3547
+ summary.skipEngines++;
3548
+ continue
3549
+ }
3550
+ const entryUrl = p.entryUrl;
3551
+ try {
3552
+ await loadPluginEntry(p, entryUrl, devMap, hostSet);
3553
+ } catch (e) {
3554
+ console.warn('[wep] script load failed', p.id, e);
3555
+ summary.skipLoad++;
3556
+ continue
3557
+ }
3558
+ const activator = window.__PLUGIN_ACTIVATORS__[p.id];
3559
+ if (typeof activator !== 'function') {
3560
+ console.warn('[wep] no activator for', p.id);
3561
+ summary.skipNoActivator++;
3562
+ continue
3563
+ }
3564
+ const hostApi = createHostApiFactory(p.id, router, hostKit);
3565
+ try {
3566
+ const pluginRecord = Object.freeze({ ...p });
3567
+ activator(hostApi, { pluginRecord });
3568
+ summary.activated++;
3569
+ } catch (e) {
3570
+ console.error('[wep] activate failed', p.id, e);
3571
+ summary.activateFail++;
3572
+ }
3573
+ }
3574
+
3575
+ if (shouldShowBootstrapSummary(opts)) {
3576
+ console.info('[wep] bootstrap_summary', { ok: true, ...summary });
3159
3577
  }
3160
3578
  }
3161
3579
 
3162
3580
  /**
3163
- * @param {ReturnType<typeof resolveRuntimeOptions>} opts
3581
+ * 运行时引导相关 API 的聚合导出(实现位于 `./runtime/`)。
3582
+ */
3583
+
3584
+ var pluginRuntime = /*#__PURE__*/Object.freeze({
3585
+ __proto__: null,
3586
+ bootstrapPlugins: bootstrapPlugins$1,
3587
+ defaultFetchWebPluginManifest: defaultFetchWebPluginManifest$1,
3588
+ resolveRuntimeOptions: resolveRuntimeOptions$1
3589
+ });
3590
+
3591
+ /**
3592
+ * 清单拉取函数的组合工具:缓存、埋点等以**中间件**形式扩展,不侵入 `bootstrapPlugins` 核心逻辑,
3593
+ * 可组合的 `fetchManifest` 包装;入参/出参与 `resolveRuntimeOptions({ fetchManifest })` 一致。
3594
+ *
3595
+ * @module runtime/manifest-fetch-composer
3596
+ */
3597
+
3598
+ /**
3599
+ * @typedef {object} FetchWebPluginManifestContext
3600
+ * @property {string} manifestUrl
3601
+ * @property {RequestCredentials} credentials
3602
+ */
3603
+
3604
+ /**
3605
+ * @typedef {object} FetchWebPluginManifestResult
3606
+ * @property {boolean} ok
3607
+ * @property {number} [status]
3608
+ * @property {{ hostPluginApiVersion?: string, plugins?: object[] }|null} [data]
3609
+ * @property {unknown} [error]
3610
+ */
3611
+
3612
+ /**
3613
+ * @callback FetchWebPluginManifestFn
3614
+ * @param {FetchWebPluginManifestContext} ctx
3615
+ * @returns {Promise<FetchWebPluginManifestResult>}
3164
3616
  */
3165
- function parseWebPluginDevMapExplicit(opts) {
3166
- if (!opts.isDev) {
3167
- return null
3168
- }
3169
- const raw = opts.webPluginDevMapJson;
3170
- if (raw === undefined || raw === null || String(raw).trim() === '') {
3171
- return null
3172
- }
3173
- try {
3174
- const map = JSON.parse(String(raw));
3175
- return map && typeof map === 'object' ? map : null
3176
- } catch {
3177
- console.warn('[plugins] webPluginDevMapJson / VITE_WEB_PLUGIN_DEV_MAP is not valid JSON');
3178
- return null
3179
- }
3617
+
3618
+ /**
3619
+ * 将内层 `fetchManifest` 包装为带缓存的版本。等价于
3620
+ * `composeManifestFetch(inner, manifestFetchCacheMiddleware(options))`。
3621
+ *
3622
+ * @param {FetchWebPluginManifestFn} inner 内层实现(如预设生成的 `fetchManifest` `defaultFetchWebPluginManifest`)
3623
+ * @param {ManifestFetchCacheOptions} [options]
3624
+ * @returns {FetchWebPluginManifestFn}
3625
+ */
3626
+ function wrapManifestFetchWithCache$1(inner, options = {}) {
3627
+ return composeManifestFetch$1(inner, manifestFetchCacheMiddleware$1(options))
3180
3628
  }
3181
3629
 
3182
3630
  /**
3183
- * @param {ReturnType<typeof resolveRuntimeOptions>} opts
3184
- * @param {Set<string>} hostSet
3631
+ * @typedef {object} ManifestFetchCacheOptions
3632
+ * @property {number} [ttlMs] 缓存时长(毫秒)。`<= 0` 或未传时**不缓存**(直接透传 `inner`)。
3633
+ * @property {'memory'|'session'|'local'} [storage='memory'] `session`/`local` 依赖 `JSON.stringify`,仅适合可序列化的 `data`。
3634
+ * @property {string} [storageKeyPrefix='wep.manifestFetch.v1'] Web Storage 键前缀(仅 storage 非 memory 时生效)。
3635
+ * @property {(ctx: FetchWebPluginManifestContext) => string} [cacheKey] 默认 `manifestUrl + '\0' + credentials`。
3636
+ * @property {(result: FetchWebPluginManifestResult) => boolean} [shouldCache] 默认:`ok === true` 且 `data` 非空。
3637
+ * @property {number} [maxEntries=50] 仅 `memory`:超过条数时淘汰最久未读条目。
3638
+ * @property {() => number} [now] 测试注入时间戳。
3185
3639
  */
3186
- async function buildImplicitWebPluginDevMap(opts, hostSet) {
3187
- if (!opts.isDev) {
3188
- return {}
3189
- }
3190
- const origin =
3191
- opts.webPluginDevOrigin === undefined || opts.webPluginDevOrigin === null
3192
- ? ''
3193
- : String(opts.webPluginDevOrigin).trim();
3194
- if (!origin) {
3195
- return {}
3196
- }
3197
- if (!isScriptHostAllowed(`${origin}/`, hostSet)) {
3198
- return {}
3640
+
3641
+ /**
3642
+ * 清单拉取**中间件工厂**:`next` 为内层 `fetchManifest`。
3643
+ *
3644
+ * @param {ManifestFetchCacheOptions} options
3645
+ * @returns {(next: FetchWebPluginManifestFn) => FetchWebPluginManifestFn}
3646
+ */
3647
+ function manifestFetchCacheMiddleware$1(options = {}) {
3648
+ const ttlMs = typeof options.ttlMs === 'number' && Number.isFinite(options.ttlMs) ? options.ttlMs : 0;
3649
+ if (ttlMs <= 0) {
3650
+ return (next) => next
3199
3651
  }
3200
3652
 
3201
- const idsRaw = opts.webPluginDevIds;
3202
- const ids =
3203
- idsRaw !== undefined && idsRaw !== null && String(idsRaw).trim() !== ''
3204
- ? String(idsRaw)
3205
- .split(',')
3206
- .map((s) => s.trim())
3207
- .filter(Boolean)
3208
- : [...opts.defaultImplicitDevPluginIds];
3653
+ const storage = options.storage || 'memory';
3654
+ const prefix = options.storageKeyPrefix || 'wep.manifestFetch.v1';
3655
+ const maxEntries = typeof options.maxEntries === 'number' && options.maxEntries > 0 ? options.maxEntries : 50;
3656
+ const getNow = typeof options.now === 'function' ? options.now : () => Date.now();
3657
+ const cacheKeyFn =
3658
+ typeof options.cacheKey === 'function'
3659
+ ? options.cacheKey
3660
+ : (ctx) => `${String(ctx.manifestUrl)}\0${String(ctx.credentials)}`;
3209
3661
 
3210
- if (ids.length === 0) {
3211
- return {}
3662
+ const shouldCache =
3663
+ typeof options.shouldCache === 'function'
3664
+ ? options.shouldCache
3665
+ : (r) => !!(r && r.ok === true && r.data != null);
3666
+
3667
+ /** @type {Map<string, { expiresAt: number, result: FetchWebPluginManifestResult, lastRead: number }>} */
3668
+ const memory = new Map();
3669
+
3670
+ function cloneResult(r) {
3671
+ try {
3672
+ if (typeof structuredClone === 'function') {
3673
+ return structuredClone(r)
3674
+ }
3675
+ } catch (_) {}
3676
+ try {
3677
+ return JSON.parse(JSON.stringify(r))
3678
+ } catch (_) {
3679
+ return { ...r, data: r.data }
3680
+ }
3212
3681
  }
3213
3682
 
3214
- const base = origin.replace(/\/$/, '');
3215
- const pingUrl = `${base}${opts.devPingPath}`;
3216
- try {
3217
- const ctrl = new AbortController();
3218
- const timer = setTimeout(() => ctrl.abort(), opts.devPingTimeoutMs);
3219
- const r = await fetch(pingUrl, {
3220
- mode: 'cors',
3221
- cache: 'no-store',
3222
- signal: ctrl.signal
3223
- });
3224
- clearTimeout(timer);
3225
- if (!r.ok) {
3226
- return {}
3683
+ function touchMemory(key) {
3684
+ const e = memory.get(key);
3685
+ if (e) {
3686
+ e.lastRead = getNow();
3227
3687
  }
3228
- const body = (await r.text()).trim();
3229
- if (body !== 'ok') {
3230
- return {}
3688
+ }
3689
+
3690
+ function pruneMemory() {
3691
+ if (memory.size <= maxEntries) {
3692
+ return
3693
+ }
3694
+ const entries = [...memory.entries()].sort((a, b) => a[1].lastRead - b[1].lastRead);
3695
+ const drop = memory.size - maxEntries;
3696
+ for (let i = 0; i < drop; i++) {
3697
+ memory.delete(entries[i][0]);
3231
3698
  }
3232
- } catch {
3233
- return {}
3234
3699
  }
3235
3700
 
3236
- const pathPart = opts.webPluginDevEntryPath;
3237
- const map = {};
3238
- for (const id of ids) {
3239
- map[id] = `${base}${pathPart}`;
3701
+ function readWebStorage(store, key) {
3702
+ try {
3703
+ const raw = store.getItem(key);
3704
+ if (!raw) {
3705
+ return null
3706
+ }
3707
+ const o = JSON.parse(raw);
3708
+ if (!o || typeof o !== 'object') {
3709
+ return null
3710
+ }
3711
+ const exp = o.expiresAt;
3712
+ const res = o.result;
3713
+ if (typeof exp !== 'number' || getNow() > exp) {
3714
+ store.removeItem(key);
3715
+ return null
3716
+ }
3717
+ return /** @type {FetchWebPluginManifestResult} */ (res)
3718
+ } catch (_) {
3719
+ return null
3720
+ }
3240
3721
  }
3241
- if (ids.length) {
3242
- console.info(
3243
- '[plugins] 已检测到插件 dev 服务(',
3244
- base,
3245
- '),下列 id 将加载隐式 dev 入口(',
3246
- pathPart,
3247
- ')而非清单 dist:',
3248
- ids.join(', ')
3249
- );
3722
+
3723
+ function writeWebStorage(store, key, result, expiresAt) {
3724
+ try {
3725
+ store.setItem(key, JSON.stringify({ expiresAt, result }));
3726
+ } catch (_) {
3727
+ /* Quota / 不可序列化 */
3728
+ }
3250
3729
  }
3251
- return map
3252
- }
3253
3730
 
3254
- /**
3255
- * @param {Record<string, string>} implicit
3256
- * @param {Record<string, string>|null} explicit
3257
- */
3258
- function mergeDevMaps(implicit, explicit) {
3259
- const i = implicit && typeof implicit === 'object' ? implicit : {};
3260
- const e = explicit && typeof explicit === 'object' ? explicit : {};
3261
- return { ...i, ...e }
3262
- }
3731
+ return (next) => {
3732
+ return async (ctx) => {
3733
+ const key = cacheKeyFn(ctx);
3734
+ const now = getNow();
3263
3735
 
3264
- /** @type {Map<string, EventSource>} */
3265
- const pluginDevEventSources = new Map();
3736
+ if (storage === 'memory') {
3737
+ const hit = memory.get(key);
3738
+ if (hit && hit.expiresAt > now) {
3739
+ touchMemory(key);
3740
+ return cloneResult(hit.result)
3741
+ }
3742
+ } else if (storage === 'session' && typeof sessionStorage !== 'undefined') {
3743
+ const sk = `${prefix}:${key}`;
3744
+ const hit = readWebStorage(sessionStorage, sk);
3745
+ if (hit) {
3746
+ return cloneResult(hit)
3747
+ }
3748
+ } else if (storage === 'local' && typeof localStorage !== 'undefined') {
3749
+ const sk = `${prefix}:${key}`;
3750
+ const hit = readWebStorage(localStorage, sk);
3751
+ if (hit) {
3752
+ return cloneResult(hit)
3753
+ }
3754
+ }
3266
3755
 
3267
- let pluginDevBeforeUnloadRegistered = false;
3756
+ const result = await next(ctx);
3268
3757
 
3269
- function closeAllPluginDevEventSources() {
3270
- for (const es of pluginDevEventSources.values()) {
3271
- try {
3272
- es.close();
3273
- } catch (_) {}
3758
+ if (!shouldCache(result)) {
3759
+ return result
3760
+ }
3761
+
3762
+ const expiresAt = now + ttlMs;
3763
+ const frozen = cloneResult(result);
3764
+
3765
+ if (storage === 'memory') {
3766
+ memory.set(key, { expiresAt, result: frozen, lastRead: now });
3767
+ pruneMemory();
3768
+ } else if (storage === 'session' && typeof sessionStorage !== 'undefined') {
3769
+ writeWebStorage(sessionStorage, `${prefix}:${key}`, frozen, expiresAt);
3770
+ } else if (storage === 'local' && typeof localStorage !== 'undefined') {
3771
+ writeWebStorage(localStorage, `${prefix}:${key}`, frozen, expiresAt);
3772
+ }
3773
+
3774
+ return result
3775
+ }
3274
3776
  }
3275
- pluginDevEventSources.clear();
3276
3777
  }
3277
3778
 
3278
- function ensurePluginDevBeforeUnload() {
3279
- if (pluginDevBeforeUnloadRegistered || typeof window === 'undefined') {
3280
- return
3779
+ /**
3780
+ * 自右向左组合中间件(与 Koa/Redux 习惯一致:`compose(f,g,h)(inner)` = f(g(h(inner))))。
3781
+ *
3782
+ * @param {FetchWebPluginManifestFn} inner 最内层拉取实现
3783
+ * @param {...function(FetchWebPluginManifestFn): FetchWebPluginManifestFn} middlewares 每个元素签名 `(next) => async (ctx) => result`
3784
+ * @returns {FetchWebPluginManifestFn}
3785
+ */
3786
+ function composeManifestFetch$1(inner, ...middlewares) {
3787
+ if (typeof inner !== 'function') {
3788
+ throw new Error('[web-extend-plugin-vue2] composeManifestFetch 需要 inner 为函数')
3281
3789
  }
3282
- pluginDevBeforeUnloadRegistered = true;
3283
- window.addEventListener('beforeunload', closeAllPluginDevEventSources);
3790
+ let f = inner;
3791
+ for (let i = middlewares.length - 1; i >= 0; i--) {
3792
+ const mw = middlewares[i];
3793
+ if (typeof mw !== 'function') {
3794
+ throw new Error('[web-extend-plugin-vue2] composeManifestFetch 中间件须为函数')
3795
+ }
3796
+ f = mw(f);
3797
+ }
3798
+ return f
3284
3799
  }
3285
3800
 
3801
+ var manifestComposer = /*#__PURE__*/Object.freeze({
3802
+ __proto__: null,
3803
+ composeManifestFetch: composeManifestFetch$1,
3804
+ manifestFetchCacheMiddleware: manifestFetchCacheMiddleware$1,
3805
+ wrapManifestFetchWithCache: wrapManifestFetchWithCache$1
3806
+ });
3807
+
3286
3808
  /**
3287
- * @param {string} origin
3288
- * @param {Set<string>} hostSet
3809
+ * 插件访问后端的受控通道:`fetch` 仅允许落在配置的路径前缀下(默认 `/api/`),默认 `credentials: 'same-origin'`。
3289
3810
  */
3290
- function isDevOriginAllowedForSse(origin, hostSet) {
3291
- try {
3292
- const u = new URL(origin);
3293
- return hostSet.has(normalizeHost(u.hostname))
3294
- } catch {
3295
- return false
3811
+
3812
+ /**
3813
+ * @param {{ allowedPathPrefixes?: string[] }} [config]
3814
+ */
3815
+ function createRequestBridge(config = {}) {
3816
+ const raw =
3817
+ Array.isArray(config.allowedPathPrefixes) && config.allowedPathPrefixes.length > 0
3818
+ ? config.allowedPathPrefixes
3819
+ : defaultWebExtendPluginRuntime.bridgeAllowedPathPrefixes;
3820
+ const allowedPathPrefixes = raw.map((p) => ensureLeadingPath(p));
3821
+
3822
+ return {
3823
+ /**
3824
+ * @param {string} path 必须以 `/` 开头,且匹配某一白名单前缀
3825
+ * @param {RequestInit} [init]
3826
+ */
3827
+ async request(path, init = {}) {
3828
+ if (typeof path !== 'string' || !path.startsWith('/')) {
3829
+ throw new Error('[wep:bridge] path must start with /')
3830
+ }
3831
+ const allowed = allowedPathPrefixes.some((p) => path.startsWith(p));
3832
+ if (!allowed) {
3833
+ throw new Error('[wep:bridge] path not allowed: ' + path)
3834
+ }
3835
+ return fetch(path, {
3836
+ credentials: 'same-origin',
3837
+ ...init
3838
+ })
3839
+ }
3296
3840
  }
3297
3841
  }
3298
3842
 
3299
3843
  /**
3300
- * @param {string} origin
3301
- * @param {boolean} isDev
3302
- * @param {Set<string>} hostSet
3303
- * @param {string} ssePath
3844
+ * 宿主侧响应式注册表:插件菜单与扩展点槽位(供布局与 `ExtensionPoint` 消费)。
3304
3845
  */
3305
- function startPluginDevReloadSse(origin, isDev, hostSet, ssePath) {
3306
- if (!isDev || pluginDevEventSources.has(origin)) {
3307
- return
3308
- }
3309
- if (!isDevOriginAllowedForSse(origin, hostSet)) {
3846
+
3847
+ /**
3848
+ * @type {{
3849
+ * menus: object[],
3850
+ * slots: Record<string, Array<{ pluginId: string, component: import('vue').Component, priority: number, key: string }>>
3851
+ * }}
3852
+ */
3853
+ const registries = Vue.observable({
3854
+ menus: [],
3855
+ slots: {},
3856
+ /** 槽位变更计数;缓解 Vue2 对动态 `Vue.set(slots, key)` 的依赖收集不完整。 */
3857
+ slotRevision: 0
3858
+ });
3859
+
3860
+ /**
3861
+ * 按插件 id 收集 `onTeardown` 回调,供 `disposeWebPlugin` 统一执行。
3862
+ */
3863
+
3864
+ /** @type {Map<string, Array<() => void>>} */
3865
+ const _byPlugin = new Map();
3866
+
3867
+ /**
3868
+ * @param {string} pluginId
3869
+ * @param {() => void} fn
3870
+ */
3871
+ function registerPluginTeardown(pluginId, fn) {
3872
+ if (typeof fn !== 'function') {
3310
3873
  return
3311
3874
  }
3312
- ensurePluginDevBeforeUnload();
3313
- const base = origin.replace(/\/$/, '');
3314
- const url = `${base}${ssePath}`;
3315
- try {
3316
- const es = new EventSource(url);
3317
- pluginDevEventSources.set(origin, es);
3318
- es.addEventListener('reload', () => {
3319
- window.location.reload();
3320
- });
3321
- es.onopen = () => {
3322
- console.info('[plugins] plugin dev reload SSE:', url);
3323
- };
3324
- } catch (e) {
3325
- console.warn('[plugins] EventSource failed', url, e);
3875
+ let list = _byPlugin.get(pluginId);
3876
+ if (!list) {
3877
+ list = [];
3878
+ _byPlugin.set(pluginId, list);
3326
3879
  }
3880
+ list.push(fn);
3327
3881
  }
3328
3882
 
3329
3883
  /**
3330
- * @param {Record<string, string>|null|undefined} devMap
3331
- * @param {boolean} isDev
3332
- * @param {Set<string>} hostSet
3333
- * @param {string} ssePath
3884
+ * @param {string} pluginId
3334
3885
  */
3335
- function startPluginDevSseForMap(devMap, isDev, hostSet, ssePath) {
3336
- if (!isDev || !devMap || typeof window === 'undefined') {
3886
+ function runPluginTeardowns(pluginId) {
3887
+ const list = _byPlugin.get(pluginId);
3888
+ if (!list) {
3337
3889
  return
3338
3890
  }
3339
- const origins = new Set();
3340
- for (const entry of Object.values(devMap)) {
3341
- if (typeof entry !== 'string') {
3342
- continue
3343
- }
3344
- const t = entry.trim();
3345
- if (!t) {
3346
- continue
3347
- }
3891
+ _byPlugin.delete(pluginId);
3892
+ for (const fn of list) {
3348
3893
  try {
3349
- origins.add(new URL(t, window.location.href).origin);
3350
- } catch {
3351
- /* skip */
3894
+ fn();
3895
+ } catch (e) {
3896
+ console.warn('[wep] teardown failed', pluginId, e);
3352
3897
  }
3353
3898
  }
3354
- for (const o of origins) {
3355
- startPluginDevReloadSse(o, isDev, hostSet, ssePath);
3356
- }
3357
- }
3358
-
3359
- const loadScriptMemo = new Map();
3360
-
3361
- function loadScript(src) {
3362
- if (typeof document === 'undefined') {
3363
- return Promise.reject(new Error('loadScript: no document'))
3364
- }
3365
- if (loadScriptMemo.has(src)) {
3366
- return loadScriptMemo.get(src)
3367
- }
3368
- const p = new Promise((resolve, reject) => {
3369
- const scripts = document.getElementsByTagName('script');
3370
- for (let i = 0; i < scripts.length; i++) {
3371
- const el = scripts[i];
3372
- if (el.src === src) {
3373
- if (el.getAttribute('data-wep-loaded') === 'true') {
3374
- resolve();
3375
- return
3376
- }
3377
- el.addEventListener(
3378
- 'load',
3379
- () => {
3380
- el.setAttribute('data-wep-loaded', 'true');
3381
- resolve();
3382
- },
3383
- { once: true }
3384
- );
3385
- el.addEventListener('error', () => reject(new Error('loadScript failed: ' + src)), { once: true });
3386
- return
3387
- }
3388
- }
3389
- const s = document.createElement('script');
3390
- s.async = true;
3391
- s.src = src;
3392
- s.onload = () => {
3393
- s.setAttribute('data-wep-loaded', 'true');
3394
- resolve();
3395
- };
3396
- s.onerror = () => reject(new Error('loadScript failed: ' + src));
3397
- document.head.appendChild(s);
3398
- });
3399
- loadScriptMemo.set(src, p);
3400
- p.catch(() => loadScriptMemo.delete(src));
3401
- return p
3402
3899
  }
3403
3900
 
3404
3901
  /**
3405
- * @param {{ id: string }} p
3406
- * @param {string} [entryUrl]
3407
- * @param {Record<string, string>|null|undefined} devMap
3408
- * @param {Set<string>} hostSet
3902
+ * 构造插件 `activator(hostApi)` 使用的宿主 API:路由、菜单、扩展点、资源与受控请求桥。
3409
3903
  */
3410
- async function loadPluginEntry(p, entryUrl, devMap, hostSet) {
3411
- const devEntry = devMap && typeof devMap[p.id] === 'string' ? devMap[p.id].trim() : '';
3412
- if (devEntry) {
3413
- if (!isScriptHostAllowed(devEntry, hostSet)) {
3414
- console.warn('[plugins] dev entry URL not allowed', p.id, devEntry);
3904
+
3905
+ let slotItemKeySeq = 0;
3906
+ let routeSynthSeq = 0;
3907
+
3908
+ /**
3909
+ * @param {unknown[]} nodes
3910
+ */
3911
+ function analyzeRouteInputTree(nodes) {
3912
+ let hasDecl = false;
3913
+ let hasCfg = false;
3914
+ /** @param {unknown} r */
3915
+ function walk(r) {
3916
+ if (!r || typeof r !== 'object') {
3415
3917
  return
3416
3918
  }
3417
- try {
3418
- await import(
3419
- /* webpackIgnore: true */
3420
- /* @vite-ignore */
3421
- devEntry
3422
- );
3423
- } catch (e) {
3424
- console.warn('[plugins] dev module import failed, try manifest entryUrl', p.id, e);
3425
- if (entryUrl && isScriptHostAllowed(entryUrl, hostSet)) {
3426
- await loadScript(entryUrl);
3919
+ const o = /** @type {Record<string, unknown>} */ (r);
3920
+ const cfg = o.component != null || o.components != null;
3921
+ const decl = typeof o.componentRef === 'string';
3922
+ if (cfg) {
3923
+ hasCfg = true;
3924
+ } else if (decl) {
3925
+ hasDecl = true;
3926
+ }
3927
+ const ch = o.children;
3928
+ if (Array.isArray(ch)) {
3929
+ for (const c of ch) {
3930
+ walk(c);
3427
3931
  }
3428
- return
3429
3932
  }
3430
- return
3431
3933
  }
3432
- if (!entryUrl || !isScriptHostAllowed(entryUrl, hostSet)) {
3433
- console.warn('[plugins] skip (entryUrl not allowed)', p.id, entryUrl);
3434
- return
3934
+ for (const n of nodes) {
3935
+ walk(n);
3435
3936
  }
3436
- await loadScript(entryUrl);
3937
+ return { hasDecl, hasCfg }
3437
3938
  }
3438
3939
 
3439
3940
  /**
3941
+ * 单插件在宿主侧的 API 句柄。工厂请传 `(id, router, kit) => createHostApi(id, r, kit)` 以便 bridge 白名单等到位。
3942
+ *
3943
+ * @param {string} pluginId
3440
3944
  * @param {import('vue-router').default} router
3441
- * @param {(pluginId: string, router: import('vue-router').default, hostKit?: { bridgeAllowedPathPrefixes: string[] }) => object} createHostApiFactory
3442
- * 始终传入三个参数;单参工厂 `(id) => createHostApi(id, router)` 仍可用,后两个实参被忽略。
3443
- * @param {WebExtendPluginRuntimeOptions} [runtimeOptions]
3945
+ * @param {Record<string, unknown>} [hostKitOptions] `resolveRuntimeOptions` 中路由/bridge 等字段一致
3444
3946
  */
3445
- async function bootstrapPlugins(router, createHostApiFactory, runtimeOptions) {
3446
- if (typeof window === 'undefined') {
3447
- console.warn('[plugins] bootstrapPlugins skipped: requires browser (window)');
3448
- return
3947
+ function createHostApi(pluginId, router, hostKitOptions = {}) {
3948
+ const bridgePrefixes =
3949
+ Array.isArray(hostKitOptions.bridgeAllowedPathPrefixes) &&
3950
+ hostKitOptions.bridgeAllowedPathPrefixes.length > 0
3951
+ ? hostKitOptions.bridgeAllowedPathPrefixes
3952
+ : defaultWebExtendPluginRuntime.bridgeAllowedPathPrefixes;
3953
+ const bridge = createRequestBridge({ allowedPathPrefixes: bridgePrefixes });
3954
+
3955
+ const parentName =
3956
+ typeof hostKitOptions.pluginRoutesParentName === 'string'
3957
+ ? hostKitOptions.pluginRoutesParentName.trim()
3958
+ : '';
3959
+
3960
+ /** @param {import('vue-router').RouteConfig[]} rawRouteConfigs */
3961
+ function applyInternalRegister(rawRouteConfigs) {
3962
+ const wrapped = rawRouteConfigs.map((r) => ({
3963
+ ...r,
3964
+ name: r.name || `__wep_${pluginId}_${routeSynthSeq++}`,
3965
+ meta: { ...(r.meta || {}), pluginId }
3966
+ }));
3967
+ if (typeof router.addRoute === 'function') {
3968
+ if (parentName) {
3969
+ for (const r of wrapped) {
3970
+ router.addRoute(parentName, r);
3971
+ }
3972
+ } else {
3973
+ for (const r of wrapped) {
3974
+ router.addRoute(r);
3975
+ }
3976
+ }
3977
+ } else {
3978
+ if (parentName) {
3979
+ console.warn(
3980
+ '[wep] pluginRoutesParentName requires vue-router 3.5+ addRoute; falling back to top-level addRoutes'
3981
+ );
3982
+ }
3983
+ router.addRoutes(wrapped);
3984
+ }
3449
3985
  }
3450
- const opts = resolveRuntimeOptions(runtimeOptions || {});
3451
- const base = String(opts.manifestBase).replace(/\/$/, '');
3452
- const manifestUrl = `${base}${opts.manifestListPath}`;
3453
- const hostSet = buildAllowedScriptHostsSet(opts.allowedScriptHosts);
3454
- const explicit = parseWebPluginDevMapExplicit(opts);
3455
3986
 
3456
- const [manifestResult, implicit] = await Promise.all([
3457
- (async () => {
3458
- try {
3459
- const res = await fetch(manifestUrl, { credentials: opts.manifestFetchCredentials });
3460
- if (!res.ok) {
3461
- return { ok: false, status: res.status, data: null }
3987
+ function injectStylesheet(href) {
3988
+ const link = document.createElement('link');
3989
+ link.rel = 'stylesheet';
3990
+ link.href = href;
3991
+ link.setAttribute('data-plugin-asset', pluginId);
3992
+ document.head.appendChild(link);
3993
+ }
3994
+
3995
+ /** @param {string} src */
3996
+ function injectScript(src) {
3997
+ return new Promise((resolve, reject) => {
3998
+ const s = document.createElement('script');
3999
+ s.async = true;
4000
+ s.src = src;
4001
+ s.setAttribute('data-plugin-asset', pluginId);
4002
+ s.onload = () => resolve();
4003
+ s.onerror = () => reject(new Error('script failed: ' + src));
4004
+ document.head.appendChild(s);
4005
+ })
4006
+ }
4007
+
4008
+ return {
4009
+ hostPluginApiVersion: HOST_PLUGIN_API_VERSION,
4010
+
4011
+ registerRoutes(routes) {
4012
+ const list = Array.isArray(routes) ? routes : [];
4013
+ if (list.length === 0) {
4014
+ return
4015
+ }
4016
+ const { hasDecl, hasCfg } = analyzeRouteInputTree(list);
4017
+ if (hasDecl && hasCfg) {
4018
+ throw new Error(
4019
+ '[wep] registerRoutes: cannot mix RouteDeclaration (componentRef) with RouteConfig (component)'
4020
+ )
4021
+ }
4022
+ let /** @type {import('vue-router').RouteConfig[]} */ configs;
4023
+ if (hasDecl) {
4024
+ const adapt = hostKitOptions.adaptRouteDeclarations;
4025
+ if (typeof adapt !== 'function') {
4026
+ throw new Error(
4027
+ '[wep] registerRoutes: RouteDeclaration (componentRef) requires adaptRouteDeclarations on the host'
4028
+ )
3462
4029
  }
3463
- const data = await res.json();
3464
- return { ok: true, data }
3465
- } catch (e) {
3466
- return { ok: false, error: e, data: null }
4030
+ configs = adapt({
4031
+ pluginId,
4032
+ router,
4033
+ declarations: /** @type {unknown[]} */ (list)
4034
+ });
4035
+ } else {
4036
+ configs = /** @type {import('vue-router').RouteConfig[]} */ (/** @type {unknown} */ (list));
3467
4037
  }
3468
- })(),
3469
- buildImplicitWebPluginDevMap(opts, hostSet)
3470
- ]);
3471
4038
 
3472
- const devMap = mergeDevMaps(implicit, explicit);
3473
- startPluginDevSseForMap(devMap, opts.isDev, hostSet, opts.devReloadSsePath);
4039
+ if (typeof hostKitOptions.transformRoutes === 'function') {
4040
+ configs = hostKitOptions.transformRoutes({
4041
+ pluginId,
4042
+ router,
4043
+ routes: configs
4044
+ });
4045
+ }
3474
4046
 
3475
- const hostKit = { bridgeAllowedPathPrefixes: opts.bridgeAllowedPathPrefixes };
4047
+ if (typeof hostKitOptions.interceptRegisterRoutes === 'function') {
4048
+ hostKitOptions.interceptRegisterRoutes({
4049
+ pluginId,
4050
+ router,
4051
+ routes: configs,
4052
+ applyInternalRegister
4053
+ });
4054
+ } else {
4055
+ applyInternalRegister(configs);
4056
+ }
4057
+ },
3476
4058
 
3477
- if (!manifestResult.ok) {
3478
- if (manifestResult.error) {
3479
- console.warn('[plugins] fetch manifest failed', manifestResult.error);
3480
- } else {
3481
- console.warn('[plugins] manifest HTTP', manifestResult.status, manifestUrl);
3482
- }
3483
- if (shouldShowBootstrapSummary(opts)) {
3484
- console.info('[plugins] bootstrap_summary', { ok: false, reason: 'manifest_fetch' });
3485
- }
3486
- return
3487
- }
3488
- /** @type {{ hostPluginApiVersion?: string, plugins?: object[] }} */
3489
- const data = manifestResult.data;
3490
- if (!data) {
3491
- if (shouldShowBootstrapSummary(opts)) {
3492
- console.info('[plugins] bootstrap_summary', { ok: false, reason: 'manifest_empty_body' });
3493
- }
3494
- return
3495
- }
4059
+ registerMenuItems(items) {
4060
+ for (const item of items) {
4061
+ registries.menus.push({ ...item, pluginId });
4062
+ }
4063
+ registries.menus.sort(
4064
+ (a, b) => (a.order != null ? a.order : 0) - (b.order != null ? b.order : 0)
4065
+ );
4066
+ },
3496
4067
 
3497
- const apiVer = data.hostPluginApiVersion;
3498
- if (apiVer) {
3499
- const coerced = semver.coerce(apiVer);
3500
- const maj = coerced ? coerced.major : 0;
3501
- const range = `^${maj}.0.0`;
3502
- if (!semver.satisfies(HOST_PLUGIN_API_VERSION, range, { includePrerelease: true })) {
3503
- console.warn(
3504
- '[plugins] host API version mismatch: host implements',
3505
- HOST_PLUGIN_API_VERSION,
3506
- 'server declares',
3507
- apiVer
4068
+ registerSlotComponents(pointId, components) {
4069
+ if (!pointId) {
4070
+ return
4071
+ }
4072
+ if (!registries.slots[pointId]) {
4073
+ Vue.set(registries.slots, pointId, []);
4074
+ }
4075
+ const list = registries.slots[pointId];
4076
+ for (const c of components) {
4077
+ list.push({
4078
+ pluginId,
4079
+ component: c.component,
4080
+ priority: c.priority != null ? c.priority : 0,
4081
+ key: `${pluginId}-${pointId}-${++slotItemKeySeq}`
4082
+ });
4083
+ }
4084
+ list.sort((a, b) => b.priority - a.priority);
4085
+ registries.slotRevision++;
4086
+ },
4087
+
4088
+ registerStylesheetUrls(urls) {
4089
+ for (const u of urls || []) {
4090
+ if (typeof u === 'string' && u) {
4091
+ injectStylesheet(u);
4092
+ }
4093
+ }
4094
+ },
4095
+
4096
+ registerScriptUrls(urls) {
4097
+ const chain = (urls || []).filter((u) => typeof u === 'string' && u).reduce(
4098
+ (p, u) => p.then(() => injectScript(u)),
4099
+ Promise.resolve()
3508
4100
  );
4101
+ chain.catch((e) => console.warn('[wep] registerScriptUrls', pluginId, e));
4102
+ },
4103
+
4104
+ registerSanitizedHtmlSnippet() {
4105
+ throw new Error('registerSanitizedHtmlSnippet is not enabled')
4106
+ },
4107
+
4108
+ getBridge: () => bridge,
4109
+
4110
+ onTeardown(_pluginId, fn) {
4111
+ if (typeof fn === 'function') {
4112
+ registerPluginTeardown(pluginId, fn);
4113
+ }
3509
4114
  }
3510
4115
  }
4116
+ }
3511
4117
 
3512
- window.__PLUGIN_ACTIVATORS__ = window.__PLUGIN_ACTIVATORS__ || {};
4118
+ /**
4119
+ * 卸载单个插件:执行 teardown、清理注册表与 activator、移除带 `data-plugin-asset` 的 DOM。
4120
+ * 注意:Vue Router 3 无公开 `removeRoute`,动态路由通常需整页刷新或宿主自行维护。
4121
+ */
3513
4122
 
3514
- const plugins = data.plugins || [];
3515
- if (plugins.length === 0) {
3516
- console.info(
3517
- '[plugins] 清单为空。请检查:① 后端清单服务(plugin-web-starter)是否已接入;② web-plugin.web-plugins-dir 是否指向含各插件子目录及 manifest.json 的路径;③ 浏览器直接访问',
3518
- manifestUrl,
3519
- '是否返回 plugins 条目。'
3520
- );
4123
+ /**
4124
+ * @param {string} pluginId 与 manifest `id` 一致
4125
+ */
4126
+ function disposeWebPlugin(pluginId) {
4127
+ if (!pluginId || typeof pluginId !== 'string') {
4128
+ return
3521
4129
  }
3522
4130
 
3523
- const summary = {
3524
- manifestCount: plugins.length,
3525
- activated: 0,
3526
- skipEngines: 0,
3527
- skipLoad: 0,
3528
- skipNoActivator: 0,
3529
- activateFail: 0
3530
- };
4131
+ runPluginTeardowns(pluginId);
3531
4132
 
3532
- for (const p of plugins) {
3533
- const range = p.engines && p.engines.host;
3534
- if (range && !semver.satisfies(HOST_PLUGIN_API_VERSION, range, { includePrerelease: true })) {
3535
- console.warn('[plugins] skip (engines.host)', p.id, range);
3536
- summary.skipEngines++;
3537
- continue
3538
- }
3539
- const entryUrl = p.entryUrl;
3540
- try {
3541
- await loadPluginEntry(p, entryUrl, devMap, hostSet);
3542
- } catch (e) {
3543
- console.warn('[plugins] script load failed', p.id, e);
3544
- summary.skipLoad++;
3545
- continue
4133
+ for (let i = registries.menus.length - 1; i >= 0; i--) {
4134
+ if (registries.menus[i].pluginId === pluginId) {
4135
+ registries.menus.splice(i, 1);
3546
4136
  }
3547
- const activator = window.__PLUGIN_ACTIVATORS__[p.id];
3548
- if (typeof activator !== 'function') {
3549
- console.warn('[plugins] no activator for', p.id);
3550
- summary.skipNoActivator++;
4137
+ }
4138
+
4139
+ const slots = registries.slots;
4140
+ for (const pointId of Object.keys(slots)) {
4141
+ const list = slots[pointId];
4142
+ if (!Array.isArray(list)) {
3551
4143
  continue
3552
4144
  }
3553
- const hostApi = createHostApiFactory(p.id, router, hostKit);
3554
- try {
3555
- activator(hostApi);
3556
- summary.activated++;
3557
- } catch (e) {
3558
- console.error('[plugins] activate failed', p.id, e);
3559
- summary.activateFail++;
4145
+ const next = list.filter((x) => x.pluginId !== pluginId);
4146
+ if (next.length === 0) {
4147
+ Vue.delete(slots, pointId);
4148
+ } else if (next.length !== list.length) {
4149
+ Vue.set(slots, pointId, next);
3560
4150
  }
3561
4151
  }
4152
+ registries.slotRevision++;
3562
4153
 
3563
- if (shouldShowBootstrapSummary(opts)) {
3564
- console.info('[plugins] bootstrap_summary', { ok: true, ...summary });
4154
+ if (typeof window !== 'undefined' && window.__PLUGIN_ACTIVATORS__) {
4155
+ delete window.__PLUGIN_ACTIVATORS__[pluginId];
3565
4156
  }
3566
- }
3567
-
3568
- /**
3569
- * 插件通过宿主访问后端的受控通道:仅允许配置的前缀路径,默认 `/api/`;强制默认 `same-origin` 携带 Cookie。
3570
- * 前缀列表由 `createRequestBridge({ allowedPathPrefixes })` 传入,与 `defaultWebExtendPluginRuntime.bridgeAllowedPathPrefixes` 对齐。
3571
- *
3572
- * @module bridge
3573
- */
3574
-
3575
- /**
3576
- * @param {string} p
3577
- */
3578
- function ensureLeadingSlash(p) {
3579
- const t = String(p || '').trim();
3580
- if (!t) {
3581
- return '/'
3582
- }
3583
- return t.startsWith('/') ? t : `/${t}`
3584
- }
3585
-
3586
- /**
3587
- * @param {{ allowedPathPrefixes?: string[] }} [config]
3588
- */
3589
- function createRequestBridge(config = {}) {
3590
- const raw =
3591
- Array.isArray(config.allowedPathPrefixes) && config.allowedPathPrefixes.length > 0
3592
- ? config.allowedPathPrefixes
3593
- : defaultWebExtendPluginRuntime.bridgeAllowedPathPrefixes;
3594
- const allowedPathPrefixes = raw.map((p) => ensureLeadingSlash(p));
3595
-
3596
- return {
3597
- /**
3598
- * 发起受控 `fetch`。
3599
- * @param {string} path 必须以 `/` 开头,且匹配某一 `allowedPathPrefixes` 前缀
3600
- * @param {RequestInit} [init] 会与默认 `{ credentials: 'same-origin' }` 合并(后者可被覆盖)
3601
- */
3602
- async request(path, init = {}) {
3603
- if (typeof path !== 'string' || !path.startsWith('/')) {
3604
- throw new Error('[bridge] path must be a string starting with /')
3605
- }
3606
- const allowed = allowedPathPrefixes.some((p) => path.startsWith(p));
3607
- if (!allowed) {
3608
- throw new Error('[bridge] path not allowed: ' + path)
3609
- }
3610
- return fetch(path, {
3611
- credentials: 'same-origin',
3612
- ...init
3613
- })
3614
- }
3615
- }
3616
- }
3617
-
3618
- /**
3619
- * 宿主全局响应式注册表:菜单与扩展点槽位,供布局与 `ExtensionPoint` 订阅。
3620
- *
3621
- * @module registries
3622
- */
3623
-
3624
- /**
3625
- * @type {{
3626
- * menus: object[],
3627
- * slots: Record<string, Array<{ pluginId: string, component: import('vue').Component, priority: number, key: string }>>
3628
- * }}
3629
- */
3630
- const registries = Vue.observable({
3631
- menus: [],
3632
- /** 扩展点 id → 已注册组件列表(内容区 / 工具栏等共用模型) */
3633
- slots: {},
3634
- /**
3635
- * 每次变更 slots 时递增,供 ExtensionPoint 计算属性显式依赖。
3636
- * Vue 2 对「先访问不存在的 slots[key]、后 Vue.set 补 key」的依赖收集不可靠,会导致扩展点不刷新。
3637
- */
3638
- slotRevision: 0
3639
- });
3640
-
3641
- /**
3642
- * 按 `pluginId` 收集 `onTeardown` 回调,供 `disposeWebPlugin` 统一执行。
3643
- *
3644
- * @module teardown-registry
3645
- */
3646
-
3647
- /** @type {Map<string, Function[]>} */
3648
- const byPlugin = new Map();
3649
-
3650
- /**
3651
- * 登记插件卸载时要执行的同步回调(建议只做解绑、清定时器等轻量逻辑)。
3652
- * @param {string} pluginId
3653
- * @param {() => void} fn
3654
- */
3655
- function registerPluginTeardown(pluginId, fn) {
3656
- if (typeof fn !== 'function') {
3657
- return
3658
- }
3659
- let arr = byPlugin.get(pluginId);
3660
- if (!arr) {
3661
- arr = [];
3662
- byPlugin.set(pluginId, arr);
3663
- }
3664
- arr.push(fn);
3665
- }
3666
-
3667
- /**
3668
- * 执行并清空该插件已登记的全部 teardown;调用后 Map 中不再保留该 id。
3669
- * @param {string} pluginId
3670
- */
3671
- function runPluginTeardowns(pluginId) {
3672
- const arr = byPlugin.get(pluginId);
3673
- if (!arr) {
3674
- return
3675
- }
3676
- byPlugin.delete(pluginId);
3677
- for (const fn of arr) {
3678
- try {
3679
- fn();
3680
- } catch (e) {
3681
- console.warn('[plugins] teardown failed', pluginId, e);
3682
- }
3683
- }
3684
- }
3685
-
3686
- /**
3687
- * 构造供插件 activator 调用的宿主 API(路由、菜单、扩展点、资源注入等)。
3688
- * 与打包工具无关;Webpack 宿主需已配置 `vue-loader` 以编译本包内的 `.vue` 依赖。
3689
- *
3690
- * @module createHostApi
3691
- */
3692
-
3693
- /** 扩展点列表项 key 递增,避免用 Date.now() 导致列表重排时误卸载组件 */
3694
- let slotItemKeySeq = 0;
3695
- /** 无 name 的动态路由合成名递增,避免多次 registerRoutes 重名 */
3696
- let routeSynthSeq = 0;
3697
-
3698
- /**
3699
- * @typedef {object} RegisterSlotEntry
3700
- * @property {import('vue').Component} component
3701
- * @property {number} [priority]
3702
- */
3703
-
3704
- /**
3705
- * @typedef {object} HostApi
3706
- * @property {string} hostPluginApiVersion 宿主实现的协议版本
3707
- * @property {(routes: import('vue-router').RouteConfig[]) => void} registerRoutes 注册路由;无 `name` 时自动生成稳定合成名;优先 `router.addRoute`
3708
- * @property {(items: object[]) => void} registerMenuItems 注册菜单并按 `order` 排序
3709
- * @property {(pointId: string, components: RegisterSlotEntry[]) => void} registerSlotComponents 向扩展点挂载组件
3710
- * @property {(urls?: string[]) => void} registerStylesheetUrls 注入 `link[rel=stylesheet]`,带 `data-plugin-asset`
3711
- * @property {(urls?: string[]) => void} registerScriptUrls 顺序注入外链脚本
3712
- * @property {() => void} registerSanitizedHtmlSnippet MVP 未实现,调用即抛错
3713
- * @property {() => ReturnType<typeof createRequestBridge>} getBridge 受控 `fetch` 代理
3714
- * @property {(pluginId: string, fn: () => void) => void} onTeardown 注册卸载回调;由 `disposeWebPlugin(pluginId)` 触发
3715
- */
3716
-
3717
- /**
3718
- * @typedef {object} HostKitOptions
3719
- * @property {string[]} [bridgeAllowedPathPrefixes] 覆盖 `getBridge().request` 允许的 URL 前缀;默认见 `defaultWebExtendPluginRuntime.bridgeAllowedPathPrefixes`
3720
- */
3721
-
3722
- /**
3723
- * 创建单个插件在宿主侧的 API 句柄,传入插件 `activator(hostApi)`。
3724
- *
3725
- * `bootstrapPlugins` 始终以 `(pluginId, router, hostKitOptions)` 调用工厂;请使用 `(id, r, kit) => createHostApi(id, r, kit)` 传入 `bridgeAllowedPathPrefixes`。
3726
- * 单参工厂 `(id) => createHostApi(id, router)` 仍可用(忽略后两个实参),此时 bridge 仅用包内默认前缀。
3727
- *
3728
- * @param {string} pluginId 与 manifest.id 一致
3729
- * @param {import('vue-router').default} router 宿主 Vue Router 实例(vue-router@3)
3730
- * @param {HostKitOptions} [hostKitOptions]
3731
- * @returns {HostApi}
3732
- */
3733
- function createHostApi(pluginId, router, hostKitOptions = {}) {
3734
- const bridgePrefixes =
3735
- Array.isArray(hostKitOptions.bridgeAllowedPathPrefixes) &&
3736
- hostKitOptions.bridgeAllowedPathPrefixes.length > 0
3737
- ? hostKitOptions.bridgeAllowedPathPrefixes
3738
- : defaultWebExtendPluginRuntime.bridgeAllowedPathPrefixes;
3739
- const bridge = createRequestBridge({ allowedPathPrefixes: bridgePrefixes });
3740
-
3741
- /**
3742
- * 注入样式表;`disposeWebPlugin` 会按 `data-plugin-asset` 移除对应节点。
3743
- * @param {string} href
3744
- */
3745
- function injectStylesheet(href) {
3746
- const link = document.createElement('link');
3747
- link.rel = 'stylesheet';
3748
- link.href = href;
3749
- link.setAttribute('data-plugin-asset', pluginId);
3750
- document.head.appendChild(link);
3751
- }
3752
-
3753
- /**
3754
- * 注入外链脚本(用于插件额外资源,非清单主入口)。
3755
- * @param {string} src
3756
- * @returns {Promise<void>}
3757
- */
3758
- function injectScript(src) {
3759
- return new Promise((resolve, reject) => {
3760
- const s = document.createElement('script');
3761
- s.async = true;
3762
- s.src = src;
3763
- s.setAttribute('data-plugin-asset', pluginId);
3764
- s.onload = () => resolve();
3765
- s.onerror = () => reject(new Error('script failed: ' + src));
3766
- document.head.appendChild(s);
3767
- })
3768
- }
3769
-
3770
- return {
3771
- hostPluginApiVersion: HOST_PLUGIN_API_VERSION,
3772
-
3773
- /**
3774
- * 动态注册路由。Vue Router 3.5+ 推荐 `addRoute`;若不存在则回退已弃用的 `addRoutes`。
3775
- * @param {import('vue-router').RouteConfig[]} routes
3776
- */
3777
- registerRoutes(routes) {
3778
- const wrapped = routes.map((r) => ({
3779
- ...r,
3780
- name: r.name || `__wep_${pluginId}_${routeSynthSeq++}`,
3781
- meta: { ...(r.meta || {}), pluginId }
3782
- }));
3783
- if (typeof router.addRoute === 'function') {
3784
- for (const r of wrapped) {
3785
- router.addRoute(r);
3786
- }
3787
- } else {
3788
- router.addRoutes(wrapped);
3789
- }
3790
- },
3791
-
3792
- /**
3793
- * 写入全局菜单注册表(响应式);按 `order` 升序排列。
3794
- * @param {object[]} items
3795
- */
3796
- registerMenuItems(items) {
3797
- for (const item of items) {
3798
- registries.menus.push({ ...item, pluginId });
3799
- }
3800
- registries.menus.sort(
3801
- (a, b) => (a.order != null ? a.order : 0) - (b.order != null ? b.order : 0)
3802
- );
3803
- },
3804
-
3805
- /**
3806
- * 向指定扩展点 id 注册 Vue 组件;`ExtensionPoint` 按 `priority` 降序渲染。
3807
- * @param {string} pointId
3808
- * @param {RegisterSlotEntry[]} components
3809
- */
3810
- registerSlotComponents(pointId, components) {
3811
- if (!pointId) {
3812
- return
3813
- }
3814
- if (!registries.slots[pointId]) {
3815
- Vue.set(registries.slots, pointId, []);
3816
- }
3817
- const list = registries.slots[pointId];
3818
- for (const c of components) {
3819
- list.push({
3820
- pluginId,
3821
- component: c.component,
3822
- priority: c.priority != null ? c.priority : 0,
3823
- key: `${pluginId}-${pointId}-${++slotItemKeySeq}`
3824
- });
3825
- }
3826
- list.sort((a, b) => b.priority - a.priority);
3827
- registries.slotRevision++;
3828
- },
3829
-
3830
- /**
3831
- * @param {string[]|undefined} urls
3832
- */
3833
- registerStylesheetUrls(urls) {
3834
- for (const u of urls || []) {
3835
- if (typeof u === 'string' && u) {
3836
- injectStylesheet(u);
3837
- }
3838
- }
3839
- },
3840
-
3841
- /**
3842
- * 串行加载多个脚本,失败仅告警不中断宿主。
3843
- * @param {string[]|undefined} urls
3844
- */
3845
- registerScriptUrls(urls) {
3846
- const chain = (urls || []).filter((u) => typeof u === 'string' && u).reduce(
3847
- (p, u) => p.then(() => injectScript(u)),
3848
- Promise.resolve()
3849
- );
3850
- chain.catch((e) => console.warn('[plugins] registerScriptUrls', pluginId, e));
3851
- },
3852
-
3853
- registerSanitizedHtmlSnippet() {
3854
- throw new Error('registerSanitizedHtmlSnippet is not enabled in MVP')
3855
- },
3856
-
3857
- getBridge: () => bridge,
3858
-
3859
- /**
3860
- * 插件卸载前清理逻辑;第一个参数为预留与协议对齐,实际以创建 API 时的 `pluginId` 为准。
3861
- * @param {string} _pluginId 预留,与 manifest.id 一致时可传入
3862
- * @param {() => void} fn
3863
- */
3864
- onTeardown(_pluginId, fn) {
3865
- if (typeof fn === 'function') {
3866
- registerPluginTeardown(pluginId, fn);
3867
- }
3868
- }
3869
- }
3870
- }
3871
4157
 
3872
- /**
3873
- * 单插件卸载:与 `bootstrapPlugins` / `createHostApi` 对称,清理注册表与 DOM 副作用。
3874
- *
3875
- * @module dispose-plugin
3876
- */
3877
-
3878
- /**
3879
- * 卸载指定 id 的插件:依次执行 teardown、移除菜单与扩展点条目、删除 activator、移除带 `data-plugin-asset` 的节点。
3880
- *
3881
- * **路由**:Vue Router 3 无公开 `removeRoute`,此处不改动 matcher;动态路由需整页刷新或自行维护路由表。
3882
- *
3883
- * @param {string} pluginId 与 manifest.id 一致
3884
- */
3885
- function disposeWebPlugin(pluginId) {
3886
- if (!pluginId || typeof pluginId !== 'string') {
3887
- return
3888
- }
3889
-
3890
- runPluginTeardowns(pluginId);
3891
-
3892
- for (let i = registries.menus.length - 1; i >= 0; i--) {
3893
- if (registries.menus[i].pluginId === pluginId) {
3894
- registries.menus.splice(i, 1);
3895
- }
3896
- }
3897
-
3898
- const slots = registries.slots;
3899
- for (const pointId of Object.keys(slots)) {
3900
- const list = slots[pointId];
3901
- if (!Array.isArray(list)) {
3902
- continue
3903
- }
3904
- const next = list.filter((x) => x.pluginId !== pluginId);
3905
- if (next.length === 0) {
3906
- Vue.delete(slots, pointId);
3907
- } else if (next.length !== list.length) {
3908
- Vue.set(slots, pointId, next);
3909
- }
3910
- }
3911
- registries.slotRevision++;
3912
-
3913
- if (typeof window !== 'undefined' && window.__PLUGIN_ACTIVATORS__) {
3914
- delete window.__PLUGIN_ACTIVATORS__[pluginId];
3915
- }
3916
-
3917
- if (typeof document !== 'undefined') {
3918
- document.querySelectorAll('[data-plugin-asset]').forEach((el) => {
3919
- if (el.getAttribute('data-plugin-asset') === pluginId) {
3920
- el.remove();
3921
- }
3922
- });
3923
- }
4158
+ if (typeof document !== 'undefined') {
4159
+ document.querySelectorAll('[data-plugin-asset]').forEach((el) => {
4160
+ if (el.getAttribute('data-plugin-asset') === pluginId) {
4161
+ el.remove();
4162
+ }
4163
+ });
4164
+ }
3924
4165
  }
3925
4166
 
3926
4167
  /**
3927
- * 在宿主布局中声明扩展点;插件通过 `hostApi.registerSlotComponents(pointId, ...)` 注入组件。
3928
- * 使用纯 render 函数,便于 Rollup 发布 dist,宿主无需再转译 .vue。
4168
+ * 布局中的扩展点占位;插件通过 `hostApi.registerSlotComponents(pointId, ...)` 注入组件。
3929
4169
  */
3930
4170
 
3931
4171
  const SlotErrorBoundary = {
@@ -3936,7 +4176,7 @@ const SlotErrorBoundary = {
3936
4176
  },
3937
4177
  errorCaptured(err) {
3938
4178
  this.error = err && err.message ? err.message : String(err);
3939
- console.error('[ExtensionPoint] render error in', this.label, err);
4179
+ console.error('[wep:ExtensionPoint]', this.label, err);
3940
4180
  return false
3941
4181
  },
3942
4182
  render(h) {
@@ -3991,17 +4231,13 @@ var ExtensionPoint = {
3991
4231
  };
3992
4232
 
3993
4233
  /**
3994
- * 一键接入:注册 `ExtensionPoint` 并执行 `bootstrapPlugins`。
3995
- * @module install
4234
+ * 注册全局 `ExtensionPoint` 并异步拉取清单、激活插件。
3996
4235
  */
3997
4236
 
3998
4237
  /**
3999
- * 注册全局组件 `ExtensionPoint` 并异步引导插件清单。
4000
- *
4001
4238
  * @param {*} Vue
4002
- * @param {*} router vue-router 实例
4003
- * @param {Record<string, unknown>} [options] 传给 `resolveRuntimeOptions` 的字段;可含 `env`(Vite 传入 `import.meta.env`)以读取 `VITE_*`。
4004
- * @returns {Promise<void>}
4239
+ * @param {*} router
4240
+ * @param {Record<string, unknown>} [options] 传给 `resolveRuntimeOptions`;可含 `env` 以读取 `VITE_*`
4005
4241
  */
4006
4242
  function installWebExtendPluginVue2(Vue, router, options) {
4007
4243
  const opts = options || {};
@@ -4012,9 +4248,189 @@ function installWebExtendPluginVue2(Vue, router, options) {
4012
4248
  if (Vue && ExtensionPoint) {
4013
4249
  Vue.component('ExtensionPoint', ExtensionPoint);
4014
4250
  }
4015
- const runtime = resolveRuntimeOptions(runtimeUser);
4016
- return bootstrapPlugins(router, (id, r, kit) => createHostApi(id, r, kit), runtime)
4251
+ const runtime = resolveRuntimeOptions$1(runtimeUser);
4252
+ return bootstrapPlugins$1(router, (id, r, kit) => createHostApi(id, r, kit), runtime)
4253
+ }
4254
+
4255
+ /**
4256
+ * Vue CLI + 统一 axios(如 RuoYi `utils/request`)场景的 `install` 预设。
4257
+ */
4258
+
4259
+ /**
4260
+ * @typedef {object} VueCliAxiosPresetDeps
4261
+ * @property {(config: { url: string, method?: string }) => Promise<unknown>} request
4262
+ */
4263
+
4264
+ /**
4265
+ * 将完整 manifest URL 转为相对 `apiBase` 的请求 path,供 axios `baseURL` 拼接。
4266
+ * @param {string} manifestUrl
4267
+ * @param {string} [apiBase]
4268
+ */
4269
+ function resolveManifestPathUnderApiBase(manifestUrl, apiBase) {
4270
+ const base = String(
4271
+ apiBase !== undefined
4272
+ ? apiBase
4273
+ : (typeof process !== 'undefined' && process.env && process.env.VUE_APP_BASE_API) || ''
4274
+ ).replace(/\/$/, '');
4275
+ if (typeof window === 'undefined') {
4276
+ return '/api/frontend-plugins'
4277
+ }
4278
+ const u = new URL(manifestUrl, window.location.origin);
4279
+ let path = u.pathname + u.search;
4280
+ if (base && path.startsWith(base)) {
4281
+ path = path.slice(base.length) || '/';
4282
+ }
4283
+ return path
4284
+ }
4285
+
4286
+ /**
4287
+ * 将裸 `{ plugins }` 与 `{ code, data: { plugins } }` 式响应解包为清单对象。
4288
+ * @param {unknown} body
4289
+ * @returns {object|null}
4290
+ */
4291
+ function unwrapNestedManifestBody(body) {
4292
+ if (!body || typeof body !== 'object') {
4293
+ return null
4294
+ }
4295
+ const o = /** @type {Record<string, unknown>} */ (body);
4296
+ if (Array.isArray(o.plugins)) {
4297
+ return o
4298
+ }
4299
+ const d = o.data;
4300
+ if (d && typeof d === 'object') {
4301
+ const inner = /** @type {Record<string, unknown>} */ (d);
4302
+ if (Array.isArray(inner.plugins)) {
4303
+ return inner
4304
+ }
4305
+ if ('plugins' in inner) {
4306
+ return inner
4307
+ }
4308
+ }
4309
+ return d !== undefined && d !== null && typeof d === 'object' ? /** @type {object} */ (d) : o
4310
+ }
4311
+
4312
+ function bridgePrefixesFromVueCliEnv() {
4313
+ const base = (
4314
+ typeof process !== 'undefined' && process.env && process.env.VUE_APP_BASE_API
4315
+ ? String(process.env.VUE_APP_BASE_API)
4316
+ : ''
4317
+ ).replace(/\/$/, '');
4318
+ const raw = [base ? `${base}/` : '', '/api/', '/dev-api/'].filter(Boolean);
4319
+ return [...new Set(raw)]
4320
+ }
4321
+
4322
+ /**
4323
+ * @param {VueCliAxiosPresetDeps} deps
4324
+ * @param {Record<string, unknown>} [extra]
4325
+ */
4326
+ function createVueCliAxiosInstallOptions(deps, extra = {}) {
4327
+ const { request } = deps;
4328
+ if (typeof request !== 'function') {
4329
+ throw new Error('[wep] createVueCliAxiosInstallOptions requires deps.request')
4330
+ }
4331
+ const envBase = (
4332
+ typeof process !== 'undefined' && process.env && process.env.VUE_APP_BASE_API
4333
+ ? String(process.env.VUE_APP_BASE_API)
4334
+ : ''
4335
+ ).replace(/\/$/, '');
4336
+ const userBase =
4337
+ extra.manifestBase !== undefined && String(extra.manifestBase).trim() !== ''
4338
+ ? String(extra.manifestBase).replace(/\/$/, '')
4339
+ : '';
4340
+ const stripBase = userBase || envBase;
4341
+
4342
+ const fetchManifest = async (ctx) => {
4343
+ try {
4344
+ const url = resolveManifestPathUnderApiBase(ctx.manifestUrl, stripBase);
4345
+ const body = await request({
4346
+ url,
4347
+ method: 'get'
4348
+ });
4349
+ const data = unwrapNestedManifestBody(body);
4350
+ if (!data || typeof data !== 'object') {
4351
+ return {
4352
+ ok: false,
4353
+ error: new Error('[wep] invalid manifest response'),
4354
+ data: null
4355
+ }
4356
+ }
4357
+ return { ok: true, data }
4358
+ } catch (e) {
4359
+ return { ok: false, error: e, data: null }
4360
+ }
4361
+ };
4362
+
4363
+ const opts = {
4364
+ manifestBase: stripBase || undefined,
4365
+ bridgeAllowedPathPrefixes: bridgePrefixesFromVueCliEnv(),
4366
+ fetchManifest,
4367
+ ...extra
4368
+ };
4369
+
4370
+ const listPath =
4371
+ typeof process !== 'undefined' && process.env && process.env.VUE_APP_WEB_PLUGIN_MANIFEST_PATH;
4372
+ if (listPath && opts.manifestListPath === undefined && extra.manifestListPath === undefined) {
4373
+ opts.manifestListPath = String(listPath);
4374
+ }
4375
+
4376
+ return opts
4017
4377
  }
4018
4378
 
4019
- export { ExtensionPoint, HOST_PLUGIN_API_VERSION, bootstrapPlugins, createHostApi, createRequestBridge, defaultWebExtendPluginRuntime, disposeWebPlugin, installWebExtendPluginVue2, registries, resolveRuntimeOptions, setWebExtendPluginEnv };
4379
+ const presetVueCliAxios = Object.freeze({
4380
+ id: 'vue-cli-axios',
4381
+ description: 'Vue CLI + unified axios request; manifest uses host request()',
4382
+ createInstallOptions: createVueCliAxiosInstallOptions,
4383
+ manifestPathForApiBase: resolveManifestPathUnderApiBase,
4384
+ unwrapManifestBody: unwrapNestedManifestBody
4385
+ });
4386
+
4387
+ /**
4388
+ * 包对外稳定导出与 `WebExtendPluginVue2` 命名空间。
4389
+ */
4390
+
4391
+ const {
4392
+ bootstrapPlugins,
4393
+ defaultFetchWebPluginManifest,
4394
+ resolveRuntimeOptions
4395
+ } = pluginRuntime;
4396
+
4397
+ const {
4398
+ composeManifestFetch,
4399
+ manifestFetchCacheMiddleware,
4400
+ wrapManifestFetchWithCache
4401
+ } = manifestComposer;
4402
+
4403
+ const WebExtendPluginVue2 = Object.freeze({
4404
+ install: installWebExtendPluginVue2,
4405
+ runtime: Object.freeze({
4406
+ bootstrapPlugins: bootstrapPlugins$1,
4407
+ resolveRuntimeOptions: resolveRuntimeOptions$1,
4408
+ defaultFetchWebPluginManifest: defaultFetchWebPluginManifest$1,
4409
+ composeManifestFetch: composeManifestFetch$1,
4410
+ manifestFetchCacheMiddleware: manifestFetchCacheMiddleware$1,
4411
+ wrapManifestFetchWithCache: wrapManifestFetchWithCache$1
4412
+ }),
4413
+ host: Object.freeze({
4414
+ createHostApi,
4415
+ disposeWebPlugin,
4416
+ createRequestBridge,
4417
+ registries
4418
+ }),
4419
+ config: Object.freeze({
4420
+ defaultWebExtendPluginRuntime,
4421
+ setWebExtendPluginEnv
4422
+ }),
4423
+ constants: Object.freeze({
4424
+ HOST_PLUGIN_API_VERSION,
4425
+ RUNTIME_CONSOLE_LABEL
4426
+ }),
4427
+ components: Object.freeze({
4428
+ ExtensionPoint
4429
+ }),
4430
+ presets: Object.freeze({
4431
+ vueCliAxios: presetVueCliAxios
4432
+ })
4433
+ });
4434
+
4435
+ export { ExtensionPoint, HOST_PLUGIN_API_VERSION, RUNTIME_CONSOLE_LABEL, WebExtendPluginVue2, bootstrapPlugins, composeManifestFetch, createHostApi, createRequestBridge, createVueCliAxiosInstallOptions, defaultFetchWebPluginManifest, defaultWebExtendPluginRuntime, disposeWebPlugin, installWebExtendPluginVue2, manifestFetchCacheMiddleware, presetVueCliAxios, registries, resolveManifestPathUnderApiBase, resolveRuntimeOptions, setWebExtendPluginEnv, unwrapNestedManifestBody, wrapManifestFetchWithCache };
4020
4436
  //# sourceMappingURL=index.mjs.map