@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.
- package/CHANGELOG.md +82 -0
- package/lib/createNpmResolutionVerifier.d.ts +13 -10
- package/lib/createNpmResolutionVerifier.js +41 -23
- package/lib/fetch.js +5 -3
- package/lib/index.d.ts +20 -8
- package/lib/index.js +63 -25
- package/lib/parseBareSpecifier.d.ts +1 -1
- package/lib/parseBareSpecifier.js +1 -1
- package/lib/pickPackage.d.ts +20 -1
- package/lib/pickPackage.js +109 -20
- package/lib/pickPackageFromMeta.d.ts +7 -1
- package/lib/pickPackageFromMeta.js +82 -3
- package/lib/publishTimes.d.ts +24 -0
- package/lib/publishTimes.js +35 -0
- package/lib/trustChecks.d.ts +14 -0
- package/lib/trustChecks.js +5 -0
- package/package.json +19 -20
- package/lib/inferRangeSpecStyle.d.ts +0 -2
- package/lib/inferRangeSpecStyle.js +0 -39
package/lib/pickPackage.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/lib/pickPackage.js
CHANGED
|
@@ -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
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
470
|
-
// `pickMatchingVersionFinal`
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
588
|
-
|
|
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(
|
|
597
|
-
globalWarn(`The metadata of ${pkgName} is missing the "time" field; skipping the
|
|
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
|
|
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 } =
|
|
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
|
package/lib/trustChecks.d.ts
CHANGED
|
@@ -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 {};
|
package/lib/trustChecks.js
CHANGED
|
@@ -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": "
|
|
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": "
|
|
33
|
-
"@pnpm/config.pick-registry-for-package": "
|
|
34
|
-
"@pnpm/config.version-policy": "1100.1
|
|
35
|
-
"@pnpm/constants": "
|
|
36
|
-
"@pnpm/core-loggers": "1100.3.
|
|
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": "
|
|
39
|
-
"@pnpm/error": "1100.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.
|
|
43
|
-
"@pnpm/resolving.jsr-specifier-parser": "1100.0.
|
|
44
|
-
"@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.
|
|
45
|
-
"@pnpm/resolving.registry.types": "1100.1.
|
|
46
|
-
"@pnpm/resolving.resolver-base": "1101.1.
|
|
47
|
-
"@pnpm/store.cafs": "1100.
|
|
48
|
-
"@pnpm/store.index": "1100.2.
|
|
49
|
-
"@pnpm/types": "
|
|
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.
|
|
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.
|
|
75
|
-
"@pnpm/resolving.npm-resolver": "
|
|
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,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
|