pr_01m47d15m3e54sn21z27rpy5n9/services/deployments/src/names.ts
| 1 | /** |
| 2 | * Where an app is served, the way Vercel names them: production at |
| 3 | * `<project>-<workspace>.g1t.page`, and a branch's preview at |
| 4 | * `<project>-git-<branch>-<workspace>.g1t.page`. The first label is also |
| 5 | * the app's script name in the dispatch namespace, so the dispatcher needs |
| 6 | * nothing but the hostname to find it. The service makes sure no two apps |
| 7 | * get the same name; this only spells them. |
| 8 | */ |
| 9 | |
| 10 | import { DEPLOYMENTS_DOMAIN } from "@g1t/contracts"; |
| 11 | |
| 12 | /** A hostname label is at most this long. */ |
| 13 | const MAX_LABEL = 63; |
| 14 | |
| 15 | /** Lowercase letters, digits and single hyphens, as a label allows. */ |
| 16 | export function clean(text: string): string { |
| 17 | return text |
| 18 | .toLowerCase() |
| 19 | .replace(/[^a-z0-9]+/g, "-") |
| 20 | .replace(/-{2,}/g, "-") |
| 21 | .replace(/^-|-$/g, ""); |
| 22 | } |
| 23 | |
| 24 | /** A short, stable fingerprint of `text`. */ |
| 25 | export async function fingerprint(text: string, bytes = 3): Promise<string> { |
| 26 | const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)); |
| 27 | return [...new Uint8Array(digest).slice(0, bytes)].map((b) => b.toString(16).padStart(2, "0")).join(""); |
| 28 | } |
| 29 | |
| 30 | /** |
| 31 | * The label for production (`branch` null) or a branch's preview. Too long |
| 32 | * for a label, the branch and then the project are shortened, and a |
| 33 | * fingerprint of the whole keeps it unique. |
| 34 | */ |
| 35 | export async function label(workspace: string, project: string, branch: string | null): Promise<string> { |
| 36 | const w = clean(workspace); |
| 37 | const p = clean(project); |
| 38 | const b = branch == null ? null : clean(branch); |
| 39 | const full = b == null ? `${p}-${w}` : `${p}-git-${b}-${w}`; |
| 40 | if (full.length <= MAX_LABEL) return full; |
| 41 | const tail = `-${await fingerprint(`${workspace}/${project}/${branch ?? ""}`)}-${w}`.slice(0, 40); |
| 42 | const room = MAX_LABEL - tail.length; |
| 43 | const head = b == null ? p : `${p.slice(0, Math.max(8, Math.floor(room / 2)))}-git-${b}`; |
| 44 | return `${head.slice(0, room).replace(/-+$/, "")}${tail}`; |
| 45 | } |
| 46 | |
| 47 | /** The same label made unique, when another app already holds it. */ |
| 48 | export async function uniqueLabel(base: string, key: string): Promise<string> { |
| 49 | const suffix = `-${await fingerprint(key, 2)}`; |
| 50 | return `${base.slice(0, MAX_LABEL - suffix.length).replace(/-+$/, "")}${suffix}`; |
| 51 | } |
| 52 | |
| 53 | export function appHost(script: string): string { |
| 54 | return `${script}.${DEPLOYMENTS_DOMAIN}`; |
| 55 | } |
| 56 | |
| 57 | export function appUrl(script: string): string { |
| 58 | return `https://${appHost(script)}`; |
| 59 | } |