diff --git a/.changeset/calm-owls-move.md b/.changeset/calm-owls-move.md new file mode 100644 index 0000000000..a4dcaeeff8 --- /dev/null +++ b/.changeset/calm-owls-move.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-node': patch +--- + +Added new `packageGraph.getDependencyHash(name)` utility. diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index 97dab9684d..6c2294012d 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -92,6 +92,7 @@ export function isMonoRepo(): Promise; export class Lockfile { createSimplifiedDependencyGraph(): Map>; diff(otherLockfile: Lockfile): LockfileDiff; + getVersions(name: string): string[]; static load(path: string): Promise; static parse(content: string): Lockfile; } @@ -116,6 +117,7 @@ export class PackageGraph extends Map { collectFn: (pkg: PackageGraphNode) => Iterable | undefined, ): Set; static fromPackages(packages: Package[]): PackageGraph; + getDependencyHash(name: string): Promise; listChangedPackages(options: { ref: string; analyzeLockfile?: boolean; diff --git a/packages/cli-node/src/monorepo/Lockfile.ts b/packages/cli-node/src/monorepo/Lockfile.ts index 8a481c77ad..d7336cf970 100644 --- a/packages/cli-node/src/monorepo/Lockfile.ts +++ b/packages/cli-node/src/monorepo/Lockfile.ts @@ -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 diff --git a/packages/cli-node/src/monorepo/PackageGraph.ts b/packages/cli-node/src/monorepo/PackageGraph.ts index 69ec99c2dd..91c5312bc1 100644 --- a/packages/cli-node/src/monorepo/PackageGraph.ts +++ b/packages/cli-node/src/monorepo/PackageGraph.ts @@ -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 { return targets; } + /** + * Generates a sha1 hex hash of the dependency graph for a package. + */ + async getDependencyHash(name: string): Promise { + 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(); + 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 { 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 { return result; } + + #lockfilePromise?: Promise; + #getLockfile(): Promise { + if (this.#lockfilePromise) { + return this.#lockfilePromise; + } + this.#lockfilePromise = Lockfile.load(paths.resolveTargetRoot('yarn.lock')); + return this.#lockfilePromise; + } }