91 lines
4.5 KiB
TypeScript
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] || "~"];
|
|
}
|