Files
dbx/apps/desktop/src/lib/plugins/pluginMarketplace.ts
T

91 lines
4.5 KiB
TypeScript

import type { InstalledPlugin, PluginMarketplaceArtifact, PluginMarketplacePlugin, PluginRepository, PluginRepositoryCatalogResult } from "@/types/database";
export type MarketplacePluginStatus = "install" | "installed" | "update" | "unsupported";
export const UNIVERSAL_PLUGIN_TARGET = "universal";
export interface MarketplacePluginListing {
key: string;
repository: PluginRepository;
plugin: PluginMarketplacePlugin;
name: string;
description: string;
target: string;
artifact?: PluginMarketplaceArtifact;
installed?: InstalledPlugin;
verified: boolean;
status: MarketplacePluginStatus;
}
export function buildMarketplacePluginListings(results: readonly PluginRepositoryCatalogResult[], installedPlugins: readonly InstalledPlugin[], locale: string): MarketplacePluginListing[] {
const installedById = new Map(installedPlugins.map((plugin) => [plugin.manifest.id, plugin]));
return results
.flatMap((result) =>
(result.catalog?.plugins || []).map((plugin) => {
const localized = marketplacePluginLocalization(plugin, locale);
const latestVersion = plugin.versions.find((version) => version.version === plugin.latestVersion);
const artifact = latestVersion ? selectMarketplaceArtifact(latestVersion.artifacts, result.target) : undefined;
const installed = installedById.get(plugin.id);
const status: MarketplacePluginStatus = !artifact ? "unsupported" : !installed ? "install" : compareVersions(plugin.latestVersion, installed.manifest.version || "0.0.0") > 0 ? "update" : "installed";
return {
key: `${result.repository.id}:${plugin.id}`,
repository: result.repository,
plugin,
name: localized.name,
description: localized.description,
target: result.target,
artifact,
installed,
verified: plugin.verified && listingRepositoryCanVerify(result.repository),
status,
};
}),
)
.sort((left, right) => left.name.localeCompare(right.name));
}
export function selectMarketplaceArtifact(artifacts: readonly PluginMarketplaceArtifact[], target: string): PluginMarketplaceArtifact | undefined {
return artifacts.find((candidate) => candidate.target === target) || artifacts.find((candidate) => candidate.target === UNIVERSAL_PLUGIN_TARGET);
}
export function listingRepositoryCanVerify(repository: PluginRepository): boolean {
return repository.kind === "official" || repository.kind === "enterprise";
}
export function filterMarketplacePluginListings(listings: readonly MarketplacePluginListing[], query: string, repositoryId: string): MarketplacePluginListing[] {
const normalizedQuery = query.trim().toLocaleLowerCase();
return listings.filter((listing) => {
if (repositoryId !== "all" && listing.repository.id !== repositoryId) return false;
if (!normalizedQuery) return true;
return [listing.plugin.id, listing.name, listing.description, listing.plugin.publisher, listing.repository.name, ...listing.plugin.tags].join("\n").toLocaleLowerCase().includes(normalizedQuery);
});
}
function marketplacePluginLocalization(plugin: PluginMarketplacePlugin, locale: string): { name: string; description: string } {
const normalizedLocale = locale.replace("_", "-").toLowerCase();
const entries = Object.entries(plugin.localizations || {});
const localization = entries.find(([key]) => key.replace("_", "-").toLowerCase() === normalizedLocale)?.[1] || entries.find(([key]) => key.replace("_", "-").toLowerCase() === normalizedLocale.split("-")[0])?.[1];
return {
name: localization?.name?.trim() || plugin.name,
description: localization?.description?.trim() || plugin.description,
};
}
function compareVersions(left: string, right: string): number {
const leftParts = parseVersion(left);
const rightParts = parseVersion(right);
if (!leftParts || !rightParts) return left.localeCompare(right, undefined, { numeric: true, sensitivity: "base" });
const leftNumbers = leftParts.slice(0, 3) as [number, number, number];
const rightNumbers = rightParts.slice(0, 3) as [number, number, number];
for (let index = 0; index < leftNumbers.length; index += 1) {
if (leftNumbers[index] !== rightNumbers[index]) return leftNumbers[index] - rightNumbers[index];
}
return leftParts[3].localeCompare(rightParts[3]);
}
function parseVersion(version: string): [number, number, number, string] | null {
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([^+]+))?/.exec(version);
if (!match) return null;
return [Number(match[1]), Number(match[2]), Number(match[3]), match[4] || "~"];
}