@pnpm/installing.deps-resolver 1100.2.7 → 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]
@@ -54,7 +54,7 @@ export async function resolveRootDependencies(ctx, importers) {
54
54
  let workspaceRootDeps;
55
55
  if (ctx.resolvePeersFromWorkspaceRoot) {
56
56
  const rootImporterIndex = importers.findIndex(({ options }) => options.parentIds[0] === '.');
57
- workspaceRootDeps = pkgAddressesByImportersWithoutPeers[rootImporterIndex]?.pkgAddresses ?? [];
57
+ workspaceRootDeps = getHoistableRootDeps(importers[rootImporterIndex], pkgAddressesByImportersWithoutPeers[rootImporterIndex]?.pkgAddresses ?? []);
58
58
  }
59
59
  else {
60
60
  workspaceRootDeps = [];
@@ -150,6 +150,38 @@ export async function resolveRootDependencies(ctx, importers) {
150
150
  time,
151
151
  };
152
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
+ }
153
185
  async function resolveDependenciesOfImporters(ctx, importers) {
154
186
  const pickLowestVersion = ctx.resolutionMode === 'time-based' || ctx.resolutionMode === 'lowest-direct';
155
187
  const resolveResults = await Promise.all(importers.map(async (importer) => {
@@ -13,6 +13,9 @@ export async function resolvePeers(opts) {
13
13
  const depGraph = {};
14
14
  const pathsByNodeId = new Map();
15
15
  const pathsByNodeIdPromises = new Map();
16
+ const awaitedPeerNodeIdsByNodeId = new Map();
17
+ const peersCacheOwnerByNodeId = new Map();
18
+ const cycleBrokenNodeIds = new Set();
16
19
  const depPathsByPkgId = new Map();
17
20
  const nodeIdsByPreviousDepPath = opts.resolvedPeerProviderPaths == null
18
21
  ? new Map()
@@ -76,6 +79,9 @@ export async function resolvePeers(opts) {
76
79
  parentDepPathsChain: [],
77
80
  pathsByNodeId,
78
81
  pathsByNodeIdPromises,
82
+ awaitedPeerNodeIdsByNodeId,
83
+ peersCacheOwnerByNodeId,
84
+ cycleBrokenNodeIds,
79
85
  depPathsByPkgId,
80
86
  nodeIdsByPreviousDepPath,
81
87
  resolvedPeerProviderPaths: opts.resolvedPeerProviderPaths,
@@ -102,6 +108,9 @@ export async function resolvePeers(opts) {
102
108
  // its cycle analysis only sees the children of one call, and providers
103
109
  // frequently peer-depend on each other, so resolving them one by one
104
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.
105
114
  const prunedProviderChildren = {};
106
115
  for (const [alias, nodeId] of Object.entries(hoistedProviderChildren)) {
107
116
  if (parentPkgsOfNode.has(nodeId))
@@ -122,6 +131,14 @@ export async function resolvePeers(opts) {
122
131
  };
123
132
  }
124
133
  }
134
+ breakDepPathAwaitCycles({
135
+ awaitedPeerNodeIdsByNodeId,
136
+ peersCacheOwnerByNodeId,
137
+ cycleBrokenNodeIds,
138
+ pathsByNodeId,
139
+ pathsByNodeIdPromises,
140
+ dependenciesTree: opts.dependenciesTree,
141
+ });
125
142
  await Promise.all(finishingList);
126
143
  const depGraphWithResolvedChildren = resolveChildren(depGraph);
127
144
  function resolveChildren(depGraph) {
@@ -168,6 +185,62 @@ export async function resolvePeers(opts) {
168
185
  pathsByNodeId,
169
186
  };
170
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
+ }
171
244
  function nodeDepsCount(node) {
172
245
  return Object.keys(node.children).length + node.resolvedPeerNames.size;
173
246
  }
@@ -389,6 +462,7 @@ async function resolvePeersOfNode(currentAlias, nodeId, parentParentPkgs, ctx) {
389
462
  wantedRange,
390
463
  });
391
464
  }
465
+ ctx.peersCacheOwnerByNodeId.set(nodeId, hit.ownerNodeId);
392
466
  return {
393
467
  missingPeers: hit.missingPeers,
394
468
  finishing: (async () => {
@@ -451,6 +525,7 @@ async function resolvePeersOfNode(currentAlias, nodeId, parentParentPkgs, ctx) {
451
525
  missingPeers: allMissingPeers,
452
526
  depPath: pDefer(),
453
527
  resolvedPeers: allResolvedPeers,
528
+ ownerNodeId: nodeId,
454
529
  };
455
530
  if (ctx.peersCache.has(resolvedPackage.pkgIdWithPatchHash)) {
456
531
  ctx.peersCache.get(resolvedPackage.pkgIdWithPatchHash).push(cache);
@@ -512,6 +587,7 @@ async function resolvePeersOfNode(currentAlias, nodeId, parentParentPkgs, ctx) {
512
587
  if (cyclicPeerAliases.has(pendingPeer.alias)) {
513
588
  const { name, version } = ctx.dependenciesTree.get(pendingPeer.nodeId)?.resolvedPackage;
514
589
  const id = `${name}@${version}`;
590
+ ctx.cycleBrokenNodeIds.add(pendingPeer.nodeId);
515
591
  ctx.pathsByNodeIdPromises.get(pendingPeer.nodeId)?.resolve(id);
516
592
  return id;
517
593
  }
@@ -521,6 +597,12 @@ async function resolvePeersOfNode(currentAlias, nodeId, parentParentPkgs, ctx) {
521
597
  return { name: peerNode.resolvedPackage.name, version: peerNode.resolvedPackage.version };
522
598
  }
523
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);
524
606
  return ctx.pathsByNodeIdPromises.get(pendingPeer.nodeId).promise;
525
607
  })),
526
608
  ], ctx.peersSuffixMaxLength);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/installing.deps-resolver",
3
- "version": "1100.2.7",
3
+ "version": "1100.2.8",
4
4
  "description": "Resolves dependency graph of a package",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -27,38 +27,23 @@
27
27
  "!*.map"
28
28
  ],
29
29
  "dependencies": {
30
- "@pnpm/util.lex-comparator": "^4.0.1",
31
- "@yarnpkg/core": "4.8.0",
32
- "graph-cycles": "3.0.0",
33
- "is-inner-link": "^5.0.0",
34
- "is-subdir": "^2.0.0",
35
- "normalize-path": "^3.0.0",
36
- "p-defer": "^4.0.1",
37
- "path-exists": "^5.0.0",
38
- "promise-share": "^2.0.1",
39
- "ramda": "npm:@pnpm/ramda@0.28.1",
40
- "rename-overwrite": "^7.0.1",
41
- "safe-promise-defer": "^2.0.0",
42
- "semver": "^7.8.4",
43
- "semver-range-intersect": "^0.3.1",
44
- "validate-npm-package-name": "7.0.2",
45
- "version-selector-type": "^3.0.0",
46
30
  "@pnpm/catalogs.resolver": "1100.0.0",
47
- "@pnpm/config.version-policy": "1100.1.6",
48
31
  "@pnpm/catalogs.types": "1100.0.0",
32
+ "@pnpm/config.version-policy": "1100.1.6",
49
33
  "@pnpm/constants": "1100.0.0",
50
- "@pnpm/deps.graph-hasher": "1100.2.8",
51
34
  "@pnpm/core-loggers": "1100.2.1",
52
- "@pnpm/deps.peer-range": "1100.0.2",
35
+ "@pnpm/deps.graph-hasher": "1100.2.9",
53
36
  "@pnpm/deps.path": "1100.0.8",
37
+ "@pnpm/deps.peer-range": "1100.0.2",
54
38
  "@pnpm/error": "1100.0.1",
55
- "@pnpm/fetching.pick-fetcher": "1100.0.14",
56
- "@pnpm/hooks.types": "1100.1.1",
57
- "@pnpm/lockfile.preferred-versions": "1100.0.18",
58
- "@pnpm/lockfile.pruner": "1100.0.13",
39
+ "@pnpm/fetching.pick-fetcher": "1100.1.0",
59
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",
60
44
  "@pnpm/lockfile.types": "1100.0.13",
61
- "@pnpm/lockfile.utils": "1100.1.1",
45
+ "@pnpm/lockfile.utils": "1100.1.2",
46
+ "@pnpm/patching.config": "1100.0.9",
62
47
  "@pnpm/patching.types": "1100.0.0",
63
48
  "@pnpm/pkg-manifest.reader": "1100.0.9",
64
49
  "@pnpm/pkg-manifest.utils": "1100.2.6",
@@ -66,20 +51,35 @@
66
51
  "@pnpm/resolving.resolver-base": "1100.5.1",
67
52
  "@pnpm/store.controller-types": "1100.1.7",
68
53
  "@pnpm/types": "1101.3.2",
69
- "@pnpm/patching.config": "1100.0.9",
70
- "@pnpm/workspace.spec-parser": "1100.0.0"
54
+ "@pnpm/util.lex-comparator": "^4.0.1",
55
+ "@pnpm/workspace.spec-parser": "1100.0.0",
56
+ "@yarnpkg/core": "4.8.0",
57
+ "graph-cycles": "3.0.0",
58
+ "is-inner-link": "^5.0.0",
59
+ "is-subdir": "^2.0.0",
60
+ "normalize-path": "^3.0.0",
61
+ "p-defer": "^4.0.1",
62
+ "path-exists": "^5.0.0",
63
+ "promise-share": "^2.0.1",
64
+ "ramda": "npm:@pnpm/ramda@0.28.1",
65
+ "rename-overwrite": "^7.0.1",
66
+ "safe-promise-defer": "^2.0.0",
67
+ "semver": "^7.8.4",
68
+ "semver-range-intersect": "^0.3.1",
69
+ "validate-npm-package-name": "7.0.2",
70
+ "version-selector-type": "^3.0.0"
71
71
  },
72
72
  "peerDependencies": {
73
73
  "@pnpm/logger": "^1100.0.0"
74
74
  },
75
75
  "devDependencies": {
76
76
  "@jest/globals": "30.4.1",
77
+ "@pnpm/installing.deps-resolver": "1100.2.8",
78
+ "@pnpm/logger": "1100.0.0",
77
79
  "@types/normalize-path": "^3.0.2",
78
80
  "@types/ramda": "0.31.1",
79
81
  "@types/semver": "7.7.1",
80
- "@types/validate-npm-package-name": "^4.0.2",
81
- "@pnpm/installing.deps-resolver": "1100.2.7",
82
- "@pnpm/logger": "1100.0.0"
82
+ "@types/validate-npm-package-name": "^4.0.2"
83
83
  },
84
84
  "engines": {
85
85
  "node": ">=22.13"