upgradeLibrary

tcmts.cli.libraries.upgradeLibrary · #vjnuhhii · term · runs on the server

uses tcmts.cli.style.dim, which uses node:util

Move a library to another version (what: a release or a namespace hash; default its package's newest release, from its sources, source first if given): its API's changes said first, then, unless check, its names replaced by the new version's in one update, each definition patched to the one replacing it, so code using it follows (what doesn't typecheck against it goes to todo). Refused while code uses something the new version doesn't have.

import { attestationsFor, attesterNames, printAttestations } from "#codebase/tcmts/cli/attest";
import type { Codebase } from "#codebase/tcmts/cli/codebase";
import {
  fetchAnnounced, fetchFromSources, libraryEffects, libraryLabel, libraryNamed, libraryReport, mountedRows, namesNow,
  printEffects, recordAt, upgradeMap, usersOf, within, withRecord,
} from "#codebase/tcmts/cli/libraries";
import type { LibraryEntry } from "#codebase/tcmts/cli/libraries";
import { printDependents, printFindings } from "#codebase/tcmts/cli/main";
import { highest } from "#codebase/tcmts/cli/semver_checks";
import { bold, dim, red, yellow } from "#codebase/tcmts/cli/style";
import { UsageError } from "#codebase/tcmts/cli/ui";

export async function upgradeLibrary(
  cb: Codebase,
  o: { library: string; what?: string | undefined; source?: string | undefined; check?: boolean },
): Promise<void> {
  const lib = libraryNamed(cb.store, o.library);
  const what = o.what ?? lib.release?.slice(0, lib.release.lastIndexOf("@"));
  if (!what)
    throw new UsageError(
      `${lib.path} was added by hash: say what to upgrade it to (<package>@<version>, or #<hash>)`,
    );
  // (one its author announced: the newer release is found among their announcements, by the DID they had then)
  const announced =
    lib.did && !what.startsWith("#")
      ? await fetchAnnounced(cb, lib.did, what, { first: o.source, also: lib.sources })
      : undefined;
  const sources =
    announced?.sources ??
    (o.source ? [o.source, ...lib.sources.filter((s) => s !== o.source)] : lib.sources);
  const { found, from } = announced ?? (await fetchFromSources(cb, sources, what));
  if (found.namespace === lib.namespace)
    return console.log(dim(`${lib.path} is ${libraryLabel(lib)} already`));
  const before = mountedRows(cb.store, lib);
  const after = cb.store.namespaceRows(found.namespace);
  const report = libraryReport(cb, before, after);
  const level = report && highest(report.findings);
  const said = level
    ? {
        major: red("breaking changes"),
        minor: yellow("additions"),
        patch: "changes with the same API",
      }[level]
    : dim("no API changes");
  console.log(
    `${bold(lib.path)}: ${libraryLabel(lib)} → ${bold(libraryLabel(found))}${report ? `: ${said}` : ""}${sources.length > 1 ? dim(` (from ${from})`) : ""}`,
  );
  if (report) printFindings(report);
  printEffects(libraryEffects(cb, after), libraryEffects(cb, before));
  // (what its author, and the people this codebase trusts, say of the version it's moving to)
  printAttestations(
    await attestationsFor(cb, [found.namespace], lib.did),
    attesterNames(cb, {
      ...(lib.did ? { did: lib.did } : {}),
      ...(announced?.handle
        ? { handle: announced.handle }
        : lib.handle
          ? { handle: lib.handle }
          : {}),
    }),
  );
  const { replaced, added, gone } = upgradeMap(before, after);
  const users = usersOf(cb.store, lib, gone);
  if (users.length)
    throw new UsageError(
      `\n${libraryLabel(found)} doesn't have what ${users.slice(0, 5).join(", ")}${users.length > 5 ? ` and ${users.length - 5} more` : ""} use${users.length === 1 ? "s" : ""}: change them not to, or add it beside this one (\`tcm lib add ${announced && found.release ? `@${announced.handle ?? announced.did}/${found.release}` : `${from} ${found.release ?? `#${found.namespace}`}`} --as <name> --at <name>\`)`,
    );
  const counts = dim(`(${replaced.size} replaced, ${added} added, ${gone.size} gone)`);
  if (o.check)
    return console.log(
      `\n${dim("would upgrade")} ${lib.path} ${counts}; ${dim("nothing changed (--check)")}`,
    );
  const entry: LibraryEntry = {
    ...found,
    sources,
    ...(lib.did ? { did: lib.did } : {}),
    ...((announced ? announced.handle : lib.handle)
      ? { handle: (announced ? announced.handle : lib.handle)! }
      : {}),
  };
  const entries = {
    ...recordAt(cb.store, lib.record),
    [lib.path.slice(lib.path.lastIndexOf(".") + 1)]: entry,
  };
  const kept = namesNow(cb.store).filter((n) => !within(n.path, lib.path));
  const rows = after.map((r) => ({ ...r, path: `${lib.path}.${r.path}` }));
  cb.store.setBranchNames(
    cb.store.branch().name,
    [...withRecord(cb, kept, lib.record, entries), ...rows],
    `lib upgrade ${lib.path}`,
  );
  for (const [old, now] of replaced) cb.store.addPatch(old, now);
  console.log(`\nupgraded ${bold(lib.path)} to ${libraryLabel(found)} ${counts}`);
  const { updated, failed } = cb.propagate(replaced);
  printDependents(updated);
  if (failed.length)
    console.log(
      red(`\n  ${failed.length} dependent definition(s) no longer typecheck; see \`tcm todo\``),
    );
  cb.refreshShim();
}

uses

used by