upgradeTo

tcmts.cli.upgrade.upgradeTo · #uf2c3e6v · term · runs on the server

uses tcmts.loader.packages.newestVersion, which uses tcmts.loader.packages.registryView, which uses node:child_process

Re-pin the definitions using name (in namespace, or all) to range target (a range, a version, or "latest"), and the packages built against it to ranges that accept it; re-hash them and their dependents.

import type { Codebase } from "#codebase/tcmts/cli/codebase";
import { TOOLS } from "#codebase/tcmts/cli/lint";
import { bold, dim, green, yellow } from "#codebase/tcmts/cli/style";
import { builtAgainst, newestFitting, usersOf } from "#codebase/tcmts/cli/upgrade";
import { shortHash } from "#codebase/tcmts/core/hash";
import { compatRange, satisfies as satisfies_ } from "#codebase/tcmts/core/semver";
import { ensureExternals, newestVersion, parseModule, versionsIn } from "#codebase/tcmts/loader/packages";

export function upgradeTo(cb: Codebase, name: string, target: string, namespace: string | undefined): boolean {
  const version = target === "latest" ? newestVersion(name) : (versionsIn(name, target)[0]?.version ?? (versionsIn(name, compatRange(target))[0]?.version));
  if (!version) throw new Error(`can't find ${name}@${target} (is the registry reachable?)`);
  const range = compatRange(version);
  // the range a package built against `name` needs, to accept the new version
  const peerRanges = new Map<string, string>();
  for (const q of builtAgainst(cb, name)) {
    const fit = newestFitting(q.name, (c) => !c.peers[name] || satisfies_(version, c.peers[name]!));
    if (fit) peerRanges.set(q.name, compatRange(fit));
  }
  const module = (m: string): string => {
    if (!m.startsWith("npm:")) return m;
    const p = parseModule(m);
    if (p.name === name) return `npm:${name}@${range}`;
    const r = peerRanges.get(p.name);
    return r ? `npm:${p.name}@${r}` : m;
  };
  const hashes = new Set<string>();
  for (const n of [name, ...peerRanges.keys()]) {
    for (const [h, r] of usersOf(cb, n, undefined, namespace)) if (module(`npm:${n}@${r}`) !== `npm:${n}@${r}`) hashes.add(h);
  }
  const where = namespace ? ` in ${namespace}` : "";
  // a tool (oxlint, oxfmt, vitest): no definition pins it; the codebase records which range it runs
  if (TOOLS.has(name) && !usersOf(cb, name).size && !namespace) {
    ensureExternals(cb.lay, [{ module: `npm:${name}@${range}`, subpath: "", export: "*" }]);
    cb.store.setSetting(`tool.${name}`, range);
    console.log(`${bold(name)} now runs at ${range} (${cb.store.lockedVersion(name, range)})`);
    return true;
  }
  if (!hashes.size) {
    console.log(dim(`nothing${where} uses ${name} other than at ${range}`));
    return true;
  }
  console.log(`re-pinning ${hashes.size} definition(s)${where} to ${bold(`${name} ${range}`)}${peerRanges.size ? dim(`, with ${[...peerRanges].map(([n, r]) => `${n} ${r}`).join(", ")}`) : ""}`);
  const r = cb.propagate(new Map(), { hashes, module, command: `upgrade ${name}@${range}` });
  for (const p of r.updated) console.log(`  ${green("~")} ${p}`);
  if (r.failed.length) {
    console.log(yellow(`\n${r.failed.length} definition(s) didn't typecheck on ${name} ${range}; they keep their old versions and are in \`tcm todo\`:`));
    for (const h of r.failed.slice(0, 10)) console.log(`  ${cb.displayRef(`#${h}`)} ${dim(shortHash(h))}: ${(cb.store.todos().find((t) => t.hash === h)?.message ?? "").split("\n")[0]}`);
  }
  return !r.failed.length;
}

uses

used by