@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.
- package/dist/module.json +1 -1
- package/dist/runtime/components/aware/AMedia.d.vue.ts +1 -1
- package/dist/runtime/components/aware/AMedia.vue.d.ts +1 -1
- package/dist/runtime/components/registry/APluginBrowser.vue +18 -2
- package/dist/runtime/components/settings/APluginInstallDialog.d.vue.ts +39 -0
- package/dist/runtime/components/settings/APluginInstallDialog.vue +254 -0
- package/dist/runtime/components/settings/APluginInstallDialog.vue.d.ts +39 -0
- package/dist/runtime/components/settings/APluginsTabInstalled.d.vue.ts +7 -0
- package/dist/runtime/components/settings/APluginsTabInstalled.vue +413 -0
- package/dist/runtime/components/settings/APluginsTabInstalled.vue.d.ts +7 -0
- package/dist/runtime/components/settings/APluginsTabPending.d.vue.ts +24 -0
- package/dist/runtime/components/settings/APluginsTabPending.vue +248 -0
- package/dist/runtime/components/settings/APluginsTabPending.vue.d.ts +24 -0
- package/dist/runtime/components/settings/ASettingsPluginsPanel.d.vue.ts +14 -1
- package/dist/runtime/components/settings/ASettingsPluginsPanel.vue +34 -80
- package/dist/runtime/components/settings/ASettingsPluginsPanel.vue.d.ts +14 -1
- package/dist/runtime/composables/useDeclinedSpacePlugins.d.ts +7 -0
- package/dist/runtime/composables/useDeclinedSpacePlugins.js +24 -0
- package/dist/runtime/composables/useLoadTimePending.d.ts +29 -0
- package/dist/runtime/composables/useLoadTimePending.js +37 -0
- package/dist/runtime/composables/usePluginCatalog.d.ts +5 -1
- package/dist/runtime/composables/usePluginCatalog.js +34 -0
- package/dist/runtime/composables/useUploadedPluginStore.d.ts +43 -0
- package/dist/runtime/composables/useUploadedPluginStore.js +66 -0
- package/dist/runtime/plugin-abracadabra.client.js +48 -1
- 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
|
-
|
|
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 {
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
274
|
+
importUrl
|
|
228
275
|
);
|
|
229
276
|
const plugin = mod.default ?? mod;
|
|
230
277
|
if (plugin?.name) {
|