Fix lockfile dependency removal detection in PackageGraph

The `otherGraph` variable in `listChangedPackages` was incorrectly
created from `thisLockfile` instead of `otherLockfile`, making the
merged dependency graph a duplicate of the current one. This meant
that dependencies only present in the old lockfile were never added
to the graph, so transitive removals could not be detected.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-04-20 16:37:26 +02:00
parent 7192e84cad
commit 357d63949e
3 changed files with 53 additions and 1 deletions
@@ -0,0 +1,5 @@
---
'@backstage/cli-node': patch
---
Fixed a bug in `PackageGraph.listChangedPackages` where removed dependencies were not detected during lockfile analysis. The dependency graph from the previous lockfile was not being merged, causing transitive dependency removals to be missed.
@@ -222,4 +222,51 @@ c-dep@^2:
'origin/master',
);
});
it('detects packages affected by a removed dependency in the lockfile', async () => {
const graph = PackageGraph.fromPackages(testPackages);
mockListChangedFiles.mockResolvedValueOnce(['yarn.lock']);
// The old lockfile (at the ref) has b depending on b-dep
mockReadFileAtRef.mockResolvedValueOnce(`
a@^1:
version: "1.0.0"
b@^1:
version: "1.0.0"
dependencies:
b-dep: ^1
b-dep@^1:
version: "1.0.0"
integrity: sha512-old
c@^1:
version: "1.0.0"
`);
// The current lockfile no longer has b-dep at all
jest.spyOn(Lockfile, 'load').mockResolvedValueOnce(
Lockfile.parse(`
a@^1:
version: "1.0.0"
b@^1:
version: "1.0.0"
c@^1:
version: "1.0.0"
`),
);
await expect(
graph
.listChangedPackages({
ref: 'origin/master',
analyzeLockfile: true,
})
.then(pkgs => pkgs.map(pkg => pkg.name)),
).resolves.toEqual(['b']);
});
});
@@ -393,7 +393,7 @@ export class PackageGraph extends Map<string, PackageGraphNode> {
// Merge the dependency graph from the other lockfile into this one in
// order to be able to detect removals accurately.
{
const otherGraph = thisLockfile.createSimplifiedDependencyGraph();
const otherGraph = otherLockfile.createSimplifiedDependencyGraph();
for (const [name, dependencies] of otherGraph) {
const node = graph.get(name);
if (node) {