diff --git a/.changeset/happy-lemons-smile.md b/.changeset/happy-lemons-smile.md new file mode 100644 index 0000000000..d3c7efa382 --- /dev/null +++ b/.changeset/happy-lemons-smile.md @@ -0,0 +1,6 @@ +--- +"@pnpm/list": patch +"pnpm": patch +--- + +Fix memory error in `pnpm why` when the dependencies tree is too big, the command will now prune the tree to just 10 end leafs and now supports `--depth` argument. diff --git a/reviewing/list/src/index.ts b/reviewing/list/src/index.ts index e18cd86a79..3ff9273052 100644 --- a/reviewing/list/src/index.ts +++ b/reviewing/list/src/index.ts @@ -6,6 +6,7 @@ import { renderJson } from './renderJson' import { renderParseable } from './renderParseable' import { renderTree } from './renderTree' import { type PackageDependencyHierarchy } from './types' +import { pruneDependenciesTrees } from './pruneTree' export type { PackageNode } from '@pnpm/reviewing.dependencies-hierarchy' export { renderJson, renderParseable, renderTree } @@ -105,8 +106,10 @@ export async function listForPackages ( const pkgs = await searchForPackages(packages, projectPaths, opts) + const prunedPkgs = pruneDependenciesTrees(pkgs ?? null, 10) + const print = getPrinter(opts.reportAs) - return print(pkgs, { + return print(prunedPkgs, { alwaysPrintRootPackage: opts.alwaysPrintRootPackage, depth: opts.depth, long: opts.long, diff --git a/reviewing/list/src/pruneTree.ts b/reviewing/list/src/pruneTree.ts new file mode 100644 index 0000000000..daae47809c --- /dev/null +++ b/reviewing/list/src/pruneTree.ts @@ -0,0 +1,78 @@ +import { type DependenciesHierarchy, type PackageNode } from '@pnpm/reviewing.dependencies-hierarchy' +import { type PackageDependencyHierarchy } from './types' +import { createHash } from 'crypto' + +export function pruneDependenciesTrees (trees: PackageDependencyHierarchy[] | null, limit: number): PackageDependencyHierarchy[] { + if (trees === null) { + return [] + } + + return trees.map((tree) => { + const endLeafPaths: PackageNode[][] = [] + const visitedNodes = new Set() + + function findEndLeaves (node: PackageNode, path: PackageNode[]): void { + if (node.circular) { + return + } + + const nodeId = `${node.name}@${node.version}` + if (visitedNodes.has(nodeId)) { + return + } + + visitedNodes.add(nodeId) + const newPath = [...path, node] + + if (!node.dependencies || node.dependencies.length === 0) { + endLeafPaths.push(newPath) + if (endLeafPaths.length >= limit) { + return + } + } + + for (const child of node.dependencies ?? []) { + findEndLeaves(child, newPath) + if (endLeafPaths.length >= limit) { + return + } + } + + visitedNodes.delete(nodeId) + } + + if (tree.dependencies) { + for (const node of tree.dependencies) { + findEndLeaves(node, []) + } + } + + const firstNPaths = endLeafPaths.slice(0, limit) + const map = new Map() + const newTree: DependenciesHierarchy = { dependencies: [] } + + for (const path of firstNPaths) { + let currentDependencies: PackageNode[] = newTree.dependencies! + let pathSoFar = '' + + for (const node of path) { + pathSoFar += `${node.name}@${node.version},` + const id = createHash('sha256').update(pathSoFar).digest('hex') + let existingNode = map.get(id) + + if (!existingNode) { + existingNode = { ...node, dependencies: [] } + currentDependencies.push(existingNode) + map.set(id, existingNode) + } + + currentDependencies = existingNode.dependencies! + } + } + + return { + ...tree, + dependencies: newTree.dependencies, + } + }) +} diff --git a/reviewing/plugin-commands-listing/src/why.ts b/reviewing/plugin-commands-listing/src/why.ts index 9bda01cc4e..74c17f6dc9 100644 --- a/reviewing/plugin-commands-listing/src/why.ts +++ b/reviewing/plugin-commands-listing/src/why.ts @@ -8,6 +8,7 @@ import { handler as list, type ListCommandOptions } from './list' export function rcOptionsTypes () { return pick([ + 'depth', 'dev', 'global-dir', 'global', @@ -79,6 +80,10 @@ For options that may be used with `-r`, see "pnpm help recursive"', description: "Don't display packages from `optionalDependencies`", name: '--no-optional', }, + { + name: '--depth ', + description: 'Max display depth of the dependency graph', + }, OPTIONS.globalDir, ...UNIVERSAL_OPTIONS, ], @@ -103,7 +108,7 @@ export async function handler ( ...opts, cliOptions: { ...(opts.cliOptions ?? {}), - depth: Infinity, + depth: opts.depth ?? Infinity, }, }, params) }