@pnpm/installing.deps-resolver 1100.2.6 → 1100.2.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/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']);
@@ -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
  }
@@ -1146,14 +1150,21 @@ async function resolveDependency(wantedDependency, ctx, options) {
1146
1150
  version: wantedDependency.alias ? wantedDependency.bareSpecifier : undefined,
1147
1151
  };
1148
1152
  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;
1153
+ if (!wantedLockfileContainsSatisfyingEntry(ctx.wantedLockfile, wantedDependency)) {
1154
+ skippedOptionalDependencyLogger.debug({
1155
+ details: err.toString(),
1156
+ package: wantedDependencyDetails,
1157
+ parents: getPkgsInfoFromIds(options.parentIds, ctx.resolvedPkgsById),
1158
+ prefix: options.prefix,
1159
+ reason: 'resolution_failure',
1160
+ });
1161
+ return null;
1162
+ }
1163
+ if (err.hint == null) {
1164
+ err.hint = 'This optional dependency is not skipped, because the lockfile contains a resolution for it. ' +
1165
+ 'Skipping it would remove the locked entries, making the lockfile differ depending on which machine ran the install. ' +
1166
+ 'If the version was intentionally removed from the registry, update the dependent package or remove the entries from the lockfile.';
1167
+ }
1157
1168
  }
1158
1169
  err.package = wantedDependencyDetails;
1159
1170
  err.prefix = options.prefix;
@@ -1417,6 +1428,39 @@ async function resolveDependency(wantedDependency, ctx, options) {
1417
1428
  finishPackageResolution();
1418
1429
  }
1419
1430
  }
1431
+ /**
1432
+ * Whether the wanted lockfile already holds a package entry that satisfies the
1433
+ * wanted dependency. An optional dependency that fails to resolve is normally
1434
+ * skipped, but when a locked resolution exists the failure is environmental
1435
+ * (e.g. a registry mirror that hasn't synced the release yet) rather than a
1436
+ * genuinely uninstallable package. Silently skipping in that case would erase
1437
+ * the locked entries, making the lockfile differ across machines from
1438
+ * identical inputs and leaving frozen installs on other hosts with nothing to
1439
+ * link (https://github.com/pnpm/pnpm/issues/12853).
1440
+ *
1441
+ * Only plain semver specifiers are checked; exotic specifiers (git, catalogs,
1442
+ * tags, URLs) keep the skip-on-failure behavior.
1443
+ *
1444
+ * The check is deliberately by package name and range rather than by the
1445
+ * current edge's locked dep path. `pnpm dedupe` — the flow where the erasure
1446
+ * bites — clears every per-snapshot dependency map before resolving
1447
+ * (`forgetResolutionsOfAllPrevWantedDeps`), so on this code path no edge-level
1448
+ * lockfile linkage exists to key on; only the package entries survive. A
1449
+ * satisfying entry locked via any edge also means the registry served this
1450
+ * package in-range before, so failing loudly instead of skipping is the right
1451
+ * outcome even when the failing edge itself was never locked.
1452
+ */
1453
+ function wantedLockfileContainsSatisfyingEntry(lockfile, wantedDependency) {
1454
+ if (!wantedDependency.alias)
1455
+ return false;
1456
+ const { pkgName, bareSpecifier } = unwrapPackageName(wantedDependency.alias, wantedDependency.bareSpecifier);
1457
+ if (semver.validRange(bareSpecifier) == null)
1458
+ return false;
1459
+ return Object.keys(lockfile.packages ?? {}).some((depPath) => {
1460
+ const parsed = dp.parse(depPath);
1461
+ return parsed.name === pkgName && parsed.version != null && semver.satisfies(parsed.version, bareSpecifier);
1462
+ });
1463
+ }
1420
1464
  export function getManifestFromResponse(pkgResponse, wantedDependency, currentPkg) {
1421
1465
  if (pkgResponse.body.manifest)
1422
1466
  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';
@@ -25,7 +26,7 @@ export async function resolvePeers(opts) {
25
26
  const finishingList = [];
26
27
  const peersCache = new Map();
27
28
  const purePkgs = new Set();
28
- for (const { directNodeIdsByAlias, declaredDirectDependencies, explicitlyRequestedDirectDependencies, topParents, rootDir, id } of opts.projects) {
29
+ for (const { directNodeIdsByAlias, hoistedPeerProviderNodeIds, declaredDirectDependencies, explicitlyRequestedDirectDependencies, topParents, rootDir, id } of opts.projects) {
29
30
  const currentProviderSources = [{
30
31
  directNodeIdsByAlias,
31
32
  declaredDirectDependencies: declaredDirectDependencies ?? new Set(),
@@ -48,10 +49,26 @@ export async function resolvePeers(opts) {
48
49
  pathsByNodeIdPromises.set(nodeId, pDefer());
49
50
  }
50
51
  }
51
- // eslint-disable-next-line no-await-in-loop
52
- const { finishing } = await resolvePeersOfChildren(Object.fromEntries(directNodeIdsByAlias.entries()), pkgsByName, {
52
+ // Hoisted peer providers stay visible as providers (via pkgsByName) but are
53
+ // not traversed as direct children: their nodeIds point into subtrees, and
54
+ // resolving them a second time in the project's root context would bind
55
+ // their peers to the project's own dependencies instead of the providers
56
+ // next to them in the tree, racing with the in-place resolution on
57
+ // pathsByNodeId and producing peer graphs that mix both contexts.
58
+ const ownDirectChildren = {};
59
+ const hoistedProviderChildren = {};
60
+ for (const [alias, nodeId] of directNodeIdsByAlias.entries()) {
61
+ if (hoistedPeerProviderNodeIds?.has(nodeId)) {
62
+ hoistedProviderChildren[alias] = nodeId;
63
+ }
64
+ else {
65
+ ownDirectChildren[alias] = nodeId;
66
+ }
67
+ }
68
+ const parentPkgsOfNode = new Map();
69
+ const projectPeersContext = {
53
70
  allPeerDepNames: opts.allPeerDepNames,
54
- parentPkgsOfNode: new Map(),
71
+ parentPkgsOfNode,
55
72
  dependenciesTree: opts.dependenciesTree,
56
73
  depGraph,
57
74
  lockfileDir: opts.lockfileDir,
@@ -71,10 +88,33 @@ export async function resolvePeers(opts) {
71
88
  rootDir,
72
89
  virtualStoreDir: opts.virtualStoreDir,
73
90
  virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
74
- });
91
+ };
92
+ // eslint-disable-next-line no-await-in-loop
93
+ const { finishing } = await resolvePeersOfChildren(ownDirectChildren, pkgsByName, projectPeersContext);
75
94
  if (finishing) {
76
95
  finishingList.push(finishing);
77
96
  }
97
+ // A provider whose tree position was pruned from the traversal (its parent
98
+ // hit peersCache, so its children were never visited) still has consumers
99
+ // awaiting its dep path, so resolve it here as a last resort. Providers
100
+ // visited by the traversal above are recorded in parentPkgsOfNode.
101
+ // All pruned providers go into a single resolvePeersOfChildren call:
102
+ // its cycle analysis only sees the children of one call, and providers
103
+ // frequently peer-depend on each other, so resolving them one by one
104
+ // would leave their dep path calculations awaiting each other forever.
105
+ const prunedProviderChildren = {};
106
+ for (const [alias, nodeId] of Object.entries(hoistedProviderChildren)) {
107
+ if (parentPkgsOfNode.has(nodeId))
108
+ continue;
109
+ prunedProviderChildren[alias] = nodeId;
110
+ }
111
+ if (Object.keys(prunedProviderChildren).length > 0) {
112
+ // eslint-disable-next-line no-await-in-loop
113
+ const { finishing } = await resolvePeersOfChildren(prunedProviderChildren, pkgsByName, projectPeersContext);
114
+ if (finishing) {
115
+ finishingList.push(finishing);
116
+ }
117
+ }
78
118
  if (Object.keys(peerDependencyIssues.bad).length > 0 || Object.keys(peerDependencyIssues.missing).length > 0) {
79
119
  peerDependencyIssuesByProjects[id] = {
80
120
  ...peerDependencyIssues,
@@ -501,7 +541,7 @@ async function resolvePeersOfNode(currentAlias, nodeId, parentParentPkgs, ctx) {
501
541
  const peerDependencies = { ...resolvedPackage.peerDependencies };
502
542
  if (!ctx.depGraph[depPath] || ctx.depGraph[depPath].depth > node.depth) {
503
543
  const modules = path.join(ctx.virtualStoreDir, depPathToFilename(depPath, ctx.virtualStoreDirMaxLength), 'node_modules');
504
- const dir = path.join(modules, resolvedPackage.name);
544
+ const dir = safeJoinModulesDir(modules, resolvedPackage.name);
505
545
  const transitivePeerDependencies = new Set();
506
546
  for (const unknownPeer of allResolvedPeers.keys()) {
507
547
  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.7",
4
4
  "description": "Resolves dependency graph of a package",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -47,25 +47,26 @@
47
47
  "@pnpm/config.version-policy": "1100.1.6",
48
48
  "@pnpm/catalogs.types": "1100.0.0",
49
49
  "@pnpm/constants": "1100.0.0",
50
+ "@pnpm/deps.graph-hasher": "1100.2.8",
50
51
  "@pnpm/core-loggers": "1100.2.1",
51
- "@pnpm/deps.graph-hasher": "1100.2.7",
52
52
  "@pnpm/deps.peer-range": "1100.0.2",
53
53
  "@pnpm/deps.path": "1100.0.8",
54
54
  "@pnpm/error": "1100.0.1",
55
- "@pnpm/hooks.types": "1100.1.1",
56
55
  "@pnpm/fetching.pick-fetcher": "1100.0.14",
56
+ "@pnpm/hooks.types": "1100.1.1",
57
57
  "@pnpm/lockfile.preferred-versions": "1100.0.18",
58
58
  "@pnpm/lockfile.pruner": "1100.0.13",
59
+ "@pnpm/fs.symlink-dependency": "1100.0.10",
59
60
  "@pnpm/lockfile.types": "1100.0.13",
60
61
  "@pnpm/lockfile.utils": "1100.1.1",
61
- "@pnpm/patching.config": "1100.0.9",
62
62
  "@pnpm/patching.types": "1100.0.0",
63
- "@pnpm/resolving.npm-resolver": "1102.1.1",
64
63
  "@pnpm/pkg-manifest.reader": "1100.0.9",
65
- "@pnpm/resolving.resolver-base": "1100.5.1",
66
64
  "@pnpm/pkg-manifest.utils": "1100.2.6",
67
- "@pnpm/types": "1101.3.2",
65
+ "@pnpm/resolving.npm-resolver": "1102.1.2",
66
+ "@pnpm/resolving.resolver-base": "1100.5.1",
68
67
  "@pnpm/store.controller-types": "1100.1.7",
68
+ "@pnpm/types": "1101.3.2",
69
+ "@pnpm/patching.config": "1100.0.9",
69
70
  "@pnpm/workspace.spec-parser": "1100.0.0"
70
71
  },
71
72
  "peerDependencies": {
@@ -77,8 +78,8 @@
77
78
  "@types/ramda": "0.31.1",
78
79
  "@types/semver": "7.7.1",
79
80
  "@types/validate-npm-package-name": "^4.0.2",
80
- "@pnpm/logger": "1100.0.0",
81
- "@pnpm/installing.deps-resolver": "1100.2.6"
81
+ "@pnpm/installing.deps-resolver": "1100.2.7",
82
+ "@pnpm/logger": "1100.0.0"
82
83
  },
83
84
  "engines": {
84
85
  "node": ">=22.13"