@pnpm/deps.inspection.tree-builder 1101.0.8 → 1101.0.10

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/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @pnpm/reviewing.dependencies-hierarchy
2
2
 
3
+ ## 1101.0.10
4
+
5
+ ### Patch Changes
6
+
7
+ - `pnpm list` now reports the correct package paths when `nodeLinker` is `hoisted` [#9593](https://github.com/pnpm/pnpm/issues/9593).
8
+
9
+ - Updated dependencies:
10
+ - @pnpm/config.normalize-registries@1101.0.3
11
+ - @pnpm/deps.path@1101.0.6
12
+ - @pnpm/fs.read-modules-dir@1100.0.4
13
+ - @pnpm/installing.modules-yaml@1101.0.6
14
+ - @pnpm/lockfile.detect-dep-types@1100.0.27
15
+ - @pnpm/lockfile.fs@1100.2.13
16
+ - @pnpm/lockfile.peer-edges@1100.0.2
17
+ - @pnpm/lockfile.utils@1102.1.6
18
+ - @pnpm/pkg-manifest.reader@1100.0.22
19
+ - @pnpm/store.cafs@1100.3.7
20
+ - @pnpm/store.index@1101.0.1
21
+
22
+ ## 1101.0.9
23
+
24
+ ### Patch Changes
25
+
26
+ - Updated dependencies:
27
+ - @pnpm/config.matcher@1100.0.3
28
+ - @pnpm/lockfile.fs@1100.2.12
29
+ - @pnpm/store.cafs@1100.3.6
30
+
3
31
  ## 1101.0.8
4
32
 
5
33
  ### Patch Changes
@@ -32,6 +32,7 @@ export interface BuildDependenciesTreeOptions {
32
32
  modulesDir?: string;
33
33
  resolvePeersFromWorkspaceRoot?: boolean;
34
34
  virtualStoreDirMaxLength: number;
35
+ nodeLinker?: 'hoisted' | 'isolated' | 'pnp';
35
36
  }
36
37
  export declare function buildDependenciesTree(projectPaths: string[] | undefined, maybeOpts: BuildDependenciesTreeOptions): Promise<{
37
38
  [projectDir: string]: DependenciesTree;
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { normalizeRegistriesByScope } from '@pnpm/config.normalize-registries';
3
+ import { withCollapsedVariants } from '@pnpm/deps.path';
3
4
  import { readModulesDir } from '@pnpm/fs.read-modules-dir';
4
5
  import { readModulesManifest } from '@pnpm/installing.modules-yaml';
5
6
  import { detectDepTypes } from '@pnpm/lockfile.detect-dep-types';
@@ -69,8 +70,8 @@ async function buildProjectsTrees(projectPaths, maybeOpts, linkedWalk) {
69
70
  return Object.fromEntries(pairs);
70
71
  }
71
72
  async function readLockfiles(modulesDir, lockfileDir) {
72
- const internalPnpmDir = path.join(modulesDir, '.pnpm');
73
- const currentLockfile = await readCurrentLockfile(internalPnpmDir, { ignoreIncompatible: false });
73
+ const installStateDir = path.join(modulesDir, '.pnpm');
74
+ const currentLockfile = await readCurrentLockfile(installStateDir, { ignoreIncompatible: false });
74
75
  const wantedLockfile = await readWantedLockfile(lockfileDir, { ignoreIncompatible: false });
75
76
  return { currentLockfile, wantedLockfile };
76
77
  }
@@ -134,6 +135,8 @@ function createTreeOptions({ treeOpts, modules, modulesDir, registriesByScope, s
134
135
  modulesDir,
135
136
  virtualStoreDir: modules?.virtualStoreDir,
136
137
  virtualStoreDirMaxLength: modules?.virtualStoreDirMaxLength ?? treeOpts.virtualStoreDirMaxLength,
138
+ nodeLinker: modules?.nodeLinker ?? treeOpts.nodeLinker,
139
+ hoistedLocations: modules?.hoistedLocations && withCollapsedVariants(modules.hoistedLocations),
137
140
  };
138
141
  }
139
142
  async function expandLinkedProjectsOfEach(pairs, opts) {
@@ -238,8 +241,6 @@ async function dependenciesHierarchyForPackage(opts, projectPath) {
238
241
  ? opts.modulesDir
239
242
  : path.join(projectPath, opts.modulesDir ?? 'node_modules');
240
243
  const { result, fieldMap } = initDependencyFields(projectSnapshot, opts.include);
241
- // Materialize the tree rooted at this importer in a single getTree call.
242
- // materializeChildren handles all dedup, search, and circular detection.
243
244
  // The depth is incremented by 1 because the importer itself is one level;
244
245
  // opts.depth controls how deep *below* the direct dependencies we go.
245
246
  const nodes = getTree({
@@ -251,7 +252,6 @@ async function dependenciesHierarchyForPackage(opts, projectPath) {
251
252
  wantedPackages: wantedLockfile?.packages ?? {},
252
253
  modulesDir,
253
254
  }, { type: 'importer', importerId });
254
- // Categorize the materialized nodes into their dependency fields.
255
255
  for (const node of nodes) {
256
256
  const field = fieldMap.get(node.alias);
257
257
  if (field != null) {
@@ -268,7 +268,6 @@ async function dependenciesHierarchyForPackage(opts, projectPath) {
268
268
  return result;
269
269
  }
270
270
  function initDependencyFields(projectSnapshot, include) {
271
- // Build a map from alias → dependency field for post-categorization.
272
271
  const result = {};
273
272
  const fieldMap = new Map();
274
273
  for (const field of DEPENDENCIES_FIELDS.sort().filter(f => include[f])) {
@@ -46,6 +46,7 @@ interface BuildDependentsTreeOptions {
46
46
  lockfile: LockfileObject;
47
47
  nameFormatter?: NameFormatter;
48
48
  resolvePeersFromWorkspaceRoot?: boolean;
49
+ nodeLinker?: 'hoisted' | 'isolated' | 'pnp';
49
50
  }
50
51
  export declare function buildDependentsTree(packages: string[], projectPaths: string[], opts: BuildDependentsTreeOptions): Promise<DependentsTree[]>;
51
52
  export {};
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { normalizeRegistriesByScope } from '@pnpm/config.normalize-registries';
3
+ import { withCollapsedVariants } from '@pnpm/deps.path';
3
4
  import { readModulesManifest } from '@pnpm/installing.modules-yaml';
4
5
  import { getLockfileImporterId, } from '@pnpm/lockfile.fs';
5
6
  import { getPeerSatisfactionEdgesToSkip } from '@pnpm/lockfile.peer-edges';
@@ -34,6 +35,9 @@ export async function buildDependentsTree(packages, projectPaths, opts) {
34
35
  wantedPackages: currentPackages,
35
36
  storeDir,
36
37
  storeIndex,
38
+ nodeLinker: modules?.nodeLinker ?? opts.nodeLinker,
39
+ hoistedLocations: modules?.hoistedLocations && withCollapsedVariants(modules.hoistedLocations),
40
+ lockfileDir: opts.lockfileDir,
37
41
  });
38
42
  const trees = collectMatchedTrees(search, {
39
43
  reverseMap,
@@ -56,7 +60,6 @@ function buildSelectedProjectsGraph(projectPaths, opts) {
56
60
  devDependencies: true,
57
61
  optionalDependencies: true,
58
62
  };
59
- // Build root IDs from the selected project paths (respects --filter / --recursive)
60
63
  const allRootIds = [];
61
64
  for (const projectPath of projectPaths) {
62
65
  const importerId = getLockfileImporterId(opts.lockfileDir, projectPath);
@@ -125,7 +128,6 @@ function buildTreeIfMatched(search, { serialized, depPath }, ctx) {
125
128
  }
126
129
  function searchPackage(search, pkg, ctx) {
127
130
  const { name, version, readManifest } = pkg;
128
- // Check canonical name first
129
131
  const matched = search({ alias: name, name, version, readManifest });
130
132
  if (matched)
131
133
  return matched;
@@ -179,7 +181,7 @@ function invertGraph(graph) {
179
181
  */
180
182
  function resolvePackageNodes(graph, currentPackages, opts) {
181
183
  const resolved = new Map();
182
- function walk(serialized, parentDir) {
184
+ function walk(serialized, parentDir, importerDir) {
183
185
  const node = graph.nodes.get(serialized);
184
186
  if (!node)
185
187
  return;
@@ -196,18 +198,18 @@ function resolvePackageNodes(graph, currentPackages, opts) {
196
198
  alias: edge.alias,
197
199
  currentPackages,
198
200
  depTypes: {},
199
- linkedPathBaseDir: opts.modulesDir, // This might need adjustment for linked deps?
201
+ linkedPathBaseDir: importerDir,
200
202
  parentDir,
201
203
  ref: edge.target.nodeId.depPath,
202
204
  skipped: new Set(),
203
205
  });
204
206
  resolved.set(childSerialized, { path: pkgInfo.path, readManifest });
205
- walk(childSerialized, pkgInfo.path);
207
+ walk(childSerialized, pkgInfo.path, importerDir);
206
208
  }
207
209
  }
208
210
  for (const [serialized, node] of graph.nodes) {
209
211
  if (node.nodeId.type === 'importer') {
210
- walk(serialized, undefined);
212
+ walk(serialized, undefined, path.join(opts.lockfileDir, node.nodeId.importerId));
211
213
  }
212
214
  }
213
215
  return resolved;
@@ -41,6 +41,9 @@ export interface GetPkgInfoOpts {
41
41
  * This is needed for subdependencies when using global virtual store.
42
42
  */
43
43
  readonly parentDir?: string;
44
+ readonly nodeLinker?: 'hoisted' | 'isolated' | 'pnp';
45
+ readonly hoistedLocations?: Record<string, string[]>;
46
+ readonly lockfileDir?: string;
44
47
  }
45
48
  export declare function getPkgInfo(opts: GetPkgInfoOpts): {
46
49
  pkgInfo: PackageInfo;
package/lib/getPkgInfo.js CHANGED
@@ -21,6 +21,11 @@ export function getPkgInfo(opts) {
21
21
  virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
22
22
  modulesDir: opts.modulesDir,
23
23
  parentDir: opts.parentDir,
24
+ nodeLinker: opts.nodeLinker,
25
+ hoistedLocations: opts.hoistedLocations,
26
+ lockfileDir: opts.lockfileDir,
27
+ projectDir: opts.linkedPathBaseDir,
28
+ version: lockedInfo.version,
24
29
  })
25
30
  : resolveLinkedPath(opts);
26
31
  const version = rewriteLinkVersion(lockedInfo.version || opts.ref, fullPackagePath, opts.rewriteLinkVersionDir);
package/lib/getTree.d.ts CHANGED
@@ -27,6 +27,8 @@ export interface BaseTreeOpts {
27
27
  showDedupedSearchMatches?: boolean;
28
28
  graph: DependencyGraph;
29
29
  materializationCache: MaterializationCache;
30
+ nodeLinker?: 'hoisted' | 'isolated' | 'pnp';
31
+ hoistedLocations?: Record<string, string[]>;
30
32
  }
31
33
  interface GetTreeOpts extends BaseTreeOpts {
32
34
  maxDepth: number;
package/lib/getTree.js CHANGED
@@ -12,11 +12,6 @@ export function getTree(opts, parentId) {
12
12
  ancestors,
13
13
  };
14
14
  const result = materializeChildren(ctx, parentId, opts.maxDepth, opts.parentDir);
15
- // Mark circular back-edges. materializeChildren truncates dependencies
16
- // at cycle boundaries but does not set the `circular` flag, so that cached
17
- // subtrees stay context-independent. fixCircularRefs walks the final tree
18
- // and adds `circular: true` wherever a node's path matches an ancestor.
19
- //
20
15
  // Seed the ancestors with parentDir (the filesystem path of parentId) so
21
16
  // that back-edges to the root of this subtree are detected — the root
22
17
  // itself does not appear as a node in the tree, only its children do.
@@ -154,7 +149,6 @@ function materializeTarget(ctx, { target, maxDepth, dir }) {
154
149
  ctx.ancestors.add(target.id);
155
150
  const childResult = materializeChildren(ctx, target.nodeId, maxDepth, dir);
156
151
  ctx.ancestors.delete(target.id);
157
- // Always cache — even results with circular truncations.
158
152
  ctx.materializationCache.set(cacheKey, {
159
153
  count: childResult.count,
160
154
  hasSearchMatch: childResult.hasSearchMatch,
@@ -228,7 +222,6 @@ function markSearchMatch(newEntry, { searchMatch, subtree, result }) {
228
222
  function fixCircularRefs(nodes, ancestors) {
229
223
  let changed = false;
230
224
  const result = nodes.map(node => {
231
- // A node whose path matches an ancestor is a circular back-edge.
232
225
  if (node.path && ancestors.has(node.path)) {
233
226
  changed = true;
234
227
  const { dependencies: _, deduped: _d, dedupedDependenciesCount: _c, ...rest } = node;
@@ -4,8 +4,10 @@
4
4
  * For local virtual stores, the path is constructed directly.
5
5
  * For global virtual stores (where virtualStoreDir is outside modulesDir),
6
6
  * symlinks are resolved to find the actual store location.
7
+ * For hoisted linker (where virtual store is empty), packages live where the
8
+ * hoisted linker placed them, recorded in hoistedLocations.
7
9
  */
8
- export declare function resolvePackagePath(opts: {
10
+ export interface ResolvePackagePathOptions {
9
11
  depPath: string;
10
12
  name: string;
11
13
  alias: string;
@@ -13,4 +15,10 @@ export declare function resolvePackagePath(opts: {
13
15
  virtualStoreDirMaxLength: number;
14
16
  modulesDir?: string;
15
17
  parentDir?: string;
16
- }): string;
18
+ nodeLinker?: 'hoisted' | 'isolated' | 'pnp';
19
+ hoistedLocations?: Record<string, string[]>;
20
+ lockfileDir?: string;
21
+ projectDir?: string;
22
+ version?: string;
23
+ }
24
+ export declare function resolvePackagePath(opts: ResolvePackagePathOptions): string;
@@ -1,14 +1,105 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { depPathToFilename } from '@pnpm/deps.path';
3
+ import { depPathToFilename, findHoistedPackageDirs } from '@pnpm/deps.path';
4
+ import { readPackageJsonFromDirSync } from '@pnpm/pkg-manifest.reader';
5
+ export function resolvePackagePath(opts) {
6
+ if (isUnsafePathComponent(opts.name)) {
7
+ return opts.virtualStoreDir;
8
+ }
9
+ if (opts.nodeLinker === 'hoisted') {
10
+ const hoistedPath = resolveHoistedPackagePath(opts);
11
+ if (hoistedPath != null)
12
+ return hoistedPath;
13
+ }
14
+ return resolveVirtualStorePackagePath(opts);
15
+ }
16
+ function resolveHoistedPackagePath(opts) {
17
+ const lockfileDir = opts.lockfileDir ?? opts.projectDir;
18
+ if (!lockfileDir)
19
+ return undefined;
20
+ const hoistedPaths = findHoistedPackageDirs(opts.hoistedLocations, opts.depPath, lockfileDir);
21
+ if (hoistedPaths.length)
22
+ return pickHoistedDir(hoistedPaths, { ...opts, lockfileDir });
23
+ return findHoistedByAlias(opts, lockfileDir);
24
+ }
4
25
  /**
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.
26
+ * Picks the copy that Node.js resolves from the parent package, or from the
27
+ * project for a direct dependency: the one in the closest modules directory
28
+ * above it. Copies on disk under the edge's alias are preferred, since one
29
+ * depPath gets a directory per alias it is installed under. Falls back to any
30
+ * copy on disk, then to the recorded one under the alias, then to the first
31
+ * recorded one.
10
32
  */
11
- export function resolvePackagePath(opts) {
33
+ function pickHoistedDir(recordedDirs, opts) {
34
+ const existing = recordedDirs.filter((dir) => fs.existsSync(dir));
35
+ const existingUnderAlias = existing.filter((dir) => isInstalledUnder(dir, opts.alias));
36
+ const candidates = existingUnderAlias.length ? existingUnderAlias : existing;
37
+ const resolveFrom = opts.parentDir ?? opts.projectDir;
38
+ if (resolveFrom) {
39
+ const relativeModulesDir = getRelativeModulesDir(opts.lockfileDir, opts.modulesDir);
40
+ const closest = candidates
41
+ .map((dir) => ({ dir, ownerDir: getOwnerDir(dir, relativeModulesDir) }))
42
+ .filter(({ ownerDir }) => isSameOrSubdir(ownerDir, resolveFrom))
43
+ .sort((a, b) => b.ownerDir.length - a.ownerDir.length)[0];
44
+ if (closest)
45
+ return closest.dir;
46
+ }
47
+ return candidates[0] ??
48
+ recordedDirs.find((dir) => isInstalledUnder(dir, opts.alias)) ??
49
+ recordedDirs[0];
50
+ }
51
+ /**
52
+ * Whether `pkgDir` is `<modules dir>/<alias>`: its trailing segments spell the
53
+ * whole alias, not just the name of a scoped package.
54
+ */
55
+ function isInstalledUnder(pkgDir, alias) {
56
+ const aliasSuffix = path.join(path.sep, alias);
57
+ return pkgDir.endsWith(aliasSuffix) &&
58
+ !path.basename(pkgDir.slice(0, -aliasSuffix.length)).startsWith('@');
59
+ }
60
+ /**
61
+ * The directory whose modules directory holds `pkgDir`.
62
+ */
63
+ function getOwnerDir(pkgDir, relativeModulesDir) {
64
+ let modulesDir = path.dirname(pkgDir);
65
+ if (path.basename(modulesDir).startsWith('@'))
66
+ modulesDir = path.dirname(modulesDir);
67
+ const rootModulesDirSuffix = path.sep + relativeModulesDir;
68
+ return modulesDir.endsWith(rootModulesDirSuffix)
69
+ ? modulesDir.slice(0, -rootModulesDirSuffix.length)
70
+ : path.dirname(modulesDir);
71
+ }
72
+ function isSameOrSubdir(parent, dir) {
73
+ return dir === parent || dir.startsWith(parent.endsWith(path.sep) ? parent : parent + path.sep);
74
+ }
75
+ /**
76
+ * Probes `<alias>` in the modules directory of the project and of the root,
77
+ * because the hoisted linker places a dependency under its alias.
78
+ */
79
+ function findHoistedByAlias(opts, lockfileDir) {
80
+ if (isUnsafePathComponent(opts.alias))
81
+ return undefined;
82
+ const relativeModulesDir = getRelativeModulesDir(lockfileDir, opts.modulesDir);
83
+ const rootModulesDir = opts.modulesDir ?? path.join(lockfileDir, 'node_modules');
84
+ const candidates = opts.projectDir ? [path.resolve(opts.projectDir, relativeModulesDir, opts.alias)] : [];
85
+ candidates.push(path.join(rootModulesDir, opts.alias));
86
+ return candidates.find((candidate) => candidateMatchesVersion(candidate, opts.version));
87
+ }
88
+ /**
89
+ * The modules directory relative to the lockfile directory, such as
90
+ * `node_modules` or `www/modules`. Falls back to its last segment when it
91
+ * lies outside the lockfile directory.
92
+ */
93
+ function getRelativeModulesDir(lockfileDir, modulesDir) {
94
+ if (!modulesDir)
95
+ return 'node_modules';
96
+ const relative = path.relative(lockfileDir, modulesDir);
97
+ if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
98
+ return path.basename(modulesDir);
99
+ }
100
+ return relative;
101
+ }
102
+ function resolveVirtualStorePackagePath(opts) {
12
103
  const fullPackagePath = path.join(opts.virtualStoreDir, depPathToFilename(opts.depPath, opts.virtualStoreDirMaxLength), 'node_modules', opts.name);
13
104
  // Resolve symlink for global virtual store.
14
105
  if (!isGlobalVirtualStore(opts.virtualStoreDir, opts.modulesDir))
@@ -46,4 +137,21 @@ function getSymlinkNodeModulesDir(opts) {
46
137
  ? path.dirname(nodeModulesDir)
47
138
  : nodeModulesDir;
48
139
  }
140
+ function candidateMatchesVersion(candidate, expectedVersion) {
141
+ if (!expectedVersion)
142
+ return fs.existsSync(candidate);
143
+ try {
144
+ const manifest = readPackageJsonFromDirSync(candidate);
145
+ return manifest.version === expectedVersion;
146
+ }
147
+ catch {
148
+ return false;
149
+ }
150
+ }
151
+ function isUnsafePathComponent(component) {
152
+ return (path.isAbsolute(component) ||
153
+ component.startsWith('\\') ||
154
+ component.includes(':') ||
155
+ component.split(/[/\\]/).some((part) => part === '..' || part === '.'));
156
+ }
49
157
  //# sourceMappingURL=resolvePackagePath.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/deps.inspection.tree-builder",
3
- "version": "1101.0.8",
3
+ "version": "1101.0.10",
4
4
  "description": "Creates a dependencies hierarchy for a symlinked `node_modules`",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -30,19 +30,19 @@
30
30
  "!*.map"
31
31
  ],
32
32
  "dependencies": {
33
- "@pnpm/config.matcher": "1100.0.2",
34
- "@pnpm/config.normalize-registries": "1101.0.2",
35
- "@pnpm/deps.path": "1101.0.5",
36
- "@pnpm/fs.read-modules-dir": "1100.0.3",
37
- "@pnpm/installing.modules-yaml": "1101.0.5",
38
- "@pnpm/lockfile.detect-dep-types": "1100.0.26",
39
- "@pnpm/lockfile.fs": "1100.2.11",
40
- "@pnpm/lockfile.peer-edges": "1100.0.1",
41
- "@pnpm/lockfile.utils": "1102.1.5",
33
+ "@pnpm/config.matcher": "1100.0.3",
34
+ "@pnpm/config.normalize-registries": "1101.0.3",
35
+ "@pnpm/deps.path": "1101.0.6",
36
+ "@pnpm/fs.read-modules-dir": "1100.0.4",
37
+ "@pnpm/installing.modules-yaml": "1101.0.6",
38
+ "@pnpm/lockfile.detect-dep-types": "1100.0.27",
39
+ "@pnpm/lockfile.fs": "1100.2.13",
40
+ "@pnpm/lockfile.peer-edges": "1100.0.2",
41
+ "@pnpm/lockfile.utils": "1102.1.6",
42
42
  "@pnpm/npm-package-arg": "^2.0.0",
43
- "@pnpm/pkg-manifest.reader": "1100.0.21",
44
- "@pnpm/store.cafs": "1100.3.5",
45
- "@pnpm/store.index": "1101.0.0",
43
+ "@pnpm/pkg-manifest.reader": "1100.0.22",
44
+ "@pnpm/store.cafs": "1100.3.7",
45
+ "@pnpm/store.index": "1101.0.1",
46
46
  "@pnpm/text.ordinal-comparator": "1100.0.0",
47
47
  "@pnpm/types": "1102.1.1",
48
48
  "load-json-file": "^7.0.1",
@@ -55,8 +55,8 @@
55
55
  },
56
56
  "devDependencies": {
57
57
  "@jest/globals": "30.4.1",
58
- "@pnpm/constants": "1102.0.0",
59
- "@pnpm/deps.inspection.tree-builder": "1101.0.8",
58
+ "@pnpm/constants": "1102.0.1",
59
+ "@pnpm/deps.inspection.tree-builder": "1101.0.10",
60
60
  "@pnpm/test-fixtures": "1100.0.1",
61
61
  "@types/normalize-path": "^3.0.2",
62
62
  "@types/semver": "7.8.0"