@pnpm/installing.deps-installer 1101.1.2 → 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.
- package/lib/install/extendInstallOptions.d.ts +52 -1
- package/lib/install/extendInstallOptions.js +1 -0
- package/lib/install/index.d.ts +14 -1
- package/lib/install/index.js +201 -21
- package/lib/install/link.js +7 -1
- package/lib/install/recordLockfileVerified.d.ts +22 -0
- package/lib/install/recordLockfileVerified.js +24 -0
- package/lib/install/verifyLockfileResolutions.d.ts +59 -0
- package/lib/install/verifyLockfileResolutions.js +225 -0
- package/lib/install/verifyLockfileResolutionsCache.d.ts +80 -0
- package/lib/install/verifyLockfileResolutionsCache.js +335 -0
- package/lib/install/writeLockfilesAndRecordVerified.d.ts +14 -0
- package/lib/install/writeLockfilesAndRecordVerified.js +32 -0
- package/lib/install/writeWantedLockfileAndRecordVerified.d.ts +12 -0
- package/lib/install/writeWantedLockfileAndRecordVerified.js +28 -0
- package/package.json +53 -53
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { LockfileObject } from '@pnpm/lockfile.fs';
|
|
2
|
+
import type { ResolutionPolicyViolation, ResolutionVerifier } from '@pnpm/resolving.resolver-base';
|
|
3
|
+
export type { ResolutionPolicyViolation };
|
|
4
|
+
export interface VerifyLockfileResolutionsOptions {
|
|
5
|
+
concurrency?: number;
|
|
6
|
+
/**
|
|
7
|
+
* pnpm's on-disk cache directory. When set together with
|
|
8
|
+
* `lockfilePath`, verification results are memoized in
|
|
9
|
+
* `<cacheDir>/lockfile-verified.jsonl` and the gate short-circuits on
|
|
10
|
+
* a repeat run against an unchanged lockfile + same-or-stricter
|
|
11
|
+
* policy. Omit to disable the cache entirely (every call rehashes
|
|
12
|
+
* the lockfile and re-verifies).
|
|
13
|
+
*/
|
|
14
|
+
cacheDir?: string;
|
|
15
|
+
/** Absolute path of the lockfile being verified. Used by the cache's stat shortcut. */
|
|
16
|
+
lockfilePath?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Policy-neutral pass that asks every resolver-supplied
|
|
20
|
+
* {@link ResolutionVerifier} to check every entry in a lockfile loaded
|
|
21
|
+
* from disk. Iteration runs before resolution decisions are touched and
|
|
22
|
+
* before any tarball is fetched, so a lockfile whose entries were
|
|
23
|
+
* resolved elsewhere (committed to the repo, restored from a cache,
|
|
24
|
+
* etc.) under a weaker or absent policy cannot reach the filesystem.
|
|
25
|
+
* Fresh local resolution is covered by the resolver's own per-version
|
|
26
|
+
* filter.
|
|
27
|
+
*
|
|
28
|
+
* Each verifier handles its own protocol short-circuit inside `verify`
|
|
29
|
+
* (returning `{ ok: true }` for resolutions outside its scope), so the
|
|
30
|
+
* fan-out is policy-neutral and dispatch-free at this layer.
|
|
31
|
+
*
|
|
32
|
+
* Designed for fail-closed semantics at the verifier level: a verifier
|
|
33
|
+
* that can't confirm a resolution is expected to return `{ ok: false }`
|
|
34
|
+
* rather than passing silently — otherwise a registry hiccup or an
|
|
35
|
+
* unpublished version would re-open the bypass.
|
|
36
|
+
*
|
|
37
|
+
* No-op when `verifiers` is empty.
|
|
38
|
+
*
|
|
39
|
+
* When `options.cacheDir` and `options.lockfilePath` are both
|
|
40
|
+
* provided, an unchanged lockfile that has already been verified
|
|
41
|
+
* under the same (or stricter) policy short-circuits the registry
|
|
42
|
+
* round-trip entirely — see {@link tryLockfileVerificationCache} for
|
|
43
|
+
* the lookup logic.
|
|
44
|
+
*/
|
|
45
|
+
export declare function verifyLockfileResolutions(lockfile: LockfileObject, verifiers: ResolutionVerifier[], options?: VerifyLockfileResolutionsOptions): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Collect-mode sibling of {@link verifyLockfileResolutions}: runs the
|
|
48
|
+
* same fan-out over every verifier and every lockfile entry, but
|
|
49
|
+
* returns the violations as data instead of throwing on the first batch.
|
|
50
|
+
* No cache lookup or write — the throw-mode `verifyLockfileResolutions`
|
|
51
|
+
* is what populates / honors the cache; this is for callers that need
|
|
52
|
+
* to inspect violations (auto-collect into `minimumReleaseAgeExclude`,
|
|
53
|
+
* the strict-mode interactive prompt, future resolver-specific
|
|
54
|
+
* policies).
|
|
55
|
+
*
|
|
56
|
+
* Returns an empty array when `verifiers` is empty or the lockfile has
|
|
57
|
+
* no packages, so callers don't need a separate emptiness check.
|
|
58
|
+
*/
|
|
59
|
+
export declare function collectResolutionPolicyViolations(lockfile: LockfileObject, verifiers: ResolutionVerifier[], options?: Pick<VerifyLockfileResolutionsOptions, 'concurrency'>): Promise<ResolutionPolicyViolation[]>;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { lockfileVerificationLogger } from '@pnpm/core-loggers';
|
|
2
|
+
import { hashObject } from '@pnpm/crypto.object-hasher';
|
|
3
|
+
import { PnpmError } from '@pnpm/error';
|
|
4
|
+
import { nameVerFromPkgSnapshot } from '@pnpm/lockfile.utils';
|
|
5
|
+
import pLimit from 'p-limit';
|
|
6
|
+
import { recordVerification, tryLockfileVerificationCache, } from './verifyLockfileResolutionsCache.js';
|
|
7
|
+
// Cap the per-entry breakdown so a verifier rejecting hundreds of entries
|
|
8
|
+
// (e.g. a poisoned lockfile) doesn't flood the terminal / CI log; the full
|
|
9
|
+
// count is in the header and the remainder is summarized at the end.
|
|
10
|
+
const MAX_VIOLATIONS_TO_PRINT = 20;
|
|
11
|
+
// 16 mirrors the floor of pnpm's package-requester network-concurrency
|
|
12
|
+
// (Math.min(64, Math.max(workers*3, 16))); keep them aligned so the
|
|
13
|
+
// verification pass doesn't push past what the rest of the install respects.
|
|
14
|
+
const DEFAULT_CONCURRENCY = 16;
|
|
15
|
+
/**
|
|
16
|
+
* Policy-neutral pass that asks every resolver-supplied
|
|
17
|
+
* {@link ResolutionVerifier} to check every entry in a lockfile loaded
|
|
18
|
+
* from disk. Iteration runs before resolution decisions are touched and
|
|
19
|
+
* before any tarball is fetched, so a lockfile whose entries were
|
|
20
|
+
* resolved elsewhere (committed to the repo, restored from a cache,
|
|
21
|
+
* etc.) under a weaker or absent policy cannot reach the filesystem.
|
|
22
|
+
* Fresh local resolution is covered by the resolver's own per-version
|
|
23
|
+
* filter.
|
|
24
|
+
*
|
|
25
|
+
* Each verifier handles its own protocol short-circuit inside `verify`
|
|
26
|
+
* (returning `{ ok: true }` for resolutions outside its scope), so the
|
|
27
|
+
* fan-out is policy-neutral and dispatch-free at this layer.
|
|
28
|
+
*
|
|
29
|
+
* Designed for fail-closed semantics at the verifier level: a verifier
|
|
30
|
+
* that can't confirm a resolution is expected to return `{ ok: false }`
|
|
31
|
+
* rather than passing silently — otherwise a registry hiccup or an
|
|
32
|
+
* unpublished version would re-open the bypass.
|
|
33
|
+
*
|
|
34
|
+
* No-op when `verifiers` is empty.
|
|
35
|
+
*
|
|
36
|
+
* When `options.cacheDir` and `options.lockfilePath` are both
|
|
37
|
+
* provided, an unchanged lockfile that has already been verified
|
|
38
|
+
* under the same (or stricter) policy short-circuits the registry
|
|
39
|
+
* round-trip entirely — see {@link tryLockfileVerificationCache} for
|
|
40
|
+
* the lookup logic.
|
|
41
|
+
*/
|
|
42
|
+
export async function verifyLockfileResolutions(lockfile, verifiers, options) {
|
|
43
|
+
if (verifiers.length === 0)
|
|
44
|
+
return;
|
|
45
|
+
if (!lockfile.packages)
|
|
46
|
+
return;
|
|
47
|
+
// Caching kicks in only when the caller surfaced both a writable
|
|
48
|
+
// cache directory and the lockfile's absolute path — that's the
|
|
49
|
+
// production wiring; unit tests that skip them get the gate without
|
|
50
|
+
// memoization and still exercise the same code path.
|
|
51
|
+
const cache = options?.cacheDir && options?.lockfilePath
|
|
52
|
+
? { cacheDir: options.cacheDir, lockfilePath: options.lockfilePath }
|
|
53
|
+
: undefined;
|
|
54
|
+
let cachePrecomputed;
|
|
55
|
+
// hashObject streams and is key-order-stable, unlike JSON.stringify.
|
|
56
|
+
let cachedHash;
|
|
57
|
+
const hashLockfile = () => {
|
|
58
|
+
if (cachedHash == null)
|
|
59
|
+
cachedHash = hashObject(lockfile);
|
|
60
|
+
return cachedHash;
|
|
61
|
+
};
|
|
62
|
+
if (cache) {
|
|
63
|
+
const result = tryLockfileVerificationCache(cache.cacheDir, {
|
|
64
|
+
lockfilePath: cache.lockfilePath,
|
|
65
|
+
verifiers,
|
|
66
|
+
hashLockfile,
|
|
67
|
+
});
|
|
68
|
+
if (result.hit)
|
|
69
|
+
return;
|
|
70
|
+
cachePrecomputed = result.precomputed;
|
|
71
|
+
}
|
|
72
|
+
// Emit started/done around the actual verification pass — the
|
|
73
|
+
// round-trip can be slow on a cold registry cache, and the cached
|
|
74
|
+
// short-circuit above doesn't reach this branch, so a user only
|
|
75
|
+
// sees these messages on installs that are doing real work.
|
|
76
|
+
// A degenerate lockfile where every snapshot fails the
|
|
77
|
+
// name/version extraction (so candidates is empty) skips emission
|
|
78
|
+
// entirely — no work, no noise.
|
|
79
|
+
const candidates = collectCandidates(lockfile);
|
|
80
|
+
if (candidates.size === 0) {
|
|
81
|
+
if (cache) {
|
|
82
|
+
recordVerification(cache.cacheDir, {
|
|
83
|
+
lockfilePath: cache.lockfilePath,
|
|
84
|
+
verifiers,
|
|
85
|
+
hashLockfile,
|
|
86
|
+
}, cachePrecomputed);
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const startedAt = Date.now();
|
|
91
|
+
lockfileVerificationLogger.debug({
|
|
92
|
+
status: 'started',
|
|
93
|
+
entries: candidates.size,
|
|
94
|
+
lockfilePath: options?.lockfilePath,
|
|
95
|
+
});
|
|
96
|
+
// Guarantee a terminal `done` or `failed` event on every exit path
|
|
97
|
+
// that emitted `started`. Without this, an unexpected throw from the
|
|
98
|
+
// registry fan-out (or the policy-violation throw below) would leave
|
|
99
|
+
// the transient "Verifying lockfile…" line as the last frame the
|
|
100
|
+
// reporter rendered for this block, hanging spinner-style above the
|
|
101
|
+
// failure output.
|
|
102
|
+
let terminalStatus = 'failed';
|
|
103
|
+
try {
|
|
104
|
+
const violations = await iterateLockfileViolations(candidates, verifiers, options?.concurrency);
|
|
105
|
+
if (violations.length === 0) {
|
|
106
|
+
terminalStatus = 'done';
|
|
107
|
+
// Persist the success so the next install can stat-only the lockfile.
|
|
108
|
+
if (cache) {
|
|
109
|
+
recordVerification(cache.cacheDir, {
|
|
110
|
+
lockfilePath: cache.lockfilePath,
|
|
111
|
+
verifiers,
|
|
112
|
+
hashLockfile,
|
|
113
|
+
}, cachePrecomputed);
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
throw buildVerificationError(violations);
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
lockfileVerificationLogger.debug({
|
|
121
|
+
status: terminalStatus,
|
|
122
|
+
entries: candidates.size,
|
|
123
|
+
elapsedMs: Date.now() - startedAt,
|
|
124
|
+
lockfilePath: options?.lockfilePath,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function buildVerificationError(violations) {
|
|
129
|
+
// Stable order so the error output is deterministic.
|
|
130
|
+
violations.sort((a, b) => `${a.name}@${a.version}`.localeCompare(`${b.name}@${b.version}`));
|
|
131
|
+
// Pick the throw code: a single-code batch keeps the per-policy code
|
|
132
|
+
// (so existing handlers / docs / search keywords still route correctly);
|
|
133
|
+
// a mixed batch (e.g. minimumReleaseAge + trust-downgrade on the same
|
|
134
|
+
// lockfile) escalates to the generic `LOCKFILE_RESOLUTION_VERIFICATION`
|
|
135
|
+
// and the per-entry code goes into the breakdown so the user can see
|
|
136
|
+
// which policy each entry tripped.
|
|
137
|
+
const distinctCodes = new Set(violations.map((v) => v.code));
|
|
138
|
+
const isMixed = distinctCodes.size > 1;
|
|
139
|
+
const errorCode = isMixed ? 'LOCKFILE_RESOLUTION_VERIFICATION' : violations[0].code;
|
|
140
|
+
const visible = violations.slice(0, MAX_VIOLATIONS_TO_PRINT);
|
|
141
|
+
const omitted = violations.length - visible.length;
|
|
142
|
+
const formatEntry = isMixed
|
|
143
|
+
? (v) => ` ${v.name}@${v.version} [${v.code}] ${v.reason}`
|
|
144
|
+
: (v) => ` ${v.name}@${v.version} ${v.reason}`;
|
|
145
|
+
const breakdown = visible.map(formatEntry).join('\n');
|
|
146
|
+
const details = omitted > 0
|
|
147
|
+
? `${breakdown}\n …and ${omitted} more`
|
|
148
|
+
: breakdown;
|
|
149
|
+
return new PnpmError(errorCode, `${violations.length} lockfile entries failed verification:\n${details}`, {
|
|
150
|
+
hint: 'The lockfile contains entries that the active policies reject. ' +
|
|
151
|
+
'This can mean the lockfile is stale, or that someone committed a ' +
|
|
152
|
+
'lockfile that bypassed the policy locally — inspect recent changes ' +
|
|
153
|
+
'to pnpm-lock.yaml before trusting it. If the changes look expected, ' +
|
|
154
|
+
'run "pnpm clean --lockfile" and then "pnpm install" to rebuild from ' +
|
|
155
|
+
'a fresh resolution. Alternatively, relax the policy that flagged ' +
|
|
156
|
+
'them.',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Collect-mode sibling of {@link verifyLockfileResolutions}: runs the
|
|
161
|
+
* same fan-out over every verifier and every lockfile entry, but
|
|
162
|
+
* returns the violations as data instead of throwing on the first batch.
|
|
163
|
+
* No cache lookup or write — the throw-mode `verifyLockfileResolutions`
|
|
164
|
+
* is what populates / honors the cache; this is for callers that need
|
|
165
|
+
* to inspect violations (auto-collect into `minimumReleaseAgeExclude`,
|
|
166
|
+
* the strict-mode interactive prompt, future resolver-specific
|
|
167
|
+
* policies).
|
|
168
|
+
*
|
|
169
|
+
* Returns an empty array when `verifiers` is empty or the lockfile has
|
|
170
|
+
* no packages, so callers don't need a separate emptiness check.
|
|
171
|
+
*/
|
|
172
|
+
export async function collectResolutionPolicyViolations(lockfile, verifiers, options) {
|
|
173
|
+
if (verifiers.length === 0)
|
|
174
|
+
return [];
|
|
175
|
+
if (!lockfile.packages)
|
|
176
|
+
return [];
|
|
177
|
+
return iterateLockfileViolations(collectCandidates(lockfile), verifiers, options?.concurrency);
|
|
178
|
+
}
|
|
179
|
+
// depPath can include peer-dependency and patch_hash suffixes (e.g.
|
|
180
|
+
// `react@18.0.0(peer)(patch_hash=…)`); the same (name, version) pair may
|
|
181
|
+
// therefore appear multiple times. Dedupe so we issue at most one
|
|
182
|
+
// verification per package version.
|
|
183
|
+
//
|
|
184
|
+
// Include a serialization of `resolution` in the key so two entries that
|
|
185
|
+
// share a (name, version) but differ in *what* was resolved (e.g. one
|
|
186
|
+
// pinned via npm, another via a git URL under the same alias) don't
|
|
187
|
+
// collapse: if the wrong shape wins the dedup, a protocol-scoped
|
|
188
|
+
// verifier short-circuits on the surviving entry and the real one is
|
|
189
|
+
// never checked.
|
|
190
|
+
function collectCandidates(lockfile) {
|
|
191
|
+
const candidates = new Map();
|
|
192
|
+
for (const [depPath, snapshot] of Object.entries(lockfile.packages ?? {})) {
|
|
193
|
+
const { name, version } = nameVerFromPkgSnapshot(depPath, snapshot);
|
|
194
|
+
if (!name || !version)
|
|
195
|
+
continue;
|
|
196
|
+
const key = `${name}@${version}@${JSON.stringify(snapshot.resolution)}`;
|
|
197
|
+
candidates.set(key, {
|
|
198
|
+
name,
|
|
199
|
+
version,
|
|
200
|
+
resolution: snapshot.resolution,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return candidates;
|
|
204
|
+
}
|
|
205
|
+
async function iterateLockfileViolations(candidates, verifiers, concurrency) {
|
|
206
|
+
const violations = [];
|
|
207
|
+
const limit = pLimit(concurrency ?? DEFAULT_CONCURRENCY);
|
|
208
|
+
await Promise.all(Array.from(candidates.values(), ({ name, version, resolution }) => limit(async () => {
|
|
209
|
+
// Fan out across every active verifier; each handles its own
|
|
210
|
+
// protocol short-circuit (e.g. the npm verifier returns ok:true for
|
|
211
|
+
// git resolutions). We stop at the first failure per entry so a
|
|
212
|
+
// multi-verifier setup doesn't produce duplicate violations for the
|
|
213
|
+
// same (name, version).
|
|
214
|
+
for (const verifier of verifiers) {
|
|
215
|
+
// eslint-disable-next-line no-await-in-loop
|
|
216
|
+
const result = await verifier.verify(resolution, { name, version });
|
|
217
|
+
if (!result.ok) {
|
|
218
|
+
violations.push({ name, version, resolution, code: result.code, reason: result.reason });
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
})));
|
|
223
|
+
return violations;
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=verifyLockfileResolutions.js.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ResolutionVerifier } from '@pnpm/resolving.resolver-base';
|
|
2
|
+
/**
|
|
3
|
+
* Subset of {@link ResolutionVerifier} the cache layer needs: the
|
|
4
|
+
* verifier's `policy` contribution plus the `canTrustPastCheck`
|
|
5
|
+
* comparator. `verify` is intentionally absent — the cache never runs
|
|
6
|
+
* verifiers, it just decides whether a previous run is still
|
|
7
|
+
* trustworthy.
|
|
8
|
+
*/
|
|
9
|
+
export type VerifierCacheIdentity = Pick<ResolutionVerifier, 'policy' | 'canTrustPastCheck'>;
|
|
10
|
+
export interface CacheLookupResult {
|
|
11
|
+
hit: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* stat + hash already computed during the lookup. When the caller
|
|
14
|
+
* follows up with {@link recordVerification} after running the gate,
|
|
15
|
+
* passing these back avoids re-stat'ing and (especially) re-hashing
|
|
16
|
+
* the lockfile a second time. Fields are undefined when the lookup
|
|
17
|
+
* couldn't (or didn't need to) compute them — `recordVerification`
|
|
18
|
+
* falls back to computing what's missing.
|
|
19
|
+
*/
|
|
20
|
+
precomputed: {
|
|
21
|
+
stat?: LockfileStat;
|
|
22
|
+
hash?: string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
interface LockfileStat {
|
|
26
|
+
size: number;
|
|
27
|
+
mtimeNs: string;
|
|
28
|
+
inode: string;
|
|
29
|
+
}
|
|
30
|
+
export interface LockfileVerificationCacheKey {
|
|
31
|
+
lockfilePath: string;
|
|
32
|
+
verifiers: readonly VerifierCacheIdentity[];
|
|
33
|
+
/**
|
|
34
|
+
* Lazy: returns a stable hex hash of the in-memory lockfile. The
|
|
35
|
+
* cache invokes this only when the stat shortcut doesn't apply (the
|
|
36
|
+
* lockfile is at a new path, or its stat has drifted from the
|
|
37
|
+
* cached record). When the stat shortcut hits, the in-memory hash is
|
|
38
|
+
* never computed.
|
|
39
|
+
*/
|
|
40
|
+
hashLockfile: () => string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Try to confirm a cached verification covers the lockfile as it
|
|
44
|
+
* currently sits on disk and the policies currently in effect. Returns
|
|
45
|
+
* `{ hit: true }` to skip the gate; `{ hit: false }` means the caller
|
|
46
|
+
* should run the verifier and persist the result with
|
|
47
|
+
* {@link recordVerification}.
|
|
48
|
+
*
|
|
49
|
+
* Lookup order:
|
|
50
|
+
*
|
|
51
|
+
* 1. **Stat shortcut** — if we've previously verified this exact path
|
|
52
|
+
* with these exact stat values, trust the cached hash and skip
|
|
53
|
+
* reading the lockfile.
|
|
54
|
+
* 2. **Content lookup** — hash the lockfile and look up by hash.
|
|
55
|
+
* Catches the worktree case (same content, different path) and
|
|
56
|
+
* CI checkouts where stat fields got reset. Refreshes the
|
|
57
|
+
* stat-shortcut entry on hit so the next install at this path
|
|
58
|
+
* skips the hash.
|
|
59
|
+
*
|
|
60
|
+
* Every active verifier must agree the cached policy snapshot is still
|
|
61
|
+
* trustworthy under what it currently demands; if any rejects, the
|
|
62
|
+
* full gate runs.
|
|
63
|
+
*/
|
|
64
|
+
export declare function tryLockfileVerificationCache(cacheDir: string, key: LockfileVerificationCacheKey): CacheLookupResult;
|
|
65
|
+
/**
|
|
66
|
+
* Persist a successful verification. Called after the gate passes; the
|
|
67
|
+
* lockfile is hashed once and the resulting record is appended to the
|
|
68
|
+
* cache file. If the file is past {@link MAX_CACHE_ENTRIES}, it is
|
|
69
|
+
* rewritten keeping the most recent entries.
|
|
70
|
+
*
|
|
71
|
+
* Reuses `precomputed` values from a prior
|
|
72
|
+
* {@link tryLockfileVerificationCache} lookup so we don't re-stat or
|
|
73
|
+
* (especially) re-hash the lockfile a second time on the miss-then-
|
|
74
|
+
* record path.
|
|
75
|
+
*/
|
|
76
|
+
export declare function recordVerification(cacheDir: string, key: LockfileVerificationCacheKey, precomputed?: {
|
|
77
|
+
stat?: LockfileStat;
|
|
78
|
+
hash?: string;
|
|
79
|
+
}): void;
|
|
80
|
+
export {};
|