@pnpm/installing.deps-installer 1101.2.0 → 1101.3.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.
@@ -186,6 +186,24 @@ export interface StrictInstallOptions {
186
186
  trustPolicyIgnoreAfter?: number;
187
187
  packageVulnerabilityAudit?: PackageVulnerabilityAudit;
188
188
  blockExoticSubdeps?: boolean;
189
+ /**
190
+ * Optional alternative install engine. When set, the frozen-install
191
+ * path invokes this callback instead of `headlessInstall`. The CLI
192
+ * layer constructs it (today: spawning the pacquet binary installed
193
+ * via `configDependencies` and forwarding pnpm's own CLI argv); the
194
+ * installer treats it as an opaque "do the install" hook so it
195
+ * doesn't need to know about pacquet's binary path, CLI surface, or
196
+ * any settings that only pacquet consumes.
197
+ *
198
+ * `filterResolvedProgress` tells the helper to drop the engine's
199
+ * own `pnpm:progress status:resolved` events because pnpm already
200
+ * emitted one per package during a preceding lockfileOnly resolve
201
+ * pass. The frozen-install path passes `false` (or nothing): no
202
+ * resolve pass ran, so the engine's events are the only source.
203
+ */
204
+ runPacquet?: (opts?: {
205
+ filterResolvedProgress?: boolean;
206
+ }) => Promise<void>;
189
207
  /**
190
208
  * If true, `mutateModules` does not emit the per-install `summary` log
191
209
  * event. Used by `pnpm add -g` when it runs multiple isolated installs
@@ -171,26 +171,50 @@ export async function mutateModules(projects, maybeOpts) {
171
171
  // resolver's own filters already cover fresh resolution. We run this
172
172
  // exactly once, right after the lockfile is loaded from disk, before any
173
173
  // path branches.
174
- const cacheActive = opts.cacheDir != null && opts.resolutionVerifiers.length > 0;
175
- const wantedLockfilePath = cacheActive
176
- ? path.resolve(ctx.lockfileDir, await getWantedLockfileName({
177
- useGitBranchLockfile: opts.useGitBranchLockfile,
178
- mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
179
- }))
180
- : undefined;
181
- try {
182
- await verifyLockfileResolutions(ctx.wantedLockfile, opts.resolutionVerifiers, {
183
- cacheDir: opts.cacheDir,
184
- lockfilePath: wantedLockfilePath,
185
- });
186
- }
187
- catch (err) {
188
- // verifyLockfileResolutions is the one throw site in this function
189
- // that's part of normal user-facing operation (a rejected lockfile);
190
- // other throws here are unexpected. Detach the reporter listener so
191
- // long-lived processes don't leak it on every rejected install.
192
- detachReporter();
193
- throw err;
174
+ //
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.
180
+ //
181
+ // The predicate mirrors every short-circuit `tryFrozenInstall` checks
182
+ // before reaching the pacquet branch: anything that would make it
183
+ // return null, throw, or fall through to the JS path must keep
184
+ // verification on. The optimistic `preferFrozenLockfile` path decides
185
+ // whether to delegate later (based on `allProjectsAreUpToDate`), which
186
+ // isn't known here — so verification still runs in that window, the
187
+ // duplicate is bounded to it.
188
+ const willDelegateToPacquet = opts.runPacquet != null &&
189
+ installsOnly &&
190
+ !opts.lockfileOnly &&
191
+ !opts.fixLockfile &&
192
+ !opts.dedupe &&
193
+ !ctx.lockfileHadConflicts &&
194
+ ctx.existsNonEmptyWantedLockfile &&
195
+ (opts.frozenLockfile === true || opts.frozenLockfileIfExists === true);
196
+ if (!willDelegateToPacquet) {
197
+ const cacheActive = opts.cacheDir != null && opts.resolutionVerifiers.length > 0;
198
+ const wantedLockfilePath = cacheActive
199
+ ? path.resolve(ctx.lockfileDir, await getWantedLockfileName({
200
+ useGitBranchLockfile: opts.useGitBranchLockfile,
201
+ mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
202
+ }))
203
+ : 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
+ }
194
218
  }
195
219
  if (opts.hooks.preResolution) {
196
220
  for (const preResolution of opts.hooks.preResolution) {
@@ -672,6 +696,28 @@ Note that in CI environments, this setting is enabled by default.`,
672
696
  else {
673
697
  logger.info({ message: 'Lockfile is up to date, resolution step is skipped', prefix: opts.lockfileDir });
674
698
  }
699
+ if (opts.runPacquet != null) {
700
+ try {
701
+ await opts.runPacquet();
702
+ }
703
+ catch (err) {
704
+ // Same reasoning as the verifyLockfileResolutions catch above: this
705
+ // is the user-facing failure path, so detach the reporter listener
706
+ // before rethrowing so long-lived processes don't leak it.
707
+ detachReporter();
708
+ throw err;
709
+ }
710
+ return {
711
+ updatedProjects: projects.map((mutatedProject) => {
712
+ const project = ctx.projects[mutatedProject.rootDir];
713
+ return {
714
+ ...project,
715
+ manifest: project.originalManifest ?? project.manifest,
716
+ };
717
+ }),
718
+ ignoredBuilds: undefined,
719
+ };
720
+ }
675
721
  try {
676
722
  const { stats, ignoredBuilds } = await headlessInstall({
677
723
  ...ctx,
@@ -1284,8 +1330,16 @@ const _installInContext = async (projects, ctx, opts) => {
1284
1330
  ...lockfileOpts,
1285
1331
  });
1286
1332
  }
1287
- if (opts.nodeLinker !== 'hoisted') {
1288
- // This is only needed because otherwise the reporter will hang
1333
+ if (opts.nodeLinker !== 'hoisted' && opts.runPacquet == null) {
1334
+ // This is only needed because otherwise the reporter will hang.
1335
+ // Skipped when pacquet is about to take over the materialization
1336
+ // phase: the default reporter completes the progress stream for
1337
+ // this prefix on `importing_done`, so emitting it from the
1338
+ // lockfileOnly resolve pass would prematurely close the stream
1339
+ // and pacquet's own `importing_started` / progress events would
1340
+ // render to a stale stream. Pacquet emits its own
1341
+ // `importing_done` after the install, which closes the stream
1342
+ // normally.
1289
1343
  stageLogger.debug({
1290
1344
  prefix: opts.lockfileDir,
1291
1345
  stage: 'importing_done',
@@ -1309,7 +1363,15 @@ const _installInContext = async (projects, ctx, opts) => {
1309
1363
  strictPeerDependencies: opts.strictPeerDependencies,
1310
1364
  rules: opts.peerDependencyRules,
1311
1365
  });
1312
- if (!opts.omitSummaryLog) {
1366
+ // Skipped when pacquet will take over the materialization. The
1367
+ // default reporter's `reportSummary` `take(1)`s the first summary
1368
+ // event and combines it with whatever `pkgsDiff` it has at that
1369
+ // moment — which is empty here, since pacquet hasn't emitted its
1370
+ // per-direct-dep `pnpm:root` events yet. Letting pnpm fire summary
1371
+ // now would lock in an empty diff. Pacquet emits its own
1372
+ // `pnpm:summary` after the install completes, by which point its
1373
+ // root events have populated the diff.
1374
+ if (!opts.omitSummaryLog && opts.runPacquet == null) {
1313
1375
  summaryLogger.debug({ prefix: opts.lockfileDir });
1314
1376
  }
1315
1377
  // Similar to the sequencing for when the original wanted lockfile is
@@ -1336,6 +1398,33 @@ const _installInContext = async (projects, ctx, opts) => {
1336
1398
  function allMutationsAreInstalls(projects) {
1337
1399
  return projects.every((project) => project.mutation === 'install' && !project.update && !project.updateMatching);
1338
1400
  }
1401
+ /**
1402
+ * Run the pacquet binary if it's configured, otherwise run the JS
1403
+ * `headlessInstall`. Callers can hand off any code path that materializes
1404
+ * an already-resolved lockfile (workspace partial install, hoisted
1405
+ * linker, agent-server install, frozen install) without restating the
1406
+ * delegation choice.
1407
+ *
1408
+ * Pacquet reads the wanted lockfile from disk and produces its own
1409
+ * `pnpm:stats` / `pnpm:ignored-scripts` log events that drive the
1410
+ * reporter. The structured stats / ignoredBuilds return values that
1411
+ * `headlessInstall` produces aren't recovered here — pacquet doesn't
1412
+ * surface them through any return path — so callers get `undefined` for
1413
+ * both. `mutateModules` already tolerates that (it falls back to a zero
1414
+ * stats record and a no-op ignoredBuilds iteration).
1415
+ */
1416
+ async function materializeOrDelegate(opts, runHeadlessInstall) {
1417
+ if (opts.runPacquet != null) {
1418
+ // Reached only from the resolve-then-materialize call sites
1419
+ // (workspace-partial, hoisted-linker, agent install). Each ran a
1420
+ // lockfileOnly resolve pass that emitted one
1421
+ // `pnpm:progress status:resolved` per package, so pacquet's
1422
+ // duplicate `resolved` events would double the reporter's count.
1423
+ await opts.runPacquet({ filterResolvedProgress: true });
1424
+ return {};
1425
+ }
1426
+ return runHeadlessInstall();
1427
+ }
1339
1428
  const installInContext = async (projects, ctx, opts) => {
1340
1429
  try {
1341
1430
  const isPathInsideWorkspace = isSubdir.bind(null, opts.lockfileDir);
@@ -1372,7 +1461,7 @@ const installInContext = async (projects, ctx, opts) => {
1372
1461
  ...opts,
1373
1462
  lockfileOnly: true,
1374
1463
  });
1375
- const { stats, ignoredBuilds } = await headlessInstall({
1464
+ const { stats, ignoredBuilds } = await materializeOrDelegate(opts, () => headlessInstall({
1376
1465
  ...ctx,
1377
1466
  ...opts,
1378
1467
  currentEngine: {
@@ -1386,7 +1475,7 @@ const installInContext = async (projects, ctx, opts) => {
1386
1475
  wantedLockfile: result.newLockfile,
1387
1476
  useLockfile: opts.useLockfile && ctx.wantedLockfileIsModified,
1388
1477
  hoistWorkspacePackages: opts.hoistWorkspacePackages,
1389
- });
1478
+ }));
1390
1479
  return {
1391
1480
  ...result,
1392
1481
  stats,
@@ -1399,7 +1488,7 @@ const installInContext = async (projects, ctx, opts) => {
1399
1488
  ...opts,
1400
1489
  lockfileOnly: true,
1401
1490
  });
1402
- const { stats, ignoredBuilds } = await headlessInstall({
1491
+ const { stats, ignoredBuilds } = await materializeOrDelegate(opts, () => headlessInstall({
1403
1492
  ...ctx,
1404
1493
  ...opts,
1405
1494
  currentEngine: {
@@ -1413,13 +1502,29 @@ const installInContext = async (projects, ctx, opts) => {
1413
1502
  wantedLockfile: result.newLockfile,
1414
1503
  useLockfile: opts.useLockfile && ctx.wantedLockfileIsModified,
1415
1504
  hoistWorkspacePackages: opts.hoistWorkspacePackages,
1416
- });
1505
+ }));
1417
1506
  return {
1418
1507
  ...result,
1419
1508
  stats,
1420
1509
  ignoredBuilds,
1421
1510
  };
1422
1511
  }
1512
+ // Isolated `nodeLinker` (the default) with a non-frozen install:
1513
+ // pacquet doesn't ship a resolver yet, so split the install in two —
1514
+ // ask `_installInContext` for a `lockfileOnly` resolve pass (writes
1515
+ // `pnpm-lock.yaml`), then hand the freshly-written lockfile to
1516
+ // pacquet for the fetch / import / link / build phases. The frozen
1517
+ // branch is handled earlier in `tryFrozenInstall`; the hoisted
1518
+ // branch above already runs the same resolve-then-materialize
1519
+ // sequence (it had to even before pacquet existed). When no pacquet
1520
+ // is configured this falls through to the full single-pass install.
1521
+ if (opts.runPacquet != null && !opts.lockfileOnly) {
1522
+ const result = await _installInContext(projects, ctx, { ...opts, lockfileOnly: true });
1523
+ // The resolve pass above emitted a `pnpm:progress status:resolved`
1524
+ // per package; ask pacquet to drop its own duplicates.
1525
+ await opts.runPacquet({ filterResolvedProgress: true });
1526
+ return result;
1527
+ }
1423
1528
  return await _installInContext(projects, ctx, opts);
1424
1529
  }
1425
1530
  catch (error) { // eslint-disable-line
@@ -1831,13 +1936,18 @@ async function installFromPnpmRegistry(manifest, rootDir, opts, allInstallProjec
1831
1936
  skipped: new Set(),
1832
1937
  wantedLockfile: lockfile,
1833
1938
  };
1939
+ const { ignoredBuilds, stats } = await materializeOrDelegate(opts,
1834
1940
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1835
- const { ignoredBuilds, stats } = await headlessInstall(headlessOpts);
1941
+ () => headlessInstall(headlessOpts));
1836
1942
  return {
1837
1943
  updatedCatalogs: undefined,
1838
1944
  updatedManifest: manifest,
1839
1945
  ignoredBuilds,
1840
- stats,
1946
+ // Pacquet doesn't surface a structured stats return; default to
1947
+ // zeros so the agent-path's non-optional `stats` slot is filled.
1948
+ // The reporter still renders accurate counts from pacquet's
1949
+ // `pnpm:stats` log events.
1950
+ stats: stats ?? { added: 0, removed: 0, linkedToRoot: 0 },
1841
1951
  lockfile,
1842
1952
  // Server-side resolution (pnpm agent) enforces `minimumReleaseAge`
1843
1953
  // itself — the agent picks only mature versions and the lockfile
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/installing.deps-installer",
3
- "version": "1101.2.0",
3
+ "version": "1101.3.0",
4
4
  "description": "Fast, disk space efficient installation engine",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -62,60 +62,60 @@
62
62
  "ramda": "npm:@pnpm/ramda@0.28.1",
63
63
  "run-groups": "^5.0.0",
64
64
  "semver": "^7.7.2",
65
- "@pnpm/agent.client": "1.0.6",
66
- "@pnpm/bins.linker": "1100.0.7",
67
- "@pnpm/bins.remover": "1100.0.4",
68
- "@pnpm/building.after-install": "1101.0.13",
69
- "@pnpm/building.during-install": "1101.0.11",
70
- "@pnpm/building.policy": "1100.0.5",
65
+ "@pnpm/agent.client": "1.0.7",
66
+ "@pnpm/bins.linker": "1100.0.8",
67
+ "@pnpm/bins.remover": "1100.0.5",
68
+ "@pnpm/building.after-install": "1101.0.14",
69
+ "@pnpm/building.during-install": "1101.0.12",
70
+ "@pnpm/building.policy": "1100.0.6",
71
71
  "@pnpm/catalogs.protocol-parser": "1100.0.0",
72
72
  "@pnpm/catalogs.resolver": "1100.0.0",
73
73
  "@pnpm/catalogs.types": "1100.0.0",
74
74
  "@pnpm/config.matcher": "1100.0.1",
75
- "@pnpm/config.normalize-registries": "1100.0.3",
75
+ "@pnpm/config.normalize-registries": "1100.0.4",
76
76
  "@pnpm/config.parse-overrides": "1100.0.1",
77
77
  "@pnpm/constants": "1100.0.0",
78
- "@pnpm/core-loggers": "1100.1.0",
78
+ "@pnpm/core-loggers": "1100.1.1",
79
79
  "@pnpm/crypto.hash": "1100.0.1",
80
- "@pnpm/crypto.object-hasher": "1100.0.0",
81
- "@pnpm/deps.graph-hasher": "1100.2.0",
80
+ "@pnpm/deps.graph-hasher": "1100.2.1",
81
+ "@pnpm/deps.path": "1100.0.4",
82
82
  "@pnpm/deps.graph-sequencer": "1100.0.0",
83
- "@pnpm/deps.path": "1100.0.3",
84
83
  "@pnpm/error": "1100.0.0",
85
- "@pnpm/exec.lifecycle": "1100.0.11",
84
+ "@pnpm/exec.lifecycle": "1100.0.12",
86
85
  "@pnpm/fs.read-modules-dir": "1100.0.1",
87
- "@pnpm/fs.symlink-dependency": "1100.0.4",
88
- "@pnpm/hooks.read-package-hook": "1100.0.3",
89
- "@pnpm/hooks.types": "1100.0.7",
90
- "@pnpm/installing.context": "1100.0.11",
91
- "@pnpm/installing.deps-resolver": "1100.1.0",
92
- "@pnpm/installing.deps-restorer": "1101.1.3",
93
- "@pnpm/installing.linking.direct-dep-linker": "1100.0.4",
94
- "@pnpm/installing.linking.hoist": "1100.0.7",
95
- "@pnpm/installing.linking.modules-cleaner": "1100.1.2",
96
- "@pnpm/installing.modules-yaml": "1100.0.4",
97
- "@pnpm/installing.package-requester": "1101.0.7",
98
- "@pnpm/lockfile.filtering": "1100.1.1",
99
- "@pnpm/lockfile.fs": "1100.1.0",
100
- "@pnpm/lockfile.preferred-versions": "1100.0.10",
101
- "@pnpm/lockfile.pruner": "1100.0.6",
102
- "@pnpm/lockfile.settings-checker": "1100.0.11",
103
- "@pnpm/lockfile.to-pnp": "1100.0.9",
104
- "@pnpm/lockfile.utils": "1100.0.8",
105
- "@pnpm/lockfile.verification": "1100.0.11",
106
- "@pnpm/lockfile.walker": "1100.0.6",
107
- "@pnpm/patching.config": "1100.0.3",
108
- "@pnpm/pkg-manifest.utils": "1100.1.4",
86
+ "@pnpm/fs.symlink-dependency": "1100.0.5",
87
+ "@pnpm/installing.context": "1100.0.12",
88
+ "@pnpm/crypto.object-hasher": "1100.0.0",
89
+ "@pnpm/installing.deps-restorer": "1101.1.4",
90
+ "@pnpm/hooks.types": "1100.0.8",
91
+ "@pnpm/installing.deps-resolver": "1100.1.1",
92
+ "@pnpm/installing.linking.direct-dep-linker": "1100.0.5",
93
+ "@pnpm/installing.linking.hoist": "1100.0.8",
94
+ "@pnpm/installing.linking.modules-cleaner": "1100.1.3",
95
+ "@pnpm/installing.package-requester": "1101.0.8",
96
+ "@pnpm/installing.modules-yaml": "1100.0.5",
97
+ "@pnpm/lockfile.filtering": "1100.1.2",
98
+ "@pnpm/hooks.read-package-hook": "1100.0.4",
99
+ "@pnpm/lockfile.fs": "1100.1.1",
100
+ "@pnpm/lockfile.preferred-versions": "1100.0.11",
101
+ "@pnpm/lockfile.pruner": "1100.0.7",
102
+ "@pnpm/lockfile.to-pnp": "1100.0.10",
103
+ "@pnpm/lockfile.utils": "1100.0.9",
104
+ "@pnpm/lockfile.walker": "1100.0.7",
105
+ "@pnpm/lockfile.settings-checker": "1100.0.12",
106
+ "@pnpm/lockfile.verification": "1100.0.12",
107
+ "@pnpm/patching.config": "1100.0.4",
108
+ "@pnpm/pkg-manifest.utils": "1100.2.0",
109
109
  "@pnpm/resolving.parse-wanted-dependency": "1100.0.1",
110
- "@pnpm/resolving.resolver-base": "1100.2.0",
111
- "@pnpm/store.controller-types": "1100.1.0",
110
+ "@pnpm/resolving.resolver-base": "1100.3.0",
111
+ "@pnpm/store.controller-types": "1100.1.1",
112
112
  "@pnpm/store.index": "1100.1.0",
113
- "@pnpm/types": "1101.1.0",
114
- "@pnpm/workspace.project-manifest-reader": "1100.0.6"
113
+ "@pnpm/types": "1101.1.1",
114
+ "@pnpm/workspace.project-manifest-reader": "1100.0.7"
115
115
  },
116
116
  "peerDependencies": {
117
117
  "@pnpm/logger": ">=1001.0.0 <1002.0.0",
118
- "@pnpm/worker": "^1100.1.6"
118
+ "@pnpm/worker": "^1100.1.7"
119
119
  },
120
120
  "devDependencies": {
121
121
  "@jest/globals": "30.3.0",
@@ -138,21 +138,21 @@
138
138
  "symlink-dir": "^10.0.1",
139
139
  "write-json-file": "^7.0.0",
140
140
  "write-yaml-file": "^6.0.0",
141
- "@pnpm/assert-project": "1100.0.9",
142
- "@pnpm/assert-store": "1100.0.9",
143
- "@pnpm/installing.deps-installer": "1101.2.0",
144
- "@pnpm/lockfile.types": "1100.0.6",
141
+ "@pnpm/assert-project": "1100.0.10",
142
+ "@pnpm/assert-store": "1100.0.10",
143
+ "@pnpm/installing.deps-installer": "1101.3.0",
144
+ "@pnpm/lockfile.types": "1100.0.7",
145
145
  "@pnpm/logger": "1100.0.0",
146
146
  "@pnpm/network.git-utils": "1100.0.1",
147
- "@pnpm/pkg-manifest.reader": "1100.0.3",
148
- "@pnpm/prepare": "1100.0.9",
149
- "@pnpm/resolving.registry.types": "1100.0.3",
150
- "@pnpm/store.cafs": "1100.1.5",
147
+ "@pnpm/pkg-manifest.reader": "1100.0.4",
148
+ "@pnpm/prepare": "1100.0.10",
149
+ "@pnpm/resolving.registry.types": "1100.0.4",
151
150
  "@pnpm/store.path": "1100.0.1",
151
+ "@pnpm/store.cafs": "1100.1.6",
152
152
  "@pnpm/test-fixtures": "1100.0.0",
153
153
  "@pnpm/test-ipc-server": "1100.0.0",
154
- "@pnpm/testing.mock-agent": "1100.0.5",
155
- "@pnpm/testing.temp-store": "1100.1.0"
154
+ "@pnpm/testing.mock-agent": "1100.0.6",
155
+ "@pnpm/testing.temp-store": "1100.1.1"
156
156
  },
157
157
  "engines": {
158
158
  "node": ">=22.13"