feat: Implement Plugin Marketplace Manager and Client
- 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.
This commit is contained in:
169
dist/PluginMarketplaceManager.js
vendored
Normal file
169
dist/PluginMarketplaceManager.js
vendored
Normal file
@@ -0,0 +1,169 @@
|
||||
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
|
||||
Reference in New Issue
Block a user