@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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-2016 Rico Sta. Cruz and other contributors
4
+ Copyright (c) 2016-2026 Zoltan Kochan and other contributors
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # @pnpm/reviewing.tree-builder
2
+
3
+ > Creates a dependencies hierarchy for a symlinked \`node_modules\`
4
+
5
+ <!--@shields('npm')-->
6
+ [![npm version](https://img.shields.io/npm/v/tree-builder.svg)](https://www.npmjs.com/package/tree-builder)
7
+ <!--/@-->
8
+
9
+ A symlinked `node_modules` is created when installing using [pnpm](https://github.com/pnpm/pnpm).
10
+
11
+ ## Installation
12
+
13
+ ```
14
+ pnpm add @pnpm/reviewing.tree-builder
15
+ ```
16
+
17
+ ## License
18
+
19
+ MIT
@@ -0,0 +1,28 @@
1
+ export interface DependencyNode {
2
+ alias: string;
3
+ circular?: true;
4
+ deduped?: true;
5
+ /**
6
+ * When `deduped` is true, the number of transitive dependencies that were
7
+ * elided because this subtree was already expanded elsewhere in the tree.
8
+ */
9
+ dedupedDependenciesCount?: number;
10
+ /**
11
+ * Short hash of the peer dependency suffix in the depPath, used to
12
+ * distinguish deduped instances of the same package with different
13
+ * peer dependency resolutions.
14
+ */
15
+ peersSuffixHash?: string;
16
+ dependencies?: DependencyNode[];
17
+ dev?: boolean;
18
+ isPeer: boolean;
19
+ isSkipped: boolean;
20
+ isMissing: boolean;
21
+ name: string;
22
+ optional?: true;
23
+ path: string;
24
+ resolved?: string;
25
+ searched?: true;
26
+ version: string;
27
+ searchMessage?: string;
28
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=DependencyNode.js.map
@@ -0,0 +1,18 @@
1
+ import type { DepPath } from '@pnpm/types';
2
+ export type TreeNodeId = TreeNodeIdImporter | TreeNodeIdPackage;
3
+ /**
4
+ * A project local to the pnpm workspace.
5
+ */
6
+ interface TreeNodeIdImporter {
7
+ readonly type: 'importer';
8
+ readonly importerId: string;
9
+ }
10
+ /**
11
+ * An npm package depended on externally.
12
+ */
13
+ interface TreeNodeIdPackage {
14
+ readonly type: 'package';
15
+ readonly depPath: DepPath;
16
+ }
17
+ export declare function serializeTreeNodeId(treeNodeId: TreeNodeId): string;
18
+ export {};
@@ -0,0 +1,17 @@
1
+ export function serializeTreeNodeId(treeNodeId) {
2
+ switch (treeNodeId.type) {
3
+ case 'importer': {
4
+ // Only serialize known fields from TreeNodeId. TypeScript is duck typed and
5
+ // objects can have any number of unknown extra fields.
6
+ const { type, importerId } = treeNodeId;
7
+ return JSON.stringify({ type, importerId });
8
+ }
9
+ case 'package': {
10
+ const { type, depPath } = treeNodeId;
11
+ return JSON.stringify({ type, depPath });
12
+ }
13
+ default:
14
+ throw new Error(`Unknown TreeNodeId type: ${treeNodeId.type}`);
15
+ }
16
+ }
17
+ //# sourceMappingURL=TreeNodeId.js.map
@@ -0,0 +1,25 @@
1
+ import { type DependenciesField, type Finder, type Registries } from '@pnpm/types';
2
+ import type { DependencyNode } from './DependencyNode.js';
3
+ export interface DependenciesTree {
4
+ dependencies?: DependencyNode[];
5
+ devDependencies?: DependencyNode[];
6
+ optionalDependencies?: DependencyNode[];
7
+ unsavedDependencies?: DependencyNode[];
8
+ }
9
+ export declare function buildDependenciesTree(projectPaths: string[] | undefined, maybeOpts: {
10
+ depth: number;
11
+ excludePeerDependencies?: boolean;
12
+ include?: {
13
+ [dependenciesField in DependenciesField]: boolean;
14
+ };
15
+ registries?: Registries;
16
+ onlyProjects?: boolean;
17
+ search?: Finder;
18
+ showDedupedSearchMatches?: boolean;
19
+ lockfileDir: string;
20
+ checkWantedLockfileOnly?: boolean;
21
+ modulesDir?: string;
22
+ virtualStoreDirMaxLength: number;
23
+ }): Promise<{
24
+ [projectDir: string]: DependenciesTree;
25
+ }>;
@@ -0,0 +1,184 @@
1
+ import path from 'node:path';
2
+ import { normalizeRegistries } from '@pnpm/config.normalize-registries';
3
+ import { readModulesDir } from '@pnpm/fs.read-modules-dir';
4
+ import { readModulesManifest } from '@pnpm/installing.modules-yaml';
5
+ import { detectDepTypes } from '@pnpm/lockfile.detect-dep-types';
6
+ import { getLockfileImporterId, readCurrentLockfile, readWantedLockfile, } from '@pnpm/lockfile.fs';
7
+ import { safeReadPackageJsonFromDir } from '@pnpm/pkg-manifest.reader';
8
+ import { StoreIndex } from '@pnpm/store.index';
9
+ import { DEPENDENCIES_FIELDS } from '@pnpm/types';
10
+ import normalizePath from 'normalize-path';
11
+ import { realpathMissing } from 'realpath-missing';
12
+ import { resolveLinkTarget } from 'resolve-link-target';
13
+ import { buildDependencyGraph } from './buildDependencyGraph.js';
14
+ import { getTree } from './getTree.js';
15
+ export async function buildDependenciesTree(projectPaths, maybeOpts) {
16
+ if (!maybeOpts?.lockfileDir) {
17
+ throw new TypeError('opts.lockfileDir is required');
18
+ }
19
+ const modulesDir = await realpathMissing(path.join(maybeOpts.lockfileDir, maybeOpts.modulesDir ?? 'node_modules'));
20
+ const modules = await readModulesManifest(modulesDir);
21
+ const registries = normalizeRegistries({
22
+ ...maybeOpts?.registries,
23
+ ...modules?.registries,
24
+ });
25
+ const internalPnpmDir = path.join(modulesDir, '.pnpm');
26
+ const currentLockfile = await readCurrentLockfile(internalPnpmDir, { ignoreIncompatible: false });
27
+ const wantedLockfile = await readWantedLockfile(maybeOpts.lockfileDir, { ignoreIncompatible: false });
28
+ if (projectPaths == null) {
29
+ projectPaths = Object.keys(wantedLockfile?.importers ?? {})
30
+ .map((id) => path.join(maybeOpts.lockfileDir, id));
31
+ }
32
+ const result = {};
33
+ const lockfileToUse = maybeOpts.checkWantedLockfileOnly ? wantedLockfile : (currentLockfile ?? wantedLockfile);
34
+ if (!lockfileToUse) {
35
+ for (const projectPath of projectPaths) {
36
+ result[projectPath] = {};
37
+ }
38
+ return result;
39
+ }
40
+ const storeDir = modules?.storeDir;
41
+ const storeIndex = storeDir ? new StoreIndex(storeDir) : undefined;
42
+ const opts = {
43
+ depth: maybeOpts.depth || 0,
44
+ excludePeerDependencies: maybeOpts.excludePeerDependencies,
45
+ include: maybeOpts.include ?? {
46
+ dependencies: true,
47
+ devDependencies: true,
48
+ optionalDependencies: true,
49
+ },
50
+ lockfileDir: maybeOpts.lockfileDir,
51
+ checkWantedLockfileOnly: maybeOpts.checkWantedLockfileOnly,
52
+ onlyProjects: maybeOpts.onlyProjects,
53
+ registries,
54
+ search: maybeOpts.search,
55
+ showDedupedSearchMatches: maybeOpts.showDedupedSearchMatches ?? (maybeOpts.search != null),
56
+ skipped: new Set(modules?.skipped ?? []),
57
+ storeDir,
58
+ storeIndex,
59
+ modulesDir,
60
+ virtualStoreDir: modules?.virtualStoreDir,
61
+ virtualStoreDirMaxLength: modules?.virtualStoreDirMaxLength ?? maybeOpts.virtualStoreDirMaxLength,
62
+ };
63
+ // Build the dependency graph ONCE for all importers and share a single
64
+ // MaterializationCache so that identical subtrees are only materialized once.
65
+ const allRootIds = [];
66
+ for (const projectPath of projectPaths) {
67
+ const importerId = getLockfileImporterId(opts.lockfileDir, projectPath);
68
+ if (lockfileToUse.importers[importerId]) {
69
+ allRootIds.push({ type: 'importer', importerId });
70
+ }
71
+ }
72
+ const sharedGraph = buildDependencyGraph(allRootIds, {
73
+ currentPackages: lockfileToUse.packages ?? {},
74
+ importers: lockfileToUse.importers,
75
+ include: opts.include,
76
+ lockfileDir: opts.lockfileDir,
77
+ onlyProjects: opts.onlyProjects,
78
+ });
79
+ const sharedMaterializationCache = new Map();
80
+ const sharedDepTypes = detectDepTypes(lockfileToUse);
81
+ const ctx = {
82
+ currentLockfile: lockfileToUse,
83
+ wantedLockfile,
84
+ ...opts,
85
+ graph: sharedGraph,
86
+ materializationCache: sharedMaterializationCache,
87
+ depTypes: sharedDepTypes,
88
+ };
89
+ const getHierarchy = dependenciesHierarchyForPackage.bind(null, ctx);
90
+ const pairs = await Promise.all(projectPaths.map(async (projectPath) => {
91
+ return [
92
+ projectPath,
93
+ await getHierarchy(projectPath),
94
+ ];
95
+ }));
96
+ for (const [projectPath, dependenciesHierarchy] of pairs) {
97
+ result[projectPath] = dependenciesHierarchy;
98
+ }
99
+ storeIndex?.close();
100
+ return result;
101
+ }
102
+ async function dependenciesHierarchyForPackage(opts, projectPath) {
103
+ const { currentLockfile, wantedLockfile } = opts;
104
+ const importerId = getLockfileImporterId(opts.lockfileDir, projectPath);
105
+ if (!currentLockfile.importers[importerId])
106
+ return {};
107
+ const modulesDir = opts.modulesDir && path.isAbsolute(opts.modulesDir)
108
+ ? opts.modulesDir
109
+ : path.join(projectPath, opts.modulesDir ?? 'node_modules');
110
+ const currentPackages = currentLockfile.packages ?? {};
111
+ const wantedPackages = wantedLockfile?.packages ?? {};
112
+ // Build a map from alias → dependency field for post-categorization.
113
+ const result = {};
114
+ const fieldMap = new Map();
115
+ for (const field of DEPENDENCIES_FIELDS.sort().filter(f => opts.include[f])) {
116
+ result[field] = [];
117
+ const fieldDeps = currentLockfile.importers[importerId][field] ?? {};
118
+ for (const alias in fieldDeps) {
119
+ fieldMap.set(alias, field);
120
+ }
121
+ }
122
+ const parentId = { type: 'importer', importerId };
123
+ // Materialize the tree rooted at this importer in a single getTree call.
124
+ // materializeChildren handles all dedup, search, and circular detection.
125
+ // The depth is incremented by 1 because the importer itself is one level;
126
+ // opts.depth controls how deep *below* the direct dependencies we go.
127
+ const nodes = getTree({
128
+ ...opts,
129
+ currentPackages,
130
+ importers: currentLockfile.importers,
131
+ rewriteLinkVersionDir: projectPath,
132
+ maxDepth: opts.depth + 1,
133
+ wantedPackages,
134
+ modulesDir,
135
+ }, parentId);
136
+ // Categorize the materialized nodes into their dependency fields.
137
+ for (const node of nodes) {
138
+ const field = fieldMap.get(node.alias);
139
+ if (field != null) {
140
+ result[field].push(node);
141
+ }
142
+ }
143
+ // Handle unsaved dependencies (packages in node_modules but not in lockfile).
144
+ // When searching, unsaved deps are irrelevant — they aren't in the lockfile
145
+ // graph and can't have dependency subtrees showing paths to the search target.
146
+ if (!opts.search) {
147
+ const savedDeps = getAllDirectDependencies(currentLockfile.importers[importerId]);
148
+ const unsavedDeps = ((await readModulesDir(modulesDir)) ?? []).filter((directDep) => !savedDeps[directDep]);
149
+ if (unsavedDeps.length > 0)
150
+ await Promise.all(unsavedDeps.map(async (unsavedDep) => {
151
+ let pkgPath = path.join(modulesDir, unsavedDep);
152
+ let version;
153
+ try {
154
+ pkgPath = await resolveLinkTarget(pkgPath);
155
+ version = `link:${normalizePath(path.relative(projectPath, pkgPath))}`;
156
+ }
157
+ catch {
158
+ // if error happened. The package is not a link
159
+ const pkg = await safeReadPackageJsonFromDir(pkgPath);
160
+ version = pkg?.version ?? 'undefined';
161
+ }
162
+ const pkg = {
163
+ alias: unsavedDep,
164
+ isMissing: false,
165
+ isPeer: false,
166
+ isSkipped: false,
167
+ name: unsavedDep,
168
+ path: pkgPath,
169
+ version,
170
+ };
171
+ result.unsavedDependencies = result.unsavedDependencies ?? [];
172
+ result.unsavedDependencies.push(pkg);
173
+ }));
174
+ }
175
+ return result;
176
+ }
177
+ function getAllDirectDependencies(projectSnapshot) {
178
+ return {
179
+ ...projectSnapshot.dependencies,
180
+ ...projectSnapshot.devDependencies,
181
+ ...projectSnapshot.optionalDependencies,
182
+ };
183
+ }
184
+ //# sourceMappingURL=buildDependenciesTree.js.map
@@ -0,0 +1,30 @@
1
+ import type { PackageSnapshots, ProjectSnapshot } from '@pnpm/lockfile.fs';
2
+ import { type TreeNodeId } from './TreeNodeId.js';
3
+ interface DependencyEdge {
4
+ alias: string;
5
+ ref: string;
6
+ target?: {
7
+ id: string;
8
+ nodeId: TreeNodeId;
9
+ };
10
+ }
11
+ interface DependencyGraphNode {
12
+ nodeId: TreeNodeId;
13
+ edges: DependencyEdge[];
14
+ peers: Set<string>;
15
+ }
16
+ export interface DependencyGraph {
17
+ nodes: Map<string, DependencyGraphNode>;
18
+ }
19
+ export declare function buildDependencyGraph(rootIds: TreeNodeId[], opts: {
20
+ currentPackages: PackageSnapshots;
21
+ importers: Record<string, ProjectSnapshot>;
22
+ include: {
23
+ dependencies?: boolean;
24
+ devDependencies?: boolean;
25
+ optionalDependencies?: boolean;
26
+ };
27
+ lockfileDir: string;
28
+ onlyProjects?: boolean;
29
+ }): DependencyGraph;
30
+ export {};
@@ -0,0 +1,77 @@
1
+ import { getTreeNodeChildId } from './getTreeNodeChildId.js';
2
+ import { serializeTreeNodeId } from './TreeNodeId.js';
3
+ export function buildDependencyGraph(rootIds, opts) {
4
+ const graph = { nodes: new Map() };
5
+ const queue = [...rootIds];
6
+ let queueIdx = 0;
7
+ const visited = new Set();
8
+ while (queueIdx < queue.length) {
9
+ const nodeId = queue[queueIdx++];
10
+ const serialized = serializeTreeNodeId(nodeId);
11
+ if (visited.has(serialized))
12
+ continue;
13
+ visited.add(serialized);
14
+ const snapshot = getSnapshot(nodeId, opts);
15
+ if (!snapshot) {
16
+ graph.nodes.set(serialized, { nodeId, edges: [], peers: new Set() });
17
+ continue;
18
+ }
19
+ // For importers, only include the dependency fields the caller selected.
20
+ // For packages, devDependencies don't exist in the lockfile.
21
+ const deps = nodeId.type === 'importer'
22
+ ? {
23
+ ...(opts.include.dependencies !== false ? snapshot.dependencies : undefined),
24
+ ...(opts.include.devDependencies !== false ? snapshot.devDependencies : undefined),
25
+ ...(opts.include.optionalDependencies ? snapshot.optionalDependencies : undefined),
26
+ }
27
+ : !opts.include.optionalDependencies
28
+ ? snapshot.dependencies
29
+ : {
30
+ ...snapshot.dependencies,
31
+ ...snapshot.optionalDependencies,
32
+ };
33
+ const peers = new Set(Object.keys(nodeId.type === 'package'
34
+ ? (opts.currentPackages[nodeId.depPath]?.peerDependencies ?? {})
35
+ : {}));
36
+ const edges = [];
37
+ if (deps != null) {
38
+ for (const alias in deps) {
39
+ const rawRef = deps[alias];
40
+ // Lockfile may expose ref as string (version) or inline { version, specifier }
41
+ const ref = typeof rawRef === 'string'
42
+ ? rawRef
43
+ : rawRef?.version;
44
+ if (ref == null)
45
+ continue;
46
+ const targetNodeId = getTreeNodeChildId({
47
+ parentId: nodeId,
48
+ dep: { alias, ref },
49
+ lockfileDir: opts.lockfileDir,
50
+ importers: opts.importers,
51
+ });
52
+ // When onlyProjects is true, only follow edges to workspace importers
53
+ if (opts.onlyProjects && targetNodeId?.type !== 'importer') {
54
+ continue;
55
+ }
56
+ const target = targetNodeId != null
57
+ ? { id: serializeTreeNodeId(targetNodeId), nodeId: targetNodeId }
58
+ : undefined;
59
+ edges.push({ alias, ref, target });
60
+ if (target && !visited.has(target.id)) {
61
+ queue.push(target.nodeId);
62
+ }
63
+ }
64
+ }
65
+ graph.nodes.set(serialized, { nodeId, edges, peers });
66
+ }
67
+ return graph;
68
+ }
69
+ function getSnapshot(treeNodeId, opts) {
70
+ switch (treeNodeId.type) {
71
+ case 'importer':
72
+ return opts.importers[treeNodeId.importerId];
73
+ case 'package':
74
+ return opts.currentPackages[treeNodeId.depPath];
75
+ }
76
+ }
77
+ //# sourceMappingURL=buildDependencyGraph.js.map
@@ -0,0 +1,46 @@
1
+ import { type LockfileObject } from '@pnpm/lockfile.fs';
2
+ import type { DependenciesField, DependencyManifest, Finder, Registries } from '@pnpm/types';
3
+ export interface DependentNode {
4
+ name: string;
5
+ displayName?: string;
6
+ version: string;
7
+ dependents?: DependentNode[];
8
+ circular?: true;
9
+ deduped?: true;
10
+ /** Short hash distinguishing peer-dep variants of the same name@version */
11
+ peersSuffixHash?: string;
12
+ /** For importer leaf nodes: which dep field */
13
+ depField?: DependenciesField;
14
+ }
15
+ export interface DependentsTree {
16
+ name: string;
17
+ displayName?: string;
18
+ version: string;
19
+ /** Resolved filesystem path to this package */
20
+ path?: string;
21
+ /** Short hash distinguishing peer-dep variants of the same name@version */
22
+ peersSuffixHash?: string;
23
+ /** Message returned by the finder function, if any */
24
+ searchMessage?: string;
25
+ dependents: DependentNode[];
26
+ }
27
+ export interface ImporterInfo {
28
+ name: string;
29
+ version: string;
30
+ }
31
+ export declare function buildDependentsTree(packages: string[], projectPaths: string[], opts: {
32
+ lockfileDir: string;
33
+ include?: {
34
+ [field in DependenciesField]?: boolean;
35
+ };
36
+ modulesDir?: string;
37
+ registries?: Registries;
38
+ finders?: Finder[];
39
+ importerInfoMap: Map<string, ImporterInfo>;
40
+ lockfile: LockfileObject;
41
+ nameFormatter?: (info: {
42
+ name: string;
43
+ version: string;
44
+ manifest: DependencyManifest;
45
+ }) => string | undefined;
46
+ }): Promise<DependentsTree[]>;