ChangesPage

tcmts.registry.pages.ChangesPage · #2okizn5c · term · runs on the server

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

used by