// Every package.json in a product repo pins each @norskvideo/ctl-* package at // the SAME range. // // A repo is a workspace of packages, and a nested one (tests/, shared/) can pin // a ctl package of its own. When the two ranges differ, the nested package runs // a different copy from the one the root was moved to, and nothing says so: // playout's tests/ pinned the dev-kit ^0.1.57 while the root moved to 0.2, its // guides ran a guide proxy that built the wrong Studio prefix, and first-light // failed every night from 2026-09-07. bump.yml now moves every pin; this keeps // a hand edit (or a new nested package) from splitting them again. import { lstatSync, readdirSync, readFileSync } from "node:fs"; import { join, relative } from "node:path"; const SCOPE = "@norskvideo/ctl-"; /** Directories that hold installed or generated packages, not this repo's own. */ const SKIP = new Set(["node_modules", ".git", "dist", "build", "test-temp", "generated", ".nix"]); function manifests(root: string, dir = root, out: string[] = []): string[] { for (const name of readdirSync(dir)) { if (SKIP.has(name)) continue; const path = join(dir, name); // lstat: never follow a symlink. A repo's own packages are real // directories, and a link (a nix `result`, a workspace link) can lead // anywhere, including out of the repo entirely. let st: ReturnType; try { st = lstatSync(path); } catch { continue; } if (st.isDirectory()) manifests(root, path, out); else if (name === "package.json") out.push(path); } return out; } export function ctlPinProblems(repoRoot: string): string[] { const problems: string[] = []; /** package -> range -> the files pinning it at that range */ const pins = new Map>(); for (const path of manifests(repoRoot).sort()) { const rel = relative(repoRoot, path); let parsed: Record | undefined>; try { parsed = JSON.parse(readFileSync(path, "utf8")); } catch (e) { problems.push(`${rel} is not parseable JSON (${e instanceof Error ? e.message : String(e)}).`); continue; } for (const field of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) { for (const [name, range] of Object.entries(parsed[field] ?? {})) { if (!name.startsWith(SCOPE) || typeof range !== "string" || range.startsWith("workspace:")) continue; const byRange = pins.get(name) ?? new Map(); byRange.set(range, [...(byRange.get(range) ?? []), rel]); pins.set(name, byRange); } } } for (const [name, byRange] of pins) { if (byRange.size < 2) continue; const where = [...byRange].flatMap(([range, files]) => files.map((f) => `${f} ${range}`)).join(", "); problems.push( `${name} is pinned at ${byRange.size} different ranges (${where}). A nested package then runs a different copy from the root's; pin it at one range everywhere (the nightly bump.yml moves every pin).`, ); } return problems; }