From c33ffbee3ffa03bbb2abe9f4406dd84c43b06ba6 Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Mon, 10 Aug 2026 13:55:22 +0200 Subject: [PATCH] Support postfixed release tags --- plugins.test.ts | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- plugins.ts | 47 +++++++++++++++++++++++++++++++++++++---------- 2 files changed, 85 insertions(+), 11 deletions(-) diff --git a/plugins.test.ts b/plugins.test.ts index 1511fc3..4b8c7e9 100644 --- a/plugins.test.ts +++ b/plugins.test.ts @@ -1,5 +1,5 @@ import { expect, it } from "vitest"; -import { tryResolveAssetUrl, tryResolveLatestJson } from "./plugins.js"; +import { getLatestInfoFromRelease, tryResolveAssetUrl, tryResolveLatestJson, tryResolveSchemaUrl } from "./plugins.js"; import { getLatestReleaseInfo } from "./utils/github.js"; function resolveAsset(url: string) { @@ -56,12 +56,59 @@ it("tryResolveAssetUrl", () => { shouldCache: false, }); + // approved repo with a semantic-version tag suffix + expect( + resolveAsset( + "https://plugins.dprint.dev/kjanat/PSScriptAnalyzer/0.1.1-dprint/asset/plugin.wasm", + ), + ).toEqual({ + githubUrl: "https://github.com/kjanat/PSScriptAnalyzer/releases/download/0.1.1-dprint/plugin.wasm", + shouldCache: true, + }); + // non-matching URL expect( resolveAsset("https://plugins.dprint.dev/dprint/dprint-plugin-prettier/0.67.0/file.zip"), ).toEqual(undefined); }); +it("resolves a postfixed dprint release tag", () => { + expect( + getLatestInfoFromRelease("kjanat", "PSScriptAnalyzer", "https://plugins.dprint.dev", { + tagName: "0.1.1-dprint", + checksum: "a".repeat(64), + kind: "wasm", + }), + ).toEqual({ + schemaVersion: 1, + url: "https://plugins.dprint.dev/kjanat/PSScriptAnalyzer/0.1.1-dprint/asset/plugin.wasm", + version: "0.1.1", + checksum: "a".repeat(64), + repoUrl: "https://github.com/kjanat/PSScriptAnalyzer", + downloadKey: "kjanat/PSScriptAnalyzer", + tag: "0.1.1-dprint", + npm: undefined, + }); + + expect( + getLatestInfoFromRelease("kjanat", "PSScriptAnalyzer", "https://plugins.dprint.dev", { + tagName: "npm-0.1.1", + checksum: "a".repeat(64), + kind: "wasm", + }), + ).toEqual(undefined); +}); + +it("resolves a schema from a postfixed dprint release tag", async () => { + expect( + await tryResolveSchemaUrl( + new URL("https://plugins.dprint.dev/kjanat/PSScriptAnalyzer/0.1.1-dprint/schema.json"), + ), + ).toEqual( + "https://github.com/kjanat/PSScriptAnalyzer/releases/download/0.1.1-dprint/schema.json", + ); +}); + it("tryResolveUserLatestJson", async () => { // non-matching expect( diff --git a/plugins.ts b/plugins.ts index ff29f1e..59e7559 100644 --- a/plugins.ts +++ b/plugins.ts @@ -1,7 +1,8 @@ import infoJson from "./info.json" with { type: "json" }; -import { checkGithubRepoExists, getLatestReleaseInfo } from "./utils/mod.js"; +import { checkGithubRepoExists, getLatestReleaseInfo, type ReleaseInfo } from "./utils/mod.js"; const tagPattern = "([A-Za-z0-9\._]+)"; +const releaseTagPattern = "([A-Za-z0-9\-\._]+)"; // repos may only contain alphanumeric, underscores, hyphens, and period const repoNamePattern = "([A-Za-z0-9\-\._]+)"; const dprintWasmPluginPattern = new URLPattern({ @@ -20,7 +21,7 @@ const userProcessPluginPattern = new URLPattern({ pathname: `/${userRepoPattern}-${tagPattern}.json`, }); const userSchemaPattern = new URLPattern({ - pathname: `/${userRepoPattern}/${tagPattern}/schema.json`, + pathname: `/${userRepoPattern}/${releaseTagPattern}/schema.json`, }); // known repos where shortname resolves to dprint-plugin-, @@ -63,6 +64,7 @@ const KNOWN_NON_PREFIXED_REPOS = new Set([ "bartlomieju/lax-sql", "sargunv/dprint-clang-format", "sargunv/dprint-cmakefmt", + "kjanat/PSScriptAnalyzer", ]); /** The npm package a plugin is distributed as. */ @@ -78,8 +80,13 @@ export interface PluginNpmInfo { // resolved repo name (ex. `dprint/dprint-plugin-typescript` and `g-plane/malva`) const npmPackagesByRepo = buildNpmPackagesByRepo(); +const RELEASE_TAG_SUFFIXES = new Map([ + ["kjanat/PSScriptAnalyzer", "-dprint"], +]); + const APPROVED_ASSET_REPOS = new Set([ "drluckyspin/dprint-plugin-swift", + "kjanat/PSScriptAnalyzer", ]); export function isAssetAllowedRepo(username: string, repo: string) { @@ -91,7 +98,7 @@ export function isAssetAllowedRepo(username: string, repo: string) { const assetNamePattern = "([A-Za-z0-9\\-\\._]+)"; const assetPattern = new URLPattern({ - pathname: `/${userRepoPattern}/${tagPattern}/asset/${assetNamePattern}`, + pathname: `/${userRepoPattern}/${releaseTagPattern}/asset/${assetNamePattern}`, }); export function tryResolveAssetUrl(url: URL): { githubUrl: string; shouldCache: boolean } | undefined { @@ -160,26 +167,46 @@ export async function getLatestInfo(username: string, repoName: string, origin: if (releaseInfo == null) { return undefined; } + return getLatestInfoFromRelease(username, repoName, origin, releaseInfo); +} + +export function getLatestInfoFromRelease( + username: string, + repoName: string, + origin: string, + releaseInfo: ReleaseInfo, +) { const displayRepoName = repoName.replace(/^dprint-plugin-/, ""); const extension = releaseInfo.kind === "wasm" ? "wasm" : "json"; + const repoKey = `${username}/${repoName}`; + const tagSuffix = RELEASE_TAG_SUFFIXES.get(repoKey); + if (tagSuffix != null && !releaseInfo.tagName.endsWith(tagSuffix)) { + return undefined; + } + const version = tagSuffix == null + ? releaseInfo.tagName.replace(/^v/, "") + : releaseInfo.tagName.slice(0, -tagSuffix.length); + const url = tagSuffix == null + ? username === "dprint" + ? `${origin}/${displayRepoName}-${releaseInfo.tagName}.${extension}` + : `${origin}/${username}/${displayRepoName}-${releaseInfo.tagName}.${extension}` + : `${origin}/${username}/${repoName}/${releaseInfo.tagName}/asset/plugin.${extension}`; // include the bare minimum in case someone else wants to implement // this behaviour on their server return { schemaVersion: 1, - url: username === "dprint" - ? `${origin}/${displayRepoName}-${releaseInfo.tagName}.${extension}` - : `${origin}/${username}/${displayRepoName}-${releaseInfo.tagName}.${extension}`, - version: releaseInfo.tagName.replace(/^v/, ""), + url, + version, checksum: releaseInfo.checksum, // the GitHub repo this plugin is published from (full name already resolved above) - repoUrl: `https://github.com/${username}/${repoName}`, + repoUrl: `https://github.com/${repoKey}`, // identifies this plugin's download analytics: the `username/repo` key that // downloads are recorded under and the tag of the latest release - downloadKey: `${username}/${repoName}`, + downloadKey: repoKey, tag: releaseInfo.tagName, // the npm package this plugin is published to, when it has one - npm: npmPackagesByRepo.get(`${username}/${repoName}`), + npm: npmPackagesByRepo.get(repoKey), }; }