- Added PluginMarketplaceManager to coordinate marketplace reads and manage installed plugins. - Introduced PluginMarketplaceClient for fetching marketplace metadata. - Implemented storage management for installed plugins using MemoryStorage. - Added methods for installing, uninstalling, and enabling/disabling plugins. - Created type definitions for PluginMarketplaceManager options and installed plugin records. - Added support for fetching full marketplace catalog and individual plugin metadata. - Included error handling for fetch operations and validation of plugin data. - Updated index.browser.ts to export new modules and types.
169 lines
7.1 KiB
JavaScript
169 lines
7.1 KiB
JavaScript
import { MemoryStorage } from './interfaces.js';
|
|
const DEFAULT_INSTALLED_PLUGINS_STORAGE_KEY = 'plugin-manager:installed-plugins';
|
|
/**
|
|
* Coordinates marketplace reads with host-specific install and enable state.
|
|
*/
|
|
export class PluginMarketplaceManager {
|
|
constructor(options) {
|
|
this.client = options.client;
|
|
this.storage = options.storage ?? new MemoryStorage();
|
|
this.host = options.host;
|
|
this.registry = options.registry;
|
|
this.installedPluginsStorageKey =
|
|
options.installedPluginsStorageKey ?? DEFAULT_INSTALLED_PLUGINS_STORAGE_KEY;
|
|
}
|
|
/**
|
|
* Fetches full marketplace catalog.
|
|
*/
|
|
async fetchMarketplacePlugins() {
|
|
return this.client.fetchPlugins();
|
|
}
|
|
/**
|
|
* Lists installed plugins merged with persisted enabled state.
|
|
*/
|
|
async listInstalledPlugins() {
|
|
const storedPlugins = this.readStoredInstalledPlugins();
|
|
if (!this.host?.listInstalledPlugins) {
|
|
return storedPlugins;
|
|
}
|
|
const hostPlugins = await this.host.listInstalledPlugins();
|
|
const mergedPlugins = mergeInstalledPlugins(hostPlugins, storedPlugins);
|
|
this.writeStoredInstalledPlugins(mergedPlugins);
|
|
return mergedPlugins;
|
|
}
|
|
/**
|
|
* Lists installed plugins currently enabled.
|
|
*/
|
|
async listEnabledPlugins() {
|
|
return (await this.listInstalledPlugins()).filter((plugin) => plugin.enabled);
|
|
}
|
|
/**
|
|
* Returns whether plugin is recorded as installed.
|
|
*/
|
|
async isPluginInstalled(pluginId) {
|
|
return (await this.listInstalledPlugins()).some((plugin) => plugin.id === pluginId);
|
|
}
|
|
/**
|
|
* Installs marketplace plugin via host adapter and persists state.
|
|
*/
|
|
async installPlugin(plugin) {
|
|
await this.host?.installPlugin?.(plugin);
|
|
const installedPlugin = {
|
|
...plugin,
|
|
installedDate: new Date().toISOString(),
|
|
enabled: true,
|
|
};
|
|
const updatedPlugins = upsertInstalledPlugin(this.readStoredInstalledPlugins(), installedPlugin);
|
|
this.writeStoredInstalledPlugins(updatedPlugins);
|
|
return updatedPlugins.find((entry) => entry.id === plugin.id) ?? installedPlugin;
|
|
}
|
|
/**
|
|
* Removes plugin from host and clears persisted install state.
|
|
*/
|
|
async uninstallPlugin(pluginId) {
|
|
if (this.registry) {
|
|
await this.registry.unregisterPlugin(pluginId);
|
|
}
|
|
await this.host?.uninstallPlugin?.(pluginId);
|
|
this.writeStoredInstalledPlugins(this.readStoredInstalledPlugins().filter((plugin) => plugin.id !== pluginId));
|
|
}
|
|
/**
|
|
* Enables or disables installed plugin.
|
|
*
|
|
* Disabling triggers `registry.unregisterPlugin` when registry exists.
|
|
* Enabling only updates persisted state; host controls actual load timing.
|
|
*/
|
|
async setPluginEnabled(pluginId, enabled) {
|
|
if (!enabled && this.registry) {
|
|
await this.registry.unregisterPlugin(pluginId);
|
|
}
|
|
const installedPlugins = await this.listInstalledPlugins();
|
|
const targetPlugin = installedPlugins.find((plugin) => plugin.id === pluginId);
|
|
if (!targetPlugin) {
|
|
return null;
|
|
}
|
|
const updatedPlugin = { ...targetPlugin, enabled };
|
|
this.writeStoredInstalledPlugins(upsertInstalledPlugin(installedPlugins, updatedPlugin));
|
|
return updatedPlugin;
|
|
}
|
|
readStoredInstalledPlugins() {
|
|
const rawValue = this.storage.getItem(this.installedPluginsStorageKey);
|
|
if (!rawValue) {
|
|
return [];
|
|
}
|
|
try {
|
|
const parsedValue = JSON.parse(rawValue);
|
|
if (!Array.isArray(parsedValue)) {
|
|
return [];
|
|
}
|
|
return parsedValue
|
|
.map((entry) => normalizeInstalledPlugin(entry))
|
|
.filter((entry) => entry !== null);
|
|
}
|
|
catch {
|
|
return [];
|
|
}
|
|
}
|
|
writeStoredInstalledPlugins(plugins) {
|
|
this.storage.setItem(this.installedPluginsStorageKey, JSON.stringify(plugins));
|
|
}
|
|
}
|
|
function mergeInstalledPlugins(hostPlugins, storedPlugins) {
|
|
const storedPluginsById = new Map(storedPlugins.map((plugin) => [plugin.id, plugin]));
|
|
const discoveredAt = new Date().toISOString();
|
|
return hostPlugins.map((plugin) => {
|
|
const storedPlugin = storedPluginsById.get(plugin.id);
|
|
return {
|
|
id: plugin.id,
|
|
name: pickString(plugin.name, storedPlugin?.name),
|
|
version: pickString(plugin.version, storedPlugin?.version),
|
|
description: pickString(plugin.description, storedPlugin?.description),
|
|
author: pickString(plugin.author, storedPlugin?.author),
|
|
repository: pickString(plugin.repository, storedPlugin?.repository),
|
|
thumbnail: pickString(plugin.thumbnail, storedPlugin?.thumbnail),
|
|
downloadUrl: pickString(plugin.downloadUrl, storedPlugin?.downloadUrl),
|
|
homepage: pickString(plugin.homepage, storedPlugin?.homepage),
|
|
tags: Array.isArray(plugin.tags)
|
|
? plugin.tags.filter((entry) => typeof entry === 'string')
|
|
: storedPlugin?.tags ?? [],
|
|
addedDate: pickString(plugin.addedDate, storedPlugin?.addedDate),
|
|
installedDate: pickString(plugin.installedDate, storedPlugin?.installedDate) || discoveredAt,
|
|
enabled: typeof plugin.enabled === 'boolean' ? plugin.enabled : storedPlugin?.enabled ?? true,
|
|
};
|
|
});
|
|
}
|
|
function upsertInstalledPlugin(plugins, nextPlugin) {
|
|
const filteredPlugins = plugins.filter((plugin) => plugin.id !== nextPlugin.id);
|
|
return [...filteredPlugins, nextPlugin];
|
|
}
|
|
function normalizeInstalledPlugin(value) {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
return null;
|
|
}
|
|
const record = value;
|
|
const id = typeof record.id === 'string' ? record.id : '';
|
|
if (!id) {
|
|
return null;
|
|
}
|
|
return {
|
|
id,
|
|
name: typeof record.name === 'string' ? record.name : '',
|
|
version: typeof record.version === 'string' ? record.version : '',
|
|
description: typeof record.description === 'string' ? record.description : '',
|
|
author: typeof record.author === 'string' ? record.author : '',
|
|
repository: typeof record.repository === 'string' ? record.repository : '',
|
|
thumbnail: typeof record.thumbnail === 'string' ? record.thumbnail : '',
|
|
downloadUrl: typeof record.downloadUrl === 'string' ? record.downloadUrl : '',
|
|
homepage: typeof record.homepage === 'string' ? record.homepage : '',
|
|
tags: Array.isArray(record.tags)
|
|
? record.tags.filter((entry) => typeof entry === 'string')
|
|
: [],
|
|
addedDate: typeof record.addedDate === 'string' ? record.addedDate : '',
|
|
installedDate: typeof record.installedDate === 'string' ? record.installedDate : '',
|
|
enabled: typeof record.enabled === 'boolean' ? record.enabled : true,
|
|
};
|
|
}
|
|
function pickString(value, fallback = '') {
|
|
return typeof value === 'string' && value.length > 0 ? value : fallback;
|
|
}
|
|
//# sourceMappingURL=PluginMarketplaceManager.js.map
|