@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.
- package/lib/install/extendInstallOptions.d.ts +35 -15
- package/lib/install/extendInstallOptions.js +14 -0
- package/lib/install/index.d.ts +18 -0
- package/lib/install/index.js +281 -58
- package/lib/install/link.d.ts +1 -1
- package/lib/install/link.js +109 -24
- package/lib/install/recordLockfileVerified.js +2 -2
- package/lib/install/verifyLockfileResolutions.d.ts +10 -6
- package/lib/install/verifyLockfileResolutions.js +60 -10
- package/package.json +68 -65
package/lib/install/index.js
CHANGED
|
@@ -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.
|
|
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
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
//
|
|
948
|
-
const originalLockfileForCheck =
|
|
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
|
-
//
|
|
1529
|
-
//
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
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
|
-
|
|
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,
|
package/lib/install/link.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type DepsStateCache } from '@pnpm/deps.graph-hasher';
|
|
2
|
-
import type
|
|
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';
|