| 1 | /** |
| 2 | * Words and commands for the Packages pages: sizes, digests, and how each |
| 3 | * registry's own tool logs in and installs. |
| 4 | */ |
| 5 | import type { Ecosystem, PackageSummary } from "@g1t/contracts"; |
| 6 | |
| 7 | /** What each registry is called on the pages. */ |
| 8 | export const ECOSYSTEM_LABEL: Record<Ecosystem, string> = { |
| 9 | container: "Container", |
| 10 | npm: "npm", |
| 11 | composer: "Composer", |
| 12 | cargo: "Cargo", |
| 13 | go: "Go", |
| 14 | maven: "Maven", |
| 15 | nuget: "NuGet", |
| 16 | rubygems: "RubyGems", |
| 17 | }; |
| 18 | |
| 19 | /** |
| 20 | * Each registry as the Packages page introduces it: what it is for, the |
| 21 | * command that starts one, its guide, and whether it is open yet. |
| 22 | */ |
| 23 | export const REGISTRIES: { ecosystem: Ecosystem; blurb: string; start: string; guide: string; ready: boolean }[] = [ |
| 24 | { |
| 25 | ecosystem: "container", |
| 26 | blurb: "Docker and OCI images, signatures and attestations, pulled by anyone the image's repository lets read it.", |
| 27 | start: "docker push g1t.sh/<workspace>/<name>:<tag>", |
| 28 | guide: "/guides/containers/", |
| 29 | ready: true, |
| 30 | }, |
| 31 | { |
| 32 | ecosystem: "npm", |
| 33 | blurb: "JavaScript and TypeScript packages under the workspace's scope, installed with npm, pnpm or yarn.", |
| 34 | start: "npm publish", |
| 35 | guide: "/guides/npm/", |
| 36 | ready: true, |
| 37 | }, |
| 38 | { |
| 39 | ecosystem: "composer", |
| 40 | blurb: "PHP packages straight from the workspace's repositories: each tag is a version, nothing to upload.", |
| 41 | start: "git tag v1.0.0 && git push --tags", |
| 42 | guide: "/guides/composer/", |
| 43 | ready: true, |
| 44 | }, |
| 45 | { |
| 46 | ecosystem: "cargo", |
| 47 | blurb: "Rust crates in a sparse registry of the workspace's own, published with cargo publish and added with cargo add.", |
| 48 | start: "cargo publish --registry <workspace>", |
| 49 | guide: "/guides/cargo/", |
| 50 | ready: true, |
| 51 | }, |
| 52 | { |
| 53 | ecosystem: "go", |
| 54 | blurb: "Go modules fetched from the repositories themselves with go get, private ones with a token.", |
| 55 | start: "go get g1t.sh/<workspace>/<repo>", |
| 56 | guide: "/guides/go/", |
| 57 | ready: true, |
| 58 | }, |
| 59 | { |
| 60 | ecosystem: "maven", |
| 61 | blurb: "Java and Kotlin libraries in a Maven repository of the workspace's own, deployed with Maven or Gradle, SNAPSHOTs included.", |
| 62 | start: "mvn deploy", |
| 63 | guide: "/guides/maven/", |
| 64 | ready: true, |
| 65 | }, |
| 66 | { |
| 67 | ecosystem: "nuget", |
| 68 | blurb: ".NET packages in a NuGet feed of the workspace's own, pushed with dotnet nuget push and restored with dotnet.", |
| 69 | start: "dotnet nuget push <package>.nupkg --source <workspace>", |
| 70 | guide: "/guides/nuget/", |
| 71 | ready: true, |
| 72 | }, |
| 73 | { |
| 74 | ecosystem: "rubygems", |
| 75 | blurb: "Ruby gems in a registry of the workspace's own, pushed with gem push and installed with Bundler.", |
| 76 | start: "gem push <gem>.gem --host https://g1t.sh/-/rubygems/<workspace>", |
| 77 | guide: "/guides/rubygems/", |
| 78 | ready: true, |
| 79 | }, |
| 80 | ]; |
| 81 | |
| 82 | export type PackageSort = "updated" | "downloads" | "name"; |
| 83 | export type VisibilityFilter = "all" | "public" | "private"; |
| 84 | |
| 85 | export const SORT_LABEL: Record<PackageSort, string> = { |
| 86 | updated: "Recently updated", |
| 87 | downloads: "Most downloads", |
| 88 | name: "Name", |
| 89 | }; |
| 90 | |
| 91 | export const VISIBILITY_LABEL: Record<VisibilityFilter, string> = { all: "All", public: "Public", private: "Private" }; |
| 92 | |
| 93 | /** The packages to list: those of the visibility asked for, in the order asked for. */ |
| 94 | export function arrange<T extends Pick<PackageSummary, "visibility" | "updated_at" | "downloads" | "name">>( |
| 95 | list: T[], |
| 96 | visibility: VisibilityFilter, |
| 97 | sort: PackageSort, |
| 98 | ): T[] { |
| 99 | const shown = visibility === "all" ? [...list] : list.filter((pkg) => pkg.visibility === visibility); |
| 100 | const byName = (a: T, b: T) => a.name.localeCompare(b.name); |
| 101 | if (sort === "name") return shown.sort(byName); |
| 102 | if (sort === "downloads") return shown.sort((a, b) => b.downloads - a.downloads || byName(a, b)); |
| 103 | return shown.sort((a, b) => b.updated_at.localeCompare(a.updated_at) || byName(a, b)); |
| 104 | } |
| 105 | |
| 106 | /** "940", "16.1k", "2.3M": download counts, short. */ |
| 107 | export function shortCount(n: number): string { |
| 108 | if (n >= 1_000_000) return `${Number((n / 1_000_000).toFixed(1))}M`; |
| 109 | if (n >= 1_000) return `${Number((n / 1_000).toFixed(1))}k`; |
| 110 | return String(Math.max(0, Math.round(n))); |
| 111 | } |
| 112 | |
| 113 | /** "0 B", "812 KB", "12.3 MB", "1.4 GB", in powers of 1,000 as registries show them. */ |
| 114 | export function formatBytes(bytes: number): string { |
| 115 | if (!Number.isFinite(bytes) || bytes <= 0) return "0 B"; |
| 116 | const units = ["B", "KB", "MB", "GB", "TB"]; |
| 117 | let value = bytes; |
| 118 | let unit = 0; |
| 119 | while (value >= 1000 && unit < units.length - 1) { |
| 120 | value /= 1000; |
| 121 | unit++; |
| 122 | } |
| 123 | const shown = unit === 0 || value >= 100 ? Math.round(value) : Number(value.toFixed(1)); |
| 124 | return `${shown} ${units[unit]}`; |
| 125 | } |
| 126 | |
| 127 | /** `sha256:3f2a…` shortened to `3f2a9c1b7d0e` for lists. */ |
| 128 | export function shortDigest(digest: string): string { |
| 129 | return digest.replace(/^sha256:/, "").slice(0, 12); |
| 130 | } |
| 131 | |
| 132 | /** The host packages are published to and installed from. */ |
| 133 | export function registryHost(address: string): string { |
| 134 | return address.split("/")[0] || "g1t.sh"; |
| 135 | } |
| 136 | |
| 137 | /** |
| 138 | * How to log in and install `pkg` at `version` (a tag or version) with its |
| 139 | * tool. `you` stands in the username a login takes. |
| 140 | */ |
| 141 | export function installCommands( |
| 142 | pkg: Pick<PackageSummary, "ecosystem" | "address" | "name" | "workspace">, |
| 143 | version: string | null, |
| 144 | you: string, |
| 145 | ): { login: string; install: string; registry?: string } { |
| 146 | const host = registryHost(pkg.address); |
| 147 | switch (pkg.ecosystem) { |
| 148 | case "container": |
| 149 | return { |
| 150 | login: `docker login ${host} -u ${you}`, |
| 151 | install: `docker pull ${pkg.address}${version ? `:${version}` : ""}`, |
| 152 | }; |
| 153 | case "npm": |
| 154 | // The scope's registry (in .npmrc, needed for any install), then the |
| 155 | // token a private package also needs. |
| 156 | return { |
| 157 | registry: `npm config set @${pkg.workspace}:registry=https://${host}/-/npm/`, |
| 158 | login: `npm config set //${host}/-/npm/:_authToken=YOUR_TOKEN`, |
| 159 | install: `npm install @${pkg.workspace}/${pkg.name}${version ? `@${version}` : ""}`, |
| 160 | }; |
| 161 | case "composer": |
| 162 | // The workspace's repository (in composer.json, needed for any |
| 163 | // install), then the credentials a private package also needs. |
| 164 | return { |
| 165 | registry: `composer config repositories.${pkg.workspace} composer https://${host}/-/composer/${pkg.workspace}/`, |
| 166 | login: `composer config --global --auth http-basic.${host} ${you} YOUR_TOKEN`, |
| 167 | install: `composer require ${pkg.name}${version ? `:${version}` : ""}`, |
| 168 | }; |
| 169 | case "cargo": |
| 170 | // The workspace's registry (in .cargo/config.toml, needed for any |
| 171 | // install), then the token a private crate also needs, which cargo |
| 172 | // login asks for. |
| 173 | return { |
| 174 | registry: `mkdir -p .cargo && printf '[registries.${pkg.workspace}]\\nindex = "sparse+https://${host}/-/cargo/${pkg.workspace}/index/"\\ncredential-provider = "cargo:token"\\n' >> .cargo/config.toml`, |
| 175 | login: `cargo login --registry ${pkg.workspace}`, |
| 176 | install: `cargo add ${pkg.name}${version ? `@${version}` : ""} --registry ${pkg.workspace}`, |
| 177 | }; |
| 178 | case "go": |
| 179 | return { |
| 180 | login: `go env -w GOPRIVATE=${host}/${pkg.workspace}`, |
| 181 | install: `go get ${pkg.address}${version ? `@${version}` : ""}`, |
| 182 | }; |
| 183 | case "maven": { |
| 184 | // Maven takes the repository with the artifact; its credentials are a |
| 185 | // <server> of the same id in ~/.m2/settings.xml, written here only |
| 186 | // when there is no settings.xml yet. |
| 187 | const repository = `https://${host}/-/maven/${pkg.workspace}/`; |
| 188 | return { |
| 189 | login: `mkdir -p ~/.m2 && [ ! -e ~/.m2/settings.xml ] && printf '<settings><servers><server><id>${pkg.workspace}</id><username>${you}</username><password>YOUR_TOKEN</password></server></servers></settings>\\n' > ~/.m2/settings.xml`, |
| 190 | install: `mvn dependency:get -Dartifact=${pkg.name}${version ? `:${version}` : ":LATEST"} -DremoteRepositories=${pkg.workspace}::default::${repository}`, |
| 191 | }; |
| 192 | } |
| 193 | case "nuget": |
| 194 | // The workspace's feed as a named source (needed for any install), |
| 195 | // then the credentials a private package also needs. |
| 196 | return { |
| 197 | registry: `dotnet nuget add source https://${host}/-/nuget/${pkg.workspace}/v3/index.json --name ${pkg.workspace}`, |
| 198 | login: `dotnet nuget update source ${pkg.workspace} --username ${you} --password YOUR_TOKEN --store-password-in-clear-text`, |
| 199 | install: `dotnet add package ${pkg.name}${version ? ` --version ${version}` : ""} --source https://${host}/-/nuget/${pkg.workspace}/v3/index.json`, |
| 200 | }; |
| 201 | case "rubygems": { |
| 202 | const source = `https://${host}/-/rubygems/${pkg.workspace}/`; |
| 203 | return { |
| 204 | login: `bundle config set --global ${source} ${you}:YOUR_TOKEN`, |
| 205 | install: `bundle add ${pkg.name}${version ? ` --version ${version}` : ""} --source ${source}`, |
| 206 | }; |
| 207 | } |
| 208 | } |
| 209 | } |