@abraca/nuxt 2.15.0 → 2.16.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.
Files changed (26) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/aware/AMedia.d.vue.ts +1 -1
  3. package/dist/runtime/components/aware/AMedia.vue.d.ts +1 -1
  4. package/dist/runtime/components/registry/APluginBrowser.vue +18 -2
  5. package/dist/runtime/components/settings/APluginInstallDialog.d.vue.ts +39 -0
  6. package/dist/runtime/components/settings/APluginInstallDialog.vue +254 -0
  7. package/dist/runtime/components/settings/APluginInstallDialog.vue.d.ts +39 -0
  8. package/dist/runtime/components/settings/APluginsTabInstalled.d.vue.ts +7 -0
  9. package/dist/runtime/components/settings/APluginsTabInstalled.vue +413 -0
  10. package/dist/runtime/components/settings/APluginsTabInstalled.vue.d.ts +7 -0
  11. package/dist/runtime/components/settings/APluginsTabPending.d.vue.ts +24 -0
  12. package/dist/runtime/components/settings/APluginsTabPending.vue +248 -0
  13. package/dist/runtime/components/settings/APluginsTabPending.vue.d.ts +24 -0
  14. package/dist/runtime/components/settings/ASettingsPluginsPanel.d.vue.ts +14 -1
  15. package/dist/runtime/components/settings/ASettingsPluginsPanel.vue +34 -80
  16. package/dist/runtime/components/settings/ASettingsPluginsPanel.vue.d.ts +14 -1
  17. package/dist/runtime/composables/useDeclinedSpacePlugins.d.ts +7 -0
  18. package/dist/runtime/composables/useDeclinedSpacePlugins.js +24 -0
  19. package/dist/runtime/composables/useLoadTimePending.d.ts +29 -0
  20. package/dist/runtime/composables/useLoadTimePending.js +37 -0
  21. package/dist/runtime/composables/usePluginCatalog.d.ts +5 -1
  22. package/dist/runtime/composables/usePluginCatalog.js +34 -0
  23. package/dist/runtime/composables/useUploadedPluginStore.d.ts +43 -0
  24. package/dist/runtime/composables/useUploadedPluginStore.js +66 -0
  25. package/dist/runtime/plugin-abracadabra.client.js +48 -1
  26. package/package.json +1 -1
@@ -0,0 +1,24 @@
1
+ import type { PluginManifest } from '@abraca/plugin';
2
+ export interface PendingSpacePluginEntry {
3
+ /** Plugin id from the manifest. */
4
+ id: string;
5
+ /** Resolved bundle URL. */
6
+ url: string;
7
+ /** Version, when the space-plugins map records one. */
8
+ version?: string;
9
+ /** Resolved manifest, when the URL exposed a sibling JSON. */
10
+ manifest?: PluginManifest;
11
+ }
12
+ type __VLS_Props = {
13
+ /**
14
+ * Space-declared plugins that did NOT auto-load (not in registry).
15
+ * Pre-filtered by the host's space-plugins watcher. When omitted, the
16
+ * tab shows the empty-state explainer.
17
+ */
18
+ declared?: readonly PendingSpacePluginEntry[];
19
+ /** Active space id. Required for decline persistence to be per-space. */
20
+ spaceId?: string;
21
+ };
22
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
23
+ declare const _default: typeof __VLS_export;
24
+ export default _default;
@@ -1,3 +1,16 @@
1
- declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
1
+ export interface BuiltinPluginEntry {
2
+ name: string;
3
+ label?: string;
4
+ version?: string;
5
+ description?: string;
6
+ icon?: string;
7
+ }
8
+ type __VLS_Props = {
9
+ /** Host-registered first-party plugins; toggle state lives in localStorage. */
10
+ builtins?: readonly BuiltinPluginEntry[];
11
+ /** Initial tab. */
12
+ initialTab?: 'browse' | 'installed' | 'pending';
13
+ };
14
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
2
15
  declare const _default: typeof __VLS_export;
3
16
  export default _default;
@@ -1,96 +1,50 @@
1
1
  <script setup>
2
- import { computed } from "vue";
3
- import { usePluginRegistry } from "../../composables/usePluginRegistry";
4
- const registry = usePluginRegistry();
5
- const pluginList = computed(
6
- () => registry.getPlugins().map((p) => ({
7
- name: p.name,
8
- description: p.description ?? "",
9
- icon: p.icon ?? "i-lucide-plug",
10
- hasExtensions: !!p.extensions?.length,
11
- hasPageTypes: !!p.pageTypes?.length,
12
- hasToolbar: !!p.toolbarItems?.length,
13
- hasSuggestions: !!p.suggestionItems?.length
14
- }))
2
+ import { computed, ref } from "vue";
3
+ import { useAbracadabra } from "../../composables/useAbracadabra";
4
+ const props = defineProps({
5
+ builtins: { type: Array, required: false },
6
+ initialTab: { type: String, required: false }
7
+ });
8
+ const tab = ref(props.initialTab ?? "installed");
9
+ const builtins = computed(() => props.builtins ?? []);
10
+ const abra = useAbracadabra();
11
+ const serverUrl = computed(
12
+ () => abra.currentServerUrl?.value || void 0
15
13
  );
14
+ const tabItems = computed(() => [
15
+ { label: "Browse", value: "browse", icon: "i-lucide-store" },
16
+ { label: "Installed", value: "installed", icon: "i-lucide-package" },
17
+ { label: "Pending", value: "pending", icon: "i-lucide-hourglass" }
18
+ ]);
16
19
  </script>
17
20
 
18
21
  <template>
19
- <div>
20
- <div class="mb-4">
22
+ <div class="flex flex-col gap-4 max-w-3xl">
23
+ <div>
21
24
  <h3 class="text-base font-semibold">
22
25
  Plugins
23
26
  </h3>
24
27
  <p class="text-sm text-(--ui-text-muted) mt-1">
25
- Installed plugins extending the editor and platform.
28
+ Browse the registry, manage installed plugins, and review what the active space is asking to load.
26
29
  </p>
27
30
  </div>
28
31
 
29
- <div class="flex flex-col gap-2">
30
- <div
31
- v-for="plugin in pluginList"
32
- :key="plugin.name"
33
- class="flex items-center gap-3 p-3 rounded-lg border border-(--ui-border)"
34
- >
35
- <UIcon
36
- :name="plugin.icon"
37
- class="size-5 text-(--ui-text-muted) shrink-0"
38
- />
39
- <div class="flex-1 min-w-0">
40
- <p class="text-sm font-medium">
41
- {{ plugin.name }}
42
- </p>
43
- <p
44
- v-if="plugin.description"
45
- class="text-xs text-(--ui-text-muted) mt-0.5"
46
- >
47
- {{ plugin.description }}
48
- </p>
49
- <div class="flex flex-wrap gap-1 mt-1">
50
- <UBadge
51
- v-if="plugin.hasExtensions"
52
- color="info"
53
- variant="subtle"
54
- label="Extensions"
55
- size="xs"
56
- />
57
- <UBadge
58
- v-if="plugin.hasPageTypes"
59
- color="success"
60
- variant="subtle"
61
- label="Page Types"
62
- size="xs"
63
- />
64
- <UBadge
65
- v-if="plugin.hasToolbar"
66
- color="warning"
67
- variant="subtle"
68
- label="Toolbar"
69
- size="xs"
70
- />
71
- <UBadge
72
- v-if="plugin.hasSuggestions"
73
- color="neutral"
74
- variant="subtle"
75
- label="Suggestions"
76
- size="xs"
77
- />
78
- </div>
79
- </div>
80
- </div>
32
+ <UTabs
33
+ v-model="tab"
34
+ :items="tabItems"
35
+ :ui="{ list: 'w-fit' }"
36
+ />
81
37
 
82
- <div
83
- v-if="pluginList.length === 0"
84
- class="flex flex-col items-center justify-center py-12"
85
- >
86
- <UIcon
87
- name="i-lucide-plug"
88
- class="size-10 text-(--ui-text-dimmed) mb-3"
89
- />
90
- <p class="text-sm text-(--ui-text-muted)">
91
- No plugins installed.
92
- </p>
93
- </div>
38
+ <div v-if="tab === 'browse'" class="min-h-[24rem]">
39
+ <APluginBrowser :server-url="serverUrl" />
40
+ </div>
41
+
42
+ <div v-else-if="tab === 'installed'">
43
+ <APluginsTabInstalled :builtins="builtins" />
44
+ </div>
45
+
46
+ <div v-else-if="tab === 'pending'">
47
+ <APluginsTabPending />
94
48
  </div>
95
49
  </div>
96
50
  </template>
@@ -1,3 +1,16 @@
1
- declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
1
+ export interface BuiltinPluginEntry {
2
+ name: string;
3
+ label?: string;
4
+ version?: string;
5
+ description?: string;
6
+ icon?: string;
7
+ }
8
+ type __VLS_Props = {
9
+ /** Host-registered first-party plugins; toggle state lives in localStorage. */
10
+ builtins?: readonly BuiltinPluginEntry[];
11
+ /** Initial tab. */
12
+ initialTab?: 'browse' | 'installed' | 'pending';
13
+ };
14
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
2
15
  declare const _default: typeof __VLS_export;
3
16
  export default _default;
@@ -0,0 +1,7 @@
1
+ export declare function useDeclinedSpacePlugins(): {
2
+ declined: import("@vueuse/shared").RemovableRef<Record<string, string[]>>;
3
+ isDeclined: (spaceId: string, pluginId: string) => boolean;
4
+ decline: (spaceId: string, pluginId: string) => void;
5
+ reset: (spaceId: string, pluginId: string) => void;
6
+ clearSpace: (spaceId: string) => void;
7
+ };
@@ -0,0 +1,24 @@
1
+ import { useLocalStorage } from "@vueuse/core";
2
+ const STORAGE_KEY = "abracadabra_declined_space_plugins";
3
+ export function useDeclinedSpacePlugins() {
4
+ const declined = useLocalStorage(STORAGE_KEY, {});
5
+ function isDeclined(spaceId, pluginId) {
6
+ return (declined.value[spaceId] ?? []).includes(pluginId);
7
+ }
8
+ function decline(spaceId, pluginId) {
9
+ const cur = declined.value[spaceId] ?? [];
10
+ if (cur.includes(pluginId)) return;
11
+ declined.value = { ...declined.value, [spaceId]: [...cur, pluginId] };
12
+ }
13
+ function reset(spaceId, pluginId) {
14
+ const cur = declined.value[spaceId] ?? [];
15
+ if (!cur.includes(pluginId)) return;
16
+ declined.value = { ...declined.value, [spaceId]: cur.filter((id) => id !== pluginId) };
17
+ }
18
+ function clearSpace(spaceId) {
19
+ if (!declined.value[spaceId]) return;
20
+ const { [spaceId]: _, ...rest } = declined.value;
21
+ declined.value = rest;
22
+ }
23
+ return { declined, isDeclined, decline, reset, clearSpace };
24
+ }
@@ -0,0 +1,29 @@
1
+ import type { PluginCapability } from '@abraca/plugin';
2
+ export interface LoadTimePendingEntry {
3
+ /** Installed-entry URL (also the join key against `useInstalledPlugins`). */
4
+ url: string;
5
+ /** Plugin id from the freshly-fetched manifest. */
6
+ id: string;
7
+ /** Version we just observed upstream — DIFFERS from `acknowledgedVersion`. */
8
+ observedVersion: string;
9
+ /** Required capabilities currently declared upstream. */
10
+ observedRequired: PluginCapability[];
11
+ /**
12
+ * Why we queued — `"version-changed"`, `"capabilities-grew"`, or
13
+ * `"never-acknowledged"`. Drives the dialog header copy on review.
14
+ */
15
+ reason: 'version-changed' | 'capabilities-grew' | 'never-acknowledged';
16
+ /** Wall-clock when the loader queued this entry. */
17
+ queuedAt: number;
18
+ }
19
+ /** Loader-side: enqueue or replace an entry. Idempotent on `url`. */
20
+ export declare function _enqueueLoadTimePending(entry: LoadTimePendingEntry): void;
21
+ /** Loader / tab-side: drop an entry once the user re-acks or removes it. */
22
+ export declare function _clearLoadTimePending(url: string): void;
23
+ /** Loader-side: read once at boot to know what was previously queued. */
24
+ export declare function _readLoadTimePending(): LoadTimePendingEntry[];
25
+ export declare function useLoadTimePending(): {
26
+ entries: import("@vueuse/shared").RemovableRef<LoadTimePendingEntry[]>;
27
+ clear: (url: string) => void;
28
+ clearAll: () => void;
29
+ };
@@ -0,0 +1,37 @@
1
+ import { useLocalStorage } from "@vueuse/core";
2
+ const STORAGE_KEY = "abracadabra_load_time_pending";
3
+ function read() {
4
+ try {
5
+ return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "[]");
6
+ } catch {
7
+ return [];
8
+ }
9
+ }
10
+ function write(list) {
11
+ try {
12
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
13
+ } catch {
14
+ }
15
+ }
16
+ export function _enqueueLoadTimePending(entry) {
17
+ const list = read().filter((e) => e.url !== entry.url);
18
+ list.push(entry);
19
+ write(list);
20
+ }
21
+ export function _clearLoadTimePending(url) {
22
+ const list = read().filter((e) => e.url !== url);
23
+ write(list);
24
+ }
25
+ export function _readLoadTimePending() {
26
+ return read();
27
+ }
28
+ export function useLoadTimePending() {
29
+ const entries = useLocalStorage(STORAGE_KEY, []);
30
+ function clear(url) {
31
+ entries.value = entries.value.filter((e) => e.url !== url);
32
+ }
33
+ function clearAll() {
34
+ entries.value = [];
35
+ }
36
+ return { entries, clear, clearAll };
37
+ }
@@ -1,4 +1,4 @@
1
- import type { PluginManifest, PluginCapability } from '@abraca/plugin';
1
+ import type { PluginManifest, PluginCapability, ExternalPluginEntry } from '@abraca/plugin';
2
2
  export interface CatalogPlugin {
3
3
  id: string;
4
4
  name: string | null;
@@ -138,6 +138,10 @@ export declare function usePluginCatalog(): {
138
138
  required: PluginCapability[];
139
139
  optional: PluginCapability[];
140
140
  };
141
+ capabilitiesGrew: (prevAcked: readonly PluginCapability[] | undefined, currentRequired: readonly PluginCapability[]) => PluginCapability[];
142
+ needsTrustReprompt: (entry: Pick<ExternalPluginEntry, "trustAcknowledgedAt" | "acknowledgedVersion" | "acknowledgedCapabilities">, manifest: Pick<PluginManifest, "version" | "capabilities">) => boolean;
143
+ resolveExternalManifest: (bundleUrl: string) => Promise<PluginManifest | null>;
144
+ synthesizeUnknownManifest: (idOrUrl: string, version?: string) => PluginManifest;
141
145
  /** Current server policy. `null` until `loadPolicy()` resolves. */
142
146
  policy: import("vue").Ref<{
143
147
  registry_url: string;
@@ -163,6 +163,36 @@ export function usePluginCatalog() {
163
163
  optional: [...m.capabilities.optional ?? []]
164
164
  };
165
165
  }
166
+ function capabilitiesGrew(prevAcked, currentRequired) {
167
+ const acked = new Set(prevAcked ?? []);
168
+ return currentRequired.filter((c) => !acked.has(c));
169
+ }
170
+ async function resolveExternalManifest(bundleUrl) {
171
+ try {
172
+ const manifestUrl = bundleUrl.replace(/\/[^/]+$/, (m2) => `${m2.replace(/\.[^.]+$/, "")}.manifest.json`);
173
+ const m = await $fetch(manifestUrl);
174
+ if (m && typeof m === "object" && typeof m.id === "string") return m;
175
+ return null;
176
+ } catch {
177
+ return null;
178
+ }
179
+ }
180
+ function synthesizeUnknownManifest(idOrUrl, version = "0.0.0") {
181
+ const m = {
182
+ manifestVersion: 1,
183
+ id: idOrUrl,
184
+ version,
185
+ capabilities: { required: [], optional: [] },
186
+ contributes: {}
187
+ };
188
+ return m;
189
+ }
190
+ function needsTrustReprompt(entry, manifest) {
191
+ if (!entry.trustAcknowledgedAt) return true;
192
+ if (entry.acknowledgedVersion !== manifest.version) return true;
193
+ const required = manifest.capabilities.required ?? [];
194
+ return capabilitiesGrew(entry.acknowledgedCapabilities, required).length > 0;
195
+ }
166
196
  async function loadPolicy(serverUrl) {
167
197
  const url = `${serverUrl.replace(/\/$/, "")}/plugins/policy`;
168
198
  const fetched = await $fetch(url);
@@ -200,6 +230,10 @@ export function usePluginCatalog() {
200
230
  installFromUrl,
201
231
  isInstalled,
202
232
  capabilitiesFor,
233
+ capabilitiesGrew,
234
+ needsTrustReprompt,
235
+ resolveExternalManifest,
236
+ synthesizeUnknownManifest,
203
237
  /** Current server policy. `null` until `loadPolicy()` resolves. */
204
238
  policy,
205
239
  loadPolicy,
@@ -0,0 +1,43 @@
1
+ /**
2
+ * `useUploadedPluginStore()` — IndexedDB-backed storage for plugin bundles
3
+ * the user uploaded directly (Upload button on the Installed tab). Keys
4
+ * are the SHA-256 of the bundle bytes; values are the bytes + MIME.
5
+ *
6
+ * The plugin loader (`plugin-abracadabra.client.ts`) recognises URLs of
7
+ * the form `idb-plugin:<sha256>` and resolves them by reading bytes from
8
+ * here, wrapping in a fresh `Blob`, and creating a `blob:` URL for
9
+ * `import()`. Blob URLs are session-scoped, so the bytes have to live in
10
+ * IDB to survive reload.
11
+ *
12
+ * Internal helpers (`_*`) are used by the loader. Public API is the
13
+ * composable for components.
14
+ */
15
+ interface StoredBundle {
16
+ sha256: string;
17
+ bytes: Uint8Array;
18
+ mime: string;
19
+ uploadedAt: number;
20
+ }
21
+ /** Store an uploaded bundle by its SHA-256 hex. Idempotent. */
22
+ export declare function _putUploadedPluginBlob(sha256: string, bytes: Uint8Array, mime?: string): Promise<void>;
23
+ /** Read an uploaded bundle. Returns `null` when the key is unknown. */
24
+ export declare function _getUploadedPluginBlob(sha256: string): Promise<StoredBundle | null>;
25
+ /** Delete an uploaded bundle. Used by `uninstall()` for upload-origin entries. */
26
+ export declare function _deleteUploadedPluginBlob(sha256: string): Promise<void>;
27
+ /**
28
+ * Resolve an `idb-plugin:<sha256>` URL to a session-scoped `blob:` URL
29
+ * suitable for dynamic `import()`. Returns `null` when the sha256 isn't
30
+ * found — the loader treats that as a missing-bundle error.
31
+ */
32
+ export declare function _resolveIdbPluginUrl(idbUrl: string): Promise<string | null>;
33
+ /**
34
+ * Composable surface — read-only for components. Components write via
35
+ * `<APluginsTabInstalled>`'s upload handler which calls
36
+ * `_putUploadedPluginBlob` directly.
37
+ */
38
+ export declare function useUploadedPluginStore(): {
39
+ get: typeof _getUploadedPluginBlob;
40
+ delete: typeof _deleteUploadedPluginBlob;
41
+ resolveUrl: typeof _resolveIdbPluginUrl;
42
+ };
43
+ export {};
@@ -0,0 +1,66 @@
1
+ const DB_NAME = "abracadabra_uploaded_plugins";
2
+ const STORE_NAME = "bundles";
3
+ const DB_VERSION = 1;
4
+ let _dbPromise = null;
5
+ function getDb() {
6
+ if (_dbPromise) return _dbPromise;
7
+ _dbPromise = new Promise((resolve, reject) => {
8
+ const req = indexedDB.open(DB_NAME, DB_VERSION);
9
+ req.onupgradeneeded = () => {
10
+ const db = req.result;
11
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
12
+ db.createObjectStore(STORE_NAME, { keyPath: "sha256" });
13
+ }
14
+ };
15
+ req.onsuccess = () => resolve(req.result);
16
+ req.onerror = () => reject(req.error);
17
+ });
18
+ return _dbPromise;
19
+ }
20
+ export async function _putUploadedPluginBlob(sha256, bytes, mime = "text/javascript") {
21
+ const db = await getDb();
22
+ await new Promise((resolve, reject) => {
23
+ const tx = db.transaction(STORE_NAME, "readwrite");
24
+ const store = tx.objectStore(STORE_NAME);
25
+ const record = { sha256, bytes, mime, uploadedAt: Date.now() };
26
+ const req = store.put(record);
27
+ req.onsuccess = () => resolve();
28
+ req.onerror = () => reject(req.error);
29
+ });
30
+ }
31
+ export async function _getUploadedPluginBlob(sha256) {
32
+ const db = await getDb();
33
+ return new Promise((resolve, reject) => {
34
+ const tx = db.transaction(STORE_NAME, "readonly");
35
+ const store = tx.objectStore(STORE_NAME);
36
+ const req = store.get(sha256);
37
+ req.onsuccess = () => resolve(req.result ?? null);
38
+ req.onerror = () => reject(req.error);
39
+ });
40
+ }
41
+ export async function _deleteUploadedPluginBlob(sha256) {
42
+ const db = await getDb();
43
+ await new Promise((resolve, reject) => {
44
+ const tx = db.transaction(STORE_NAME, "readwrite");
45
+ const store = tx.objectStore(STORE_NAME);
46
+ const req = store.delete(sha256);
47
+ req.onsuccess = () => resolve();
48
+ req.onerror = () => reject(req.error);
49
+ });
50
+ }
51
+ export async function _resolveIdbPluginUrl(idbUrl) {
52
+ const prefix = "idb-plugin:";
53
+ if (!idbUrl.startsWith(prefix)) return null;
54
+ const sha = idbUrl.slice(prefix.length);
55
+ const record = await _getUploadedPluginBlob(sha);
56
+ if (!record) return null;
57
+ const blob = new Blob([new Uint8Array(record.bytes)], { type: record.mime });
58
+ return URL.createObjectURL(blob);
59
+ }
60
+ export function useUploadedPluginStore() {
61
+ return {
62
+ get: _getUploadedPluginBlob,
63
+ delete: _deleteUploadedPluginBlob,
64
+ resolveUrl: _resolveIdbPluginUrl
65
+ };
66
+ }
@@ -220,11 +220,58 @@ export default defineNuxtPlugin({
220
220
  const storedExternal = JSON.parse(
221
221
  localStorage.getItem(STORAGE_KEY_EXTERNAL_PLUGINS) ?? "[]"
222
222
  );
223
+ const [
224
+ { _resolveIdbPluginUrl },
225
+ { _enqueueLoadTimePending, _clearLoadTimePending }
226
+ ] = await Promise.all([
227
+ import("./composables/useUploadedPluginStore.js"),
228
+ import("./composables/useLoadTimePending.js")
229
+ ]);
230
+ async function refetchManifest(url) {
231
+ try {
232
+ const manifestUrl = url.replace(/\/[^/]+$/, (m2) => `${m2.replace(/\.[^.]+$/, "")}.manifest.json`);
233
+ const res = await fetch(manifestUrl, { cache: "no-store" });
234
+ if (!res.ok) return null;
235
+ const m = await res.json();
236
+ if (typeof m.version !== "string") return null;
237
+ return { version: m.version, required: m.capabilities?.required ?? [] };
238
+ } catch {
239
+ return null;
240
+ }
241
+ }
223
242
  for (const entry of storedExternal.filter((p) => p.enabled)) {
224
243
  try {
244
+ const origin = entry.origin ?? "url";
245
+ if (origin !== "registry" && origin !== "upload" && !entry.url.startsWith("idb-plugin:")) {
246
+ const observed = await refetchManifest(entry.url);
247
+ if (observed) {
248
+ const acked = new Set(entry.acknowledgedCapabilities ?? []);
249
+ const grew = observed.required.filter((c) => !acked.has(c));
250
+ const versionChanged = entry.acknowledgedVersion !== observed.version;
251
+ const neverAcked = !entry.trustAcknowledgedAt;
252
+ if (neverAcked || versionChanged || grew.length > 0) {
253
+ _enqueueLoadTimePending({
254
+ url: entry.url,
255
+ id: entry.id ?? entry.name ?? entry.url,
256
+ observedVersion: observed.version,
257
+ observedRequired: observed.required,
258
+ reason: neverAcked ? "never-acknowledged" : grew.length > 0 ? "capabilities-grew" : "version-changed",
259
+ queuedAt: Date.now()
260
+ });
261
+ continue;
262
+ }
263
+ _clearLoadTimePending(entry.url);
264
+ }
265
+ }
266
+ let importUrl = entry.url;
267
+ if (importUrl.startsWith("idb-plugin:")) {
268
+ const resolved = await _resolveIdbPluginUrl(importUrl);
269
+ if (!resolved) throw new Error("Uploaded plugin bundle missing from storage \u2014 was it cleared?");
270
+ importUrl = resolved;
271
+ }
225
272
  const mod = await import(
226
273
  /* @vite-ignore */
227
- entry.url
274
+ importUrl
228
275
  );
229
276
  const plugin = mod.default ?? mod;
230
277
  if (plugin?.name) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abraca/nuxt",
3
- "version": "2.15.0",
3
+ "version": "2.16.0",
4
4
  "description": "First-class Nuxt module for the Abracadabra CRDT collaboration platform",
5
5
  "repository": "abracadabra/abracadabra-nuxt",
6
6
  "license": "MIT",