cli-node: add utility for generating package dependency hash

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-10-06 11:39:01 +02:00
parent 80a1f41334
commit fec7278938
4 changed files with 63 additions and 3 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli-node': patch
---
Added new `packageGraph.getDependencyHash(name)` utility.
+2
View File
@@ -92,6 +92,7 @@ export function isMonoRepo(): Promise<boolean>;
export class Lockfile {
createSimplifiedDependencyGraph(): Map<string, Set<string>>;
diff(otherLockfile: Lockfile): LockfileDiff;
getVersions(name: string): string[];
static load(path: string): Promise<Lockfile>;
static parse(content: string): Lockfile;
}
@@ -116,6 +117,7 @@ export class PackageGraph extends Map<string, PackageGraphNode> {
collectFn: (pkg: PackageGraphNode) => Iterable<string> | undefined,
): Set<string>;
static fromPackages(packages: Package[]): PackageGraph;
getDependencyHash(name: string): Promise<string>;
listChangedPackages(options: {
ref: string;
analyzeLockfile?: boolean;
@@ -133,6 +133,14 @@ export class Lockfile {
private readonly data: LockfileData,
) {}
/**
* Returns all versions of a package in the lockfile.
*/
getVersions(name: string): string[] {
const queries = this.packages.get(name);
return queries ? queries.map(q => q.version) : [];
}
/**
* Creates a simplified dependency graph from the lockfile data, where each
* key is a package, and the value is a set of all packages that it depends on
+48 -3
View File
@@ -15,6 +15,7 @@
*/
import path from 'path';
import crypto from 'node:crypto';
import { getPackages, Package } from '@manypkg/get-packages';
import { paths } from '../paths';
import { PackageRole } from '../roles';
@@ -283,6 +284,43 @@ export class PackageGraph extends Map<string, PackageGraphNode> {
return targets;
}
/**
* Generates a sha1 hex hash of the dependency graph for a package.
*/
async getDependencyHash(name: string): Promise<string> {
const pkg = this.get(name);
if (!pkg) {
throw new Error(`Package '${name}' not found`);
}
const lockfile = await this.#getLockfile();
const depGraph = lockfile.createSimplifiedDependencyGraph();
const seen = new Set<string>();
const queue = [name];
while (queue.length > 0) {
const deps = depGraph.get(queue.pop()!);
if (deps) {
for (const dep of deps) {
if (!seen.has(dep)) {
seen.add(dep);
queue.push(dep);
}
}
}
}
const hash = crypto.createHash('sha1');
for (const dep of Array.from(seen).sort()) {
hash.update(dep);
hash.update('\0');
hash.update(lockfile.getVersions(dep).join(' '));
hash.update('\0');
}
return hash.digest('hex');
}
/**
* Lists all packages that have changed since a given git ref.
*
@@ -342,9 +380,7 @@ export class PackageGraph extends Map<string, PackageGraphNode> {
let thisLockfile: Lockfile;
let otherLockfile: Lockfile;
try {
thisLockfile = await Lockfile.load(
paths.resolveTargetRoot('yarn.lock'),
);
thisLockfile = await this.#getLockfile();
otherLockfile = Lockfile.parse(
await GitUtils.readFileAtRef('yarn.lock', options.ref),
);
@@ -410,4 +446,13 @@ export class PackageGraph extends Map<string, PackageGraphNode> {
return result;
}
#lockfilePromise?: Promise<Lockfile>;
#getLockfile(): Promise<Lockfile> {
if (this.#lockfilePromise) {
return this.#lockfilePromise;
}
this.#lockfilePromise = Lockfile.load(paths.resolveTargetRoot('yarn.lock'));
return this.#lockfilePromise;
}
}