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/bridge.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 插件通过宿主访问后端的受控通道:仅允许配置的前缀路径,默认 `/api/`;强制默认 `same-origin` 携带 Cookie。
|
|
3
|
-
* 前缀列表由 `createRequestBridge({ allowedPathPrefixes })` 传入,与 `defaultWebExtendPluginRuntime.bridgeAllowedPathPrefixes` 对齐。
|
|
4
|
-
*
|
|
5
|
-
* @module bridge
|
|
6
|
-
*/
|
|
7
|
-
import { defaultWebExtendPluginRuntime } from './default-runtime-config.js'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @param {string} p
|
|
11
|
-
*/
|
|
12
|
-
function ensureLeadingSlash(p) {
|
|
13
|
-
const t = String(p || '').trim()
|
|
14
|
-
if (!t) {
|
|
15
|
-
return '/'
|
|
16
|
-
}
|
|
17
|
-
return t.startsWith('/') ? t : `/${t}`
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* @param {{ allowedPathPrefixes?: string[] }} [config]
|
|
22
|
-
*/
|
|
23
|
-
export function createRequestBridge(config = {}) {
|
|
24
|
-
const raw =
|
|
25
|
-
Array.isArray(config.allowedPathPrefixes) && config.allowedPathPrefixes.length > 0
|
|
26
|
-
? config.allowedPathPrefixes
|
|
27
|
-
: defaultWebExtendPluginRuntime.bridgeAllowedPathPrefixes
|
|
28
|
-
const allowedPathPrefixes = raw.map((p) => ensureLeadingSlash(p))
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
/**
|
|
32
|
-
* 发起受控 `fetch`。
|
|
33
|
-
* @param {string} path 必须以 `/` 开头,且匹配某一 `allowedPathPrefixes` 前缀
|
|
34
|
-
* @param {RequestInit} [init] 会与默认 `{ credentials: 'same-origin' }` 合并(后者可被覆盖)
|
|
35
|
-
*/
|
|
36
|
-
async request(path, init = {}) {
|
|
37
|
-
if (typeof path !== 'string' || !path.startsWith('/')) {
|
|
38
|
-
throw new Error('[bridge] path must be a string starting with /')
|
|
39
|
-
}
|
|
40
|
-
const allowed = allowedPathPrefixes.some((p) => path.startsWith(p))
|
|
41
|
-
if (!allowed) {
|
|
42
|
-
throw new Error('[bridge] path not allowed: ' + path)
|
|
43
|
-
}
|
|
44
|
-
return fetch(path, {
|
|
45
|
-
credentials: 'same-origin',
|
|
46
|
-
...init
|
|
47
|
-
})
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
package/src/bridge.test.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
import { createRequestBridge } from './bridge.js'
|
|
3
|
-
|
|
4
|
-
describe('createRequestBridge', () => {
|
|
5
|
-
const originalFetch = globalThis.fetch
|
|
6
|
-
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
globalThis.fetch = vi.fn(() => Promise.resolve(new Response(null, { status: 200 })))
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
afterEach(() => {
|
|
12
|
-
globalThis.fetch = originalFetch
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
it('allows paths under /api/', async () => {
|
|
16
|
-
const bridge = createRequestBridge()
|
|
17
|
-
await bridge.request('/api/channels')
|
|
18
|
-
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
19
|
-
'/api/channels',
|
|
20
|
-
expect.objectContaining({ credentials: 'same-origin' })
|
|
21
|
-
)
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it('rejects paths outside allowlist', async () => {
|
|
25
|
-
const bridge = createRequestBridge()
|
|
26
|
-
await expect(bridge.request('/fp-api/x')).rejects.toThrow(/not allowed/)
|
|
27
|
-
await expect(bridge.request('http://evil.com/api/x')).rejects.toThrow()
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('honors custom allowedPathPrefixes', async () => {
|
|
31
|
-
const bridge = createRequestBridge({ allowedPathPrefixes: ['/fp-api/'] })
|
|
32
|
-
await bridge.request('/fp-api/frontend-plugins')
|
|
33
|
-
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
34
|
-
'/fp-api/frontend-plugins',
|
|
35
|
-
expect.objectContaining({ credentials: 'same-origin' })
|
|
36
|
-
)
|
|
37
|
-
})
|
|
38
|
-
})
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="extension-point" :data-point-id="pointId">
|
|
3
|
-
<SlotErrorBoundary
|
|
4
|
-
v-for="item in items"
|
|
5
|
-
:key="item.key"
|
|
6
|
-
:label="item.pluginId"
|
|
7
|
-
>
|
|
8
|
-
<component :is="item.component" v-bind="forwardProps" />
|
|
9
|
-
</SlotErrorBoundary>
|
|
10
|
-
</div>
|
|
11
|
-
</template>
|
|
12
|
-
|
|
13
|
-
<script>
|
|
14
|
-
/**
|
|
15
|
-
* 在宿主布局中声明扩展点;插件通过 `hostApi.registerSlotComponents(pointId, ...)` 注入组件。
|
|
16
|
-
* 使用 `Vue.observable` 注册表驱动重渲染;子树错误由边界组件捕获,避免拖垮整页。
|
|
17
|
-
*/
|
|
18
|
-
import { registries } from '../registries.js'
|
|
19
|
-
|
|
20
|
-
const SlotErrorBoundary = {
|
|
21
|
-
name: 'SlotErrorBoundary',
|
|
22
|
-
props: { label: String },
|
|
23
|
-
data() {
|
|
24
|
-
return { error: null }
|
|
25
|
-
},
|
|
26
|
-
errorCaptured(err) {
|
|
27
|
-
this.error = err && err.message ? err.message : String(err)
|
|
28
|
-
console.error('[ExtensionPoint] render error in', this.label, err)
|
|
29
|
-
return false
|
|
30
|
-
},
|
|
31
|
-
render(h) {
|
|
32
|
-
if (this.error) {
|
|
33
|
-
return h(
|
|
34
|
-
'div',
|
|
35
|
-
{ class: 'plugin-point-error', style: { color: '#c00', fontSize: '12px' } },
|
|
36
|
-
`[插件 ${this.label}] 渲染失败`
|
|
37
|
-
)
|
|
38
|
-
}
|
|
39
|
-
const d = this.$slots.default
|
|
40
|
-
return d && d[0] ? d[0] : h('span')
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export default {
|
|
45
|
-
name: 'ExtensionPoint',
|
|
46
|
-
components: { SlotErrorBoundary },
|
|
47
|
-
props: {
|
|
48
|
-
pointId: { type: String, required: true },
|
|
49
|
-
slotProps: { type: Object, default: () => ({}) }
|
|
50
|
-
},
|
|
51
|
-
computed: {
|
|
52
|
-
items() {
|
|
53
|
-
void registries.slotRevision
|
|
54
|
-
return registries.slots[this.pointId] || []
|
|
55
|
-
},
|
|
56
|
-
forwardProps() {
|
|
57
|
-
return this.slotProps || {}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
</script>
|
|
62
|
-
|
|
63
|
-
<style scoped>
|
|
64
|
-
.extension-point {
|
|
65
|
-
min-height: 8px;
|
|
66
|
-
}
|
|
67
|
-
</style>
|
package/src/constants.js
DELETED
package/src/createHostApi.js
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 构造供插件 activator 调用的宿主 API(路由、菜单、扩展点、资源注入等)。
|
|
3
|
-
* 与打包工具无关;Webpack 宿主需已配置 `vue-loader` 以编译本包内的 `.vue` 依赖。
|
|
4
|
-
*
|
|
5
|
-
* @module createHostApi
|
|
6
|
-
*/
|
|
7
|
-
import Vue from 'vue'
|
|
8
|
-
import { HOST_PLUGIN_API_VERSION } from './constants.js'
|
|
9
|
-
import { createRequestBridge } from './bridge.js'
|
|
10
|
-
import { defaultWebExtendPluginRuntime } from './default-runtime-config.js'
|
|
11
|
-
import { registries } from './registries.js'
|
|
12
|
-
import { registerPluginTeardown } from './teardown-registry.js'
|
|
13
|
-
|
|
14
|
-
/** 扩展点列表项 key 递增,避免用 Date.now() 导致列表重排时误卸载组件 */
|
|
15
|
-
let slotItemKeySeq = 0
|
|
16
|
-
/** 无 name 的动态路由合成名递增,避免多次 registerRoutes 重名 */
|
|
17
|
-
let routeSynthSeq = 0
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* @typedef {object} RegisterSlotEntry
|
|
21
|
-
* @property {import('vue').Component} component
|
|
22
|
-
* @property {number} [priority]
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @typedef {object} HostApi
|
|
27
|
-
* @property {string} hostPluginApiVersion 宿主实现的协议版本
|
|
28
|
-
* @property {(routes: import('vue-router').RouteConfig[]) => void} registerRoutes 注册路由;无 `name` 时自动生成稳定合成名;优先 `router.addRoute`
|
|
29
|
-
* @property {(items: object[]) => void} registerMenuItems 注册菜单并按 `order` 排序
|
|
30
|
-
* @property {(pointId: string, components: RegisterSlotEntry[]) => void} registerSlotComponents 向扩展点挂载组件
|
|
31
|
-
* @property {(urls?: string[]) => void} registerStylesheetUrls 注入 `link[rel=stylesheet]`,带 `data-plugin-asset`
|
|
32
|
-
* @property {(urls?: string[]) => void} registerScriptUrls 顺序注入外链脚本
|
|
33
|
-
* @property {() => void} registerSanitizedHtmlSnippet MVP 未实现,调用即抛错
|
|
34
|
-
* @property {() => ReturnType<typeof createRequestBridge>} getBridge 受控 `fetch` 代理
|
|
35
|
-
* @property {(pluginId: string, fn: () => void) => void} onTeardown 注册卸载回调;由 `disposeWebPlugin(pluginId)` 触发
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* @typedef {object} HostKitOptions
|
|
40
|
-
* @property {string[]} [bridgeAllowedPathPrefixes] 覆盖 `getBridge().request` 允许的 URL 前缀;默认见 `defaultWebExtendPluginRuntime.bridgeAllowedPathPrefixes`
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* 创建单个插件在宿主侧的 API 句柄,传入插件 `activator(hostApi)`。
|
|
45
|
-
*
|
|
46
|
-
* `bootstrapPlugins` 始终以 `(pluginId, router, hostKitOptions)` 调用工厂;请使用 `(id, r, kit) => createHostApi(id, r, kit)` 传入 `bridgeAllowedPathPrefixes`。
|
|
47
|
-
* 单参工厂 `(id) => createHostApi(id, router)` 仍可用(忽略后两个实参),此时 bridge 仅用包内默认前缀。
|
|
48
|
-
*
|
|
49
|
-
* @param {string} pluginId 与 manifest.id 一致
|
|
50
|
-
* @param {import('vue-router').default} router 宿主 Vue Router 实例(vue-router@3)
|
|
51
|
-
* @param {HostKitOptions} [hostKitOptions]
|
|
52
|
-
* @returns {HostApi}
|
|
53
|
-
*/
|
|
54
|
-
export function createHostApi(pluginId, router, hostKitOptions = {}) {
|
|
55
|
-
const bridgePrefixes =
|
|
56
|
-
Array.isArray(hostKitOptions.bridgeAllowedPathPrefixes) &&
|
|
57
|
-
hostKitOptions.bridgeAllowedPathPrefixes.length > 0
|
|
58
|
-
? hostKitOptions.bridgeAllowedPathPrefixes
|
|
59
|
-
: defaultWebExtendPluginRuntime.bridgeAllowedPathPrefixes
|
|
60
|
-
const bridge = createRequestBridge({ allowedPathPrefixes: bridgePrefixes })
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* 注入样式表;`disposeWebPlugin` 会按 `data-plugin-asset` 移除对应节点。
|
|
64
|
-
* @param {string} href
|
|
65
|
-
*/
|
|
66
|
-
function injectStylesheet(href) {
|
|
67
|
-
const link = document.createElement('link')
|
|
68
|
-
link.rel = 'stylesheet'
|
|
69
|
-
link.href = href
|
|
70
|
-
link.setAttribute('data-plugin-asset', pluginId)
|
|
71
|
-
document.head.appendChild(link)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* 注入外链脚本(用于插件额外资源,非清单主入口)。
|
|
76
|
-
* @param {string} src
|
|
77
|
-
* @returns {Promise<void>}
|
|
78
|
-
*/
|
|
79
|
-
function injectScript(src) {
|
|
80
|
-
return new Promise((resolve, reject) => {
|
|
81
|
-
const s = document.createElement('script')
|
|
82
|
-
s.async = true
|
|
83
|
-
s.src = src
|
|
84
|
-
s.setAttribute('data-plugin-asset', pluginId)
|
|
85
|
-
s.onload = () => resolve()
|
|
86
|
-
s.onerror = () => reject(new Error('script failed: ' + src))
|
|
87
|
-
document.head.appendChild(s)
|
|
88
|
-
})
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return {
|
|
92
|
-
hostPluginApiVersion: HOST_PLUGIN_API_VERSION,
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* 动态注册路由。Vue Router 3.5+ 推荐 `addRoute`;若不存在则回退已弃用的 `addRoutes`。
|
|
96
|
-
* @param {import('vue-router').RouteConfig[]} routes
|
|
97
|
-
*/
|
|
98
|
-
registerRoutes(routes) {
|
|
99
|
-
const wrapped = routes.map((r) => ({
|
|
100
|
-
...r,
|
|
101
|
-
name: r.name || `__wep_${pluginId}_${routeSynthSeq++}`,
|
|
102
|
-
meta: { ...(r.meta || {}), pluginId }
|
|
103
|
-
}))
|
|
104
|
-
if (typeof router.addRoute === 'function') {
|
|
105
|
-
for (const r of wrapped) {
|
|
106
|
-
router.addRoute(r)
|
|
107
|
-
}
|
|
108
|
-
} else {
|
|
109
|
-
router.addRoutes(wrapped)
|
|
110
|
-
}
|
|
111
|
-
},
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* 写入全局菜单注册表(响应式);按 `order` 升序排列。
|
|
115
|
-
* @param {object[]} items
|
|
116
|
-
*/
|
|
117
|
-
registerMenuItems(items) {
|
|
118
|
-
for (const item of items) {
|
|
119
|
-
registries.menus.push({ ...item, pluginId })
|
|
120
|
-
}
|
|
121
|
-
registries.menus.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
|
122
|
-
},
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* 向指定扩展点 id 注册 Vue 组件;`ExtensionPoint` 按 `priority` 降序渲染。
|
|
126
|
-
* @param {string} pointId
|
|
127
|
-
* @param {RegisterSlotEntry[]} components
|
|
128
|
-
*/
|
|
129
|
-
registerSlotComponents(pointId, components) {
|
|
130
|
-
if (!pointId) {
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
if (!registries.slots[pointId]) {
|
|
134
|
-
Vue.set(registries.slots, pointId, [])
|
|
135
|
-
}
|
|
136
|
-
const list = registries.slots[pointId]
|
|
137
|
-
for (const c of components) {
|
|
138
|
-
list.push({
|
|
139
|
-
pluginId,
|
|
140
|
-
component: c.component,
|
|
141
|
-
priority: c.priority ?? 0,
|
|
142
|
-
key: `${pluginId}-${pointId}-${++slotItemKeySeq}`
|
|
143
|
-
})
|
|
144
|
-
}
|
|
145
|
-
list.sort((a, b) => b.priority - a.priority)
|
|
146
|
-
registries.slotRevision++
|
|
147
|
-
},
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* @param {string[]|undefined} urls
|
|
151
|
-
*/
|
|
152
|
-
registerStylesheetUrls(urls) {
|
|
153
|
-
for (const u of urls || []) {
|
|
154
|
-
if (typeof u === 'string' && u) {
|
|
155
|
-
injectStylesheet(u)
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
},
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* 串行加载多个脚本,失败仅告警不中断宿主。
|
|
162
|
-
* @param {string[]|undefined} urls
|
|
163
|
-
*/
|
|
164
|
-
registerScriptUrls(urls) {
|
|
165
|
-
const chain = (urls || []).filter((u) => typeof u === 'string' && u).reduce(
|
|
166
|
-
(p, u) => p.then(() => injectScript(u)),
|
|
167
|
-
Promise.resolve()
|
|
168
|
-
)
|
|
169
|
-
chain.catch((e) => console.warn('[plugins] registerScriptUrls', pluginId, e))
|
|
170
|
-
},
|
|
171
|
-
|
|
172
|
-
registerSanitizedHtmlSnippet() {
|
|
173
|
-
throw new Error('registerSanitizedHtmlSnippet is not enabled in MVP')
|
|
174
|
-
},
|
|
175
|
-
|
|
176
|
-
getBridge: () => bridge,
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* 插件卸载前清理逻辑;第一个参数为预留与协议对齐,实际以创建 API 时的 `pluginId` 为准。
|
|
180
|
-
* @param {string} _pluginId 预留,与 manifest.id 一致时可传入
|
|
181
|
-
* @param {() => void} fn
|
|
182
|
-
*/
|
|
183
|
-
onTeardown(_pluginId, fn) {
|
|
184
|
-
if (typeof fn === 'function') {
|
|
185
|
-
registerPluginTeardown(pluginId, fn)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 宿主可覆盖的默认运行时配置(路径、白名单、超时等)。
|
|
3
|
-
* 使用方式:`resolveRuntimeOptions({ ...defaultWebExtendPluginRuntime, manifestListPath: '/api/my-plugins' })`
|
|
4
|
-
* 或只传需要改动的字段:`resolveRuntimeOptions({ manifestListPath: '/api/my-plugins' })`。
|
|
5
|
-
*
|
|
6
|
-
* @module default-runtime-config
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @typedef {typeof defaultWebExtendPluginRuntime} WebExtendPluginDefaultRuntime
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
export const defaultWebExtendPluginRuntime = {
|
|
14
|
-
/** 清单 HTTP 服务前缀(与后端 context-path 对齐),不含尾部 `/` */
|
|
15
|
-
manifestBase: '/fp-api',
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* 拉取插件清单的 **路径段**(以 `/` 开头),拼在 `manifestBase` 之后。
|
|
19
|
-
* 完整 URL:`{manifestBase}{manifestListPath}` → 默认 `/fp-api/api/frontend-plugins`
|
|
20
|
-
*/
|
|
21
|
-
manifestListPath: '/api/frontend-plugins',
|
|
22
|
-
|
|
23
|
-
/** 清单 `fetch` 的 `credentials`,需 Cookie 会话时用 `include` */
|
|
24
|
-
manifestFetchCredentials: 'include',
|
|
25
|
-
|
|
26
|
-
/** 插件 dev 服务存活探测路径(拼在 `webPluginDevOrigin` 后) */
|
|
27
|
-
devPingPath: '/__web_plugin_dev_ping',
|
|
28
|
-
|
|
29
|
-
/** 插件 dev 热更新 SSE 路径(拼在插件 dev 的 origin 后) */
|
|
30
|
-
devReloadSsePath: '/__web_plugin_reload_stream',
|
|
31
|
-
|
|
32
|
-
/** 隐式 dev 映射里,每个 id 对应的入口路径(相对插件 dev origin) */
|
|
33
|
-
webPluginDevEntryPath: '/src/plugin-entry.js',
|
|
34
|
-
|
|
35
|
-
/** `fetch(devPingUrl)` 超时毫秒数 */
|
|
36
|
-
devPingTimeoutMs: 500,
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* **隐式 dev 映射**(可选):开发模式下若 `webPluginDevOrigin` 上 ping 成功,运行时会为若干插件 id 自动生成
|
|
40
|
-
* `{ id → origin + webPluginDevEntryPath }`,从而用 dev 服务入口替代清单里的 dist,便于热更新联调。
|
|
41
|
-
*
|
|
42
|
-
* 插件 id 来源优先级:`webPluginDevIds`(或 `VITE_WEB_PLUGIN_DEV_IDS`)→ 本字段。**默认 `[]`**,
|
|
43
|
-
* 避免把仓库示例 id 写进通用宿主;联调时在 `.env` 里设 `VITE_WEB_PLUGIN_DEV_IDS=com.xxx,com.yyy`,
|
|
44
|
-
* 或 `resolveRuntimeOptions({ defaultImplicitDevPluginIds: ['com.xxx'] })` 即可。
|
|
45
|
-
*/
|
|
46
|
-
defaultImplicitDevPluginIds: [],
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* 允许通过 `<script>` / 动态 `import()` 加载的插件脚本所在主机名(小写),防误配公网 URL。
|
|
50
|
-
*/
|
|
51
|
-
allowedScriptHosts: ['localhost', '127.0.0.1', '::1'],
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* `hostApi.getBridge().request(path)` 允许的 path 前缀;须以 `/` 开头。
|
|
55
|
-
* 需与后端实际 API 前缀一致。
|
|
56
|
-
*/
|
|
57
|
-
bridgeAllowedPathPrefixes: ['/api/']
|
|
58
|
-
}
|
package/src/dispose-plugin.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 单插件卸载:与 `bootstrapPlugins` / `createHostApi` 对称,清理注册表与 DOM 副作用。
|
|
3
|
-
*
|
|
4
|
-
* @module dispose-plugin
|
|
5
|
-
*/
|
|
6
|
-
import Vue from 'vue'
|
|
7
|
-
import { registries } from './registries.js'
|
|
8
|
-
import { runPluginTeardowns } from './teardown-registry.js'
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* 卸载指定 id 的插件:依次执行 teardown、移除菜单与扩展点条目、删除 activator、移除带 `data-plugin-asset` 的节点。
|
|
12
|
-
*
|
|
13
|
-
* **路由**:Vue Router 3 无公开 `removeRoute`,此处不改动 matcher;动态路由需整页刷新或自行维护路由表。
|
|
14
|
-
*
|
|
15
|
-
* @param {string} pluginId 与 manifest.id 一致
|
|
16
|
-
*/
|
|
17
|
-
export function disposeWebPlugin(pluginId) {
|
|
18
|
-
if (!pluginId || typeof pluginId !== 'string') {
|
|
19
|
-
return
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
runPluginTeardowns(pluginId)
|
|
23
|
-
|
|
24
|
-
for (let i = registries.menus.length - 1; i >= 0; i--) {
|
|
25
|
-
if (registries.menus[i].pluginId === pluginId) {
|
|
26
|
-
registries.menus.splice(i, 1)
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const slots = registries.slots
|
|
31
|
-
for (const pointId of Object.keys(slots)) {
|
|
32
|
-
const list = slots[pointId]
|
|
33
|
-
if (!Array.isArray(list)) {
|
|
34
|
-
continue
|
|
35
|
-
}
|
|
36
|
-
const next = list.filter((x) => x.pluginId !== pluginId)
|
|
37
|
-
if (next.length === 0) {
|
|
38
|
-
Vue.delete(slots, pointId)
|
|
39
|
-
} else if (next.length !== list.length) {
|
|
40
|
-
Vue.set(slots, pointId, next)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
registries.slotRevision++
|
|
44
|
-
|
|
45
|
-
if (typeof window !== 'undefined' && window.__PLUGIN_ACTIVATORS__) {
|
|
46
|
-
delete window.__PLUGIN_ACTIVATORS__[pluginId]
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (typeof document !== 'undefined') {
|
|
50
|
-
document.querySelectorAll('[data-plugin-asset]').forEach((el) => {
|
|
51
|
-
if (el.getAttribute('data-plugin-asset') === pluginId) {
|
|
52
|
-
el.remove()
|
|
53
|
-
}
|
|
54
|
-
})
|
|
55
|
-
}
|
|
56
|
-
}
|
package/src/index.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vue 2.7 宿主 Web 扩展插件运行时公共入口。
|
|
3
|
-
* @see README 中 Webpack 宿主需配置 DefinePlugin 或显式 `resolveRuntimeOptions`
|
|
4
|
-
*/
|
|
5
|
-
export { defaultWebExtendPluginRuntime } from './default-runtime-config.js'
|
|
6
|
-
export { bootstrapPlugins, resolveRuntimeOptions } from './PluginRuntime.js'
|
|
7
|
-
export { createHostApi } from './createHostApi.js'
|
|
8
|
-
export { disposeWebPlugin } from './dispose-plugin.js'
|
|
9
|
-
export { registries } from './registries.js'
|
|
10
|
-
export { createRequestBridge } from './bridge.js'
|
|
11
|
-
export { HOST_PLUGIN_API_VERSION } from './constants.js'
|
|
12
|
-
export { default as ExtensionPoint } from './components/ExtensionPoint.vue'
|
package/src/registries.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 宿主全局响应式注册表:菜单与扩展点槽位,供布局与 `ExtensionPoint` 订阅。
|
|
3
|
-
*
|
|
4
|
-
* @module registries
|
|
5
|
-
*/
|
|
6
|
-
import Vue from 'vue'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @type {{
|
|
10
|
-
* menus: object[],
|
|
11
|
-
* slots: Record<string, Array<{ pluginId: string, component: import('vue').Component, priority: number, key: string }>>
|
|
12
|
-
* }}
|
|
13
|
-
*/
|
|
14
|
-
export const registries = Vue.observable({
|
|
15
|
-
menus: [],
|
|
16
|
-
/** 扩展点 id → 已注册组件列表(内容区 / 工具栏等共用模型) */
|
|
17
|
-
slots: {},
|
|
18
|
-
/**
|
|
19
|
-
* 每次变更 slots 时递增,供 ExtensionPoint 计算属性显式依赖。
|
|
20
|
-
* Vue 2 对「先访问不存在的 slots[key]、后 Vue.set 补 key」的依赖收集不可靠,会导致扩展点不刷新。
|
|
21
|
-
*/
|
|
22
|
-
slotRevision: 0
|
|
23
|
-
})
|
package/src/teardown-registry.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 按 `pluginId` 收集 `onTeardown` 回调,供 `disposeWebPlugin` 统一执行。
|
|
3
|
-
*
|
|
4
|
-
* @module teardown-registry
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/** @type {Map<string, Function[]>} */
|
|
8
|
-
const byPlugin = new Map()
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* 登记插件卸载时要执行的同步回调(建议只做解绑、清定时器等轻量逻辑)。
|
|
12
|
-
* @param {string} pluginId
|
|
13
|
-
* @param {() => void} fn
|
|
14
|
-
*/
|
|
15
|
-
export function registerPluginTeardown(pluginId, fn) {
|
|
16
|
-
if (typeof fn !== 'function') {
|
|
17
|
-
return
|
|
18
|
-
}
|
|
19
|
-
let arr = byPlugin.get(pluginId)
|
|
20
|
-
if (!arr) {
|
|
21
|
-
arr = []
|
|
22
|
-
byPlugin.set(pluginId, arr)
|
|
23
|
-
}
|
|
24
|
-
arr.push(fn)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* 执行并清空该插件已登记的全部 teardown;调用后 Map 中不再保留该 id。
|
|
29
|
-
* @param {string} pluginId
|
|
30
|
-
*/
|
|
31
|
-
export function runPluginTeardowns(pluginId) {
|
|
32
|
-
const arr = byPlugin.get(pluginId)
|
|
33
|
-
if (!arr) {
|
|
34
|
-
return
|
|
35
|
-
}
|
|
36
|
-
byPlugin.delete(pluginId)
|
|
37
|
-
for (const fn of arr) {
|
|
38
|
-
try {
|
|
39
|
-
fn()
|
|
40
|
-
} catch (e) {
|
|
41
|
-
console.warn('[plugins] teardown failed', pluginId, e)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|