Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions website/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ https://raw.githubusercontent.com/vastsa/pi-desktop-plugins/main/catalog.json
Catalog responses are revalidated every five minutes. Set `CATALOG_URL` to
use a different catalog provider during preview or development.

Package downloads use the catalog's `artifactBaseUrl` when it is present, and
otherwise resolve relative to `CATALOG_URL`. Run `pnpm test` with Node.js 22.6 or
later to check URL resolution.

The site defaults to English and uses `?lang=<locale>` for language selection.
When no explicit language is present, it matches the browser's
`Accept-Language` header against the locales available in the catalog. A manual
Expand Down
2 changes: 1 addition & 1 deletion website/app/plugins/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default async function PluginDetailPage({ params, searchParams }: { param
const version = currentVersion(plugin);
const permissions = version.permissions ?? [];
const risk = permissionRisk(permissions);
const packageHref = packageUrl(plugin);
const packageHref = packageUrl(plugin, catalog);

return <><SiteHeader locale={locale} availableLocales={availableLocales} /><main className="detail-shell" lang={locale}><div className="container"><div className="breadcrumbs"><Link href={localeHref("/plugins", locale)}><ArrowLeft size={14} /> {copy.detail.plugins}</Link><span>/</span><span>{localized.name}</span></div><div className="detail-layout"><div><header className="detail-heading"><div className="detail-icon"><PluginIcon id={plugin.id} category={plugin.categories[0]} /></div><h1>{localized.name}</h1><p>{localized.description}</p><div className="detail-meta"><span>{copy.detail.by} <strong>{plugin.author}</strong></span><span>·</span><span>v{version.version}</span>{plugin.verified && <span className="verified-label"><Check size={13} /> {copy.detail.catalogOfficial}</span>}</div><div className="detail-actions"><a className="button primary-button" href={packageHref}><ArrowDownToLine size={16} /> {copy.detail.download}</a><CopyPackageButton url={packageHref} locale={locale} /><a className="button secondary-button" href={plugin.homepage ?? `${PLUGIN_CENTER_URL}/plugins/${plugin.id}`} target="_blank" rel="noreferrer">{copy.detail.source} <ArrowUpRight size={15} /></a></div></header><article className="detail-content"><h2>{copy.detail.about}</h2>{localized.readmeMarkdown ? <ReactMarkdown remarkPlugins={[remarkGfm]}>{localized.readmeMarkdown}</ReactMarkdown> : <p>{copy.detail.noReadme}</p>}</article></div><aside className="detail-aside"><div className="aside-card"><h2>{copy.detail.facts}</h2><div className="fact-row"><span>{copy.detail.latestVersion}</span><strong>v{version.version}</strong></div><div className="fact-row"><span>{copy.detail.requires}</span><strong>{version.minPiDesktop ?? "PI-Desktop"}</strong></div><div className="fact-row"><span>{copy.detail.packageSize}</span><strong>{formatBytes(version.sizeBytes)}</strong></div><div className="fact-row"><span>{copy.detail.published}</span><strong>{formatLocalizedDate(version.publishedAt, locale)}</strong></div><div className="fact-row"><span>{copy.detail.category}</span><strong>{categoryCopy(plugin.categories[0], locale).label}</strong></div></div><div className="aside-card"><h2>{copy.detail.permissions}</h2><div style={{ marginBottom: 14 }}><PermissionBadge plugin={plugin} locale={locale} /></div>{permissions.length ? <div className="permission-list">{permissions.map((permission) => <span className="permission-name" key={permission}>{permission}</span>)}</div> : <p className="safety-note">{copy.detail.noPermissions}</p>}{risk.tone === "high" && <p className="safety-note" style={{ marginTop: 15 }}><strong><ShieldAlert size={14} /> {copy.detail.reviewTitle}</strong>{copy.detail.reviewDescription}</p>}</div>{localized.safetyNotes && <div className="aside-card safety-note"><strong>{copy.detail.safetyNotes}</strong>{localized.safetyNotes}</div>}<div className="aside-card"><h2>{copy.detail.installTitle}</h2><p className="safety-note">{copy.detail.installDescription}</p><a className="text-link" href={packageHref}>{copy.detail.downloadPackage} <ExternalLink size={14} /></a></div></aside></div></div></main><SiteFooter locale={locale} availableLocales={availableLocales} /></>;
}
7 changes: 3 additions & 4 deletions website/lib/catalog.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defaultLocale, siteLocales, type Locale } from "./i18n";
import { resolvePackageUrl } from "./package-url";

export type Permission = string;

Expand Down Expand Up @@ -100,10 +101,8 @@ export function currentVersion(plugin: Plugin): PluginVersion {
return plugin.versions[0];
}

export function packageUrl(plugin: Plugin): string {
const url = currentVersion(plugin).url;
if (url.startsWith("http://") || url.startsWith("https://")) return url;
return new URL(url, CATALOG_URL).toString();
export function packageUrl(plugin: Plugin, catalog: Pick<Catalog, "artifactBaseUrl">): string {
return resolvePackageUrl(currentVersion(plugin).url, CATALOG_URL, catalog.artifactBaseUrl);
}

export function formatBytes(bytes?: number): string {
Expand Down
6 changes: 6 additions & 0 deletions website/lib/package-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function resolvePackageUrl(url: string, catalogUrl: string, artifactBaseUrl?: string): string {
if (url.startsWith("http://") || url.startsWith("https://")) return url;
const base = artifactBaseUrl?.trim();
if (!base) return new URL(url, catalogUrl).toString();
return new URL(url.replace(/^\/+/, ""), base.endsWith("/") ? base : `${base}/`).toString();
}
1 change: 1 addition & 0 deletions website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"scripts": {
"dev": "next dev",
"build": "next build",
"test": "node --experimental-strip-types --test tests/*.test.mjs",
"start": "next start",
"lint": "next lint"
},
Expand Down
32 changes: 32 additions & 0 deletions website/tests/package-url.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import assert from "node:assert/strict";
import test from "node:test";
import { resolvePackageUrl } from "../lib/package-url.ts";

const catalogUrl = "https://plugins.aiuo.net/catalog.json";
const mirrorBase = "https://raw.githubusercontent.com/vastsa/pi-desktop-plugins/main";
const packagePath = "packages/pi.gitlens-0.2.7.piplug";

test("relative downloads use the declared artifact base on another host", () => {
for (const base of [mirrorBase, `${mirrorBase}/`]) {
assert.equal(resolvePackageUrl(packagePath, catalogUrl, base), `${mirrorBase}/${packagePath}`);
}
});

test("a leading slash in a package path still uses the artifact directory", () => {
assert.equal(resolvePackageUrl(`/${packagePath}`, catalogUrl, mirrorBase), `${mirrorBase}/${packagePath}`);
});

test("absolute package URLs keep their own origin and query string", () => {
for (const url of ["https://cdn.example/plugin.piplug?version=1", "http://localhost/plugin.piplug"]) {
assert.equal(resolvePackageUrl(url, catalogUrl, mirrorBase), url);
}
});

test("catalogs without an artifact base resolve relative to the catalog", () => {
for (const base of [undefined, "", " "]) {
assert.equal(
resolvePackageUrl(packagePath, "https://mirror.example/catalogs/catalog.json", base),
`https://mirror.example/catalogs/${packagePath}`,
);
}
});