@pnpm/installing.deps-installer 1101.9.0 → 1102.1.0

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.
@@ -3,22 +3,24 @@ import { linkBins, linkBinsOfPackages } from '@pnpm/bins.linker';
3
3
  import { buildSelectedPkgs } from '@pnpm/building.after-install';
4
4
  import { buildModules, linkBinsOfDependencies } from '@pnpm/building.during-install';
5
5
  import { createAllowBuildFunction, isBuildExplicitlyDisallowed } from '@pnpm/building.policy';
6
+ import { mergeCatalogs } from '@pnpm/catalogs.config';
6
7
  import { parseCatalogProtocol } from '@pnpm/catalogs.protocol-parser';
7
8
  import { matchCatalogResolveResult, resolveFromCatalog } from '@pnpm/catalogs.resolver';
9
+ import { parseOverrides } from '@pnpm/config.parse-overrides';
8
10
  import { LAYOUT_VERSION, LOCKFILE_MAJOR_VERSION, LOCKFILE_VERSION, WANTED_LOCKFILE, } from '@pnpm/constants';
9
11
  import { ignoredScriptsLogger, stageLogger, summaryLogger, } from '@pnpm/core-loggers';
10
12
  import { hashObjectNullableWithPrefix } from '@pnpm/crypto.object-hasher';
11
13
  import * as dp from '@pnpm/deps.path';
12
14
  import { PnpmError } from '@pnpm/error';
13
- import { makeNodeRequireOption, runLifecycleHook, runLifecycleHooksConcurrently, } from '@pnpm/exec.lifecycle';
15
+ import { makeNodePackageMapOption, makeNodeRequireOption, runLifecycleHook, runLifecycleHooksConcurrently, } from '@pnpm/exec.lifecycle';
14
16
  import { getContext } from '@pnpm/installing.context';
15
17
  import { getWantedDependencies, resolveDependencies, } from '@pnpm/installing.deps-resolver';
16
18
  import { extendProjectsWithTargetDirs, headlessInstall } from '@pnpm/installing.deps-restorer';
17
- import { writeModulesManifest } from '@pnpm/installing.modules-yaml';
18
- import { cleanGitBranchLockfiles, getWantedLockfileName, isEmptyLockfile, readWantedLockfileFile, writeCurrentLockfile, writeLockfiles, writeWantedLockfile, } from '@pnpm/lockfile.fs';
19
+ import { readModulesManifest, writeModulesManifest } from '@pnpm/installing.modules-yaml';
20
+ import { cleanGitBranchLockfiles, getWantedLockfileName, isEmptyLockfile, readEnvLockfile, readWantedLockfile, readWantedLockfileFile, writeCurrentLockfile, writeEnvLockfile, writeLockfiles, writeWantedLockfile, } from '@pnpm/lockfile.fs';
19
21
  import { getPreferredVersionsFromLockfileAndManifests } from '@pnpm/lockfile.preferred-versions';
20
22
  import { calcPatchHashes, createOverridesMapFromParsed, getOutdatedLockfileSetting, } from '@pnpm/lockfile.settings-checker';
21
- import { writePnpFile } from '@pnpm/lockfile.to-pnp';
23
+ import { PACKAGE_MAP_FILENAME, writePackageMap, writePnpFile } from '@pnpm/lockfile.to-pnp';
22
24
  import { allProjectsAreUpToDate, satisfiesPackageManifest } from '@pnpm/lockfile.verification';
23
25
  import { globalInfo, logger, streamParser } from '@pnpm/logger';
24
26
  import { groupPatchedDependencies } from '@pnpm/patching.config';
@@ -60,7 +62,7 @@ export async function install(manifest, opts) {
60
62
  if (opts.pnprServer) {
61
63
  return installViaPnprServer(manifest, rootDir, opts);
62
64
  }
63
- const { updatedCatalogs, updatedProjects: projects, ignoredBuilds, resolutionPolicyViolations } = await mutateModules([
65
+ const { updatedCatalogs, updatedProjects: projects, ignoredBuilds, resolutionPolicyViolations, dryRunResult } = await mutateModules([
64
66
  {
65
67
  mutation: 'install',
66
68
  pruneDirectDependencies: opts.pruneDirectDependencies,
@@ -79,7 +81,7 @@ export async function install(manifest, opts) {
79
81
  binsDir: opts.binsDir,
80
82
  }],
81
83
  });
82
- return { updatedCatalogs, updatedManifest: projects[0].manifest, ignoredBuilds, resolutionPolicyViolations };
84
+ return { updatedCatalogs, updatedManifest: projects[0].manifest, ignoredBuilds, resolutionPolicyViolations, dryRunResult };
83
85
  }
84
86
  export async function mutateModulesInSingleProject(project, maybeOpts) {
85
87
  const result = await mutateModules([
@@ -102,6 +104,7 @@ export async function mutateModulesInSingleProject(project, maybeOpts) {
102
104
  updatedProject: result.updatedProjects[0],
103
105
  ignoredBuilds: result.ignoredBuilds,
104
106
  resolutionPolicyViolations: result.resolutionPolicyViolations,
107
+ dryRunResult: result.dryRunResult,
105
108
  };
106
109
  }
107
110
  const pickCatalogSpecifier = {
@@ -168,15 +171,24 @@ export async function mutateModules(projects, maybeOpts) {
168
171
  // attaching their own verifiers). The threat model is a lockfile that
169
172
  // someone else resolved — committed to the repo, restored from a CI
170
173
  // cache, etc. — bypassing the local resolver's policy filters; the local
171
- // resolver's own filters already cover fresh resolution. We run this
172
- // exactly once, right after the lockfile is loaded from disk, before any
173
- // path branches.
174
+ // resolver's own filters already cover fresh resolution.
174
175
  //
175
- // Skipped when we already know pacquet will run the install: pacquet's
176
- // frozen-install path applies the same resolver-policy gate (port of
177
- // this function), so re-running here would duplicate the work and
178
- // for `minimumReleaseAge` in strict mode each lockfile entry is an
179
- // HTTP probe.
176
+ // The verification is kicked off here, right after the lockfile is loaded,
177
+ // but not awaited inline it would otherwise block every later install
178
+ // stage on per-entry registry round trips. Its synchronous prologue (cache
179
+ // lookup, lockfile hashing, candidate collection) runs now against the
180
+ // pristine lockfile, so the async fan-out reads a stable snapshot even
181
+ // while the install mutates `ctx.wantedLockfile` concurrently. The verdict
182
+ // is reconciled with the install in `settleInstall`: a failure aborts the
183
+ // install even mid-flight, and an install that finishes first is held back
184
+ // until the verdict arrives.
185
+ //
186
+ // Skipped when we already know pacquet will run the install: pacquet
187
+ // applies the same resolver-policy gate (port of this function) whether
188
+ // it materializes a frozen lockfile or re-resolves from the manifests,
189
+ // so re-running here would duplicate the work — and for
190
+ // `minimumReleaseAge` in strict mode each lockfile entry is an HTTP
191
+ // probe.
180
192
  //
181
193
  // The predicate mirrors every short-circuit `tryFrozenInstall` checks
182
194
  // before reaching the pacquet branch: anything that would make it
@@ -186,13 +198,28 @@ export async function mutateModules(projects, maybeOpts) {
186
198
  // isn't known here — so verification still runs in that window, the
187
199
  // duplicate is bounded to it.
188
200
  const willDelegateToPacquet = opts.runPacquet != null &&
201
+ opts.useLockfile &&
202
+ !opts.useGitBranchLockfile &&
203
+ !opts.mergeGitBranchLockfiles &&
204
+ !isCheckOnlyInstall(opts) &&
205
+ opts.enableModulesDir &&
189
206
  installsOnly &&
190
207
  !opts.lockfileOnly &&
191
208
  !opts.fixLockfile &&
192
209
  !opts.dedupe &&
193
210
  !ctx.lockfileHadConflicts &&
194
- ctx.existsNonEmptyWantedLockfile &&
195
- (opts.frozenLockfile === true || opts.frozenLockfileIfExists === true);
211
+ (
212
+ // Frozen materialization: pacquet reads the existing lockfile and
213
+ // re-applies the resolver-policy gate as it walks it.
214
+ (ctx.existsNonEmptyWantedLockfile &&
215
+ (opts.frozenLockfile === true || opts.frozenLockfileIfExists === true)) ||
216
+ // Resolving install: pacquet (>= 0.11.7) re-resolves from the
217
+ // manifests itself — applying the policy during fresh resolution —
218
+ // so the existing lockfile entries verified here would just be
219
+ // discarded. If a policy handler is active, keep resolution in pnpm
220
+ // so violations can be returned to the command layer.
221
+ (opts.saveLockfile && opts.runPacquet.supportsResolution && opts.frozenLockfile !== true && opts.nodeLinker !== 'hoisted' && opts.handleResolutionPolicyViolations == null));
222
+ let verifyLockfilePromise;
196
223
  if (!willDelegateToPacquet && !opts.trustLockfile) {
197
224
  const cacheActive = opts.cacheDir != null && opts.resolutionVerifiers.length > 0;
198
225
  const wantedLockfilePath = cacheActive
@@ -201,21 +228,21 @@ export async function mutateModules(projects, maybeOpts) {
201
228
  mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
202
229
  }))
203
230
  : undefined;
204
- try {
205
- await verifyLockfileResolutions(ctx.wantedLockfile, opts.resolutionVerifiers, {
206
- cacheDir: opts.cacheDir,
207
- lockfilePath: wantedLockfilePath,
208
- });
209
- }
210
- catch (err) {
211
- // verifyLockfileResolutions is the one throw site in this function
212
- // that's part of normal user-facing operation (a rejected lockfile);
213
- // other throws here are unexpected. Detach the reporter listener so
214
- // long-lived processes don't leak it on every rejected install.
215
- detachReporter();
216
- throw err;
217
- }
231
+ verifyLockfilePromise = verifyLockfileResolutions(ctx.wantedLockfile, opts.resolutionVerifiers, {
232
+ cacheDir: opts.cacheDir,
233
+ lockfilePath: wantedLockfilePath,
234
+ });
235
+ // Keep the rejection from going unhandled in the window before
236
+ // `settleInstall` awaits the verdict — a preResolution hook or the
237
+ // install kickoff below could throw and bail out before we get there.
238
+ verifyLockfilePromise.catch(() => { });
218
239
  }
240
+ // Gate passed down to the build phase: fetching and linking overlap with
241
+ // verification, but no dependency lifecycle script may run until the verdict
242
+ // is in. Awaiting the promise here throws if verification failed, aborting
243
+ // before any script executes. `settleInstall` is the catch-all that still
244
+ // reconciles the verdict on paths that never reach the build phase.
245
+ const verifyLockfile = verifyLockfilePromise && (() => verifyLockfilePromise);
219
246
  if (opts.hooks.preResolution) {
220
247
  for (const preResolution of opts.hooks.preResolution) {
221
248
  // eslint-disable-next-line no-await-in-loop
@@ -251,7 +278,7 @@ export async function mutateModules(projects, maybeOpts) {
251
278
  }
252
279
  }
253
280
  }
254
- const result = await _install();
281
+ const result = await settleInstall(_install(), verifyLockfilePromise);
255
282
  // @ts-expect-error
256
283
  if (global['verifiedFileIntegrity'] > 1000) {
257
284
  // @ts-expect-error
@@ -264,6 +291,7 @@ export async function mutateModules(projects, maybeOpts) {
264
291
  if (!opts.ignoreScripts && ignoredBuilds?.size) {
265
292
  ignoredBuilds = await runUnignoredDependencyBuilds(opts, ignoredBuilds, ctx.wantedLockfile, allowBuild);
266
293
  }
294
+ let revokedBuilds = false;
267
295
  // Detect packages whose build approval was revoked between the previous
268
296
  // and current install. A package is considered revoked when it was
269
297
  // previously allowed (true) but is now undecided (undefined). Packages
@@ -286,10 +314,28 @@ export async function mutateModules(projects, maybeOpts) {
286
314
  if (allowBuild?.(depPath) === undefined) {
287
315
  ignoredBuilds ??= new Set();
288
316
  ignoredBuilds.add(depPath);
317
+ revokedBuilds = true;
289
318
  }
290
319
  }
291
320
  }
292
321
  }
322
+ if (revokedBuilds && !opts.lockfileOnly && opts.enableModulesDir) {
323
+ // The install path already wrote .modules.yaml with the current
324
+ // install's state, but it captured ignoredBuilds before the revocation
325
+ // scan above added to it. Re-read the manifest from disk so we only
326
+ // update ignoredBuilds and don't clobber fields (hoistedDependencies,
327
+ // pendingBuilds, etc.) the install just wrote. The current computed
328
+ // set is authoritative — runUnignoredDependencyBuilds may have removed
329
+ // entries (for packages it successfully rebuilt) that the on-disk
330
+ // manifest still records, and those must not be re-introduced.
331
+ const writtenManifest = await readModulesManifest(ctx.rootModulesDir);
332
+ if (writtenManifest) {
333
+ // writeModulesManifest converts ignoredBuilds to an array before
334
+ // serializing, so a Set is fine here.
335
+ writtenManifest.ignoredBuilds = ignoredBuilds;
336
+ await writeModulesManifest(ctx.rootModulesDir, writtenManifest);
337
+ }
338
+ }
293
339
  ignoredScriptsLogger.debug({
294
340
  packageNames: ignoredBuilds ? dedupePackageNamesFromIgnoredBuilds(ignoredBuilds) : [],
295
341
  });
@@ -301,7 +347,33 @@ export async function mutateModules(projects, maybeOpts) {
301
347
  depsRequiringBuild: result.depsRequiringBuild,
302
348
  ignoredBuilds,
303
349
  resolutionPolicyViolations: result.resolutionPolicyViolations ?? [],
350
+ dryRunResult: result.dryRunResult,
304
351
  };
352
+ // Reconcile the install with the lockfile verification that runs alongside
353
+ // it. The verification verdict is awaited first so it takes precedence and
354
+ // aborts as soon as it fails, even while the install is still in flight —
355
+ // matching the original sequencing where verification gated the install, so
356
+ // a rejected lockfile surfaces its own error rather than whatever the
357
+ // concurrent install happened to throw. Only once verification passes is the
358
+ // install's result (or error) surfaced. detachReporter mirrors the success
359
+ // path's cleanup so a long-lived process doesn't leak the stream listener on
360
+ // a rejected install.
361
+ async function settleInstall(install, verification) {
362
+ if (verification == null)
363
+ return install;
364
+ // Handle the install's eventual rejection up front so a fail-fast
365
+ // verification throw below doesn't leave the still-running install
366
+ // unhandled.
367
+ install.catch(() => { });
368
+ try {
369
+ await verification;
370
+ return await install;
371
+ }
372
+ catch (err) {
373
+ detachReporter();
374
+ throw err;
375
+ }
376
+ }
305
377
  async function _install() {
306
378
  const scriptsOpts = {
307
379
  extraBinPaths: opts.extraBinPaths,
@@ -589,6 +661,7 @@ export async function mutateModules(projects, maybeOpts) {
589
661
  scriptsOpts,
590
662
  updateLockfileMinorVersion: true,
591
663
  patchedDependencies: patchGroups,
664
+ verifyLockfile,
592
665
  });
593
666
  return {
594
667
  updatedCatalogs: result.updatedCatalogs,
@@ -597,6 +670,7 @@ export async function mutateModules(projects, maybeOpts) {
597
670
  depsRequiringBuild: result.depsRequiringBuild,
598
671
  ignoredBuilds: result.ignoredBuilds,
599
672
  resolutionPolicyViolations: result.resolutionPolicyViolations,
673
+ dryRunResult: result.dryRunResult,
600
674
  };
601
675
  }
602
676
  /**
@@ -630,6 +704,11 @@ export async function mutateModules(projects, maybeOpts) {
630
704
  !ctx.lockfileHadConflicts &&
631
705
  !opts.fixLockfile &&
632
706
  !opts.dedupe &&
707
+ // A check-only install (`lockfileCheck`, used by `--dry-run` and
708
+ // `dedupe --check`) must always run a full resolution so the wanted
709
+ // lockfile can be compared, and must never materialize anything. The
710
+ // frozen path would skip resolution and/or perform a real install.
711
+ !isCheckOnlyInstall(opts) &&
633
712
  installsOnly &&
634
713
  (
635
714
  // If the user explicitly requested a frozen lockfile install, attempt
@@ -717,9 +796,9 @@ Note that in CI environments, this setting is enabled by default.`,
717
796
  else {
718
797
  logger.info({ message: 'Lockfile is up to date, resolution step is skipped', prefix: opts.lockfileDir });
719
798
  }
720
- if (opts.runPacquet != null) {
799
+ if (opts.runPacquet != null && opts.useLockfile && !opts.useGitBranchLockfile && !opts.mergeGitBranchLockfiles && !isCheckOnlyInstall(opts) && opts.enableModulesDir) {
721
800
  try {
722
- await opts.runPacquet();
801
+ await opts.runPacquet.run();
723
802
  }
724
803
  catch (err) {
725
804
  // Same reasoning as the verifyLockfileResolutions catch above: this
@@ -755,6 +834,7 @@ Note that in CI environments, this setting is enabled by default.`,
755
834
  pruneVirtualStore,
756
835
  wantedLockfile: maybeOpts.ignorePackageManifest ? undefined : ctx.wantedLockfile,
757
836
  useLockfile: opts.useLockfile && ctx.wantedLockfileIsModified,
837
+ verifyLockfile,
758
838
  });
759
839
  if (opts.useLockfile && opts.saveLockfile && opts.mergeGitBranchLockfiles ||
760
840
  !upToDateLockfileMajorVersion && !opts.frozenLockfile) {
@@ -938,18 +1018,29 @@ export async function addDependenciesToPackage(manifest, dependencySelectors, op
938
1018
  });
939
1019
  return { updatedCatalogs, updatedManifest: projects[0].manifest, ignoredBuilds, resolutionPolicyViolations };
940
1020
  }
1021
+ /**
1022
+ * A "check-only" install resolves fully but writes nothing: `dryRun`
1023
+ * (`pnpm install --dry-run`) and `lockfileCheck` (`pnpm dedupe --check`)
1024
+ * both take this path. The shared flag suppresses every write and forces a
1025
+ * full resolution (the frozen/headless fast paths are skipped) so the wanted
1026
+ * lockfile can always be compared.
1027
+ */
1028
+ function isCheckOnlyInstall(opts) {
1029
+ return opts.lockfileCheck != null || opts.dryRun === true;
1030
+ }
941
1031
  const _installInContext = async (projects, ctx, opts) => {
1032
+ // Aliasing for clarity in boolean expressions below. True for both
1033
+ // `--dry-run` and `dedupe --check`: resolve fully, write nothing.
1034
+ const isInstallationOnlyForLockfileCheck = isCheckOnlyInstall(opts);
942
1035
  // The wanted lockfile is mutated during installation. To compare changes, a
943
1036
  // deep copy before installation is needed. This copy should represent the
944
1037
  // original wanted lockfile on disk as close as possible.
945
1038
  //
946
1039
  // This object can be quite large. Intentionally avoiding an expensive copy
947
- // if no lockfileCheck option was passed in.
948
- const originalLockfileForCheck = opts.lockfileCheck != null
1040
+ // unless this is a check-only install that needs the comparison.
1041
+ const originalLockfileForCheck = isInstallationOnlyForLockfileCheck
949
1042
  ? clone(ctx.wantedLockfile)
950
1043
  : null;
951
- // Aliasing for clarity in boolean expressions below.
952
- const isInstallationOnlyForLockfileCheck = opts.lockfileCheck != null;
953
1044
  ctx.wantedLockfile.importers = ctx.wantedLockfile.importers || {};
954
1045
  for (const { id } of projects) {
955
1046
  if (!ctx.wantedLockfile.importers[id]) {
@@ -1108,6 +1199,17 @@ const _installInContext = async (projects, ctx, opts) => {
1108
1199
  prefix: ctx.lockfileDir,
1109
1200
  stage: 'resolution_done',
1110
1201
  });
1202
+ // `pnpm update` may bump catalog entries during resolution. Overrides that
1203
+ // reference a catalog (e.g. `overrides: { foo: 'catalog:' }`) were resolved
1204
+ // against the pre-update catalog when the install options were extended, so
1205
+ // re-resolve them against the updated catalog. Done before `afterAllResolved`
1206
+ // so that hook still sees (and can amend) the final overrides. Otherwise
1207
+ // lockfile `overrides` keeps pointing at the old version while `catalogs`
1208
+ // advances, and a later `--frozen-lockfile` install fails with
1209
+ // ERR_PNPM_LOCKFILE_CONFIG_MISMATCH.
1210
+ if (updatedCatalogs != null && opts.overrides != null && Object.keys(opts.overrides).length > 0) {
1211
+ newLockfile.overrides = createOverridesMapFromParsed(parseOverrides(opts.overrides, mergeCatalogs(opts.catalogs, updatedCatalogs)));
1212
+ }
1111
1213
  newLockfile = ((opts.hooks?.afterAllResolved) != null)
1112
1214
  ? await pipeWith(async (f, res) => f(await res), opts.hooks.afterAllResolved)(newLockfile) // eslint-disable-line
1113
1215
  : newLockfile;
@@ -1121,6 +1223,7 @@ const _installInContext = async (projects, ctx, opts) => {
1121
1223
  };
1122
1224
  let stats;
1123
1225
  let ignoredBuilds;
1226
+ const shouldWritePackageMap = opts.enableModulesDir !== false && opts.nodeLinker === 'isolated' && !opts.virtualStoreOnly;
1124
1227
  if (!opts.lockfileOnly && !isInstallationOnlyForLockfileCheck && opts.enableModulesDir) {
1125
1228
  const result = await linkPackages(projects, dependenciesGraph, {
1126
1229
  allowBuild: opts.allowBuild,
@@ -1160,6 +1263,20 @@ const _installInContext = async (projects, ctx, opts) => {
1160
1263
  supportedArchitectures: opts.supportedArchitectures,
1161
1264
  });
1162
1265
  stats = result.stats;
1266
+ if (shouldWritePackageMap) {
1267
+ // Omit the importer self-mapping when a project has no name (see the
1268
+ // deps-restorer write): a non-package-name key like `.` would be invalid.
1269
+ const importerNames = Object.fromEntries(projects.map(({ manifest, id }) => [id, manifest.name]));
1270
+ await writePackageMap(result.currentLockfile, {
1271
+ importerNames,
1272
+ lockfileDir: ctx.lockfileDir,
1273
+ locationByDepPath: Object.fromEntries(Object.values(dependenciesGraph).map((node) => [node.depPath, node.dir])),
1274
+ packageMapType: opts.nodePackageMapType,
1275
+ rootModulesDir: ctx.rootModulesDir,
1276
+ virtualStoreDir: ctx.virtualStoreDir,
1277
+ virtualStoreDirMaxLength: ctx.virtualStoreDirMaxLength,
1278
+ });
1279
+ }
1163
1280
  if (opts.enablePnp) {
1164
1281
  const importerNames = Object.fromEntries(projects.map(({ manifest, id }) => [id, manifest.name ?? id]));
1165
1282
  await writePnpFile(result.currentLockfile, {
@@ -1186,9 +1303,17 @@ const _installInContext = async (projects, ctx, opts) => {
1186
1303
  if (opts.enablePnp) {
1187
1304
  extraEnv = {
1188
1305
  ...extraEnv,
1189
- ...makeNodeRequireOption(path.join(opts.lockfileDir, '.pnp.cjs')),
1306
+ ...makeNodeRequireOption(path.join(opts.lockfileDir, '.pnp.cjs'), extraEnv),
1307
+ };
1308
+ }
1309
+ if (opts.nodeExperimentalPackageMap && shouldWritePackageMap) {
1310
+ extraEnv = {
1311
+ ...extraEnv,
1312
+ ...makeNodePackageMapOption(path.join(ctx.rootModulesDir, PACKAGE_MAP_FILENAME), extraEnv),
1190
1313
  };
1191
1314
  }
1315
+ // Dependency lifecycle scripts must not run on an unverified lockfile.
1316
+ await opts.verifyLockfile?.();
1192
1317
  ignoredBuilds = (await buildModules(dependenciesGraph, rootNodes, {
1193
1318
  allowBuild: opts.allowBuild,
1194
1319
  childConcurrency: opts.childConcurrency,
@@ -1210,6 +1335,7 @@ const _installInContext = async (projects, ctx, opts) => {
1210
1335
  unsafePerm: opts.unsafePerm,
1211
1336
  userAgent: opts.userAgent,
1212
1337
  enableGlobalVirtualStore: opts.enableGlobalVirtualStore,
1338
+ frozenStore: opts.frozenStore,
1213
1339
  })).ignoredBuilds;
1214
1340
  if (ctx.modulesFile?.ignoredBuilds?.size) {
1215
1341
  ignoredBuilds ??= new Set();
@@ -1328,10 +1454,20 @@ const _installInContext = async (projects, ctx, opts) => {
1328
1454
  if (opts.enablePnp) {
1329
1455
  opts.scriptsOpts.extraEnv = {
1330
1456
  ...opts.scriptsOpts.extraEnv,
1331
- ...makeNodeRequireOption(path.join(opts.lockfileDir, '.pnp.cjs')),
1457
+ ...makeNodeRequireOption(path.join(opts.lockfileDir, '.pnp.cjs'), opts.scriptsOpts.extraEnv),
1458
+ };
1459
+ }
1460
+ if (opts.nodeExperimentalPackageMap && shouldWritePackageMap) {
1461
+ opts.scriptsOpts.extraEnv = {
1462
+ ...opts.scriptsOpts.extraEnv,
1463
+ ...makeNodePackageMapOption(path.join(ctx.rootModulesDir, PACKAGE_MAP_FILENAME), opts.scriptsOpts.extraEnv),
1332
1464
  };
1333
1465
  }
1334
1466
  const projectsToBeBuilt = projectsWithTargetDirs.filter(({ mutation }) => mutation === 'install');
1467
+ // The projects' own lifecycle scripts import dependency code linked
1468
+ // from the lockfile, so they are held to the same gate as dependency
1469
+ // builds — also when no new dep paths made the buildModules branch run.
1470
+ await opts.verifyLockfile?.();
1335
1471
  await runLifecycleHooksConcurrently(['preinstall', 'install', 'postinstall', 'preprepare', 'prepare', 'postprepare'], projectsToBeBuilt, opts.childConcurrency, opts.scriptsOpts);
1336
1472
  }
1337
1473
  }
@@ -1408,11 +1544,36 @@ const _installInContext = async (projects, ctx, opts) => {
1408
1544
  depsRequiringBuild,
1409
1545
  ignoredBuilds,
1410
1546
  resolutionPolicyViolations,
1547
+ dryRunResult: (opts.dryRun && originalLockfileForCheck != null)
1548
+ ? { originalLockfile: originalLockfileForCheck, wantedLockfile: newLockfile }
1549
+ : undefined,
1411
1550
  };
1412
1551
  };
1413
1552
  function allMutationsAreInstalls(projects) {
1414
1553
  return projects.every((project) => project.mutation === 'install' && !project.update && !project.updateMatching);
1415
1554
  }
1555
+ /**
1556
+ * The `InstallFunctionResult` for an install pacquet resolved and
1557
+ * materialized end-to-end. pacquet wrote `pnpm-lock.yaml` and the
1558
+ * `node_modules` tree itself. `ctx.wantedLockfile` has already been
1559
+ * refreshed from disk, and pacquet reports its own stats / ignored-builds
1560
+ * via NDJSON, so the structured `stats` / `ignoredBuilds` fall back to
1561
+ * their no-op defaults. Resolution-policy handlers are guarded out before
1562
+ * this path, so there are no command-layer policy violations to return.
1563
+ * Manifests are returned unchanged — this path only runs for plain
1564
+ * installs, which don't rewrite `package.json`.
1565
+ */
1566
+ function pacquetResolveResult(projects, ctx) {
1567
+ return {
1568
+ newLockfile: ctx.wantedLockfile,
1569
+ projects: projects.map((project) => ({
1570
+ manifest: project.originalManifest ?? project.manifest,
1571
+ rootDir: project.rootDir,
1572
+ })),
1573
+ depsRequiringBuild: [],
1574
+ resolutionPolicyViolations: [],
1575
+ };
1576
+ }
1416
1577
  /**
1417
1578
  * Run the pacquet binary if it's configured, otherwise run the JS
1418
1579
  * `headlessInstall`. Callers can hand off any code path that materializes
@@ -1429,13 +1590,17 @@ function allMutationsAreInstalls(projects) {
1429
1590
  * stats record and a no-op ignoredBuilds iteration).
1430
1591
  */
1431
1592
  async function materializeOrDelegate(opts, runHeadlessInstall) {
1432
- if (opts.runPacquet != null) {
1593
+ if (opts.runPacquet != null &&
1594
+ opts.useLockfile !== false &&
1595
+ opts.saveLockfile !== false &&
1596
+ opts.useGitBranchLockfile !== true &&
1597
+ opts.mergeGitBranchLockfiles !== true) {
1433
1598
  // Reached only from the resolve-then-materialize call sites
1434
1599
  // (workspace-partial, hoisted-linker, pnpr server install). Each ran a
1435
1600
  // lockfileOnly resolve pass that emitted one
1436
1601
  // `pnpm:progress status:resolved` per package, so pacquet's
1437
1602
  // duplicate `resolved` events would double the reporter's count.
1438
- await opts.runPacquet({ filterResolvedProgress: true });
1603
+ await opts.runPacquet.run({ filterResolvedProgress: true });
1439
1604
  return {};
1440
1605
  }
1441
1606
  return runHeadlessInstall();
@@ -1446,7 +1611,7 @@ const installInContext = async (projects, ctx, opts) => {
1446
1611
  if (!opts.frozenLockfile && opts.useLockfile) {
1447
1612
  const allProjectsLocatedInsideWorkspace = Object.values(ctx.projects)
1448
1613
  .filter((project) => isPathInsideWorkspace(project.rootDirRealPath ?? project.rootDir));
1449
- if (allProjectsLocatedInsideWorkspace.length > projects.length) {
1614
+ if (allProjectsLocatedInsideWorkspace.length > projects.length && !isCheckOnlyInstall(opts) && opts.enableModulesDir) {
1450
1615
  const newProjects = [...projects];
1451
1616
  const getWantedDepsOpts = {
1452
1617
  autoInstallPeers: opts.autoInstallPeers,
@@ -1498,7 +1663,7 @@ const installInContext = async (projects, ctx, opts) => {
1498
1663
  };
1499
1664
  }
1500
1665
  }
1501
- if (opts.nodeLinker === 'hoisted' && !opts.lockfileOnly) {
1666
+ if (opts.nodeLinker === 'hoisted' && !opts.lockfileOnly && !isCheckOnlyInstall(opts) && opts.enableModulesDir) {
1502
1667
  const result = await _installInContext(projects, ctx, {
1503
1668
  ...opts,
1504
1669
  lockfileOnly: true,
@@ -1524,20 +1689,69 @@ const installInContext = async (projects, ctx, opts) => {
1524
1689
  ignoredBuilds,
1525
1690
  };
1526
1691
  }
1527
- // Isolated `nodeLinker` (the default) with a non-frozen install:
1528
- // pacquet doesn't ship a resolver yet, so split the install in two —
1529
- // ask `_installInContext` for a `lockfileOnly` resolve pass (writes
1530
- // `pnpm-lock.yaml`), then hand the freshly-written lockfile to
1531
- // pacquet for the fetch / import / link / build phases. The frozen
1532
- // branch is handled earlier in `tryFrozenInstall`; the hoisted
1533
- // branch above already runs the same resolve-then-materialize
1534
- // sequence (it had to even before pacquet existed). When no pacquet
1535
- // is configured this falls through to the full single-pass install.
1536
- if (opts.runPacquet != null && !opts.lockfileOnly) {
1692
+ // Isolated `nodeLinker` (the default) with a non-frozen install.
1693
+ // The frozen branch is handled earlier in `tryFrozenInstall`; the
1694
+ // hoisted branch above runs a resolve-then-materialize sequence.
1695
+ if (opts.runPacquet != null && opts.useLockfile && opts.saveLockfile && !opts.useGitBranchLockfile && !opts.mergeGitBranchLockfiles && !opts.lockfileOnly && !isCheckOnlyInstall(opts) && opts.enableModulesDir) {
1696
+ // pacquet >= 0.11.7 resolves itself: hand it the whole install
1697
+ // (resolve + fetch + import + link + build, writing the lockfile)
1698
+ // in a single non-frozen pass. Only for plain installs — `add` /
1699
+ // `update` / `remove` need pnpm to mutate the manifests and
1700
+ // resolve the new specs first (pacquet's `install` reads
1701
+ // package.json from disk, which pnpm hasn't rewritten yet).
1702
+ if (opts.runPacquet.supportsResolution && !opts.frozenLockfile && opts.handleResolutionPolicyViolations == null && allMutationsAreInstalls(projects)) {
1703
+ // `configDependencies` are recorded in a YAML document prepended
1704
+ // to `pnpm-lock.yaml` — purely a pnpm concept that pacquet doesn't
1705
+ // model. Capture it before pacquet rewrites the lockfile and
1706
+ // restore it afterwards (`writeEnvLockfile` re-reads pacquet's main
1707
+ // document and re-prepends the env document), otherwise the next
1708
+ // `--frozen-lockfile` install fails its config-deps freshness gate.
1709
+ // The restore runs even if pacquet fails partway: a non-zero exit can
1710
+ // still leave a rewritten lockfile behind, so the env document must be
1711
+ // put back regardless.
1712
+ const envLockfile = await readEnvLockfile(ctx.lockfileDir);
1713
+ let pacquetError;
1714
+ try {
1715
+ await opts.runPacquet.run({ resolve: true });
1716
+ }
1717
+ catch (err) {
1718
+ pacquetError = err;
1719
+ throw err;
1720
+ }
1721
+ finally {
1722
+ if (envLockfile != null) {
1723
+ await writeEnvLockfile(ctx.lockfileDir, envLockfile).catch((restoreErr) => {
1724
+ if (pacquetError == null) {
1725
+ throw restoreErr;
1726
+ }
1727
+ logger.warn({
1728
+ error: restoreErr,
1729
+ message: `Failed to restore the configDependencies document in pnpm-lock.yaml: ${restoreErr.message}`,
1730
+ prefix: ctx.lockfileDir,
1731
+ });
1732
+ });
1733
+ }
1734
+ }
1735
+ const wantedLockfile = await readWantedLockfile(ctx.lockfileDir, {
1736
+ ignoreIncompatible: opts.force || opts.ci === true,
1737
+ mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
1738
+ useGitBranchLockfile: opts.useGitBranchLockfile,
1739
+ wantedVersions: [LOCKFILE_VERSION],
1740
+ });
1741
+ if (wantedLockfile == null) {
1742
+ throw new PnpmError('PACQUET_LOCKFILE_READ_FAILED', `pacquet did not write a readable ${WANTED_LOCKFILE}`);
1743
+ }
1744
+ ctx.wantedLockfile = wantedLockfile;
1745
+ return pacquetResolveResult(projects, ctx);
1746
+ }
1747
+ // Older pacquet can only materialize: split the install in two —
1748
+ // ask `_installInContext` for a `lockfileOnly` resolve pass (writes
1749
+ // `pnpm-lock.yaml`), then hand the freshly-written lockfile to
1750
+ // pacquet for the fetch / import / link / build phases. The resolve
1751
+ // pass emitted a `pnpm:progress status:resolved` per package; ask
1752
+ // pacquet to drop its own duplicates.
1537
1753
  const result = await _installInContext(projects, ctx, { ...opts, lockfileOnly: true });
1538
- // The resolve pass above emitted a `pnpm:progress status:resolved`
1539
- // per package; ask pacquet to drop its own duplicates.
1540
- await opts.runPacquet({ filterResolvedProgress: true });
1754
+ await opts.runPacquet.run({ filterResolvedProgress: true });
1541
1755
  return result;
1542
1756
  }
1543
1757
  return await _installInContext(projects, ctx, opts);
@@ -1811,6 +2025,15 @@ async function mutateModulesViaPnpr(projects, opts) {
1811
2025
  * and links packages into node_modules — like a normal install.
1812
2026
  */
1813
2027
  async function installViaPnprServer(manifest, rootDir, opts, allInstallProjects) {
2028
+ // The pnpr server path re-resolves and persists new `index.db` entries plus a
2029
+ // freshly written lockfile, so it inherently writes the store. `frozenStore`
2030
+ // promises the store is complete and read-only, so the two are mutually
2031
+ // exclusive — and the unconditional pnpr gate means this path runs even under
2032
+ // `--offline --frozen-lockfile`, so refuse up front with guidance instead of
2033
+ // crashing later on the read-only `index.db` open.
2034
+ if (opts.frozenStore) {
2035
+ throw new PnpmError('FROZEN_STORE_INCOMPATIBLE_WITH_PNPR', 'The pnpr server resolves dependencies and writes new entries into the store, which is opened read-only when frozenStore is enabled.', { hint: 'Disable the pnpr server (unset `--pnpr-server` / `pnprServer` in pnpm-workspace.yaml) so the install reads from the existing store, or unset `frozenStore` to allow store writes.' });
2036
+ }
1814
2037
  // The pnpr server path skips client-side resolution, so resolver-side policies
1815
2038
  // can't be enforced locally. `minimumReleaseAge` is forwarded to the
1816
2039
  // pnpr server and enforced server-side. `trustPolicy` has no server-side
@@ -1820,7 +2043,7 @@ async function installViaPnprServer(manifest, rootDir, opts, allInstallProjects)
1820
2043
  throw new PnpmError('TRUST_POLICY_INCOMPATIBLE_WITH_PNPR', 'The pnpr server does not yet enforce `trustPolicy: no-downgrade`, so running an install through it under this policy would produce a lockfile that the local verifier rejects.', { hint: 'Unset `trustPolicy` for this install, or disable the pnpr server (unset `--pnpr-server` / `pnprServer` in pnpm-workspace.yaml) so resolution runs locally and the trust check applies.' });
1821
2044
  }
1822
2045
  const { resolveViaPnprServer } = await import('@pnpm/pnpr.client');
1823
- const { createGetAuthHeaderByURI, getAuthHeadersFromCreds } = await import('@pnpm/network.auth-header');
2046
+ const { createGetAuthHeaderByURI, getAuthHeadersByScope, getAuthHeadersFromCreds } = await import('@pnpm/network.auth-header');
1824
2047
  // Forward the whole credential map (the registries a graph touches
1825
2048
  // aren't known up front), so the server attaches the right token per
1826
2049
  // URL. `authorization` also identifies the caller to pnpr's gate.
@@ -1856,7 +2079,7 @@ async function installViaPnprServer(manifest, rootDir, opts, allInstallProjects)
1856
2079
  projects: projectsList,
1857
2080
  registry: opts.registries?.default,
1858
2081
  namedRegistries: opts.namedRegistries,
1859
- authHeaders: forwardedAuthHeaders,
2082
+ authHeaders: getAuthHeadersByScope(forwardedAuthHeaders),
1860
2083
  authorization: pnprAuthorization,
1861
2084
  overrides: opts.overrides,
1862
2085
  minimumReleaseAge: opts.minimumReleaseAge,
@@ -1,5 +1,5 @@
1
1
  import { type DepsStateCache } from '@pnpm/deps.graph-hasher';
2
- import type { DependenciesGraph, LinkedDependency } from '@pnpm/installing.deps-resolver';
2
+ import { type DependenciesGraph, type LinkedDependency } from '@pnpm/installing.deps-resolver';
3
3
  import type { InstallationResultStats } from '@pnpm/installing.deps-restorer';
4
4
  import type { IncludedDependencies } from '@pnpm/installing.modules-yaml';
5
5
  import type { LockfileObject } from '@pnpm/lockfile.fs';