@pnpm/resolving.npm-resolver 1102.1.2 → 1102.1.4
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/CHANGELOG.md +1885 -0
- package/package.json +26 -26
- package/lib/clearMeta.d.ts +0 -16
- package/lib/clearMeta.js +0 -55
- package/lib/createNpmResolutionVerifier.d.ts +0 -89
- package/lib/createNpmResolutionVerifier.js +0 -724
- package/lib/fetch.d.ts +0 -37
- package/lib/fetch.js +0 -209
- package/lib/fetchAttestationPublishedAt.d.ts +0 -31
- package/lib/fetchAttestationPublishedAt.js +0 -99
- package/lib/fetchFullMetadataCached.d.ts +0 -33
- package/lib/fetchFullMetadataCached.js +0 -63
- package/lib/index.d.ts +0 -133
- package/lib/memoizeFetchMetadata.d.ts +0 -24
- package/lib/memoizeFetchMetadata.js +0 -37
- package/lib/normalizeRegistryUrl.d.ts +0 -4
- package/lib/normalizeRegistryUrl.js +0 -12
- package/lib/parseBareSpecifier.d.ts +0 -16
- package/lib/parseBareSpecifier.js +0 -143
- package/lib/pickPackage.d.ts +0 -109
- package/lib/pickPackage.js +0 -631
- package/lib/pickPackageFromMeta.d.ts +0 -20
- package/lib/pickPackageFromMeta.js +0 -227
- package/lib/toRaw.d.ts +0 -2
- package/lib/toRaw.js +0 -4
- package/lib/trustChecks.d.ts +0 -9
- package/lib/trustChecks.js +0 -96
- package/lib/violationCodes.d.ts +0 -14
- package/lib/violationCodes.js +0 -15
- package/lib/whichVersionIsPinned.d.ts +0 -2
- package/lib/whichVersionIsPinned.js +0 -36
- package/lib/workspacePrefToNpm.d.ts +0 -1
- package/lib/workspacePrefToNpm.js +0 -13
package/lib/pickPackage.js
DELETED
|
@@ -1,631 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { ABBREVIATED_META_DIR, FULL_FILTERED_META_DIR, FULL_META_DIR } from '@pnpm/constants';
|
|
4
|
-
import { createHexHash } from '@pnpm/crypto.hash';
|
|
5
|
-
import { PnpmError } from '@pnpm/error';
|
|
6
|
-
import gfs from '@pnpm/fs.graceful-fs';
|
|
7
|
-
import { globalWarn, logger } from '@pnpm/logger';
|
|
8
|
-
import getRegistryName from 'encode-registry';
|
|
9
|
-
import pLimit, {} from 'p-limit';
|
|
10
|
-
import { fastPathTemp as pathTemp } from 'path-temp';
|
|
11
|
-
import { renameOverwrite } from 'rename-overwrite';
|
|
12
|
-
import semver from 'semver';
|
|
13
|
-
import { clearMeta } from './clearMeta.js';
|
|
14
|
-
import { pickLowestVersionByVersionRange, pickPackageFromMeta, pickVersionByVersionRange, } from './pickPackageFromMeta.js';
|
|
15
|
-
import { toRaw } from './toRaw.js';
|
|
16
|
-
/**
|
|
17
|
-
* prevents simultaneous operations on the meta.json
|
|
18
|
-
* otherwise it would cause EPERM exceptions
|
|
19
|
-
*/
|
|
20
|
-
const metafileOperationLimits = {};
|
|
21
|
-
/**
|
|
22
|
-
* To prevent metafileOperationLimits from holding onto objects in memory on
|
|
23
|
-
* the order of the number of packages, refcount the limiters and drop them
|
|
24
|
-
* once they are no longer needed. Callers of this function should ensure
|
|
25
|
-
* that the limiter is no longer referenced once fn's Promise has resolved.
|
|
26
|
-
*/
|
|
27
|
-
async function runLimited(pkgMirror, fn) {
|
|
28
|
-
let entry;
|
|
29
|
-
try {
|
|
30
|
-
entry = metafileOperationLimits[pkgMirror] ??= { count: 0, limit: pLimit(1) };
|
|
31
|
-
entry.count++;
|
|
32
|
-
return await fn(entry.limit);
|
|
33
|
-
}
|
|
34
|
-
finally {
|
|
35
|
-
entry.count--;
|
|
36
|
-
if (entry.count === 0) {
|
|
37
|
-
metafileOperationLimits[pkgMirror] = undefined;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
// When includeLatestTag is set, the "latest" dist-tag is added as a candidate
|
|
42
|
-
// alongside the requested spec, and the higher-versioned pick wins.
|
|
43
|
-
function runPicker(pickerOpts, spec, pickOne) {
|
|
44
|
-
const currentPkg = pickOne(spec);
|
|
45
|
-
if (!pickerOpts.includeLatestTag)
|
|
46
|
-
return currentPkg;
|
|
47
|
-
const latestPkg = pickOne({ ...spec, type: 'tag', fetchSpec: 'latest' });
|
|
48
|
-
return pickMax(latestPkg, currentPkg);
|
|
49
|
-
}
|
|
50
|
-
// Returns whichever pick has the higher version, treating null as "no match".
|
|
51
|
-
function pickMax(a, b) {
|
|
52
|
-
if (!a)
|
|
53
|
-
return b;
|
|
54
|
-
if (!b)
|
|
55
|
-
return a;
|
|
56
|
-
return semver.lt(a.version, b.version) ? b : a;
|
|
57
|
-
}
|
|
58
|
-
const pickHighest = pickPackageFromMeta.bind(null, pickVersionByVersionRange);
|
|
59
|
-
const pickLowest = pickPackageFromMeta.bind(null, pickLowestVersionByVersionRange);
|
|
60
|
-
// When minimumReleaseAge is active: try the highest mature version; if none
|
|
61
|
-
// satisfies the range, fall back to the lowest version regardless of maturity
|
|
62
|
-
// so the resolver can report the violation inline and let the install layer
|
|
63
|
-
// (or other caller) decide what to do — never throw at this layer.
|
|
64
|
-
function pickRespectingMinReleaseAge(pickerOpts, spec, meta) {
|
|
65
|
-
return runPicker(pickerOpts, spec, (targetSpec) => {
|
|
66
|
-
const highest = pickHighest(pickerOpts, meta, targetSpec);
|
|
67
|
-
if (highest)
|
|
68
|
-
return highest;
|
|
69
|
-
return pickLowest({
|
|
70
|
-
preferredVersionSelectors: pickerOpts.preferredVersionSelectors,
|
|
71
|
-
}, meta, targetSpec);
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
// When minimumReleaseAge is not active: pick by pickLowestVersion preference.
|
|
75
|
-
function pickIgnoringReleaseAge(pickerOpts, spec, meta) {
|
|
76
|
-
const pickVersion = pickerOpts.pickLowestVersion ? pickLowest : pickHighest;
|
|
77
|
-
return runPicker(pickerOpts, spec, (targetSpec) => pickVersion(pickerOpts, meta, targetSpec));
|
|
78
|
-
}
|
|
79
|
-
// Used in shortcut/fall-through paths: if it fails (including with
|
|
80
|
-
// ERR_PNPM_MISSING_TIME), the caller falls through to the next path — e.g.
|
|
81
|
-
// the network fetch that can upgrade abbreviated metadata to full.
|
|
82
|
-
function pickMatchingVersionFast(pickerOpts, spec, meta) {
|
|
83
|
-
return pickerOpts.publishedBy
|
|
84
|
-
? pickRespectingMinReleaseAge(pickerOpts, spec, meta)
|
|
85
|
-
: pickIgnoringReleaseAge(pickerOpts, spec, meta);
|
|
86
|
-
}
|
|
87
|
-
// Used at terminal return sites where no further fallback path exists. When
|
|
88
|
-
// metadata lacks the per-version `time` field and ignoreMissingTimeField is
|
|
89
|
-
// enabled, skip the minimumReleaseAge filter with a warning instead of
|
|
90
|
-
// failing hard.
|
|
91
|
-
function pickMatchingVersionFinal(pickerOpts, spec, meta) {
|
|
92
|
-
try {
|
|
93
|
-
return pickMatchingVersionFast(pickerOpts, spec, meta);
|
|
94
|
-
}
|
|
95
|
-
catch (err) {
|
|
96
|
-
if (pickerOpts.ignoreMissingTimeField && isMissingTimeError(err)) {
|
|
97
|
-
warnMissingTimeFieldOnce(meta.name);
|
|
98
|
-
return pickMatchingVersionFast({
|
|
99
|
-
...pickerOpts,
|
|
100
|
-
publishedBy: undefined,
|
|
101
|
-
publishedByExclude: undefined,
|
|
102
|
-
}, spec, meta);
|
|
103
|
-
}
|
|
104
|
-
throw err;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Packuments promoted into the in-memory cache straight from the on-disk
|
|
109
|
-
* mirror, without registry validation. The mirror may predate versions the
|
|
110
|
-
* registry has, so when a cache hit on such an entry can't satisfy the
|
|
111
|
-
* requested spec (and the resolver isn't offline), `pickPackage` falls
|
|
112
|
-
* through to the regular flow — a conditional registry request — instead of
|
|
113
|
-
* failing the pick, exactly as it would have before the entry was promoted.
|
|
114
|
-
* Network-fetched and 304-revalidated packuments are never in this set, so
|
|
115
|
-
* hits on them keep returning directly even when the pick fails (the caller
|
|
116
|
-
* then falls back to workspace packages or reports no matching version).
|
|
117
|
-
*/
|
|
118
|
-
const unverifiedDiskPackuments = new WeakSet();
|
|
119
|
-
/**
|
|
120
|
-
* Promote a packument parsed from the on-disk mirror into the in-memory
|
|
121
|
-
* cache, so repeat resolutions of the same package (common across a large
|
|
122
|
-
* dependency graph) don't re-read and re-parse the mirror. The entry is
|
|
123
|
-
* remembered as disk-sourced (see {@link unverifiedDiskPackuments}) because it
|
|
124
|
-
* never went through registry validation.
|
|
125
|
-
*/
|
|
126
|
-
function cacheDiskLoadedMeta(metaCache, cacheKey, meta) {
|
|
127
|
-
unverifiedDiskPackuments.add(meta);
|
|
128
|
-
metaCache.set(cacheKey, meta);
|
|
129
|
-
}
|
|
130
|
-
export async function pickPackage(ctx, spec, opts) {
|
|
131
|
-
opts = opts || {};
|
|
132
|
-
const pickerOpts = {
|
|
133
|
-
preferredVersionSelectors: opts.preferredVersionSelectors,
|
|
134
|
-
publishedBy: opts.publishedBy,
|
|
135
|
-
publishedByExclude: opts.publishedByExclude,
|
|
136
|
-
pickLowestVersion: opts.pickLowestVersion,
|
|
137
|
-
includeLatestTag: opts.includeLatestTag,
|
|
138
|
-
ignoreMissingTimeField: ctx.ignoreMissingTimeField,
|
|
139
|
-
};
|
|
140
|
-
validatePackageName(spec.name);
|
|
141
|
-
// Use full metadata for optional dependencies to get libc field.
|
|
142
|
-
// See: https://github.com/pnpm/pnpm/issues/9950
|
|
143
|
-
const fullMetadata = opts.optional === true || ctx.fullMetadata === true;
|
|
144
|
-
const metaDir = fullMetadata
|
|
145
|
-
? (ctx.filterMetadata ? FULL_FILTERED_META_DIR : FULL_META_DIR)
|
|
146
|
-
: ABBREVIATED_META_DIR;
|
|
147
|
-
// Cache key includes the registry so a package of the same name served by two
|
|
148
|
-
// registries in one install can't share a slot (which would resolve the wrong
|
|
149
|
-
// tarball/integrity), plus fullMetadata/filterMetadata so a request is never
|
|
150
|
-
// served a less-detailed or differently-stripped document than it asked for.
|
|
151
|
-
const cacheKey = getPkgMetaCacheKey(opts.registry, spec.name, fullMetadata, ctx.filterMetadata === true);
|
|
152
|
-
const pkgMirror = getPkgMirrorPath(ctx.cacheDir, metaDir, opts.registry, spec.name);
|
|
153
|
-
// updateChecksums must reach the conditional registry request below, so it
|
|
154
|
-
// can't be served from the in-memory cache — which may hold a disk-promoted
|
|
155
|
-
// entry rather than a fresh network fetch (see the updateChecksums doc).
|
|
156
|
-
const cachedMeta = opts.updateChecksums ? undefined : ctx.metaCache.get(cacheKey);
|
|
157
|
-
if (cachedMeta != null) {
|
|
158
|
-
// The in-memory cache may hold abbreviated metadata from an earlier call
|
|
159
|
-
// that didn't need `time` (no publishedBy then). If this call has
|
|
160
|
-
// publishedBy and the package was modified recently, upgrade to full
|
|
161
|
-
// metadata so the maturity check runs properly.
|
|
162
|
-
const upgrade = await maybeUpgradeAbbreviatedMetaForReleaseAge(ctx, spec, opts, cachedMeta);
|
|
163
|
-
let metaForCache = upgrade.meta;
|
|
164
|
-
if (upgrade.upgradedFrom != null) {
|
|
165
|
-
// Persist the upgraded meta to disk too: the on-disk mirror still holds
|
|
166
|
-
// the abbreviated form, so without this a fresh process would re-trigger
|
|
167
|
-
// the upgrade fetch on its next install.
|
|
168
|
-
metaForCache = opts.dryRun
|
|
169
|
-
? upgrade.meta
|
|
170
|
-
: persistUpgradedMeta(ctx, pkgMirror, upgrade.upgradedFrom);
|
|
171
|
-
ctx.metaCache.set(cacheKey, metaForCache);
|
|
172
|
-
}
|
|
173
|
-
const pickedPackage = pickMatchingVersionFinal(pickerOpts, spec, metaForCache);
|
|
174
|
-
if (pickedPackage != null || ctx.offline === true || !unverifiedDiskPackuments.has(metaForCache)) {
|
|
175
|
-
return {
|
|
176
|
-
meta: metaForCache,
|
|
177
|
-
pickedPackage,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
// Disk-promoted meta that can't satisfy the spec: fall through and
|
|
181
|
-
// revalidate against the registry (see unverifiedDiskPackuments).
|
|
182
|
-
}
|
|
183
|
-
return runLimited(pkgMirror, async (limit) => {
|
|
184
|
-
let metaCachedInStore;
|
|
185
|
-
if (ctx.offline === true || ctx.preferOffline === true || opts.pickLowestVersion) {
|
|
186
|
-
metaCachedInStore = await limit(async () => loadMeta(pkgMirror));
|
|
187
|
-
if (ctx.offline) {
|
|
188
|
-
if (metaCachedInStore != null) {
|
|
189
|
-
// maybeUpgradeAbbreviatedMetaForReleaseAge short-circuits when
|
|
190
|
-
// offline, so a later in-memory cache hit returns this same meta
|
|
191
|
-
// without any network access.
|
|
192
|
-
cacheDiskLoadedMeta(ctx.metaCache, cacheKey, metaCachedInStore);
|
|
193
|
-
return {
|
|
194
|
-
meta: metaCachedInStore,
|
|
195
|
-
pickedPackage: pickMatchingVersionFinal(pickerOpts, spec, metaCachedInStore),
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
throw new PnpmError('NO_OFFLINE_META', `Failed to resolve ${toRaw(spec)} in package mirror ${pkgMirror}`);
|
|
199
|
-
}
|
|
200
|
-
if (metaCachedInStore != null) {
|
|
201
|
-
// Disk-cached meta may be abbreviated; upgrade for the maturity check
|
|
202
|
-
// before letting pickMatchingVersionFinal warn-and-skip on missing time.
|
|
203
|
-
const upgrade = await maybeUpgradeAbbreviatedMetaForReleaseAge(ctx, spec, opts, metaCachedInStore);
|
|
204
|
-
metaCachedInStore = upgrade.meta;
|
|
205
|
-
if (upgrade.upgradedFrom != null) {
|
|
206
|
-
// Persist so the next install skips this upgrade fetch entirely.
|
|
207
|
-
if (!opts.dryRun) {
|
|
208
|
-
metaCachedInStore = persistUpgradedMeta(ctx, pkgMirror, upgrade.upgradedFrom);
|
|
209
|
-
}
|
|
210
|
-
ctx.metaCache.set(cacheKey, metaCachedInStore);
|
|
211
|
-
}
|
|
212
|
-
const pickedPackage = pickMatchingVersionFinal(pickerOpts, spec, metaCachedInStore);
|
|
213
|
-
if (pickedPackage) {
|
|
214
|
-
// A cache hit re-runs maybeUpgradeAbbreviatedMetaForReleaseAge, so
|
|
215
|
-
// serving this meta from memory can't bypass the release-age
|
|
216
|
-
// upgrade. When the upgrade branch above already cached the
|
|
217
|
-
// registry-validated upgraded meta, don't overwrite it with a
|
|
218
|
-
// disk-sourced marking.
|
|
219
|
-
if (upgrade.upgradedFrom == null) {
|
|
220
|
-
cacheDiskLoadedMeta(ctx.metaCache, cacheKey, metaCachedInStore);
|
|
221
|
-
}
|
|
222
|
-
return {
|
|
223
|
-
meta: metaCachedInStore,
|
|
224
|
-
pickedPackage,
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
if (!opts.includeLatestTag && !opts.updateChecksums && spec.type === 'version') {
|
|
230
|
-
metaCachedInStore = metaCachedInStore ?? await limit(async () => loadMeta(pkgMirror));
|
|
231
|
-
// use the cached meta only if it has the required package version
|
|
232
|
-
// otherwise it is probably out of date
|
|
233
|
-
if ((metaCachedInStore?.versions?.[spec.fetchSpec]) != null) {
|
|
234
|
-
try {
|
|
235
|
-
const pickedPackage = pickMatchingVersionFast(pickerOpts, spec, metaCachedInStore);
|
|
236
|
-
if (pickedPackage) {
|
|
237
|
-
cacheDiskLoadedMeta(ctx.metaCache, cacheKey, metaCachedInStore);
|
|
238
|
-
return {
|
|
239
|
-
meta: metaCachedInStore,
|
|
240
|
-
pickedPackage,
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
catch {
|
|
245
|
-
// Swallow fast-path errors (e.g. ERR_PNPM_MISSING_TIME from
|
|
246
|
-
// abbreviated meta) and fall through to the network fetch, which
|
|
247
|
-
// can upgrade to full metadata and run the maturity check on
|
|
248
|
-
// real `time` data.
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
if (opts.publishedBy && opts.publishedByExclude?.(spec.name) !== true) {
|
|
253
|
-
const mtime = await limit(async () => getFileMtime(pkgMirror));
|
|
254
|
-
if (mtime != null && mtime >= opts.publishedBy) {
|
|
255
|
-
metaCachedInStore = metaCachedInStore ?? await limit(async () => loadMeta(pkgMirror));
|
|
256
|
-
if (metaCachedInStore != null) {
|
|
257
|
-
try {
|
|
258
|
-
const pickedPackage = pickMatchingVersionFast(pickerOpts, spec, metaCachedInStore);
|
|
259
|
-
if (pickedPackage) {
|
|
260
|
-
return {
|
|
261
|
-
meta: metaCachedInStore,
|
|
262
|
-
pickedPackage,
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
catch {
|
|
267
|
-
// Same as above — fall through to the network fetch.
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
try {
|
|
273
|
-
// Load only the cache headers (etag, modified) for conditional request headers.
|
|
274
|
-
// This avoids reading and parsing the full metadata file (which can be megabytes)
|
|
275
|
-
// when the registry returns 200 and the old metadata would be discarded anyway.
|
|
276
|
-
const cacheHeaders = metaCachedInStore != null
|
|
277
|
-
? { etag: metaCachedInStore.etag, modified: metaCachedInStore.modified ?? metaCachedInStore.time?.modified }
|
|
278
|
-
: await limit(async () => loadMetaHeaders(pkgMirror));
|
|
279
|
-
let fetchResult = await ctx.fetch(spec.name, {
|
|
280
|
-
authHeaderValue: opts.authHeaderValue,
|
|
281
|
-
fullMetadata,
|
|
282
|
-
etag: cacheHeaders?.etag,
|
|
283
|
-
modified: cacheHeaders?.modified,
|
|
284
|
-
registry: opts.registry,
|
|
285
|
-
});
|
|
286
|
-
// 304 Not Modified — registry confirmed local cache is still fresh.
|
|
287
|
-
// Now we need the full metadata, so load it from disk.
|
|
288
|
-
if (fetchResult.notModified) {
|
|
289
|
-
metaCachedInStore = metaCachedInStore ?? await limit(async () => loadMeta(pkgMirror));
|
|
290
|
-
if (metaCachedInStore != null) {
|
|
291
|
-
// The registry just vouched that the cached packument equals its
|
|
292
|
-
// current one, so the validation clock restarts now: bump the
|
|
293
|
-
// mirror's mtime so the publishedBy freshness shortcut above can
|
|
294
|
-
// fire again on the next install. Without this, a mirror older
|
|
295
|
-
// than minimumReleaseAge re-validates on every subsequent
|
|
296
|
-
// install — a 304 never rewrites the file. Fire-and-forget: a
|
|
297
|
-
// read-only cache dir only costs another conditional request.
|
|
298
|
-
if (!opts.dryRun) {
|
|
299
|
-
const now = new Date();
|
|
300
|
-
fs.utimes(pkgMirror, now, now).catch(() => { });
|
|
301
|
-
}
|
|
302
|
-
// The cached metadata may be abbreviated (no per-version `time`).
|
|
303
|
-
// When minimumReleaseAge is active we need `time` for the maturity check,
|
|
304
|
-
// so upgrade to full metadata via a follow-up fetch when warranted.
|
|
305
|
-
// Without this, repeat installs of recently-modified packages would
|
|
306
|
-
// silently bypass the maturity check via the warn-and-skip fallback.
|
|
307
|
-
const upgrade = await maybeUpgradeAbbreviatedMetaForReleaseAge(ctx, spec, opts, metaCachedInStore);
|
|
308
|
-
metaCachedInStore = upgrade.meta;
|
|
309
|
-
if (upgrade.upgradedFrom != null && !opts.dryRun) {
|
|
310
|
-
// Persist the upgraded full metadata to disk so subsequent installs
|
|
311
|
-
// skip this upgrade fetch entirely (the cached meta will then have
|
|
312
|
-
// `time` populated, so the upgrade condition won't trigger).
|
|
313
|
-
metaCachedInStore = persistUpgradedMeta(ctx, pkgMirror, upgrade.upgradedFrom);
|
|
314
|
-
}
|
|
315
|
-
ctx.metaCache.set(cacheKey, metaCachedInStore);
|
|
316
|
-
return {
|
|
317
|
-
meta: metaCachedInStore,
|
|
318
|
-
pickedPackage: pickMatchingVersionFinal(pickerOpts, spec, metaCachedInStore),
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
throw new PnpmError('CACHE_MISSING_AFTER_304', `Metadata cache for ${spec.name} is unreadable after receiving 304 Not Modified`);
|
|
322
|
-
}
|
|
323
|
-
let meta = fetchResult.meta;
|
|
324
|
-
let resultToSave = fetchResult;
|
|
325
|
-
// When minimumReleaseAge is active and we fetched abbreviated metadata,
|
|
326
|
-
// check if the package was recently modified and needs full metadata
|
|
327
|
-
// for per-version time-based filtering.
|
|
328
|
-
//
|
|
329
|
-
// This two-step approach is intentional: abbreviated metadata is much smaller,
|
|
330
|
-
// and most packages won't have been modified recently enough to need the full
|
|
331
|
-
// document. We only upgrade to full metadata when the package's modification
|
|
332
|
-
// date is recent enough that some versions might not yet be "mature."
|
|
333
|
-
if (opts.publishedBy &&
|
|
334
|
-
!fullMetadata &&
|
|
335
|
-
meta.time == null &&
|
|
336
|
-
opts.publishedByExclude?.(spec.name) !== true) {
|
|
337
|
-
const modifiedDate = meta.modified ? new Date(meta.modified) : null;
|
|
338
|
-
const isModifiedValid = modifiedDate != null && !Number.isNaN(modifiedDate.getTime());
|
|
339
|
-
// Strict `>` (not `>=`) so the boundary case `modified == publishedBy`
|
|
340
|
-
// takes the abbreviated fast path: `modified` is an upper bound on
|
|
341
|
-
// every version's publish time, so when it equals the cutoff every
|
|
342
|
-
// version passes the per-version `<=` filter in
|
|
343
|
-
// `filterPkgMetadataByPublishDate` and a full re-fetch isn't needed.
|
|
344
|
-
if (!isModifiedValid || modifiedDate > opts.publishedBy) {
|
|
345
|
-
// Save the abbreviated metadata to the abbreviated cache before re-fetching full.
|
|
346
|
-
if (!opts.dryRun) {
|
|
347
|
-
const abbreviatedJson = prepareJsonForDisk(resultToSave.meta, resultToSave.etag, resultToSave.jsonText);
|
|
348
|
-
// Fire-and-forget save to the abbreviated cache path (pkgMirror).
|
|
349
|
-
runLimited(pkgMirror, (limit) => limit(async () => {
|
|
350
|
-
try {
|
|
351
|
-
await saveMeta(pkgMirror, abbreviatedJson);
|
|
352
|
-
}
|
|
353
|
-
catch (err) { // eslint-disable-line
|
|
354
|
-
// We don't care if this file was not written to the cache
|
|
355
|
-
}
|
|
356
|
-
}));
|
|
357
|
-
}
|
|
358
|
-
const fullFetchResult = await ctx.fetch(spec.name, {
|
|
359
|
-
authHeaderValue: opts.authHeaderValue,
|
|
360
|
-
fullMetadata: true,
|
|
361
|
-
registry: opts.registry,
|
|
362
|
-
});
|
|
363
|
-
if (!fullFetchResult.notModified) {
|
|
364
|
-
resultToSave = fullFetchResult;
|
|
365
|
-
meta = fullFetchResult.meta;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
if (ctx.filterMetadata) {
|
|
370
|
-
meta = clearMeta(meta);
|
|
371
|
-
}
|
|
372
|
-
if (!opts.dryRun) {
|
|
373
|
-
// Serialize before setting meta.etag so it only lives in the headers line, not the body.
|
|
374
|
-
const jsonForDisk = ctx.filterMetadata
|
|
375
|
-
? prepareJsonForDisk(meta, resultToSave.etag)
|
|
376
|
-
: prepareJsonForDisk(resultToSave.meta, resultToSave.etag, resultToSave.jsonText);
|
|
377
|
-
runLimited(pkgMirror, (limit) => limit(async () => {
|
|
378
|
-
try {
|
|
379
|
-
await saveMeta(pkgMirror, jsonForDisk);
|
|
380
|
-
}
|
|
381
|
-
catch (err) { // eslint-disable-line
|
|
382
|
-
// We don't care if this file was not written to the cache
|
|
383
|
-
}
|
|
384
|
-
}));
|
|
385
|
-
}
|
|
386
|
-
meta.etag = resultToSave.etag;
|
|
387
|
-
// only save meta to cache, when it is fresh
|
|
388
|
-
ctx.metaCache.set(cacheKey, meta);
|
|
389
|
-
return {
|
|
390
|
-
meta,
|
|
391
|
-
pickedPackage: pickMatchingVersionFinal(pickerOpts, spec, meta),
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
catch (err) { // eslint-disable-line
|
|
395
|
-
err.spec = spec;
|
|
396
|
-
const meta = await loadMeta(pkgMirror); // TODO: add test for this usecase
|
|
397
|
-
if (meta == null)
|
|
398
|
-
throw err;
|
|
399
|
-
logger.error(err, err);
|
|
400
|
-
logger.debug({ message: `Using cached meta from ${pkgMirror}` });
|
|
401
|
-
return {
|
|
402
|
-
meta,
|
|
403
|
-
pickedPackage: pickMatchingVersionFinal(pickerOpts, spec, meta),
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
// When `minimumReleaseAge` is active and we have abbreviated metadata (which
|
|
409
|
-
// the npm registry serves by default and which omits per-version `time`),
|
|
410
|
-
// the maturity check can't run on the data we have. If the package has been
|
|
411
|
-
// modified since the maturity cutoff, re-fetch with `fullMetadata: true` so
|
|
412
|
-
// `time` is populated and the check can proceed properly. Without this,
|
|
413
|
-
// `pickMatchingVersionFinal` would fall back to its warn-and-skip path,
|
|
414
|
-
// silently bypassing the minimumReleaseAge guarantee for affected packages.
|
|
415
|
-
//
|
|
416
|
-
// Returns the original meta when no upgrade is needed. When an upgrade
|
|
417
|
-
// happens, returns both the upgraded meta and the underlying fetch result
|
|
418
|
-
// so callers can persist it to disk and avoid re-fetching on next install.
|
|
419
|
-
async function maybeUpgradeAbbreviatedMetaForReleaseAge(ctx, spec, opts, meta) {
|
|
420
|
-
if (ctx.offline === true ||
|
|
421
|
-
!opts.publishedBy ||
|
|
422
|
-
meta.time != null ||
|
|
423
|
-
opts.publishedByExclude?.(spec.name) === true) {
|
|
424
|
-
return { meta };
|
|
425
|
-
}
|
|
426
|
-
const modifiedDate = meta.modified ? new Date(meta.modified) : null;
|
|
427
|
-
const isModifiedValid = modifiedDate != null && !Number.isNaN(modifiedDate.getTime());
|
|
428
|
-
if (isModifiedValid && modifiedDate <= opts.publishedBy) {
|
|
429
|
-
// The package was last modified at or before the maturity cutoff. Since
|
|
430
|
-
// `modified` is an upper bound on every version's publish time, no version
|
|
431
|
-
// can be newer than the cutoff, so the abbreviated form is fine.
|
|
432
|
-
// Inclusive at the boundary on purpose: matches the per-version `<=` filter
|
|
433
|
-
// in `filterPkgMetadataByPublishDate`.
|
|
434
|
-
return { meta };
|
|
435
|
-
}
|
|
436
|
-
// When `modified` is missing or malformed we fall through to the upgrade
|
|
437
|
-
// fetch: prefer correctness (run the maturity check on real `time` data)
|
|
438
|
-
// over saving a network call when our cached freshness signal is unusable.
|
|
439
|
-
// Forward etag/modified so the registry can answer 304 if the upgraded
|
|
440
|
-
// representation hasn't actually changed (rare on the npm registry where
|
|
441
|
-
// full and abbreviated have distinct etags, but cheap to support).
|
|
442
|
-
const fullFetchResult = await ctx.fetch(spec.name, {
|
|
443
|
-
authHeaderValue: opts.authHeaderValue,
|
|
444
|
-
fullMetadata: true,
|
|
445
|
-
etag: meta.etag,
|
|
446
|
-
modified: meta.modified,
|
|
447
|
-
registry: opts.registry,
|
|
448
|
-
});
|
|
449
|
-
if (fullFetchResult.notModified) {
|
|
450
|
-
// Upgrade fetch came back 304: keep the abbreviated meta. The downstream
|
|
451
|
-
// `pickMatchingVersionFinal` will fall through to its warn-and-skip path.
|
|
452
|
-
return { meta };
|
|
453
|
-
}
|
|
454
|
-
return { meta: fullFetchResult.meta, upgradedFrom: fullFetchResult };
|
|
455
|
-
}
|
|
456
|
-
// Persists upgraded full metadata to the on-disk cache mirror and returns
|
|
457
|
-
// the meta to store in the in-memory cache. When `filterMetadata` is on, the
|
|
458
|
-
// in-memory and on-disk forms are both stripped via `clearMeta`; otherwise
|
|
459
|
-
// the original raw response body is written and the unstripped meta is kept.
|
|
460
|
-
function persistUpgradedMeta(ctx, pkgMirror, upgradedFrom) {
|
|
461
|
-
const metaForCache = ctx.filterMetadata ? clearMeta(upgradedFrom.meta) : upgradedFrom.meta;
|
|
462
|
-
const jsonForDisk = ctx.filterMetadata
|
|
463
|
-
? prepareJsonForDisk(metaForCache, upgradedFrom.etag)
|
|
464
|
-
: prepareJsonForDisk(upgradedFrom.meta, upgradedFrom.etag, upgradedFrom.jsonText);
|
|
465
|
-
runLimited(pkgMirror, (l) => l(async () => {
|
|
466
|
-
try {
|
|
467
|
-
await saveMeta(pkgMirror, jsonForDisk);
|
|
468
|
-
}
|
|
469
|
-
catch (err) { // eslint-disable-line
|
|
470
|
-
// We don't care if this file was not written to the cache
|
|
471
|
-
}
|
|
472
|
-
}));
|
|
473
|
-
return metaForCache;
|
|
474
|
-
}
|
|
475
|
-
export function encodePkgName(pkgName) {
|
|
476
|
-
if (pkgName !== pkgName.toLowerCase()) {
|
|
477
|
-
return `${pkgName}_${createHexHash(pkgName)}`;
|
|
478
|
-
}
|
|
479
|
-
return pkgName;
|
|
480
|
-
}
|
|
481
|
-
/**
|
|
482
|
-
* Key for the in-memory `metaCache` holding a package's registry metadata. The
|
|
483
|
-
* registry is part of the key so that a package of the same name served by two
|
|
484
|
-
* registries in one install can't collide on a single slot (which would resolve
|
|
485
|
-
* the wrong tarball/integrity). `fullMetadata` and `filterMetadata` keep the
|
|
486
|
-
* abbreviated, full, and filtered-full documents in distinct slots, mirroring
|
|
487
|
-
* the on-disk `metaDir` split: a `filterMetadata` resolver stores a `clearMeta`-
|
|
488
|
-
* stripped packument, so it must not share a slot with an unfiltered full one
|
|
489
|
-
* (reachable only when a `metaCache` is shared across resolvers with different
|
|
490
|
-
* settings). `filterMetadata` only narrows the full slot — abbreviated metadata
|
|
491
|
-
* shares one on-disk mirror regardless, so its key carries no filtered variant.
|
|
492
|
-
* `\x00` can't appear in a registry URL or a package name, so it's an
|
|
493
|
-
* unambiguous separator. The verifier reads this same cache and must build the
|
|
494
|
-
* key with this function.
|
|
495
|
-
*
|
|
496
|
-
* The registry is canonicalized to its origin plus a trailing-slashed path, so
|
|
497
|
-
* the resolver (which may pass a configured named-registry URL verbatim) and
|
|
498
|
-
* the verifier (which routes through trailing-slashed prefixes) converge on one
|
|
499
|
-
* key for the same logical registry instead of creating duplicate slots. Origin
|
|
500
|
-
* and path are preserved, so two registries that genuinely differ never collapse.
|
|
501
|
-
*/
|
|
502
|
-
export function getPkgMetaCacheKey(registry, pkgName, fullMetadata, filterMetadata) {
|
|
503
|
-
const key = `${canonicalizeRegistry(registry)}\x00${pkgName}`;
|
|
504
|
-
if (!fullMetadata)
|
|
505
|
-
return key;
|
|
506
|
-
return filterMetadata ? `${key}:full:filtered` : `${key}:full`;
|
|
507
|
-
}
|
|
508
|
-
function canonicalizeRegistry(registry) {
|
|
509
|
-
try {
|
|
510
|
-
const parsed = new URL(registry);
|
|
511
|
-
const pathname = parsed.pathname.endsWith('/') ? parsed.pathname : `${parsed.pathname}/`;
|
|
512
|
-
return `${parsed.origin}${pathname}`;
|
|
513
|
-
}
|
|
514
|
-
catch {
|
|
515
|
-
return registry;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
/**
|
|
519
|
-
* Path of the on-disk JSONL document where pnpm mirrors a package's registry
|
|
520
|
-
* metadata. `metaDir` selects between abbreviated and full caches.
|
|
521
|
-
*/
|
|
522
|
-
export function getPkgMirrorPath(cacheDir, metaDir, registry, pkgName) {
|
|
523
|
-
return path.join(cacheDir, metaDir, getRegistryName(registry), `${encodePkgName(pkgName)}.jsonl`);
|
|
524
|
-
}
|
|
525
|
-
/**
|
|
526
|
-
* Formats metadata for disk storage as two-line NDJSON:
|
|
527
|
-
* Line 1: cache headers (etag, modified) — small, fast to read
|
|
528
|
-
* Line 2: the full registry metadata JSON — unchanged from the registry response
|
|
529
|
-
*/
|
|
530
|
-
export function prepareJsonForDisk(meta, etag, jsonText) {
|
|
531
|
-
const modified = meta.modified ?? meta.time?.modified;
|
|
532
|
-
const headers = JSON.stringify({ etag, modified });
|
|
533
|
-
const body = jsonText ?? JSON.stringify(meta);
|
|
534
|
-
return `${headers}\n${body}`;
|
|
535
|
-
}
|
|
536
|
-
function isMissingTimeError(err) {
|
|
537
|
-
return (err != null &&
|
|
538
|
-
typeof err === 'object' &&
|
|
539
|
-
'code' in err &&
|
|
540
|
-
err.code === 'ERR_PNPM_MISSING_TIME');
|
|
541
|
-
}
|
|
542
|
-
// Cap the size so long-lived processes (daemons, store servers) can't leak
|
|
543
|
-
// memory via this Set as they resolve ever more distinct packages.
|
|
544
|
-
const MAX_WARNED_MISSING_TIME = 1024;
|
|
545
|
-
const warnedMissingTimeFor = new Set();
|
|
546
|
-
export function warnMissingTimeFieldOnce(pkgName) {
|
|
547
|
-
if (warnedMissingTimeFor.has(pkgName))
|
|
548
|
-
return;
|
|
549
|
-
if (warnedMissingTimeFor.size >= MAX_WARNED_MISSING_TIME) {
|
|
550
|
-
// Set preserves insertion order, so the first entry is the oldest.
|
|
551
|
-
const oldest = warnedMissingTimeFor.values().next().value;
|
|
552
|
-
if (oldest != null)
|
|
553
|
-
warnedMissingTimeFor.delete(oldest);
|
|
554
|
-
}
|
|
555
|
-
warnedMissingTimeFor.add(pkgName);
|
|
556
|
-
globalWarn(`The metadata of ${pkgName} is missing the "time" field; skipping the minimumReleaseAge check for this package.`);
|
|
557
|
-
}
|
|
558
|
-
async function getFileMtime(filePath) {
|
|
559
|
-
try {
|
|
560
|
-
const stat = await fs.stat(filePath);
|
|
561
|
-
return stat.mtime;
|
|
562
|
-
}
|
|
563
|
-
catch {
|
|
564
|
-
return null;
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
/**
|
|
568
|
-
* Reads only the first line of the cached NDJSON metadata file to extract
|
|
569
|
-
* the cache headers (etag, modified). This avoids reading and
|
|
570
|
-
* parsing the full metadata (which can be megabytes for popular packages)
|
|
571
|
-
* when we only need conditional-request headers.
|
|
572
|
-
*/
|
|
573
|
-
export async function loadMetaHeaders(pkgMirror) {
|
|
574
|
-
let fh;
|
|
575
|
-
try {
|
|
576
|
-
fh = await fs.open(pkgMirror, 'r');
|
|
577
|
-
// The first line (headers JSON) is typically ~100 bytes; 1 KB is plenty.
|
|
578
|
-
const buf = Buffer.alloc(1024);
|
|
579
|
-
const { bytesRead } = await fh.read(buf, 0, 1024, 0);
|
|
580
|
-
if (bytesRead === 0)
|
|
581
|
-
return null;
|
|
582
|
-
const chunk = buf.toString('utf8', 0, bytesRead);
|
|
583
|
-
const newlineIdx = chunk.indexOf('\n');
|
|
584
|
-
if (newlineIdx === -1)
|
|
585
|
-
return null;
|
|
586
|
-
return JSON.parse(chunk.slice(0, newlineIdx));
|
|
587
|
-
}
|
|
588
|
-
catch {
|
|
589
|
-
return null;
|
|
590
|
-
}
|
|
591
|
-
finally {
|
|
592
|
-
await fh?.close();
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
/**
|
|
596
|
-
* Reads the full metadata from the cached NDJSON file.
|
|
597
|
-
* Line 1: cache headers (etag, modified)
|
|
598
|
-
* Line 2: registry metadata JSON
|
|
599
|
-
*/
|
|
600
|
-
export async function loadMeta(pkgMirror) {
|
|
601
|
-
try {
|
|
602
|
-
const data = await gfs.readFile(pkgMirror, 'utf8');
|
|
603
|
-
const newlineIdx = data.indexOf('\n');
|
|
604
|
-
if (newlineIdx === -1)
|
|
605
|
-
return null;
|
|
606
|
-
const headers = JSON.parse(data.slice(0, newlineIdx));
|
|
607
|
-
const meta = JSON.parse(data.slice(newlineIdx + 1));
|
|
608
|
-
meta.etag = headers.etag;
|
|
609
|
-
return meta;
|
|
610
|
-
}
|
|
611
|
-
catch {
|
|
612
|
-
return null;
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
const createdDirs = new Set();
|
|
616
|
-
export async function saveMeta(pkgMirror, json) {
|
|
617
|
-
const dir = path.dirname(pkgMirror);
|
|
618
|
-
if (!createdDirs.has(dir)) {
|
|
619
|
-
await fs.mkdir(dir, { recursive: true });
|
|
620
|
-
createdDirs.add(dir);
|
|
621
|
-
}
|
|
622
|
-
const temp = pathTemp(pkgMirror);
|
|
623
|
-
await gfs.writeFile(temp, json, 'utf8');
|
|
624
|
-
await renameOverwrite(temp, pkgMirror);
|
|
625
|
-
}
|
|
626
|
-
function validatePackageName(pkgName) {
|
|
627
|
-
if (pkgName.includes('/') && pkgName[0] !== '@') {
|
|
628
|
-
throw new PnpmError('INVALID_PACKAGE_NAME', `Package name ${pkgName} is invalid, it should have a @scope`);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
//# sourceMappingURL=pickPackage.js.map
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { PackageInRegistry, PackageMeta, PackageMetaWithTime } from '@pnpm/resolving.registry.types';
|
|
2
|
-
import type { VersionSelectors } from '@pnpm/resolving.resolver-base';
|
|
3
|
-
import type { PackageVersionPolicy } from '@pnpm/types';
|
|
4
|
-
import type { RegistryPackageSpec } from './parseBareSpecifier.js';
|
|
5
|
-
export interface PickVersionByVersionRangeOptions {
|
|
6
|
-
meta: PackageMeta;
|
|
7
|
-
versionRange: string;
|
|
8
|
-
preferredVersionSelectors?: VersionSelectors;
|
|
9
|
-
publishedBy?: Date;
|
|
10
|
-
}
|
|
11
|
-
export type PickVersionByVersionRange = (options: PickVersionByVersionRangeOptions) => string | null;
|
|
12
|
-
export interface PickPackageFromMetaOptions {
|
|
13
|
-
preferredVersionSelectors: VersionSelectors | undefined;
|
|
14
|
-
publishedBy?: Date;
|
|
15
|
-
publishedByExclude?: PackageVersionPolicy;
|
|
16
|
-
}
|
|
17
|
-
export declare function pickPackageFromMeta(pickVersionByVersionRangeFn: PickVersionByVersionRange, { preferredVersionSelectors, publishedBy, publishedByExclude, }: PickPackageFromMetaOptions, meta: PackageMeta, spec: RegistryPackageSpec): PackageInRegistry | null;
|
|
18
|
-
export declare function assertMetaHasTime(meta: PackageMeta): asserts meta is PackageMetaWithTime;
|
|
19
|
-
export declare function pickLowestVersionByVersionRange({ meta, versionRange, preferredVersionSelectors }: PickVersionByVersionRangeOptions): string | null;
|
|
20
|
-
export declare function pickVersionByVersionRange({ meta, versionRange, preferredVersionSelectors }: PickVersionByVersionRangeOptions): string | null;
|