@pnpm/installing.deps-installer 1101.8.0 → 1102.0.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 +26 -15
- package/lib/install/extendInstallOptions.js +12 -0
- package/lib/install/index.js +189 -47
- package/lib/install/recordLockfileVerified.js +2 -2
- package/lib/install/verifyLockfileResolutions.d.ts +10 -6
- package/lib/install/verifyLockfileResolutions.js +79 -16
- package/lib/install/verifyLockfileResolutionsCache.d.ts +7 -0
- package/lib/install/verifyLockfileResolutionsCache.js +4 -2
- package/package.json +69 -67
|
@@ -18,6 +18,7 @@ export interface StrictInstallOptions {
|
|
|
18
18
|
cleanupUnusedCatalogs: boolean;
|
|
19
19
|
frozenLockfile: boolean;
|
|
20
20
|
frozenLockfileIfExists: boolean;
|
|
21
|
+
frozenStore: boolean;
|
|
21
22
|
enableGlobalVirtualStore: boolean;
|
|
22
23
|
enablePnp: boolean;
|
|
23
24
|
extraBinPaths: string[];
|
|
@@ -205,23 +206,33 @@ export interface StrictInstallOptions {
|
|
|
205
206
|
packageVulnerabilityAudit?: PackageVulnerabilityAudit;
|
|
206
207
|
blockExoticSubdeps?: boolean;
|
|
207
208
|
/**
|
|
208
|
-
* Optional alternative install engine. When set, the
|
|
209
|
-
*
|
|
210
|
-
* layer constructs it (today:
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
209
|
+
* Optional alternative install engine. When set, the installer
|
|
210
|
+
* delegates the install to `run` instead of calling `headlessInstall`.
|
|
211
|
+
* The CLI layer constructs it (today: the pacquet binary installed via
|
|
212
|
+
* `configDependencies`, forwarding pnpm's own CLI argv); the installer
|
|
213
|
+
* treats it as an opaque "do the install" hook so it doesn't need to
|
|
214
|
+
* know about pacquet's binary path, CLI surface, or any settings that
|
|
215
|
+
* only pacquet consumes.
|
|
215
216
|
*
|
|
216
|
-
* `
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
217
|
+
* `supportsResolution` is `true` when the engine can resolve
|
|
218
|
+
* dependencies itself (pacquet >= 0.11.7). When `false` the installer
|
|
219
|
+
* runs its own resolve pass first and the engine only materializes the
|
|
220
|
+
* written lockfile.
|
|
221
|
+
*
|
|
222
|
+
* `run`'s `filterResolvedProgress` tells the helper to drop the
|
|
223
|
+
* engine's own `pnpm:progress status:resolved` events because pnpm
|
|
224
|
+
* already emitted one per package during a preceding lockfileOnly
|
|
225
|
+
* resolve pass. `resolve` tells the engine to do the resolution
|
|
226
|
+
* itself (non-frozen install). The frozen/materialize paths leave
|
|
227
|
+
* both unset.
|
|
221
228
|
*/
|
|
222
|
-
runPacquet?:
|
|
223
|
-
|
|
224
|
-
|
|
229
|
+
runPacquet?: {
|
|
230
|
+
supportsResolution: boolean;
|
|
231
|
+
run: (opts?: {
|
|
232
|
+
filterResolvedProgress?: boolean;
|
|
233
|
+
resolve?: boolean;
|
|
234
|
+
}) => Promise<void>;
|
|
235
|
+
};
|
|
225
236
|
/**
|
|
226
237
|
* If true, `mutateModules` does not emit the per-install `summary` log
|
|
227
238
|
* event. Used by `pnpm add -g` when it runs multiple isolated installs
|
|
@@ -27,6 +27,7 @@ const defaults = (opts) => {
|
|
|
27
27
|
force: false,
|
|
28
28
|
forceFullResolution: false,
|
|
29
29
|
frozenLockfile: false,
|
|
30
|
+
frozenStore: false,
|
|
30
31
|
hoistPattern: undefined,
|
|
31
32
|
publicHoistPattern: undefined,
|
|
32
33
|
hooks: {},
|
|
@@ -144,6 +145,17 @@ export function extendOptions(opts) {
|
|
|
144
145
|
throw new PnpmError('CONFIG_CONFLICT_LOCKFILE_ONLY_WITH_NO_LOCKFILE', `Cannot generate a ${WANTED_LOCKFILE} because lockfile is set to false`);
|
|
145
146
|
}
|
|
146
147
|
}
|
|
148
|
+
if (extendedOpts.frozenStore && extendedOpts.force) {
|
|
149
|
+
throw new PnpmError('CONFIG_CONFLICT_FROZEN_STORE_WITH_FORCE', 'Cannot use force together with frozenStore: --force re-imports packages into the store, which is opened read-only when frozenStore is enabled');
|
|
150
|
+
}
|
|
151
|
+
if (extendedOpts.frozenStore) {
|
|
152
|
+
// The side-effects cache is written into the store, which frozenStore opens
|
|
153
|
+
// read-only. Caching is an optimization, not a correctness requirement, so
|
|
154
|
+
// force it off rather than failing (the writable seed-build already
|
|
155
|
+
// populated it). Without this, a build under frozenStore (e.g. with the
|
|
156
|
+
// global virtual store disabled) would attempt a store write.
|
|
157
|
+
extendedOpts.sideEffectsCacheWrite = false;
|
|
158
|
+
}
|
|
147
159
|
if (extendedOpts.userAgent.startsWith('npm/')) {
|
|
148
160
|
extendedOpts.userAgent = `${extendedOpts.packageManager.name}/${extendedOpts.packageManager.version} ${extendedOpts.userAgent}`;
|
|
149
161
|
}
|
package/lib/install/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { getContext } from '@pnpm/installing.context';
|
|
|
15
15
|
import { getWantedDependencies, resolveDependencies, } from '@pnpm/installing.deps-resolver';
|
|
16
16
|
import { extendProjectsWithTargetDirs, headlessInstall } from '@pnpm/installing.deps-restorer';
|
|
17
17
|
import { writeModulesManifest } from '@pnpm/installing.modules-yaml';
|
|
18
|
-
import { cleanGitBranchLockfiles, getWantedLockfileName, isEmptyLockfile, readWantedLockfileFile, writeCurrentLockfile, writeLockfiles, writeWantedLockfile, } from '@pnpm/lockfile.fs';
|
|
18
|
+
import { cleanGitBranchLockfiles, getWantedLockfileName, isEmptyLockfile, readEnvLockfile, readWantedLockfile, readWantedLockfileFile, writeCurrentLockfile, writeEnvLockfile, writeLockfiles, writeWantedLockfile, } from '@pnpm/lockfile.fs';
|
|
19
19
|
import { getPreferredVersionsFromLockfileAndManifests } from '@pnpm/lockfile.preferred-versions';
|
|
20
20
|
import { calcPatchHashes, createOverridesMapFromParsed, getOutdatedLockfileSetting, } from '@pnpm/lockfile.settings-checker';
|
|
21
21
|
import { writePnpFile } from '@pnpm/lockfile.to-pnp';
|
|
@@ -168,15 +168,24 @@ export async function mutateModules(projects, maybeOpts) {
|
|
|
168
168
|
// attaching their own verifiers). The threat model is a lockfile that
|
|
169
169
|
// someone else resolved — committed to the repo, restored from a CI
|
|
170
170
|
// 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.
|
|
171
|
+
// resolver's own filters already cover fresh resolution.
|
|
174
172
|
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
173
|
+
// The verification is kicked off here, right after the lockfile is loaded,
|
|
174
|
+
// but not awaited inline — it would otherwise block every later install
|
|
175
|
+
// stage on per-entry registry round trips. Its synchronous prologue (cache
|
|
176
|
+
// lookup, lockfile hashing, candidate collection) runs now against the
|
|
177
|
+
// pristine lockfile, so the async fan-out reads a stable snapshot even
|
|
178
|
+
// while the install mutates `ctx.wantedLockfile` concurrently. The verdict
|
|
179
|
+
// is reconciled with the install in `settleInstall`: a failure aborts the
|
|
180
|
+
// install even mid-flight, and an install that finishes first is held back
|
|
181
|
+
// until the verdict arrives.
|
|
182
|
+
//
|
|
183
|
+
// Skipped when we already know pacquet will run the install: pacquet
|
|
184
|
+
// applies the same resolver-policy gate (port of this function) whether
|
|
185
|
+
// it materializes a frozen lockfile or re-resolves from the manifests,
|
|
186
|
+
// so re-running here would duplicate the work — and for
|
|
187
|
+
// `minimumReleaseAge` in strict mode each lockfile entry is an HTTP
|
|
188
|
+
// probe.
|
|
180
189
|
//
|
|
181
190
|
// The predicate mirrors every short-circuit `tryFrozenInstall` checks
|
|
182
191
|
// before reaching the pacquet branch: anything that would make it
|
|
@@ -186,13 +195,28 @@ export async function mutateModules(projects, maybeOpts) {
|
|
|
186
195
|
// isn't known here — so verification still runs in that window, the
|
|
187
196
|
// duplicate is bounded to it.
|
|
188
197
|
const willDelegateToPacquet = opts.runPacquet != null &&
|
|
198
|
+
opts.useLockfile &&
|
|
199
|
+
!opts.useGitBranchLockfile &&
|
|
200
|
+
!opts.mergeGitBranchLockfiles &&
|
|
201
|
+
opts.lockfileCheck == null &&
|
|
202
|
+
opts.enableModulesDir &&
|
|
189
203
|
installsOnly &&
|
|
190
204
|
!opts.lockfileOnly &&
|
|
191
205
|
!opts.fixLockfile &&
|
|
192
206
|
!opts.dedupe &&
|
|
193
207
|
!ctx.lockfileHadConflicts &&
|
|
194
|
-
|
|
195
|
-
|
|
208
|
+
(
|
|
209
|
+
// Frozen materialization: pacquet reads the existing lockfile and
|
|
210
|
+
// re-applies the resolver-policy gate as it walks it.
|
|
211
|
+
(ctx.existsNonEmptyWantedLockfile &&
|
|
212
|
+
(opts.frozenLockfile === true || opts.frozenLockfileIfExists === true)) ||
|
|
213
|
+
// Resolving install: pacquet (>= 0.11.7) re-resolves from the
|
|
214
|
+
// manifests itself — applying the policy during fresh resolution —
|
|
215
|
+
// so the existing lockfile entries verified here would just be
|
|
216
|
+
// discarded. If a policy handler is active, keep resolution in pnpm
|
|
217
|
+
// so violations can be returned to the command layer.
|
|
218
|
+
(opts.saveLockfile && opts.runPacquet.supportsResolution && opts.frozenLockfile !== true && opts.nodeLinker !== 'hoisted' && opts.handleResolutionPolicyViolations == null));
|
|
219
|
+
let verifyLockfilePromise;
|
|
196
220
|
if (!willDelegateToPacquet && !opts.trustLockfile) {
|
|
197
221
|
const cacheActive = opts.cacheDir != null && opts.resolutionVerifiers.length > 0;
|
|
198
222
|
const wantedLockfilePath = cacheActive
|
|
@@ -201,21 +225,21 @@ export async function mutateModules(projects, maybeOpts) {
|
|
|
201
225
|
mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
|
|
202
226
|
}))
|
|
203
227
|
: 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
|
-
}
|
|
228
|
+
verifyLockfilePromise = verifyLockfileResolutions(ctx.wantedLockfile, opts.resolutionVerifiers, {
|
|
229
|
+
cacheDir: opts.cacheDir,
|
|
230
|
+
lockfilePath: wantedLockfilePath,
|
|
231
|
+
});
|
|
232
|
+
// Keep the rejection from going unhandled in the window before
|
|
233
|
+
// `settleInstall` awaits the verdict — a preResolution hook or the
|
|
234
|
+
// install kickoff below could throw and bail out before we get there.
|
|
235
|
+
verifyLockfilePromise.catch(() => { });
|
|
218
236
|
}
|
|
237
|
+
// Gate passed down to the build phase: fetching and linking overlap with
|
|
238
|
+
// verification, but no dependency lifecycle script may run until the verdict
|
|
239
|
+
// is in. Awaiting the promise here throws if verification failed, aborting
|
|
240
|
+
// before any script executes. `settleInstall` is the catch-all that still
|
|
241
|
+
// reconciles the verdict on paths that never reach the build phase.
|
|
242
|
+
const verifyLockfile = verifyLockfilePromise && (() => verifyLockfilePromise);
|
|
219
243
|
if (opts.hooks.preResolution) {
|
|
220
244
|
for (const preResolution of opts.hooks.preResolution) {
|
|
221
245
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -251,7 +275,7 @@ export async function mutateModules(projects, maybeOpts) {
|
|
|
251
275
|
}
|
|
252
276
|
}
|
|
253
277
|
}
|
|
254
|
-
const result = await _install();
|
|
278
|
+
const result = await settleInstall(_install(), verifyLockfilePromise);
|
|
255
279
|
// @ts-expect-error
|
|
256
280
|
if (global['verifiedFileIntegrity'] > 1000) {
|
|
257
281
|
// @ts-expect-error
|
|
@@ -302,6 +326,31 @@ export async function mutateModules(projects, maybeOpts) {
|
|
|
302
326
|
ignoredBuilds,
|
|
303
327
|
resolutionPolicyViolations: result.resolutionPolicyViolations ?? [],
|
|
304
328
|
};
|
|
329
|
+
// Reconcile the install with the lockfile verification that runs alongside
|
|
330
|
+
// it. The verification verdict is awaited first so it takes precedence and
|
|
331
|
+
// aborts as soon as it fails, even while the install is still in flight —
|
|
332
|
+
// matching the original sequencing where verification gated the install, so
|
|
333
|
+
// a rejected lockfile surfaces its own error rather than whatever the
|
|
334
|
+
// concurrent install happened to throw. Only once verification passes is the
|
|
335
|
+
// install's result (or error) surfaced. detachReporter mirrors the success
|
|
336
|
+
// path's cleanup so a long-lived process doesn't leak the stream listener on
|
|
337
|
+
// a rejected install.
|
|
338
|
+
async function settleInstall(install, verification) {
|
|
339
|
+
if (verification == null)
|
|
340
|
+
return install;
|
|
341
|
+
// Handle the install's eventual rejection up front so a fail-fast
|
|
342
|
+
// verification throw below doesn't leave the still-running install
|
|
343
|
+
// unhandled.
|
|
344
|
+
install.catch(() => { });
|
|
345
|
+
try {
|
|
346
|
+
await verification;
|
|
347
|
+
return await install;
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
detachReporter();
|
|
351
|
+
throw err;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
305
354
|
async function _install() {
|
|
306
355
|
const scriptsOpts = {
|
|
307
356
|
extraBinPaths: opts.extraBinPaths,
|
|
@@ -589,6 +638,7 @@ export async function mutateModules(projects, maybeOpts) {
|
|
|
589
638
|
scriptsOpts,
|
|
590
639
|
updateLockfileMinorVersion: true,
|
|
591
640
|
patchedDependencies: patchGroups,
|
|
641
|
+
verifyLockfile,
|
|
592
642
|
});
|
|
593
643
|
return {
|
|
594
644
|
updatedCatalogs: result.updatedCatalogs,
|
|
@@ -717,9 +767,9 @@ Note that in CI environments, this setting is enabled by default.`,
|
|
|
717
767
|
else {
|
|
718
768
|
logger.info({ message: 'Lockfile is up to date, resolution step is skipped', prefix: opts.lockfileDir });
|
|
719
769
|
}
|
|
720
|
-
if (opts.runPacquet != null) {
|
|
770
|
+
if (opts.runPacquet != null && opts.useLockfile && !opts.useGitBranchLockfile && !opts.mergeGitBranchLockfiles && opts.lockfileCheck == null && opts.enableModulesDir) {
|
|
721
771
|
try {
|
|
722
|
-
await opts.runPacquet();
|
|
772
|
+
await opts.runPacquet.run();
|
|
723
773
|
}
|
|
724
774
|
catch (err) {
|
|
725
775
|
// Same reasoning as the verifyLockfileResolutions catch above: this
|
|
@@ -755,6 +805,7 @@ Note that in CI environments, this setting is enabled by default.`,
|
|
|
755
805
|
pruneVirtualStore,
|
|
756
806
|
wantedLockfile: maybeOpts.ignorePackageManifest ? undefined : ctx.wantedLockfile,
|
|
757
807
|
useLockfile: opts.useLockfile && ctx.wantedLockfileIsModified,
|
|
808
|
+
verifyLockfile,
|
|
758
809
|
});
|
|
759
810
|
if (opts.useLockfile && opts.saveLockfile && opts.mergeGitBranchLockfiles ||
|
|
760
811
|
!upToDateLockfileMajorVersion && !opts.frozenLockfile) {
|
|
@@ -1189,6 +1240,8 @@ const _installInContext = async (projects, ctx, opts) => {
|
|
|
1189
1240
|
...makeNodeRequireOption(path.join(opts.lockfileDir, '.pnp.cjs')),
|
|
1190
1241
|
};
|
|
1191
1242
|
}
|
|
1243
|
+
// Dependency lifecycle scripts must not run on an unverified lockfile.
|
|
1244
|
+
await opts.verifyLockfile?.();
|
|
1192
1245
|
ignoredBuilds = (await buildModules(dependenciesGraph, rootNodes, {
|
|
1193
1246
|
allowBuild: opts.allowBuild,
|
|
1194
1247
|
childConcurrency: opts.childConcurrency,
|
|
@@ -1210,6 +1263,7 @@ const _installInContext = async (projects, ctx, opts) => {
|
|
|
1210
1263
|
unsafePerm: opts.unsafePerm,
|
|
1211
1264
|
userAgent: opts.userAgent,
|
|
1212
1265
|
enableGlobalVirtualStore: opts.enableGlobalVirtualStore,
|
|
1266
|
+
frozenStore: opts.frozenStore,
|
|
1213
1267
|
})).ignoredBuilds;
|
|
1214
1268
|
if (ctx.modulesFile?.ignoredBuilds?.size) {
|
|
1215
1269
|
ignoredBuilds ??= new Set();
|
|
@@ -1332,6 +1386,10 @@ const _installInContext = async (projects, ctx, opts) => {
|
|
|
1332
1386
|
};
|
|
1333
1387
|
}
|
|
1334
1388
|
const projectsToBeBuilt = projectsWithTargetDirs.filter(({ mutation }) => mutation === 'install');
|
|
1389
|
+
// The projects' own lifecycle scripts import dependency code linked
|
|
1390
|
+
// from the lockfile, so they are held to the same gate as dependency
|
|
1391
|
+
// builds — also when no new dep paths made the buildModules branch run.
|
|
1392
|
+
await opts.verifyLockfile?.();
|
|
1335
1393
|
await runLifecycleHooksConcurrently(['preinstall', 'install', 'postinstall', 'preprepare', 'prepare', 'postprepare'], projectsToBeBuilt, opts.childConcurrency, opts.scriptsOpts);
|
|
1336
1394
|
}
|
|
1337
1395
|
}
|
|
@@ -1413,6 +1471,28 @@ const _installInContext = async (projects, ctx, opts) => {
|
|
|
1413
1471
|
function allMutationsAreInstalls(projects) {
|
|
1414
1472
|
return projects.every((project) => project.mutation === 'install' && !project.update && !project.updateMatching);
|
|
1415
1473
|
}
|
|
1474
|
+
/**
|
|
1475
|
+
* The `InstallFunctionResult` for an install pacquet resolved and
|
|
1476
|
+
* materialized end-to-end. pacquet wrote `pnpm-lock.yaml` and the
|
|
1477
|
+
* `node_modules` tree itself. `ctx.wantedLockfile` has already been
|
|
1478
|
+
* refreshed from disk, and pacquet reports its own stats / ignored-builds
|
|
1479
|
+
* via NDJSON, so the structured `stats` / `ignoredBuilds` fall back to
|
|
1480
|
+
* their no-op defaults. Resolution-policy handlers are guarded out before
|
|
1481
|
+
* this path, so there are no command-layer policy violations to return.
|
|
1482
|
+
* Manifests are returned unchanged — this path only runs for plain
|
|
1483
|
+
* installs, which don't rewrite `package.json`.
|
|
1484
|
+
*/
|
|
1485
|
+
function pacquetResolveResult(projects, ctx) {
|
|
1486
|
+
return {
|
|
1487
|
+
newLockfile: ctx.wantedLockfile,
|
|
1488
|
+
projects: projects.map((project) => ({
|
|
1489
|
+
manifest: project.originalManifest ?? project.manifest,
|
|
1490
|
+
rootDir: project.rootDir,
|
|
1491
|
+
})),
|
|
1492
|
+
depsRequiringBuild: [],
|
|
1493
|
+
resolutionPolicyViolations: [],
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1416
1496
|
/**
|
|
1417
1497
|
* Run the pacquet binary if it's configured, otherwise run the JS
|
|
1418
1498
|
* `headlessInstall`. Callers can hand off any code path that materializes
|
|
@@ -1429,13 +1509,17 @@ function allMutationsAreInstalls(projects) {
|
|
|
1429
1509
|
* stats record and a no-op ignoredBuilds iteration).
|
|
1430
1510
|
*/
|
|
1431
1511
|
async function materializeOrDelegate(opts, runHeadlessInstall) {
|
|
1432
|
-
if (opts.runPacquet != null
|
|
1512
|
+
if (opts.runPacquet != null &&
|
|
1513
|
+
opts.useLockfile !== false &&
|
|
1514
|
+
opts.saveLockfile !== false &&
|
|
1515
|
+
opts.useGitBranchLockfile !== true &&
|
|
1516
|
+
opts.mergeGitBranchLockfiles !== true) {
|
|
1433
1517
|
// Reached only from the resolve-then-materialize call sites
|
|
1434
1518
|
// (workspace-partial, hoisted-linker, pnpr server install). Each ran a
|
|
1435
1519
|
// lockfileOnly resolve pass that emitted one
|
|
1436
1520
|
// `pnpm:progress status:resolved` per package, so pacquet's
|
|
1437
1521
|
// duplicate `resolved` events would double the reporter's count.
|
|
1438
|
-
await opts.runPacquet({ filterResolvedProgress: true });
|
|
1522
|
+
await opts.runPacquet.run({ filterResolvedProgress: true });
|
|
1439
1523
|
return {};
|
|
1440
1524
|
}
|
|
1441
1525
|
return runHeadlessInstall();
|
|
@@ -1446,7 +1530,7 @@ const installInContext = async (projects, ctx, opts) => {
|
|
|
1446
1530
|
if (!opts.frozenLockfile && opts.useLockfile) {
|
|
1447
1531
|
const allProjectsLocatedInsideWorkspace = Object.values(ctx.projects)
|
|
1448
1532
|
.filter((project) => isPathInsideWorkspace(project.rootDirRealPath ?? project.rootDir));
|
|
1449
|
-
if (allProjectsLocatedInsideWorkspace.length > projects.length) {
|
|
1533
|
+
if (allProjectsLocatedInsideWorkspace.length > projects.length && opts.lockfileCheck == null && opts.enableModulesDir) {
|
|
1450
1534
|
const newProjects = [...projects];
|
|
1451
1535
|
const getWantedDepsOpts = {
|
|
1452
1536
|
autoInstallPeers: opts.autoInstallPeers,
|
|
@@ -1498,7 +1582,7 @@ const installInContext = async (projects, ctx, opts) => {
|
|
|
1498
1582
|
};
|
|
1499
1583
|
}
|
|
1500
1584
|
}
|
|
1501
|
-
if (opts.nodeLinker === 'hoisted' && !opts.lockfileOnly) {
|
|
1585
|
+
if (opts.nodeLinker === 'hoisted' && !opts.lockfileOnly && opts.lockfileCheck == null && opts.enableModulesDir) {
|
|
1502
1586
|
const result = await _installInContext(projects, ctx, {
|
|
1503
1587
|
...opts,
|
|
1504
1588
|
lockfileOnly: true,
|
|
@@ -1524,20 +1608,69 @@ const installInContext = async (projects, ctx, opts) => {
|
|
|
1524
1608
|
ignoredBuilds,
|
|
1525
1609
|
};
|
|
1526
1610
|
}
|
|
1527
|
-
// Isolated `nodeLinker` (the default) with a non-frozen install
|
|
1528
|
-
//
|
|
1529
|
-
//
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1611
|
+
// Isolated `nodeLinker` (the default) with a non-frozen install.
|
|
1612
|
+
// The frozen branch is handled earlier in `tryFrozenInstall`; the
|
|
1613
|
+
// hoisted branch above runs a resolve-then-materialize sequence.
|
|
1614
|
+
if (opts.runPacquet != null && opts.useLockfile && opts.saveLockfile && !opts.useGitBranchLockfile && !opts.mergeGitBranchLockfiles && !opts.lockfileOnly && opts.lockfileCheck == null && opts.enableModulesDir) {
|
|
1615
|
+
// pacquet >= 0.11.7 resolves itself: hand it the whole install
|
|
1616
|
+
// (resolve + fetch + import + link + build, writing the lockfile)
|
|
1617
|
+
// in a single non-frozen pass. Only for plain installs — `add` /
|
|
1618
|
+
// `update` / `remove` need pnpm to mutate the manifests and
|
|
1619
|
+
// resolve the new specs first (pacquet's `install` reads
|
|
1620
|
+
// package.json from disk, which pnpm hasn't rewritten yet).
|
|
1621
|
+
if (opts.runPacquet.supportsResolution && !opts.frozenLockfile && opts.handleResolutionPolicyViolations == null && allMutationsAreInstalls(projects)) {
|
|
1622
|
+
// `configDependencies` are recorded in a YAML document prepended
|
|
1623
|
+
// to `pnpm-lock.yaml` — purely a pnpm concept that pacquet doesn't
|
|
1624
|
+
// model. Capture it before pacquet rewrites the lockfile and
|
|
1625
|
+
// restore it afterwards (`writeEnvLockfile` re-reads pacquet's main
|
|
1626
|
+
// document and re-prepends the env document), otherwise the next
|
|
1627
|
+
// `--frozen-lockfile` install fails its config-deps freshness gate.
|
|
1628
|
+
// The restore runs even if pacquet fails partway: a non-zero exit can
|
|
1629
|
+
// still leave a rewritten lockfile behind, so the env document must be
|
|
1630
|
+
// put back regardless.
|
|
1631
|
+
const envLockfile = await readEnvLockfile(ctx.lockfileDir);
|
|
1632
|
+
let pacquetError;
|
|
1633
|
+
try {
|
|
1634
|
+
await opts.runPacquet.run({ resolve: true });
|
|
1635
|
+
}
|
|
1636
|
+
catch (err) {
|
|
1637
|
+
pacquetError = err;
|
|
1638
|
+
throw err;
|
|
1639
|
+
}
|
|
1640
|
+
finally {
|
|
1641
|
+
if (envLockfile != null) {
|
|
1642
|
+
await writeEnvLockfile(ctx.lockfileDir, envLockfile).catch((restoreErr) => {
|
|
1643
|
+
if (pacquetError == null) {
|
|
1644
|
+
throw restoreErr;
|
|
1645
|
+
}
|
|
1646
|
+
logger.warn({
|
|
1647
|
+
error: restoreErr,
|
|
1648
|
+
message: `Failed to restore the configDependencies document in pnpm-lock.yaml: ${restoreErr.message}`,
|
|
1649
|
+
prefix: ctx.lockfileDir,
|
|
1650
|
+
});
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
const wantedLockfile = await readWantedLockfile(ctx.lockfileDir, {
|
|
1655
|
+
ignoreIncompatible: opts.force || opts.ci === true,
|
|
1656
|
+
mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
|
|
1657
|
+
useGitBranchLockfile: opts.useGitBranchLockfile,
|
|
1658
|
+
wantedVersions: [LOCKFILE_VERSION],
|
|
1659
|
+
});
|
|
1660
|
+
if (wantedLockfile == null) {
|
|
1661
|
+
throw new PnpmError('PACQUET_LOCKFILE_READ_FAILED', `pacquet did not write a readable ${WANTED_LOCKFILE}`);
|
|
1662
|
+
}
|
|
1663
|
+
ctx.wantedLockfile = wantedLockfile;
|
|
1664
|
+
return pacquetResolveResult(projects, ctx);
|
|
1665
|
+
}
|
|
1666
|
+
// Older pacquet can only materialize: split the install in two —
|
|
1667
|
+
// ask `_installInContext` for a `lockfileOnly` resolve pass (writes
|
|
1668
|
+
// `pnpm-lock.yaml`), then hand the freshly-written lockfile to
|
|
1669
|
+
// pacquet for the fetch / import / link / build phases. The resolve
|
|
1670
|
+
// pass emitted a `pnpm:progress status:resolved` per package; ask
|
|
1671
|
+
// pacquet to drop its own duplicates.
|
|
1537
1672
|
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 });
|
|
1673
|
+
await opts.runPacquet.run({ filterResolvedProgress: true });
|
|
1541
1674
|
return result;
|
|
1542
1675
|
}
|
|
1543
1676
|
return await _installInContext(projects, ctx, opts);
|
|
@@ -1811,6 +1944,15 @@ async function mutateModulesViaPnpr(projects, opts) {
|
|
|
1811
1944
|
* and links packages into node_modules — like a normal install.
|
|
1812
1945
|
*/
|
|
1813
1946
|
async function installViaPnprServer(manifest, rootDir, opts, allInstallProjects) {
|
|
1947
|
+
// The pnpr server path re-resolves and persists new `index.db` entries plus a
|
|
1948
|
+
// freshly written lockfile, so it inherently writes the store. `frozenStore`
|
|
1949
|
+
// promises the store is complete and read-only, so the two are mutually
|
|
1950
|
+
// exclusive — and the unconditional pnpr gate means this path runs even under
|
|
1951
|
+
// `--offline --frozen-lockfile`, so refuse up front with guidance instead of
|
|
1952
|
+
// crashing later on the read-only `index.db` open.
|
|
1953
|
+
if (opts.frozenStore) {
|
|
1954
|
+
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.' });
|
|
1955
|
+
}
|
|
1814
1956
|
// The pnpr server path skips client-side resolution, so resolver-side policies
|
|
1815
1957
|
// can't be enforced locally. `minimumReleaseAge` is forwarded to the
|
|
1816
1958
|
// pnpr server and enforced server-side. `trustPolicy` has no server-side
|
|
@@ -1820,7 +1962,7 @@ async function installViaPnprServer(manifest, rootDir, opts, allInstallProjects)
|
|
|
1820
1962
|
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
1963
|
}
|
|
1822
1964
|
const { resolveViaPnprServer } = await import('@pnpm/pnpr.client');
|
|
1823
|
-
const { createGetAuthHeaderByURI, getAuthHeadersFromCreds } = await import('@pnpm/network.auth-header');
|
|
1965
|
+
const { createGetAuthHeaderByURI, getAuthHeadersByScope, getAuthHeadersFromCreds } = await import('@pnpm/network.auth-header');
|
|
1824
1966
|
// Forward the whole credential map (the registries a graph touches
|
|
1825
1967
|
// aren't known up front), so the server attaches the right token per
|
|
1826
1968
|
// URL. `authorization` also identifies the caller to pnpr's gate.
|
|
@@ -1856,7 +1998,7 @@ async function installViaPnprServer(manifest, rootDir, opts, allInstallProjects)
|
|
|
1856
1998
|
projects: projectsList,
|
|
1857
1999
|
registry: opts.registries?.default,
|
|
1858
2000
|
namedRegistries: opts.namedRegistries,
|
|
1859
|
-
authHeaders: forwardedAuthHeaders,
|
|
2001
|
+
authHeaders: getAuthHeadersByScope(forwardedAuthHeaders),
|
|
1860
2002
|
authorization: pnprAuthorization,
|
|
1861
2003
|
overrides: opts.overrides,
|
|
1862
2004
|
minimumReleaseAge: opts.minimumReleaseAge,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { hashObject } from '@pnpm/crypto.object-hasher';
|
|
2
|
-
import {
|
|
2
|
+
import { withOfflineCheckCacheIdentities } from './verifyLockfileResolutions.js';
|
|
3
3
|
import { recordVerification } from './verifyLockfileResolutionsCache.js';
|
|
4
4
|
/**
|
|
5
5
|
* Records the post-resolution lockfile as verified so the next install
|
|
@@ -18,7 +18,7 @@ export function recordLockfileVerified(opts) {
|
|
|
18
18
|
return;
|
|
19
19
|
recordVerification(opts.cacheDir, {
|
|
20
20
|
lockfilePath: opts.lockfilePath,
|
|
21
|
-
verifiers:
|
|
21
|
+
verifiers: withOfflineCheckCacheIdentities(opts.resolutionVerifiers),
|
|
22
22
|
hashLockfile: () => hashObject(opts.lockfile),
|
|
23
23
|
});
|
|
24
24
|
}
|
|
@@ -3,15 +3,19 @@ import type { ResolutionPolicyViolation, ResolutionVerifier } from '@pnpm/resolv
|
|
|
3
3
|
import { type VerifierCacheIdentity } from './verifyLockfileResolutionsCache.js';
|
|
4
4
|
export type { ResolutionPolicyViolation };
|
|
5
5
|
export declare const RESOLUTION_SHAPE_MISMATCH_VIOLATION_CODE = "RESOLUTION_SHAPE_MISMATCH";
|
|
6
|
+
export declare const INVALID_DEPENDENCY_ALIAS_CODE = "INVALID_DEPENDENCY_NAME";
|
|
6
7
|
/**
|
|
7
8
|
* Every verifier list that flows into the verification cache must carry
|
|
8
|
-
* the
|
|
9
|
-
* existed cannot stat-fast-path around
|
|
10
|
-
*
|
|
11
|
-
* the
|
|
12
|
-
*
|
|
9
|
+
* the always-on offline structural checks' identities, so a record
|
|
10
|
+
* written before one of those rules existed cannot stat-fast-path around
|
|
11
|
+
* it — its missing flag fails `canTrustPastCheck`, forcing a
|
|
12
|
+
* re-verification that runs the new check. Used by the gate itself and by
|
|
13
|
+
* {@link recordLockfileVerified}, whose freshly-resolved lockfile
|
|
14
|
+
* satisfies these invariants by construction (the resolver validates
|
|
15
|
+
* aliases at manifest-read time and derives every resolution key from the
|
|
16
|
+
* resolution it just produced).
|
|
13
17
|
*/
|
|
14
|
-
export declare function
|
|
18
|
+
export declare function withOfflineCheckCacheIdentities(verifiers: readonly VerifierCacheIdentity[]): VerifierCacheIdentity[];
|
|
15
19
|
export interface VerifyLockfileResolutionsOptions {
|
|
16
20
|
concurrency?: number;
|
|
17
21
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { lockfileVerificationLogger } from '@pnpm/core-loggers';
|
|
2
2
|
import { hashObject } from '@pnpm/crypto.object-hasher';
|
|
3
3
|
import { PnpmError } from '@pnpm/error';
|
|
4
|
+
import { isValidDependencyAlias } from '@pnpm/installing.deps-resolver';
|
|
4
5
|
import { isGitHostedTarballUrl, nameVerFromPkgSnapshot } from '@pnpm/lockfile.utils';
|
|
5
6
|
import pLimit from 'p-limit';
|
|
6
7
|
import { recordVerification, tryLockfileVerificationCache, } from './verifyLockfileResolutionsCache.js';
|
|
@@ -8,25 +9,34 @@ import { recordVerification, tryLockfileVerificationCache, } from './verifyLockf
|
|
|
8
9
|
// (e.g. a poisoned lockfile) doesn't flood the terminal / CI log; the full
|
|
9
10
|
// count is in the header and the remainder is summarized at the end.
|
|
10
11
|
const MAX_VIOLATIONS_TO_PRINT = 20;
|
|
11
|
-
//
|
|
12
|
-
// (Math.min(
|
|
12
|
+
// 64 mirrors the floor of pnpm's package-requester network-concurrency
|
|
13
|
+
// (Math.min(96, Math.max(workers*3, 64))); keep them aligned so the
|
|
13
14
|
// verification pass doesn't push past what the rest of the install respects.
|
|
14
|
-
const DEFAULT_CONCURRENCY =
|
|
15
|
+
const DEFAULT_CONCURRENCY = 64;
|
|
15
16
|
export const RESOLUTION_SHAPE_MISMATCH_VIOLATION_CODE = 'RESOLUTION_SHAPE_MISMATCH';
|
|
17
|
+
// Same code the sink-level guards (`safeJoinModulesDir`) throw.
|
|
18
|
+
export const INVALID_DEPENDENCY_ALIAS_CODE = 'INVALID_DEPENDENCY_NAME';
|
|
16
19
|
const RESOLUTION_SHAPE_CACHE_IDENTITY = {
|
|
17
20
|
policy: { resolutionShapeCheck: true },
|
|
18
21
|
canTrustPastCheck: (cached) => cached.resolutionShapeCheck === true,
|
|
19
22
|
};
|
|
23
|
+
const DEPENDENCY_ALIAS_CACHE_IDENTITY = {
|
|
24
|
+
policy: { dependencyAliasCheck: true },
|
|
25
|
+
canTrustPastCheck: (cached) => cached.dependencyAliasCheck === true,
|
|
26
|
+
};
|
|
20
27
|
/**
|
|
21
28
|
* Every verifier list that flows into the verification cache must carry
|
|
22
|
-
* the
|
|
23
|
-
* existed cannot stat-fast-path around
|
|
24
|
-
*
|
|
25
|
-
* the
|
|
26
|
-
*
|
|
29
|
+
* the always-on offline structural checks' identities, so a record
|
|
30
|
+
* written before one of those rules existed cannot stat-fast-path around
|
|
31
|
+
* it — its missing flag fails `canTrustPastCheck`, forcing a
|
|
32
|
+
* re-verification that runs the new check. Used by the gate itself and by
|
|
33
|
+
* {@link recordLockfileVerified}, whose freshly-resolved lockfile
|
|
34
|
+
* satisfies these invariants by construction (the resolver validates
|
|
35
|
+
* aliases at manifest-read time and derives every resolution key from the
|
|
36
|
+
* resolution it just produced).
|
|
27
37
|
*/
|
|
28
|
-
export function
|
|
29
|
-
return [...verifiers, RESOLUTION_SHAPE_CACHE_IDENTITY];
|
|
38
|
+
export function withOfflineCheckCacheIdentities(verifiers) {
|
|
39
|
+
return [...verifiers, RESOLUTION_SHAPE_CACHE_IDENTITY, DEPENDENCY_ALIAS_CACHE_IDENTITY];
|
|
30
40
|
}
|
|
31
41
|
/**
|
|
32
42
|
* Policy-neutral pass that asks every resolver-supplied
|
|
@@ -65,7 +75,7 @@ export async function verifyLockfileResolutions(lockfile, verifiers, options) {
|
|
|
65
75
|
const cache = options?.cacheDir && options?.lockfilePath
|
|
66
76
|
? { cacheDir: options.cacheDir, lockfilePath: options.lockfilePath }
|
|
67
77
|
: undefined;
|
|
68
|
-
const cacheVerifiers =
|
|
78
|
+
const cacheVerifiers = withOfflineCheckCacheIdentities(verifiers);
|
|
69
79
|
let cachePrecomputed;
|
|
70
80
|
// hashObject streams and is key-order-stable, unlike JSON.stringify.
|
|
71
81
|
let cachedHash;
|
|
@@ -80,18 +90,34 @@ export async function verifyLockfileResolutions(lockfile, verifiers, options) {
|
|
|
80
90
|
verifiers: cacheVerifiers,
|
|
81
91
|
hashLockfile,
|
|
82
92
|
});
|
|
83
|
-
if (result.hit)
|
|
93
|
+
if (result.hit) {
|
|
94
|
+
// A silent short-circuit looks like the policy gate never ran
|
|
95
|
+
// (pnpm/pnpm#12324), so surface the reused verdict — but only
|
|
96
|
+
// when policy verifiers are active; the shape-only run that
|
|
97
|
+
// every install performs stays quiet.
|
|
98
|
+
if (verifiers.length > 0) {
|
|
99
|
+
lockfileVerificationLogger.debug({
|
|
100
|
+
status: 'cached',
|
|
101
|
+
verifiedAt: result.verifiedAt,
|
|
102
|
+
lockfilePath: options?.lockfilePath,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
84
105
|
return;
|
|
106
|
+
}
|
|
85
107
|
cachePrecomputed = result.precomputed;
|
|
86
108
|
}
|
|
87
109
|
// Emit started/done around the actual verification pass — the
|
|
88
110
|
// round-trip can be slow on a cold registry cache, and the cached
|
|
89
|
-
// short-circuit above
|
|
90
|
-
// sees these messages on installs that are doing
|
|
111
|
+
// short-circuit above announces itself with its own `cached` event,
|
|
112
|
+
// so a user only sees these messages on installs that are doing
|
|
113
|
+
// real work.
|
|
91
114
|
// A degenerate lockfile where every snapshot fails the
|
|
92
115
|
// name/version extraction (so candidates is empty) skips emission
|
|
93
116
|
// entirely — no work, no noise.
|
|
94
|
-
const { candidates, shapeViolations } = collectCandidates(lockfile);
|
|
117
|
+
const { candidates, shapeViolations, invalidAliases } = collectCandidates(lockfile);
|
|
118
|
+
if (invalidAliases.length > 0) {
|
|
119
|
+
throw buildInvalidAliasError(invalidAliases);
|
|
120
|
+
}
|
|
95
121
|
if (shapeViolations.length > 0) {
|
|
96
122
|
throw buildVerificationError(shapeViolations);
|
|
97
123
|
}
|
|
@@ -145,6 +171,19 @@ export async function verifyLockfileResolutions(lockfile, verifiers, options) {
|
|
|
145
171
|
});
|
|
146
172
|
}
|
|
147
173
|
}
|
|
174
|
+
function buildInvalidAliasError(aliases) {
|
|
175
|
+
const sorted = [...aliases].sort();
|
|
176
|
+
const visible = sorted.slice(0, MAX_VIOLATIONS_TO_PRINT);
|
|
177
|
+
const omitted = sorted.length - visible.length;
|
|
178
|
+
const breakdown = visible.map((alias) => ` ${JSON.stringify(alias)}`).join('\n');
|
|
179
|
+
const details = omitted > 0 ? `${breakdown}\n …and ${omitted} more` : breakdown;
|
|
180
|
+
const plural = aliases.length === 1 ? 'alias' : 'aliases';
|
|
181
|
+
return new PnpmError(INVALID_DEPENDENCY_ALIAS_CODE, `The lockfile contains ${aliases.length} dependency ${plural} that are not valid package names:\n${details}`, {
|
|
182
|
+
hint: 'A dependency alias becomes a directory under node_modules, so it must be a valid npm package name — a single `name` or `@scope/name` with no leading `.` or `_`, and not a reserved name such as `node_modules`. ' +
|
|
183
|
+
'An alias containing path-traversal segments or a reserved name such as `.bin` or `.pnpm` could make an install write outside the intended directory or overwrite pnpm-owned layout. ' +
|
|
184
|
+
'This usually means the lockfile was tampered with — inspect recent changes to pnpm-lock.yaml before trusting it.',
|
|
185
|
+
});
|
|
186
|
+
}
|
|
148
187
|
function buildVerificationError(violations) {
|
|
149
188
|
// Stable order so the error output is deterministic.
|
|
150
189
|
violations.sort((a, b) => `${a.name}@${a.version}`.localeCompare(`${b.name}@${b.version}`));
|
|
@@ -253,7 +292,17 @@ function isRegistryShapedResolution(resolution) {
|
|
|
253
292
|
function collectCandidates(lockfile) {
|
|
254
293
|
const candidates = new Map();
|
|
255
294
|
const shapeViolations = [];
|
|
295
|
+
// The importer alias maps are the one source not reached by the
|
|
296
|
+
// package loop below, so they're scanned here.
|
|
297
|
+
const invalidAliases = new Set();
|
|
298
|
+
for (const importer of Object.values(lockfile.importers ?? {})) {
|
|
299
|
+
pushInvalidAliases(importer.dependencies, invalidAliases);
|
|
300
|
+
pushInvalidAliases(importer.devDependencies, invalidAliases);
|
|
301
|
+
pushInvalidAliases(importer.optionalDependencies, invalidAliases);
|
|
302
|
+
}
|
|
256
303
|
for (const [depPath, snapshot] of Object.entries(lockfile.packages ?? {})) {
|
|
304
|
+
pushInvalidAliases(snapshot.dependencies, invalidAliases);
|
|
305
|
+
pushInvalidAliases(snapshot.optionalDependencies, invalidAliases);
|
|
257
306
|
const { name, version, nonSemverVersion } = nameVerFromPkgSnapshot(depPath, snapshot);
|
|
258
307
|
if (!name || !version)
|
|
259
308
|
continue;
|
|
@@ -279,7 +328,21 @@ function collectCandidates(lockfile) {
|
|
|
279
328
|
resolution: snapshot.resolution,
|
|
280
329
|
});
|
|
281
330
|
}
|
|
282
|
-
return { candidates, shapeViolations };
|
|
331
|
+
return { candidates, shapeViolations, invalidAliases: Array.from(invalidAliases) };
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Add every key of `deps` that is not a valid {@link isValidDependencyAlias}
|
|
335
|
+
* to `invalid`. Only pass maps whose keys become `node_modules/<alias>`
|
|
336
|
+
* directories — not `overrides` (`foo>bar` selectors) or
|
|
337
|
+
* `patchedDependencies` (`name@version` keys).
|
|
338
|
+
*/
|
|
339
|
+
function pushInvalidAliases(deps, invalid) {
|
|
340
|
+
if (deps == null)
|
|
341
|
+
return;
|
|
342
|
+
for (const alias of Object.keys(deps)) {
|
|
343
|
+
if (!isValidDependencyAlias(alias))
|
|
344
|
+
invalid.add(alias);
|
|
345
|
+
}
|
|
283
346
|
}
|
|
284
347
|
async function iterateLockfileViolations(candidates, verifiers, concurrency) {
|
|
285
348
|
const violations = [];
|
|
@@ -9,6 +9,13 @@ import type { ResolutionVerifier } from '@pnpm/resolving.resolver-base';
|
|
|
9
9
|
export type VerifierCacheIdentity = Pick<ResolutionVerifier, 'policy' | 'canTrustPastCheck'>;
|
|
10
10
|
export interface CacheLookupResult {
|
|
11
11
|
hit: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* ISO-8601 timestamp of the verification run the hit is reusing.
|
|
14
|
+
* Set only on a hit, and only when the record carries a usable
|
|
15
|
+
* timestamp (records written before the field existed normalize to
|
|
16
|
+
* an empty string and surface as `undefined` here).
|
|
17
|
+
*/
|
|
18
|
+
verifiedAt?: string;
|
|
12
19
|
/**
|
|
13
20
|
* stat + hash already computed during the lookup. When the caller
|
|
14
21
|
* follows up with {@link recordVerification} after running the gate,
|
|
@@ -159,8 +159,10 @@ export function tryLockfileVerificationCache(cacheDir, key) {
|
|
|
159
159
|
// hash without reading the file. Microseconds.
|
|
160
160
|
const byPathRecord = indexes.byPath.get(key.lockfilePath);
|
|
161
161
|
if (byPathRecord && statMatches(stat, byPathRecord.lockfile)) {
|
|
162
|
+
const hit = everyVerifierTrustsCachedRun(byPathRecord, key.verifiers);
|
|
162
163
|
return {
|
|
163
|
-
hit
|
|
164
|
+
hit,
|
|
165
|
+
verifiedAt: hit ? byPathRecord.verifiedAt || undefined : undefined,
|
|
164
166
|
// The stat-match implies the file content is unchanged since the
|
|
165
167
|
// cached record was written, so its hash is still correct. Pass
|
|
166
168
|
// it through to skip hashing on the miss-then-record path.
|
|
@@ -190,7 +192,7 @@ export function tryLockfileVerificationCache(cacheDir, key) {
|
|
|
190
192
|
...byHashRecord,
|
|
191
193
|
lockfile: { ...byHashRecord.lockfile, path: key.lockfilePath, size: stat.size, mtimeNs: stat.mtimeNs, inode: stat.inode },
|
|
192
194
|
});
|
|
193
|
-
return { hit: true, precomputed: { stat, hash } };
|
|
195
|
+
return { hit: true, verifiedAt: byHashRecord.verifiedAt || undefined, precomputed: { stat, hash } };
|
|
194
196
|
}
|
|
195
197
|
function everyVerifierTrustsCachedRun(record, verifiers) {
|
|
196
198
|
for (const verifier of verifiers) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/installing.deps-installer",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1102.0.0",
|
|
4
4
|
"description": "Fast, disk space efficient installation engine",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pnpm",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@inquirer/prompts": "^8.4.3",
|
|
54
54
|
"@pnpm/npm-package-arg": "^2.0.0",
|
|
55
|
-
"@pnpm/util.lex-comparator": "^
|
|
55
|
+
"@pnpm/util.lex-comparator": "^4.0.1",
|
|
56
56
|
"@zkochan/rimraf": "^4.0.0",
|
|
57
57
|
"is-inner-link": "^5.0.0",
|
|
58
58
|
"is-subdir": "^2.0.0",
|
|
@@ -64,104 +64,104 @@
|
|
|
64
64
|
"path-exists": "^5.0.0",
|
|
65
65
|
"ramda": "npm:@pnpm/ramda@0.28.1",
|
|
66
66
|
"run-groups": "^5.0.0",
|
|
67
|
-
"semver": "^7.8.
|
|
68
|
-
"@pnpm/
|
|
69
|
-
"@pnpm/building.
|
|
67
|
+
"semver": "^7.8.4",
|
|
68
|
+
"@pnpm/building.after-install": "1102.0.0",
|
|
69
|
+
"@pnpm/building.during-install": "1102.0.0",
|
|
70
70
|
"@pnpm/catalogs.protocol-parser": "1100.0.0",
|
|
71
|
-
"@pnpm/
|
|
72
|
-
"@pnpm/building.policy": "1100.0.9",
|
|
73
|
-
"@pnpm/catalogs.types": "1100.0.0",
|
|
71
|
+
"@pnpm/bins.remover": "1100.0.10",
|
|
74
72
|
"@pnpm/catalogs.resolver": "1100.0.0",
|
|
75
|
-
"@pnpm/config.normalize-registries": "1100.0.
|
|
76
|
-
"@pnpm/config.
|
|
77
|
-
"@pnpm/
|
|
73
|
+
"@pnpm/config.normalize-registries": "1100.0.8",
|
|
74
|
+
"@pnpm/config.parse-overrides": "1100.0.1",
|
|
75
|
+
"@pnpm/catalogs.types": "1100.0.0",
|
|
76
|
+
"@pnpm/core-loggers": "1100.2.1",
|
|
78
77
|
"@pnpm/constants": "1100.0.0",
|
|
79
|
-
"@pnpm/
|
|
80
|
-
"@pnpm/deps.graph-hasher": "1100.2.
|
|
81
|
-
"@pnpm/
|
|
78
|
+
"@pnpm/config.matcher": "1100.0.1",
|
|
79
|
+
"@pnpm/deps.graph-hasher": "1100.2.5",
|
|
80
|
+
"@pnpm/deps.path": "1100.0.8",
|
|
82
81
|
"@pnpm/deps.graph-sequencer": "1100.0.0",
|
|
83
|
-
"@pnpm/
|
|
82
|
+
"@pnpm/building.policy": "1100.0.10",
|
|
83
|
+
"@pnpm/bins.linker": "1100.0.14",
|
|
84
|
+
"@pnpm/crypto.object-hasher": "1100.0.0",
|
|
85
|
+
"@pnpm/exec.lifecycle": "1100.0.18",
|
|
86
|
+
"@pnpm/hooks.read-package-hook": "1100.0.8",
|
|
87
|
+
"@pnpm/fs.symlink-dependency": "1100.0.10",
|
|
88
|
+
"@pnpm/hooks.types": "1100.0.12",
|
|
84
89
|
"@pnpm/error": "1100.0.0",
|
|
90
|
+
"@pnpm/installing.context": "1100.0.18",
|
|
91
|
+
"@pnpm/installing.linking.direct-dep-linker": "1100.0.10",
|
|
92
|
+
"@pnpm/installing.deps-resolver": "1100.2.3",
|
|
93
|
+
"@pnpm/crypto.hash": "1100.0.1",
|
|
85
94
|
"@pnpm/fs.read-modules-dir": "1100.0.1",
|
|
86
|
-
"@pnpm/
|
|
87
|
-
"@pnpm/
|
|
88
|
-
"@pnpm/
|
|
89
|
-
"@pnpm/
|
|
90
|
-
"@pnpm/
|
|
91
|
-
"@pnpm/installing.
|
|
92
|
-
"@pnpm/
|
|
93
|
-
"@pnpm/installing.
|
|
94
|
-
"@pnpm/
|
|
95
|
-
"@pnpm/
|
|
96
|
-
"@pnpm/
|
|
97
|
-
"@pnpm/
|
|
98
|
-
"@pnpm/
|
|
99
|
-
"@pnpm/
|
|
100
|
-
"@pnpm/installing.package-requester": "1101.0.12",
|
|
101
|
-
"@pnpm/lockfile.pruner": "1100.0.10",
|
|
102
|
-
"@pnpm/lockfile.settings-checker": "1100.0.16",
|
|
103
|
-
"@pnpm/lockfile.preferred-versions": "1100.0.14",
|
|
104
|
-
"@pnpm/lockfile.filtering": "1100.1.5",
|
|
105
|
-
"@pnpm/lockfile.to-pnp": "1100.0.13",
|
|
106
|
-
"@pnpm/lockfile.verification": "1100.0.16",
|
|
107
|
-
"@pnpm/lockfile.utils": "1100.0.12",
|
|
108
|
-
"@pnpm/lockfile.walker": "1100.0.10",
|
|
109
|
-
"@pnpm/network.auth-header": "1101.1.1",
|
|
95
|
+
"@pnpm/installing.linking.hoist": "1100.0.14",
|
|
96
|
+
"@pnpm/installing.package-requester": "1102.0.0",
|
|
97
|
+
"@pnpm/installing.deps-restorer": "1102.0.0",
|
|
98
|
+
"@pnpm/lockfile.fs": "1100.1.5",
|
|
99
|
+
"@pnpm/lockfile.pruner": "1100.0.11",
|
|
100
|
+
"@pnpm/installing.linking.modules-cleaner": "1100.1.8",
|
|
101
|
+
"@pnpm/lockfile.preferred-versions": "1100.0.16",
|
|
102
|
+
"@pnpm/installing.modules-yaml": "1100.0.9",
|
|
103
|
+
"@pnpm/lockfile.settings-checker": "1100.0.18",
|
|
104
|
+
"@pnpm/lockfile.utils": "1100.0.13",
|
|
105
|
+
"@pnpm/lockfile.to-pnp": "1100.0.14",
|
|
106
|
+
"@pnpm/network.auth-header": "1101.1.2",
|
|
107
|
+
"@pnpm/lockfile.walker": "1100.0.11",
|
|
108
|
+
"@pnpm/pnpr.client": "1.2.1",
|
|
110
109
|
"@pnpm/resolving.parse-wanted-dependency": "1100.0.1",
|
|
111
|
-
"@pnpm/pkg-manifest.utils": "1100.2.
|
|
112
|
-
"@pnpm/
|
|
113
|
-
"@pnpm/
|
|
114
|
-
"@pnpm/
|
|
115
|
-
"@pnpm/
|
|
116
|
-
"@pnpm/workspace.project-manifest-reader": "1100.0.
|
|
117
|
-
"@pnpm/store.
|
|
118
|
-
"@pnpm/
|
|
110
|
+
"@pnpm/pkg-manifest.utils": "1100.2.5",
|
|
111
|
+
"@pnpm/resolving.resolver-base": "1100.4.2",
|
|
112
|
+
"@pnpm/lockfile.filtering": "1100.1.7",
|
|
113
|
+
"@pnpm/types": "1101.3.2",
|
|
114
|
+
"@pnpm/patching.config": "1100.0.8",
|
|
115
|
+
"@pnpm/workspace.project-manifest-reader": "1100.0.13",
|
|
116
|
+
"@pnpm/store.controller-types": "1100.1.5",
|
|
117
|
+
"@pnpm/lockfile.verification": "1100.0.18",
|
|
118
|
+
"@pnpm/store.index": "1100.2.0"
|
|
119
119
|
},
|
|
120
120
|
"peerDependencies": {
|
|
121
|
-
"@pnpm/logger": "^
|
|
122
|
-
"@pnpm/worker": "^1100.
|
|
121
|
+
"@pnpm/logger": "^1100.0.0",
|
|
122
|
+
"@pnpm/worker": "^1100.2.0"
|
|
123
123
|
},
|
|
124
124
|
"devDependencies": {
|
|
125
|
-
"@jest/globals": "30.
|
|
125
|
+
"@jest/globals": "30.4.1",
|
|
126
126
|
"@types/fs-extra": "^11.0.4",
|
|
127
127
|
"@types/is-windows": "^1.0.2",
|
|
128
128
|
"@types/normalize-path": "^3.0.2",
|
|
129
129
|
"@types/ramda": "0.31.1",
|
|
130
130
|
"@types/semver": "7.7.1",
|
|
131
|
-
"@yarnpkg/core": "4.
|
|
131
|
+
"@yarnpkg/core": "4.8.0",
|
|
132
132
|
"ci-info": "^4.4.0",
|
|
133
133
|
"deep-require-cwd": "1.0.0",
|
|
134
134
|
"execa": "npm:safe-execa@0.3.0",
|
|
135
135
|
"exists-link": "2.0.0",
|
|
136
136
|
"is-windows": "^1.0.2",
|
|
137
|
-
"nock": "13.3.4",
|
|
138
137
|
"path-name": "^1.0.0",
|
|
139
138
|
"read-yaml-file": "^3.0.0",
|
|
140
139
|
"resolve-link-target": "^3.0.0",
|
|
141
140
|
"symlink-dir": "^10.0.1",
|
|
142
141
|
"write-json-file": "^7.0.0",
|
|
143
142
|
"write-yaml-file": "^6.0.0",
|
|
144
|
-
"@pnpm/assert-project": "1100.0.
|
|
145
|
-
"@pnpm/
|
|
146
|
-
"@pnpm/
|
|
147
|
-
"@pnpm/assert-store": "1100.0.14",
|
|
148
|
-
"@pnpm/network.git-utils": "1100.0.1",
|
|
143
|
+
"@pnpm/assert-project": "1100.0.16",
|
|
144
|
+
"@pnpm/installing.deps-installer": "1102.0.0",
|
|
145
|
+
"@pnpm/assert-store": "1100.0.16",
|
|
149
146
|
"@pnpm/logger": "1100.0.0",
|
|
150
|
-
"@pnpm/
|
|
151
|
-
"@pnpm/
|
|
152
|
-
"@pnpm/
|
|
153
|
-
"@pnpm/
|
|
154
|
-
"@pnpm/store.cafs": "1100.1.9",
|
|
155
|
-
"@pnpm/testing.mock-agent": "1101.0.1",
|
|
156
|
-
"@pnpm/test-ipc-server": "1100.0.0",
|
|
147
|
+
"@pnpm/network.git-utils": "1100.0.1",
|
|
148
|
+
"@pnpm/pkg-manifest.reader": "1100.0.8",
|
|
149
|
+
"@pnpm/store.cafs": "1100.1.10",
|
|
150
|
+
"@pnpm/prepare": "1100.0.16",
|
|
157
151
|
"@pnpm/store.path": "1100.0.1",
|
|
158
|
-
"@pnpm/
|
|
159
|
-
"@pnpm/testing.registry-mock": "1100.0.
|
|
152
|
+
"@pnpm/test-ipc-server": "1100.0.0",
|
|
153
|
+
"@pnpm/testing.registry-mock": "1100.0.6",
|
|
154
|
+
"@pnpm/testing.mock-agent": "1101.0.3",
|
|
155
|
+
"@pnpm/test-fixtures": "1100.0.0",
|
|
156
|
+
"@pnpm/testing.temp-store": "1100.1.9",
|
|
157
|
+
"@pnpm/resolving.registry.types": "1100.1.3",
|
|
158
|
+
"@pnpm/lockfile.types": "1100.0.11"
|
|
160
159
|
},
|
|
161
160
|
"engines": {
|
|
162
161
|
"node": ">=22.13"
|
|
163
162
|
},
|
|
164
163
|
"jest": {
|
|
164
|
+
"collectCoverage": false,
|
|
165
165
|
"preset": "@pnpm/jest-config/with-registry"
|
|
166
166
|
},
|
|
167
167
|
"scripts": {
|
|
@@ -170,6 +170,8 @@
|
|
|
170
170
|
"test-with-preview": "preview && pnpm run test:e2e",
|
|
171
171
|
"test": "pn compile && pn .test",
|
|
172
172
|
"compile": "tsgo --build && pn lint --fix",
|
|
173
|
-
".test": "
|
|
173
|
+
".test": "pn .test:heavy && pn .test:rest",
|
|
174
|
+
".test:heavy": "cross-env PNPM_REGISTRY_MOCK_PORT=7769 NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest test/install/deepRecursive.ts",
|
|
175
|
+
".test:rest": "cross-env PNPM_REGISTRY_MOCK_PORT=7769 NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest \"^(?!.*deepRecursive)\""
|
|
174
176
|
}
|
|
175
177
|
}
|