@pnpm/deps.inspection.tree-builder 1101.0.9 → 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,24 @@
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
+
3
22
  ## 1101.0.9
4
23
 
5
24
  ### 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';
@@ -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) {
@@ -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,
@@ -177,7 +181,7 @@ function invertGraph(graph) {
177
181
  */
178
182
  function resolvePackageNodes(graph, currentPackages, opts) {
179
183
  const resolved = new Map();
180
- function walk(serialized, parentDir) {
184
+ function walk(serialized, parentDir, importerDir) {
181
185
  const node = graph.nodes.get(serialized);
182
186
  if (!node)
183
187
  return;
@@ -194,18 +198,18 @@ function resolvePackageNodes(graph, currentPackages, opts) {
194
198
  alias: edge.alias,
195
199
  currentPackages,
196
200
  depTypes: {},
197
- linkedPathBaseDir: opts.modulesDir, // This might need adjustment for linked deps?
201
+ linkedPathBaseDir: importerDir,
198
202
  parentDir,
199
203
  ref: edge.target.nodeId.depPath,
200
204
  skipped: new Set(),
201
205
  });
202
206
  resolved.set(childSerialized, { path: pkgInfo.path, readManifest });
203
- walk(childSerialized, pkgInfo.path);
207
+ walk(childSerialized, pkgInfo.path, importerDir);
204
208
  }
205
209
  }
206
210
  for (const [serialized, node] of graph.nodes) {
207
211
  if (node.nodeId.type === 'importer') {
208
- walk(serialized, undefined);
212
+ walk(serialized, undefined, path.join(opts.lockfileDir, node.nodeId.importerId));
209
213
  }
210
214
  }
211
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;
@@ -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.9",
3
+ "version": "1101.0.10",
4
4
  "description": "Creates a dependencies hierarchy for a symlinked `node_modules`",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -31,18 +31,18 @@
31
31
  ],
32
32
  "dependencies": {
33
33
  "@pnpm/config.matcher": "1100.0.3",
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.12",
40
- "@pnpm/lockfile.peer-edges": "1100.0.1",
41
- "@pnpm/lockfile.utils": "1102.1.5",
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.6",
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.9",
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"