@pnpm/resolving.npm-resolver 1103.0.0 → 1103.2.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 CHANGED
@@ -1,5 +1,97 @@
1
1
  # @pnpm/npm-resolver
2
2
 
3
+ ## 1103.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Lockfile verification now honors offline mode by using cached registry metadata instead of reaching the registry. When the required metadata is not available locally, verification reports the same `ERR_PNPM_NO_OFFLINE_META` condition used by offline resolution.
8
+
9
+ ### Patch Changes
10
+
11
+ - The held-back-update warning printed by `pnpm update` no longer fires when `minimumReleaseAge` is the actual reason a newer version was not picked. The warning's baseline now applies the same maturity cutoff as the pick itself, so it no longer wrongly attributes the hold-back to "your manifests and already installed dependencies" or recommends an override that would defeat the age gate. See pnpm/pnpm#13071.
12
+
13
+ - Updated dependencies:
14
+ - @pnpm/resolving.registry.pkg-metadata-filter@1100.0.16
15
+
16
+ ## 1103.1.0
17
+
18
+ ### Minor Changes
19
+
20
+ - **Security fix.** Affects projects using `namedRegistries` on pnpm 11.1.0–11.19.x. It is **semi-breaking** for those projects — see "If you use named registries" below.
21
+
22
+ The lockfile recorded no marker for which registry a package came from. Packages were keyed by `name@version` alone, and entry lookup went through `refToRelative(ref, name)`, so a dependency you declared against one registry could be satisfied by an entry that was actually resolved from another. When two registries served the same name and version, both collapsed onto a single `packages:` entry and whichever resolved first decided the tarball every consumer got.
23
+
24
+ That is a package-substitution risk: a package you expect from your private registry could be installed from a different registry that publishes the same name and version, and the lockfile recorded nothing that would let you tell.
25
+
26
+ Packages resolved from a named registry are now recorded under registry-qualified keys (`<name>@<registryName>:<version>`, e.g. `foo@work:1.0.0`), so each registry gets its own entry and the lockfile pins which one a dependency came from.
27
+
28
+ The lockfile format version is unchanged. Registry-qualified keys appear only for packages resolved from a named registry, so a project that does not use `namedRegistries` sees no difference, and older pnpm versions keep reading the file.
29
+
30
+ ### If you use named registries
31
+
32
+ Your next non-frozen install re-keys those entries, which shows up as a lockfile diff. Commit it — that diff is the fix being applied. Review it: an entry that moves to a registry you did not expect is worth investigating.
33
+
34
+ Everyone working on the project should be on this version or newer before you do. An older pnpm reads the re-keyed lockfile fine — frozen installs are unaffected — but it does not produce registry-qualified keys itself, so any install that updates the lockfile writes those entries back to the old shape, and the next install on a current pnpm re-qualifies them. The result is a lockfile that flips back and forth, and while it is in the old shape the project is exposed again. Because the lockfile format version is deliberately unchanged, pnpm cannot detect this and warn you about it.
35
+
36
+ There is no setting to keep the old behavior: the old shape is the vulnerability.
37
+
38
+ Tarball URLs that follow the standard registry layout are no longer written to the lockfile for named-registry packages; they are recomputed from the `namedRegistries` setting on demand.
39
+
40
+ To use named registries, map your aliases in `pnpm-workspace.yaml`:
41
+
42
+ ```yaml
43
+ namedRegistries:
44
+ work: https://npm.enterprise.example.com/
45
+ ```
46
+
47
+ ### New built-in `npmjs:` alias
48
+
49
+ `npmjs:` now resolves to `https://registry.npmjs.org/` with no configuration, alongside the existing `gh:` alias for GitHub Packages. It pins a dependency to the public registry even when `registry` points elsewhere, such as an internal proxy:
50
+
51
+ ```json
52
+ { "dependencies": { "left-pad": "npmjs:^1.3.0" } }
53
+ ```
54
+
55
+ `npm:` cannot do this — it is the alias protocol (`npm:<name>@<range>`) and resolves through whatever `registry` points at.
56
+
57
+ **If you mirror or proxy npmjs, point the alias at your mirror:**
58
+
59
+ ```yaml
60
+ namedRegistries:
61
+ npmjs: https://npm.internal.example.com/
62
+ ```
63
+
64
+ Built-in registry URLs are also the prefixes a lockfile's recorded tarball URL is matched against when pnpm verifies a package. Without the override, an entry whose tarball URL is on `registry.npmjs.org` is verified against the public registry rather than your mirror. This only affects lockfiles that record such URLs — a canonical URL for your configured registry is omitted from the lockfile and unaffected — and only when a tarball-URL, `minimumReleaseAge`, or `trustPolicy` check runs. Overriding the alias is the same escape hatch GHES users already have for `gh`.
65
+
66
+ Every alias the lockfile references must stay in `namedRegistries`: reading an entry whose alias is gone fails with `ERR_PNPM_MISSING_NAMED_REGISTRY` rather than silently falling back to the default registry, since that would fetch a different package. Renaming an alias re-resolves the packages that used it.
67
+
68
+ Named registry aliases that shadow a reserved dependency specifier prefix (`file`, `link`, `workspace`, `runtime`, `npm`, `jsr`, ...) are now rejected with `ERR_PNPM_RESERVED_NAMED_REGISTRY_NAME` instead of being silently shadowed by the corresponding resolver.
69
+
70
+ `pnpm licenses` and `pnpm sbom` now keep the two artifacts apart as well: license records carry the registry alias, and SBOM components carry the purl `repository_url` qualifier.
71
+
72
+ ### Patch Changes
73
+
74
+ - Fixed the order in which pnpm matches a lockfile's recorded tarball URL against known registry URLs. Two registry URLs of equal length were previously ordered arbitrarily, so which one a tarball URL matched could differ between runs.
75
+
76
+ - Dependency resolution is faster: package metadata is now filtered once per packument instead of once per dependency edge when `minimumReleaseAge` is active, and parsed semver versions and ranges are reused instead of re-parsed on every comparison.
77
+
78
+ - Updated dependencies:
79
+ - @pnpm/config.normalize-registries@1100.1.0
80
+ - @pnpm/config.pick-registry-for-package@1100.1.0
81
+ - @pnpm/config.version-policy@1100.1.12
82
+ - @pnpm/constants@1101.0.0
83
+ - @pnpm/core-loggers@1100.3.2
84
+ - @pnpm/deps.path@1100.1.0
85
+ - @pnpm/error@1100.1.1
86
+ - @pnpm/pkg-manifest.utils@1100.3.1
87
+ - @pnpm/resolving.jsr-specifier-parser@1100.0.4
88
+ - @pnpm/resolving.registry.pkg-metadata-filter@1100.0.15
89
+ - @pnpm/resolving.registry.types@1100.1.9
90
+ - @pnpm/resolving.resolver-base@1101.1.0
91
+ - @pnpm/store.cafs@1100.1.18
92
+ - @pnpm/store.index@1100.2.3
93
+ - @pnpm/types@1101.9.0
94
+
3
95
  ## 1103.0.0
4
96
 
5
97
  ### Major Changes
package/lib/clearMeta.js CHANGED
@@ -1,4 +1,3 @@
1
- import { pick } from 'ramda';
2
1
  // The list taken from https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
3
2
  // with the addition of 'libc'
4
3
  const ABBREVIATED_VERSION_FIELDS = [
@@ -48,7 +47,7 @@ export function clearMeta(pkg) {
48
47
  // prototype of the map (js/prototype-polluting-assignment).
49
48
  const versions = Object.create(null);
50
49
  for (const [version, info] of Object.entries(pkg.versions ?? {})) {
51
- versions[version] = pick(ABBREVIATED_VERSION_FIELDS, info);
50
+ versions[version] = pickAbbreviatedVersionFields(info);
52
51
  }
53
52
  const condensed = {
54
53
  name: pkg.name,
@@ -64,6 +63,20 @@ export function clearMeta(pkg) {
64
63
  condensedPackuments.set(condensed, condensed);
65
64
  return condensed;
66
65
  }
66
+ // Hand-rolled rather than delegated to a generic field picker because it runs
67
+ // for every version of every packument an install parses. Testing the value
68
+ // for `undefined` is equivalent to testing for the key's presence: version
69
+ // objects come from JSON, which cannot encode undefined.
70
+ function pickAbbreviatedVersionFields(info) {
71
+ const picked = {};
72
+ for (const field of ABBREVIATED_VERSION_FIELDS) {
73
+ const value = info[field];
74
+ if (value !== undefined) {
75
+ picked[field] = value;
76
+ }
77
+ }
78
+ return picked;
79
+ }
67
80
  /**
68
81
  * Whether a resolver keeps full packuments rather than condensing them with
69
82
  * {@link clearMeta}: only `fullMetadata` without `filterMetadata`, whose
@@ -58,6 +58,11 @@ export interface CreateNpmResolutionVerifierOptions {
58
58
  fetchOpts: FetchMetadataFromFromRegistryOptions;
59
59
  getAuthHeaderValueByURI: GetAuthHeader;
60
60
  cacheDir?: FetchFullMetadataCachedOptions['cacheDir'];
61
+ /**
62
+ * When true, verifier metadata lookups must use the local mirror
63
+ * only and never reach the registry or attestation endpoint.
64
+ */
65
+ offline?: boolean;
61
66
  /**
62
67
  * Per-install LRU shared with the npm resolver's `pickPackage`
63
68
  * (`{ get, set }` over `PackageMeta`). When provided, the verifier
@@ -1,4 +1,6 @@
1
- import { pickRegistryForPackage } from '@pnpm/config.pick-registry-for-package';
1
+ import { createHash } from 'node:crypto';
2
+ import { normalizeNamedRegistries } from '@pnpm/config.normalize-registries';
3
+ import { namedRegistryTarballPrefixes, pickRegistryForPackage } from '@pnpm/config.pick-registry-for-package';
2
4
  import { createPackageVersionPolicy } from '@pnpm/config.version-policy';
3
5
  import { FULL_META_DIR } from '@pnpm/constants';
4
6
  import { PnpmError } from '@pnpm/error';
@@ -7,10 +9,9 @@ import semver from 'semver';
7
9
  import { fetchAttestationPublishedAt } from './fetchAttestationPublishedAt.js';
8
10
  import { fetchAbbreviatedMetadataCached, fetchFullMetadataCached, } from './fetchFullMetadataCached.js';
9
11
  import { normalizeRegistryUrl } from './normalizeRegistryUrl.js';
10
- import { BUILTIN_NAMED_REGISTRIES } from './parseBareSpecifier.js';
11
12
  import { getPkgMetaCacheKey, getPkgMirrorPath, loadMeta, warnMissingTimeFieldOnce } from './pickPackage.js';
12
13
  import { failIfTrustDowngraded } from './trustChecks.js';
13
- import { MINIMUM_RELEASE_AGE_VIOLATION_CODE, MISSING_TARBALL_INTEGRITY_VIOLATION_CODE, TARBALL_URL_MISMATCH_VIOLATION_CODE, TRUST_DOWNGRADE_VIOLATION_CODE, } from './violationCodes.js';
14
+ import { MINIMUM_RELEASE_AGE_VIOLATION_CODE, MISSING_NAMED_REGISTRY_VIOLATION_CODE, MISSING_TARBALL_INTEGRITY_VIOLATION_CODE, TARBALL_URL_MISMATCH_VIOLATION_CODE, TRUST_DOWNGRADE_VIOLATION_CODE, } from './violationCodes.js';
14
15
  /**
15
16
  * Returns a `ResolutionVerifier` for npm-registry-resolved lockfile
16
17
  * entries. It always binds each entry's recorded tarball URL to the
@@ -38,30 +39,8 @@ export function createNpmResolutionVerifier(opts) {
38
39
  const trustExcludePolicy = opts.trustPolicyExclude?.length
39
40
  ? createExcludePolicy(opts.trustPolicyExclude, 'trustPolicyExclude')
40
41
  : undefined;
41
- // Pre-normalize named-registry URLs and sort by length so two registries
42
- // that share a hostname but differ by path (e.g. `https://npm/team-a/` vs
43
- // `https://npm/team-b/`) route to the longest matching prefix — matching
44
- // only `origin` would silently send lookups to the wrong one. Built-in
45
- // aliases (`gh:` → npm.pkg.github.com, etc.) are merged in alongside the
46
- // user-defined ones so the verifier recognizes the same set of named
47
- // registries the resolver does; otherwise a package resolved via `gh:`
48
- // would land in the lockfile with a tarball URL the verifier can't route.
49
- const namedRegistryPrefixes = Object.values({
50
- ...BUILTIN_NAMED_REGISTRIES,
51
- ...(opts.namedRegistries ?? {}),
52
- })
53
- .map((url) => {
54
- const parsed = tryParseUrl(url);
55
- if (!parsed)
56
- return null;
57
- // Ensure trailing slash so prefix matching against tarball URLs (which
58
- // always include the package path under the registry root) does not
59
- // accidentally match a sibling registry whose URL shares a prefix string.
60
- const pathname = parsed.pathname.endsWith('/') ? parsed.pathname : `${parsed.pathname}/`;
61
- return `${parsed.origin}${pathname}`;
62
- })
63
- .filter((value) => value != null)
64
- .sort((a, b) => b.length - a.length);
42
+ const mergedNamedRegistries = normalizeNamedRegistries(opts.namedRegistries);
43
+ const namedRegistryPrefixes = namedRegistryTarballPrefixes(mergedNamedRegistries);
65
44
  // Per-install dedup of every network/disk fetch the verifier issues.
66
45
  // The maturity check uses the layered `fetchPublishedAt` lookup; the
67
46
  // trust check uses an attestation fast-path before falling back to
@@ -74,6 +53,7 @@ export function createNpmResolutionVerifier(opts) {
74
53
  fetchOpts: opts.fetchOpts,
75
54
  getAuthHeaderValueByURI: opts.getAuthHeaderValueByURI,
76
55
  cacheDir: opts.cacheDir,
56
+ offline: opts.offline === true,
77
57
  cutoffMs: cutoff,
78
58
  sharedMetaCache: opts.metaCache,
79
59
  abbreviatedMetaCache: new Map(),
@@ -85,7 +65,7 @@ export function createNpmResolutionVerifier(opts) {
85
65
  const minimumReleaseAge = opts.minimumReleaseAge ?? 0;
86
66
  const trustPolicy = opts.trustPolicy;
87
67
  const trustPolicyIgnoreAfter = opts.trustPolicyIgnoreAfter;
88
- const verify = async (resolution, { name, version, nonSemverVersion }) => {
68
+ const verify = async (resolution, { name, version, nonSemverVersion, registryName }) => {
89
69
  if (!isRegistryTarballResolution(resolution))
90
70
  return { ok: true };
91
71
  // Network-free structural checks must run before registry metadata shortcuts.
@@ -120,7 +100,26 @@ export function createNpmResolutionVerifier(opts) {
120
100
  };
121
101
  }
122
102
  const tarballUrl = typeof rawTarball === 'string' ? rawTarball : undefined;
123
- const registry = pickRegistryForVersion(opts.registries, namedRegistryPrefixes, name, tarballUrl);
103
+ let registry;
104
+ if (registryName != null) {
105
+ // Registry-qualified entries name their registry in the dep path, so
106
+ // routing does not depend on a recorded tarball URL (canonical URLs
107
+ // are omitted from the lockfile in the 12.0 format).
108
+ const namedRegistry = mergedNamedRegistries[registryName];
109
+ if (!namedRegistry) {
110
+ // Fail closed: without the registry URL, none of the metadata-backed
111
+ // checks below can vouch for this entry.
112
+ return {
113
+ ok: false,
114
+ code: MISSING_NAMED_REGISTRY_VIOLATION_CODE,
115
+ reason: `was resolved from the named registry '${registryName}:', which is not present in the namedRegistries setting`,
116
+ };
117
+ }
118
+ registry = namedRegistry;
119
+ }
120
+ else {
121
+ registry = pickRegistryForVersion(opts.registries, namedRegistryPrefixes, name, tarballUrl);
122
+ }
124
123
  // A registry entry that pins an explicit tarball URL must point at the
125
124
  // artifact the registry's own metadata lists. Otherwise a trusted
126
125
  // `name@version` could front bytes from an attacker-chosen URL (with a
@@ -164,6 +163,10 @@ export function createNpmResolutionVerifier(opts) {
164
163
  // stays trusted after its exclude entry has been pulled.
165
164
  const sortedMinAgeExcludes = [...new Set(opts.minimumReleaseAgeExclude ?? [])].sort();
166
165
  const sortedTrustExcludes = [...new Set(opts.trustPolicyExclude ?? [])].sort();
166
+ const sortedNamedRegistries = Object.fromEntries(Object.entries(mergedNamedRegistries).sort(([aliasA], [aliasB]) => aliasA.localeCompare(aliasB)));
167
+ const namedRegistriesRouting = createHash('sha256')
168
+ .update(JSON.stringify(sortedNamedRegistries))
169
+ .digest('hex');
167
170
  return {
168
171
  verify,
169
172
  policy: {
@@ -175,6 +178,7 @@ export function createNpmResolutionVerifier(opts) {
175
178
  tarballUrlBinding: true,
176
179
  // Same cache identity rule for the missing-integrity structural check.
177
180
  integrityRequired: true,
181
+ namedRegistriesRouting,
178
182
  minimumReleaseAge,
179
183
  minimumReleaseAgeExclude: sortedMinAgeExcludes,
180
184
  trustPolicy: trustPolicy ?? null,
@@ -190,6 +194,8 @@ export function createNpmResolutionVerifier(opts) {
190
194
  // without the flag cannot prove they rejected unverifiable tarballs.
191
195
  if (cached.integrityRequired !== true)
192
196
  return false;
197
+ if (cached.namedRegistriesRouting !== namedRegistriesRouting)
198
+ return false;
193
199
  // Maturity: a previously cached run under a larger cutoff
194
200
  // (stricter window) is trustworthy under a smaller current one —
195
201
  // its set of accepted versions is a subset of today's. The
@@ -386,6 +392,7 @@ function fetchFullMetaForTrust(context, registry, name) {
386
392
  registry,
387
393
  authHeaderValue: context.getAuthHeaderValueByURI(registry, { pkgName: name }),
388
394
  cacheDir: context.cacheDir,
395
+ offline: context.offline,
389
396
  }).then(projectTrustMeta);
390
397
  }
391
398
  context.fullMetaForTrustCache.set(cacheKey, cachedPromise);
@@ -480,12 +487,14 @@ async function resolvePublishedAt(context, registry, name, version) {
480
487
  const localTime = await readLocalMetaTime(context, registry, name);
481
488
  if (localTime?.[version])
482
489
  return localTime[version];
483
- const attestationTime = await fetchAttestationPublishedAt(context.fetchOpts, name, version, {
484
- registry,
485
- authHeaderValue: context.getAuthHeaderValueByURI(registry, { pkgName: name }),
486
- });
487
- if (attestationTime != null)
488
- return attestationTime;
490
+ if (!context.offline) {
491
+ const attestationTime = await fetchAttestationPublishedAt(context.fetchOpts, name, version, {
492
+ registry,
493
+ authHeaderValue: context.getAuthHeaderValueByURI(registry, { pkgName: name }),
494
+ });
495
+ if (attestationTime != null)
496
+ return attestationTime;
497
+ }
489
498
  const fullMetaTime = await fetchFullMetaTime(context, registry, name);
490
499
  return fullMetaTime?.[version];
491
500
  }
@@ -550,6 +559,7 @@ function fetchAbbreviatedMeta(context, registry, name) {
550
559
  registry,
551
560
  authHeaderValue: context.getAuthHeaderValueByURI(registry, { pkgName: name }),
552
561
  cacheDir: context.cacheDir,
562
+ offline: context.offline,
553
563
  }).then((meta) => ({ meta: projectAbbreviatedMeta(meta) }), (error) => ({ error }));
554
564
  }
555
565
  context.abbreviatedMetaCache.set(cacheKey, cachedPromise);
@@ -641,6 +651,7 @@ function fetchFullMetaTime(context, registry, name) {
641
651
  registry,
642
652
  authHeaderValue: context.getAuthHeaderValueByURI(registry, { pkgName: name }),
643
653
  cacheDir: context.cacheDir,
654
+ offline: context.offline,
644
655
  }).then((meta) => meta.time);
645
656
  context.fullMetaCache.set(cacheKey, cachedPromise);
646
657
  }
@@ -10,6 +10,10 @@ export interface FetchMetadataCachedOptions {
10
10
  * back. Omit to disable caching — every call re-fetches.
11
11
  */
12
12
  cacheDir?: string;
13
+ /**
14
+ * Use only pnpm's on-disk metadata mirror and never reach the registry.
15
+ */
16
+ offline?: boolean;
13
17
  }
14
18
  export type FetchFullMetadataCachedOptions = FetchMetadataCachedOptions;
15
19
  /**
@@ -1,4 +1,5 @@
1
1
  import { ABBREVIATED_META_DIR, FULL_META_DIR } from '@pnpm/constants';
2
+ import { PnpmError } from '@pnpm/error';
2
3
  import { fetchMetadataFromFromRegistry, } from './fetch.js';
3
4
  import { getPkgMirrorPath, loadMeta, loadMetaHeaders, prepareJsonForDisk, saveMeta } from './pickPackage.js';
4
5
  /**
@@ -28,6 +29,14 @@ async function fetchMetadataCached(fetchOpts, pkgName, opts) {
28
29
  const pkgMirror = opts.cacheDir != null
29
30
  ? getPkgMirrorPath(opts.cacheDir, opts.metaDir, opts.registry, pkgName)
30
31
  : null;
32
+ if (opts.offline === true) {
33
+ if (pkgMirror != null) {
34
+ const cached = await loadMeta(pkgMirror);
35
+ if (cached != null)
36
+ return cached;
37
+ }
38
+ throw new PnpmError('NO_OFFLINE_META', `Failed to resolve ${pkgName} in package mirror ${pkgMirror ?? ''}`);
39
+ }
31
40
  const cacheHeaders = pkgMirror != null ? await loadMetaHeaders(pkgMirror) : null;
32
41
  const conditional = await fetchMetadataFromFromRegistry(fetchOpts, pkgName, {
33
42
  registry: opts.registry,
package/lib/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { pickRegistryForPackage } from '@pnpm/config.pick-registry-for-package';
3
+ import { isWellFormedRegistryName, RESERVED_VERSION_PREFIXES } from '@pnpm/deps.path';
3
4
  import { PnpmError } from '@pnpm/error';
4
5
  import { globalWarn } from '@pnpm/logger';
5
6
  import { rangeSpecGranularity, versionWithRangeSpecStyle } from '@pnpm/pkg-manifest.utils';
@@ -20,7 +21,7 @@ import { memoizeFetchMetadata } from './memoizeFetchMetadata.js';
20
21
  import { normalizeRegistryUrl } from './normalizeRegistryUrl.js';
21
22
  import { BUILTIN_NAMED_REGISTRIES, parseBareSpecifier, parseJsrSpecifierToRegistryPackageSpec, parseNamedRegistrySpecifierToRegistryPackageSpec, } from './parseBareSpecifier.js';
22
23
  import { pickPackage, } from './pickPackage.js';
23
- import { pickPackageFromMeta, pickVersionByVersionRange } from './pickPackageFromMeta.js';
24
+ import { applyPublishedByPolicy, pickPackageFromMeta, pickVersionByVersionRange } from './pickPackageFromMeta.js';
24
25
  import { failIfTrustDowngraded } from './trustChecks.js';
25
26
  import { MINIMUM_RELEASE_AGE_VIOLATION_CODE } from './violationCodes.js';
26
27
  import { workspacePrefToNpm } from './workspacePrefToNpm.js';
@@ -211,7 +212,10 @@ function stripLockfileVersionPins(selectors) {
211
212
  * The baseline for "held back" is the pick with only the non-pin selectors
212
213
  * applied — `range`/`tag` selectors such as the `pnpm audit --fix`
213
214
  * vulnerability penalties steer the baseline too, so the warning never
214
- * recommends a version those selectors avoid.
215
+ * recommends a version those selectors avoid. The baseline also honors the
216
+ * `publishedBy` maturity cutoff the actual pick applied: a version blocked
217
+ * by `minimumReleaseAge` is not an update the manifests held back, and
218
+ * recommending an override for it would defeat the age gate.
215
219
  *
216
220
  * The recommended override is scoped to the declared range being resolved
217
221
  * (`name@<range>`), so applying it can never violate any consumer's range:
@@ -232,8 +236,14 @@ function warnOnceOnHeldBackUpdate(ctx, opts, spec, meta, pickedVersion) {
232
236
  nonPinSelectors ??= Object.create(null);
233
237
  nonPinSelectors[selector] = value;
234
238
  }
239
+ // `needsFullMetadata` is not this caller's problem: the pick already
240
+ // succeeded on this metadata, which for an abbreviated packument means
241
+ // every version cleared the cutoff, so `meta` is the filtered view.
242
+ const baselineMeta = opts.publishedBy != null
243
+ ? applyPublishedByPolicy(meta, opts.publishedBy, opts.publishedByExclude).meta
244
+ : meta;
235
245
  const preferred = pickVersionByVersionRange({
236
- meta,
246
+ meta: baselineMeta,
237
247
  versionRange: spec.fetchSpec,
238
248
  preferredVersionSelectors: nonPinSelectors,
239
249
  });
@@ -539,6 +549,11 @@ function mergeNamedRegistries(userDefined) {
539
549
  if (!userDefined)
540
550
  return merged;
541
551
  for (const [alias, url] of Object.entries(userDefined)) {
552
+ if (RESERVED_VERSION_PREFIXES.has(alias) || !isWellFormedRegistryName(alias)) {
553
+ throw new PnpmError('RESERVED_NAMED_REGISTRY_NAME', RESERVED_VERSION_PREFIXES.has(alias)
554
+ ? `'${alias}' cannot be used as a named registry alias: it is a reserved dependency specifier prefix.`
555
+ : `'${alias}' cannot be used as a named registry alias: aliases must start with a letter and contain only letters, digits, ".", "_", and "-".`, { hint: 'Rename the entry in the namedRegistries setting.' });
556
+ }
542
557
  if (typeof url !== 'string' || !isValidHttpUrl(url)) {
543
558
  throw new PnpmError('INVALID_NAMED_REGISTRY_URL', `The named registry alias '${alias}' is mapped to '${String(url)}', which is not a valid http(s) URL.`, { hint: 'Provide a URL that starts with http:// or https://, e.g. https://npm.pkg.example.com/' });
544
559
  }
@@ -574,6 +589,11 @@ async function resolveFromNamedRegistry(ctx, wantedDependency, opts) {
574
589
  const picked = await pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, registry);
575
590
  return {
576
591
  ...picked,
592
+ // Qualifying the id with the registry alias is what keeps the same
593
+ // name@version resolved from two registries distinct in the lockfile.
594
+ // Without it they collapse onto one entry and whichever resolved first
595
+ // decides the tarball both consumers get.
596
+ id: `${picked.manifest.name}@${spec.registryName}:${picked.manifest.version}`,
577
597
  normalizedBareSpecifier: opts.calcSpecifier
578
598
  ? calcPrefixedSpecifier(`${spec.registryName}:`, spec.name, wantedDependency, picked.manifest.version, opts.rangeSpecStyle)
579
599
  : undefined,
@@ -9,7 +9,7 @@ export interface JsrRegistryPackageSpec extends RegistryPackageSpec {
9
9
  jsrPkgName: string;
10
10
  }
11
11
  export declare function parseJsrSpecifierToRegistryPackageSpec(rawSpecifier: string, alias: string | undefined, defaultTag: string): JsrRegistryPackageSpec | null;
12
- export declare const BUILTIN_NAMED_REGISTRIES: Readonly<Record<string, string>>;
12
+ export { BUILTIN_NAMED_REGISTRIES } from '@pnpm/constants';
13
13
  export interface NamedRegistryPackageSpec extends RegistryPackageSpec {
14
14
  registryName: string;
15
15
  }
@@ -64,9 +64,7 @@ export function parseJsrSpecifierToRegistryPackageSpec(rawSpecifier, alias, defa
64
64
  jsrPkgName: spec.jsrPkgName,
65
65
  };
66
66
  }
67
- export const BUILTIN_NAMED_REGISTRIES = Object.freeze({
68
- gh: 'https://npm.pkg.github.com/',
69
- });
67
+ export { BUILTIN_NAMED_REGISTRIES } from '@pnpm/constants';
70
68
  // Parses a named-registry specifier of the shape `<alias>:<body>` into a
71
69
  // RegistryPackageSpec. Returns `null` when the specifier does not use one of
72
70
  // the configured aliases, so the caller can fall through to other resolvers.
@@ -191,13 +191,29 @@ export async function pickPackage(ctx, spec, opts) {
191
191
  };
192
192
  let diskMeta;
193
193
  if (ctx.offline === true || ctx.preferOffline === true || opts.pickLowestVersion) {
194
- diskMeta = await limit(loadMetaCondensed);
194
+ // Concurrent offline picks of one package all miss the pre-queue cache
195
+ // check and queue behind this limiter, so the check is repeated inside
196
+ // the queue and the promotion happens before the limiter releases —
197
+ // otherwise every queued pick re-reads and re-parses the mirror. Serving
198
+ // a queued pick from the cache is equivalent to it having arrived after
199
+ // the first caller cached it: offline entries are always disk-sourced
200
+ // and maybeUpgradeAbbreviatedMetaForReleaseAge short-circuits when
201
+ // offline, so an in-memory hit returns this same meta with no network
202
+ // access.
203
+ diskMeta = await limit(async () => {
204
+ if (ctx.offline !== true)
205
+ return loadMetaCondensed();
206
+ const cached = ctx.metaCache.get(cacheKey);
207
+ if (cached != null)
208
+ return cached;
209
+ const meta = await loadMetaCondensed();
210
+ if (meta != null) {
211
+ cacheDiskLoadedMeta(ctx.metaCache, cacheKey, meta);
212
+ }
213
+ return meta;
214
+ });
195
215
  if (ctx.offline) {
196
216
  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
217
  return {
202
218
  meta: diskMeta,
203
219
  pickedPackage: pickMatchingVersionFinal(pickerOpts, spec, diskMeta),
@@ -15,6 +15,29 @@ export interface PickPackageFromMetaOptions {
15
15
  publishedByExclude?: PackageVersionPolicy;
16
16
  }
17
17
  export declare function pickPackageFromMeta(pickVersionByVersionRangeFn: PickVersionByVersionRange, { preferredVersionSelectors, publishedBy, publishedByExclude, }: PickPackageFromMetaOptions, meta: PackageMeta, spec: RegistryPackageSpec): PackageInRegistry | null;
18
+ export interface PublishedByView {
19
+ /** The metadata the cutoff leaves visible. `meta` itself when nothing is filtered out. */
20
+ meta: PackageMeta;
21
+ /**
22
+ * The cutoff could not be applied: the metadata is abbreviated, so there
23
+ * are no per-version timestamps to filter on. Whether that is fatal is the
24
+ * caller's call — the pick needs full metadata to honor the cutoff, while a
25
+ * caller reasoning about a pick that already succeeded knows the versions
26
+ * cleared the cutoff some other way.
27
+ */
28
+ needsFullMetadata: boolean;
29
+ }
30
+ /**
31
+ * Narrows `meta` to the versions the `publishedBy` cutoff admits, honoring
32
+ * `publishedByExclude`: a package the policy excludes wholesale keeps its
33
+ * unfiltered metadata, and versions the policy names explicitly stay in
34
+ * regardless of their age.
35
+ *
36
+ * Every consumer of the cutoff goes through here so they agree on what the
37
+ * policy admits — a baseline that filters differently from the pick would
38
+ * misreport why a version was chosen.
39
+ */
40
+ export declare function applyPublishedByPolicy(meta: PackageMeta, publishedBy: Date, publishedByExclude?: PackageVersionPolicy): PublishedByView;
18
41
  export declare function assertMetaHasTime(meta: PackageMeta): asserts meta is PackageMetaWithTime;
19
42
  export declare function pickLowestVersionByVersionRange({ meta, versionRange, preferredVersionSelectors }: PickVersionByVersionRangeOptions): string | null;
20
43
  export declare function pickVersionByVersionRange({ meta, versionRange, preferredVersionSelectors }: PickVersionByVersionRangeOptions): string | null;
@@ -4,28 +4,20 @@ import { filterPkgMetadataByPublishDate } from '@pnpm/resolving.registry.pkg-met
4
4
  import semver from 'semver';
5
5
  export function pickPackageFromMeta(pickVersionByVersionRangeFn, { preferredVersionSelectors, publishedBy, publishedByExclude, }, meta, spec) {
6
6
  if (publishedBy) {
7
- const excludeResult = publishedByExclude?.(meta.name) ?? false;
8
- if (excludeResult !== true) {
9
- if (meta.time != null) {
10
- // Full metadata with per-version timestamps: filter normally
7
+ const view = applyPublishedByPolicy(meta, publishedBy, publishedByExclude);
8
+ meta = view.meta;
9
+ if (view.needsFullMetadata) {
10
+ const modifiedDate = parseModifiedDate(meta.modified);
11
+ if (modifiedDate == null || modifiedDate > publishedBy) {
12
+ // The package was modified after the cutoff (or carries no usable
13
+ // `modified`), so which of its versions are mature is unknowable
14
+ // from abbreviated metadata. The error tells the caller to refetch.
11
15
  assertMetaHasTime(meta);
12
- const trustedVersions = Array.isArray(excludeResult) ? excludeResult : undefined;
13
- meta = filterPkgMetadataByPublishDate(meta, publishedBy, trustedVersions);
14
- }
15
- else {
16
- const modifiedDate = parseModifiedDate(meta.modified);
17
- if (modifiedDate == null || modifiedDate > publishedBy) {
18
- // Abbreviated metadata without per-version timestamps, and the package
19
- // was recently modified (or has no/invalid modified field). We cannot determine
20
- // which individual versions are mature enough — need full metadata.
21
- assertMetaHasTime(meta);
22
- }
23
- // else: meta.modified <= publishedBy — every version was published at or
24
- // before the cutoff (modified is an upper bound on per-version time), so
25
- // they all pass the per-version `<=` maturity filter and no filtering is
26
- // needed. Inclusive at the boundary on purpose so this branch matches the
27
- // per-version filter in `filterPkgMetadataByPublishDate`.
28
16
  }
17
+ // else: `modified` is an upper bound on every per-version timestamp, so
18
+ // `modified <= publishedBy` means they all pass the maturity filter and
19
+ // nothing would be dropped. Inclusive at the boundary on purpose, to
20
+ // match the per-version `<=` in `filterPkgMetadataByPublishDate`.
29
21
  }
30
22
  }
31
23
  if ((!meta.versions || Object.keys(meta.versions).length === 0) && !publishedBy) {
@@ -77,6 +69,29 @@ export function pickPackageFromMeta(pickVersionByVersionRangeFn, { preferredVers
77
69
  throw new PnpmError('MALFORMED_METADATA', `Received malformed metadata for "${spec.name}"`, { hint: 'This might mean that the package was unpublished from the registry', cause: err });
78
70
  }
79
71
  }
72
+ /**
73
+ * Narrows `meta` to the versions the `publishedBy` cutoff admits, honoring
74
+ * `publishedByExclude`: a package the policy excludes wholesale keeps its
75
+ * unfiltered metadata, and versions the policy names explicitly stay in
76
+ * regardless of their age.
77
+ *
78
+ * Every consumer of the cutoff goes through here so they agree on what the
79
+ * policy admits — a baseline that filters differently from the pick would
80
+ * misreport why a version was chosen.
81
+ */
82
+ export function applyPublishedByPolicy(meta, publishedBy, publishedByExclude) {
83
+ const excludeResult = publishedByExclude?.(meta.name) ?? false;
84
+ if (excludeResult === true)
85
+ return { meta, needsFullMetadata: false };
86
+ if (meta.time == null)
87
+ return { meta, needsFullMetadata: true };
88
+ assertMetaHasTime(meta);
89
+ const trustedVersions = Array.isArray(excludeResult) ? excludeResult : undefined;
90
+ return {
91
+ meta: filterPkgMetadataByPublishDate(meta, publishedBy, trustedVersions),
92
+ needsFullMetadata: false,
93
+ };
94
+ }
80
95
  export function assertMetaHasTime(meta) {
81
96
  if (meta.time == null) {
82
97
  throw new PnpmError('MISSING_TIME', `The metadata of ${meta.name} is missing the "time" field`);
@@ -90,36 +105,11 @@ function parseModifiedDate(modified) {
90
105
  return null;
91
106
  return date;
92
107
  }
93
- const semverRangeCache = new Map();
94
- // This is a performance optimization; working with string-ish semver
95
- // causes lots of allocations and repeated work, but caching the Range
96
- // and ensuring we give it a SemVer instance greatly speeds things up.
97
- function semverSatisfiesLoose(version, range) {
98
- let semverRange = semverRangeCache.get(range);
99
- if (semverRange === undefined) {
100
- try {
101
- semverRange = new semver.Range(range, true);
102
- }
103
- catch {
104
- semverRange = null;
105
- }
106
- semverRangeCache.set(range, semverRange);
107
- }
108
- if (semverRange) {
109
- try {
110
- return semverRange.test(new semver.SemVer(version, true));
111
- }
112
- catch {
113
- return false;
114
- }
115
- }
116
- return false;
117
- }
118
108
  export function pickLowestVersionByVersionRange({ meta, versionRange, preferredVersionSelectors }) {
119
109
  if (preferredVersionSelectors != null && Object.keys(preferredVersionSelectors).length > 0) {
120
110
  const prioritizedPreferredVersions = prioritizePreferredVersions(meta, versionRange, preferredVersionSelectors);
121
111
  for (const preferredVersions of prioritizedPreferredVersions) {
122
- const preferredVersion = semver.minSatisfying(preferredVersions, versionRange, true);
112
+ const preferredVersion = minSatisfyingLoose(preferredVersions, versionRange);
123
113
  if (preferredVersion) {
124
114
  return preferredVersion;
125
115
  }
@@ -128,7 +118,7 @@ export function pickLowestVersionByVersionRange({ meta, versionRange, preferredV
128
118
  if (versionRange === '*') {
129
119
  return Object.keys(meta.versions).sort(semver.compare)[0];
130
120
  }
131
- return semver.minSatisfying(Object.keys(meta.versions), versionRange, true);
121
+ return minSatisfyingLoose(Object.keys(meta.versions), versionRange);
132
122
  }
133
123
  export function pickVersionByVersionRange({ meta, versionRange, preferredVersionSelectors }) {
134
124
  const latest = meta['dist-tags'].latest;
@@ -138,7 +128,7 @@ export function pickVersionByVersionRange({ meta, versionRange, preferredVersion
138
128
  if (preferredVersions.includes(latest) && semverSatisfiesLoose(latest, versionRange)) {
139
129
  return latest;
140
130
  }
141
- const preferredVersion = semver.maxSatisfying(preferredVersions, versionRange, true);
131
+ const preferredVersion = maxSatisfyingLoose(preferredVersions, versionRange);
142
132
  if (preferredVersion) {
143
133
  return preferredVersion;
144
134
  }
@@ -150,13 +140,13 @@ export function pickVersionByVersionRange({ meta, versionRange, preferredVersion
150
140
  // E.g.: 1.0.0-beta.1. See issue: https://github.com/pnpm/pnpm/issues/865
151
141
  return latest;
152
142
  }
153
- const maxVersion = semver.maxSatisfying(versions, versionRange, true);
143
+ const maxVersion = maxSatisfyingLoose(versions, versionRange);
154
144
  // if the selected version is deprecated, try to find a non-deprecated one that satisfies the range
155
145
  if (maxVersion && meta.versions[maxVersion].deprecated && versions.length > 1) {
156
146
  const nonDeprecatedVersions = versions.map((version) => meta.versions[version])
157
147
  .filter((versionMeta) => !versionMeta.deprecated)
158
148
  .map((versionMeta) => versionMeta.version);
159
- const maxNonDeprecatedVersion = semver.maxSatisfying(nonDeprecatedVersions, versionRange, true);
149
+ const maxNonDeprecatedVersion = maxSatisfyingLoose(nonDeprecatedVersions, versionRange);
160
150
  if (maxNonDeprecatedVersion)
161
151
  return maxNonDeprecatedVersion;
162
152
  }
@@ -224,4 +214,76 @@ class PreferredVersionsPrioritizer {
224
214
  .map((weight) => versionsByWeight[parseInt(weight, 10)]);
225
215
  }
226
216
  }
217
+ function semverSatisfiesLoose(version, range) {
218
+ const semverRange = parseRangeLoose(range);
219
+ if (semverRange == null)
220
+ return false;
221
+ const parsedVersion = parseSemverLoose(version);
222
+ return parsedVersion != null && semverRange.test(parsedVersion);
223
+ }
224
+ // semver's own maxSatisfying/minSatisfying re-parse the range and every
225
+ // version string on each call, which dominates resolution time on large
226
+ // packuments; these reuse the parse caches instead.
227
+ function maxSatisfyingLoose(versions, range) {
228
+ return findSatisfyingLoose(versions, range, (candidate, best) => candidate.compare(best) > 0);
229
+ }
230
+ function minSatisfyingLoose(versions, range) {
231
+ return findSatisfyingLoose(versions, range, (candidate, best) => candidate.compare(best) < 0);
232
+ }
233
+ function findSatisfyingLoose(versions, range, isBetter) {
234
+ const semverRange = parseRangeLoose(range);
235
+ if (semverRange == null)
236
+ return null;
237
+ let bestVersion = null;
238
+ let bestParsed = null;
239
+ for (const version of versions) {
240
+ const parsed = parseSemverLoose(version);
241
+ if (parsed == null || !semverRange.test(parsed))
242
+ continue;
243
+ if (bestParsed == null || isBetter(parsed, bestParsed)) {
244
+ bestVersion = version;
245
+ bestParsed = parsed;
246
+ }
247
+ }
248
+ return bestVersion;
249
+ }
250
+ function parseRangeLoose(range) {
251
+ let semverRange = semverRangeCache.get(range);
252
+ if (semverRange === undefined) {
253
+ try {
254
+ semverRange = new semver.Range(range, true);
255
+ }
256
+ catch {
257
+ semverRange = null;
258
+ }
259
+ if (semverRangeCache.size >= SEMVER_CACHE_MAX_SIZE)
260
+ semverRangeCache.clear();
261
+ semverRangeCache.set(range, semverRange);
262
+ }
263
+ return semverRange;
264
+ }
265
+ function parseSemverLoose(version) {
266
+ let parsed = semverInstanceCache.get(version);
267
+ if (parsed === undefined) {
268
+ try {
269
+ parsed = new semver.SemVer(version, true);
270
+ }
271
+ catch {
272
+ parsed = null;
273
+ }
274
+ if (semverInstanceCache.size >= SEMVER_CACHE_MAX_SIZE)
275
+ semverInstanceCache.clear();
276
+ semverInstanceCache.set(version, parsed);
277
+ }
278
+ return parsed;
279
+ }
280
+ // Working with string-ish semver causes lots of allocations and repeated
281
+ // work, and a dependency graph tests the same ranges and versions over and
282
+ // over, so both parses are cached. Parse failures are cached as null, so a
283
+ // malformed version string is never re-parsed either. The caches are dropped
284
+ // wholesale once they grow past this size, so a long-lived process (daemon,
285
+ // store server) can't retain them without bound.
286
+ const SEMVER_CACHE_MAX_SIZE = 50_000;
287
+ const semverRangeCache = new Map();
288
+ const semverInstanceCache = new Map();
227
289
  //# sourceMappingURL=pickPackageFromMeta.js.map
@@ -12,3 +12,4 @@ export declare const MINIMUM_RELEASE_AGE_VIOLATION_CODE = "MINIMUM_RELEASE_AGE_V
12
12
  export declare const TRUST_DOWNGRADE_VIOLATION_CODE = "TRUST_DOWNGRADE";
13
13
  export declare const TARBALL_URL_MISMATCH_VIOLATION_CODE = "TARBALL_URL_MISMATCH";
14
14
  export declare const MISSING_TARBALL_INTEGRITY_VIOLATION_CODE = "MISSING_TARBALL_INTEGRITY";
15
+ export declare const MISSING_NAMED_REGISTRY_VIOLATION_CODE = "MISSING_NAMED_REGISTRY";
@@ -12,4 +12,5 @@ export const MINIMUM_RELEASE_AGE_VIOLATION_CODE = 'MINIMUM_RELEASE_AGE_VIOLATION
12
12
  export const TRUST_DOWNGRADE_VIOLATION_CODE = 'TRUST_DOWNGRADE';
13
13
  export const TARBALL_URL_MISMATCH_VIOLATION_CODE = 'TARBALL_URL_MISMATCH';
14
14
  export const MISSING_TARBALL_INTEGRITY_VIOLATION_CODE = 'MISSING_TARBALL_INTEGRITY';
15
+ export const MISSING_NAMED_REGISTRY_VIOLATION_CODE = 'MISSING_NAMED_REGISTRY';
15
16
  //# sourceMappingURL=violationCodes.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/resolving.npm-resolver",
3
- "version": "1103.0.0",
3
+ "version": "1103.2.0",
4
4
  "description": "Resolver for npm-hosted packages",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -29,22 +29,24 @@
29
29
  "!*.map"
30
30
  ],
31
31
  "dependencies": {
32
- "@pnpm/config.pick-registry-for-package": "1100.0.14",
33
- "@pnpm/config.version-policy": "1100.1.11",
34
- "@pnpm/constants": "1100.0.1",
35
- "@pnpm/core-loggers": "1100.3.1",
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",
36
37
  "@pnpm/crypto.hash": "1100.0.2",
37
- "@pnpm/error": "1100.1.0",
38
+ "@pnpm/deps.path": "1100.1.0",
39
+ "@pnpm/error": "1100.1.1",
38
40
  "@pnpm/fetching.types": "1100.0.3",
39
41
  "@pnpm/fs.graceful-fs": "1100.1.1",
40
- "@pnpm/pkg-manifest.utils": "1100.3.0",
41
- "@pnpm/resolving.jsr-specifier-parser": "1100.0.3",
42
- "@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.14",
43
- "@pnpm/resolving.registry.types": "1100.1.8",
44
- "@pnpm/resolving.resolver-base": "1101.0.0",
45
- "@pnpm/store.cafs": "1100.1.17",
46
- "@pnpm/store.index": "1100.2.2",
47
- "@pnpm/types": "1101.8.0",
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",
48
50
  "@pnpm/workspace.range-resolver": "1100.0.3",
49
51
  "@pnpm/workspace.spec-parser": "1100.0.1",
50
52
  "@zkochan/retry": "^0.2.0",
@@ -64,18 +66,18 @@
64
66
  },
65
67
  "peerDependencies": {
66
68
  "@pnpm/logger": "^1100.0.0",
67
- "@pnpm/worker": "^1100.2.9"
69
+ "@pnpm/worker": "^1100.2.10"
68
70
  },
69
71
  "devDependencies": {
70
72
  "@jest/globals": "30.4.1",
71
73
  "@pnpm/logger": "1100.0.0",
72
- "@pnpm/network.fetch": "1100.1.10",
73
- "@pnpm/resolving.npm-resolver": "1103.0.0",
74
+ "@pnpm/network.fetch": "1100.1.11",
75
+ "@pnpm/resolving.npm-resolver": "1103.2.0",
74
76
  "@pnpm/test-fixtures": "1100.0.1",
75
77
  "@pnpm/testing.mock-agent": "1101.0.7",
76
78
  "@types/normalize-path": "^3.0.2",
77
79
  "@types/ramda": "0.32.0",
78
- "@types/semver": "7.7.1",
80
+ "@types/semver": "7.8.0",
79
81
  "@types/ssri": "^7.1.5",
80
82
  "@types/validate-npm-package-name": "^4.0.2",
81
83
  "load-json-file": "^7.0.1",