web-extend-plugin-vue2 0.1.2 → 0.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "web-extend-plugin-vue2",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Vue 2.7 host runtime for web-extend-plugin: manifest bootstrap, hostApi, registries, ExtensionPoint",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -1,38 +1,38 @@
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
+ 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
+ })
@@ -50,6 +50,7 @@ export default {
50
50
  },
51
51
  computed: {
52
52
  items() {
53
+ void registries.slotRevision
53
54
  return registries.slots[this.pointId] || []
54
55
  },
55
56
  forwardProps() {
@@ -143,6 +143,7 @@ export function createHostApi(pluginId, router, hostKitOptions = {}) {
143
143
  })
144
144
  }
145
145
  list.sort((a, b) => b.priority - a.priority)
146
+ registries.slotRevision++
146
147
  },
147
148
 
148
149
  /**
@@ -1,58 +1,58 @@
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
- }
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
+ }
@@ -1,55 +1,56 @@
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
-
44
- if (typeof window !== 'undefined' && window.__PLUGIN_ACTIVATORS__) {
45
- delete window.__PLUGIN_ACTIVATORS__[pluginId]
46
- }
47
-
48
- if (typeof document !== 'undefined') {
49
- document.querySelectorAll('[data-plugin-asset]').forEach((el) => {
50
- if (el.getAttribute('data-plugin-asset') === pluginId) {
51
- el.remove()
52
- }
53
- })
54
- }
55
- }
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/registries.js CHANGED
@@ -14,5 +14,10 @@ import Vue from 'vue'
14
14
  export const registries = Vue.observable({
15
15
  menus: [],
16
16
  /** 扩展点 id → 已注册组件列表(内容区 / 工具栏等共用模型) */
17
- slots: {}
17
+ slots: {},
18
+ /**
19
+ * 每次变更 slots 时递增,供 ExtensionPoint 计算属性显式依赖。
20
+ * Vue 2 对「先访问不存在的 slots[key]、后 Vue.set 补 key」的依赖收集不可靠,会导致扩展点不刷新。
21
+ */
22
+ slotRevision: 0
18
23
  })
@@ -1,44 +1,44 @@
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
- }
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
+ }