testSemverChecks
semver checks: each kind of API change, the lint it trips and the version it calls for
import { codebase } from "#codebase/tcmts/cli/test/e2e/releases"; import assert from "node:assert/strict"; export const testSemverChecks = () => { const a = codebase("lints"); const before: Record<string, string> = { opt: `export const f = (x: number): number => x;`, req: `export const f = (x: number): number => x;`, wide: `export const f = (x: number): string => String(x);`, ret: `export const f = (x: number): number => x;`, union: `export type Shape = "circle" | "square";`, prop: `export interface Options { size: number }`, gone: `export interface Options { size: number; label: string }`, impl: `export const f = (x: number): number => x * 2;`, rename: `export const f = 1;`, }; const after: Record<string, string> = { opt: `export const f = (x: number, y?: number): number => x + (y ?? 0);`, req: `export const f = (x: number, y: number): number => x + y;`, wide: `export const f = (x: number | string): string => String(x);`, ret: `export const f = (x: number): number | undefined => x || undefined;`, union: `export type Shape = "circle" | "square" | "triangle";`, prop: `export interface Options { size: number; label?: string }`, gone: `export interface Options { size: number }`, impl: `export const f = (x: number): number => x + x;`, }; for (const [ns, text] of Object.entries(before)) a.save(`// tcm: namespace ${ns}\nexport const manifest = { name: "${ns}" };\n${text}\n`, "add"); for (const ns of Object.keys(before)) a.tcm("release", `${ns}@1.0.0`); for (const [ns, text] of Object.entries(after)) a.save(`// tcm: namespace ${ns}\n${text}\n`); a.tcm("mv", "rename.f", "rename.g"); const check = (ns: string) => a.tcm("release", "check", ns); assert.match(check("opt"), /1\.1\.0 .*: additions[\s\S]*minor {2}opt\.f takes a new optional parameter: y\n {9}function_optional_parameter_added/); assert.match(check("req"), /2\.0\.0 .*: breaking changes[\s\S]*major {2}req\.f takes a required parameter it didn't: y: number\n {9}function_required_parameter_added/); assert.match(check("wide"), /1\.1\.0[\s\S]*minor {2}wide\.f accepts more for a parameter: x: number → string \| number/); assert.match(check("ret"), /2\.0\.0[\s\S]*major {2}ret\.f can return more kinds of value: number → number \| undefined\n {9}function_return_type_widened: callers may get a value they don't handle/); assert.match(check("union"), /2\.0\.0[\s\S]*major {2}union\.Shape has a new case: "triangle"\n {9}type_union_member_added: code handling every case no longer does/); assert.match(check("prop"), /1\.1\.0[\s\S]*minor {2}prop\.Options has a new optional property: label/); assert.match(check("gone"), /2\.0\.0[\s\S]*major {2}gone\.Options lost a property: label/); assert.match(check("impl"), /1\.0\.1 .*: changes with the same API[\s\S]*patch {2}impl\.f changed, with the same API/); assert.match(check("rename"), /2\.0\.0[\s\S]*major {2}rename\.f renamed: rename\.g\n {9}definition_renamed: code that imports it by its old name no longer compiles/); }