From dcd16c7b36cf95dc2abb9b09a81d66e87cd3fe97 Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Thu, 26 Feb 2026 02:00:45 +0100 Subject: [PATCH] fix: pnpm why -r --parseable (#10595) close #8100 --- .changeset/fix-why-parseable-dedup.md | 6 + reviewing/list/src/renderParseable.ts | 27 +++-- reviewing/list/test/index.ts | 151 ++++++++++++++++++++++++++ 3 files changed, 170 insertions(+), 14 deletions(-) create mode 100644 .changeset/fix-why-parseable-dedup.md diff --git a/.changeset/fix-why-parseable-dedup.md b/.changeset/fix-why-parseable-dedup.md new file mode 100644 index 0000000000..f406d534bb --- /dev/null +++ b/.changeset/fix-why-parseable-dedup.md @@ -0,0 +1,6 @@ +--- +"@pnpm/list": patch +"pnpm": patch +--- + +Fix `pnpm why -r --parseable` missing dependents when multiple workspace packages share the same dependency [#8100](https://github.com/pnpm/pnpm/issues/8100). diff --git a/reviewing/list/src/renderParseable.ts b/reviewing/list/src/renderParseable.ts index 48bd94c75c..5ff98d6330 100644 --- a/reviewing/list/src/renderParseable.ts +++ b/reviewing/list/src/renderParseable.ts @@ -30,18 +30,17 @@ function renderParseableForPackage ( }, pkg: PackageDependencyHierarchy ): string { - const pkgs = sortPackages( - flatten( - depPaths, - [ - ...(pkg.optionalDependencies ?? []), - ...(pkg.dependencies ?? []), - ...(pkg.devDependencies ?? []), - ...(pkg.unsavedDependencies ?? []), - ] - ) - ) - if (!opts.alwaysPrintRootPackage && (pkgs.length === 0)) return '' + const rootAlreadySeen = depPaths.has(pkg.path) + depPaths.add(pkg.path) + const allDeps = [ + ...(pkg.optionalDependencies ?? []), + ...(pkg.dependencies ?? []), + ...(pkg.devDependencies ?? []), + ...(pkg.unsavedDependencies ?? []), + ] + const pkgs = sortPackages(flatten(depPaths, allDeps)) + if (rootAlreadySeen && pkgs.length === 0) return '' + if (!opts.alwaysPrintRootPackage && pkgs.length === 0 && allDeps.length === 0) return '' if (opts.long) { let firstLine = pkg.path if (pkg.name) { @@ -54,7 +53,7 @@ function renderParseableForPackage ( } } return [ - firstLine, + ...(rootAlreadySeen ? [] : [firstLine]), ...pkgs.map((pkgNode) => { const node = pkgNode as DependencyNode if (node.alias !== node.name) { @@ -73,7 +72,7 @@ function renderParseableForPackage ( ].join('\n') } return [ - pkg.path, + ...(rootAlreadySeen ? [] : [pkg.path]), ...pkgs.map((pkg) => pkg.path), ].join('\n') } diff --git a/reviewing/list/test/index.ts b/reviewing/list/test/index.ts index 2a3a7a9988..36ac552278 100644 --- a/reviewing/list/test/index.ts +++ b/reviewing/list/test/index.ts @@ -971,3 +971,154 @@ test('renderParseable displays file: protocol correctly for aliased packages', a expect(output).toContain('my-alias my-local-pkg@file:local-pkg') }) + +test('renderParseable search: shared dep across packages is not duplicated', async () => { + const output = await renderParseable( + [ + { + name: 'pkg-a', + path: '/workspace/packages/pkg-a', + version: '1.0.0', + dependencies: [ + { + alias: '@org/shared', + name: '@org/shared', + version: '1.0.0', + path: '/workspace/packages/shared', + isMissing: false, + isPeer: false, + isSkipped: false, + }, + ], + }, + { + name: 'pkg-b', + path: '/workspace/packages/pkg-b', + version: '1.0.0', + dependencies: [ + { + alias: '@org/shared', + name: '@org/shared', + version: '1.0.0', + path: '/workspace/packages/shared', + isMissing: false, + isPeer: false, + isSkipped: false, + }, + ], + }, + { + name: '@org/shared', + path: '/workspace/packages/shared', + version: '1.0.0', + }, + ], + { + alwaysPrintRootPackage: false, + depth: 0, + long: false, + search: true, + } + ) + + const lines = output.split('\n') + expect(lines).toContain('/workspace/packages/pkg-a') + expect(lines).toContain('/workspace/packages/pkg-b') + expect(lines).toContain('/workspace/packages/shared') + expect(lines.filter((l) => l === '/workspace/packages/shared')).toHaveLength(1) +}) + +test('renderParseable search: packages unrelated to search are excluded', async () => { + const output = await renderParseable( + [ + { + name: 'root', + path: '/workspace', + version: '1.0.0', + }, + { + name: 'pkg-a', + path: '/workspace/packages/pkg-a', + version: '1.0.0', + dependencies: [ + { + alias: '@org/shared', + name: '@org/shared', + version: '1.0.0', + path: '/workspace/packages/shared', + isMissing: false, + isPeer: false, + isSkipped: false, + }, + ], + }, + { + name: 'unrelated', + path: '/workspace/packages/unrelated', + version: '1.0.0', + }, + ], + { + alwaysPrintRootPackage: false, + depth: 0, + long: false, + search: true, + } + ) + + const lines = output.split('\n') + expect(lines).toContain('/workspace/packages/pkg-a') + expect(lines).toContain('/workspace/packages/shared') + expect(lines).not.toContain('/workspace') + expect(lines).not.toContain('/workspace/packages/unrelated') +}) + +test('renderParseable search long: shared dep across packages is not duplicated', async () => { + const output = await renderParseable( + [ + { + name: 'pkg-a', + path: '/workspace/packages/pkg-a', + version: '1.0.0', + dependencies: [ + { + alias: '@org/shared', + name: '@org/shared', + version: 'link:../shared', + path: '/workspace/packages/shared', + isMissing: false, + isPeer: false, + isSkipped: false, + }, + ], + }, + { + name: 'pkg-b', + path: '/workspace/packages/pkg-b', + version: '1.0.0', + dependencies: [ + { + alias: '@org/shared', + name: '@org/shared', + version: 'link:../shared', + path: '/workspace/packages/shared', + isMissing: false, + isPeer: false, + isSkipped: false, + }, + ], + }, + ], + { + alwaysPrintRootPackage: false, + depth: 0, + long: true, + search: true, + } + ) + + const lines = output.split('\n') + expect(lines).toContain('/workspace/packages/pkg-a:pkg-a@1.0.0') + expect(lines).toContain('/workspace/packages/pkg-b:pkg-b@1.0.0') + expect(lines.filter((l) => l.startsWith('/workspace/packages/shared'))).toHaveLength(1) +})