web-extend-plugin-vue2 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -7
- package/dist/index.cjs +1307 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +1291 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +20 -11
- package/src/PluginRuntime.js +0 -729
- package/src/bridge.js +0 -50
- package/src/bridge.test.js +0 -38
- package/src/components/ExtensionPoint.vue +0 -67
- package/src/constants.js +0 -5
- package/src/createHostApi.js +0 -189
- package/src/default-runtime-config.js +0 -58
- package/src/dispose-plugin.js +0 -56
- package/src/index.js +0 -12
- package/src/registries.js +0 -23
- package/src/teardown-registry.js +0 -44
package/src/PluginRuntime.js
DELETED
|
@@ -1,729 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 宿主侧插件引导:拉取清单、dev 映射、加载入口脚本、调用 activator。
|
|
3
|
-
* 路径与白名单等默认值见 `defaultWebExtendPluginRuntime`,可通过 `resolveRuntimeOptions` / 环境变量覆盖。
|
|
4
|
-
*
|
|
5
|
-
* **Webpack 宿主**:无 `import.meta.env` 时,用 `DefinePlugin` 注入 `process.env.VITE_*` 或 **`PLUGIN_*`**(等价键)或传入第三参。
|
|
6
|
-
*
|
|
7
|
-
* @module PluginRuntime
|
|
8
|
-
*/
|
|
9
|
-
import { coerce, satisfies } from 'semver'
|
|
10
|
-
import { HOST_PLUGIN_API_VERSION } from './constants.js'
|
|
11
|
-
import { defaultWebExtendPluginRuntime } from './default-runtime-config.js'
|
|
12
|
-
|
|
13
|
-
const DEF = defaultWebExtendPluginRuntime
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @typedef {object} WebExtendPluginRuntimeOptions
|
|
17
|
-
* @property {string} [manifestBase] 清单服务 URL 前缀
|
|
18
|
-
* @property {string} [manifestListPath] 清单接口路径(以 `/` 开头),拼在 manifestBase 后
|
|
19
|
-
* @property {RequestCredentials} [manifestFetchCredentials] 清单 fetch 的 credentials
|
|
20
|
-
* @property {boolean} [isDev] 开发模式
|
|
21
|
-
* @property {string} [webPluginDevOrigin] 插件 dev origin
|
|
22
|
-
* @property {string} [webPluginDevIds] 逗号分隔 id,隐式 dev 映射
|
|
23
|
-
* @property {string} [webPluginDevMapJson] 显式 dev 映射 JSON
|
|
24
|
-
* @property {string} [webPluginDevEntryPath] 隐式 dev 入口路径(相对插件 dev origin)
|
|
25
|
-
* @property {string} [devPingPath] dev 存活探测路径
|
|
26
|
-
* @property {string} [devReloadSsePath] dev 热更新 SSE 路径
|
|
27
|
-
* @property {number} [devPingTimeoutMs] 探测超时
|
|
28
|
-
* @property {string[]} [defaultImplicitDevPluginIds] 无 `webPluginDevIds`/env 时用于隐式 dev 的 id;包内默认 `[]`
|
|
29
|
-
* @property {string[]} [allowedScriptHosts] 允许加载脚本的主机名
|
|
30
|
-
* @property {string[]} [bridgeAllowedPathPrefixes] bridge.request 白名单前缀
|
|
31
|
-
* @property {boolean} [bootstrapSummary] bootstrap 结束是否打印摘要
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* 从 Vite 注入的 `import.meta.env` 读取字符串配置。
|
|
36
|
-
* @param {string} key
|
|
37
|
-
* @returns {string|undefined}
|
|
38
|
-
*/
|
|
39
|
-
function readImportMetaEnv(key) {
|
|
40
|
-
try {
|
|
41
|
-
if (typeof import.meta !== 'undefined' && import.meta.env) {
|
|
42
|
-
const v = import.meta.env[key]
|
|
43
|
-
if (v !== undefined && v !== '') {
|
|
44
|
-
return String(v)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
} catch (_) {}
|
|
48
|
-
return undefined
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* 从 Webpack `DefinePlugin` 等注入的 `process.env` 读取。
|
|
53
|
-
* @param {string} key
|
|
54
|
-
* @returns {string|undefined}
|
|
55
|
-
*/
|
|
56
|
-
function readProcessEnv(key) {
|
|
57
|
-
try {
|
|
58
|
-
if (typeof process !== 'undefined' && process.env && key in process.env) {
|
|
59
|
-
const v = process.env[key]
|
|
60
|
-
if (v !== undefined && v !== '') {
|
|
61
|
-
return String(v)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
} catch (_) {}
|
|
65
|
-
return undefined
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* `VITE_*` 的并列命名:同值可读 `PLUGIN_*`(`VITE_WEB_PLUGIN_X` → `PLUGIN_WEB_PLUGIN_X`)。
|
|
70
|
-
* Vite 需在 `defineConfig({ envPrefix: ['VITE_', 'PLUGIN_'] })` 中暴露 `PLUGIN_`;Webpack 用 DefinePlugin 注入即可。
|
|
71
|
-
* @param {string} viteStyleKey 以 `VITE_` 开头的键名
|
|
72
|
-
* @returns {string|null}
|
|
73
|
-
*/
|
|
74
|
-
function viteKeyToPluginAlternate(viteStyleKey) {
|
|
75
|
-
if (typeof viteStyleKey !== 'string' || !viteStyleKey.startsWith('VITE_')) {
|
|
76
|
-
return null
|
|
77
|
-
}
|
|
78
|
-
return `PLUGIN_${viteStyleKey.slice(5)}`
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* 先读 `VITE_*`,再读对应的 `PLUGIN_*`,再 `process.env`,最后 `fallback`。
|
|
83
|
-
* @param {string} key 仍以 `VITE_*` 为逻辑名(与文档一致)
|
|
84
|
-
* @param {string} [fallback='']
|
|
85
|
-
*/
|
|
86
|
-
function resolveBundledEnv(key, fallback = '') {
|
|
87
|
-
const alt = viteKeyToPluginAlternate(key)
|
|
88
|
-
const fromMeta =
|
|
89
|
-
readImportMetaEnv(key) ?? (alt ? readImportMetaEnv(alt) : undefined)
|
|
90
|
-
const fromProcess =
|
|
91
|
-
readProcessEnv(key) ?? (alt ? readProcessEnv(alt) : undefined)
|
|
92
|
-
return fromMeta ?? fromProcess ?? fallback
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* @returns {boolean}
|
|
97
|
-
*/
|
|
98
|
-
function resolveBundledIsDev() {
|
|
99
|
-
try {
|
|
100
|
-
if (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.DEV === true) {
|
|
101
|
-
return true
|
|
102
|
-
}
|
|
103
|
-
} catch (_) {}
|
|
104
|
-
try {
|
|
105
|
-
if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'development') {
|
|
106
|
-
return true
|
|
107
|
-
}
|
|
108
|
-
} catch (_) {}
|
|
109
|
-
return false
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* @param {string} p
|
|
114
|
-
*/
|
|
115
|
-
function ensureLeadingPath(p) {
|
|
116
|
-
const t = String(p || '').trim()
|
|
117
|
-
if (!t) {
|
|
118
|
-
return '/'
|
|
119
|
-
}
|
|
120
|
-
return t.startsWith('/') ? t : `/${t}`
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* 解析 `include` | `omit` | `same-origin`,非法时回退默认值。
|
|
125
|
-
* @param {string|undefined} userVal
|
|
126
|
-
* @param {string} envKey
|
|
127
|
-
* @param {RequestCredentials} fallback
|
|
128
|
-
*/
|
|
129
|
-
function resolveManifestCredentials(userVal, envKey, fallback) {
|
|
130
|
-
if (userVal !== undefined && userVal !== '') {
|
|
131
|
-
const s = String(userVal)
|
|
132
|
-
if (s === 'include' || s === 'omit' || s === 'same-origin') {
|
|
133
|
-
return s
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
const e = resolveBundledEnv(envKey, '')
|
|
137
|
-
if (e === 'include' || e === 'omit' || e === 'same-origin') {
|
|
138
|
-
return e
|
|
139
|
-
}
|
|
140
|
-
return fallback
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* @param {number|undefined} userVal
|
|
145
|
-
* @param {string} envKey
|
|
146
|
-
* @param {number} fallback
|
|
147
|
-
*/
|
|
148
|
-
function resolvePositiveInt(userVal, envKey, fallback) {
|
|
149
|
-
if (typeof userVal === 'number' && Number.isFinite(userVal) && userVal > 0) {
|
|
150
|
-
return Math.floor(userVal)
|
|
151
|
-
}
|
|
152
|
-
const raw = resolveBundledEnv(envKey, '')
|
|
153
|
-
const n = raw ? parseInt(raw, 10) : NaN
|
|
154
|
-
if (Number.isFinite(n) && n > 0) {
|
|
155
|
-
return n
|
|
156
|
-
}
|
|
157
|
-
return fallback
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* 合并用户、环境变量与 `defaultWebExtendPluginRuntime`,得到完整运行时选项(宿主可只传需要覆盖的字段)。
|
|
162
|
-
* @param {WebExtendPluginRuntimeOptions} [user]
|
|
163
|
-
* @returns {object}
|
|
164
|
-
*/
|
|
165
|
-
export function resolveRuntimeOptions(user = {}) {
|
|
166
|
-
const manifestBaseRaw =
|
|
167
|
-
user.manifestBase !== undefined && user.manifestBase !== ''
|
|
168
|
-
? String(user.manifestBase)
|
|
169
|
-
: resolveBundledEnv('VITE_FRONTEND_PLUGIN_BASE', DEF.manifestBase) || DEF.manifestBase
|
|
170
|
-
|
|
171
|
-
const manifestListPath = ensureLeadingPath(
|
|
172
|
-
user.manifestListPath !== undefined && user.manifestListPath !== ''
|
|
173
|
-
? user.manifestListPath
|
|
174
|
-
: resolveBundledEnv('VITE_WEB_PLUGIN_MANIFEST_PATH', DEF.manifestListPath)
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
const defaultImplicitDevPluginIds =
|
|
178
|
-
Array.isArray(user.defaultImplicitDevPluginIds)
|
|
179
|
-
? user.defaultImplicitDevPluginIds.map(String).filter(Boolean)
|
|
180
|
-
: (() => {
|
|
181
|
-
const e = resolveBundledEnv('VITE_WEB_PLUGIN_IMPLICIT_DEV_IDS', '')
|
|
182
|
-
if (e) {
|
|
183
|
-
return e
|
|
184
|
-
.split(',')
|
|
185
|
-
.map((s) => s.trim())
|
|
186
|
-
.filter(Boolean)
|
|
187
|
-
}
|
|
188
|
-
return [...DEF.defaultImplicitDevPluginIds]
|
|
189
|
-
})()
|
|
190
|
-
|
|
191
|
-
const allowedScriptHosts =
|
|
192
|
-
Array.isArray(user.allowedScriptHosts) && user.allowedScriptHosts.length > 0
|
|
193
|
-
? user.allowedScriptHosts.map((h) => normalizeHost(String(h))).filter(Boolean)
|
|
194
|
-
: (() => {
|
|
195
|
-
const e = resolveBundledEnv('VITE_WEB_PLUGIN_ALLOWED_SCRIPT_HOSTS', '')
|
|
196
|
-
if (e) {
|
|
197
|
-
return e
|
|
198
|
-
.split(',')
|
|
199
|
-
.map((s) => normalizeHost(s.trim()))
|
|
200
|
-
.filter(Boolean)
|
|
201
|
-
}
|
|
202
|
-
return [...DEF.allowedScriptHosts]
|
|
203
|
-
})()
|
|
204
|
-
|
|
205
|
-
const bridgeAllowedPathPrefixes =
|
|
206
|
-
Array.isArray(user.bridgeAllowedPathPrefixes) && user.bridgeAllowedPathPrefixes.length > 0
|
|
207
|
-
? user.bridgeAllowedPathPrefixes.map((p) => ensureLeadingPath(p)).filter(Boolean)
|
|
208
|
-
: (() => {
|
|
209
|
-
const e = resolveBundledEnv('VITE_WEB_PLUGIN_BRIDGE_PREFIXES', '')
|
|
210
|
-
if (e) {
|
|
211
|
-
return e
|
|
212
|
-
.split(',')
|
|
213
|
-
.map((s) => ensureLeadingPath(s.trim()))
|
|
214
|
-
.filter(Boolean)
|
|
215
|
-
}
|
|
216
|
-
return [...DEF.bridgeAllowedPathPrefixes]
|
|
217
|
-
})()
|
|
218
|
-
|
|
219
|
-
return {
|
|
220
|
-
manifestBase: manifestBaseRaw.replace(/\/$/, '') || DEF.manifestBase.replace(/\/$/, ''),
|
|
221
|
-
manifestListPath,
|
|
222
|
-
manifestFetchCredentials: resolveManifestCredentials(
|
|
223
|
-
user.manifestFetchCredentials,
|
|
224
|
-
'VITE_WEB_PLUGIN_MANIFEST_CREDENTIALS',
|
|
225
|
-
DEF.manifestFetchCredentials
|
|
226
|
-
),
|
|
227
|
-
isDev: user.isDev !== undefined ? user.isDev : resolveBundledIsDev(),
|
|
228
|
-
webPluginDevOrigin:
|
|
229
|
-
user.webPluginDevOrigin !== undefined ? user.webPluginDevOrigin : resolveBundledEnv('VITE_WEB_PLUGIN_DEV_ORIGIN', ''),
|
|
230
|
-
webPluginDevIds:
|
|
231
|
-
user.webPluginDevIds !== undefined ? user.webPluginDevIds : resolveBundledEnv('VITE_WEB_PLUGIN_DEV_IDS', ''),
|
|
232
|
-
webPluginDevMapJson:
|
|
233
|
-
user.webPluginDevMapJson !== undefined
|
|
234
|
-
? user.webPluginDevMapJson
|
|
235
|
-
: resolveBundledEnv('VITE_WEB_PLUGIN_DEV_MAP', ''),
|
|
236
|
-
webPluginDevEntryPath: ensureLeadingPath(
|
|
237
|
-
user.webPluginDevEntryPath !== undefined && user.webPluginDevEntryPath !== ''
|
|
238
|
-
? user.webPluginDevEntryPath
|
|
239
|
-
: resolveBundledEnv('VITE_WEB_PLUGIN_DEV_ENTRY', DEF.webPluginDevEntryPath)
|
|
240
|
-
),
|
|
241
|
-
devPingPath: ensureLeadingPath(
|
|
242
|
-
user.devPingPath !== undefined && user.devPingPath !== ''
|
|
243
|
-
? user.devPingPath
|
|
244
|
-
: resolveBundledEnv('VITE_WEB_PLUGIN_DEV_PING_PATH', DEF.devPingPath)
|
|
245
|
-
),
|
|
246
|
-
devReloadSsePath: ensureLeadingPath(
|
|
247
|
-
user.devReloadSsePath !== undefined && user.devReloadSsePath !== ''
|
|
248
|
-
? user.devReloadSsePath
|
|
249
|
-
: resolveBundledEnv('VITE_WEB_PLUGIN_DEV_SSE_PATH', DEF.devReloadSsePath)
|
|
250
|
-
),
|
|
251
|
-
devPingTimeoutMs: resolvePositiveInt(user.devPingTimeoutMs, 'VITE_WEB_PLUGIN_DEV_PING_TIMEOUT_MS', DEF.devPingTimeoutMs),
|
|
252
|
-
defaultImplicitDevPluginIds,
|
|
253
|
-
allowedScriptHosts,
|
|
254
|
-
bridgeAllowedPathPrefixes,
|
|
255
|
-
bootstrapSummary: user.bootstrapSummary
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* @param {ReturnType<typeof resolveRuntimeOptions>} opts
|
|
261
|
-
*/
|
|
262
|
-
function shouldShowBootstrapSummary(opts) {
|
|
263
|
-
if (opts.bootstrapSummary === true) {
|
|
264
|
-
return true
|
|
265
|
-
}
|
|
266
|
-
if (opts.bootstrapSummary === false) {
|
|
267
|
-
return false
|
|
268
|
-
}
|
|
269
|
-
const env = resolveBundledEnv('VITE_PLUGINS_BOOTSTRAP_SUMMARY', '')
|
|
270
|
-
if (env === '0' || env === 'false') {
|
|
271
|
-
return false
|
|
272
|
-
}
|
|
273
|
-
if (env === '1' || env === 'true') {
|
|
274
|
-
return true
|
|
275
|
-
}
|
|
276
|
-
return resolveBundledIsDev()
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* @param {string} hostname
|
|
281
|
-
*/
|
|
282
|
-
function normalizeHost(hostname) {
|
|
283
|
-
if (!hostname) {
|
|
284
|
-
return ''
|
|
285
|
-
}
|
|
286
|
-
const h = hostname.toLowerCase()
|
|
287
|
-
if (h.startsWith('[') && h.endsWith(']')) {
|
|
288
|
-
return h.slice(1, -1)
|
|
289
|
-
}
|
|
290
|
-
return h
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* @param {string[]} hostnames
|
|
295
|
-
* @returns {Set<string>}
|
|
296
|
-
*/
|
|
297
|
-
function buildAllowedScriptHostsSet(hostnames) {
|
|
298
|
-
const s = new Set()
|
|
299
|
-
for (const h of hostnames) {
|
|
300
|
-
const n = normalizeHost(h)
|
|
301
|
-
if (n) {
|
|
302
|
-
s.add(n)
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
return s
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* @param {string} url
|
|
310
|
-
* @param {Set<string>} hostSet
|
|
311
|
-
*/
|
|
312
|
-
function isScriptHostAllowed(url, hostSet) {
|
|
313
|
-
if (typeof window === 'undefined') {
|
|
314
|
-
return false
|
|
315
|
-
}
|
|
316
|
-
try {
|
|
317
|
-
const u = new URL(url, window.location.origin)
|
|
318
|
-
const h = normalizeHost(u.hostname)
|
|
319
|
-
return hostSet.has(h)
|
|
320
|
-
} catch {
|
|
321
|
-
return false
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* @param {ReturnType<typeof resolveRuntimeOptions>} opts
|
|
327
|
-
*/
|
|
328
|
-
function parseWebPluginDevMapExplicit(opts) {
|
|
329
|
-
if (!opts.isDev) {
|
|
330
|
-
return null
|
|
331
|
-
}
|
|
332
|
-
const raw = opts.webPluginDevMapJson
|
|
333
|
-
if (raw === undefined || raw === null || String(raw).trim() === '') {
|
|
334
|
-
return null
|
|
335
|
-
}
|
|
336
|
-
try {
|
|
337
|
-
const map = JSON.parse(String(raw))
|
|
338
|
-
return map && typeof map === 'object' ? map : null
|
|
339
|
-
} catch {
|
|
340
|
-
console.warn('[plugins] webPluginDevMapJson / VITE_WEB_PLUGIN_DEV_MAP is not valid JSON')
|
|
341
|
-
return null
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* @param {ReturnType<typeof resolveRuntimeOptions>} opts
|
|
347
|
-
* @param {Set<string>} hostSet
|
|
348
|
-
*/
|
|
349
|
-
async function buildImplicitWebPluginDevMap(opts, hostSet) {
|
|
350
|
-
if (!opts.isDev) {
|
|
351
|
-
return {}
|
|
352
|
-
}
|
|
353
|
-
const origin =
|
|
354
|
-
opts.webPluginDevOrigin === undefined || opts.webPluginDevOrigin === null
|
|
355
|
-
? ''
|
|
356
|
-
: String(opts.webPluginDevOrigin).trim()
|
|
357
|
-
if (!origin) {
|
|
358
|
-
return {}
|
|
359
|
-
}
|
|
360
|
-
if (!isScriptHostAllowed(`${origin}/`, hostSet)) {
|
|
361
|
-
return {}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const idsRaw = opts.webPluginDevIds
|
|
365
|
-
const ids =
|
|
366
|
-
idsRaw !== undefined && idsRaw !== null && String(idsRaw).trim() !== ''
|
|
367
|
-
? String(idsRaw)
|
|
368
|
-
.split(',')
|
|
369
|
-
.map((s) => s.trim())
|
|
370
|
-
.filter(Boolean)
|
|
371
|
-
: [...opts.defaultImplicitDevPluginIds]
|
|
372
|
-
|
|
373
|
-
if (ids.length === 0) {
|
|
374
|
-
return {}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const base = origin.replace(/\/$/, '')
|
|
378
|
-
const pingUrl = `${base}${opts.devPingPath}`
|
|
379
|
-
try {
|
|
380
|
-
const ctrl = new AbortController()
|
|
381
|
-
const timer = setTimeout(() => ctrl.abort(), opts.devPingTimeoutMs)
|
|
382
|
-
const r = await fetch(pingUrl, {
|
|
383
|
-
mode: 'cors',
|
|
384
|
-
cache: 'no-store',
|
|
385
|
-
signal: ctrl.signal
|
|
386
|
-
})
|
|
387
|
-
clearTimeout(timer)
|
|
388
|
-
if (!r.ok) {
|
|
389
|
-
return {}
|
|
390
|
-
}
|
|
391
|
-
const body = (await r.text()).trim()
|
|
392
|
-
if (body !== 'ok') {
|
|
393
|
-
return {}
|
|
394
|
-
}
|
|
395
|
-
} catch {
|
|
396
|
-
return {}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const pathPart = opts.webPluginDevEntryPath
|
|
400
|
-
const map = {}
|
|
401
|
-
for (const id of ids) {
|
|
402
|
-
map[id] = `${base}${pathPart}`
|
|
403
|
-
}
|
|
404
|
-
if (ids.length) {
|
|
405
|
-
console.info(
|
|
406
|
-
'[plugins] 已检测到插件 dev 服务(',
|
|
407
|
-
base,
|
|
408
|
-
'),下列 id 将加载隐式 dev 入口(',
|
|
409
|
-
pathPart,
|
|
410
|
-
')而非清单 dist:',
|
|
411
|
-
ids.join(', ')
|
|
412
|
-
)
|
|
413
|
-
}
|
|
414
|
-
return map
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* @param {Record<string, string>} implicit
|
|
419
|
-
* @param {Record<string, string>|null} explicit
|
|
420
|
-
*/
|
|
421
|
-
function mergeDevMaps(implicit, explicit) {
|
|
422
|
-
const i = implicit && typeof implicit === 'object' ? implicit : {}
|
|
423
|
-
const e = explicit && typeof explicit === 'object' ? explicit : {}
|
|
424
|
-
return { ...i, ...e }
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/** @type {Map<string, EventSource>} */
|
|
428
|
-
const pluginDevEventSources = new Map()
|
|
429
|
-
|
|
430
|
-
let pluginDevBeforeUnloadRegistered = false
|
|
431
|
-
|
|
432
|
-
function closeAllPluginDevEventSources() {
|
|
433
|
-
for (const es of pluginDevEventSources.values()) {
|
|
434
|
-
try {
|
|
435
|
-
es.close()
|
|
436
|
-
} catch (_) {}
|
|
437
|
-
}
|
|
438
|
-
pluginDevEventSources.clear()
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function ensurePluginDevBeforeUnload() {
|
|
442
|
-
if (pluginDevBeforeUnloadRegistered || typeof window === 'undefined') {
|
|
443
|
-
return
|
|
444
|
-
}
|
|
445
|
-
pluginDevBeforeUnloadRegistered = true
|
|
446
|
-
window.addEventListener('beforeunload', closeAllPluginDevEventSources)
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* @param {string} origin
|
|
451
|
-
* @param {Set<string>} hostSet
|
|
452
|
-
*/
|
|
453
|
-
function isDevOriginAllowedForSse(origin, hostSet) {
|
|
454
|
-
try {
|
|
455
|
-
const u = new URL(origin)
|
|
456
|
-
return hostSet.has(normalizeHost(u.hostname))
|
|
457
|
-
} catch {
|
|
458
|
-
return false
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
* @param {string} origin
|
|
464
|
-
* @param {boolean} isDev
|
|
465
|
-
* @param {Set<string>} hostSet
|
|
466
|
-
* @param {string} ssePath
|
|
467
|
-
*/
|
|
468
|
-
function startPluginDevReloadSse(origin, isDev, hostSet, ssePath) {
|
|
469
|
-
if (!isDev || pluginDevEventSources.has(origin)) {
|
|
470
|
-
return
|
|
471
|
-
}
|
|
472
|
-
if (!isDevOriginAllowedForSse(origin, hostSet)) {
|
|
473
|
-
return
|
|
474
|
-
}
|
|
475
|
-
ensurePluginDevBeforeUnload()
|
|
476
|
-
const base = origin.replace(/\/$/, '')
|
|
477
|
-
const url = `${base}${ssePath}`
|
|
478
|
-
try {
|
|
479
|
-
const es = new EventSource(url)
|
|
480
|
-
pluginDevEventSources.set(origin, es)
|
|
481
|
-
es.addEventListener('reload', () => {
|
|
482
|
-
window.location.reload()
|
|
483
|
-
})
|
|
484
|
-
es.onopen = () => {
|
|
485
|
-
console.info('[plugins] plugin dev reload SSE:', url)
|
|
486
|
-
}
|
|
487
|
-
} catch (e) {
|
|
488
|
-
console.warn('[plugins] EventSource failed', url, e)
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
/**
|
|
493
|
-
* @param {Record<string, string>|null|undefined} devMap
|
|
494
|
-
* @param {boolean} isDev
|
|
495
|
-
* @param {Set<string>} hostSet
|
|
496
|
-
* @param {string} ssePath
|
|
497
|
-
*/
|
|
498
|
-
function startPluginDevSseForMap(devMap, isDev, hostSet, ssePath) {
|
|
499
|
-
if (!isDev || !devMap || typeof window === 'undefined') {
|
|
500
|
-
return
|
|
501
|
-
}
|
|
502
|
-
const origins = new Set()
|
|
503
|
-
for (const entry of Object.values(devMap)) {
|
|
504
|
-
if (typeof entry !== 'string') {
|
|
505
|
-
continue
|
|
506
|
-
}
|
|
507
|
-
const t = entry.trim()
|
|
508
|
-
if (!t) {
|
|
509
|
-
continue
|
|
510
|
-
}
|
|
511
|
-
try {
|
|
512
|
-
origins.add(new URL(t, window.location.href).origin)
|
|
513
|
-
} catch {
|
|
514
|
-
/* skip */
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
for (const o of origins) {
|
|
518
|
-
startPluginDevReloadSse(o, isDev, hostSet, ssePath)
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
const loadScriptMemo = new Map()
|
|
523
|
-
|
|
524
|
-
function loadScript(src) {
|
|
525
|
-
if (typeof document === 'undefined') {
|
|
526
|
-
return Promise.reject(new Error('loadScript: no document'))
|
|
527
|
-
}
|
|
528
|
-
if (loadScriptMemo.has(src)) {
|
|
529
|
-
return loadScriptMemo.get(src)
|
|
530
|
-
}
|
|
531
|
-
const p = new Promise((resolve, reject) => {
|
|
532
|
-
const scripts = document.getElementsByTagName('script')
|
|
533
|
-
for (let i = 0; i < scripts.length; i++) {
|
|
534
|
-
const el = scripts[i]
|
|
535
|
-
if (el.src === src) {
|
|
536
|
-
if (el.getAttribute('data-wep-loaded') === 'true') {
|
|
537
|
-
resolve()
|
|
538
|
-
return
|
|
539
|
-
}
|
|
540
|
-
el.addEventListener(
|
|
541
|
-
'load',
|
|
542
|
-
() => {
|
|
543
|
-
el.setAttribute('data-wep-loaded', 'true')
|
|
544
|
-
resolve()
|
|
545
|
-
},
|
|
546
|
-
{ once: true }
|
|
547
|
-
)
|
|
548
|
-
el.addEventListener('error', () => reject(new Error('loadScript failed: ' + src)), { once: true })
|
|
549
|
-
return
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
const s = document.createElement('script')
|
|
553
|
-
s.async = true
|
|
554
|
-
s.src = src
|
|
555
|
-
s.onload = () => {
|
|
556
|
-
s.setAttribute('data-wep-loaded', 'true')
|
|
557
|
-
resolve()
|
|
558
|
-
}
|
|
559
|
-
s.onerror = () => reject(new Error('loadScript failed: ' + src))
|
|
560
|
-
document.head.appendChild(s)
|
|
561
|
-
})
|
|
562
|
-
loadScriptMemo.set(src, p)
|
|
563
|
-
p.catch(() => loadScriptMemo.delete(src))
|
|
564
|
-
return p
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
/**
|
|
568
|
-
* @param {{ id: string }} p
|
|
569
|
-
* @param {string} [entryUrl]
|
|
570
|
-
* @param {Record<string, string>|null|undefined} devMap
|
|
571
|
-
* @param {Set<string>} hostSet
|
|
572
|
-
*/
|
|
573
|
-
async function loadPluginEntry(p, entryUrl, devMap, hostSet) {
|
|
574
|
-
const devEntry = devMap && typeof devMap[p.id] === 'string' ? devMap[p.id].trim() : ''
|
|
575
|
-
if (devEntry) {
|
|
576
|
-
if (!isScriptHostAllowed(devEntry, hostSet)) {
|
|
577
|
-
console.warn('[plugins] dev entry URL not allowed', p.id, devEntry)
|
|
578
|
-
return
|
|
579
|
-
}
|
|
580
|
-
try {
|
|
581
|
-
await import(
|
|
582
|
-
/* webpackIgnore: true */
|
|
583
|
-
/* @vite-ignore */
|
|
584
|
-
devEntry
|
|
585
|
-
)
|
|
586
|
-
} catch (e) {
|
|
587
|
-
console.warn('[plugins] dev module import failed, try manifest entryUrl', p.id, e)
|
|
588
|
-
if (entryUrl && isScriptHostAllowed(entryUrl, hostSet)) {
|
|
589
|
-
await loadScript(entryUrl)
|
|
590
|
-
}
|
|
591
|
-
return
|
|
592
|
-
}
|
|
593
|
-
return
|
|
594
|
-
}
|
|
595
|
-
if (!entryUrl || !isScriptHostAllowed(entryUrl, hostSet)) {
|
|
596
|
-
console.warn('[plugins] skip (entryUrl not allowed)', p.id, entryUrl)
|
|
597
|
-
return
|
|
598
|
-
}
|
|
599
|
-
await loadScript(entryUrl)
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
/**
|
|
603
|
-
* @param {import('vue-router').default} router
|
|
604
|
-
* @param {(pluginId: string, router: import('vue-router').default, hostKit?: { bridgeAllowedPathPrefixes: string[] }) => object} createHostApiFactory
|
|
605
|
-
* 始终传入三个参数;单参工厂 `(id) => createHostApi(id, router)` 仍可用,后两个实参被忽略。
|
|
606
|
-
* @param {WebExtendPluginRuntimeOptions} [runtimeOptions]
|
|
607
|
-
*/
|
|
608
|
-
export async function bootstrapPlugins(router, createHostApiFactory, runtimeOptions) {
|
|
609
|
-
if (typeof window === 'undefined') {
|
|
610
|
-
console.warn('[plugins] bootstrapPlugins skipped: requires browser (window)')
|
|
611
|
-
return
|
|
612
|
-
}
|
|
613
|
-
const opts = resolveRuntimeOptions(runtimeOptions || {})
|
|
614
|
-
const base = String(opts.manifestBase).replace(/\/$/, '')
|
|
615
|
-
const manifestUrl = `${base}${opts.manifestListPath}`
|
|
616
|
-
const hostSet = buildAllowedScriptHostsSet(opts.allowedScriptHosts)
|
|
617
|
-
const explicit = parseWebPluginDevMapExplicit(opts)
|
|
618
|
-
|
|
619
|
-
const [manifestResult, implicit] = await Promise.all([
|
|
620
|
-
(async () => {
|
|
621
|
-
try {
|
|
622
|
-
const res = await fetch(manifestUrl, { credentials: opts.manifestFetchCredentials })
|
|
623
|
-
if (!res.ok) {
|
|
624
|
-
return { ok: false, status: res.status, data: null }
|
|
625
|
-
}
|
|
626
|
-
const data = await res.json()
|
|
627
|
-
return { ok: true, data }
|
|
628
|
-
} catch (e) {
|
|
629
|
-
return { ok: false, error: e, data: null }
|
|
630
|
-
}
|
|
631
|
-
})(),
|
|
632
|
-
buildImplicitWebPluginDevMap(opts, hostSet)
|
|
633
|
-
])
|
|
634
|
-
|
|
635
|
-
const devMap = mergeDevMaps(implicit, explicit)
|
|
636
|
-
startPluginDevSseForMap(devMap, opts.isDev, hostSet, opts.devReloadSsePath)
|
|
637
|
-
|
|
638
|
-
const hostKit = { bridgeAllowedPathPrefixes: opts.bridgeAllowedPathPrefixes }
|
|
639
|
-
|
|
640
|
-
if (!manifestResult.ok) {
|
|
641
|
-
if (manifestResult.error) {
|
|
642
|
-
console.warn('[plugins] fetch manifest failed', manifestResult.error)
|
|
643
|
-
} else {
|
|
644
|
-
console.warn('[plugins] manifest HTTP', manifestResult.status, manifestUrl)
|
|
645
|
-
}
|
|
646
|
-
if (shouldShowBootstrapSummary(opts)) {
|
|
647
|
-
console.info('[plugins] bootstrap_summary', { ok: false, reason: 'manifest_fetch' })
|
|
648
|
-
}
|
|
649
|
-
return
|
|
650
|
-
}
|
|
651
|
-
/** @type {{ hostPluginApiVersion?: string, plugins?: object[] }} */
|
|
652
|
-
const data = manifestResult.data
|
|
653
|
-
if (!data) {
|
|
654
|
-
if (shouldShowBootstrapSummary(opts)) {
|
|
655
|
-
console.info('[plugins] bootstrap_summary', { ok: false, reason: 'manifest_empty_body' })
|
|
656
|
-
}
|
|
657
|
-
return
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
const apiVer = data.hostPluginApiVersion
|
|
661
|
-
if (apiVer) {
|
|
662
|
-
const coerced = coerce(apiVer)
|
|
663
|
-
const maj = coerced ? coerced.major : 0
|
|
664
|
-
const range = `^${maj}.0.0`
|
|
665
|
-
if (!satisfies(HOST_PLUGIN_API_VERSION, range, { includePrerelease: true })) {
|
|
666
|
-
console.warn(
|
|
667
|
-
'[plugins] host API version mismatch: host implements',
|
|
668
|
-
HOST_PLUGIN_API_VERSION,
|
|
669
|
-
'server declares',
|
|
670
|
-
apiVer
|
|
671
|
-
)
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
window.__PLUGIN_ACTIVATORS__ = window.__PLUGIN_ACTIVATORS__ || {}
|
|
676
|
-
|
|
677
|
-
const plugins = data.plugins || []
|
|
678
|
-
if (plugins.length === 0) {
|
|
679
|
-
console.info(
|
|
680
|
-
'[plugins] 清单为空。请检查:① web-extend-plugin-server 是否已启动;② frontend-plugin.web-plugins-dir 是否指向含各插件子目录及 manifest.json 的路径;③ 浏览器直接访问',
|
|
681
|
-
manifestUrl,
|
|
682
|
-
'是否返回 plugins 条目。'
|
|
683
|
-
)
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
const summary = {
|
|
687
|
-
manifestCount: plugins.length,
|
|
688
|
-
activated: 0,
|
|
689
|
-
skipEngines: 0,
|
|
690
|
-
skipLoad: 0,
|
|
691
|
-
skipNoActivator: 0,
|
|
692
|
-
activateFail: 0
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
for (const p of plugins) {
|
|
696
|
-
const range = p.engines && p.engines.host
|
|
697
|
-
if (range && !satisfies(HOST_PLUGIN_API_VERSION, range, { includePrerelease: true })) {
|
|
698
|
-
console.warn('[plugins] skip (engines.host)', p.id, range)
|
|
699
|
-
summary.skipEngines++
|
|
700
|
-
continue
|
|
701
|
-
}
|
|
702
|
-
const entryUrl = p.entryUrl
|
|
703
|
-
try {
|
|
704
|
-
await loadPluginEntry(p, entryUrl, devMap, hostSet)
|
|
705
|
-
} catch (e) {
|
|
706
|
-
console.warn('[plugins] script load failed', p.id, e)
|
|
707
|
-
summary.skipLoad++
|
|
708
|
-
continue
|
|
709
|
-
}
|
|
710
|
-
const activator = window.__PLUGIN_ACTIVATORS__[p.id]
|
|
711
|
-
if (typeof activator !== 'function') {
|
|
712
|
-
console.warn('[plugins] no activator for', p.id)
|
|
713
|
-
summary.skipNoActivator++
|
|
714
|
-
continue
|
|
715
|
-
}
|
|
716
|
-
const hostApi = createHostApiFactory(p.id, router, hostKit)
|
|
717
|
-
try {
|
|
718
|
-
activator(hostApi)
|
|
719
|
-
summary.activated++
|
|
720
|
-
} catch (e) {
|
|
721
|
-
console.error('[plugins] activate failed', p.id, e)
|
|
722
|
-
summary.activateFail++
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
if (shouldShowBootstrapSummary(opts)) {
|
|
727
|
-
console.info('[plugins] bootstrap_summary', { ok: true, ...summary })
|
|
728
|
-
}
|
|
729
|
-
}
|