apiReport
uses tcmts.cli.semver_checks.checkPairs, which uses node:path
What changed in a package's API from one state of it to another, lint by lint.
import type { Codebase } from "#codebase/tcmts/cli/codebase"; import { diffNamespaces, keyPath, stateOf } from "#codebase/tcmts/cli/diff"; import { summaryOf } from "#codebase/tcmts/cli/releases"; import type { ApiReport, PackageState } from "#codebase/tcmts/cli/releases"; import { checkPairs, levelOf } from "#codebase/tcmts/cli/semver_checks"; import type { Finding } from "#codebase/tcmts/cli/semver_checks"; export function apiReport(cb: Codebase, before: PackageState, after: PackageState): ApiReport { const def = (h: string) => cb.store.getDef(h)!; const api = (s: PackageState) => stateOf(s.rows.filter((r) => s.exported.has(r.path))); const irOf = (h: string) => def(h).ir; const d = diffNamespaces(api(before), api(after), irOf); // re-hashed only because something it uses changed, wherever that is (another package, say): judged on the // whole namespace, since the API's slice of it doesn't have the cause const following = new Set( diffNamespaces(stateOf(before.rows), stateOf(after.rows), irOf).propagated.map((c) => keyPath(c.path), ), ); const findings: Finding[] = []; const summaries = new Map<string, string>(); const signatures = new Map<string, string>(); for (const r of d.removed) findings.push({ lint: "definition_removed", path: keyPath(r.path) }); for (const r of d.renamed) findings.push({ lint: "definition_renamed", path: keyPath(r.from), detail: keyPath(r.to) }); for (const a of [...d.added, ...d.aliased]) { findings.push({ lint: "definition_added", path: keyPath(a.path) }); const n = def(a.hash); if (n.kind !== "type") signatures.set(keyPath(a.path), n.signature); const s = summaryOf(n.doc); if (s) summaries.set(keyPath(a.path), s); } for (const [p, sub] of after.exported) { const was = before.exported.get(p); if (was !== undefined && was !== sub) findings.push({ lint: "export_moved", path: p, detail: `${was} → ${sub}` }); } const [binsBefore, binsAfter] = [before.pkg.manifest.bin, after.pkg.manifest.bin]; for (const cmd of Object.keys(binsBefore)) if (!(cmd in binsAfter)) findings.push({ lint: "bin_removed", path: cmd }); for (const cmd of Object.keys(binsAfter)) if (!(cmd in binsBefore)) findings.push({ lint: "bin_added", path: cmd }); const checked = checkPairs( cb, [...d.edited, ...d.propagated].map((c) => ({ path: keyPath(c.path), old: def(c.old), new: def(c.new), })), ); const followed: string[] = []; for (const f of checked) { // a definition whose code didn't change, only what it uses: its API change (if any) is reported, its // "implementation change" is the dependency's if (f.lint === "implementation_changed" && following.has(f.path)) followed.push(f.path); else findings.push(f); } for (const c of d.edited) { const s = summaryOf(def(c.new).doc); if (s) summaries.set(keyPath(c.path), s); } // `a: Point → Point`: the change is Point's; said once, where Point is (unless Point isn't in the API) const reported = new Set(findings.map((f) => f.path.slice(f.path.lastIndexOf(".") + 1))); const echo = (f: Finding) => { const m = /^(?:[\w$]+: )?(.+), whose definition changed$/.exec(f.detail ?? ""); return !!m && [...m[1]!.matchAll(/[A-Za-z_$][\w$]*/g)].some((t) => reported.has(t[0])); }; const kept = findings.filter((f) => !echo(f)); // breaking changes first, then additions, then the rest; by name within each const rank = { major: 0, minor: 1, patch: 2 }; kept.sort( (a, b) => rank[levelOf(a)] - rank[levelOf(b)] || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0), ); return { findings: kept, following: followed, summaries, signatures }; }
uses
- tcmts.cli.codebase.Codebase
- tcmts.cli.diff.diffNamespaces
- tcmts.cli.diff.keyPath
- tcmts.cli.diff.stateOf
- tcmts.cli.releases.ApiReport
- tcmts.cli.releases.PackageState
- tcmts.cli.releases.summaryOf
- tcmts.cli.semver_checks.Finding
- tcmts.cli.semver_checks.checkPairs
- tcmts.cli.semver_checks.levelOf