@pnpm/installing.deps-resolver 1100.2.6 → 1100.2.8

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.
@@ -1,9 +1,14 @@
1
1
  import type { PreferredVersions } from '@pnpm/resolving.resolver-base';
2
- import type { PkgAddressOrLink } from './resolveDependencies.js';
2
+ /** One workspace-root dependency that a missing peer can be satisfied with. */
3
+ export interface HoistableRootDep {
4
+ alias: string;
5
+ pkgName: string;
6
+ normalizedBareSpecifier?: string;
7
+ }
3
8
  export declare function hoistPeers(opts: {
4
9
  autoInstallPeers: boolean;
5
10
  allPreferredVersions?: PreferredVersions;
6
- workspaceRootDeps: PkgAddressOrLink[];
11
+ workspaceRootDeps: HoistableRootDep[];
7
12
  }, missingRequiredPeers: Array<[string, {
8
13
  range: string;
9
14
  }]>): Record<string, string>;
package/lib/hoistPeers.js CHANGED
@@ -9,7 +9,7 @@ export function hoistPeers(opts, missingRequiredPeers) {
9
9
  continue;
10
10
  }
11
11
  const rootDep = opts.workspaceRootDeps
12
- .filter((rootDep) => rootDep.pkg.name === peerName)
12
+ .filter((rootDep) => rootDep.pkgName === peerName)
13
13
  .sort((rootDep1, rootDep2) => lexCompare(rootDep1.alias, rootDep2.alias))[0];
14
14
  if (rootDep?.normalizedBareSpecifier) {
15
15
  dependencies[peerName] = rootDep.normalizedBareSpecifier;
@@ -27,22 +27,28 @@ export function hoistPeers(opts, missingRequiredPeers) {
27
27
  nonVersions.push(spec);
28
28
  }
29
29
  }
30
- // When the range is an exact version (e.g. pinned by an override like "4.3.0"),
31
- // try to find a preferred version that satisfies it. This prevents a stale
32
- // higher version from the lockfile being picked over the overridden version.
33
- // For regular semver ranges (e.g. "^1.0.0"), use the highest preferred
34
- // version for deduplication.
35
- const isExactVersion = semver.valid(range) != null;
36
- const satisfyingVersion = isExactVersion
30
+ // Dedupe onto a preferred version only when it actually satisfies the
31
+ // wanted peer range. Picking the highest preferred version regardless of
32
+ // the range lets a version resolved for one importer be auto-installed as
33
+ // another importer's peer even though nothing in that importer's closure
34
+ // accepts it, silently producing a peer graph that mixes incompatible
35
+ // majors. Ranges that are not semver (workspace:, npm: aliases, dist-tags)
36
+ // cannot be checked, so they keep the dedupe-to-highest behavior.
37
+ const isSemverRange = semver.validRange(range, { includePrerelease: true }) != null;
38
+ const satisfyingVersion = isSemverRange
37
39
  ? semver.maxSatisfying(versions, range, { includePrerelease: true })
38
40
  : null;
39
41
  if (satisfyingVersion) {
40
42
  dependencies[peerName] = [satisfyingVersion, ...nonVersions].join(' || ');
41
43
  }
42
- else if (isExactVersion && opts.autoInstallPeers) {
43
- // No preferred version satisfies the exact override version.
44
- // Use the range directly so pnpm resolves it from the registry.
45
- dependencies[peerName] = range;
44
+ else if (isSemverRange && versions.length > 0) {
45
+ // Preferred versions exist but none satisfies the wanted range.
46
+ // Use the range directly so pnpm resolves it from the registry rather
47
+ // than installing a version the peer explicitly rejects. Without
48
+ // autoInstallPeers, hoist nothing and leave the peer missing.
49
+ if (opts.autoInstallPeers) {
50
+ dependencies[peerName] = range;
51
+ }
46
52
  }
47
53
  else {
48
54
  dependencies[peerName] = [semver.maxSatisfying(versions, '*', { includePrerelease: true }), ...nonVersions]
package/lib/index.js CHANGED
@@ -3,6 +3,7 @@ import { packageManifestLogger, } from '@pnpm/core-loggers';
3
3
  import { findRuntimeNodeVersion, iterateHashedGraphNodes } from '@pnpm/deps.graph-hasher';
4
4
  import { isRuntimeDepPath } from '@pnpm/deps.path';
5
5
  import { PnpmError } from '@pnpm/error';
6
+ import { safeJoinModulesDir } from '@pnpm/fs.symlink-dependency';
6
7
  import { verifyPatches } from '@pnpm/patching.config';
7
8
  import { safeReadPackageJsonFromDir } from '@pnpm/pkg-manifest.reader';
8
9
  import { getAllDependenciesFromManifest, getSpecFromPackageManifest, } from '@pnpm/pkg-manifest.utils';
@@ -90,6 +91,7 @@ export async function resolveDependencies(importers, opts) {
90
91
  ...project.wantedDependencies.flatMap(({ alias, isNew }) => isNew && alias != null ? [alias] : []),
91
92
  ]),
92
93
  directNodeIdsByAlias: resolvedImporter.directNodeIdsByAlias,
94
+ hoistedPeerProviderNodeIds: resolvedImporter.hoistedPeerProviderNodeIds,
93
95
  explicitlyRequestedDirectDependencies: new Set(project.wantedDependencies.flatMap(({ alias, bareSpecifier, isNew, prevSpecifier, updateSpec }) => alias != null && (isNew === true || updateSpec === true || (prevSpecifier != null && bareSpecifier !== prevSpecifier))
94
96
  ? [alias]
95
97
  : [])),
@@ -403,7 +405,7 @@ function extendGraph(graph, opts) {
403
405
  const node = graph[depPath];
404
406
  Object.assign(node, {
405
407
  modules,
406
- dir: path.join(modules, node.name),
408
+ dir: safeJoinModulesDir(modules, node.name),
407
409
  });
408
410
  }
409
411
  return graph;
@@ -191,6 +191,13 @@ export interface PkgAddress extends PkgAddressOrLinkBase {
191
191
  saveCatalogName?: string;
192
192
  lockedPeerContext?: LockedPeerContext;
193
193
  previousDepPath?: DepPath;
194
+ /**
195
+ * A peer dependency provider attached to the root importer so that other
196
+ * subtrees can reuse it. Its `nodeId` keeps pointing at the provider's
197
+ * original position inside the dependency tree, so the node must be
198
+ * peer-resolved there — not in the root context.
199
+ */
200
+ hoistedPeerProvider?: boolean;
194
201
  }
195
202
  export type PkgAddressOrLink = PkgAddress | LinkedDependency;
196
203
  export interface PeerDependency {
@@ -24,6 +24,7 @@ import { safeIntersect } from './mergePeers.js';
24
24
  import { nextNodeId } from './nextNodeId.js';
25
25
  import { parentIdsContainSequence } from './parentIdsContainSequence.js';
26
26
  import { replaceVersionInBareSpecifier } from './replaceVersionInBareSpecifier.js';
27
+ import { unwrapPackageName } from './unwrapPackageName.js';
27
28
  import { wantedDepIsLocallyAvailable } from './wantedDepIsLocallyAvailable.js';
28
29
  const dependencyResolvedLogger = logger('_dependency_resolved');
29
30
  const omitDepsFields = omit(['dependencies', 'optionalDependencies', 'peerDependencies', 'peerDependenciesMeta']);
@@ -53,7 +54,7 @@ export async function resolveRootDependencies(ctx, importers) {
53
54
  let workspaceRootDeps;
54
55
  if (ctx.resolvePeersFromWorkspaceRoot) {
55
56
  const rootImporterIndex = importers.findIndex(({ options }) => options.parentIds[0] === '.');
56
- workspaceRootDeps = pkgAddressesByImportersWithoutPeers[rootImporterIndex]?.pkgAddresses ?? [];
57
+ workspaceRootDeps = getHoistableRootDeps(importers[rootImporterIndex], pkgAddressesByImportersWithoutPeers[rootImporterIndex]?.pkgAddresses ?? []);
57
58
  }
58
59
  else {
59
60
  workspaceRootDeps = [];
@@ -89,7 +90,10 @@ export async function resolveRootDependencies(ctx, importers) {
89
90
  // even those peers should be hoisted that are not autoinstalled
90
91
  for (const [resolvedPeerName, resolvedPeerAddress] of Object.entries(importerResolutionResult.resolvedPeers ?? {})) {
91
92
  if (!parentPkgAliases[resolvedPeerName]) {
92
- importerResolutionResult.pkgAddresses.push(resolvedPeerAddress);
93
+ importerResolutionResult.pkgAddresses.push({
94
+ ...resolvedPeerAddress,
95
+ hoistedPeerProvider: true,
96
+ });
93
97
  }
94
98
  }
95
99
  }
@@ -146,6 +150,38 @@ export async function resolveRootDependencies(ctx, importers) {
146
150
  time,
147
151
  };
148
152
  }
153
+ /**
154
+ * Lists the workspace-root dependencies that `hoistPeers` may satisfy a
155
+ * missing peer with. A root dependency reused from the lockfile skips full
156
+ * resolution, so it has no address (or an address without a
157
+ * normalizedBareSpecifier); its wanted specifier is used instead, so that
158
+ * re-resolving with a lockfile hoists the same version as a fresh install of
159
+ * the same manifest.
160
+ */
161
+ function getHoistableRootDeps(rootImporter, rootPkgAddresses) {
162
+ const wantedSpecifierByAlias = new Map();
163
+ for (const wantedDep of rootImporter?.wantedDependencies ?? []) {
164
+ if (wantedDep.alias && wantedDep.bareSpecifier) {
165
+ wantedSpecifierByAlias.set(wantedDep.alias, wantedDep.bareSpecifier);
166
+ }
167
+ }
168
+ const rootDeps = rootPkgAddresses.map((pkgAddress) => ({
169
+ alias: pkgAddress.alias,
170
+ pkgName: pkgAddress.pkg.name,
171
+ normalizedBareSpecifier: pkgAddress.normalizedBareSpecifier ?? wantedSpecifierByAlias.get(pkgAddress.alias),
172
+ }));
173
+ const coveredAliases = new Set(rootDeps.map(({ alias }) => alias));
174
+ for (const [alias, bareSpecifier] of wantedSpecifierByAlias) {
175
+ if (coveredAliases.has(alias))
176
+ continue;
177
+ rootDeps.push({
178
+ alias,
179
+ pkgName: unwrapPackageName(alias, bareSpecifier).pkgName,
180
+ normalizedBareSpecifier: bareSpecifier,
181
+ });
182
+ }
183
+ return rootDeps;
184
+ }
149
185
  async function resolveDependenciesOfImporters(ctx, importers) {
150
186
  const pickLowestVersion = ctx.resolutionMode === 'time-based' || ctx.resolutionMode === 'lowest-direct';
151
187
  const resolveResults = await Promise.all(importers.map(async (importer) => {
@@ -1146,14 +1182,21 @@ async function resolveDependency(wantedDependency, ctx, options) {
1146
1182
  version: wantedDependency.alias ? wantedDependency.bareSpecifier : undefined,
1147
1183
  };
1148
1184
  if (wantedDependency.optional && err.code !== 'ERR_PNPM_TRUST_DOWNGRADE') {
1149
- skippedOptionalDependencyLogger.debug({
1150
- details: err.toString(),
1151
- package: wantedDependencyDetails,
1152
- parents: getPkgsInfoFromIds(options.parentIds, ctx.resolvedPkgsById),
1153
- prefix: options.prefix,
1154
- reason: 'resolution_failure',
1155
- });
1156
- return null;
1185
+ if (!wantedLockfileContainsSatisfyingEntry(ctx.wantedLockfile, wantedDependency)) {
1186
+ skippedOptionalDependencyLogger.debug({
1187
+ details: err.toString(),
1188
+ package: wantedDependencyDetails,
1189
+ parents: getPkgsInfoFromIds(options.parentIds, ctx.resolvedPkgsById),
1190
+ prefix: options.prefix,
1191
+ reason: 'resolution_failure',
1192
+ });
1193
+ return null;
1194
+ }
1195
+ if (err.hint == null) {
1196
+ err.hint = 'This optional dependency is not skipped, because the lockfile contains a resolution for it. ' +
1197
+ 'Skipping it would remove the locked entries, making the lockfile differ depending on which machine ran the install. ' +
1198
+ 'If the version was intentionally removed from the registry, update the dependent package or remove the entries from the lockfile.';
1199
+ }
1157
1200
  }
1158
1201
  err.package = wantedDependencyDetails;
1159
1202
  err.prefix = options.prefix;
@@ -1417,6 +1460,39 @@ async function resolveDependency(wantedDependency, ctx, options) {
1417
1460
  finishPackageResolution();
1418
1461
  }
1419
1462
  }
1463
+ /**
1464
+ * Whether the wanted lockfile already holds a package entry that satisfies the
1465
+ * wanted dependency. An optional dependency that fails to resolve is normally
1466
+ * skipped, but when a locked resolution exists the failure is environmental
1467
+ * (e.g. a registry mirror that hasn't synced the release yet) rather than a
1468
+ * genuinely uninstallable package. Silently skipping in that case would erase
1469
+ * the locked entries, making the lockfile differ across machines from
1470
+ * identical inputs and leaving frozen installs on other hosts with nothing to
1471
+ * link (https://github.com/pnpm/pnpm/issues/12853).
1472
+ *
1473
+ * Only plain semver specifiers are checked; exotic specifiers (git, catalogs,
1474
+ * tags, URLs) keep the skip-on-failure behavior.
1475
+ *
1476
+ * The check is deliberately by package name and range rather than by the
1477
+ * current edge's locked dep path. `pnpm dedupe` — the flow where the erasure
1478
+ * bites — clears every per-snapshot dependency map before resolving
1479
+ * (`forgetResolutionsOfAllPrevWantedDeps`), so on this code path no edge-level
1480
+ * lockfile linkage exists to key on; only the package entries survive. A
1481
+ * satisfying entry locked via any edge also means the registry served this
1482
+ * package in-range before, so failing loudly instead of skipping is the right
1483
+ * outcome even when the failing edge itself was never locked.
1484
+ */
1485
+ function wantedLockfileContainsSatisfyingEntry(lockfile, wantedDependency) {
1486
+ if (!wantedDependency.alias)
1487
+ return false;
1488
+ const { pkgName, bareSpecifier } = unwrapPackageName(wantedDependency.alias, wantedDependency.bareSpecifier);
1489
+ if (semver.validRange(bareSpecifier) == null)
1490
+ return false;
1491
+ return Object.keys(lockfile.packages ?? {}).some((depPath) => {
1492
+ const parsed = dp.parse(depPath);
1493
+ return parsed.name === pkgName && parsed.version != null && semver.satisfies(parsed.version, bareSpecifier);
1494
+ });
1495
+ }
1420
1496
  export function getManifestFromResponse(pkgResponse, wantedDependency, currentPkg) {
1421
1497
  if (pkgResponse.body.manifest)
1422
1498
  return pkgResponse.body.manifest;
@@ -12,6 +12,7 @@ export interface ResolvedImporters {
12
12
  [id: string]: {
13
13
  directDependencies: ResolvedDirectDependency[];
14
14
  directNodeIdsByAlias: Map<string, NodeId>;
15
+ hoistedPeerProviderNodeIds: Set<NodeId>;
15
16
  linkedDependencies: LinkedDependency[];
16
17
  };
17
18
  }
@@ -168,6 +168,7 @@ export async function resolveDependencyTree(importers, opts) {
168
168
  };
169
169
  }),
170
170
  directNodeIdsByAlias: new Map(directNonLinkedDeps.map(({ alias, nodeId }) => [alias, nodeId])),
171
+ hoistedPeerProviderNodeIds: new Set(directNonLinkedDeps.filter((dep) => dep.hoistedPeerProvider).map(({ nodeId }) => nodeId)),
171
172
  linkedDependencies,
172
173
  };
173
174
  }
@@ -30,6 +30,7 @@ export interface GenericDependenciesGraphWithResolvedChildren<T extends PartialR
30
30
  }
31
31
  export interface ProjectToResolve {
32
32
  directNodeIdsByAlias: Map<string, NodeId>;
33
+ hoistedPeerProviderNodeIds?: Set<NodeId>;
33
34
  declaredDirectDependencies?: Set<string>;
34
35
  explicitlyRequestedDirectDependencies?: Set<string>;
35
36
  topParents: Array<{
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { createPeerDepGraphHash, depPathToFilename, parseDepPath } from '@pnpm/deps.path';
3
+ import { safeJoinModulesDir } from '@pnpm/fs.symlink-dependency';
3
4
  import * as semverUtils from '@yarnpkg/core/semverUtils';
4
5
  import { analyzeGraph } from 'graph-cycles';
5
6
  import pDefer, {} from 'p-defer';
@@ -12,6 +13,9 @@ export async function resolvePeers(opts) {
12
13
  const depGraph = {};
13
14
  const pathsByNodeId = new Map();
14
15
  const pathsByNodeIdPromises = new Map();
16
+ const awaitedPeerNodeIdsByNodeId = new Map();
17
+ const peersCacheOwnerByNodeId = new Map();
18
+ const cycleBrokenNodeIds = new Set();
15
19
  const depPathsByPkgId = new Map();
16
20
  const nodeIdsByPreviousDepPath = opts.resolvedPeerProviderPaths == null
17
21
  ? new Map()
@@ -25,7 +29,7 @@ export async function resolvePeers(opts) {
25
29
  const finishingList = [];
26
30
  const peersCache = new Map();
27
31
  const purePkgs = new Set();
28
- for (const { directNodeIdsByAlias, declaredDirectDependencies, explicitlyRequestedDirectDependencies, topParents, rootDir, id } of opts.projects) {
32
+ for (const { directNodeIdsByAlias, hoistedPeerProviderNodeIds, declaredDirectDependencies, explicitlyRequestedDirectDependencies, topParents, rootDir, id } of opts.projects) {
29
33
  const currentProviderSources = [{
30
34
  directNodeIdsByAlias,
31
35
  declaredDirectDependencies: declaredDirectDependencies ?? new Set(),
@@ -48,10 +52,26 @@ export async function resolvePeers(opts) {
48
52
  pathsByNodeIdPromises.set(nodeId, pDefer());
49
53
  }
50
54
  }
51
- // eslint-disable-next-line no-await-in-loop
52
- const { finishing } = await resolvePeersOfChildren(Object.fromEntries(directNodeIdsByAlias.entries()), pkgsByName, {
55
+ // Hoisted peer providers stay visible as providers (via pkgsByName) but are
56
+ // not traversed as direct children: their nodeIds point into subtrees, and
57
+ // resolving them a second time in the project's root context would bind
58
+ // their peers to the project's own dependencies instead of the providers
59
+ // next to them in the tree, racing with the in-place resolution on
60
+ // pathsByNodeId and producing peer graphs that mix both contexts.
61
+ const ownDirectChildren = {};
62
+ const hoistedProviderChildren = {};
63
+ for (const [alias, nodeId] of directNodeIdsByAlias.entries()) {
64
+ if (hoistedPeerProviderNodeIds?.has(nodeId)) {
65
+ hoistedProviderChildren[alias] = nodeId;
66
+ }
67
+ else {
68
+ ownDirectChildren[alias] = nodeId;
69
+ }
70
+ }
71
+ const parentPkgsOfNode = new Map();
72
+ const projectPeersContext = {
53
73
  allPeerDepNames: opts.allPeerDepNames,
54
- parentPkgsOfNode: new Map(),
74
+ parentPkgsOfNode,
55
75
  dependenciesTree: opts.dependenciesTree,
56
76
  depGraph,
57
77
  lockfileDir: opts.lockfileDir,
@@ -59,6 +79,9 @@ export async function resolvePeers(opts) {
59
79
  parentDepPathsChain: [],
60
80
  pathsByNodeId,
61
81
  pathsByNodeIdPromises,
82
+ awaitedPeerNodeIdsByNodeId,
83
+ peersCacheOwnerByNodeId,
84
+ cycleBrokenNodeIds,
62
85
  depPathsByPkgId,
63
86
  nodeIdsByPreviousDepPath,
64
87
  resolvedPeerProviderPaths: opts.resolvedPeerProviderPaths,
@@ -71,10 +94,36 @@ export async function resolvePeers(opts) {
71
94
  rootDir,
72
95
  virtualStoreDir: opts.virtualStoreDir,
73
96
  virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
74
- });
97
+ };
98
+ // eslint-disable-next-line no-await-in-loop
99
+ const { finishing } = await resolvePeersOfChildren(ownDirectChildren, pkgsByName, projectPeersContext);
75
100
  if (finishing) {
76
101
  finishingList.push(finishing);
77
102
  }
103
+ // A provider whose tree position was pruned from the traversal (its parent
104
+ // hit peersCache, so its children were never visited) still has consumers
105
+ // awaiting its dep path, so resolve it here as a last resort. Providers
106
+ // visited by the traversal above are recorded in parentPkgsOfNode.
107
+ // All pruned providers go into a single resolvePeersOfChildren call:
108
+ // its cycle analysis only sees the children of one call, and providers
109
+ // frequently peer-depend on each other, so resolving them one by one
110
+ // would leave their dep path calculations awaiting each other forever.
111
+ // A peer cycle that spans this call and the traversal above is still
112
+ // invisible here; breakDepPathAwaitCycles resolves those before the
113
+ // finishing promises are awaited.
114
+ const prunedProviderChildren = {};
115
+ for (const [alias, nodeId] of Object.entries(hoistedProviderChildren)) {
116
+ if (parentPkgsOfNode.has(nodeId))
117
+ continue;
118
+ prunedProviderChildren[alias] = nodeId;
119
+ }
120
+ if (Object.keys(prunedProviderChildren).length > 0) {
121
+ // eslint-disable-next-line no-await-in-loop
122
+ const { finishing } = await resolvePeersOfChildren(prunedProviderChildren, pkgsByName, projectPeersContext);
123
+ if (finishing) {
124
+ finishingList.push(finishing);
125
+ }
126
+ }
78
127
  if (Object.keys(peerDependencyIssues.bad).length > 0 || Object.keys(peerDependencyIssues.missing).length > 0) {
79
128
  peerDependencyIssuesByProjects[id] = {
80
129
  ...peerDependencyIssues,
@@ -82,6 +131,14 @@ export async function resolvePeers(opts) {
82
131
  };
83
132
  }
84
133
  }
134
+ breakDepPathAwaitCycles({
135
+ awaitedPeerNodeIdsByNodeId,
136
+ peersCacheOwnerByNodeId,
137
+ cycleBrokenNodeIds,
138
+ pathsByNodeId,
139
+ pathsByNodeIdPromises,
140
+ dependenciesTree: opts.dependenciesTree,
141
+ });
85
142
  await Promise.all(finishingList);
86
143
  const depGraphWithResolvedChildren = resolveChildren(depGraph);
87
144
  function resolveChildren(depGraph) {
@@ -128,6 +185,62 @@ export async function resolvePeers(opts) {
128
185
  pathsByNodeId,
129
186
  };
130
187
  }
188
+ // Cycle analysis inside resolvePeersOfChildren only sees the children of a
189
+ // single call, but a dep path calculation may await a node resolved in a
190
+ // different call or at a different tree level (a hoisted peer provider and
191
+ // a consumer that peer-depend on each other, https://github.com/pnpm/pnpm/issues/12921).
192
+ // Such an await cycle never settles on its own, so before the finishing
193
+ // promises are awaited, walk the recorded await edges and resolve every
194
+ // dep path promise on a cycle to the peer's `name@version` — the same
195
+ // collapse in-call cycle detection applies (see calculateDepPath).
196
+ // A node that hit the peers cache awaits its dep path from the node that
197
+ // created the cache entry, so it borrows that owner's await edges.
198
+ function breakDepPathAwaitCycles(opts) {
199
+ const isSettled = (nodeId) => opts.pathsByNodeId.has(nodeId) || opts.cycleBrokenNodeIds.has(nodeId);
200
+ const keysByNodeId = new Map();
201
+ const nodeIdsByKey = new Map();
202
+ function keyOf(nodeId) {
203
+ let key = keysByNodeId.get(nodeId);
204
+ if (key == null) {
205
+ key = String(keysByNodeId.size);
206
+ keysByNodeId.set(nodeId, key);
207
+ nodeIdsByKey.set(key, nodeId);
208
+ }
209
+ return key;
210
+ }
211
+ const graphEntries = [];
212
+ const awaitingNodeIds = new Set([
213
+ ...opts.awaitedPeerNodeIdsByNodeId.keys(),
214
+ ...opts.peersCacheOwnerByNodeId.keys(),
215
+ ]);
216
+ for (const nodeId of awaitingNodeIds) {
217
+ if (isSettled(nodeId))
218
+ continue;
219
+ const cacheOwnerNodeId = opts.peersCacheOwnerByNodeId.get(nodeId);
220
+ const awaitedNodeIds = opts.awaitedPeerNodeIdsByNodeId.get(nodeId) ??
221
+ (cacheOwnerNodeId == null ? undefined : opts.awaitedPeerNodeIdsByNodeId.get(cacheOwnerNodeId));
222
+ if (awaitedNodeIds == null)
223
+ continue;
224
+ const liveTargets = [];
225
+ for (const awaitedNodeId of awaitedNodeIds) {
226
+ if (!isSettled(awaitedNodeId)) {
227
+ liveTargets.push(keyOf(awaitedNodeId));
228
+ }
229
+ }
230
+ if (liveTargets.length > 0) {
231
+ graphEntries.push([keyOf(nodeId), liveTargets]);
232
+ }
233
+ }
234
+ if (graphEntries.length === 0)
235
+ return;
236
+ const { cycles } = analyzeGraph(graphEntries);
237
+ for (const key of new Set(cycles.flat())) {
238
+ const nodeId = nodeIdsByKey.get(key);
239
+ const { name, version } = opts.dependenciesTree.get(nodeId).resolvedPackage;
240
+ opts.cycleBrokenNodeIds.add(nodeId);
241
+ opts.pathsByNodeIdPromises.get(nodeId)?.resolve(`${name}@${version}`);
242
+ }
243
+ }
131
244
  function nodeDepsCount(node) {
132
245
  return Object.keys(node.children).length + node.resolvedPeerNames.size;
133
246
  }
@@ -349,6 +462,7 @@ async function resolvePeersOfNode(currentAlias, nodeId, parentParentPkgs, ctx) {
349
462
  wantedRange,
350
463
  });
351
464
  }
465
+ ctx.peersCacheOwnerByNodeId.set(nodeId, hit.ownerNodeId);
352
466
  return {
353
467
  missingPeers: hit.missingPeers,
354
468
  finishing: (async () => {
@@ -411,6 +525,7 @@ async function resolvePeersOfNode(currentAlias, nodeId, parentParentPkgs, ctx) {
411
525
  missingPeers: allMissingPeers,
412
526
  depPath: pDefer(),
413
527
  resolvedPeers: allResolvedPeers,
528
+ ownerNodeId: nodeId,
414
529
  };
415
530
  if (ctx.peersCache.has(resolvedPackage.pkgIdWithPatchHash)) {
416
531
  ctx.peersCache.get(resolvedPackage.pkgIdWithPatchHash).push(cache);
@@ -472,6 +587,7 @@ async function resolvePeersOfNode(currentAlias, nodeId, parentParentPkgs, ctx) {
472
587
  if (cyclicPeerAliases.has(pendingPeer.alias)) {
473
588
  const { name, version } = ctx.dependenciesTree.get(pendingPeer.nodeId)?.resolvedPackage;
474
589
  const id = `${name}@${version}`;
590
+ ctx.cycleBrokenNodeIds.add(pendingPeer.nodeId);
475
591
  ctx.pathsByNodeIdPromises.get(pendingPeer.nodeId)?.resolve(id);
476
592
  return id;
477
593
  }
@@ -481,6 +597,12 @@ async function resolvePeersOfNode(currentAlias, nodeId, parentParentPkgs, ctx) {
481
597
  return { name: peerNode.resolvedPackage.name, version: peerNode.resolvedPackage.version };
482
598
  }
483
599
  }
600
+ let awaitedPeerNodeIds = ctx.awaitedPeerNodeIdsByNodeId.get(nodeId);
601
+ if (awaitedPeerNodeIds == null) {
602
+ awaitedPeerNodeIds = new Set();
603
+ ctx.awaitedPeerNodeIdsByNodeId.set(nodeId, awaitedPeerNodeIds);
604
+ }
605
+ awaitedPeerNodeIds.add(pendingPeer.nodeId);
484
606
  return ctx.pathsByNodeIdPromises.get(pendingPeer.nodeId).promise;
485
607
  })),
486
608
  ], ctx.peersSuffixMaxLength);
@@ -501,7 +623,7 @@ async function resolvePeersOfNode(currentAlias, nodeId, parentParentPkgs, ctx) {
501
623
  const peerDependencies = { ...resolvedPackage.peerDependencies };
502
624
  if (!ctx.depGraph[depPath] || ctx.depGraph[depPath].depth > node.depth) {
503
625
  const modules = path.join(ctx.virtualStoreDir, depPathToFilename(depPath, ctx.virtualStoreDirMaxLength), 'node_modules');
504
- const dir = path.join(modules, resolvedPackage.name);
626
+ const dir = safeJoinModulesDir(modules, resolvedPackage.name);
505
627
  const transitivePeerDependencies = new Set();
506
628
  for (const unknownPeer of allResolvedPeers.keys()) {
507
629
  if (!peerDependencies[unknownPeer]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/installing.deps-resolver",
3
- "version": "1100.2.6",
3
+ "version": "1100.2.8",
4
4
  "description": "Resolves dependency graph of a package",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -27,7 +27,32 @@
27
27
  "!*.map"
28
28
  ],
29
29
  "dependencies": {
30
+ "@pnpm/catalogs.resolver": "1100.0.0",
31
+ "@pnpm/catalogs.types": "1100.0.0",
32
+ "@pnpm/config.version-policy": "1100.1.6",
33
+ "@pnpm/constants": "1100.0.0",
34
+ "@pnpm/core-loggers": "1100.2.1",
35
+ "@pnpm/deps.graph-hasher": "1100.2.9",
36
+ "@pnpm/deps.path": "1100.0.8",
37
+ "@pnpm/deps.peer-range": "1100.0.2",
38
+ "@pnpm/error": "1100.0.1",
39
+ "@pnpm/fetching.pick-fetcher": "1100.1.0",
40
+ "@pnpm/fs.symlink-dependency": "1100.0.10",
41
+ "@pnpm/hooks.types": "1100.2.0",
42
+ "@pnpm/lockfile.preferred-versions": "1100.0.19",
43
+ "@pnpm/lockfile.pruner": "1100.0.13",
44
+ "@pnpm/lockfile.types": "1100.0.13",
45
+ "@pnpm/lockfile.utils": "1100.1.2",
46
+ "@pnpm/patching.config": "1100.0.9",
47
+ "@pnpm/patching.types": "1100.0.0",
48
+ "@pnpm/pkg-manifest.reader": "1100.0.9",
49
+ "@pnpm/pkg-manifest.utils": "1100.2.6",
50
+ "@pnpm/resolving.npm-resolver": "1102.1.2",
51
+ "@pnpm/resolving.resolver-base": "1100.5.1",
52
+ "@pnpm/store.controller-types": "1100.1.7",
53
+ "@pnpm/types": "1101.3.2",
30
54
  "@pnpm/util.lex-comparator": "^4.0.1",
55
+ "@pnpm/workspace.spec-parser": "1100.0.0",
31
56
  "@yarnpkg/core": "4.8.0",
32
57
  "graph-cycles": "3.0.0",
33
58
  "is-inner-link": "^5.0.0",
@@ -42,43 +67,19 @@
42
67
  "semver": "^7.8.4",
43
68
  "semver-range-intersect": "^0.3.1",
44
69
  "validate-npm-package-name": "7.0.2",
45
- "version-selector-type": "^3.0.0",
46
- "@pnpm/catalogs.resolver": "1100.0.0",
47
- "@pnpm/config.version-policy": "1100.1.6",
48
- "@pnpm/catalogs.types": "1100.0.0",
49
- "@pnpm/constants": "1100.0.0",
50
- "@pnpm/core-loggers": "1100.2.1",
51
- "@pnpm/deps.graph-hasher": "1100.2.7",
52
- "@pnpm/deps.peer-range": "1100.0.2",
53
- "@pnpm/deps.path": "1100.0.8",
54
- "@pnpm/error": "1100.0.1",
55
- "@pnpm/hooks.types": "1100.1.1",
56
- "@pnpm/fetching.pick-fetcher": "1100.0.14",
57
- "@pnpm/lockfile.preferred-versions": "1100.0.18",
58
- "@pnpm/lockfile.pruner": "1100.0.13",
59
- "@pnpm/lockfile.types": "1100.0.13",
60
- "@pnpm/lockfile.utils": "1100.1.1",
61
- "@pnpm/patching.config": "1100.0.9",
62
- "@pnpm/patching.types": "1100.0.0",
63
- "@pnpm/resolving.npm-resolver": "1102.1.1",
64
- "@pnpm/pkg-manifest.reader": "1100.0.9",
65
- "@pnpm/resolving.resolver-base": "1100.5.1",
66
- "@pnpm/pkg-manifest.utils": "1100.2.6",
67
- "@pnpm/types": "1101.3.2",
68
- "@pnpm/store.controller-types": "1100.1.7",
69
- "@pnpm/workspace.spec-parser": "1100.0.0"
70
+ "version-selector-type": "^3.0.0"
70
71
  },
71
72
  "peerDependencies": {
72
73
  "@pnpm/logger": "^1100.0.0"
73
74
  },
74
75
  "devDependencies": {
75
76
  "@jest/globals": "30.4.1",
77
+ "@pnpm/installing.deps-resolver": "1100.2.8",
78
+ "@pnpm/logger": "1100.0.0",
76
79
  "@types/normalize-path": "^3.0.2",
77
80
  "@types/ramda": "0.31.1",
78
81
  "@types/semver": "7.7.1",
79
- "@types/validate-npm-package-name": "^4.0.2",
80
- "@pnpm/logger": "1100.0.0",
81
- "@pnpm/installing.deps-resolver": "1100.2.6"
82
+ "@types/validate-npm-package-name": "^4.0.2"
82
83
  },
83
84
  "engines": {
84
85
  "node": ">=22.13"