g1t/apps/web/app/lib/packages.ts

156 lines5,903 bytesCodeBlame
1/**
2 * Words and commands for the Packages pages: sizes, digests, and how each
3 * registry's own tool logs in and installs.
4 */
5import type { Ecosystem, PackageSummary } from "@g1t/contracts";
6
7/** What each registry is called on the pages. */
8export const ECOSYSTEM_LABEL: Record<Ecosystem, string> = {
9 container: "Container",
10 npm: "npm",
11 composer: "Composer",
12 cargo: "Cargo",
13 go: "Go",
14};
15
16/**
17 * Each registry as the Packages page introduces it: what it is for, the
18 * command that starts one, its guide, and whether it is open yet.
19 */
20export const REGISTRIES: { ecosystem: Ecosystem; blurb: string; start: string; guide: string; ready: boolean }[] = [
21 {
22 ecosystem: "container",
23 blurb: "Docker and OCI images, signatures and attestations, pulled by anyone the image's repository lets read it.",
24 start: "docker push g1t.sh/<workspace>/<name>:<tag>",
25 guide: "/guides/containers/",
26 ready: true,
27 },
28 {
29 ecosystem: "npm",
30 blurb: "JavaScript and TypeScript packages under the workspace's scope, installed with npm, pnpm or yarn.",
31 start: "npm publish",
32 guide: "/guides/npm/",
33 ready: true,
34 },
35 {
36 ecosystem: "composer",
37 blurb: "PHP packages straight from the workspace's repositories: each tag is a version, nothing to upload.",
38 start: "git tag v1.0.0 && git push --tags",
39 guide: "/guides/composer/",
40 ready: true,
41 },
42 {
43 ecosystem: "go",
44 blurb: "Go modules fetched from the repositories themselves with go get, private ones with a token.",
45 start: "go get g1t.sh/<workspace>/<repo>",
46 guide: "/guides/go/",
47 ready: true,
48 },
49 {
50 ecosystem: "cargo",
51 blurb: "Rust crates in a registry of the workspace's own, published with cargo publish.",
52 start: "cargo publish --registry <workspace>",
53 guide: "/guides/packages/",
54 ready: false,
55 },
56];
57
58export type PackageSort = "updated" | "downloads" | "name";
59export type VisibilityFilter = "all" | "public" | "private";
60
61export const SORT_LABEL: Record<PackageSort, string> = {
62 updated: "Recently updated",
63 downloads: "Most downloads",
64 name: "Name",
65};
66
67export const VISIBILITY_LABEL: Record<VisibilityFilter, string> = { all: "All", public: "Public", private: "Private" };
68
69/** The packages to list: those of the visibility asked for, in the order asked for. */
70export function arrange<T extends Pick<PackageSummary, "visibility" | "updated_at" | "downloads" | "name">>(
71 list: T[],
72 visibility: VisibilityFilter,
73 sort: PackageSort,
74): T[] {
75 const shown = visibility === "all" ? [...list] : list.filter((pkg) => pkg.visibility === visibility);
76 const byName = (a: T, b: T) => a.name.localeCompare(b.name);
77 if (sort === "name") return shown.sort(byName);
78 if (sort === "downloads") return shown.sort((a, b) => b.downloads - a.downloads || byName(a, b));
79 return shown.sort((a, b) => b.updated_at.localeCompare(a.updated_at) || byName(a, b));
80}
81
82/** "940", "16.1k", "2.3M": download counts, short. */
83export function shortCount(n: number): string {
84 if (n >= 1_000_000) return `${Number((n / 1_000_000).toFixed(1))}M`;
85 if (n >= 1_000) return `${Number((n / 1_000).toFixed(1))}k`;
86 return String(Math.max(0, Math.round(n)));
87}
88
89/** "0 B", "812 KB", "12.3 MB", "1.4 GB", in powers of 1,000 as registries show them. */
90export function formatBytes(bytes: number): string {
91 if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
92 const units = ["B", "KB", "MB", "GB", "TB"];
93 let value = bytes;
94 let unit = 0;
95 while (value >= 1000 && unit < units.length - 1) {
96 value /= 1000;
97 unit++;
98 }
99 const shown = unit === 0 || value >= 100 ? Math.round(value) : Number(value.toFixed(1));
100 return `${shown} ${units[unit]}`;
101}
102
103/** `sha256:3f2a…` shortened to `3f2a9c1b7d0e` for lists. */
104export function shortDigest(digest: string): string {
105 return digest.replace(/^sha256:/, "").slice(0, 12);
106}
107
108/** The host packages are published to and installed from. */
109export function registryHost(address: string): string {
110 return address.split("/")[0] || "g1t.sh";
111}
112
113/**
114 * How to log in and install `pkg` at `version` (a tag or version) with its
115 * tool. `you` stands in the username a login takes.
116 */
117export function installCommands(
118 pkg: Pick<PackageSummary, "ecosystem" | "address" | "name" | "workspace">,
119 version: string | null,
120 you: string,
121): { login: string; install: string; registry?: string } {
122 const host = registryHost(pkg.address);
123 switch (pkg.ecosystem) {
124 case "container":
125 return {
126 login: `docker login ${host} -u ${you}`,
127 install: `docker pull ${pkg.address}${version ? `:${version}` : ""}`,
128 };
129 case "npm":
130 // The scope's registry (in .npmrc, needed for any install), then the
131 // token a private package also needs.
132 return {
133 registry: `npm config set @${pkg.workspace}:registry=https://${host}/-/npm/`,
134 login: `npm config set //${host}/-/npm/:_authToken=YOUR_TOKEN`,
135 install: `npm install @${pkg.workspace}/${pkg.name}${version ? `@${version}` : ""}`,
136 };
137 case "composer":
138 // The workspace's repository (in composer.json, needed for any
139 // install), then the credentials a private package also needs.
140 return {
141 registry: `composer config repositories.${pkg.workspace} composer https://${host}/-/composer/${pkg.workspace}/`,
142 login: `composer config --global --auth http-basic.${host} ${you} YOUR_TOKEN`,
143 install: `composer require ${pkg.name}${version ? `:${version}` : ""}`,
144 };
145 case "cargo":
146 return {
147 login: `cargo login --registry ${pkg.workspace}`,
148 install: `cargo add ${pkg.name} --registry ${pkg.workspace}${version ? ` --vers ${version}` : ""}`,
149 };
150 case "go":
151 return {
152 login: `go env -w GOPRIVATE=${host}/${pkg.workspace}`,
153 install: `go get ${pkg.address}${version ? `@${version}` : ""}`,
154 };
155 }
156}