g1t/scripts/deploy/lockfiles.mjs
| 1 | // Which units a lockfile change reaches. A change to Cargo.lock or |
| 2 | // package-lock.json touches only the units whose dependency graph includes |
| 3 | // a package that changed, read from the lockfiles themselves (their graph |
| 4 | // is a superset of any one target's, so this errs toward deploying). |
| 5 | |
| 6 | /** Cargo.lock: name -> list of entries { version, source, checksum, deps: [names] }. */ |
| 7 | export function parseCargoLock(text) { |
| 8 | const packages = new Map(); |
| 9 | if (!text) return packages; |
| 10 | for (const block of text.split(/^\[\[package\]\]\s*$/m).slice(1)) { |
| 11 | const field = (key) => new RegExp(`^${key} = "([^"]*)"`, "m").exec(block)?.[1] ?? null; |
| 12 | const depsBlock = /^dependencies = \[([\s\S]*?)\]/m.exec(block)?.[1] ?? ""; |
| 13 | const deps = [...depsBlock.matchAll(/"([^"\s]+)[^"]*"/g)].map((m) => m[1]); |
| 14 | const entry = { version: field("version"), source: field("source"), checksum: field("checksum"), deps }; |
| 15 | const name = field("name"); |
| 16 | if (!packages.has(name)) packages.set(name, []); |
| 17 | packages.get(name).push(entry); |
| 18 | } |
| 19 | return packages; |
| 20 | } |
| 21 | |
| 22 | /** package-lock.json (v2/v3): name -> list of entries; workspace folders keep their path as name. */ |
| 23 | export function parseNpmLock(text) { |
| 24 | const packages = new Map(); |
| 25 | if (!text) return packages; |
| 26 | const lock = JSON.parse(text); |
| 27 | for (const [key, entry] of Object.entries(lock.packages ?? {})) { |
| 28 | const at = key.lastIndexOf("node_modules/"); |
| 29 | const name = at < 0 ? key : key.slice(at + "node_modules/".length); |
| 30 | // A workspace package is a link to its folder's entry. |
| 31 | const deps = entry.link |
| 32 | ? [entry.resolved] |
| 33 | : Object.keys({ ...entry.dependencies, ...entry.devDependencies, ...entry.optionalDependencies, ...entry.peerDependencies }); |
| 34 | const record = { key, version: entry.version ?? null, resolved: entry.resolved ?? null, integrity: entry.integrity ?? null, deps }; |
| 35 | if (!packages.has(name)) packages.set(name, []); |
| 36 | packages.get(name).push(record); |
| 37 | } |
| 38 | return packages; |
| 39 | } |
| 40 | |
| 41 | const signature = (entries) => JSON.stringify((entries ?? []).map((e) => ({ ...e, deps: [...e.deps].sort() })).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)))); |
| 42 | |
| 43 | /** Names whose entries differ between two parsed lockfiles. */ |
| 44 | export function changedNames(before, after) { |
| 45 | const names = new Set(); |
| 46 | for (const name of new Set([...before.keys(), ...after.keys()])) { |
| 47 | if (signature(before.get(name)) !== signature(after.get(name))) names.add(name); |
| 48 | } |
| 49 | return names; |
| 50 | } |
| 51 | |
| 52 | /** Whether any of `names` is reachable from `roots` in a parsed lockfile (roots included). */ |
| 53 | export function reaches(packages, roots, names) { |
| 54 | if (!names.size) return false; |
| 55 | const seen = new Set(); |
| 56 | const stack = [...roots]; |
| 57 | while (stack.length) { |
| 58 | const name = stack.pop(); |
| 59 | if (seen.has(name)) continue; |
| 60 | seen.add(name); |
| 61 | if (names.has(name)) return true; |
| 62 | for (const entry of packages.get(name) ?? []) stack.push(...entry.deps); |
| 63 | } |
| 64 | return false; |
| 65 | } |
| 66 | |
| 67 | /** |
| 68 | * Where a unit starts in each lockfile: its crate (or its image's) in |
| 69 | * Cargo.lock; its folder, and the root's tools (Wrangler bundles every |
| 70 | * TypeScript Worker), in package-lock.json. |
| 71 | */ |
| 72 | export function lockRoots(unit) { |
| 73 | const cargo = [unit.crate, unit.image?.crate].filter(Boolean); |
| 74 | const npm = unit.kind === "rust-worker" ? [] : [unit.path, ""]; |
| 75 | return { cargo, npm }; |
| 76 | } |