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