mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
fix(plugins): normalize marketplace release url per host
This commit is contained in:
@@ -63,6 +63,7 @@ import {
|
||||
formatMarketplaceReleasedDate,
|
||||
marketplaceHomepageUrl,
|
||||
pluginSourceChange,
|
||||
resolveMarketplaceReleaseUrl,
|
||||
sortMarketplacePluginListings,
|
||||
type InstalledPluginUpdateEntry,
|
||||
type MarketplacePluginListing,
|
||||
@@ -333,10 +334,7 @@ function marketplaceVersionPublishedLabel(version: PluginMarketplaceVersion): st
|
||||
// The expanded entry links to the release page for that exact version in the
|
||||
// plugin's source repository, mirroring the 更新管理 panel's GitHub Release button.
|
||||
function marketplaceVersionReleaseUrl(version: PluginMarketplaceVersion): string {
|
||||
const source = selectedMarketplaceListing.value?.plugin.source?.trim();
|
||||
if (!source) return "";
|
||||
const tag = version.version.startsWith("v") ? version.version : `v${version.version}`;
|
||||
return `${source.replace(/\/+$/, "")}/releases/tag/${encodeURIComponent(tag)}`;
|
||||
return resolveMarketplaceReleaseUrl(selectedMarketplaceListing.value?.plugin.source, version);
|
||||
}
|
||||
const batchUpdatableListings = computed(() => filteredMarketplaceListings.value.filter((listing) => listing.status === "update"));
|
||||
const batchSelectedListings = computed(() => filteredMarketplaceListings.value.filter((listing) => isBatchSelectableListing(listing.status) && selectedListingKeys.value.has(listing.key)));
|
||||
|
||||
+17
@@ -1700,6 +1700,23 @@ describe("marketplace detail update history", () => {
|
||||
expect(openSpy).toHaveBeenCalledWith("https://github.com/jinpy/dbx-ssh-plugin/releases/tag/v3.0.0", "_blank", "noopener,noreferrer");
|
||||
openSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("links expanded version to GitHub release page when source has a branch tree URL", async () => {
|
||||
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
|
||||
const cat = multiVersionCatalog();
|
||||
cat.catalog!.plugins[0].source = "https://github.com/Ezreal-byte/dbx-plugin-xxl-job-console/tree/v0.3.3";
|
||||
state.catalogResults = [cat];
|
||||
await flushUi();
|
||||
state.openMarketplaceDetails(state.marketplaceListings[0]);
|
||||
await flushUi();
|
||||
const dialog = document.querySelector('[role="dialog"]')!;
|
||||
const releaseButton = [...dialog.querySelectorAll("button")].find((element) => element.textContent?.includes("pluginPlatform.pluginDetailsOpenRelease"))!;
|
||||
expect(releaseButton).toBeDefined();
|
||||
releaseButton.click();
|
||||
await flushUi();
|
||||
expect(openSpy).toHaveBeenCalledWith("https://github.com/Ezreal-byte/dbx-plugin-xxl-job-console/releases/tag/v3.0.0", "_blank", "noopener,noreferrer");
|
||||
openSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("marketplace author filtering", () => {
|
||||
|
||||
@@ -3,10 +3,12 @@ import {
|
||||
buildInstalledUpdateIndex,
|
||||
buildMarketplacePluginListings,
|
||||
compareVersions,
|
||||
extractRepositoryBaseUrl,
|
||||
filterMarketplacePluginListings,
|
||||
formatMarketplaceReleasedDate,
|
||||
marketplaceHomepageUrl,
|
||||
pluginSourceChange,
|
||||
resolveMarketplaceReleaseUrl,
|
||||
selectMarketplaceArtifact,
|
||||
sortMarketplacePluginListings,
|
||||
type MarketplacePluginListing,
|
||||
@@ -136,6 +138,72 @@ describe("plugin marketplace listings", () => {
|
||||
expect(marketplaceHomepageUrl("https://github.com/dbxio/example/", "https://github.com/dbxio/example#readme")).toBeUndefined();
|
||||
expect(marketplaceHomepageUrl("https://github.com/dbxio/example", "https://dbxio.com/plugins/example")).toBe("https://dbxio.com/plugins/example");
|
||||
});
|
||||
|
||||
describe("resolveMarketplaceReleaseUrl", () => {
|
||||
it("resolves GitHub branch and tag tree URLs to root repository release tag", () => {
|
||||
// Regression for #12516: source with /tree/<ref> should not result in /tree/<ref>/releases/tag/...
|
||||
expect(resolveMarketplaceReleaseUrl("https://github.com/Ezreal-byte/dbx-plugin-xxl-job-console/tree/v0.3.3", "0.3.3")).toBe("https://github.com/Ezreal-byte/dbx-plugin-xxl-job-console/releases/tag/v0.3.3");
|
||||
|
||||
expect(resolveMarketplaceReleaseUrl("https://github.com/Ezreal-byte/dbx-plugin-xxl-job-console/tree/v0.3.3/", "0.3.3")).toBe("https://github.com/Ezreal-byte/dbx-plugin-xxl-job-console/releases/tag/v0.3.3");
|
||||
|
||||
expect(resolveMarketplaceReleaseUrl("https://github.com/t8y2/dbx/tree/main/plugins/examples/hello-workbench", "0.1.0")).toBe("https://github.com/t8y2/dbx/releases/tag/v0.1.0");
|
||||
});
|
||||
|
||||
it("resolves standard repository URLs with and without trailing slash or .git", () => {
|
||||
expect(resolveMarketplaceReleaseUrl("https://github.com/jinpy/dbx-ssh-plugin", "3.0.0")).toBe("https://github.com/jinpy/dbx-ssh-plugin/releases/tag/v3.0.0");
|
||||
expect(resolveMarketplaceReleaseUrl("https://github.com/jinpy/dbx-ssh-plugin/", "3.0.0")).toBe("https://github.com/jinpy/dbx-ssh-plugin/releases/tag/v3.0.0");
|
||||
expect(resolveMarketplaceReleaseUrl("https://github.com/jinpy/dbx-ssh-plugin.git", "3.0.0")).toBe("https://github.com/jinpy/dbx-ssh-plugin/releases/tag/v3.0.0");
|
||||
expect(resolveMarketplaceReleaseUrl("https://github.com/jinpy/dbx-ssh-plugin", "v3.0.0")).toBe("https://github.com/jinpy/dbx-ssh-plugin/releases/tag/v3.0.0");
|
||||
});
|
||||
|
||||
it("resolves blob, releases, and ssh URLs", () => {
|
||||
expect(resolveMarketplaceReleaseUrl("https://github.com/owner/repo/blob/main/README.md", "1.0.0")).toBe("https://github.com/owner/repo/releases/tag/v1.0.0");
|
||||
expect(resolveMarketplaceReleaseUrl("https://github.com/owner/repo/releases", "1.0.0")).toBe("https://github.com/owner/repo/releases/tag/v1.0.0");
|
||||
expect(resolveMarketplaceReleaseUrl("https://github.com/owner/repo/releases/tag/v0.1.0", "1.0.0")).toBe("https://github.com/owner/repo/releases/tag/v1.0.0");
|
||||
expect(resolveMarketplaceReleaseUrl("git@github.com:Ezreal-byte/dbx-plugin-xxl-job-console.git", "0.3.3")).toBe("https://github.com/Ezreal-byte/dbx-plugin-xxl-job-console/releases/tag/v0.3.3");
|
||||
});
|
||||
|
||||
it("supports Gitee, GitLab, and CNB repository formats", () => {
|
||||
expect(resolveMarketplaceReleaseUrl("https://gitee.com/someone/my-plugin/tree/master", "1.0.0")).toBe("https://gitee.com/someone/my-plugin/releases/tag/v1.0.0");
|
||||
expect(resolveMarketplaceReleaseUrl("https://gitlab.com/group/subgroup/my-plugin/-/tree/main", "1.0.0")).toBe("https://gitlab.com/group/subgroup/my-plugin/-/releases/v1.0.0");
|
||||
expect(resolveMarketplaceReleaseUrl("https://cnb.cool/dbxio.com/my-plugin/-/tree/main", "1.0.0")).toBe("https://cnb.cool/dbxio.com/my-plugin/-/releases/tag/v1.0.0");
|
||||
});
|
||||
|
||||
it("accepts PluginMarketplaceVersion objects and handles empty inputs gracefully", () => {
|
||||
expect(
|
||||
resolveMarketplaceReleaseUrl("https://github.com/owner/repo", {
|
||||
version: "2.1.0",
|
||||
artifacts: [],
|
||||
}),
|
||||
).toBe("https://github.com/owner/repo/releases/tag/v2.1.0");
|
||||
|
||||
expect(resolveMarketplaceReleaseUrl("", "1.0.0")).toBe("");
|
||||
expect(resolveMarketplaceReleaseUrl(" ", "1.0.0")).toBe("");
|
||||
expect(resolveMarketplaceReleaseUrl(undefined, "1.0.0")).toBe("");
|
||||
expect(resolveMarketplaceReleaseUrl(null, "1.0.0")).toBe("");
|
||||
expect(resolveMarketplaceReleaseUrl("https://github.com/owner/repo", "")).toBe("");
|
||||
expect(resolveMarketplaceReleaseUrl("https://github.com/owner/repo", " ")).toBe("");
|
||||
expect(resolveMarketplaceReleaseUrl("https://github.com/owner/repo", undefined)).toBe("");
|
||||
expect(resolveMarketplaceReleaseUrl("https://github.com/owner/repo", { version: "", artifacts: [] })).toBe("");
|
||||
});
|
||||
|
||||
it("extracts base repository URL from various git URL formats", () => {
|
||||
expect(extractRepositoryBaseUrl("https://github.com/Ezreal-byte/dbx-plugin-xxl-job-console/tree/v0.3.3")).toEqual({
|
||||
baseUrl: "https://github.com/Ezreal-byte/dbx-plugin-xxl-job-console",
|
||||
host: "github.com",
|
||||
});
|
||||
expect(extractRepositoryBaseUrl("https://github.com/owner/repo.git")).toEqual({
|
||||
baseUrl: "https://github.com/owner/repo",
|
||||
host: "github.com",
|
||||
});
|
||||
expect(extractRepositoryBaseUrl("git@github.com:owner/repo.git")).toEqual({
|
||||
baseUrl: "https://github.com/owner/repo",
|
||||
host: "github.com",
|
||||
});
|
||||
expect(extractRepositoryBaseUrl("")).toBeNull();
|
||||
expect(extractRepositoryBaseUrl(" ")).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildInstalledUpdateIndex", () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { InstalledPlugin, PluginMarketplaceArtifact, PluginMarketplacePlugin, PluginRepository, PluginRepositoryCatalogResult } from "@/types/database";
|
||||
import type { InstalledPlugin, PluginMarketplaceArtifact, PluginMarketplacePlugin, PluginMarketplaceVersion, PluginRepository, PluginRepositoryCatalogResult } from "@/types/database";
|
||||
import { uuid } from "@/lib/common/utils";
|
||||
|
||||
export type MarketplacePluginStatus = "install" | "installed" | "update" | "unsupported";
|
||||
@@ -36,6 +36,99 @@ export function marketplaceHomepageUrl(source?: string, homepage?: string): stri
|
||||
return homepage?.trim() || undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the external release URL for a specific plugin version.
|
||||
*
|
||||
* Plugin catalogs often provide repository URLs pointing to a specific branch,
|
||||
* tag, or subfolder (e.g. `https://github.com/owner/repo/tree/v0.3.3` or
|
||||
* `https://github.com/owner/repo/tree/main/packages/plugin`), or git clone URLs
|
||||
* (`.git`). This extracts the base repository root and appends the appropriate
|
||||
* release path for the given version tag.
|
||||
*/
|
||||
export function resolveMarketplaceReleaseUrl(source?: string | null, version?: string | PluginMarketplaceVersion | null): string {
|
||||
const trimmedSource = source?.trim();
|
||||
const rawVersion = typeof version === "string" ? version.trim() : (version?.version?.trim() ?? "");
|
||||
if (!trimmedSource || !rawVersion) return "";
|
||||
|
||||
const tag = rawVersion.startsWith("v") || rawVersion.startsWith("V") ? rawVersion : `v${rawVersion}`;
|
||||
const repo = extractRepositoryBaseUrl(trimmedSource);
|
||||
if (!repo) return "";
|
||||
|
||||
if (repo.host === "cnb.cool" || repo.host.endsWith(".cnb.cool")) {
|
||||
return `${repo.baseUrl}/-/releases/tag/${encodeURIComponent(tag)}`;
|
||||
}
|
||||
if (repo.host === "gitlab.com" || repo.host.endsWith(".gitlab.com")) {
|
||||
return `${repo.baseUrl}/-/releases/${encodeURIComponent(tag)}`;
|
||||
}
|
||||
return `${repo.baseUrl}/releases/tag/${encodeURIComponent(tag)}`;
|
||||
}
|
||||
|
||||
export function extractRepositoryBaseUrl(source: string): { baseUrl: string; host: string } | null {
|
||||
const trimmed = source.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
// Handle SSH URLs: git@github.com:owner/repo.git or ssh://git@github.com/owner/repo.git
|
||||
const sshMatch = /^(?:ssh:\/\/)?git@([^/:]+)[:/](.+)$/i.exec(trimmed);
|
||||
let normalized = trimmed;
|
||||
if (sshMatch) {
|
||||
const host = sshMatch[1];
|
||||
const path = sshMatch[2].replace(/\.git$/i, "").replace(/^\/+/, "");
|
||||
normalized = `https://${host}/${path}`;
|
||||
} else if (!/^https?:\/\//i.test(normalized)) {
|
||||
normalized = `https://${normalized}`;
|
||||
}
|
||||
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(normalized);
|
||||
} catch {
|
||||
const cleaned = trimmed
|
||||
.replace(/\/(?:-?\/)?(?:tree|blob|releases|src)(?:\/.*)?$/i, "")
|
||||
.replace(/\.git$/i, "")
|
||||
.replace(/\/+$/, "");
|
||||
return cleaned ? { baseUrl: cleaned, host: "" } : null;
|
||||
}
|
||||
|
||||
const host = parsed.hostname.toLowerCase();
|
||||
const origin = parsed.origin;
|
||||
const segments = parsed.pathname.split("/").filter(Boolean);
|
||||
if (segments.length === 0) return null;
|
||||
|
||||
// On GitHub and Gitee, repositories are strictly <owner>/<repo>.
|
||||
// Any trailing segment (e.g. /tree/<ref>, /blob/<ref>, /releases) is not part of the repository.
|
||||
if (host === "github.com" || host.endsWith(".github.com") || host === "gitee.com" || host.endsWith(".gitee.com")) {
|
||||
if (segments.length >= 2) {
|
||||
const owner = segments[0];
|
||||
const repoName = segments[1].replace(/\.git$/i, "");
|
||||
return { baseUrl: `${origin}/${owner}/${repoName}`, host };
|
||||
}
|
||||
return { baseUrl: `${origin}/${segments[0].replace(/\.git$/i, "")}`, host };
|
||||
}
|
||||
|
||||
// On GitLab, CNB, etc., UI routes (tree, blob, releases, etc.) follow a "/-/" segment:
|
||||
// e.g. https://gitlab.com/group/subgroup/project/-/tree/main
|
||||
// e.g. https://cnb.cool/org/project/-/releases/tag/v1.0.0
|
||||
const dashIndex = segments.indexOf("-");
|
||||
if (dashIndex > 0) {
|
||||
const repoSegments = segments.slice(0, dashIndex);
|
||||
repoSegments[repoSegments.length - 1] = repoSegments[repoSegments.length - 1].replace(/\.git$/i, "");
|
||||
return { baseUrl: `${origin}/${repoSegments.join("/")}`, host };
|
||||
}
|
||||
|
||||
// Generic git hosts (Gitea, Bitbucket, self-hosted, etc.):
|
||||
// Strip trailing branches/trees/blobs/releases/src if present
|
||||
const cutIndex = segments.findIndex((seg) => ["tree", "blob", "releases", "src"].includes(seg.toLowerCase()));
|
||||
if (cutIndex > 0) {
|
||||
const repoSegments = segments.slice(0, cutIndex);
|
||||
repoSegments[repoSegments.length - 1] = repoSegments[repoSegments.length - 1].replace(/\.git$/i, "");
|
||||
return { baseUrl: `${origin}/${repoSegments.join("/")}`, host };
|
||||
}
|
||||
|
||||
// Default: strip .git from the last segment
|
||||
segments[segments.length - 1] = segments[segments.length - 1].replace(/\.git$/i, "");
|
||||
return { baseUrl: `${origin}/${segments.join("/")}`, host };
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user