@pnpm/resolving.npm-resolver 1102.1.6 → 1102.1.8

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