@pnpm/deps.inspection.tree-builder 1001.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/getTree.js ADDED
@@ -0,0 +1,220 @@
1
+ import path from 'node:path';
2
+ import { lexCompare } from '@pnpm/util.lex-comparator';
3
+ import { getPkgInfo } from './getPkgInfo.js';
4
+ import { peersSuffixHashFromDepPath } from './peersSuffixHash.js';
5
+ import { serializeTreeNodeId } from './TreeNodeId.js';
6
+ export function getTree(opts, parentId) {
7
+ const ancestors = new Set();
8
+ ancestors.add(serializeTreeNodeId(parentId));
9
+ const ctx = {
10
+ ...opts,
11
+ ancestors,
12
+ };
13
+ const result = materializeChildren(ctx, parentId, opts.maxDepth, opts.parentDir);
14
+ // Mark circular back-edges. materializeChildren truncates dependencies
15
+ // at cycle boundaries but does not set the `circular` flag, so that cached
16
+ // subtrees stay context-independent. fixCircularRefs walks the final tree
17
+ // and adds `circular: true` wherever a node's path matches an ancestor.
18
+ //
19
+ // Seed the ancestors with parentDir (the filesystem path of parentId) so
20
+ // that back-edges to the root of this subtree are detected — the root
21
+ // itself does not appear as a node in the tree, only its children do.
22
+ const circularAncestors = new Set();
23
+ if (opts.parentDir) {
24
+ circularAncestors.add(opts.parentDir);
25
+ }
26
+ return fixCircularRefs(result.nodes, circularAncestors);
27
+ }
28
+ // ---------------------------------------------------------------------------
29
+ // Materialize DependencyNode[] tree from the graph
30
+ // ---------------------------------------------------------------------------
31
+ function materializeCacheKey(nodeId, depth) {
32
+ if (depth === Infinity)
33
+ return nodeId;
34
+ return `${nodeId}@d${depth}`;
35
+ }
36
+ /**
37
+ * Core materialization function. Walks the pre-built dependency graph to
38
+ * produce the `DependencyNode[]` tree that downstream renderers expect.
39
+ *
40
+ * The cache is keyed by `(nodeId, remainingDepth)` and stores the
41
+ * `DependencyNode[]` children of a given node. It is populated
42
+ * unconditionally, including results where recursion was truncated at a
43
+ * cycle boundary. Cycle detection uses a mutable `ancestors` Set to
44
+ * stop recursion but does NOT set the `circular` flag — that is handled
45
+ * by `fixCircularRefs` in a separate pass over the final tree. This
46
+ * keeps cached subtrees free of context-dependent circular markers.
47
+ */
48
+ function materializeChildren(ctx, parentId, maxDepth, parentDir) {
49
+ if (maxDepth <= 0)
50
+ return { nodes: [], count: 0, hasSearchMatch: false, searchMessages: [] };
51
+ const parentSerialized = serializeTreeNodeId(parentId);
52
+ const graphNode = ctx.graph.nodes.get(parentSerialized);
53
+ if (!graphNode) {
54
+ throw new Error(`Node ${parentSerialized} not found in the dependency graph`);
55
+ }
56
+ const childTreeMaxDepth = maxDepth - 1;
57
+ const linkedPathBaseDir = parentId.type === 'importer'
58
+ ? path.join(ctx.lockfileDir, parentId.importerId)
59
+ : ctx.lockfileDir;
60
+ const resultDependencies = [];
61
+ let resultCount = 0;
62
+ let resultHasSearchMatch = false;
63
+ const resultSearchMessages = ctx.showDedupedSearchMatches ? [] : undefined;
64
+ // Sort edges by alias so that deduplication is deterministic:
65
+ // the alphabetically-first dependency always gets fully expanded.
66
+ const sortedEdges = [...graphNode.edges].sort((a, b) => lexCompare(a.alias, b.alias));
67
+ for (const edge of sortedEdges) {
68
+ if (ctx.onlyProjects && edge.target?.nodeId.type !== 'importer') {
69
+ continue;
70
+ }
71
+ const { pkgInfo: packageInfo, readManifest } = getPkgInfo({
72
+ ...ctx,
73
+ alias: edge.alias,
74
+ ref: edge.ref,
75
+ peers: graphNode.peers,
76
+ linkedPathBaseDir,
77
+ parentDir,
78
+ });
79
+ const searchMatch = ctx.search?.({
80
+ alias: edge.alias,
81
+ name: packageInfo.name,
82
+ version: packageInfo.version,
83
+ readManifest,
84
+ });
85
+ let newEntry = null;
86
+ let childCount = 0;
87
+ let dedupedHasSearchMatch = false;
88
+ let dedupedSearchMessages = [];
89
+ if (edge.target == null) {
90
+ // External link or unresolvable — no traversal possible
91
+ if (ctx.search == null || searchMatch) {
92
+ newEntry = packageInfo;
93
+ }
94
+ else {
95
+ continue;
96
+ }
97
+ }
98
+ else {
99
+ let dependencies;
100
+ let childHasSearchMatch = false;
101
+ let childSearchMessages = [];
102
+ let dedupedCount;
103
+ const circular = ctx.ancestors.has(edge.target.id);
104
+ if (circular) {
105
+ dependencies = [];
106
+ }
107
+ else {
108
+ const cacheKey = materializeCacheKey(edge.target.id, childTreeMaxDepth);
109
+ const cached = ctx.materializationCache.get(cacheKey);
110
+ if (cached !== undefined) {
111
+ // This subtree was already returned to a parent elsewhere in
112
+ // the output tree — elide it to avoid repeating the same nodes.
113
+ dependencies = [];
114
+ if (cached.count > 0) {
115
+ dedupedCount = cached.count;
116
+ }
117
+ if (ctx.showDedupedSearchMatches) {
118
+ dedupedHasSearchMatch = cached.hasSearchMatch;
119
+ dedupedSearchMessages = cached.searchMessages;
120
+ }
121
+ }
122
+ else {
123
+ ctx.ancestors.add(edge.target.id);
124
+ const childResult = materializeChildren(ctx, edge.target.nodeId, childTreeMaxDepth, packageInfo.path);
125
+ ctx.ancestors.delete(edge.target.id);
126
+ dependencies = childResult.nodes;
127
+ childCount = childResult.count;
128
+ childHasSearchMatch = childResult.hasSearchMatch;
129
+ childSearchMessages = childResult.searchMessages;
130
+ // Always cache — even results with circular truncations.
131
+ ctx.materializationCache.set(cacheKey, {
132
+ count: childCount,
133
+ hasSearchMatch: childHasSearchMatch,
134
+ searchMessages: childSearchMessages,
135
+ });
136
+ }
137
+ if (childHasSearchMatch || dedupedHasSearchMatch) {
138
+ resultHasSearchMatch = true;
139
+ }
140
+ resultSearchMessages?.push(...childSearchMessages, ...dedupedSearchMessages);
141
+ }
142
+ if (dependencies.length > 0) {
143
+ newEntry = {
144
+ ...packageInfo,
145
+ dependencies,
146
+ };
147
+ }
148
+ else if (ctx.search == null || searchMatch || dedupedHasSearchMatch) {
149
+ newEntry = packageInfo;
150
+ }
151
+ else {
152
+ continue;
153
+ }
154
+ if (dedupedCount != null) {
155
+ newEntry.deduped = true;
156
+ newEntry.dedupedDependenciesCount = dedupedCount;
157
+ }
158
+ if (edge.target.nodeId.type === 'package') {
159
+ const peerHash = peersSuffixHashFromDepPath(edge.target.nodeId.depPath);
160
+ if (peerHash != null) {
161
+ newEntry.peersSuffixHash = peerHash;
162
+ }
163
+ }
164
+ }
165
+ if (searchMatch) {
166
+ newEntry.searched = true;
167
+ resultHasSearchMatch = true;
168
+ if (typeof searchMatch === 'string') {
169
+ newEntry.searchMessage = searchMatch;
170
+ resultSearchMessages?.push(searchMatch);
171
+ }
172
+ }
173
+ else if (dedupedHasSearchMatch) {
174
+ newEntry.searched = true;
175
+ if (dedupedSearchMessages.length > 0) {
176
+ newEntry.searchMessage = dedupedSearchMessages.join('\n');
177
+ }
178
+ }
179
+ if (!newEntry.isPeer || !ctx.excludePeerDependencies || newEntry.dependencies?.length) {
180
+ resultDependencies.push(newEntry);
181
+ resultCount += 1 + (newEntry.dependencies?.length ? childCount : 0);
182
+ }
183
+ }
184
+ return {
185
+ count: resultCount,
186
+ hasSearchMatch: resultHasSearchMatch,
187
+ nodes: resultDependencies,
188
+ searchMessages: resultSearchMessages ?? [],
189
+ };
190
+ }
191
+ /**
192
+ * Walks the materialized DependencyNode[] tree and marks circular back-edges.
193
+ * A node whose `path` matches an ancestor is a cycle — it gets
194
+ * `circular: true` and its dependencies (if any) are stripped.
195
+ *
196
+ * With deduplication in place (deduped nodes are leaves), the walk is O(N).
197
+ */
198
+ function fixCircularRefs(nodes, ancestors) {
199
+ let changed = false;
200
+ const result = nodes.map(node => {
201
+ // A node whose path matches an ancestor is a circular back-edge.
202
+ if (node.path && ancestors.has(node.path)) {
203
+ changed = true;
204
+ const { dependencies: _, deduped: _d, dedupedDependenciesCount: _c, ...rest } = node;
205
+ return { ...rest, circular: true };
206
+ }
207
+ if (!node.dependencies?.length)
208
+ return node;
209
+ ancestors.add(node.path);
210
+ const fixedDeps = fixCircularRefs(node.dependencies, ancestors);
211
+ ancestors.delete(node.path);
212
+ if (fixedDeps !== node.dependencies) {
213
+ changed = true;
214
+ return { ...node, dependencies: fixedDeps };
215
+ }
216
+ return node;
217
+ });
218
+ return changed ? result : nodes;
219
+ }
220
+ //# sourceMappingURL=getTree.js.map
@@ -0,0 +1,12 @@
1
+ import { type ProjectSnapshot } from '@pnpm/lockfile.fs';
2
+ import type { TreeNodeId } from './TreeNodeId.js';
3
+ export interface GetTreeNodeChildIdOpts {
4
+ readonly parentId: TreeNodeId;
5
+ readonly dep: {
6
+ readonly alias: string;
7
+ readonly ref: string;
8
+ };
9
+ readonly lockfileDir: string;
10
+ readonly importers: Record<string, ProjectSnapshot>;
11
+ }
12
+ export declare function getTreeNodeChildId(opts: GetTreeNodeChildIdOpts): TreeNodeId | undefined;
@@ -0,0 +1,36 @@
1
+ import path from 'node:path';
2
+ import { refToRelative } from '@pnpm/deps.path';
3
+ import { getLockfileImporterId } from '@pnpm/lockfile.fs';
4
+ export function getTreeNodeChildId(opts) {
5
+ const depPath = refToRelative(opts.dep.ref, opts.dep.alias);
6
+ if (depPath !== null) {
7
+ return { type: 'package', depPath };
8
+ }
9
+ switch (opts.parentId.type) {
10
+ case 'importer': {
11
+ // This should be a link given depPath is null.
12
+ //
13
+ // TODO: Consider updating refToRelative (or writing a new function) to
14
+ // return an enum so there's no implicit assumptions.
15
+ const linkValue = opts.dep.ref.slice('link:'.length);
16
+ // It's a bit roundabout to prepend the lockfile dir only to remove it
17
+ // through getLockfileImporterId, but we can be more certain the right
18
+ // importerId is created by reusing the getLockfileImporterId function.
19
+ const absoluteLinkedPath = path.join(opts.lockfileDir, opts.parentId.importerId, linkValue);
20
+ const childImporterId = getLockfileImporterId(opts.lockfileDir, absoluteLinkedPath);
21
+ // A 'link:' reference may refer to a package outside of the pnpm workspace.
22
+ // Return undefined in that case since it would be difficult to list/traverse
23
+ // that package outside of the pnpm workspace.
24
+ const isLinkOutsideWorkspace = opts.importers[childImporterId] == null;
25
+ return isLinkOutsideWorkspace
26
+ ? undefined
27
+ : { type: 'importer', importerId: childImporterId };
28
+ }
29
+ case 'package':
30
+ // In theory an external package could be overridden to link to a
31
+ // dependency in the pnpm workspace. Avoid traversing through this
32
+ // edge case for now.
33
+ return undefined;
34
+ }
35
+ }
36
+ //# sourceMappingURL=getTreeNodeChildId.js.map
package/lib/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { buildDependenciesTree, type DependenciesTree } from './buildDependenciesTree.js';
2
+ export { buildDependentsTree, type DependentNode, type DependentsTree, type ImporterInfo } from './buildDependentsTree.js';
3
+ export { createPackagesSearcher } from './createPackagesSearcher.js';
4
+ export { type DependencyNode } from './DependencyNode.js';
package/lib/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { buildDependenciesTree } from './buildDependenciesTree.js';
2
+ export { buildDependentsTree } from './buildDependentsTree.js';
3
+ export { createPackagesSearcher } from './createPackagesSearcher.js';
4
+ export {} from './DependencyNode.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ export declare function peersSuffixHashFromDepPath(depPath: string): string | undefined;
@@ -0,0 +1,9 @@
1
+ import crypto from 'node:crypto';
2
+ import { parseDepPath } from '@pnpm/deps.path';
3
+ export function peersSuffixHashFromDepPath(depPath) {
4
+ const { peerDepGraphHash } = parseDepPath(depPath);
5
+ if (!peerDepGraphHash)
6
+ return undefined;
7
+ return crypto.createHash('sha256').update(peerDepGraphHash).digest('hex').slice(0, 4);
8
+ }
9
+ //# sourceMappingURL=peersSuffixHash.js.map
@@ -0,0 +1,11 @@
1
+ import { type StoreIndex } from '@pnpm/store.index';
2
+ import type { DependencyManifest } from '@pnpm/types';
3
+ /**
4
+ * Attempts to read a package manifest from the content-addressable store (CAFS)
5
+ * using its integrity hash. Returns `undefined` if the manifest cannot be read.
6
+ */
7
+ export declare function readManifestFromCafs(storeDir: string, storeIndex: StoreIndex, pkg: {
8
+ integrity: string;
9
+ name: string;
10
+ version: string;
11
+ }): DependencyManifest | undefined;
@@ -0,0 +1,26 @@
1
+ import { getFilePathByModeInCafs } from '@pnpm/store.cafs';
2
+ import { storeIndexKey } from '@pnpm/store.index';
3
+ import { loadJsonFileSync } from 'load-json-file';
4
+ /**
5
+ * Attempts to read a package manifest from the content-addressable store (CAFS)
6
+ * using its integrity hash. Returns `undefined` if the manifest cannot be read.
7
+ */
8
+ export function readManifestFromCafs(storeDir, storeIndex, pkg) {
9
+ try {
10
+ const pkgId = `${pkg.name}@${pkg.version}`;
11
+ const indexPath = storeIndexKey(pkg.integrity, pkgId);
12
+ const pkgIndex = storeIndex.get(indexPath);
13
+ if (!pkgIndex)
14
+ return undefined;
15
+ const pkgJsonEntry = pkgIndex.files.get('package.json');
16
+ if (pkgJsonEntry) {
17
+ const filePath = getFilePathByModeInCafs(storeDir, pkgJsonEntry.digest, pkgJsonEntry.mode);
18
+ return loadJsonFileSync(filePath);
19
+ }
20
+ }
21
+ catch {
22
+ // Fall through to undefined
23
+ }
24
+ return undefined;
25
+ }
26
+ //# sourceMappingURL=readManifestFromCafs.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Resolves the filesystem path for a package identified by its depPath.
3
+ *
4
+ * For local virtual stores, the path is constructed directly.
5
+ * For global virtual stores (where virtualStoreDir is outside modulesDir),
6
+ * symlinks are resolved to find the actual store location.
7
+ */
8
+ export declare function resolvePackagePath(opts: {
9
+ depPath: string;
10
+ name: string;
11
+ alias: string;
12
+ virtualStoreDir: string;
13
+ virtualStoreDirMaxLength: number;
14
+ modulesDir?: string;
15
+ parentDir?: string;
16
+ }): string;
@@ -0,0 +1,47 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { depPathToFilename } from '@pnpm/deps.path';
4
+ /**
5
+ * Resolves the filesystem path for a package identified by its depPath.
6
+ *
7
+ * For local virtual stores, the path is constructed directly.
8
+ * For global virtual stores (where virtualStoreDir is outside modulesDir),
9
+ * symlinks are resolved to find the actual store location.
10
+ */
11
+ export function resolvePackagePath(opts) {
12
+ let fullPackagePath = path.join(opts.virtualStoreDir, depPathToFilename(opts.depPath, opts.virtualStoreDirMaxLength), 'node_modules', opts.name);
13
+ // Resolve symlink for global virtual store.
14
+ // Global virtual store is detected when virtualStoreDir is outside the project's node_modules.
15
+ const resolvedVirtualStoreDir = path.resolve(opts.virtualStoreDir);
16
+ const resolvedModulesDir = opts.modulesDir ? path.resolve(opts.modulesDir) : undefined;
17
+ const isGlobalVirtualStore = resolvedModulesDir &&
18
+ !resolvedVirtualStoreDir.startsWith(resolvedModulesDir + path.sep) &&
19
+ resolvedVirtualStoreDir !== resolvedModulesDir;
20
+ if (isGlobalVirtualStore) {
21
+ try {
22
+ let nodeModulesDir;
23
+ if (opts.parentDir) {
24
+ // parentDir example: /store/.../node_modules/express
25
+ // /store/.../node_modules/@scope/pkg
26
+ // We need the node_modules directory to find sibling packages
27
+ nodeModulesDir = path.dirname(opts.parentDir);
28
+ // For scoped packages (@org/pkg), go up one more level
29
+ if (path.basename(nodeModulesDir).startsWith('@')) {
30
+ nodeModulesDir = path.dirname(nodeModulesDir);
31
+ }
32
+ }
33
+ else if (opts.modulesDir) {
34
+ nodeModulesDir = opts.modulesDir;
35
+ }
36
+ else {
37
+ return fullPackagePath;
38
+ }
39
+ fullPackagePath = fs.realpathSync(path.join(nodeModulesDir, opts.alias));
40
+ }
41
+ catch {
42
+ // Fallback to constructed path if symlink doesn't exist
43
+ }
44
+ }
45
+ return fullPackagePath;
46
+ }
47
+ //# sourceMappingURL=resolvePackagePath.js.map
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@pnpm/deps.inspection.tree-builder",
3
+ "version": "1001.1.3",
4
+ "description": "Creates a dependencies hierarchy for a symlinked `node_modules`",
5
+ "keywords": [
6
+ "pnpm",
7
+ "pnpm11",
8
+ "dependencies",
9
+ "hierarchy",
10
+ "node_modules"
11
+ ],
12
+ "license": "MIT",
13
+ "funding": "https://opencollective.com/pnpm",
14
+ "repository": "https://github.com/pnpm/pnpm/tree/main/deps/inspection/tree-builder",
15
+ "homepage": "https://github.com/pnpm/pnpm/tree/main/deps/inspection/tree-builder#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/pnpm/pnpm/issues"
18
+ },
19
+ "type": "module",
20
+ "main": "lib/index.js",
21
+ "types": "lib/index.d.ts",
22
+ "exports": {
23
+ ".": "./lib/index.js"
24
+ },
25
+ "files": [
26
+ "lib",
27
+ "!*.map"
28
+ ],
29
+ "dependencies": {
30
+ "@pnpm/npm-package-arg": "^2.0.0",
31
+ "@pnpm/util.lex-comparator": "^3.0.2",
32
+ "load-json-file": "^7.0.1",
33
+ "normalize-path": "^3.0.0",
34
+ "realpath-missing": "^2.0.0",
35
+ "resolve-link-target": "^3.0.0",
36
+ "semver": "^7.7.2",
37
+ "@pnpm/config.normalize-registries": "1000.1.4",
38
+ "@pnpm/config.matcher": "1000.1.0",
39
+ "@pnpm/deps.path": "1001.1.3",
40
+ "@pnpm/fs.read-modules-dir": "1000.0.0",
41
+ "@pnpm/lockfile.fs": "1001.1.21",
42
+ "@pnpm/installing.modules-yaml": "1000.3.6",
43
+ "@pnpm/lockfile.utils": "1003.0.3",
44
+ "@pnpm/lockfile.detect-dep-types": "1001.0.16",
45
+ "@pnpm/pkg-manifest.reader": "1000.1.2",
46
+ "@pnpm/store.index": "1000.0.0-0",
47
+ "@pnpm/store.cafs": "1000.0.19",
48
+ "@pnpm/types": "1000.9.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/normalize-path": "^3.0.2",
52
+ "@types/semver": "7.7.1",
53
+ "@pnpm/constants": "1001.3.1",
54
+ "@pnpm/deps.inspection.tree-builder": "1001.1.3",
55
+ "@pnpm/test-fixtures": "1000.0.0"
56
+ },
57
+ "engines": {
58
+ "node": ">=22.13"
59
+ },
60
+ "jest": {
61
+ "preset": "@pnpm/jest-config"
62
+ },
63
+ "scripts": {
64
+ "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
65
+ "_test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest",
66
+ "test": "pnpm run compile && pnpm run _test",
67
+ "compile": "tsgo --build && pnpm run lint --fix"
68
+ }
69
+ }