flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/scripts/deploy/lockfiles.mjs

76 lines3,336 bytesCodeBlame
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] }. */
7export 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. */
23export 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
41const 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. */
44export 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). */
53export 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 */
72export 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}