@pnpm/resolving.npm-resolver 1103.2.0 → 1104.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.
@@ -1,4 +1,5 @@
1
1
  import type { PackageInRegistry, PackageMeta } from '@pnpm/resolving.registry.types';
2
+ import type { TrustPolicy } from '@pnpm/types';
2
3
  import { type FetchMetadataNotModifiedResult, type FetchMetadataResult } from './fetch.js';
3
4
  import type { RegistryPackageSpec } from './parseBareSpecifier.js';
4
5
  import { type PickPackageFromMetaOptions } from './pickPackageFromMeta.js';
@@ -22,6 +23,7 @@ export interface PickPackageOptions extends PickPackageFromMetaOptions {
22
23
  dryRun: boolean;
23
24
  includeLatestTag?: boolean;
24
25
  optional?: boolean;
26
+ trustPolicy?: TrustPolicy;
25
27
  /**
26
28
  * When true, force a conditional registry request so a stale on-disk
27
29
  * packument can't satisfy the call: the on-disk exact-version fast
@@ -44,12 +46,23 @@ export declare function pickPackage(ctx: {
44
46
  modified?: string;
45
47
  }) => Promise<FetchMetadataResult | FetchMetadataNotModifiedResult>;
46
48
  fullMetadata?: boolean;
49
+ /**
50
+ * Whether a time-based resolution has to read the full metadata of
51
+ * `registry`, because that registry's abbreviated form carries no `time`.
52
+ * Asked per registry: the answer differs between the public registry and a
53
+ * proxy that does carry it, and the mirror path and cache key below are
54
+ * already keyed by registry, so two registries may disagree within one
55
+ * install.
56
+ */
57
+ needsFullMetadataFor?: (registry: string) => boolean;
47
58
  metaCache: PackageMetaCache;
48
59
  cacheDir: string;
49
60
  offline?: boolean;
50
61
  preferOffline?: boolean;
51
62
  filterMetadata?: boolean;
52
63
  ignoreMissingTimeField?: boolean;
64
+ /** Packuments whose release-age upgrade fetch already answered 304 in this resolver. */
65
+ releaseAgeUpgradeCheckedPackuments?: WeakSet<PackageMeta>;
53
66
  }, spec: RegistryPackageSpec, opts: PickPackageOptions): Promise<{
54
67
  meta: PackageMeta;
55
68
  pickedPackage: PackageInRegistry | null;
@@ -91,7 +104,13 @@ export declare function getPkgMirrorPath(cacheDir: string, metaDir: string, regi
91
104
  * there), so a `meta` that carries one is serialized without it.
92
105
  */
93
106
  export declare function prepareJsonForDisk(meta: PackageMeta, etag: string | undefined, jsonText?: string): string;
94
- export declare function warnMissingTimeFieldOnce(pkgName: string): void;
107
+ /**
108
+ * At most one warning per package per check. `minimumReleaseAge` and
109
+ * `trustPolicy` both go dark on the same missing field, so keying by package
110
+ * alone would let whichever check ran first silence the other and leave the
111
+ * user told about only one of the two skips.
112
+ */
113
+ export declare function warnMissingTimeFieldOnce(pkgName: string, skippedCheck: 'minimumReleaseAge' | 'trustPolicy'): void;
95
114
  interface MetaHeaders {
96
115
  etag?: string;
97
116
  modified?: string;
@@ -12,7 +12,8 @@ import { renameOverwrite } from 'rename-overwrite';
12
12
  import semver from 'semver';
13
13
  import { clearMeta, retainsFullMeta } from './clearMeta.js';
14
14
  import { notModifiedWithoutCacheError, } from './fetch.js';
15
- import { pickLowestVersionByVersionRange, pickPackageFromMeta, pickVersionByVersionRange, } from './pickPackageFromMeta.js';
15
+ import { getDominantLockfileVersion, pickLowestVersionByVersionRange, pickPackageFromMeta, pickStableCachedRangeVersion, pickVersionByVersionRange, } from './pickPackageFromMeta.js';
16
+ import { dropIncompletePublishTimes } from './publishTimes.js';
16
17
  import { toRaw } from './toRaw.js';
17
18
  /**
18
19
  * prevents simultaneous operations on the meta.json
@@ -39,6 +40,13 @@ async function runLimited(pkgMirror, fn) {
39
40
  }
40
41
  }
41
42
  }
43
+ function canReuseStableCachedRange(spec, opts) {
44
+ return (spec.type === 'range' &&
45
+ !opts.includeLatestTag &&
46
+ !opts.updateChecksums &&
47
+ opts.publishedBy == null &&
48
+ opts.trustPolicy !== 'no-downgrade');
49
+ }
42
50
  // When includeLatestTag is set, the "latest" dist-tag is added as a candidate
43
51
  // alongside the requested spec, and the higher-versioned pick wins.
44
52
  function runPicker(pickerOpts, spec, pickOne) {
@@ -58,15 +66,17 @@ function pickMax(a, b) {
58
66
  }
59
67
  const pickHighest = pickPackageFromMeta.bind(null, pickVersionByVersionRange);
60
68
  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.
69
+ // `minimumReleaseAge` narrows which versions are on offer; `pickLowestVersion`
70
+ // decides which end of what is left to take. The fallback deliberately drops
71
+ // the maturity filter so a range no mature version satisfies still yields a
72
+ // pick, which the install layer reports as a violation rather than this layer
73
+ // throwing.
65
74
  function pickRespectingMinReleaseAge(pickerOpts, spec, meta) {
66
75
  return runPicker(pickerOpts, spec, (targetSpec) => {
67
- const highest = pickHighest(pickerOpts, meta, targetSpec);
68
- if (highest)
69
- return highest;
76
+ const pickMature = pickerOpts.pickLowestVersion ? pickLowest : pickHighest;
77
+ const mature = pickMature(pickerOpts, meta, targetSpec);
78
+ if (mature)
79
+ return mature;
70
80
  return pickLowest({
71
81
  preferredVersionSelectors: pickerOpts.preferredVersionSelectors,
72
82
  }, meta, targetSpec);
@@ -95,7 +105,7 @@ function pickMatchingVersionFinal(pickerOpts, spec, meta) {
95
105
  }
96
106
  catch (err) {
97
107
  if (pickerOpts.ignoreMissingTimeField && isMissingTimeError(err)) {
98
- warnMissingTimeFieldOnce(meta.name);
108
+ warnMissingTimeFieldOnce(meta.name, 'minimumReleaseAge');
99
109
  return pickMatchingVersionFast({
100
110
  ...pickerOpts,
101
111
  publishedBy: undefined,
@@ -150,7 +160,12 @@ export async function pickPackage(ctx, spec, opts) {
150
160
  validatePackageName(spec.name);
151
161
  // Use full metadata for optional dependencies to get libc field.
152
162
  // See: https://github.com/pnpm/pnpm/issues/9950
153
- const fullMetadata = opts.optional === true || ctx.fullMetadata === true;
163
+ // The per-registry answer is authoritative when the caller can give one: it
164
+ // already folds in the reasons that hold for every registry, so a registry
165
+ // that carries `time` is free to stay on abbreviated metadata while the
166
+ // others do not.
167
+ const policyWantsFullMetadata = ctx.needsFullMetadataFor?.(opts.registry) ?? ctx.fullMetadata === true;
168
+ const fullMetadata = opts.optional === true || policyWantsFullMetadata;
154
169
  const metaDir = fullMetadata
155
170
  ? (ctx.filterMetadata ? FULL_FILTERED_META_DIR : FULL_META_DIR)
156
171
  : ABBREVIATED_META_DIR;
@@ -175,7 +190,19 @@ export async function pickPackage(ctx, spec, opts) {
175
190
  ctx.metaCache.set(cacheKey, metaForCache);
176
191
  }
177
192
  const pickedPackage = pickMatchingVersionFinal(pickerOpts, spec, metaForCache);
178
- if (pickedPackage != null || ctx.offline === true || !unverifiedDiskPackuments.has(metaForCache)) {
193
+ const unverified = unverifiedDiskPackuments.has(metaForCache);
194
+ const stableCachedRangeVersion = unverified &&
195
+ canReuseStableCachedRange(spec, opts)
196
+ ? getDominantLockfileVersion(spec.fetchSpec, opts.preferredVersionSelectors)
197
+ : null;
198
+ const unverifiedPickIsSafe = ctx.preferOffline === true ||
199
+ opts.pickLowestVersion === true ||
200
+ spec.type === 'version' ||
201
+ (pickedPackage != null && pickedPackage.version === stableCachedRangeVersion);
202
+ const cacheResultCanReturn = ctx.offline === true ||
203
+ !unverified ||
204
+ (pickedPackage != null && unverifiedPickIsSafe);
205
+ if (cacheResultCanReturn) {
179
206
  return {
180
207
  meta: metaForCache,
181
208
  pickedPackage,
@@ -269,6 +296,35 @@ export async function pickPackage(ctx, spec, opts) {
269
296
  }
270
297
  }
271
298
  }
299
+ const dominantLockfileVersion = canReuseStableCachedRange(spec, opts)
300
+ ? getDominantLockfileVersion(spec.fetchSpec, opts.preferredVersionSelectors)
301
+ : null;
302
+ if (dominantLockfileVersion != null) {
303
+ diskMeta = diskMeta ?? await limit(loadMetaCondensed);
304
+ if (diskMeta != null) {
305
+ try {
306
+ const stableVersion = pickStableCachedRangeVersion({
307
+ meta: diskMeta,
308
+ preferredVersionSelectors: opts.preferredVersionSelectors,
309
+ versionRange: spec.fetchSpec,
310
+ });
311
+ if (stableVersion != null) {
312
+ // Strict dominance makes the preferred tier a singleton, so the
313
+ // highest-version picker used by the proof and the normal picker
314
+ // agree even if pickLowestVersion reaches this code in the future.
315
+ const pickedPackage = pickMatchingVersionFast(pickerOpts, spec, diskMeta);
316
+ if (pickedPackage?.version === stableVersion) {
317
+ cacheDiskLoadedMeta(ctx.metaCache, cacheKey, diskMeta);
318
+ return { meta: diskMeta, pickedPackage };
319
+ }
320
+ }
321
+ }
322
+ catch {
323
+ // Any malformed cached metadata falls through to normal online
324
+ // resolution, matching the neighboring disk fast paths.
325
+ }
326
+ }
327
+ }
272
328
  if (opts.publishedBy && opts.publishedByExclude?.(spec.name) !== true) {
273
329
  const mtime = await limit(async () => getFileMtime(pkgMirror));
274
330
  if (mtime != null && mtime >= opts.publishedBy) {
@@ -375,6 +431,7 @@ export async function pickPackage(ctx, spec, opts) {
375
431
  // and most packages won't have been modified recently enough to need the full
376
432
  // document. We only upgrade to full metadata when the package's modification
377
433
  // date is recent enough that some versions might not yet be "mature."
434
+ let attemptedReleaseAgeUpgrade = false;
378
435
  if (opts.publishedBy &&
379
436
  !fullMetadata &&
380
437
  meta.time == null &&
@@ -391,6 +448,7 @@ export async function pickPackage(ctx, spec, opts) {
391
448
  if (!opts.dryRun) {
392
449
  saveMetaBestEffort(pkgMirror, prepareJsonForDisk(resultToSave.meta, resultToSave.etag, resultToSave.jsonText));
393
450
  }
451
+ attemptedReleaseAgeUpgrade = true;
394
452
  const fullFetchResult = await ctx.fetch(spec.name, {
395
453
  authHeaderValue: opts.authHeaderValue,
396
454
  fullMetadata: true,
@@ -403,6 +461,9 @@ export async function pickPackage(ctx, spec, opts) {
403
461
  }
404
462
  }
405
463
  meta = condenseMetaForCache(ctx, meta);
464
+ if (attemptedReleaseAgeUpgrade) {
465
+ ctx.releaseAgeUpgradeCheckedPackuments?.add(meta);
466
+ }
406
467
  if (!opts.dryRun) {
407
468
  // Mirror the raw registry body, unless the retained form is
408
469
  // deliberately narrower: `filterMetadata` always mirrors the stripped
@@ -439,6 +500,7 @@ async function maybeUpgradeAbbreviatedMetaForReleaseAge(ctx, spec, opts, meta) {
439
500
  if (ctx.offline === true ||
440
501
  !opts.publishedBy ||
441
502
  meta.time != null ||
503
+ ctx.releaseAgeUpgradeCheckedPackuments?.has(meta) === true ||
442
504
  opts.publishedByExclude?.(spec.name) === true) {
443
505
  return { meta };
444
506
  }
@@ -466,8 +528,11 @@ async function maybeUpgradeAbbreviatedMetaForReleaseAge(ctx, spec, opts, meta) {
466
528
  registry: opts.registry,
467
529
  });
468
530
  if (fullFetchResult.notModified) {
469
- // Upgrade fetch came back 304: keep the abbreviated meta. The downstream
470
- // `pickMatchingVersionFinal` will fall through to its warn-and-skip path.
531
+ // Upgrade fetch came back 304: the registry has no fuller form of this
532
+ // document, so keep it and let `pickMatchingVersionFinal` fall through to
533
+ // its warn-and-skip path. Remember the outcome against the packument
534
+ // itself so no other pick in this resolver repeats the request.
535
+ ctx.releaseAgeUpgradeCheckedPackuments?.add(meta);
471
536
  return { meta };
472
537
  }
473
538
  return { meta: fullFetchResult.meta, upgradedFrom: fullFetchResult };
@@ -478,12 +543,26 @@ async function maybeUpgradeAbbreviatedMetaForReleaseAge(ctx, spec, opts, meta) {
478
543
  * pre-upgrade abbreviated form without `time`, and every future install
479
544
  * would re-trigger the upgrade fetch.
480
545
  */
546
+ /**
547
+ * The document to serve and cache after an upgrade attempt, marked so no
548
+ * later pick in this resolver repeats the request.
549
+ *
550
+ * Marking belongs here rather than in
551
+ * {@link maybeUpgradeAbbreviatedMetaForReleaseAge} because persisting the
552
+ * response to the mirror can hand back a different object, and only the one
553
+ * that reaches the cache is worth remembering. A registry whose full form is
554
+ * no more complete than its abbreviated one answers `200` rather than `304`,
555
+ * so both outcomes have to be marked — otherwise every dependency edge
556
+ * re-asks for the same full document.
557
+ */
481
558
  function upgradeMetaForCache(ctx, upgrade, opts) {
482
559
  if (upgrade.upgradedFrom == null)
483
560
  return upgrade.meta;
484
- if (opts.dryRun)
485
- return condenseMetaForCache(ctx, upgrade.meta);
486
- return persistUpgradedMeta(ctx, opts.pkgMirror, upgrade.upgradedFrom);
561
+ const meta = opts.dryRun
562
+ ? condenseMetaForCache(ctx, upgrade.meta)
563
+ : persistUpgradedMeta(ctx, opts.pkgMirror, upgrade.upgradedFrom);
564
+ ctx.releaseAgeUpgradeCheckedPackuments?.add(meta);
565
+ return meta;
487
566
  }
488
567
  // A condensing resolver keeps and mirrors the condensed form — the mirror
489
568
  // only has to carry `time` into the next install; otherwise the raw response
@@ -584,8 +663,17 @@ function isMissingTimeError(err) {
584
663
  // memory via this Set as they resolve ever more distinct packages.
585
664
  const MAX_WARNED_MISSING_TIME = 1024;
586
665
  const warnedMissingTimeFor = new Set();
587
- export function warnMissingTimeFieldOnce(pkgName) {
588
- if (warnedMissingTimeFor.has(pkgName))
666
+ /**
667
+ * At most one warning per package per check. `minimumReleaseAge` and
668
+ * `trustPolicy` both go dark on the same missing field, so keying by package
669
+ * alone would let whichever check ran first silence the other and leave the
670
+ * user told about only one of the two skips.
671
+ */
672
+ export function warnMissingTimeFieldOnce(pkgName, skippedCheck) {
673
+ // A package name cannot contain ':', so the check name prefix cannot
674
+ // collide with a name that happens to embed it.
675
+ const key = `${skippedCheck}:${pkgName}`;
676
+ if (warnedMissingTimeFor.has(key))
589
677
  return;
590
678
  if (warnedMissingTimeFor.size >= MAX_WARNED_MISSING_TIME) {
591
679
  // Set preserves insertion order, so the first entry is the oldest.
@@ -593,8 +681,8 @@ export function warnMissingTimeFieldOnce(pkgName) {
593
681
  if (oldest != null)
594
682
  warnedMissingTimeFor.delete(oldest);
595
683
  }
596
- warnedMissingTimeFor.add(pkgName);
597
- globalWarn(`The metadata of ${pkgName} is missing the "time" field; skipping the minimumReleaseAge check for this package.`);
684
+ warnedMissingTimeFor.add(key);
685
+ globalWarn(`The metadata of ${pkgName} is missing the "time" field; skipping the ${skippedCheck} check for this package.`);
598
686
  }
599
687
  async function getFileMtime(filePath) {
600
688
  try {
@@ -646,6 +734,7 @@ export async function loadMeta(pkgMirror) {
646
734
  return null;
647
735
  const headers = JSON.parse(data.slice(0, newlineIdx));
648
736
  const meta = JSON.parse(data.slice(newlineIdx + 1));
737
+ dropIncompletePublishTimes(meta);
649
738
  meta.etag = headers.etag;
650
739
  return meta;
651
740
  }
@@ -1,5 +1,5 @@
1
1
  import type { PackageInRegistry, PackageMeta, PackageMetaWithTime } from '@pnpm/resolving.registry.types';
2
- import type { VersionSelectors } from '@pnpm/resolving.resolver-base';
2
+ import { type VersionSelectors } from '@pnpm/resolving.resolver-base';
3
3
  import type { PackageVersionPolicy } from '@pnpm/types';
4
4
  import type { RegistryPackageSpec } from './parseBareSpecifier.js';
5
5
  export interface PickVersionByVersionRangeOptions {
@@ -41,3 +41,9 @@ export declare function applyPublishedByPolicy(meta: PackageMeta, publishedBy: D
41
41
  export declare function assertMetaHasTime(meta: PackageMeta): asserts meta is PackageMetaWithTime;
42
42
  export declare function pickLowestVersionByVersionRange({ meta, versionRange, preferredVersionSelectors }: PickVersionByVersionRangeOptions): string | null;
43
43
  export declare function pickVersionByVersionRange({ meta, versionRange, preferredVersionSelectors }: PickVersionByVersionRangeOptions): string | null;
44
+ /**
45
+ * Returns the cached version only when lockfile preferences prove that no
46
+ * version missing from the cached packument could tie or outrank it.
47
+ */
48
+ export declare function pickStableCachedRangeVersion({ meta, preferredVersionSelectors, versionRange, }: PickVersionByVersionRangeOptions): string | null;
49
+ export declare function getDominantLockfileVersion(versionRange: string, preferredVersionSelectors?: VersionSelectors): string | null;
@@ -1,6 +1,7 @@
1
1
  import util from 'node:util';
2
2
  import { PnpmError } from '@pnpm/error';
3
3
  import { filterPkgMetadataByPublishDate } from '@pnpm/resolving.registry.pkg-metadata-filter';
4
+ import { EXISTING_VERSION_SELECTOR_WEIGHT, } from '@pnpm/resolving.resolver-base';
4
5
  import semver from 'semver';
5
6
  export function pickPackageFromMeta(pickVersionByVersionRangeFn, { preferredVersionSelectors, publishedBy, publishedByExclude, }, meta, spec) {
6
7
  if (publishedBy) {
@@ -152,6 +153,86 @@ export function pickVersionByVersionRange({ meta, versionRange, preferredVersion
152
153
  }
153
154
  return maxVersion;
154
155
  }
156
+ /**
157
+ * Returns the cached version only when lockfile preferences prove that no
158
+ * version missing from the cached packument could tie or outrank it.
159
+ */
160
+ export function pickStableCachedRangeVersion({ meta, preferredVersionSelectors, versionRange, }) {
161
+ const dominantLockfileVersion = getDominantLockfileVersion(versionRange, preferredVersionSelectors);
162
+ if (dominantLockfileVersion == null || meta.versions[dominantLockfileVersion] == null)
163
+ return null;
164
+ try {
165
+ const pickedVersion = pickVersionByVersionRange({ meta, preferredVersionSelectors, versionRange });
166
+ return pickedVersion === dominantLockfileVersion ? dominantLockfileVersion : null;
167
+ }
168
+ catch {
169
+ return null;
170
+ }
171
+ }
172
+ export function getDominantLockfileVersion(versionRange, preferredVersionSelectors) {
173
+ if (preferredVersionSelectors == null)
174
+ return null;
175
+ let lockfileVersion;
176
+ for (const [selector, value] of Object.entries(preferredVersionSelectors)) {
177
+ if (selector === versionRange)
178
+ continue;
179
+ const { selectorType, weight } = preferredSelectorInfo(value);
180
+ if (!Number.isSafeInteger(weight) || weight <= 0)
181
+ return null;
182
+ if (selectorType === 'version' &&
183
+ weight >= EXISTING_VERSION_SELECTOR_WEIGHT &&
184
+ semverSatisfiesLoose(selector, versionRange)) {
185
+ if (lockfileVersion != null)
186
+ return null;
187
+ lockfileVersion = selector;
188
+ }
189
+ }
190
+ if (lockfileVersion == null)
191
+ return null;
192
+ let guaranteedLockfileWeight = 0;
193
+ let maximumOtherVersionWeight = 0;
194
+ for (const [selector, value] of Object.entries(preferredVersionSelectors)) {
195
+ if (selector === versionRange)
196
+ continue;
197
+ const { selectorType, weight } = preferredSelectorInfo(value);
198
+ switch (selectorType) {
199
+ case 'version':
200
+ if (selector === lockfileVersion) {
201
+ guaranteedLockfileWeight += weight;
202
+ }
203
+ else if (weight < EXISTING_VERSION_SELECTOR_WEIGHT &&
204
+ semverSatisfiesLoose(selector, versionRange)) {
205
+ maximumOtherVersionWeight += weight;
206
+ }
207
+ break;
208
+ case 'range':
209
+ if (semverSatisfiesLoose(lockfileVersion, selector)) {
210
+ guaranteedLockfileWeight += weight;
211
+ }
212
+ // Conservatively assume an unseen version can satisfy every preferred
213
+ // range, even when proving range intersection would be more precise.
214
+ maximumOtherVersionWeight += weight;
215
+ break;
216
+ case 'tag':
217
+ // A registry can move a tag between requests. Do not count its current
218
+ // target toward the lockfile version, and assume all tags could move to
219
+ // the same unseen version.
220
+ maximumOtherVersionWeight += weight;
221
+ break;
222
+ }
223
+ if (!Number.isSafeInteger(guaranteedLockfileWeight) ||
224
+ !Number.isSafeInteger(maximumOtherVersionWeight))
225
+ return null;
226
+ }
227
+ return guaranteedLockfileWeight > maximumOtherVersionWeight
228
+ ? lockfileVersion
229
+ : null;
230
+ }
231
+ function preferredSelectorInfo(value) {
232
+ return typeof value === 'string'
233
+ ? { selectorType: value, weight: 1 }
234
+ : value;
235
+ }
155
236
  function prioritizePreferredVersions(meta, versionRange, preferredVerSelectors) {
156
237
  const preferredVerSelectorsArr = Object.entries(preferredVerSelectors ?? {});
157
238
  const versionsPrioritizer = new PreferredVersionsPrioritizer();
@@ -163,9 +244,7 @@ function prioritizePreferredVersions(meta, versionRange, preferredVerSelectors)
163
244
  }
164
245
  // Then apply weights from preferred selectors
165
246
  for (const [preferredSelector, preferredSelectorType] of preferredVerSelectorsArr) {
166
- const { selectorType, weight } = typeof preferredSelectorType === 'string'
167
- ? { selectorType: preferredSelectorType, weight: 1 }
168
- : preferredSelectorType;
247
+ const { selectorType, weight } = preferredSelectorInfo(preferredSelectorType);
169
248
  if (preferredSelector === versionRange)
170
249
  continue;
171
250
  switch (selectorType) {
@@ -0,0 +1,24 @@
1
+ import type { PackageMeta } from '@pnpm/resolving.registry.types';
2
+ /**
3
+ * Drops `time` unless it carries a publish timestamp for every version the
4
+ * packument lists.
5
+ *
6
+ * Registries may answer with a partial map: npmmirror adds `time` to its
7
+ * abbreviated documents but fills it in only for the versions it has synced
8
+ * since it started recording publish times, leaving the rest out. A partial
9
+ * map is indistinguishable from a complete one at the point of use, so the
10
+ * `minimumReleaseAge` filter reads every absent timestamp as "not mature"
11
+ * and silently drops the version — resolution then falls back to the lowest
12
+ * match.
13
+ *
14
+ * A map that can't decide maturity is worth nothing to the resolver, so it
15
+ * is normalized away where the document is parsed. Every packument past that
16
+ * point then carries either a complete `time` or none at all — the shape the
17
+ * npm registry's own abbreviated documents have, and the one the rest of the
18
+ * resolver is written against.
19
+ *
20
+ * A packument with no versions keeps whatever `time` it has — there is
21
+ * nothing for the map to be incomplete about — and a version whose entry is
22
+ * an empty string counts as absent.
23
+ */
24
+ export declare function dropIncompletePublishTimes(meta: PackageMeta): void;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Drops `time` unless it carries a publish timestamp for every version the
3
+ * packument lists.
4
+ *
5
+ * Registries may answer with a partial map: npmmirror adds `time` to its
6
+ * abbreviated documents but fills it in only for the versions it has synced
7
+ * since it started recording publish times, leaving the rest out. A partial
8
+ * map is indistinguishable from a complete one at the point of use, so the
9
+ * `minimumReleaseAge` filter reads every absent timestamp as "not mature"
10
+ * and silently drops the version — resolution then falls back to the lowest
11
+ * match.
12
+ *
13
+ * A map that can't decide maturity is worth nothing to the resolver, so it
14
+ * is normalized away where the document is parsed. Every packument past that
15
+ * point then carries either a complete `time` or none at all — the shape the
16
+ * npm registry's own abbreviated documents have, and the one the rest of the
17
+ * resolver is written against.
18
+ *
19
+ * A packument with no versions keeps whatever `time` it has — there is
20
+ * nothing for the map to be incomplete about — and a version whose entry is
21
+ * an empty string counts as absent.
22
+ */
23
+ export function dropIncompletePublishTimes(meta) {
24
+ if (meta.time == null)
25
+ return;
26
+ for (const version in meta.versions) {
27
+ if (!Object.hasOwn(meta.versions, version))
28
+ continue;
29
+ if (!Object.hasOwn(meta.time, version) || !meta.time[version]) {
30
+ delete meta.time;
31
+ return;
32
+ }
33
+ }
34
+ }
35
+ //# sourceMappingURL=publishTimes.js.map
@@ -4,6 +4,20 @@ type TrustEvidence = 'provenance' | 'trustedPublisher' | 'stagedPublish';
4
4
  export declare function failIfTrustDowngraded(meta: PackageMeta, version: string, opts?: {
5
5
  trustPolicyExclude?: PackageVersionPolicy;
6
6
  trustPolicyIgnoreAfter?: number;
7
+ /**
8
+ * The `minimumReleaseAgeIgnoreMissingTime` opt-in, which declares that
9
+ * the registry cannot date its releases. The downgrade check orders
10
+ * history by publish date, so a packument with no `time` map leaves it
11
+ * nothing to order and the check is skipped with a warning rather than
12
+ * aborting the install.
13
+ *
14
+ * Scoped to the whole map being absent, which `dropIncompletePublishTimes`
15
+ * makes the only shape a registry that dates some of its versions can
16
+ * reach here in. A packument that dates every version it lists is instead
17
+ * saying it does not have this one, so that shape keeps failing closed
18
+ * however this flag is set.
19
+ */
20
+ ignoreMissingTimeField?: boolean;
7
21
  }): void;
8
22
  export declare function getTrustEvidence(manifest: PackageInRegistry): TrustEvidence | undefined;
9
23
  export {};
@@ -1,5 +1,6 @@
1
1
  import { PnpmError } from '@pnpm/error';
2
2
  import semver from 'semver';
3
+ import { warnMissingTimeFieldOnce } from './pickPackage.js';
3
4
  import { assertMetaHasTime } from './pickPackageFromMeta.js';
4
5
  const TRUST_RANK = {
5
6
  stagedPublish: 3,
@@ -16,6 +17,10 @@ export function failIfTrustDowngraded(meta, version, opts) {
16
17
  return;
17
18
  }
18
19
  }
20
+ if (meta.time == null && opts?.ignoreMissingTimeField) {
21
+ warnMissingTimeFieldOnce(meta.name, 'trustPolicy');
22
+ return;
23
+ }
19
24
  assertMetaHasTime(meta);
20
25
  const versionPublishedAt = meta.time[version];
21
26
  if (!versionPublishedAt) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/resolving.npm-resolver",
3
- "version": "1103.2.0",
3
+ "version": "1104.0.0",
4
4
  "description": "Resolver for npm-hosted packages",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -29,24 +29,24 @@
29
29
  "!*.map"
30
30
  ],
31
31
  "dependencies": {
32
- "@pnpm/config.normalize-registries": "1100.1.0",
33
- "@pnpm/config.pick-registry-for-package": "1100.1.0",
34
- "@pnpm/config.version-policy": "1100.1.12",
35
- "@pnpm/constants": "1101.0.0",
36
- "@pnpm/core-loggers": "1100.3.2",
32
+ "@pnpm/config.normalize-registries": "1101.0.0",
33
+ "@pnpm/config.pick-registry-for-package": "1101.0.0",
34
+ "@pnpm/config.version-policy": "1100.2.1",
35
+ "@pnpm/constants": "1102.0.0",
36
+ "@pnpm/core-loggers": "1100.3.3",
37
37
  "@pnpm/crypto.hash": "1100.0.2",
38
- "@pnpm/deps.path": "1100.1.0",
39
- "@pnpm/error": "1100.1.1",
38
+ "@pnpm/deps.path": "1101.0.0",
39
+ "@pnpm/error": "1100.1.3",
40
40
  "@pnpm/fetching.types": "1100.0.3",
41
41
  "@pnpm/fs.graceful-fs": "1100.1.1",
42
- "@pnpm/pkg-manifest.utils": "1100.3.1",
43
- "@pnpm/resolving.jsr-specifier-parser": "1100.0.4",
44
- "@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.16",
45
- "@pnpm/resolving.registry.types": "1100.1.9",
46
- "@pnpm/resolving.resolver-base": "1101.1.0",
47
- "@pnpm/store.cafs": "1100.1.18",
48
- "@pnpm/store.index": "1100.2.3",
49
- "@pnpm/types": "1101.9.0",
42
+ "@pnpm/pkg-manifest.utils": "1100.4.1",
43
+ "@pnpm/resolving.jsr-specifier-parser": "1100.0.6",
44
+ "@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.17",
45
+ "@pnpm/resolving.registry.types": "1100.1.10",
46
+ "@pnpm/resolving.resolver-base": "1101.1.1",
47
+ "@pnpm/store.cafs": "1100.2.0",
48
+ "@pnpm/store.index": "1100.2.5",
49
+ "@pnpm/types": "1102.0.0",
50
50
  "@pnpm/workspace.range-resolver": "1100.0.3",
51
51
  "@pnpm/workspace.spec-parser": "1100.0.1",
52
52
  "@zkochan/retry": "^0.2.0",
@@ -59,20 +59,19 @@
59
59
  "ramda": "npm:@pnpm/ramda@0.28.1",
60
60
  "rename-overwrite": "^7.0.1",
61
61
  "semver": "^7.8.5",
62
- "semver-utils": "^1.1.4",
63
62
  "ssri": "13.0.1",
64
63
  "validate-npm-package-name": "7.0.2",
65
64
  "version-selector-type": "^3.0.0"
66
65
  },
67
66
  "peerDependencies": {
68
67
  "@pnpm/logger": "^1100.0.0",
69
- "@pnpm/worker": "^1100.2.10"
68
+ "@pnpm/worker": "^1100.3.0"
70
69
  },
71
70
  "devDependencies": {
72
71
  "@jest/globals": "30.4.1",
73
72
  "@pnpm/logger": "1100.0.0",
74
- "@pnpm/network.fetch": "1100.1.11",
75
- "@pnpm/resolving.npm-resolver": "1103.2.0",
73
+ "@pnpm/network.fetch": "1100.1.13",
74
+ "@pnpm/resolving.npm-resolver": "1104.0.0",
76
75
  "@pnpm/test-fixtures": "1100.0.1",
77
76
  "@pnpm/testing.mock-agent": "1101.0.7",
78
77
  "@types/normalize-path": "^3.0.2",
@@ -1,2 +0,0 @@
1
- import type { RangeSpecStyle } from '@pnpm/types';
2
- export declare function inferRangeSpecStyle(spec: string): RangeSpecStyle | undefined;
@@ -1,39 +0,0 @@
1
- import { parseRange } from 'semver-utils';
2
- export function inferRangeSpecStyle(spec) {
3
- // A catalog reference carries no version pinning of its own; the pinning is
4
- // defined by the catalog entry it points to. Bail out so a catalog name that
5
- // happens to look like a version (e.g. "catalog:express4-21") isn't misread
6
- // as a pinned version.
7
- if (spec.startsWith('catalog:'))
8
- return undefined;
9
- const colonIndex = spec.indexOf(':');
10
- if (colonIndex !== -1) {
11
- spec = spec.substring(colonIndex + 1);
12
- }
13
- const index = spec.lastIndexOf('@');
14
- if (index !== -1) {
15
- spec = spec.slice(index + 1);
16
- }
17
- if (spec === '*')
18
- return 'none';
19
- const parsedRange = parseRange(spec);
20
- if (parsedRange.length !== 1)
21
- return undefined;
22
- const versionObject = parsedRange[0];
23
- switch (versionObject.operator) {
24
- case '~': return 'minor';
25
- case '^': return 'major';
26
- // A bare '=' before a full version is an explicit exact pin; a partial
27
- // '=' pins the same way the plain version it prefixes does.
28
- case '=':
29
- case undefined:
30
- if (versionObject.patch)
31
- return versionObject.operator === '=' ? 'exact' : 'patch';
32
- if (versionObject.minor)
33
- return 'minor';
34
- if (versionObject.major)
35
- return 'major';
36
- }
37
- return undefined;
38
- }
39
- //# sourceMappingURL=inferRangeSpecStyle.js.map