ChangesPage
is an async component
What changed between two versions of a library, as tcm lib upgrade would say it: what broke and what was added,
each with its signature before and after, then what changed with the same API; how many of what the registry has use
each version; and what upgrading does.
import { libraryName, libraryReport } from "#codebase/tcmts/cli/libraries"; import { levelOf, LINTS } from "#codebase/tcmts/cli/semver_checks"; import type { Finding } from "#codebase/tcmts/cli/semver_checks"; import { libraryHref, libraryKey, registryShare, releases, said, shownRelease, usersByNamespace, whoHref, whoIs, } from "#codebase/tcmts/registry"; import { Level, NotHere, RegistryFrame } from "#codebase/tcmts/registry/pages"; import { at, CopyButton, shareNow, Trail } from "#codebase/tcmts/share"; import type { NameRow } from "#codebase/tcmts/store"; export async function ChangesPage({ who: asked, pkg, range }: { who: string; pkg: string; range: string }) { registryShare(); const who = await whoIs(asked); const [from, to] = range.split("..."); if (!who || !from || !to) return <NotHere what="Which two versions? (…/changes/1.1.0...1.2.0)" />; const all = (await releases.find({ did: who.did, package: pkg })).map((r) => r.value); const [a, b] = [all.find((r) => r.version === from), all.find((r) => r.version === to)]; if (!a?.mount || !b?.mount) return <NotHere what={`The registry hasn't mirrored ${pkg} ${from} and ${to}.`} />; const cb = shareNow().cb; const [before, after] = [cb.store.namespaceRows(a.namespace), cb.store.namespaceRows(b.namespace)]; const report = libraryReport(cb, before, after); const findings = report?.findings ?? []; const of = (level: string) => findings.filter((f) => levelOf(f) === level); const [breaking, added, changed] = [of("major"), of("minor"), of("patch")]; const signature = (rows: NameRow[], path: string) => { const row = rows.find((r) => r.path === path); const rec = row && cb.store.getDef(row.hash); return rec ? (rec.kind === "type" ? "type" : cb.store.shownSignature(rec)) : undefined; }; // (what the registry has that uses each: exactly, by hash) const users = await usersByNamespace((await releases.find()).map((r) => r.value).filter(shownRelease)); const using = (namespace: string) => (users.get(namespace) ?? []).filter((d) => libraryKey(d) !== libraryKey(b)).length; const [onFrom, onTo] = [using(a.namespace), using(b.namespace)]; const box = (f: Finding) => { const major = levelOf(f) === "major"; const l = LINTS[f.lint]; const [was, now] = [signature(before, f.path), signature(after, f.path)]; return ( <div className="box" key={`${f.lint} ${f.path}`}> <div className="boxhead"> <span className={`badge ${major ? "b-rm" : "b-new"}`}>{major ? "breaking" : "added"}</span> <b className="mono"> {pkg}.{f.path} </b> <span className="muted"> {l.summary} {f.detail ? `: ${f.detail}` : ""} </span> </div> {(was || now) && ( <div className="diff sigdiff"> {was && was !== now && <div className="d">- {`${f.path}: ${was}`}</div>} {now && <div className="a">+ {`${f.path}: ${now}`}</div>} </div> )} {major && <div className="boxbody muted note">{l.why}</div>} </div> ); }; return ( <RegistryFrame tab="libraries" title={`${pkg} ${from} → ${to}`}> <div className="narrow"> <div className="crumbs"> <Trail items={[ { label: "Libraries", href: at("/libraries") }, { label: said(who), href: whoHref(who) }, { label: pkg, href: libraryHref(who, pkg) }, { label: `${from} → ${to}` }, ]} /> </div> <h2 className="title"> {from} → {to} <Level level={b.facts?.level} /> </h2> <div className="facts"> {breaking.length > 0 && <span>{breaking.length} breaking</span>} {added.length > 0 && <span>{added.length} added or extended</span>} {changed.length > 0 && <span>{changed.length} changed, same API</span>} {(onFrom > 0 || onTo > 0) && ( <span className="right"> {onTo} dependent{onTo === 1 ? "" : "s"} on {to}; {onFrom} still on {from} </span> )} </div> {breaking.map(box)} {added.map(box)} {changed.length > 0 && ( <div className="box"> <div className="boxhead"> <span className="badge b-edit">changed</span> <b className="mono"> {pkg}.{changed[0]!.path} </b> {changed.length > 1 && ( <span className="muted"> {changed .slice(1, 6) .map((f) => `· ${pkg}.${f.path}`) .join(" ")} {changed.length > 6 ? ` and ${changed.length - 6} more` : ""} </span> )} <span className="muted right">same API</span> </div> </div> )} {!findings.length && ( <div className="empty"> {report ? "Its API didn't change." : "One of them isn't a package, so there's no API to compare."} </div> )} <div className="box"> <div className="boxbody muted note"> <div className="copy upgrade"> <code>{`tcm lib upgrade ${libraryName(pkg)}`}</code> <CopyButton text={`tcm lib upgrade ${libraryName(pkg)}`} label="Copy" /> </div> In your codebase, this applies it as one patch: callers that still typecheck follow; the rest go to{" "} <code>tcm todo</code>, on {from}. </div> </div> </div> </RegistryFrame> ); }
uses
- tcmts.cli.libraries.libraryName
- tcmts.cli.libraries.libraryReport
- tcmts.cli.semver_checks.Finding
- tcmts.cli.semver_checks.LINTS
- tcmts.cli.semver_checks.levelOf
- tcmts.registry.libraryHref
- tcmts.registry.libraryKey
- tcmts.registry.pages.Level
- tcmts.registry.pages.NotHere
- tcmts.registry.pages.RegistryFrame
- tcmts.registry.registryShare
- tcmts.registry.releases
- tcmts.registry.said
- tcmts.registry.shownRelease
- tcmts.registry.usersByNamespace
- tcmts.registry.whoHref
- tcmts.registry.whoIs
- tcmts.share.CopyButton
- tcmts.share.Trail
- tcmts.share.at
- tcmts.share.shareNow
- tcmts.store.NameRow