Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| A project is an app or a library: libraries show their package and how to ship a release, not production | 1 | /** |
| 2 | * Words and links for a project that is a library or a tool rather than an | |
| 3 | * app: which of its repository's packages to show, and how to publish a | |
| 4 | * first one. Whether a project is one is decided by the projects service | |
| 5 | * (`Project.kind`); this only says it on the pages. | |
| 6 | */ | |
| Merge branch 'projects-kind-and-links' | 7 | import type { |
| 8 | Ecosystem, | |
| 9 | PackageSummary, | |
| 10 | Project, | |
| 11 | ProjectChanges, | |
| 12 | ProjectEcosystem, | |
| 13 | ProjectKind, | |
| 14 | ProjectLinks, | |
| 15 | ProjectRuns, | |
| 16 | ProjectSetting, | |
| 17 | } from "@g1t/contracts"; | |
| A project is an app or a library: libraries show their package and how to ship a release, not production | 18 | |
| 19 | /** As lib/packages.ts names each registry; repeated so this module stands alone in tests. */ | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 20 | const REGISTRY: Record<Ecosystem, string> = { |
| 21 | container: "Container", | |
| 22 | npm: "npm", | |
| 23 | composer: "Composer", | |
| 24 | cargo: "Cargo", | |
| 25 | go: "Go", | |
| 26 | maven: "Maven", | |
| 27 | nuget: "NuGet", | |
| 28 | rubygems: "RubyGems", | |
| 29 | }; | |
| A project is an app or a library: libraries show their package and how to ship a release, not production | 30 | |
| 31 | const DOCS = "https://docs.g1t.sh"; | |
| 32 | ||
| Merge branch 'projects-kind-and-links' | 33 | /** One choice of what a project is, as its settings and overview offer it. */ |
| 34 | export type KindChoice = "auto" | "g1t" | "elsewhere" | "library" | "tool" | "docs" | "other"; | |
| 35 | ||
| 36 | /** What each choice sets: a kind and where it runs, `auto` for what is left to detection. */ | |
| 37 | export const KIND_CHOICE_SETS: Record<KindChoice, { kind: ProjectKind | "auto"; runs: ProjectRuns | "auto" }> = { | |
| 38 | auto: { kind: "auto", runs: "auto" }, | |
| 39 | g1t: { kind: "app", runs: "g1t" }, | |
| 40 | elsewhere: { kind: "app", runs: "elsewhere" }, | |
| 41 | library: { kind: "library", runs: "auto" }, | |
| 42 | tool: { kind: "tool", runs: "auto" }, | |
| 43 | docs: { kind: "docs", runs: "auto" }, | |
| 44 | other: { kind: "other", runs: "auto" }, | |
| 45 | }; | |
| 46 | ||
| 47 | /** The choices for "What it is" in a project's settings, in order. */ | |
| 48 | export const KIND_CHOICES: { value: KindChoice; label: string; hint: string }[] = [ | |
| A project is an app or a library: libraries show their package and how to ship a release, not production | 49 | { value: "auto", label: "Detect automatically", hint: "From whether Deployments are on, the packages it publishes and the files at its root." }, |
| Merge branch 'projects-kind-and-links' | 50 | { value: "g1t", label: "App or site, deployed on g1t", hint: "g1t builds it: production on g1t.page from the default branch, and a preview for every pull request." }, |
| 51 | { | |
| 52 | value: "elsewhere", | |
| 53 | label: "App or site, deployed elsewhere", | |
| 54 | hint: "Your own pipeline deploys it. Its overview shows production at the address you give.", | |
| 55 | }, | |
| 56 | { value: "library", label: "Library or package", hint: "Installed by other code: its overview shows its packages and releases." }, | |
| 57 | { value: "tool", label: "Tool or CLI", hint: "Installed and run by people: its overview shows its releases and packages." }, | |
| 58 | { value: "docs", label: "Documentation", hint: "Docs or site content: its overview shows where they are read." }, | |
| 59 | { value: "other", label: "Something else", hint: "Configuration, research, notes: its overview shows its links and its work." }, | |
| A project is an app or a library: libraries show their package and how to ship a release, not production | 60 | ]; |
| 61 | ||
| Merge branch 'projects-kind-and-links' | 62 | /** The choice a project's setting is; null when it is one no choice makes (set through the API). */ |
| 63 | export function choiceOf(setting: ProjectSetting): KindChoice | null { | |
| 64 | if (!setting.kind && !setting.runs) return "auto"; | |
| 65 | if (setting.kind && setting.kind !== "app") return setting.kind; | |
| 66 | if (setting.runs) return setting.runs; | |
| 67 | return null; | |
| 68 | } | |
| 69 | ||
| 70 | /** Whether it is set to be something that never deploys: a library, a tool or other. */ | |
| 71 | export function neverDeploys(project: Pick<Project, "setting">): boolean { | |
| 72 | const kind = project.setting?.kind; | |
| 73 | return kind != null && kind !== "app" && kind !== "docs"; | |
| 74 | } | |
| 75 | ||
| 76 | /** What a project is, in a word or three, for its badge. */ | |
| 77 | export function kindLabel(project: Pick<Project, "kind" | "runs">): string { | |
| 78 | switch (project.kind) { | |
| 79 | case "app": | |
| 80 | return project.runs === "g1t" ? "App on g1t" : project.runs === "elsewhere" ? "App, deployed elsewhere" : "App"; | |
| 81 | case "library": | |
| 82 | return "Library"; | |
| 83 | case "tool": | |
| 84 | return "Tool"; | |
| 85 | case "docs": | |
| 86 | return project.runs === "g1t" ? "Docs on g1t" : "Docs"; | |
| 87 | default: | |
| 88 | return "Project"; | |
| 89 | } | |
| 90 | } | |
| 91 | ||
| 92 | /** A link as pages list it: what it is, its label and address. */ | |
| 93 | export type ShownLink = { key: string; type: "homepage" | "docs" | "production" | "custom"; label: string; url: string }; | |
| 94 | ||
| 95 | /** An address without its scheme, or a trailing slash: `g1t.sh/docs`. */ | |
| 96 | export function bare(url: string): string { | |
| 97 | return url.replace(/^https?:\/\//i, "").replace(/\/$/, ""); | |
| 98 | } | |
| 99 | ||
| 100 | /** | |
| 101 | * A project's links in the order pages show them: its homepage, its docs, | |
| 102 | * then the rest. An address already shown elsewhere on the page (such as | |
| 103 | * production's) is given in `shown` and left out, so nothing is listed twice. | |
| 104 | */ | |
| 105 | export function linksToShow(links: ProjectLinks, shown: (string | null | undefined)[] = []): ShownLink[] { | |
| 106 | const seen = new Set(shown.filter((url): url is string => !!url).map((url) => bare(url).toLowerCase())); | |
| 107 | const out: ShownLink[] = []; | |
| 108 | const add = (link: ShownLink) => { | |
| 109 | const key = bare(link.url).toLowerCase(); | |
| 110 | if (seen.has(key)) return; | |
| 111 | seen.add(key); | |
| 112 | out.push(link); | |
| 113 | }; | |
| 114 | if (links.homepage) add({ key: "homepage", type: "homepage", label: bare(links.homepage), url: links.homepage }); | |
| 115 | if (links.docs) add({ key: "docs", type: "docs", label: "Docs", url: links.docs }); | |
| 116 | links.custom.forEach((link, index) => add({ key: `custom:${index}`, type: "custom", label: link.label, url: link.url })); | |
| 117 | return out; | |
| 118 | } | |
| 119 | ||
| 120 | /** | |
| 121 | * The one address a project's card or row shows: production as g1t | |
| 122 | * serves it (`live`, from Deployments), production deployed elsewhere, | |
| 123 | * then its homepage, then its docs. Null when it has none. | |
| 124 | */ | |
| 125 | export function primaryLink(project: Pick<Project, "runs" | "productionUrl" | "links">, live: string | null | undefined): string | null { | |
| 126 | if (project.runs === "g1t" && live) return live; | |
| 127 | if (project.runs === "elsewhere" && project.productionUrl) return project.productionUrl; | |
| 128 | return live ?? project.links?.homepage ?? project.links?.docs ?? null; | |
| 129 | } | |
| 130 | ||
| 131 | /** | |
| 132 | * The change a form on the overview asks for: only the fields it carries. | |
| 133 | * `choice` sets what it is and where it runs; `links` says its rows are the | |
| 134 | * whole list of other links, so a form with every row removed clears them. | |
| 135 | */ | |
| 136 | export function projectChanges(form: Pick<FormData, "get" | "getAll" | "has">): ProjectChanges { | |
| 137 | const changes: ProjectChanges = {}; | |
| 138 | const choice = form.get("choice"); | |
| 139 | if (typeof choice === "string" && choice in KIND_CHOICE_SETS) Object.assign(changes, KIND_CHOICE_SETS[choice as KindChoice]); | |
| 140 | const text = (name: string) => (form.has(name) ? String(form.get(name) ?? "") : undefined); | |
| 141 | const fields = { description: text("description"), productionUrl: text("productionUrl"), homepage: text("homepage"), docsUrl: text("docsUrl") }; | |
| 142 | for (const [key, value] of Object.entries(fields)) if (value !== undefined) (changes as Record<string, unknown>)[key] = value; | |
| 143 | if (form.has("links")) changes.links = linksFromForm(form); | |
| 144 | return changes; | |
| 145 | } | |
| 146 | ||
| 147 | /** A repository's newest tag, by its commit's date: its latest release. */ | |
| 148 | export function latestTag(tags: { name: string; commit: { authoredAt: string } | null }[]): { name: string; at: string | null } | null { | |
| 149 | const dated = tags.filter((tag) => tag.commit).sort((a, b) => b.commit!.authoredAt.localeCompare(a.commit!.authoredAt)); | |
| 150 | const newest = dated[0] ?? tags[0]; | |
| 151 | return newest ? { name: newest.name, at: newest.commit?.authoredAt ?? null } : null; | |
| 152 | } | |
| 153 | ||
| 154 | /** The custom links a form gives, as rows of `linkLabel` and `linkUrl` fields, in order. */ | |
| 155 | export function linksFromForm(form: Pick<FormData, "getAll">): { label: string; url: string }[] { | |
| 156 | const labels = form.getAll("linkLabel").map(String); | |
| 157 | const urls = form.getAll("linkUrl").map(String); | |
| 158 | return urls.map((url, index) => ({ label: labels[index] ?? "", url })).filter((link) => link.label.trim() || link.url.trim()); | |
| 159 | } | |
| 160 | ||
| A project is an app or a library: libraries show their package and how to ship a release, not production | 161 | /** A registry's guide, and the command that publishes a first version there. */ |
| 162 | export type PublishGuide = { label: string; guide: string; start: string | null }; | |
| 163 | ||
| 164 | /** The registries with a guide, for a library whose files do not say which it is. */ | |
| 165 | export const PUBLISH_GUIDES: PublishGuide[] = [ | |
| 166 | { label: "Composer", guide: `${DOCS}/guides/composer/`, start: "git tag v1.0.0 && git push --tags" }, | |
| 167 | { label: "npm", guide: `${DOCS}/guides/npm/`, start: "npm publish" }, | |
| 168 | { label: "Go", guide: `${DOCS}/guides/go/`, start: "git tag v1.0.0 && git push --tags" }, | |
| 169 | { label: "Containers", guide: `${DOCS}/guides/containers/`, start: null }, | |
| 170 | ]; | |
| 171 | ||
| 172 | /** How a library of `ecosystem` publishes its first version; null when its files do not say. */ | |
| 173 | export function publishGuide(ecosystem: ProjectEcosystem | null): PublishGuide | null { | |
| 174 | switch (ecosystem) { | |
| 175 | case "composer": | |
| 176 | return PUBLISH_GUIDES[0]!; | |
| 177 | case "npm": | |
| 178 | return PUBLISH_GUIDES[1]!; | |
| 179 | case "go": | |
| 180 | return PUBLISH_GUIDES[2]!; | |
| 181 | case "cargo": | |
| 182 | return { label: "Cargo", guide: `${DOCS}/guides/packages/`, start: null }; | |
| 183 | case "python": | |
| 184 | return { label: "Python", guide: `${DOCS}/guides/packages/`, start: null }; | |
| 185 | default: | |
| 186 | return null; | |
| 187 | } | |
| 188 | } | |
| 189 | ||
| 190 | /** A package as its own tool names it: `@acme/ui` for npm, the name otherwise. */ | |
| 191 | export function packageName(pkg: Pick<PackageSummary, "ecosystem" | "name" | "workspace">): string { | |
| 192 | return pkg.ecosystem === "npm" ? `@${pkg.workspace}/${pkg.name}` : pkg.name; | |
| 193 | } | |
| 194 | ||
| 195 | /** `Composer · psr/log 3.0.2`, for a project's card. */ | |
| 196 | export function packageLine(pkg: Pick<PackageSummary, "ecosystem" | "name" | "workspace" | "latest">): string { | |
| 197 | return `${REGISTRY[pkg.ecosystem]} · ${packageName(pkg)}${pkg.latest ? ` ${pkg.latest}` : ""}`; | |
| 198 | } | |
| 199 | ||
| 200 | /** The package's page. */ | |
| 201 | export function packagePath(pkg: Pick<PackageSummary, "ecosystem" | "name" | "workspace">): string { | |
| 202 | return `/${pkg.workspace}/-/packages/${pkg.ecosystem}/${pkg.name}`; | |
| 203 | } | |
| 204 | ||
| 205 | /** | |
| 206 | * The packages a library's overview shows, from its repository's: what it | |
| 207 | * publishes for others to install first, a container image last, as an | |
| 208 | * image is how apps ship too. | |
| 209 | */ | |
| 210 | export function libraryPackages<T extends Pick<PackageSummary, "ecosystem" | "versions" | "updated_at">>(list: T[]): T[] { | |
| 211 | return [...list].sort( | |
| 212 | (a, b) => | |
| 213 | Number(a.ecosystem === "container") - Number(b.ecosystem === "container") || | |
| 214 | Number(b.versions > 0) - Number(a.versions > 0) || | |
| 215 | b.updated_at.localeCompare(a.updated_at), | |
| 216 | ); | |
| 217 | } | |
| 218 | ||
| 219 | /** Whether any of the packages has a version: a release is out. */ | |
| 220 | export function hasRelease(list: Pick<PackageSummary, "versions">[]): boolean { | |
| 221 | return list.some((pkg) => pkg.versions > 0); | |
| 222 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.