web-extend-plugin-vue2 0.1.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 ADDED
@@ -0,0 +1,22 @@
1
+ # web-extend-plugin-vue2
2
+
3
+ Vue 2.7 宿主侧 **Web 扩展插件**运行时:拉取清单、`hostApi`、菜单与 **`registerSlotComponents` 注册表**,以及 **`ExtensionPoint`**(同一组件覆盖内容区、工具栏等任意扩展点;插件在该点挂载 Vue 组件即可)。
4
+
5
+ ## 依赖
6
+
7
+ - `peerDependencies`: `vue@^2.7`, `vue-router@^3.6`
8
+ - `dependencies`: `semver`
9
+
10
+ ## 在 Vite 宿主中使用
11
+
12
+ ```js
13
+ import { bootstrapPlugins, createHostApi, registries } from 'web-extend-plugin-vue2'
14
+
15
+ bootstrapPlugins(router, (pluginId) => createHostApi(pluginId, router)).catch(console.warn)
16
+ ```
17
+
18
+ 开发环境下清单前缀、插件 Vite 联调等默认从 `import.meta.env`(`VITE_FRONTEND_PLUGIN_BASE`、`VITE_WEB_PLUGIN_DEV_*`)读取。非 Vite 或需覆盖时可传入第三参数,见源码 `resolveRuntimeOptions` / `WebExtendPluginRuntimeOptions`。
19
+
20
+ ## 发布到 npm
21
+
22
+ 在包目录执行 `npm publish`(需有权限)。本仓库可用 `file:../web-extend-plugin-vue2` 联调示例宿主。
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "web-extend-plugin-vue2",
3
+ "version": "0.1.0",
4
+ "description": "Vue 2.7 host runtime for web-extend-plugin: manifest bootstrap, hostApi, registries, ExtensionPoint",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "files": [
11
+ "src"
12
+ ],
13
+ "peerDependencies": {
14
+ "vue": "^2.7.0",
15
+ "vue-router": "^3.6.0"
16
+ },
17
+ "dependencies": {
18
+ "semver": "^7.6.3"
19
+ },
20
+ "devDependencies": {
21
+ "vitest": "^2.1.9",
22
+ "vue": "^2.7.16",
23
+ "vue-router": "^3.6.5"
24
+ },
25
+ "scripts": {
26
+ "test": "vitest run"
27
+ },
28
+ "license": "Apache-2.0",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/xtemplus/extend-plugin-framework.git",
32
+ "directory": "web-extend-plugin-vue2"
33
+ },
34
+ "keywords": [
35
+ "vue2",
36
+ "plugin",
37
+ "extension"
38
+ ]
39
+ }
@@ -0,0 +1,347 @@
1
+ import * as semver from 'semver'
2
+ import { HOST_PLUGIN_API_VERSION } from './constants.js'
3
+
4
+ /**
5
+ * @typedef {object} WebExtendPluginRuntimeOptions
6
+ * @property {string} [manifestBase] 清单服务 URL 前缀,默认 `/fp-api` 或 `VITE_FRONTEND_PLUGIN_BASE`
7
+ * @property {boolean} [isDev] 是否开发模式;默认取 Vite `import.meta.env.DEV`
8
+ * @property {string} [webPluginDevOrigin] 插件 Vite dev origin,如 `http://localhost:5188`
9
+ * @property {string} [webPluginDevIds] 逗号分隔插件 id,用于隐式 dev 入口
10
+ * @property {string} [webPluginDevMapJson] JSON 字符串,id → 入口 URL,等价 `VITE_WEB_PLUGIN_DEV_MAP`
11
+ */
12
+
13
+ /** 与清单里 entryUrl 的 hostname 一致;含 IPv6 本机,避免部分环境解析 localhost → ::1 后被宿主拒绝 */
14
+ const DEFAULT_ALLOWED_HOSTS = new Set(['localhost', '127.0.0.1', '::1'])
15
+
16
+ function viteEnv(key, fallback = '') {
17
+ try {
18
+ if (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env[key] !== undefined) {
19
+ const v = import.meta.env[key]
20
+ if (v !== '' && v !== undefined) return v
21
+ }
22
+ } catch (_) {
23
+ /* non-Vite bundler */
24
+ }
25
+ return fallback
26
+ }
27
+
28
+ function viteIsDev() {
29
+ try {
30
+ return !!import.meta.env.DEV
31
+ } catch (_) {
32
+ return false
33
+ }
34
+ }
35
+
36
+ /**
37
+ * 合并宿主传入项与 Vite 环境变量(便于 npm 包在 Vite 宿主中零配置;亦可在非 Vite 场景显式传入)。
38
+ * @param {WebExtendPluginRuntimeOptions} [user]
39
+ */
40
+ export function resolveRuntimeOptions(user = {}) {
41
+ return {
42
+ manifestBase:
43
+ user.manifestBase !== undefined && user.manifestBase !== ''
44
+ ? user.manifestBase
45
+ : viteEnv('VITE_FRONTEND_PLUGIN_BASE', '/fp-api') || '/fp-api',
46
+ isDev: user.isDev !== undefined ? user.isDev : viteIsDev(),
47
+ webPluginDevOrigin:
48
+ user.webPluginDevOrigin !== undefined
49
+ ? user.webPluginDevOrigin
50
+ : viteEnv('VITE_WEB_PLUGIN_DEV_ORIGIN', ''),
51
+ webPluginDevIds:
52
+ user.webPluginDevIds !== undefined
53
+ ? user.webPluginDevIds
54
+ : viteEnv('VITE_WEB_PLUGIN_DEV_IDS', ''),
55
+ webPluginDevMapJson:
56
+ user.webPluginDevMapJson !== undefined
57
+ ? user.webPluginDevMapJson
58
+ : viteEnv('VITE_WEB_PLUGIN_DEV_MAP', '')
59
+ }
60
+ }
61
+
62
+ function normalizeHost(hostname) {
63
+ if (!hostname) return ''
64
+ const h = hostname.toLowerCase()
65
+ if (h.startsWith('[') && h.endsWith(']')) {
66
+ return h.slice(1, -1)
67
+ }
68
+ return h
69
+ }
70
+
71
+ function isScriptHostAllowed(url) {
72
+ try {
73
+ const u = new URL(url, window.location.origin)
74
+ const h = normalizeHost(u.hostname)
75
+ return DEFAULT_ALLOWED_HOSTS.has(h)
76
+ } catch {
77
+ return false
78
+ }
79
+ }
80
+
81
+ function parseWebPluginDevMapExplicit(opts) {
82
+ if (!opts.isDev) {
83
+ return null
84
+ }
85
+ const raw = opts.webPluginDevMapJson
86
+ if (raw === undefined || raw === null || String(raw).trim() === '') {
87
+ return null
88
+ }
89
+ try {
90
+ const map = JSON.parse(String(raw))
91
+ return map && typeof map === 'object' ? map : null
92
+ } catch {
93
+ console.warn('[plugins] VITE_WEB_PLUGIN_DEV_MAP is not valid JSON')
94
+ return null
95
+ }
96
+ }
97
+
98
+ async function buildImplicitWebPluginDevMap(opts) {
99
+ if (!opts.isDev) {
100
+ return {}
101
+ }
102
+ const origin = opts.webPluginDevOrigin === undefined || opts.webPluginDevOrigin === null
103
+ ? ''
104
+ : String(opts.webPluginDevOrigin).trim()
105
+ if (!origin) {
106
+ return {}
107
+ }
108
+ if (!isScriptHostAllowed(`${origin}/`)) {
109
+ return {}
110
+ }
111
+ const base = origin.replace(/\/$/, '')
112
+ const pingUrl = `${base}/__web_plugin_dev_ping`
113
+ try {
114
+ const ctrl = new AbortController()
115
+ const timer = setTimeout(() => ctrl.abort(), 500)
116
+ const r = await fetch(pingUrl, {
117
+ mode: 'cors',
118
+ cache: 'no-store',
119
+ signal: ctrl.signal
120
+ })
121
+ clearTimeout(timer)
122
+ if (!r.ok) {
123
+ return {}
124
+ }
125
+ const body = (await r.text()).trim()
126
+ if (body !== 'ok') {
127
+ return {}
128
+ }
129
+ } catch {
130
+ return {}
131
+ }
132
+
133
+ const idsRaw = opts.webPluginDevIds
134
+ const ids =
135
+ idsRaw !== undefined && idsRaw !== null && String(idsRaw).trim() !== ''
136
+ ? String(idsRaw)
137
+ .split(',')
138
+ .map((s) => s.trim())
139
+ .filter(Boolean)
140
+ : ['com.example.frontend.demo']
141
+
142
+ const map = {}
143
+ for (const id of ids) {
144
+ map[id] = `${base}/src/plugin-entry.js`
145
+ }
146
+ console.info(
147
+ '[plugins] 已检测到插件 Vite(',
148
+ base,
149
+ '),下列 id 将加载源码入口而非清单 dist:',
150
+ ids.join(', ')
151
+ )
152
+ return map
153
+ }
154
+
155
+ function mergeDevMaps(implicit, explicit) {
156
+ const i = implicit && typeof implicit === 'object' ? implicit : {}
157
+ const e = explicit && typeof explicit === 'object' ? explicit : {}
158
+ return { ...i, ...e }
159
+ }
160
+
161
+ /** @type {Set<string>} */
162
+ const pluginDevSseStarted = typeof window !== 'undefined' ? new Set() : new Set()
163
+
164
+ function isDevOriginAllowedForSse(origin) {
165
+ try {
166
+ const u = new URL(origin)
167
+ return DEFAULT_ALLOWED_HOSTS.has(normalizeHost(u.hostname))
168
+ } catch {
169
+ return false
170
+ }
171
+ }
172
+
173
+ function startPluginDevReloadSse(origin, isDev) {
174
+ if (!isDev || pluginDevSseStarted.has(origin)) {
175
+ return
176
+ }
177
+ if (!isDevOriginAllowedForSse(origin)) {
178
+ return
179
+ }
180
+ pluginDevSseStarted.add(origin)
181
+ const base = origin.replace(/\/$/, '')
182
+ const url = `${base}/__web_plugin_reload_stream`
183
+ try {
184
+ const es = new EventSource(url)
185
+ es.addEventListener('reload', () => {
186
+ window.location.reload()
187
+ })
188
+ es.onopen = () => {
189
+ console.info('[plugins] plugin dev reload SSE:', url)
190
+ }
191
+ } catch (e) {
192
+ console.warn('[plugins] EventSource failed', url, e)
193
+ }
194
+ }
195
+
196
+ function startPluginDevSseForMap(devMap, isDev) {
197
+ if (!isDev || !devMap) {
198
+ return
199
+ }
200
+ const origins = new Set()
201
+ for (const entry of Object.values(devMap)) {
202
+ if (typeof entry !== 'string') {
203
+ continue
204
+ }
205
+ const t = entry.trim()
206
+ if (!t) {
207
+ continue
208
+ }
209
+ try {
210
+ origins.add(new URL(t, window.location.href).origin)
211
+ } catch {
212
+ /* skip */
213
+ }
214
+ }
215
+ for (const o of origins) {
216
+ startPluginDevReloadSse(o, isDev)
217
+ }
218
+ }
219
+
220
+ function loadScript(src) {
221
+ return new Promise((resolve, reject) => {
222
+ const s = document.createElement('script')
223
+ s.async = true
224
+ s.src = src
225
+ s.onload = () => resolve()
226
+ s.onerror = () => reject(new Error('loadScript failed: ' + src))
227
+ document.head.appendChild(s)
228
+ })
229
+ }
230
+
231
+ async function loadPluginEntry(p, entryUrl, devMap) {
232
+ const devEntry = devMap && typeof devMap[p.id] === 'string' ? devMap[p.id].trim() : ''
233
+ if (devEntry) {
234
+ if (!isScriptHostAllowed(devEntry)) {
235
+ console.warn('[plugins] dev entry URL not allowed', p.id, devEntry)
236
+ return
237
+ }
238
+ try {
239
+ await import(/* @vite-ignore */ devEntry)
240
+ } catch (e) {
241
+ console.warn('[plugins] dev module import failed, try manifest entryUrl', p.id, e)
242
+ if (entryUrl && isScriptHostAllowed(entryUrl)) {
243
+ await loadScript(entryUrl)
244
+ }
245
+ return
246
+ }
247
+ return
248
+ }
249
+ if (!entryUrl || !isScriptHostAllowed(entryUrl)) {
250
+ console.warn('[plugins] skip (entryUrl not allowed)', p.id, entryUrl)
251
+ return
252
+ }
253
+ await loadScript(entryUrl)
254
+ }
255
+
256
+ /**
257
+ * @param {import('vue-router').default} router
258
+ * @param {(pluginId: string) => object} createHostApi
259
+ * @param {WebExtendPluginRuntimeOptions} [runtimeOptions] 可选;不传则从 Vite `import.meta.env` 读取(若存在)
260
+ */
261
+ export async function bootstrapPlugins(router, createHostApi, runtimeOptions) {
262
+ const opts = resolveRuntimeOptions(runtimeOptions || {})
263
+ const base = String(opts.manifestBase).replace(/\/$/, '')
264
+ const explicit = parseWebPluginDevMapExplicit(opts)
265
+
266
+ const [manifestResult, implicit] = await Promise.all([
267
+ (async () => {
268
+ try {
269
+ const res = await fetch(`${base}/api/frontend-plugins`, { credentials: 'include' })
270
+ if (!res.ok) {
271
+ return { ok: false, status: res.status, data: null }
272
+ }
273
+ const data = await res.json()
274
+ return { ok: true, data }
275
+ } catch (e) {
276
+ return { ok: false, error: e, data: null }
277
+ }
278
+ })(),
279
+ buildImplicitWebPluginDevMap(opts)
280
+ ])
281
+
282
+ const devMap = mergeDevMaps(implicit, explicit)
283
+ startPluginDevSseForMap(devMap, opts.isDev)
284
+
285
+ if (!manifestResult.ok) {
286
+ if (manifestResult.error) {
287
+ console.warn('[plugins] fetch manifest failed', manifestResult.error)
288
+ } else {
289
+ console.warn('[plugins] manifest HTTP', manifestResult.status)
290
+ }
291
+ return
292
+ }
293
+ /** @type {{ hostPluginApiVersion?: string, plugins?: object[] }} */
294
+ const data = manifestResult.data
295
+ if (!data) {
296
+ return
297
+ }
298
+
299
+ const apiVer = data.hostPluginApiVersion
300
+ if (apiVer) {
301
+ const coerced = semver.coerce(apiVer)
302
+ const maj = coerced ? coerced.major : 0
303
+ const range = `^${maj}.0.0`
304
+ if (!semver.satisfies(HOST_PLUGIN_API_VERSION, range, { includePrerelease: true })) {
305
+ console.warn(
306
+ '[plugins] host API version mismatch: host implements',
307
+ HOST_PLUGIN_API_VERSION,
308
+ 'server declares',
309
+ apiVer
310
+ )
311
+ }
312
+ }
313
+
314
+ window.__PLUGIN_ACTIVATORS__ = window.__PLUGIN_ACTIVATORS__ || {}
315
+
316
+ const plugins = data.plugins || []
317
+ if (plugins.length === 0) {
318
+ console.info(
319
+ '[plugins] 清单为空。请检查:① web-extend-plugin-server 是否已启动;② 其 JVM 工作目录下 web-plugins-dir 是否指向含 manifest 的目录(通常应在 web-extend-plugin-server 目录执行 mvn spring-boot:run,且存在 ../web-plugins/子目录);③ 浏览器访问清单 URL 是否有 plugins 条目。'
320
+ )
321
+ }
322
+ for (const p of plugins) {
323
+ const range = p.engines && p.engines.host
324
+ if (range && !semver.satisfies(HOST_PLUGIN_API_VERSION, range, { includePrerelease: true })) {
325
+ console.warn('[plugins] skip (engines.host)', p.id, range)
326
+ continue
327
+ }
328
+ const entryUrl = p.entryUrl
329
+ try {
330
+ await loadPluginEntry(p, entryUrl, devMap)
331
+ } catch (e) {
332
+ console.warn('[plugins] script load failed', p.id, e)
333
+ continue
334
+ }
335
+ const activator = window.__PLUGIN_ACTIVATORS__[p.id]
336
+ if (typeof activator !== 'function') {
337
+ console.warn('[plugins] no activator for', p.id)
338
+ continue
339
+ }
340
+ const hostApi = createHostApi(p.id)
341
+ try {
342
+ activator(hostApi)
343
+ } catch (e) {
344
+ console.error('[plugins] activate failed', p.id, e)
345
+ }
346
+ }
347
+ }
package/src/bridge.js ADDED
@@ -0,0 +1,22 @@
1
+ const ALLOWED_PREFIXES = ['/api/']
2
+
3
+ /**
4
+ * @returns {{ request: (path: string, init?: RequestInit) => Promise<Response> }}
5
+ */
6
+ export function createRequestBridge() {
7
+ return {
8
+ async request(path, init = {}) {
9
+ if (typeof path !== 'string' || !path.startsWith('/')) {
10
+ throw new Error('[bridge] path must be a string starting with /')
11
+ }
12
+ const allowed = ALLOWED_PREFIXES.some((p) => path.startsWith(p))
13
+ if (!allowed) {
14
+ throw new Error('[bridge] path not allowed: ' + path)
15
+ }
16
+ return fetch(path, {
17
+ credentials: 'same-origin',
18
+ ...init
19
+ })
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,29 @@
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
+ })
@@ -0,0 +1,62 @@
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
+ import { registries } from '../registries.js'
15
+
16
+ const SlotErrorBoundary = {
17
+ name: 'SlotErrorBoundary',
18
+ props: { label: String },
19
+ data() {
20
+ return { error: null }
21
+ },
22
+ errorCaptured(err) {
23
+ this.error = err && err.message ? err.message : String(err)
24
+ console.error('[ExtensionPoint] render error in', this.label, err)
25
+ return false
26
+ },
27
+ render(h) {
28
+ if (this.error) {
29
+ return h(
30
+ 'div',
31
+ { class: 'plugin-point-error', style: { color: '#c00', fontSize: '12px' } },
32
+ `[插件 ${this.label}] 渲染失败`
33
+ )
34
+ }
35
+ const d = this.$slots.default
36
+ return d && d[0] ? d[0] : h('span')
37
+ }
38
+ }
39
+
40
+ export default {
41
+ name: 'ExtensionPoint',
42
+ components: { SlotErrorBoundary },
43
+ props: {
44
+ pointId: { type: String, required: true },
45
+ slotProps: { type: Object, default: () => ({}) }
46
+ },
47
+ computed: {
48
+ items() {
49
+ return registries.slots[this.pointId] || []
50
+ },
51
+ forwardProps() {
52
+ return this.slotProps || {}
53
+ }
54
+ }
55
+ }
56
+ </script>
57
+
58
+ <style scoped>
59
+ .extension-point {
60
+ min-height: 8px;
61
+ }
62
+ </style>
@@ -0,0 +1,2 @@
1
+ /** Must match web-extend-plugin-server FrontendPluginsResponse.hostPluginApiVersion */
2
+ export const HOST_PLUGIN_API_VERSION = '1.0.0'
@@ -0,0 +1,93 @@
1
+ import Vue from 'vue'
2
+ import { HOST_PLUGIN_API_VERSION } from './constants.js'
3
+ import { createRequestBridge } from './bridge.js'
4
+ import { registries } from './registries.js'
5
+
6
+ /**
7
+ * @param {string} pluginId
8
+ * @param {import('vue-router').default} router
9
+ */
10
+ export function createHostApi(pluginId, router) {
11
+ const bridge = createRequestBridge()
12
+ const teardowns = []
13
+
14
+ function injectStylesheet(href) {
15
+ const link = document.createElement('link')
16
+ link.rel = 'stylesheet'
17
+ link.href = href
18
+ link.setAttribute('data-plugin-asset', pluginId)
19
+ document.head.appendChild(link)
20
+ }
21
+
22
+ function injectScript(src) {
23
+ return new Promise((resolve, reject) => {
24
+ const s = document.createElement('script')
25
+ s.async = true
26
+ s.src = src
27
+ s.setAttribute('data-plugin-asset', pluginId)
28
+ s.onload = () => resolve()
29
+ s.onerror = () => reject(new Error('script failed: ' + src))
30
+ document.head.appendChild(s)
31
+ })
32
+ }
33
+
34
+ return {
35
+ hostPluginApiVersion: HOST_PLUGIN_API_VERSION,
36
+
37
+ registerRoutes(routes) {
38
+ const wrapped = routes.map((r) => ({
39
+ ...r,
40
+ meta: { ...(r.meta || {}), pluginId }
41
+ }))
42
+ router.addRoutes(wrapped)
43
+ },
44
+
45
+ registerMenuItems(items) {
46
+ for (const item of items) {
47
+ registries.menus.push({ ...item, pluginId })
48
+ }
49
+ registries.menus.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
50
+ },
51
+
52
+ registerSlotComponents(pointId, components) {
53
+ if (!pointId) return
54
+ if (!registries.slots[pointId]) {
55
+ Vue.set(registries.slots, pointId, [])
56
+ }
57
+ const list = registries.slots[pointId]
58
+ for (const c of components) {
59
+ list.push({
60
+ pluginId,
61
+ component: c.component,
62
+ priority: c.priority ?? 0,
63
+ key: `${pluginId}-${pointId}-${list.length}-${Date.now()}`
64
+ })
65
+ }
66
+ list.sort((a, b) => b.priority - a.priority)
67
+ },
68
+
69
+ registerStylesheetUrls(urls) {
70
+ for (const u of urls || []) {
71
+ if (typeof u === 'string' && u) injectStylesheet(u)
72
+ }
73
+ },
74
+
75
+ registerScriptUrls(urls) {
76
+ const chain = (urls || []).filter((u) => typeof u === 'string' && u).reduce(
77
+ (p, u) => p.then(() => injectScript(u)),
78
+ Promise.resolve()
79
+ )
80
+ chain.catch((e) => console.warn('[plugins] registerScriptUrls', pluginId, e))
81
+ },
82
+
83
+ registerSanitizedHtmlSnippet() {
84
+ throw new Error('registerSanitizedHtmlSnippet is not enabled in MVP')
85
+ },
86
+
87
+ getBridge: () => bridge,
88
+
89
+ onTeardown(_pid, fn) {
90
+ if (typeof fn === 'function') teardowns.push(fn)
91
+ }
92
+ }
93
+ }
package/src/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { bootstrapPlugins, resolveRuntimeOptions } from './PluginRuntime.js'
2
+ export { createHostApi } from './createHostApi.js'
3
+ export { registries } from './registries.js'
4
+ export { createRequestBridge } from './bridge.js'
5
+ export { HOST_PLUGIN_API_VERSION } from './constants.js'
6
+ export { default as ExtensionPoint } from './components/ExtensionPoint.vue'
@@ -0,0 +1,10 @@
1
+ import Vue from 'vue'
2
+
3
+ /**
4
+ * Reactive registries for menus, extension slots, and button toolbars.
5
+ */
6
+ export const registries = Vue.observable({
7
+ menus: [],
8
+ /** pointId -> Array<{ pluginId, component, priority, key }>(工具栏等扩展点与内容区共用同一模型) */
9
+ slots: {}
10
+ })