@pnpm/deps.inspection.tree-builder 1101.0.5 → 1101.0.7

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,53 @@
1
1
  # @pnpm/reviewing.dependencies-hierarchy
2
2
 
3
+ ## 1101.0.7
4
+
5
+ ### Patch Changes
6
+
7
+ - `pnpm install --dev` and `pnpm fetch --dev` now install the optional dependencies of devDependencies, such as the platform binaries of Biome and oxlint. The project's own `optionalDependencies` are still skipped [#9678](https://github.com/pnpm/pnpm/issues/9678).
8
+
9
+ - `pnpm install` no longer fails when a package from the registry declares a `file:` dependency on a directory inside itself, such as `"@types/css-tree": "file:./typings/css-tree"`. pnpm links that dependency to the directory inside the package, as npm and Yarn do. The lockfile records it as `link:<root>/typings/css-tree` [#9141](https://github.com/pnpm/pnpm/issues/9141).
10
+
11
+ - `pnpm list` now shows the correct path of a `link:` dependency that points to a directory on another drive on Windows. The path used to be appended to the project directory, such as `C:\project\D:\lib`, and `pnpm list --long` could not show the package's details [#10362](https://github.com/pnpm/pnpm/issues/10362).
12
+
13
+ - Updated dependencies:
14
+ - @pnpm/deps.path@1101.0.5
15
+ - @pnpm/fs.read-modules-dir@1100.0.3
16
+ - @pnpm/installing.modules-yaml@1101.0.5
17
+ - @pnpm/lockfile.detect-dep-types@1100.0.26
18
+ - @pnpm/lockfile.fs@1100.2.10
19
+ - @pnpm/lockfile.peer-edges@1100.0.1
20
+ - @pnpm/lockfile.utils@1102.1.5
21
+ - @pnpm/pkg-manifest.reader@1100.0.21
22
+ - @pnpm/store.cafs@1100.3.5
23
+ - @pnpm/store.index@1100.3.3
24
+
25
+ ## 1101.0.6
26
+
27
+ ### Patch Changes
28
+
29
+ - `pnpm list --only-projects` now lists the workspace projects when `sharedWorkspaceLockfile` is `false` [#7151](https://github.com/pnpm/pnpm/issues/7151).
30
+
31
+ - `pnpm list --only-projects` now lists a workspace project that sets `publishConfig.directory`. Dependents link such a project through its publish directory, which `--only-projects` did not recognize as a project [#10635](https://github.com/pnpm/pnpm/issues/10635).
32
+
33
+ - `pnpm list --only-projects` now prints every project selected with `--filter` or `--recursive`, including a project that has no workspace dependencies [#9770](https://github.com/pnpm/pnpm/issues/9770).
34
+
35
+ `pnpm list --only-projects` no longer reports packages in `node_modules` that are missing from the lockfile [#9528](https://github.com/pnpm/pnpm/issues/9528).
36
+
37
+ - `pnpm install --prod`, `pnpm fetch --prod` and `pnpm deploy --prod` no longer install a devDependency that is only there to satisfy an optional peer dependency of a production dependency. `pnpm list`, `pnpm why`, `pnpm licenses`, `pnpm sbom` and `pnpm audit` leave it out of `--prod` results too. The same applies to `--dev`. A peer that is not optional is still installed and audited [#15344](https://github.com/pnpm/pnpm/issues/15344).
38
+
39
+ - Updated dependencies:
40
+ - @pnpm/config.normalize-registries@1101.0.2
41
+ - @pnpm/deps.path@1101.0.4
42
+ - @pnpm/installing.modules-yaml@1101.0.4
43
+ - @pnpm/lockfile.detect-dep-types@1100.0.25
44
+ - @pnpm/lockfile.fs@1100.2.9
45
+ - @pnpm/lockfile.utils@1102.1.4
46
+ - @pnpm/pkg-manifest.reader@1100.0.20
47
+ - @pnpm/store.cafs@1100.3.4
48
+ - @pnpm/store.index@1100.3.2
49
+ - @pnpm/types@1102.1.1
50
+
3
51
  ## 1101.0.5
4
52
 
5
53
  ### Patch Changes
@@ -6,7 +6,7 @@ export interface DependenciesTree {
6
6
  optionalDependencies?: DependencyNode[];
7
7
  unsavedDependencies?: DependencyNode[];
8
8
  }
9
- export declare function buildDependenciesTree(projectPaths: string[] | undefined, maybeOpts: {
9
+ export interface BuildDependenciesTreeOptions {
10
10
  depth: number;
11
11
  excludePeerDependencies?: boolean;
12
12
  include?: {
@@ -15,12 +15,24 @@ export declare function buildDependenciesTree(projectPaths: string[] | undefined
15
15
  registriesByScope?: RegistriesByScope;
16
16
  registriesByPrefix?: Record<string, string>;
17
17
  onlyProjects?: boolean;
18
+ /**
19
+ * The workspace projects that `onlyProjects` follows through their own
20
+ * lockfiles when the lockfile being read has no importer for them.
21
+ */
22
+ workspaceProjectDirs?: string[];
23
+ /**
24
+ * The workspace projects that dependents link through their publish
25
+ * directory (`publishConfig.directory`), keyed by that directory.
26
+ */
27
+ workspaceProjectPublishDirs?: Record<string, string>;
18
28
  search?: Finder;
19
29
  showDedupedSearchMatches?: boolean;
20
30
  lockfileDir: string;
21
31
  checkWantedLockfileOnly?: boolean;
22
32
  modulesDir?: string;
33
+ resolvePeersFromWorkspaceRoot?: boolean;
23
34
  virtualStoreDirMaxLength: number;
24
- }): Promise<{
35
+ }
36
+ export declare function buildDependenciesTree(projectPaths: string[] | undefined, maybeOpts: BuildDependenciesTreeOptions): Promise<{
25
37
  [projectDir: string]: DependenciesTree;
26
38
  }>;
@@ -4,6 +4,7 @@ import { readModulesDir } from '@pnpm/fs.read-modules-dir';
4
4
  import { readModulesManifest } from '@pnpm/installing.modules-yaml';
5
5
  import { detectDepTypes } from '@pnpm/lockfile.detect-dep-types';
6
6
  import { getLockfileImporterId, readCurrentLockfile, readWantedLockfile, } from '@pnpm/lockfile.fs';
7
+ import { getPeerSatisfactionEdgesToSkip } from '@pnpm/lockfile.peer-edges';
7
8
  import { safeReadPackageJsonFromDir } from '@pnpm/pkg-manifest.reader';
8
9
  import { StoreIndex } from '@pnpm/store.index';
9
10
  import { DEPENDENCIES_FIELDS } from '@pnpm/types';
@@ -16,6 +17,16 @@ import { buildDependencyGraph } from './buildDependencyGraph.js';
16
17
  import { getTree } from './getTree.js';
17
18
  const limitUnsavedReads = pLimit(4);
18
19
  export async function buildDependenciesTree(projectPaths, maybeOpts) {
20
+ return buildProjectsTrees(projectPaths, maybeOpts, {
21
+ ancestors: new Set(),
22
+ expanded: new Map(),
23
+ linkedProjectDirs: new Map([
24
+ ...Object.entries(maybeOpts.workspaceProjectPublishDirs ?? {}),
25
+ ...(maybeOpts.workspaceProjectDirs ?? []).map((dir) => [dir, dir]),
26
+ ]),
27
+ });
28
+ }
29
+ async function buildProjectsTrees(projectPaths, maybeOpts, linkedWalk) {
19
30
  if (!maybeOpts?.lockfileDir) {
20
31
  throw new TypeError('opts.lockfileDir is required');
21
32
  }
@@ -76,9 +87,13 @@ export async function buildDependenciesTree(projectPaths, maybeOpts) {
76
87
  include: opts.include,
77
88
  lockfileDir: opts.lockfileDir,
78
89
  onlyProjects: opts.onlyProjects,
90
+ peerSatisfactionEdges: getPeerSatisfactionEdgesToSkip(lockfileToUse, {
91
+ include: opts.include,
92
+ resolvePeersFromWorkspaceRoot: maybeOpts.resolvePeersFromWorkspaceRoot,
93
+ }),
79
94
  });
80
95
  const sharedMaterializationCache = new Map();
81
- const sharedDepTypes = detectDepTypes(lockfileToUse);
96
+ const sharedDepTypes = detectDepTypes(lockfileToUse, maybeOpts);
82
97
  const ctx = {
83
98
  currentLockfile: lockfileToUse,
84
99
  wantedLockfile,
@@ -94,12 +109,106 @@ export async function buildDependenciesTree(projectPaths, maybeOpts) {
94
109
  await getHierarchy(projectPath),
95
110
  ];
96
111
  }));
112
+ storeIndex?.close();
97
113
  for (const [projectPath, dependenciesHierarchy] of pairs) {
98
114
  result[projectPath] = dependenciesHierarchy;
99
115
  }
100
- storeIndex?.close();
116
+ if (opts.onlyProjects) {
117
+ // Sequential, so that the first occurrence of a linked project is the
118
+ // one expanded, as with the deduplication of a shared lockfile.
119
+ for (const [projectPath, dependenciesHierarchy] of pairs) {
120
+ // eslint-disable-next-line no-await-in-loop
121
+ await expandLinkedProjects(dependenciesHierarchy, {
122
+ importers: lockfileToUse.importers,
123
+ lockfileDir: opts.lockfileDir,
124
+ depth: opts.depth,
125
+ treeOpts: maybeOpts,
126
+ walk: { ...linkedWalk, ancestors: new Set([...linkedWalk.ancestors, projectPath]) },
127
+ rewriteLinkVersionDir: projectPath,
128
+ });
129
+ }
130
+ }
101
131
  return result;
102
132
  }
133
+ /**
134
+ * Attaches the project dependencies of every linked workspace project that
135
+ * the lockfile has no importer for. With `sharedWorkspaceLockfile: false`,
136
+ * the lockfile that the tree was built from knows nothing about the
137
+ * dependencies of the other workspace projects.
138
+ */
139
+ async function expandLinkedProjects(tree, ctx) {
140
+ for (const field of DEPENDENCIES_FIELDS) {
141
+ if (tree[field] != null) {
142
+ // eslint-disable-next-line no-await-in-loop
143
+ tree[field] = await expandLinkedProjectNodes(tree[field], 0, ctx);
144
+ }
145
+ }
146
+ }
147
+ async function expandLinkedProjectNodes(nodes, level, ctx) {
148
+ const expanded = [];
149
+ for (const node of nodes) {
150
+ let expandedNode = node;
151
+ if (node.dependencies != null) {
152
+ // eslint-disable-next-line no-await-in-loop
153
+ expandedNode = keepSearched({ ...node, dependencies: await expandLinkedProjectNodes(node.dependencies, level + 1, ctx) }, ctx);
154
+ }
155
+ else if (!node.circular && ctx.importers[getLockfileImporterId(ctx.lockfileDir, node.path)] == null) {
156
+ // eslint-disable-next-line no-await-in-loop
157
+ expandedNode = await expandLinkedProject(node, level, ctx);
158
+ }
159
+ if (expandedNode != null)
160
+ expanded.push(expandedNode);
161
+ }
162
+ return expanded;
163
+ }
164
+ async function expandLinkedProject(linkedNode, level, ctx) {
165
+ const projectDir = ctx.walk.linkedProjectDirs.get(linkedNode.path);
166
+ if (projectDir == null)
167
+ return undefined;
168
+ const node = { ...linkedNode, path: projectDir };
169
+ if (ctx.walk.ancestors.has(node.path))
170
+ return keepSearched({ ...node, circular: true }, ctx);
171
+ if (level >= ctx.depth)
172
+ return keepSearched(node, ctx);
173
+ const depth = ctx.depth - level - 1;
174
+ const key = `${node.path}@${depth}`;
175
+ const previousCount = ctx.walk.expanded.get(key);
176
+ if (previousCount != null) {
177
+ return previousCount > 0
178
+ ? { ...node, deduped: true, dedupedDependenciesCount: previousCount }
179
+ : keepSearched(node, ctx);
180
+ }
181
+ const linkedTrees = await buildProjectsTrees([node.path], {
182
+ ...ctx.treeOpts,
183
+ lockfileDir: node.path,
184
+ depth,
185
+ }, ctx.walk);
186
+ const linkedTree = linkedTrees[node.path];
187
+ const dependencies = DEPENDENCIES_FIELDS.flatMap((field) => linkedTree[field] ?? []);
188
+ ctx.walk.expanded.set(key, countNodes(dependencies));
189
+ return keepSearched(dependencies.length > 0
190
+ ? { ...node, dependencies: rewriteLinkVersions(dependencies, node.path, ctx.rewriteLinkVersionDir) }
191
+ : node, ctx);
192
+ }
193
+ function countNodes(nodes) {
194
+ return nodes.reduce((count, node) => count + 1 + countNodes(node.dependencies ?? []), 0);
195
+ }
196
+ function keepSearched(node, ctx) {
197
+ return ctx.treeOpts.search == null || node.searched || node.dependencies?.length ? node : undefined;
198
+ }
199
+ /**
200
+ * Rebases the `link:` versions of a linked project's dependencies, which are
201
+ * relative to `linkedProjectDir`, onto `rewriteLinkVersionDir`.
202
+ */
203
+ function rewriteLinkVersions(nodes, linkedProjectDir, rewriteLinkVersionDir) {
204
+ return nodes.map((node) => ({
205
+ ...node,
206
+ version: node.version.startsWith('link:')
207
+ ? `link:${normalizePath(path.relative(rewriteLinkVersionDir, path.resolve(linkedProjectDir, node.version.slice('link:'.length))))}`
208
+ : node.version,
209
+ ...(node.dependencies && { dependencies: rewriteLinkVersions(node.dependencies, linkedProjectDir, rewriteLinkVersionDir) }),
210
+ }));
211
+ }
103
212
  async function dependenciesHierarchyForPackage(opts, projectPath) {
104
213
  const { currentLockfile, wantedLockfile } = opts;
105
214
  const importerId = getLockfileImporterId(opts.lockfileDir, projectPath);
@@ -144,7 +253,8 @@ async function dependenciesHierarchyForPackage(opts, projectPath) {
144
253
  // Handle unsaved dependencies (packages in node_modules but not in lockfile).
145
254
  // When searching, unsaved deps are irrelevant — they aren't in the lockfile
146
255
  // graph and can't have dependency subtrees showing paths to the search target.
147
- if (!opts.search) {
256
+ // They aren't workspace projects either, which is all onlyProjects lists.
257
+ if (!opts.search && !opts.onlyProjects) {
148
258
  const savedDeps = getAllDirectDependencies(currentLockfile.importers[importerId]);
149
259
  const unsavedDeps = ((await readModulesDir(modulesDir)) ?? []).filter((directDep) => !savedDeps[directDep]);
150
260
  if (unsavedDeps.length > 0)
@@ -1,6 +1,7 @@
1
1
  import type { PackageSnapshots, ProjectSnapshot } from '@pnpm/lockfile.fs';
2
+ import { type PeerSatisfactionEdges } from '@pnpm/lockfile.peer-edges';
2
3
  import { type TreeNodeId } from './TreeNodeId.js';
3
- interface DependencyEdge {
4
+ export interface DependencyEdge {
4
5
  alias: string;
5
6
  ref: string;
6
7
  target?: {
@@ -26,5 +27,15 @@ export declare function buildDependencyGraph(rootIds: TreeNodeId[], opts: {
26
27
  };
27
28
  lockfileDir: string;
28
29
  onlyProjects?: boolean;
30
+ /** Edges left out of the graph, from `getPeerSatisfactionEdgesToSkip`. */
31
+ peerSatisfactionEdges?: PeerSatisfactionEdges;
29
32
  }): DependencyGraph;
33
+ /**
34
+ * Whether an edge may lead to a project. Besides the importers of the lockfile,
35
+ * this includes a project's `link:` dependency on a directory outside the
36
+ * lockfile: with a dedicated lockfile per project, every other workspace
37
+ * project is such a directory. `buildDependenciesTree` keeps those only when
38
+ * the directory is a workspace project.
39
+ */
40
+ export declare function isProjectEdge(parentId: TreeNodeId, edge: DependencyEdge): boolean;
30
41
  export {};
@@ -1,3 +1,4 @@
1
+ import { isPeerSatisfactionEdge } from '@pnpm/lockfile.peer-edges';
1
2
  import { getTreeNodeChildId } from './getTreeNodeChildId.js';
2
3
  import { serializeTreeNodeId } from './TreeNodeId.js';
3
4
  export function buildDependencyGraph(rootIds, opts) {
@@ -22,7 +23,7 @@ export function buildDependencyGraph(rootIds, opts) {
22
23
  ? {
23
24
  ...(opts.include.dependencies !== false ? snapshot.dependencies : undefined),
24
25
  ...(opts.include.devDependencies !== false ? snapshot.devDependencies : undefined),
25
- ...(opts.include.optionalDependencies ? snapshot.optionalDependencies : undefined),
26
+ ...(opts.include.dependencies !== false && opts.include.optionalDependencies ? snapshot.optionalDependencies : undefined),
26
27
  }
27
28
  : !opts.include.optionalDependencies
28
29
  ? snapshot.dependencies
@@ -43,20 +44,22 @@ export function buildDependencyGraph(rootIds, opts) {
43
44
  : rawRef?.version;
44
45
  if (ref == null)
45
46
  continue;
47
+ if (nodeId.type === 'package' && isPeerSatisfactionEdge(opts.peerSatisfactionEdges, nodeId.depPath, alias))
48
+ continue;
46
49
  const targetNodeId = getTreeNodeChildId({
47
50
  parentId: nodeId,
48
51
  dep: { alias, ref },
49
52
  lockfileDir: opts.lockfileDir,
50
53
  importers: opts.importers,
51
54
  });
52
- // When onlyProjects is true, only follow edges to workspace importers
53
- if (opts.onlyProjects && targetNodeId?.type !== 'importer') {
54
- continue;
55
- }
56
55
  const target = targetNodeId != null
57
56
  ? { id: serializeTreeNodeId(targetNodeId), nodeId: targetNodeId }
58
57
  : undefined;
59
- edges.push({ alias, ref, target });
58
+ const edge = { alias, ref, target };
59
+ if (opts.onlyProjects && !isProjectEdge(nodeId, edge)) {
60
+ continue;
61
+ }
62
+ edges.push(edge);
60
63
  if (target && !visited.has(target.id)) {
61
64
  queue.push(target.nodeId);
62
65
  }
@@ -66,6 +69,18 @@ export function buildDependencyGraph(rootIds, opts) {
66
69
  }
67
70
  return graph;
68
71
  }
72
+ /**
73
+ * Whether an edge may lead to a project. Besides the importers of the lockfile,
74
+ * this includes a project's `link:` dependency on a directory outside the
75
+ * lockfile: with a dedicated lockfile per project, every other workspace
76
+ * project is such a directory. `buildDependenciesTree` keeps those only when
77
+ * the directory is a workspace project.
78
+ */
79
+ export function isProjectEdge(parentId, edge) {
80
+ if (edge.target != null)
81
+ return edge.target.nodeId.type === 'importer';
82
+ return parentId.type === 'importer' && edge.ref.startsWith('link:');
83
+ }
69
84
  function getSnapshot(treeNodeId, opts) {
70
85
  switch (treeNodeId.type) {
71
86
  case 'importer':
@@ -44,4 +44,5 @@ export declare function buildDependentsTree(packages: string[], projectPaths: st
44
44
  version: string;
45
45
  manifest: DependencyManifest;
46
46
  }) => string | undefined;
47
+ resolvePeersFromWorkspaceRoot?: boolean;
47
48
  }): Promise<DependentsTree[]>;
@@ -2,6 +2,7 @@ import path from 'node:path';
2
2
  import { normalizeRegistriesByScope } from '@pnpm/config.normalize-registries';
3
3
  import { readModulesManifest } from '@pnpm/installing.modules-yaml';
4
4
  import { getLockfileImporterId, } from '@pnpm/lockfile.fs';
5
+ import { getPeerSatisfactionEdgesToSkip } from '@pnpm/lockfile.peer-edges';
5
6
  import { nameVerFromPkgSnapshot } from '@pnpm/lockfile.utils';
6
7
  import { StoreIndex } from '@pnpm/store.index';
7
8
  import { lexCompare } from '@pnpm/text.ordinal-comparator';
@@ -37,6 +38,10 @@ export async function buildDependentsTree(packages, projectPaths, opts) {
37
38
  importers: opts.lockfile.importers,
38
39
  include,
39
40
  lockfileDir: opts.lockfileDir,
41
+ peerSatisfactionEdges: getPeerSatisfactionEdgesToSkip(opts.lockfile, {
42
+ include,
43
+ resolvePeersFromWorkspaceRoot: opts.resolvePeersFromWorkspaceRoot,
44
+ }),
40
45
  });
41
46
  const reverseMap = invertGraph(graph);
42
47
  const search = createPackagesSearcher(packages, opts.finders);
@@ -17,7 +17,9 @@ export interface GetPkgInfoOpts {
17
17
  readonly virtualStoreDirMaxLength: number;
18
18
  readonly depTypes: DepTypes;
19
19
  /**
20
- * The base dir if the `ref` argument is a `"link:"` relative path.
20
+ * The base dir if the `ref` argument is a `"link:"` relative path. An
21
+ * absolute `"link:"` path, such as one on another drive on Windows, is used
22
+ * as is.
21
23
  */
22
24
  readonly linkedPathBaseDir: string;
23
25
  /**
package/lib/getPkgInfo.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { refToRelative } from '@pnpm/deps.path';
2
+ import { packageRootLinkTarget, refToRelative } from '@pnpm/deps.path';
3
3
  import { DepType } from '@pnpm/lockfile.detect-dep-types';
4
4
  import { nameVerFromPkgSnapshot, pkgSnapshotToResolution, } from '@pnpm/lockfile.utils';
5
5
  import { readPackageJsonFromDirSync } from '@pnpm/pkg-manifest.reader';
@@ -73,7 +73,7 @@ export function getPkgInfo(opts) {
73
73
  modulesDir: opts.modulesDir,
74
74
  parentDir: opts.parentDir,
75
75
  })
76
- : path.join(opts.linkedPathBaseDir, opts.ref.slice(5));
76
+ : resolveLinkedPath(opts);
77
77
  if (version.startsWith('link:') && opts.rewriteLinkVersionDir) {
78
78
  version = `link:${normalizePath(path.relative(opts.rewriteLinkVersionDir, fullPackagePath))}`;
79
79
  }
@@ -110,4 +110,10 @@ export function getPkgInfo(opts) {
110
110
  },
111
111
  };
112
112
  }
113
+ function resolveLinkedPath(opts) {
114
+ const target = packageRootLinkTarget(opts.ref);
115
+ return target != null && opts.parentDir != null
116
+ ? path.join(opts.parentDir, target)
117
+ : path.resolve(opts.linkedPathBaseDir, opts.ref.slice(5));
118
+ }
113
119
  //# sourceMappingURL=getPkgInfo.js.map
package/lib/getTree.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { DepTypes } from '@pnpm/lockfile.detect-dep-types';
2
2
  import type { PackageSnapshots, ProjectSnapshot } from '@pnpm/lockfile.fs';
3
3
  import type { StoreIndex } from '@pnpm/store.index';
4
4
  import type { Finder, RegistriesByScope } from '@pnpm/types';
5
- import type { DependencyGraph } from './buildDependencyGraph.js';
5
+ import { type DependencyGraph } from './buildDependencyGraph.js';
6
6
  import type { DependencyNode } from './DependencyNode.js';
7
7
  import { type TreeNodeId } from './TreeNodeId.js';
8
8
  export interface BaseTreeOpts {
package/lib/getTree.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { lexCompare } from '@pnpm/text.ordinal-comparator';
3
+ import { isProjectEdge } from './buildDependencyGraph.js';
3
4
  import { getPkgInfo } from './getPkgInfo.js';
4
5
  import { peersSuffixHashFromDepPath } from './peersSuffixHash.js';
5
6
  import { serializeTreeNodeId } from './TreeNodeId.js';
@@ -65,7 +66,7 @@ function materializeChildren(ctx, parentId, maxDepth, parentDir) {
65
66
  // the alphabetically-first dependency always gets fully expanded.
66
67
  const sortedEdges = [...graphNode.edges].sort((a, b) => lexCompare(a.alias, b.alias));
67
68
  for (const edge of sortedEdges) {
68
- if (ctx.onlyProjects && edge.target?.nodeId.type !== 'importer') {
69
+ if (ctx.onlyProjects && !isProjectEdge(parentId, edge)) {
69
70
  continue;
70
71
  }
71
72
  const { pkgInfo: packageInfo, readManifest } = getPkgInfo({
@@ -76,6 +77,11 @@ function materializeChildren(ctx, parentId, maxDepth, parentDir) {
76
77
  linkedPathBaseDir,
77
78
  parentDir,
78
79
  });
80
+ // A project linked through its publish directory is listed at its own
81
+ // directory.
82
+ if (ctx.onlyProjects && edge.target?.nodeId.type === 'importer') {
83
+ packageInfo.path = path.join(ctx.lockfileDir, edge.target.nodeId.importerId);
84
+ }
79
85
  const searchMatch = ctx.search?.({
80
86
  alias: edge.alias,
81
87
  name: packageInfo.name,
@@ -87,8 +93,10 @@ function materializeChildren(ctx, parentId, maxDepth, parentDir) {
87
93
  let dedupedHasSearchMatch = false;
88
94
  let dedupedSearchMessages = [];
89
95
  if (edge.target == null) {
90
- // External link or unresolvable — no traversal possible
91
- if (ctx.search == null || searchMatch) {
96
+ // External link or unresolvable — no traversal possible. With
97
+ // onlyProjects, this is a link to a project outside the lockfile, which
98
+ // buildDependenciesTree walks and prunes for the search afterwards.
99
+ if (ctx.search == null || searchMatch || ctx.onlyProjects) {
92
100
  newEntry = packageInfo;
93
101
  }
94
102
  else {
@@ -18,13 +18,16 @@ export function getTreeNodeChildId(opts) {
18
18
  // importerId is created by reusing the getLockfileImporterId function.
19
19
  const absoluteLinkedPath = path.join(opts.lockfileDir, opts.parentId.importerId, linkValue);
20
20
  const childImporterId = getLockfileImporterId(opts.lockfileDir, absoluteLinkedPath);
21
+ if (opts.importers[childImporterId] != null) {
22
+ return { type: 'importer', importerId: childImporterId };
23
+ }
21
24
  // A 'link:' reference may refer to a package outside of the pnpm workspace.
22
25
  // Return undefined in that case since it would be difficult to list/traverse
23
26
  // that package outside of the pnpm workspace.
24
- const isLinkOutsideWorkspace = opts.importers[childImporterId] == null;
25
- return isLinkOutsideWorkspace
27
+ const publishingImporterId = getPublishDirectoryImporters(opts.lockfileDir, opts.importers).get(childImporterId);
28
+ return publishingImporterId == null
26
29
  ? undefined
27
- : { type: 'importer', importerId: childImporterId };
30
+ : { type: 'importer', importerId: publishingImporterId };
28
31
  }
29
32
  case 'package':
30
33
  // In theory an external package could be overridden to link to a
@@ -33,4 +36,24 @@ export function getTreeNodeChildId(opts) {
33
36
  return undefined;
34
37
  }
35
38
  }
39
+ const publishDirectoryImportersCache = new WeakMap();
40
+ /**
41
+ * The importer each publish directory belongs to, keyed like an importer id.
42
+ * Dependents link a project with `publishConfig.directory` there unless
43
+ * `publishConfig.linkDirectory` is false.
44
+ */
45
+ function getPublishDirectoryImporters(lockfileDir, importers) {
46
+ let publishDirectoryImporters = publishDirectoryImportersCache.get(importers);
47
+ if (publishDirectoryImporters == null) {
48
+ publishDirectoryImporters = new Map();
49
+ for (const [importerId, { publishDirectory, linkDirectory }] of Object.entries(importers)) {
50
+ if (publishDirectory != null && linkDirectory !== false) {
51
+ const linkedImporterId = getLockfileImporterId(lockfileDir, path.join(lockfileDir, importerId, publishDirectory));
52
+ publishDirectoryImporters.set(linkedImporterId, importerId);
53
+ }
54
+ }
55
+ publishDirectoryImportersCache.set(importers, publishDirectoryImporters);
56
+ }
57
+ return publishDirectoryImporters;
58
+ }
36
59
  //# sourceMappingURL=getTreeNodeChildId.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/deps.inspection.tree-builder",
3
- "version": "1101.0.5",
3
+ "version": "1101.0.7",
4
4
  "description": "Creates a dependencies hierarchy for a symlinked `node_modules`",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -31,19 +31,20 @@
31
31
  ],
32
32
  "dependencies": {
33
33
  "@pnpm/config.matcher": "1100.0.2",
34
- "@pnpm/config.normalize-registries": "1101.0.1",
35
- "@pnpm/deps.path": "1101.0.3",
36
- "@pnpm/fs.read-modules-dir": "1100.0.2",
37
- "@pnpm/installing.modules-yaml": "1101.0.3",
38
- "@pnpm/lockfile.detect-dep-types": "1100.0.24",
39
- "@pnpm/lockfile.fs": "1100.2.8",
40
- "@pnpm/lockfile.utils": "1102.1.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.10",
40
+ "@pnpm/lockfile.peer-edges": "1100.0.1",
41
+ "@pnpm/lockfile.utils": "1102.1.5",
41
42
  "@pnpm/npm-package-arg": "^2.0.0",
42
- "@pnpm/pkg-manifest.reader": "1100.0.19",
43
- "@pnpm/store.cafs": "1100.3.3",
44
- "@pnpm/store.index": "1100.3.1",
43
+ "@pnpm/pkg-manifest.reader": "1100.0.21",
44
+ "@pnpm/store.cafs": "1100.3.5",
45
+ "@pnpm/store.index": "1100.3.3",
45
46
  "@pnpm/text.ordinal-comparator": "1100.0.0",
46
- "@pnpm/types": "1102.1.0",
47
+ "@pnpm/types": "1102.1.1",
47
48
  "load-json-file": "^7.0.1",
48
49
  "normalize-path": "^3.0.0",
49
50
  "p-limit": "^7.3.3",
@@ -55,7 +56,7 @@
55
56
  "devDependencies": {
56
57
  "@jest/globals": "30.4.1",
57
58
  "@pnpm/constants": "1102.0.0",
58
- "@pnpm/deps.inspection.tree-builder": "1101.0.5",
59
+ "@pnpm/deps.inspection.tree-builder": "1101.0.7",
59
60
  "@pnpm/test-fixtures": "1100.0.1",
60
61
  "@types/normalize-path": "^3.0.2",
61
62
  "@types/semver": "7.8.0"