@pnpm/resolving.npm-resolver 1102.1.9 → 1103.1.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,107 @@
1
1
  # @pnpm/npm-resolver
2
2
 
3
+ ## 1103.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **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.
8
+
9
+ 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.
10
+
11
+ 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.
12
+
13
+ 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.
14
+
15
+ 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.
16
+
17
+ ### If you use named registries
18
+
19
+ 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.
20
+
21
+ 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.
22
+
23
+ There is no setting to keep the old behavior: the old shape is the vulnerability.
24
+
25
+ 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.
26
+
27
+ To use named registries, map your aliases in `pnpm-workspace.yaml`:
28
+
29
+ ```yaml
30
+ namedRegistries:
31
+ work: https://npm.enterprise.example.com/
32
+ ```
33
+
34
+ ### New built-in `npmjs:` alias
35
+
36
+ `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:
37
+
38
+ ```json
39
+ { "dependencies": { "left-pad": "npmjs:^1.3.0" } }
40
+ ```
41
+
42
+ `npm:` cannot do this — it is the alias protocol (`npm:<name>@<range>`) and resolves through whatever `registry` points at.
43
+
44
+ **If you mirror or proxy npmjs, point the alias at your mirror:**
45
+
46
+ ```yaml
47
+ namedRegistries:
48
+ npmjs: https://npm.internal.example.com/
49
+ ```
50
+
51
+ 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`.
52
+
53
+ 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.
54
+
55
+ 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.
56
+
57
+ `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.
58
+
59
+ ### Patch Changes
60
+
61
+ - 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.
62
+
63
+ - 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.
64
+
65
+ - Updated dependencies:
66
+ - @pnpm/config.normalize-registries@1100.1.0
67
+ - @pnpm/config.pick-registry-for-package@1100.1.0
68
+ - @pnpm/config.version-policy@1100.1.12
69
+ - @pnpm/constants@1101.0.0
70
+ - @pnpm/core-loggers@1100.3.2
71
+ - @pnpm/deps.path@1100.1.0
72
+ - @pnpm/error@1100.1.1
73
+ - @pnpm/pkg-manifest.utils@1100.3.1
74
+ - @pnpm/resolving.jsr-specifier-parser@1100.0.4
75
+ - @pnpm/resolving.registry.pkg-metadata-filter@1100.0.15
76
+ - @pnpm/resolving.registry.types@1100.1.9
77
+ - @pnpm/resolving.resolver-base@1101.1.0
78
+ - @pnpm/store.cafs@1100.1.18
79
+ - @pnpm/store.index@1100.2.3
80
+ - @pnpm/types@1101.9.0
81
+
82
+ ## 1103.0.0
83
+
84
+ ### Major Changes
85
+
86
+ - Renamed the `PinnedVersion` type to `RangeSpecStyle` and the `pinnedVersion` option fields to `rangeSpecStyle`: the value selects the operator a specifier is saved with, not a pin. `whichVersionIsPinned` is now `inferRangeSpecStyle`, and the new `rangeSpecGranularity` helper collapses the `exact` spelling to its `patch` range width. `@pnpm/types` keeps `PinnedVersion` as a deprecated alias of `RangeSpecStyle`. The rename itself changes no CLI behavior; the `=`-pin handling is described in its own changesets.
87
+
88
+ ### Patch Changes
89
+
90
+ - The install summary no longer prints `(X is available)` when the registry's `dist-tags.latest` is still held back by the active `minimumReleaseAge` policy. The hint only ever names the actual latest tag, so an immature latest suppresses the hint instead of advertising the version pnpm just refused to install [#11698](https://github.com/pnpm/pnpm/issues/11698).
91
+
92
+ - `pnpm update` keeps the explicit `=` operator of an exact version pin: a dependency saved as `=3.5.1` now updates to `=3.5.2` instead of the bare `3.5.2`. See pnpm/pnpm#13168.
93
+
94
+ - Updated dependencies:
95
+ - @pnpm/config.pick-registry-for-package@1100.0.14
96
+ - @pnpm/config.version-policy@1100.1.11
97
+ - @pnpm/core-loggers@1100.3.1
98
+ - @pnpm/pkg-manifest.utils@1100.3.0
99
+ - @pnpm/resolving.registry.pkg-metadata-filter@1100.0.14
100
+ - @pnpm/resolving.registry.types@1100.1.8
101
+ - @pnpm/resolving.resolver-base@1101.0.0
102
+ - @pnpm/store.cafs@1100.1.17
103
+ - @pnpm/types@1101.8.0
104
+
3
105
  ## 1102.1.9
4
106
 
5
107
  ### Patch 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
@@ -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
@@ -85,7 +64,7 @@ export function createNpmResolutionVerifier(opts) {
85
64
  const minimumReleaseAge = opts.minimumReleaseAge ?? 0;
86
65
  const trustPolicy = opts.trustPolicy;
87
66
  const trustPolicyIgnoreAfter = opts.trustPolicyIgnoreAfter;
88
- const verify = async (resolution, { name, version, nonSemverVersion }) => {
67
+ const verify = async (resolution, { name, version, nonSemverVersion, registryName }) => {
89
68
  if (!isRegistryTarballResolution(resolution))
90
69
  return { ok: true };
91
70
  // Network-free structural checks must run before registry metadata shortcuts.
@@ -120,7 +99,26 @@ export function createNpmResolutionVerifier(opts) {
120
99
  };
121
100
  }
122
101
  const tarballUrl = typeof rawTarball === 'string' ? rawTarball : undefined;
123
- const registry = pickRegistryForVersion(opts.registries, namedRegistryPrefixes, name, tarballUrl);
102
+ let registry;
103
+ if (registryName != null) {
104
+ // Registry-qualified entries name their registry in the dep path, so
105
+ // routing does not depend on a recorded tarball URL (canonical URLs
106
+ // are omitted from the lockfile in the 12.0 format).
107
+ const namedRegistry = mergedNamedRegistries[registryName];
108
+ if (!namedRegistry) {
109
+ // Fail closed: without the registry URL, none of the metadata-backed
110
+ // checks below can vouch for this entry.
111
+ return {
112
+ ok: false,
113
+ code: MISSING_NAMED_REGISTRY_VIOLATION_CODE,
114
+ reason: `was resolved from the named registry '${registryName}:', which is not present in the namedRegistries setting`,
115
+ };
116
+ }
117
+ registry = namedRegistry;
118
+ }
119
+ else {
120
+ registry = pickRegistryForVersion(opts.registries, namedRegistryPrefixes, name, tarballUrl);
121
+ }
124
122
  // A registry entry that pins an explicit tarball URL must point at the
125
123
  // artifact the registry's own metadata lists. Otherwise a trusted
126
124
  // `name@version` could front bytes from an attacker-chosen URL (with a
@@ -164,6 +162,10 @@ export function createNpmResolutionVerifier(opts) {
164
162
  // stays trusted after its exclude entry has been pulled.
165
163
  const sortedMinAgeExcludes = [...new Set(opts.minimumReleaseAgeExclude ?? [])].sort();
166
164
  const sortedTrustExcludes = [...new Set(opts.trustPolicyExclude ?? [])].sort();
165
+ const sortedNamedRegistries = Object.fromEntries(Object.entries(mergedNamedRegistries).sort(([aliasA], [aliasB]) => aliasA.localeCompare(aliasB)));
166
+ const namedRegistriesRouting = createHash('sha256')
167
+ .update(JSON.stringify(sortedNamedRegistries))
168
+ .digest('hex');
167
169
  return {
168
170
  verify,
169
171
  policy: {
@@ -175,6 +177,7 @@ export function createNpmResolutionVerifier(opts) {
175
177
  tarballUrlBinding: true,
176
178
  // Same cache identity rule for the missing-integrity structural check.
177
179
  integrityRequired: true,
180
+ namedRegistriesRouting,
178
181
  minimumReleaseAge,
179
182
  minimumReleaseAgeExclude: sortedMinAgeExcludes,
180
183
  trustPolicy: trustPolicy ?? null,
@@ -190,6 +193,8 @@ export function createNpmResolutionVerifier(opts) {
190
193
  // without the flag cannot prove they rejected unverifiable tarballs.
191
194
  if (cached.integrityRequired !== true)
192
195
  return false;
196
+ if (cached.namedRegistriesRouting !== namedRegistriesRouting)
197
+ return false;
193
198
  // Maturity: a previously cached run under a larger cutoff
194
199
  // (stricter window) is trustworthy under a smaller current one —
195
200
  // its set of accepted versions is a subset of today's. The
package/lib/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import { PnpmError } from '@pnpm/error';
2
2
  import type { FetchFromRegistry, GetAuthHeader, RetryTimeoutOptions } from '@pnpm/fetching.types';
3
3
  import type { PackageMeta } from '@pnpm/resolving.registry.types';
4
4
  import type { DirectoryResolution, LatestInfo, LatestQuery, PkgResolutionId, PreferredVersions, ResolveOptions, ResolveResult, TarballResolution, WantedDependency, WorkspacePackages } from '@pnpm/resolving.resolver-base';
5
- import type { DependencyManifest, PackageVersionPolicy, PinnedVersion, Registries, TrustPolicy } from '@pnpm/types';
5
+ import type { DependencyManifest, PackageVersionPolicy, RangeSpecStyle, Registries, TrustPolicy } from '@pnpm/types';
6
6
  import { fetchMetadataFromFromRegistry, type FetchMetadataFromFromRegistryOptions, RegistryResponseError } from './fetch.js';
7
7
  import { BUILTIN_NAMED_REGISTRIES, parseBareSpecifier, type RegistryPackageSpec } from './parseBareSpecifier.js';
8
8
  import { type PackageMetaCache, pickPackage, type PickPackageOptions } from './pickPackage.js';
@@ -20,8 +20,8 @@ export declare class NoMatchingVersionError extends PnpmError {
20
20
  export declare function formatTimeAgo(date: Date): string | null;
21
21
  export { BUILTIN_NAMED_REGISTRIES, fetchMetadataFromFromRegistry, type FetchMetadataFromFromRegistryOptions, type PackageMeta, type PackageMetaCache, parseBareSpecifier, pickPackageFromMeta, pickVersionByVersionRange, type RegistryPackageSpec, RegistryResponseError, workspacePrefToNpm, };
22
22
  export { createNpmResolutionVerifier, type CreateNpmResolutionVerifierOptions } from './createNpmResolutionVerifier.js';
23
+ export { inferRangeSpecStyle } from './inferRangeSpecStyle.js';
23
24
  export { MINIMUM_RELEASE_AGE_VIOLATION_CODE, TRUST_DOWNGRADE_VIOLATION_CODE, } from './violationCodes.js';
24
- export { whichVersionIsPinned } from './whichVersionIsPinned.js';
25
25
  export interface ResolverFactoryOptions {
26
26
  cacheDir: string;
27
27
  storeDir?: string;
@@ -115,7 +115,7 @@ export type ResolveFromNpmOptions = {
115
115
  updateChecksums?: boolean;
116
116
  injectWorkspacePackages?: boolean;
117
117
  calcSpecifier?: boolean;
118
- pinnedVersion?: PinnedVersion;
118
+ rangeSpecStyle?: RangeSpecStyle;
119
119
  } & ({
120
120
  projectDir?: string;
121
121
  workspacePackages?: undefined;
package/lib/index.js CHANGED
@@ -1,7 +1,9 @@
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';
6
+ import { rangeSpecGranularity, versionWithRangeSpecStyle } from '@pnpm/pkg-manifest.utils';
5
7
  import { EXISTING_VERSION_SELECTOR_WEIGHT, } from '@pnpm/resolving.resolver-base';
6
8
  import { storeIndexKey } from '@pnpm/store.index';
7
9
  import { readPkgFromCafs, } from '@pnpm/worker';
@@ -14,6 +16,7 @@ import ssri from 'ssri';
14
16
  import versionSelectorType from 'version-selector-type';
15
17
  import { clearMeta, retainsFullMeta } from './clearMeta.js';
16
18
  import { fetchMetadataFromFromRegistry, RegistryResponseError } from './fetch.js';
19
+ import { inferRangeSpecStyle } from './inferRangeSpecStyle.js';
17
20
  import { memoizeFetchMetadata } from './memoizeFetchMetadata.js';
18
21
  import { normalizeRegistryUrl } from './normalizeRegistryUrl.js';
19
22
  import { BUILTIN_NAMED_REGISTRIES, parseBareSpecifier, parseJsrSpecifierToRegistryPackageSpec, parseNamedRegistrySpecifierToRegistryPackageSpec, } from './parseBareSpecifier.js';
@@ -21,7 +24,6 @@ import { pickPackage, } from './pickPackage.js';
21
24
  import { pickPackageFromMeta, pickVersionByVersionRange } from './pickPackageFromMeta.js';
22
25
  import { failIfTrustDowngraded } from './trustChecks.js';
23
26
  import { MINIMUM_RELEASE_AGE_VIOLATION_CODE } from './violationCodes.js';
24
- import { whichVersionIsPinned } from './whichVersionIsPinned.js';
25
27
  import { workspacePrefToNpm } from './workspacePrefToNpm.js';
26
28
  export class NoMatchingVersionError extends PnpmError {
27
29
  packageMeta;
@@ -64,8 +66,8 @@ export function formatTimeAgo(date) {
64
66
  }
65
67
  export { BUILTIN_NAMED_REGISTRIES, fetchMetadataFromFromRegistry, parseBareSpecifier, pickPackageFromMeta, pickVersionByVersionRange, RegistryResponseError, workspacePrefToNpm, };
66
68
  export { createNpmResolutionVerifier } from './createNpmResolutionVerifier.js';
69
+ export { inferRangeSpecStyle } from './inferRangeSpecStyle.js';
67
70
  export { MINIMUM_RELEASE_AGE_VIOLATION_CODE, TRUST_DOWNGRADE_VIOLATION_CODE, } from './violationCodes.js';
68
- export { whichVersionIsPinned } from './whichVersionIsPinned.js';
69
71
  export function createNpmResolver(fetchFromRegistry, getAuthHeader, opts) {
70
72
  if (typeof opts.cacheDir !== 'string') {
71
73
  throw new TypeError('`opts.cacheDir` is required and needs to be a string');
@@ -310,7 +312,7 @@ async function resolveNpm(ctx, wantedDependency, opts) {
310
312
  update: Boolean(opts.update),
311
313
  saveWorkspaceProtocol: ctx.saveWorkspaceProtocol !== false ? ctx.saveWorkspaceProtocol : true,
312
314
  calcSpecifier: opts.calcSpecifier,
313
- pinnedVersion: opts.pinnedVersion,
315
+ rangeSpecStyle: opts.rangeSpecStyle,
314
316
  });
315
317
  if (resolvedFromWorkspace != null) {
316
318
  return resolvedFromWorkspace;
@@ -397,7 +399,7 @@ async function resolveNpm(ctx, wantedDependency, opts) {
397
399
  update: false,
398
400
  saveWorkspaceProtocol: ctx.saveWorkspaceProtocol,
399
401
  calcSpecifier: opts.calcSpecifier,
400
- pinnedVersion: opts.pinnedVersion,
402
+ rangeSpecStyle: opts.rangeSpecStyle,
401
403
  });
402
404
  }
403
405
  catch (workspaceErr) {
@@ -424,7 +426,7 @@ async function resolveNpm(ctx, wantedDependency, opts) {
424
426
  update: false,
425
427
  saveWorkspaceProtocol: ctx.saveWorkspaceProtocol,
426
428
  calcSpecifier: opts.calcSpecifier,
427
- pinnedVersion: opts.pinnedVersion,
429
+ rangeSpecStyle: opts.rangeSpecStyle,
428
430
  });
429
431
  }
430
432
  catch (workspaceErr) {
@@ -441,6 +443,7 @@ async function resolveNpm(ctx, wantedDependency, opts) {
441
443
  else if (opts.trustPolicy === 'no-downgrade') {
442
444
  failIfTrustDowngraded(meta, pickedPackage.version, opts);
443
445
  }
446
+ const latest = latestAllowedByPolicy(meta, opts);
444
447
  const workspacePkgsMatchingName = workspacePackages?.get(pickedPackage.name);
445
448
  if (workspacePkgsMatchingName && opts.projectDir) {
446
449
  const matchedPkg = workspacePkgsMatchingName.get(pickedPackage.version);
@@ -453,9 +456,9 @@ async function resolveNpm(ctx, wantedDependency, opts) {
453
456
  hardLinkLocalPackages: opts.injectWorkspacePackages === true || wantedDependency.injected,
454
457
  saveWorkspaceProtocol: ctx.saveWorkspaceProtocol,
455
458
  calcSpecifier: opts.calcSpecifier,
456
- pinnedVersion: opts.pinnedVersion,
459
+ rangeSpecStyle: opts.rangeSpecStyle,
457
460
  }),
458
- latest: meta['dist-tags'].latest,
461
+ latest,
459
462
  };
460
463
  }
461
464
  const localVersion = pickMatchingLocalVersionOrNull(workspacePkgsMatchingName, spec);
@@ -468,9 +471,9 @@ async function resolveNpm(ctx, wantedDependency, opts) {
468
471
  hardLinkLocalPackages: opts.injectWorkspacePackages === true || wantedDependency.injected,
469
472
  saveWorkspaceProtocol: ctx.saveWorkspaceProtocol,
470
473
  calcSpecifier: opts.calcSpecifier,
471
- pinnedVersion: opts.pinnedVersion,
474
+ rangeSpecStyle: opts.rangeSpecStyle,
472
475
  }),
473
- latest: meta['dist-tags'].latest,
476
+ latest,
474
477
  };
475
478
  }
476
479
  }
@@ -486,13 +489,13 @@ async function resolveNpm(ctx, wantedDependency, opts) {
486
489
  wantedDependency,
487
490
  spec,
488
491
  version: pickedPackage.version,
489
- defaultPinnedVersion: opts.pinnedVersion,
492
+ defaultRangeSpecStyle: opts.rangeSpecStyle,
490
493
  });
491
494
  }
492
495
  const publishedAt = meta.time?.[pickedPackage.version];
493
496
  return {
494
497
  id,
495
- latest: meta['dist-tags'].latest,
498
+ latest,
496
499
  manifest: pickedPackage,
497
500
  resolution,
498
501
  resolvedVia: 'npm-registry',
@@ -518,7 +521,7 @@ async function resolveJsr(ctx, wantedDependency, opts) {
518
521
  return {
519
522
  ...picked,
520
523
  normalizedBareSpecifier: opts.calcSpecifier
521
- ? calcPrefixedSpecifier('jsr:', spec.jsrPkgName, wantedDependency, picked.manifest.version, opts.pinnedVersion)
524
+ ? calcPrefixedSpecifier('jsr:', spec.jsrPkgName, wantedDependency, picked.manifest.version, opts.rangeSpecStyle)
522
525
  : undefined,
523
526
  resolvedVia: 'jsr-registry',
524
527
  alias: spec.jsrPkgName,
@@ -537,6 +540,11 @@ function mergeNamedRegistries(userDefined) {
537
540
  if (!userDefined)
538
541
  return merged;
539
542
  for (const [alias, url] of Object.entries(userDefined)) {
543
+ if (RESERVED_VERSION_PREFIXES.has(alias) || !isWellFormedRegistryName(alias)) {
544
+ throw new PnpmError('RESERVED_NAMED_REGISTRY_NAME', RESERVED_VERSION_PREFIXES.has(alias)
545
+ ? `'${alias}' cannot be used as a named registry alias: it is a reserved dependency specifier prefix.`
546
+ : `'${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.' });
547
+ }
540
548
  if (typeof url !== 'string' || !isValidHttpUrl(url)) {
541
549
  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/' });
542
550
  }
@@ -572,8 +580,13 @@ async function resolveFromNamedRegistry(ctx, wantedDependency, opts) {
572
580
  const picked = await pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, registry);
573
581
  return {
574
582
  ...picked,
583
+ // Qualifying the id with the registry alias is what keeps the same
584
+ // name@version resolved from two registries distinct in the lockfile.
585
+ // Without it they collapse onto one entry and whichever resolved first
586
+ // decides the tarball both consumers get.
587
+ id: `${picked.manifest.name}@${spec.registryName}:${picked.manifest.version}`,
575
588
  normalizedBareSpecifier: opts.calcSpecifier
576
- ? calcPrefixedSpecifier(`${spec.registryName}:`, spec.name, wantedDependency, picked.manifest.version, opts.pinnedVersion)
589
+ ? calcPrefixedSpecifier(`${spec.registryName}:`, spec.name, wantedDependency, picked.manifest.version, opts.rangeSpecStyle)
577
590
  : undefined,
578
591
  resolvedVia: 'named-registry',
579
592
  registryName: spec.registryName,
@@ -611,7 +624,7 @@ async function pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, registr
611
624
  const publishedAt = meta.time?.[pickedPackage.version];
612
625
  return {
613
626
  id: `${pickedPackage.name}@${pickedPackage.version}`,
614
- latest: meta['dist-tags'].latest,
627
+ latest: latestAllowedByPolicy(meta, opts),
615
628
  manifest: pickedPackage,
616
629
  resolution,
617
630
  publishedAt,
@@ -629,29 +642,29 @@ async function pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, registr
629
642
  // when the dependency alias matches the package name). Shared between the
630
643
  // jsr and named-registry resolvers since they only differ in `prefix` and
631
644
  // which spec field holds the package name.
632
- function calcPrefixedSpecifier(prefix, pkgName, wantedDependency, version, defaultPinnedVersion) {
633
- const range = calcRange(version, wantedDependency, defaultPinnedVersion);
645
+ function calcPrefixedSpecifier(prefix, pkgName, wantedDependency, version, defaultRangeSpecStyle) {
646
+ const range = calcRange(version, wantedDependency, defaultRangeSpecStyle);
634
647
  if (!wantedDependency.alias || pkgName === wantedDependency.alias)
635
648
  return `${prefix}${range}`;
636
649
  return `${prefix}${pkgName}@${range}`;
637
650
  }
638
- function calcSpecifier({ wantedDependency, spec, version, defaultPinnedVersion, }) {
651
+ function calcSpecifier({ wantedDependency, spec, version, defaultRangeSpecStyle, }) {
639
652
  if (wantedDependency.prevSpecifier === wantedDependency.bareSpecifier && wantedDependency.prevSpecifier && versionSelectorType(wantedDependency.prevSpecifier)?.type === 'tag') {
640
653
  return wantedDependency.prevSpecifier;
641
654
  }
642
- const range = calcRange(version, wantedDependency, defaultPinnedVersion);
655
+ const range = calcRange(version, wantedDependency, defaultRangeSpecStyle);
643
656
  if (!wantedDependency.alias || spec.name === wantedDependency.alias)
644
657
  return range;
645
658
  return `npm:${spec.name}@${range}`;
646
659
  }
647
- function calcRange(version, wantedDependency, defaultPinnedVersion) {
660
+ function calcRange(version, wantedDependency, defaultRangeSpecStyle) {
648
661
  if (semver.parse(version)?.prerelease.length) {
649
662
  return version;
650
663
  }
651
- const pinnedVersion = (wantedDependency.prevSpecifier ? whichVersionIsPinned(wantedDependency.prevSpecifier) : undefined) ??
652
- (wantedDependency.bareSpecifier ? whichVersionIsPinned(wantedDependency.bareSpecifier) : undefined) ??
653
- defaultPinnedVersion;
654
- return createVersionSpec(version, pinnedVersion);
664
+ const rangeSpecStyle = (wantedDependency.prevSpecifier ? inferRangeSpecStyle(wantedDependency.prevSpecifier) : undefined) ??
665
+ (wantedDependency.bareSpecifier ? inferRangeSpecStyle(wantedDependency.bareSpecifier) : undefined) ??
666
+ defaultRangeSpecStyle;
667
+ return versionWithRangeSpecStyle(version, rangeSpecStyle ?? 'major');
655
668
  }
656
669
  function tryResolveFromWorkspace(wantedDependency, opts) {
657
670
  if (!wantedDependency.bareSpecifier?.startsWith('workspace:')) {
@@ -675,7 +688,7 @@ function tryResolveFromWorkspace(wantedDependency, opts) {
675
688
  update: opts.update,
676
689
  saveWorkspaceProtocol: opts.saveWorkspaceProtocol,
677
690
  calcSpecifier: opts.calcSpecifier,
678
- pinnedVersion: opts.pinnedVersion,
691
+ rangeSpecStyle: opts.rangeSpecStyle,
679
692
  });
680
693
  }
681
694
  function tryResolveFromWorkspacePackages(workspacePackages, spec, opts) {
@@ -730,7 +743,7 @@ function resolveFromLocalPackage(localPackage, spec, opts) {
730
743
  spec,
731
744
  saveWorkspaceProtocol: opts.saveWorkspaceProtocol,
732
745
  version: localPackage.manifest.version,
733
- defaultPinnedVersion: opts.pinnedVersion,
746
+ defaultRangeSpecStyle: opts.rangeSpecStyle,
734
747
  });
735
748
  }
736
749
  return {
@@ -744,9 +757,9 @@ function resolveFromLocalPackage(localPackage, spec, opts) {
744
757
  normalizedBareSpecifier,
745
758
  };
746
759
  }
747
- function calcSpecifierForWorkspaceDep({ wantedDependency, spec, saveWorkspaceProtocol, version, defaultPinnedVersion, }) {
760
+ function calcSpecifierForWorkspaceDep({ wantedDependency, spec, saveWorkspaceProtocol, version, defaultRangeSpecStyle, }) {
748
761
  if (!saveWorkspaceProtocol && !wantedDependency.bareSpecifier?.startsWith('workspace:')) {
749
- return calcSpecifier({ wantedDependency, spec, version, defaultPinnedVersion });
762
+ return calcSpecifier({ wantedDependency, spec, version, defaultRangeSpecStyle });
750
763
  }
751
764
  const prefix = (!wantedDependency.alias || spec.name === wantedDependency.alias) ? 'workspace:' : `workspace:${spec.name}@`;
752
765
  if (saveWorkspaceProtocol === 'rolling') {
@@ -754,8 +767,8 @@ function calcSpecifierForWorkspaceDep({ wantedDependency, spec, saveWorkspacePro
754
767
  if (specifier) {
755
768
  if ([`${prefix}*`, `${prefix}^`, `${prefix}~`].includes(specifier))
756
769
  return specifier;
757
- const pinnedVersion = whichVersionIsPinned(specifier);
758
- switch (pinnedVersion) {
770
+ const rangeSpecStyle = inferRangeSpecStyle(specifier);
771
+ switch (rangeSpecStyle && rangeSpecGranularity(rangeSpecStyle)) {
759
772
  case 'major': return `${prefix}^`;
760
773
  case 'minor': return `${prefix}~`;
761
774
  case 'patch':
@@ -767,8 +780,8 @@ function calcSpecifierForWorkspaceDep({ wantedDependency, spec, saveWorkspacePro
767
780
  if (semver.parse(version)?.prerelease.length) {
768
781
  return `${prefix}${version}`;
769
782
  }
770
- const pinnedVersion = (wantedDependency.prevSpecifier ? whichVersionIsPinned(wantedDependency.prevSpecifier) : undefined) ?? defaultPinnedVersion;
771
- const range = createVersionSpec(version, pinnedVersion);
783
+ const rangeSpecStyle = (wantedDependency.prevSpecifier ? inferRangeSpecStyle(wantedDependency.prevSpecifier) : undefined) ?? defaultRangeSpecStyle;
784
+ const range = versionWithRangeSpecStyle(version, rangeSpecStyle ?? 'major');
772
785
  return `${prefix}${range}`;
773
786
  }
774
787
  function resolveLocalPackageDir(localPackage) {
@@ -784,6 +797,31 @@ function defaultTagForAlias(alias, defaultTag) {
784
797
  type: 'tag',
785
798
  };
786
799
  }
800
+ /**
801
+ * The raw `dist-tags.latest` when the active `minimumReleaseAge` policy would
802
+ * allow installing it, `undefined` otherwise. The install summary's
803
+ * "(X is available)" hint must only ever name the actual latest tag, so an
804
+ * immature latest suppresses the hint instead of being rewritten to an older
805
+ * mature version. Suppression requires positive evidence of immaturity: a
806
+ * missing or unparsable timestamp keeps the raw tag, matching
807
+ * `detectMinReleaseAgeViolation`, which likewise only flags a version it can
808
+ * date.
809
+ */
810
+ function latestAllowedByPolicy(meta, opts) {
811
+ const latest = meta['dist-tags'].latest;
812
+ if (!latest || !opts.publishedBy)
813
+ return latest;
814
+ const excludeResult = opts.publishedByExclude?.(meta.name);
815
+ if (excludeResult === true)
816
+ return latest;
817
+ if (Array.isArray(excludeResult) && excludeResult.includes(latest))
818
+ return latest;
819
+ const publishedAt = meta.time?.[latest];
820
+ if (publishedAt == null)
821
+ return latest;
822
+ const ts = new Date(publishedAt).getTime();
823
+ return (Number.isNaN(ts) || ts <= opts.publishedBy.getTime()) ? latest : undefined;
824
+ }
787
825
  /**
788
826
  * Inline minimumReleaseAge detection: returns a violation entry when the
789
827
  * picked version's publish timestamp is past the policy cutoff (and
@@ -831,19 +869,6 @@ function getIntegrity(dist) {
831
869
  }
832
870
  return integrity.toString();
833
871
  }
834
- function createVersionSpec(version, pinnedVersion) {
835
- switch (pinnedVersion ?? 'major') {
836
- case 'none':
837
- case 'major':
838
- return `^${version}`;
839
- case 'minor':
840
- return `~${version}`;
841
- case 'patch':
842
- return version;
843
- default:
844
- throw new PnpmError('BAD_PINNED_VERSION', `Cannot pin '${pinnedVersion ?? 'undefined'}'`);
845
- }
846
- }
847
872
  /**
848
873
  * Construct the LRU `PackageMetaCache` instance the resolver uses by
849
874
  * default. Exported so the install layer can build one cache and hand
@@ -0,0 +1,2 @@
1
+ import type { RangeSpecStyle } from '@pnpm/types';
2
+ export declare function inferRangeSpecStyle(spec: string): RangeSpecStyle | undefined;
@@ -1,5 +1,5 @@
1
1
  import { parseRange } from 'semver-utils';
2
- export function whichVersionIsPinned(spec) {
2
+ export function inferRangeSpecStyle(spec) {
3
3
  // A catalog reference carries no version pinning of its own; the pinning is
4
4
  // defined by the catalog entry it points to. Bail out so a catalog name that
5
5
  // happens to look like a version (e.g. "catalog:express4-21") isn't misread
@@ -23,11 +23,12 @@ export function whichVersionIsPinned(spec) {
23
23
  switch (versionObject.operator) {
24
24
  case '~': return 'minor';
25
25
  case '^': return 'major';
26
- // A bare '=' pins the same way the plain version it prefixes does.
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.
27
28
  case '=':
28
29
  case undefined:
29
30
  if (versionObject.patch)
30
- return 'patch';
31
+ return versionObject.operator === '=' ? 'exact' : 'patch';
31
32
  if (versionObject.minor)
32
33
  return 'minor';
33
34
  if (versionObject.major)
@@ -35,4 +36,4 @@ export function whichVersionIsPinned(spec) {
35
36
  }
36
37
  return undefined;
37
38
  }
38
- //# sourceMappingURL=whichVersionIsPinned.js.map
39
+ //# sourceMappingURL=inferRangeSpecStyle.js.map
@@ -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),
@@ -90,36 +90,11 @@ function parseModifiedDate(modified) {
90
90
  return null;
91
91
  return date;
92
92
  }
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
93
  export function pickLowestVersionByVersionRange({ meta, versionRange, preferredVersionSelectors }) {
119
94
  if (preferredVersionSelectors != null && Object.keys(preferredVersionSelectors).length > 0) {
120
95
  const prioritizedPreferredVersions = prioritizePreferredVersions(meta, versionRange, preferredVersionSelectors);
121
96
  for (const preferredVersions of prioritizedPreferredVersions) {
122
- const preferredVersion = semver.minSatisfying(preferredVersions, versionRange, true);
97
+ const preferredVersion = minSatisfyingLoose(preferredVersions, versionRange);
123
98
  if (preferredVersion) {
124
99
  return preferredVersion;
125
100
  }
@@ -128,7 +103,7 @@ export function pickLowestVersionByVersionRange({ meta, versionRange, preferredV
128
103
  if (versionRange === '*') {
129
104
  return Object.keys(meta.versions).sort(semver.compare)[0];
130
105
  }
131
- return semver.minSatisfying(Object.keys(meta.versions), versionRange, true);
106
+ return minSatisfyingLoose(Object.keys(meta.versions), versionRange);
132
107
  }
133
108
  export function pickVersionByVersionRange({ meta, versionRange, preferredVersionSelectors }) {
134
109
  const latest = meta['dist-tags'].latest;
@@ -138,7 +113,7 @@ export function pickVersionByVersionRange({ meta, versionRange, preferredVersion
138
113
  if (preferredVersions.includes(latest) && semverSatisfiesLoose(latest, versionRange)) {
139
114
  return latest;
140
115
  }
141
- const preferredVersion = semver.maxSatisfying(preferredVersions, versionRange, true);
116
+ const preferredVersion = maxSatisfyingLoose(preferredVersions, versionRange);
142
117
  if (preferredVersion) {
143
118
  return preferredVersion;
144
119
  }
@@ -150,13 +125,13 @@ export function pickVersionByVersionRange({ meta, versionRange, preferredVersion
150
125
  // E.g.: 1.0.0-beta.1. See issue: https://github.com/pnpm/pnpm/issues/865
151
126
  return latest;
152
127
  }
153
- const maxVersion = semver.maxSatisfying(versions, versionRange, true);
128
+ const maxVersion = maxSatisfyingLoose(versions, versionRange);
154
129
  // if the selected version is deprecated, try to find a non-deprecated one that satisfies the range
155
130
  if (maxVersion && meta.versions[maxVersion].deprecated && versions.length > 1) {
156
131
  const nonDeprecatedVersions = versions.map((version) => meta.versions[version])
157
132
  .filter((versionMeta) => !versionMeta.deprecated)
158
133
  .map((versionMeta) => versionMeta.version);
159
- const maxNonDeprecatedVersion = semver.maxSatisfying(nonDeprecatedVersions, versionRange, true);
134
+ const maxNonDeprecatedVersion = maxSatisfyingLoose(nonDeprecatedVersions, versionRange);
160
135
  if (maxNonDeprecatedVersion)
161
136
  return maxNonDeprecatedVersion;
162
137
  }
@@ -224,4 +199,76 @@ class PreferredVersionsPrioritizer {
224
199
  .map((weight) => versionsByWeight[parseInt(weight, 10)]);
225
200
  }
226
201
  }
202
+ function semverSatisfiesLoose(version, range) {
203
+ const semverRange = parseRangeLoose(range);
204
+ if (semverRange == null)
205
+ return false;
206
+ const parsedVersion = parseSemverLoose(version);
207
+ return parsedVersion != null && semverRange.test(parsedVersion);
208
+ }
209
+ // semver's own maxSatisfying/minSatisfying re-parse the range and every
210
+ // version string on each call, which dominates resolution time on large
211
+ // packuments; these reuse the parse caches instead.
212
+ function maxSatisfyingLoose(versions, range) {
213
+ return findSatisfyingLoose(versions, range, (candidate, best) => candidate.compare(best) > 0);
214
+ }
215
+ function minSatisfyingLoose(versions, range) {
216
+ return findSatisfyingLoose(versions, range, (candidate, best) => candidate.compare(best) < 0);
217
+ }
218
+ function findSatisfyingLoose(versions, range, isBetter) {
219
+ const semverRange = parseRangeLoose(range);
220
+ if (semverRange == null)
221
+ return null;
222
+ let bestVersion = null;
223
+ let bestParsed = null;
224
+ for (const version of versions) {
225
+ const parsed = parseSemverLoose(version);
226
+ if (parsed == null || !semverRange.test(parsed))
227
+ continue;
228
+ if (bestParsed == null || isBetter(parsed, bestParsed)) {
229
+ bestVersion = version;
230
+ bestParsed = parsed;
231
+ }
232
+ }
233
+ return bestVersion;
234
+ }
235
+ function parseRangeLoose(range) {
236
+ let semverRange = semverRangeCache.get(range);
237
+ if (semverRange === undefined) {
238
+ try {
239
+ semverRange = new semver.Range(range, true);
240
+ }
241
+ catch {
242
+ semverRange = null;
243
+ }
244
+ if (semverRangeCache.size >= SEMVER_CACHE_MAX_SIZE)
245
+ semverRangeCache.clear();
246
+ semverRangeCache.set(range, semverRange);
247
+ }
248
+ return semverRange;
249
+ }
250
+ function parseSemverLoose(version) {
251
+ let parsed = semverInstanceCache.get(version);
252
+ if (parsed === undefined) {
253
+ try {
254
+ parsed = new semver.SemVer(version, true);
255
+ }
256
+ catch {
257
+ parsed = null;
258
+ }
259
+ if (semverInstanceCache.size >= SEMVER_CACHE_MAX_SIZE)
260
+ semverInstanceCache.clear();
261
+ semverInstanceCache.set(version, parsed);
262
+ }
263
+ return parsed;
264
+ }
265
+ // Working with string-ish semver causes lots of allocations and repeated
266
+ // work, and a dependency graph tests the same ranges and versions over and
267
+ // over, so both parses are cached. Parse failures are cached as null, so a
268
+ // malformed version string is never re-parsed either. The caches are dropped
269
+ // wholesale once they grow past this size, so a long-lived process (daemon,
270
+ // store server) can't retain them without bound.
271
+ const SEMVER_CACHE_MAX_SIZE = 50_000;
272
+ const semverRangeCache = new Map();
273
+ const semverInstanceCache = new Map();
227
274
  //# 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": "1102.1.9",
3
+ "version": "1103.1.0",
4
4
  "description": "Resolver for npm-hosted packages",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -29,21 +29,24 @@
29
29
  "!*.map"
30
30
  ],
31
31
  "dependencies": {
32
- "@pnpm/config.pick-registry-for-package": "1100.0.13",
33
- "@pnpm/config.version-policy": "1100.1.10",
34
- "@pnpm/constants": "1100.0.1",
35
- "@pnpm/core-loggers": "1100.3.0",
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/resolving.jsr-specifier-parser": "1100.0.3",
41
- "@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.13",
42
- "@pnpm/resolving.registry.types": "1100.1.7",
43
- "@pnpm/resolving.resolver-base": "1100.5.5",
44
- "@pnpm/store.cafs": "1100.1.16",
45
- "@pnpm/store.index": "1100.2.2",
46
- "@pnpm/types": "1101.7.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.15",
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",
47
50
  "@pnpm/workspace.range-resolver": "1100.0.3",
48
51
  "@pnpm/workspace.spec-parser": "1100.0.1",
49
52
  "@zkochan/retry": "^0.2.0",
@@ -63,13 +66,13 @@
63
66
  },
64
67
  "peerDependencies": {
65
68
  "@pnpm/logger": "^1100.0.0",
66
- "@pnpm/worker": "^1100.2.8"
69
+ "@pnpm/worker": "^1100.2.10"
67
70
  },
68
71
  "devDependencies": {
69
72
  "@jest/globals": "30.4.1",
70
73
  "@pnpm/logger": "1100.0.0",
71
- "@pnpm/network.fetch": "1100.1.9",
72
- "@pnpm/resolving.npm-resolver": "1102.1.9",
74
+ "@pnpm/network.fetch": "1100.1.11",
75
+ "@pnpm/resolving.npm-resolver": "1103.1.0",
73
76
  "@pnpm/test-fixtures": "1100.0.1",
74
77
  "@pnpm/testing.mock-agent": "1101.0.7",
75
78
  "@types/normalize-path": "^3.0.2",
@@ -1,2 +0,0 @@
1
- import type { PinnedVersion } from '@pnpm/types';
2
- export declare function whichVersionIsPinned(spec: string): PinnedVersion | undefined;