diff --git a/homeView.test.ts b/homeView.test.ts index 76bbfd7..fc99b10 100644 --- a/homeView.test.ts +++ b/homeView.test.ts @@ -90,3 +90,15 @@ it("should keep the url searchable when a specifier replaced it", () => { expect(search).toContain("https://plugins.dprint.dev/json-1.0.0.wasm"); expect(search).toContain("npm:@dprint/json@1.2.3"); }); + +it("should list plugins by repo without the dprint org or plugin prefix", () => { + const { html } = renderReferences([ + createPlugin({ name: "dprint-plugin-json", repo: "dprint/dprint-plugin-json" }), + createPlugin({ name: "dprint_plugin_malva", repo: "g-plane/malva" }), + createPlugin({ name: "dprint-plugin-kdl", repo: "kachick/dprint-plugin-kdl" }), + // data cached before info.json declared the repo + createPlugin({ name: "dprint-plugin-cached" }), + ]); + const names = [...html.matchAll(/([^<]*)<\/span>/g)].map((m) => m[1]); + expect(names).toEqual(["dprint-plugin-json", "g-plane/malva", "kachick/kdl", "dprint-plugin-cached"]); +}); diff --git a/homeView.tsx b/homeView.tsx index f746709..dcee7bb 100644 --- a/homeView.tsx +++ b/homeView.tsx @@ -86,7 +86,9 @@ function renderPage(pluginsData: PluginsData) { autocomplete="off" spellcheck={false} /> - dprint plugin docs ↗ + + dprint plugin docs ↗ + {renderPlugins(pluginsData)} @@ -101,6 +103,7 @@ function renderPage(pluginsData: PluginsData) { function pluginSearchText(plugin: PluginData) { const parts: (string | undefined)[] = [ plugin.name, + plugin.repo, // the url stays searchable even when the npm specifier is what's shown plugin.url, latestReference(plugin), @@ -164,7 +167,7 @@ function renderPlugin(plugin: PluginData) {
- {plugin.name} + {displayName(plugin)} {plugin.version ? {plugin.version} : null}
{renderPluginLinks(plugin)} @@ -174,7 +177,7 @@ function renderPlugin(plugin: PluginData) { {latestReference(plugin)}
- Downloads (30d) + Downloads (30d) {plugin.downloadCount.allVersions?.toLocaleString("en-US")}
@@ -210,6 +213,18 @@ function latestReference(plugin: PluginData) { // the repo and docs links shown beneath a plugin's name. both are optional: the // repo url is derived during the build and docs only exist for plugins with a // dprint.dev page. +// plugins are listed by the repo they're published from: the dprint org is +// left off its own plugins and the `dprint-plugin-` prefix off everyone else's +// (ex. `dprint-plugin-json` and `kachick/kdl`) +function displayName(plugin: PluginData) { + if (plugin.repo == null) { + return plugin.name; + } + return plugin.repo.startsWith("dprint/") + ? plugin.repo.slice("dprint/".length) + : plugin.repo.replace("/dprint-plugin-", "/"); +} + function renderPluginLinks(plugin: PluginData) { if (plugin.repoUrl == null && plugin.website == null) { return null; diff --git a/info.json b/info.json index e72cfc9..0347308 100644 --- a/info.json +++ b/info.json @@ -4,6 +4,7 @@ "latest": [ { "name": "dprint-plugin-typescript", + "repo": "dprint/dprint-plugin-typescript", "description": "TypeScript/JavaScript code formatter.", "website": "https://dprint.dev/plugins/typescript/", "npm": { @@ -34,6 +35,7 @@ }, { "name": "dprint-plugin-json", + "repo": "dprint/dprint-plugin-json", "description": "JSON/JSONC code formatter.", "website": "https://dprint.dev/plugins/json/", "npm": { @@ -55,6 +57,7 @@ }, { "name": "dprint-plugin-markdown", + "repo": "dprint/dprint-plugin-markdown", "description": "Markdown code formatter.", "website": "https://dprint.dev/plugins/markdown/", "npm": { @@ -79,6 +82,7 @@ }, { "name": "dprint-plugin-toml", + "repo": "dprint/dprint-plugin-toml", "description": "TOML code formatter.", "website": "https://dprint.dev/plugins/toml/", "npm": { @@ -97,6 +101,7 @@ }, { "name": "dprint-plugin-dockerfile", + "repo": "dprint/dprint-plugin-dockerfile", "description": "Dockerfile code formatter.", "website": "https://dprint.dev/plugins/dockerfile/", "npm": { @@ -119,6 +124,7 @@ }, { "name": "dprint-plugin-biome", + "repo": "dprint/dprint-plugin-biome", "description": "Biome (JS/TS/JSON) wrapper plugin.", "website": "https://dprint.dev/plugins/biome/", "npm": { @@ -151,6 +157,7 @@ }, { "name": "dprint-plugin-mago", + "repo": "dprint/dprint-plugin-mago", "description": "Mago (PHP) wrapper plugin.", "website": "https://dprint.dev/plugins/mago/", "npm": { @@ -170,6 +177,7 @@ }, { "name": "dprint-plugin-ruff", + "repo": "dprint/dprint-plugin-ruff", "description": "Ruff (Python) wrapper plugin.", "website": "https://dprint.dev/plugins/ruff/", "npm": { @@ -191,6 +199,7 @@ }, { "name": "dprint-plugin-jupyter", + "repo": "dprint/dprint-plugin-jupyter", "description": "Jupyter notebook code block formatter.", "website": "https://dprint.dev/plugins/jupyter/", "npm": { @@ -210,7 +219,8 @@ "configExcludes": [] }, { - "name": "jakebailey/gofumpt", + "name": "dprint-plugin-gofumpt", + "repo": "jakebailey/dprint-plugin-gofumpt", "description": "Gofumpt (Go) wrapper plugin.", "website": "https://dprint.dev/plugins/gofumpt/", "npm": { @@ -230,7 +240,8 @@ "configExcludes": [] }, { - "name": "kachick/kdl", + "name": "dprint-plugin-kdl", + "repo": "kachick/dprint-plugin-kdl", "description": "KDL crate wrapper plugin.", "npm": { "name": "@kachick/dprint-plugin-kdl" @@ -246,7 +257,8 @@ "configExcludes": [] }, { - "name": "kachick/nix", + "name": "dprint-plugin-nix", + "repo": "kachick/dprint-plugin-nix", "description": "Nixfmt-rs (Nix) wrapper plugin.", "npm": { "name": "@kachick/dprint-plugin-nix" @@ -263,7 +275,8 @@ "configExcludes": [] }, { - "name": "kachick/sh", + "name": "dprint-plugin-sh", + "repo": "kachick/dprint-plugin-sh", "description": "Shuck-formatter (shell script) wrapper plugin.", "npm": { "name": "@kachick/dprint-plugin-sh" @@ -305,6 +318,7 @@ }, { "name": "kachick/typstyle", + "repo": "kachick/dprint-plugin-typstyle", "description": "Typstyle (Typst) wrapper plugin.", "npm": { "name": "@kachick/dprint-plugin-typstyle" @@ -321,7 +335,8 @@ "configExcludes": [] }, { - "name": "g-plane/malva", + "name": "dprint_plugin_malva", + "repo": "g-plane/malva", "description": "CSS, SCSS, Sass and Less formatter.", "website": "https://dprint.dev/plugins/malva/", "npm": { @@ -347,7 +362,8 @@ ] }, { - "name": "g-plane/markup_fmt", + "name": "dprint_plugin_markup", + "repo": "g-plane/markup_fmt", "description": "HTML, Vue, Svelte, Astro, Angular, Jinja, Twig, Nunjucks, and Vento formatter.", "website": "https://dprint.dev/plugins/markup_fmt/", "npm": { @@ -396,7 +412,8 @@ "configExcludes": [] }, { - "name": "g-plane/pretty_yaml", + "name": "dprint_plugin_yaml", + "repo": "g-plane/pretty_yaml", "description": "YAML formatter.", "website": "https://dprint.dev/plugins/pretty_yaml/", "npm": { @@ -417,7 +434,8 @@ "configExcludes": [] }, { - "name": "g-plane/pretty_graphql", + "name": "dprint_plugin_graphql", + "repo": "g-plane/pretty_graphql", "description": "GraphQL formatter.", "website": "https://dprint.dev/plugins/pretty_graphql/", "npm": { @@ -438,6 +456,7 @@ }, { "name": "dprint-plugin-oxc", + "repo": "dprint/dprint-plugin-oxc", "description": "Oxc (JS/TS/JSON/CSS/SCSS/Less/GraphQL/YAML/TOML) wrapper plugin.", "website": "https://dprint.dev/plugins/oxc/", "npm": { @@ -486,7 +505,8 @@ ] }, { - "name": "apcamargo/bibtex-tidy", + "name": "dprint-plugin-bibtex-tidy", + "repo": "apcamargo/dprint-plugin-bibtex-tidy", "description": "Tidy bibtex files.", "selected": false, "configKey": "bibtex-tidy", @@ -501,6 +521,7 @@ }, { "name": "apcamargo/typstyle", + "repo": "apcamargo/dprint-plugin-typstyle", "description": "A beautiful and reliable code formatter for Typst.", "selected": false, "configKey": "typstyle", @@ -515,7 +536,8 @@ "configExcludes": [] }, { - "name": "bartlomieju/lax-css", + "name": "lax-css", + "repo": "bartlomieju/lax-css", "description": "CSS, SCSS, and Less formatter that never reinterprets your styles.", "npm": { "name": "lax-css" @@ -538,7 +560,8 @@ ] }, { - "name": "bartlomieju/lax-markup", + "name": "lax-markup", + "repo": "bartlomieju/lax-markup", "description": "HTML, XML, SVG, Vue, Svelte, and Astro formatter that preserves rendering-critical whitespace.", "npm": { "name": "lax-markup" @@ -566,7 +589,8 @@ "configExcludes": [] }, { - "name": "bartlomieju/lax-sql", + "name": "lax-sql", + "repo": "bartlomieju/lax-sql", "description": "Dialect-agnostic SQL formatter.", "npm": { "name": "lax-sql" @@ -587,7 +611,8 @@ "configExcludes": [] }, { - "name": "jolars/panache", + "name": "dprint-plugin-panache", + "repo": "jolars/dprint-plugin-panache", "description": "Quarto, Pandoc, R Markdown, and Markdown formatter.", "selected": false, "configKey": "panache", @@ -611,7 +636,8 @@ "configExcludes": [] }, { - "name": "drluckyspin/swift", + "name": "dprint-plugin-swift", + "repo": "drluckyspin/dprint-plugin-swift", "description": "Swift code formatter (SwiftFormat wrapper).", "selected": false, "configKey": "swiftformat", @@ -630,6 +656,7 @@ }, { "name": "dprint-plugin-prettier", + "repo": "dprint/dprint-plugin-prettier", "description": "Prettier wrapper plugin (consider faster dedicated plugins for JS/TS, CSS, etc.).", "website": "https://dprint.dev/plugins/prettier/", "npm": { @@ -678,6 +705,7 @@ }, { "name": "dprint-plugin-roslyn", + "repo": "dprint/dprint-plugin-roslyn", "description": "C# and Visual Basic code formatter (Roslyn).", "website": "https://dprint.dev/plugins/roslyn/", "npm": { @@ -702,6 +730,7 @@ }, { "name": "dprint-plugin-exec", + "repo": "dprint/dprint-plugin-exec", "description": "Formats code using formatting CLIs installed on the host machine (rustfmt, clang-format, shfmt, etc.).", "website": "https://dprint.dev/plugins/exec/", "npm": { @@ -818,7 +847,8 @@ ] }, { - "name": "sargunv/dprint-clang-format", + "name": "dprint-plugin-clang-format", + "repo": "sargunv/dprint-clang-format", "description": "C, C++, and Objective-C code formatter (clang-format).", "selected": false, "configKey": "clangFormat", @@ -846,7 +876,8 @@ "configExcludes": [] }, { - "name": "sargunv/dprint-cmakefmt", + "name": "dprint-cmakefmt", + "repo": "sargunv/dprint-cmakefmt", "description": "CMake code formatter (cmakefmt).", "selected": false, "configKey": "cmakefmt", @@ -865,7 +896,8 @@ "configExcludes": [] }, { - "name": "jolars/arity", + "name": "dprint-plugin-arity", + "repo": "jolars/dprint-plugin-arity", "description": "R code formatter (arity).", "selected": false, "configKey": "arity", @@ -882,7 +914,8 @@ "configExcludes": [] }, { - "name": "jolars/fatou", + "name": "dprint-plugin-fatou", + "repo": "jolars/dprint-plugin-fatou", "description": "Julia code formatter (fatou).", "selected": false, "configKey": "fatou", @@ -896,7 +929,8 @@ "configExcludes": [] }, { - "name": "jolars/badness", + "name": "dprint-plugin-badness", + "repo": "jolars/dprint-plugin-badness", "description": "LaTeX and BibTeX code formatter (badness).", "selected": false, "configKey": "badness", diff --git a/plugins.test.ts b/plugins.test.ts index 1511fc3..db88573 100644 --- a/plugins.test.ts +++ b/plugins.test.ts @@ -1,11 +1,26 @@ import { expect, it } from "vitest"; -import { tryResolveAssetUrl, tryResolveLatestJson } from "./plugins.js"; +import infoJson from "./info.json" with { type: "json" }; +import { getPluginRepo, tryResolveAssetUrl, tryResolveLatestJson } from "./plugins.js"; import { getLatestReleaseInfo } from "./utils/github.js"; function resolveAsset(url: string) { return tryResolveAssetUrl(new URL(url)); } +it("getPluginRepo", () => { + expect(getPluginRepo({ repo: "dprint/dprint-plugin-typescript" })).toEqual({ + username: "dprint", + repoName: "dprint-plugin-typescript", + }); + expect(getPluginRepo({ repo: "g-plane/malva" })).toEqual({ username: "g-plane", repoName: "malva" }); +}); + +it("info.json entries should say what repo they're published from", () => { + for (const plugin of infoJson.latest) { + expect(plugin.repo, plugin.name).toMatch(/^[A-Za-z0-9-]+\/[A-Za-z0-9._-]+$/); + } +}); + it("tryResolveAssetUrl", () => { // allowed repo — should cache expect( @@ -151,6 +166,18 @@ it("tryResolveUserLatestJson", async () => { npm: { name: "dprint-plugin-malva" }, }); } + // community plugin asked for by the name it has in info.json + { + const result = await getValidResultForUrl("https://plugins.dprint.dev/dprint/dprint_plugin_malva/latest.json"); + const releaseInfo = await getLatestReleaseInfo("g-plane", "malva"); + expect(result).toEqual({ + schemaVersion: 1, + url: `https://plugins.dprint.dev/g-plane/malva-${releaseInfo!.tagName}.wasm`, + version: releaseInfo!.tagName.replace(/^v/, ""), + checksum: releaseInfo!.checksum, + npm: { name: "dprint-plugin-malva" }, + }); + } async function getValidResultForUrl(url: string) { const result = await tryResolveLatestJson(new URL(url))!; diff --git a/plugins.ts b/plugins.ts index 6bb26d6..57d3039 100644 --- a/plugins.ts +++ b/plugins.ts @@ -82,6 +82,19 @@ export interface PluginNpmInfo { // resolved repo name (ex. `dprint/dprint-plugin-typescript` and `g-plane/malva`) const npmPackagesByRepo = buildNpmPackagesByRepo(); +// the repos of the info.json plugins, keyed by plugin name +const reposByPluginName = buildReposByPluginName(); + +/** + * The GitHub repo an info.json entry is published from. An entry's `name` is + * the name the plugin reports about itself, which the cli matches against the + * plugins in a config file, so `repo` is what says where it's published from. + */ +export function getPluginRepo(plugin: { repo: string }) { + const slashIndex = plugin.repo.indexOf("/"); + return { username: plugin.repo.slice(0, slashIndex), repoName: plugin.repo.slice(slashIndex + 1) }; +} + const APPROVED_ASSET_REPOS = new Set([ "drluckyspin/dprint-plugin-swift", ]); @@ -139,8 +152,13 @@ export async function tryResolveLatestJson(url: URL) { if (!result) { return undefined; } - const username = result.pathname.groups[0]!; - const shortRepoName = result.pathname.groups[1]!; + // `dprint add ` asks for `dprint/`, and the name a + // plugin has in info.json isn't necessarily the repo it's published from + const namedRepo = result.pathname.groups[0] === "dprint" + ? reposByPluginName.get(result.pathname.groups[1]!) + : undefined; + const username = namedRepo?.username ?? result.pathname.groups[0]!; + const shortRepoName = namedRepo?.repoName ?? result.pathname.groups[1]!; const latestInfo = await getLatestInfo(username, shortRepoName, url.origin); if (latestInfo == null) { return 404; @@ -235,19 +253,26 @@ async function userRepoTagPatternMapper( function buildNpmPackagesByRepo() { const result = new Map(); - for (const plugin of infoJson.latest as { name: string; npm?: PluginNpmInfo }[]) { + for (const plugin of infoJson.latest as { repo: string; npm?: PluginNpmInfo }[]) { if (plugin.npm == null) { continue; } - const slashIndex = plugin.name.indexOf("/"); - const username = slashIndex === -1 ? "dprint" : plugin.name.slice(0, slashIndex); - const shortName = plugin.name.slice(slashIndex + 1).replace(/^dprint-plugin-/, ""); + const { username, repoName } = getPluginRepo(plugin); + const shortName = repoName.replace(/^dprint-plugin-/, ""); result.set(`${username}/${shortName}`, plugin.npm); result.set(`${username}/dprint-plugin-${shortName}`, plugin.npm); } return result; } +function buildReposByPluginName() { + const result = new Map(); + for (const plugin of infoJson.latest) { + result.set(plugin.name, getPluginRepo(plugin)); + } + return result; +} + async function getFullRepoName(username: string, repoName: string) { if (repoName.startsWith("dprint-plugin-")) { return repoName; diff --git a/readInfoFile.ts b/readInfoFile.ts index 3960511..a83f270 100644 --- a/readInfoFile.ts +++ b/readInfoFile.ts @@ -1,6 +1,6 @@ import { env } from "cloudflare:workers"; import infoJson from "./info.json" with { type: "json" }; -import { getLatestInfo, type PluginNpmInfo } from "./plugins.js"; +import { getLatestInfo, getPluginRepo, type PluginNpmInfo } from "./plugins.js"; import { getDownloadCounts, type PluginDownloadCounts } from "./utils/analytics.js"; import { getNpmDownloadCounts, getNpmLatestVersions } from "./utils/npm.js"; @@ -10,7 +10,11 @@ export interface PluginsData { } export interface PluginData { + // the name the plugin reports about itself name: string; + // the `username/repo` the plugin is published from. carried over from + // info.json, so it's absent from data cached before info.json declared it + repo?: string; url: string; version: string; downloadCount: { @@ -162,10 +166,8 @@ async function buildInfoFile(origin: string): Promise> { const released = []; for (const plugin of latest) { - const [username, pluginName] = plugin.name.split("/"); - const info = pluginName - ? await getLatestInfo(username, pluginName, origin) - : await getLatestInfo("dprint", plugin.name, origin); + const { username, repoName } = getPluginRepo(plugin); + const info = await getLatestInfo(username, repoName, origin); if (info != null) { released.push({ plugin, info }); }