@pnpm/resolving.npm-resolver 1103.2.0 → 1104.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,87 @@
1
1
  # @pnpm/npm-resolver
2
2
 
3
+ ## 1104.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - The three registry lookups are now named for what they are keyed by, so that none of them is called `registries` — a name the `registries` setting itself has taken:
8
+
9
+ | before | after |
10
+ |---|---|
11
+ | `Config.registries` | `Config.registriesByScope` |
12
+ | `Config.namedRegistries` | `Config.registriesByPrefix` |
13
+ | `Config.registryOptions` | `Config.registryOptionsByUrl` |
14
+
15
+ The same rename applies to the `RegistryContext` fields, the `Registries` and `NamedRegistries` types (now `RegistriesByScope` and `RegistriesByPrefix`), `normalizeRegistries` / `normalizeNamedRegistries` (now `normalizeRegistriesByScope` / `normalizeRegistriesByPrefix`), and the `BUILTIN_NAMED_REGISTRIES` constant (now `BUILTIN_REGISTRIES_BY_PREFIX`).
16
+
17
+ This is an internal rename: no setting, error code, lockfile field, or `.pnpmfile.cjs` hook field changes. A `preResolution` hook still reads `ctx.registries`, which is the name pacquet passes as well. The `registries` and `namedRegistries` settings are read under the names users write them.
18
+
19
+ The pnpr resolve request sends `registriesByPrefix` where it sent `namedRegistries`. A pnpr server and its clients must be on matching versions, which is already the case for an experimental server.
20
+
21
+ ### Minor Changes
22
+
23
+ - A registry can now declare that its abbreviated metadata carries the `time` field, so `resolutionMode: time-based` reads the full metadata document only from the registries that need it:
24
+
25
+ ```yaml
26
+ resolutionMode: time-based
27
+ registries:
28
+ https://npm.internal.example/:
29
+ supportsTimeField: true
30
+ ```
31
+
32
+ `registry.npmjs.org` omits `time` from abbreviated metadata, so a time-based resolution has to fall back to the much larger full document. That fallback used to be all-or-nothing: `registrySupportsTimeField` answered for every registry at once, so a project resolving from both the public registry and a Verdaccio instance either paid for full metadata everywhere or claimed a `time` field npmjs does not serve. The answer is now per registry, and `registrySupportsTimeField` remains the answer for every registry that does not declare one.
33
+
34
+ The declaration is also sent to a pnpr server, which applies it to the resolution it runs on the client's behalf.
35
+
36
+ ### Patch Changes
37
+
38
+ - Re-fetch full registry metadata when `minimumReleaseAge` is enabled and an abbreviated packument's `time` map omits timestamps for some versions. This prevents mature versions from being filtered out and resolution from falling back to the lowest matching version [pnpm/pnpm#13741](https://github.com/pnpm/pnpm/issues/13741).
39
+
40
+ - Reduced registry metadata requests during dependency resolution by reusing cached metadata when lockfile preferences prove that no uncached version can win [pnpm/pnpm#13976](https://github.com/pnpm/pnpm/issues/13976).
41
+
42
+ - Installs are faster in workspaces that declare inter-workspace dependencies with plain ranges (`"*"`, `"^1.2.3"`) rather than the `workspace:` protocol. With `preferWorkspacePackages` enabled, linking such a dependency no longer makes a registry request that cannot change the outcome — and workspace packages that were never published no longer cost a 404 on every install.
43
+
44
+ - Added `fetchWarnTimeoutMs` and `fetchMinSpeedKiBps` to the Rust pnpm CLI and its N-API bindings. Slow registry metadata requests and tarball downloads now emit pnpm-compatible warnings without exposing URL credentials, query parameters, fragments, or control characters [pnpm/pnpm#12042](https://github.com/pnpm/pnpm/issues/12042).
45
+
46
+ - `trustPolicy: no-downgrade` no longer aborts the install with `ERR_PNPM_MISSING_TIME` on registries that serve no per-version `time` field when `minimumReleaseAgeIgnoreMissingTime` is set. The trust check reads the same publish dates the `minimumReleaseAge` check does, so it now honors the same opt-in and skips the affected package with a warning [#12446](https://github.com/pnpm/pnpm/issues/12446).
47
+
48
+ `minimumReleaseAgeIgnoreMissingTime` no longer lets a lockfile entry the registry does not list pass the `minimumReleaseAge` check during lockfile verification. The opt-in covers a registry that cannot date its releases; a packument that does date every version it lists is saying it never published this one, which stays a hard failure.
49
+
50
+ The missing-`time` warning now names the check it is reporting on, so a package whose `minimumReleaseAge` and `trustPolicy` checks are both skipped warns about both instead of only the first.
51
+
52
+ - Updated dependencies:
53
+ - @pnpm/config.normalize-registries@1101.0.0
54
+ - @pnpm/config.pick-registry-for-package@1101.0.0
55
+ - @pnpm/config.version-policy@1100.2.1
56
+ - @pnpm/constants@1102.0.0
57
+ - @pnpm/core-loggers@1100.3.3
58
+ - @pnpm/deps.path@1101.0.0
59
+ - @pnpm/error@1100.1.3
60
+ - @pnpm/pkg-manifest.utils@1100.4.1
61
+ - @pnpm/resolving.jsr-specifier-parser@1100.0.6
62
+ - @pnpm/resolving.registry.pkg-metadata-filter@1100.0.17
63
+ - @pnpm/resolving.registry.types@1100.1.10
64
+ - @pnpm/resolving.resolver-base@1101.1.1
65
+ - @pnpm/store.cafs@1100.2.0
66
+ - @pnpm/store.index@1100.2.5
67
+ - @pnpm/types@1102.0.0
68
+
69
+ ## 1103.2.1
70
+
71
+ ### Patch Changes
72
+
73
+ - `pnpm add` no longer re-resolves the dependency graph when `pnpm-lock.yaml` already holds a version satisfying the request — promoting a transitive dependency to a direct one, or adding to a second workspace package what a first one already depends on, now only saves the dependency in `package.json` and records its importer entry. A satisfying locked version is necessary but not sufficient: the install still falls back to a full resolution for a dist tag, an alias, a `workspace:`/`catalog:`/git/tarball specifier, `--save-peer`, an overridden package, a `catalogMode` other than `manual`, and — under `resolutionMode: time-based` or `lowest-direct`, which resolve a direct dependency to the low end of its range — a range several locked versions satisfy.
74
+
75
+ - `resolutionMode` is no longer ignored when `minimumReleaseAge` is in effect. `lowest-direct` and `time-based` pick the lowest satisfying version of a direct dependency again; previously any active release-age cutoff — including the built-in default — silently forced the highest, so `resolutionMode` only worked when `minimumReleaseAge: 0` was set explicitly [#13752](https://github.com/pnpm/pnpm/issues/13752).
76
+
77
+ - Updated dependencies:
78
+ - @pnpm/config.version-policy@1100.2.0
79
+ - @pnpm/error@1100.1.2
80
+ - @pnpm/pkg-manifest.utils@1100.4.0
81
+ - @pnpm/resolving.jsr-specifier-parser@1100.0.5
82
+ - @pnpm/store.cafs@1100.1.19
83
+ - @pnpm/store.index@1100.2.4
84
+
3
85
  ## 1103.2.0
4
86
 
5
87
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  import type { GetAuthHeader } from '@pnpm/fetching.types';
2
2
  import { type ResolutionVerifier } from '@pnpm/resolving.resolver-base';
3
- import type { Registries, TrustPolicy } from '@pnpm/types';
3
+ import type { RegistriesByScope, TrustPolicy } from '@pnpm/types';
4
4
  import type { FetchMetadataFromFromRegistryOptions } from './fetch.js';
5
5
  import { type FetchFullMetadataCachedOptions } from './fetchFullMetadataCached.js';
6
6
  import type { PackageMetaCache } from './pickPackage.js';
@@ -23,12 +23,15 @@ export interface CreateNpmResolutionVerifierOptions {
23
23
  /**
24
24
  * When the registry's metadata lacks the per-version `time` field
25
25
  * (some self-hosted registries strip it), the verifier can't apply
26
- * the maturity cutoff. Set this to `true` to mirror the resolver's
27
- * `pickMatchingVersionFinal` warn-and-skip behavior — the verifier
28
- * passes the entry with a one-time `globalWarn`, instead of failing
29
- * closed. Defaults to `false` so the verifier stays stricter than
30
- * the resolver only when the user has explicitly opted in to the
31
- * skip on the resolver side.
26
+ * the maturity cutoff, and the trust check has no publish order to
27
+ * walk. Set this to `true` to mirror the resolver's warn-and-skip
28
+ * behavior for both — the verifier passes the entry with a one-time
29
+ * `globalWarn`, instead of failing closed. Defaults to `false` so
30
+ * the verifier stays stricter than the resolver only when the user
31
+ * has explicitly opted in to the skip on the resolver side. Scoped
32
+ * to a packument with no usable `time` map: one that dates every
33
+ * version it lists is saying it never published this pin, which
34
+ * fails closed either way.
32
35
  */
33
36
  ignoreMissingTimeField?: boolean;
34
37
  /**
@@ -42,14 +45,14 @@ export interface CreateNpmResolutionVerifierOptions {
42
45
  trustPolicy?: TrustPolicy;
43
46
  trustPolicyExclude?: string[];
44
47
  trustPolicyIgnoreAfter?: number;
45
- registries: Registries;
48
+ registriesByScope: RegistriesByScope;
46
49
  /**
47
- * Registries reached via the named-registry resolver chain (e.g. `gh:` →
50
+ * RegistriesByScope reached via the named-registry resolver chain (e.g. `gh:` →
48
51
  * GitHub Packages). When a lockfile entry's tarball URL falls under one of
49
52
  * these registry base URLs, route the manifest fetch there instead of the
50
53
  * scope-derived default.
51
54
  */
52
- namedRegistries?: Record<string, string>;
55
+ registriesByPrefix?: Record<string, string>;
53
56
  /**
54
57
  * Cache-aware full-metadata fetcher. Decoupled from the resolver pipeline
55
58
  * so abbreviated metadata and `peekManifestFromStore` fast paths cannot
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { normalizeNamedRegistries } from '@pnpm/config.normalize-registries';
2
+ import { normalizeRegistriesByPrefix } from '@pnpm/config.normalize-registries';
3
3
  import { namedRegistryTarballPrefixes, pickRegistryForPackage } from '@pnpm/config.pick-registry-for-package';
4
4
  import { createPackageVersionPolicy } from '@pnpm/config.version-policy';
5
5
  import { FULL_META_DIR } from '@pnpm/constants';
@@ -39,8 +39,8 @@ export function createNpmResolutionVerifier(opts) {
39
39
  const trustExcludePolicy = opts.trustPolicyExclude?.length
40
40
  ? createExcludePolicy(opts.trustPolicyExclude, 'trustPolicyExclude')
41
41
  : undefined;
42
- const mergedNamedRegistries = normalizeNamedRegistries(opts.namedRegistries);
43
- const namedRegistryPrefixes = namedRegistryTarballPrefixes(mergedNamedRegistries);
42
+ const mergedRegistriesByPrefix = normalizeRegistriesByPrefix(opts.registriesByPrefix);
43
+ const namedRegistryPrefixes = namedRegistryTarballPrefixes(mergedRegistriesByPrefix);
44
44
  // Per-install dedup of every network/disk fetch the verifier issues.
45
45
  // The maturity check uses the layered `fetchPublishedAt` lookup; the
46
46
  // trust check uses an attestation fast-path before falling back to
@@ -65,6 +65,7 @@ export function createNpmResolutionVerifier(opts) {
65
65
  const minimumReleaseAge = opts.minimumReleaseAge ?? 0;
66
66
  const trustPolicy = opts.trustPolicy;
67
67
  const trustPolicyIgnoreAfter = opts.trustPolicyIgnoreAfter;
68
+ const ignoreMissingTimeField = opts.ignoreMissingTimeField === true;
68
69
  const verify = async (resolution, { name, version, nonSemverVersion, registryName }) => {
69
70
  if (!isRegistryTarballResolution(resolution))
70
71
  return { ok: true };
@@ -105,20 +106,20 @@ export function createNpmResolutionVerifier(opts) {
105
106
  // Registry-qualified entries name their registry in the dep path, so
106
107
  // routing does not depend on a recorded tarball URL (canonical URLs
107
108
  // are omitted from the lockfile in the 12.0 format).
108
- const namedRegistry = mergedNamedRegistries[registryName];
109
+ const namedRegistry = mergedRegistriesByPrefix[registryName];
109
110
  if (!namedRegistry) {
110
111
  // Fail closed: without the registry URL, none of the metadata-backed
111
112
  // checks below can vouch for this entry.
112
113
  return {
113
114
  ok: false,
114
115
  code: MISSING_NAMED_REGISTRY_VIOLATION_CODE,
115
- reason: `was resolved from the named registry '${registryName}:', which is not present in the namedRegistries setting`,
116
+ reason: `was resolved from the named registry '${registryName}:', which is not present in the registriesByPrefix setting`,
116
117
  };
117
118
  }
118
119
  registry = namedRegistry;
119
120
  }
120
121
  else {
121
- registry = pickRegistryForVersion(opts.registries, namedRegistryPrefixes, name, tarballUrl);
122
+ registry = pickRegistryForVersion(opts.registriesByScope, namedRegistryPrefixes, name, tarballUrl);
122
123
  }
123
124
  // A registry entry that pins an explicit tarball URL must point at the
124
125
  // artifact the registry's own metadata lists. Otherwise a trusted
@@ -138,7 +139,7 @@ export function createNpmResolutionVerifier(opts) {
138
139
  if (!ageApplies && !trustApplies)
139
140
  return { ok: true };
140
141
  if (ageApplies) {
141
- const ageViolation = await runAgeCheck(lookupContext, registry, name, version, cutoff, opts.ignoreMissingTimeField === true);
142
+ const ageViolation = await runAgeCheck(lookupContext, registry, name, version, cutoff, ignoreMissingTimeField);
142
143
  if (ageViolation)
143
144
  return ageViolation;
144
145
  }
@@ -146,6 +147,7 @@ export function createNpmResolutionVerifier(opts) {
146
147
  const trustViolation = await runTrustCheck(lookupContext, registry, name, version, {
147
148
  trustPolicyExclude: trustExcludePolicy,
148
149
  trustPolicyIgnoreAfter,
150
+ ignoreMissingTimeField,
149
151
  });
150
152
  if (trustViolation)
151
153
  return trustViolation;
@@ -163,9 +165,9 @@ export function createNpmResolutionVerifier(opts) {
163
165
  // stays trusted after its exclude entry has been pulled.
164
166
  const sortedMinAgeExcludes = [...new Set(opts.minimumReleaseAgeExclude ?? [])].sort();
165
167
  const sortedTrustExcludes = [...new Set(opts.trustPolicyExclude ?? [])].sort();
166
- const sortedNamedRegistries = Object.fromEntries(Object.entries(mergedNamedRegistries).sort(([aliasA], [aliasB]) => aliasA.localeCompare(aliasB)));
168
+ const sortedRegistriesByPrefix = Object.fromEntries(Object.entries(mergedRegistriesByPrefix).sort(([aliasA], [aliasB]) => aliasA.localeCompare(aliasB)));
167
169
  const namedRegistriesRouting = createHash('sha256')
168
- .update(JSON.stringify(sortedNamedRegistries))
170
+ .update(JSON.stringify(sortedRegistriesByPrefix))
169
171
  .digest('hex');
170
172
  return {
171
173
  verify,
@@ -184,6 +186,7 @@ export function createNpmResolutionVerifier(opts) {
184
186
  trustPolicy: trustPolicy ?? null,
185
187
  trustPolicyExclude: sortedTrustExcludes,
186
188
  trustPolicyIgnoreAfter: trustPolicyIgnoreAfter ?? null,
189
+ minimumReleaseAgeIgnoreMissingTime: ignoreMissingTimeField,
187
190
  },
188
191
  canTrustPastCheck: (cached) => {
189
192
  // The tarball-URL binding is unconditional today; a cached run that
@@ -233,6 +236,14 @@ export function createNpmResolutionVerifier(opts) {
233
236
  const todayIgnoreAfter = trustPolicyIgnoreAfter ?? null;
234
237
  if (pastIgnoreAfter !== todayIgnoreAfter)
235
238
  return false;
239
+ // Missing-time tolerance: a cached run that failed closed on an
240
+ // absent `time` field accepted a subset of what today's tolerant
241
+ // policy accepts, so it stays trustworthy. Turning the tolerance
242
+ // off invalidates it — entries the past run waved through are the
243
+ // ones today's policy exists to reject. Older records (no field)
244
+ // read as intolerant, which is the safe direction.
245
+ if (cached.minimumReleaseAgeIgnoreMissingTime === true && !ignoreMissingTimeField)
246
+ return false;
236
247
  return true;
237
248
  },
238
249
  };
@@ -245,15 +256,23 @@ async function runAgeCheck(context, registry, name, version, cutoff, ignoreMissi
245
256
  const published = await fetchPublishedAt(context, registry, name, version);
246
257
  if (!published) {
247
258
  // No source — attestation, local mirror, or full metadata —
248
- // surfaced a publish timestamp for this version. The resolver's
249
- // pickMatchingVersionFinal honors `minimumReleaseAgeIgnoreMissingTime`
250
- // for the same shape (some self-hosted registries strip per-version
251
- // `time`); the verifier mirrors that so it can't be stricter than
252
- // fresh resolution. Without the flag we still fail closed — better
253
- // a false reject than silent bypass when the user hasn't opted in.
259
+ // surfaced a publish timestamp for this version. What
260
+ // `minimumReleaseAgeIgnoreMissingTime` opts out of is a registry that
261
+ // cannot date its releases, so the skip is granted only when the
262
+ // packument carries no usable `time` map at all the same shape the
263
+ // resolver's `pickMatchingVersionFinal` warns and skips on, so the
264
+ // verifier can't be stricter than fresh resolution. A packument that
265
+ // does date every version it lists is instead telling us this pin is
266
+ // not one of them (`dropIncompletePublishTimes` leaves no partial maps
267
+ // for that to be ambiguous), and an unpublished or never-published pin
268
+ // must fail closed however the flag is set.
254
269
  if (ignoreMissingTimeField) {
255
- warnMissingTimeFieldOnce(name);
256
- return undefined;
270
+ // Already awaited by the lookup above, so this is a cache hit.
271
+ const timeMap = await fetchFullMetaTime(context, registry, name);
272
+ if (timeMap == null) {
273
+ warnMissingTimeFieldOnce(name, 'minimumReleaseAge');
274
+ return undefined;
275
+ }
257
276
  }
258
277
  return {
259
278
  ok: false,
@@ -315,10 +334,9 @@ async function runTarballUrlCheck(context, registry, name, version, lockfileTarb
315
334
  function sameTarballUrl(a, b) {
316
335
  return canonicalTarballUrl(a) === canonicalTarballUrl(b);
317
336
  }
318
- // Mirror the tolerance toLockfileResolution applies when it decides whether
319
- // a tarball URL is "the expected one": ignore the protocol and `%2f` scope
320
- // encoding so a benign http/https or encoding difference isn't read as
321
- // tampering. The `%2f` match is case-insensitive because `normalizeRegistryUrl`
337
+ // Both URLs come from the registry, so ignore the protocol and `%2f` scope
338
+ // encoding: a benign http/https or encoding difference isn't tampering. The
339
+ // `%2f` match is case-insensitive because `normalizeRegistryUrl`
322
340
  // (`new URL().toString()`) can upper-case percent-escapes to `%2F`.
323
341
  function canonicalTarballUrl(url) {
324
342
  const normalized = normalizeRegistryUrl(url).replace(/%2f/gi, '/');
@@ -657,7 +675,7 @@ function fetchFullMetaTime(context, registry, name) {
657
675
  }
658
676
  return cachedPromise;
659
677
  }
660
- function pickRegistryForVersion(registries, namedRegistryPrefixes, name, tarballUrl) {
678
+ function pickRegistryForVersion(registriesByScope, namedRegistryPrefixes, name, tarballUrl) {
661
679
  // If the lockfile records where the tarball lives, prefer that — scope
662
680
  // routing (`@scope:registry`) only covers scoped packages, but named
663
681
  // registries (`gh:`, `jsr:` aliases, custom) ship un-scoped packages whose
@@ -674,7 +692,7 @@ function pickRegistryForVersion(registries, namedRegistryPrefixes, name, tarball
674
692
  return prefix;
675
693
  }
676
694
  }
677
- return pickRegistryForPackage(registries, name);
695
+ return pickRegistryForPackage(registriesByScope, name);
678
696
  }
679
697
  function tryParseUrl(url) {
680
698
  try {
package/lib/fetch.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import url from 'node:url';
2
2
  import util from 'node:util';
3
3
  import { requestRetryLogger } from '@pnpm/core-loggers';
4
- import { FetchError, PnpmError, redactUrlCredentials, } from '@pnpm/error';
4
+ import { FetchError, PnpmError, redactUrlCredentials, redactUrlForDisplay, } from '@pnpm/error';
5
5
  import { globalWarn } from '@pnpm/logger';
6
6
  import * as retry from '@zkochan/retry';
7
7
  import semver from 'semver';
8
8
  import { clearMeta } from './clearMeta.js';
9
+ import { dropIncompletePublishTimes } from './publishTimes.js';
9
10
  /**
10
11
  * Content type of an abbreviated (install-oriented) package metadata document.
11
12
  * A spec-compliant registry echoes this in the response `Content-Type` when it
@@ -147,10 +148,11 @@ export async function fetchMetadataFromFromRegistry(fetchOpts, pkgName, { authHe
147
148
  try {
148
149
  const jsonText = await response.text();
149
150
  const meta = JSON.parse(jsonText);
151
+ dropIncompletePublishTimes(meta);
150
152
  // Check if request took longer than expected
151
153
  const elapsedMs = Date.now() - startTime;
152
154
  if (elapsedMs > fetchOpts.fetchWarnTimeoutMs) {
153
- globalWarn(`Request took ${elapsedMs}ms: ${uri}`);
155
+ globalWarn(`Request took ${elapsedMs}ms: ${redactUrlForDisplay(uri)}`);
154
156
  }
155
157
  resolve({
156
158
  ...normalizeAbbreviatedResponse({ fullMetadata, meta, jsonText, response }),
@@ -200,7 +202,7 @@ export function notModifiedWithoutCacheError(pkgName) {
200
202
  * carry the megabytes of install-irrelevant data (scripts, exports, readme,
201
203
  * custom fields) that a full document contains.
202
204
  *
203
- * Registries that honor the header (e.g. the npm registry) echo the abbreviated
205
+ * RegistriesByScope that honor the header (e.g. the npm registry) echo the abbreviated
204
206
  * `Content-Type`, so this is a no-op for them: no re-serialization, no field
205
207
  * stripping — the happy path pays nothing.
206
208
  */
package/lib/index.d.ts CHANGED
@@ -2,9 +2,9 @@ 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, RangeSpecStyle, Registries, TrustPolicy } from '@pnpm/types';
5
+ import type { DependencyManifest, PackageVersionPolicy, RangeSpecStyle, RegistriesByScope, TrustPolicy } from '@pnpm/types';
6
6
  import { fetchMetadataFromFromRegistry, type FetchMetadataFromFromRegistryOptions, RegistryResponseError } from './fetch.js';
7
- import { BUILTIN_NAMED_REGISTRIES, parseBareSpecifier, type RegistryPackageSpec } from './parseBareSpecifier.js';
7
+ import { BUILTIN_REGISTRIES_BY_PREFIX, parseBareSpecifier, type RegistryPackageSpec } from './parseBareSpecifier.js';
8
8
  import { type PackageMetaCache, pickPackage, type PickPackageOptions } from './pickPackage.js';
9
9
  import { pickPackageFromMeta, pickVersionByVersionRange } from './pickPackageFromMeta.js';
10
10
  import { workspacePrefToNpm } from './workspacePrefToNpm.js';
@@ -18,22 +18,28 @@ export declare class NoMatchingVersionError extends PnpmError {
18
18
  constructor(opts: NoMatchingVersionErrorOptions);
19
19
  }
20
20
  export declare function formatTimeAgo(date: Date): string | null;
21
- export { BUILTIN_NAMED_REGISTRIES, fetchMetadataFromFromRegistry, type FetchMetadataFromFromRegistryOptions, type PackageMeta, type PackageMetaCache, parseBareSpecifier, pickPackageFromMeta, pickVersionByVersionRange, type RegistryPackageSpec, RegistryResponseError, workspacePrefToNpm, };
21
+ export { BUILTIN_REGISTRIES_BY_PREFIX, 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';
24
23
  export { MINIMUM_RELEASE_AGE_VIOLATION_CODE, TRUST_DOWNGRADE_VIOLATION_CODE, } from './violationCodes.js';
25
24
  export interface ResolverFactoryOptions {
26
25
  cacheDir: string;
27
26
  storeDir?: string;
28
27
  frozenStore?: boolean;
29
28
  fullMetadata?: boolean;
29
+ /**
30
+ * Asked instead of {@link ResolverFactoryOptions.fullMetadata} when the
31
+ * caller can answer per registry — a registry that declares
32
+ * `supportsTimeField` needs no full metadata for a time-based resolution
33
+ * even when the others do.
34
+ */
35
+ needsFullMetadataFor?: (registry: string) => boolean;
30
36
  filterMetadata?: boolean;
31
37
  offline?: boolean;
32
38
  preferOffline?: boolean;
33
39
  retry?: RetryTimeoutOptions;
34
40
  timeout?: number;
35
- registries: Registries;
36
- namedRegistries?: Record<string, string>;
41
+ registriesByScope: RegistriesByScope;
42
+ registriesByPrefix?: Record<string, string>;
37
43
  saveWorkspaceProtocol?: boolean | 'rolling';
38
44
  preserveAbsolutePaths?: boolean;
39
45
  ignoreMissingTimeField?: boolean;
@@ -84,10 +90,16 @@ export declare function createNpmResolver(fetchFromRegistry: FetchFromRegistry,
84
90
  export interface ResolveFromNpmContext {
85
91
  pickPackage: (spec: RegistryPackageSpec, opts: PickPackageOptions) => ReturnType<typeof pickPackage>;
86
92
  getAuthHeaderValueByURI: GetAuthHeader;
87
- registries: Registries;
88
- namedRegistries: Record<string, string>;
93
+ registriesByScope: RegistriesByScope;
94
+ registriesByPrefix: Record<string, string>;
89
95
  namedRegistryNames: ReadonlySet<string>;
90
96
  saveWorkspaceProtocol?: boolean | 'rolling';
97
+ /**
98
+ * The `minimumReleaseAgeIgnoreMissingTime` opt-in, reaching the trust
99
+ * check as well as the version pick: both read the same per-version
100
+ * `time`, so a registry that strips it takes both down together.
101
+ */
102
+ ignoreMissingTimeField?: boolean;
91
103
  peekManifestFromStore?: (opts: {
92
104
  id: PkgResolutionId;
93
105
  integrity: string;
package/lib/index.js CHANGED
@@ -3,7 +3,7 @@ import { pickRegistryForPackage } from '@pnpm/config.pick-registry-for-package';
3
3
  import { isWellFormedRegistryName, RESERVED_VERSION_PREFIXES } from '@pnpm/deps.path';
4
4
  import { PnpmError } from '@pnpm/error';
5
5
  import { globalWarn } from '@pnpm/logger';
6
- import { rangeSpecGranularity, versionWithRangeSpecStyle } from '@pnpm/pkg-manifest.utils';
6
+ import { calcVersionRange, inferRangeSpecStyle, rangeSpecGranularity, versionWithRangeSpecStyle } from '@pnpm/pkg-manifest.utils';
7
7
  import { EXISTING_VERSION_SELECTOR_WEIGHT, } from '@pnpm/resolving.resolver-base';
8
8
  import { storeIndexKey } from '@pnpm/store.index';
9
9
  import { readPkgFromCafs, } from '@pnpm/worker';
@@ -16,10 +16,9 @@ import ssri from 'ssri';
16
16
  import versionSelectorType from 'version-selector-type';
17
17
  import { clearMeta, retainsFullMeta } from './clearMeta.js';
18
18
  import { fetchMetadataFromFromRegistry, RegistryResponseError } from './fetch.js';
19
- import { inferRangeSpecStyle } from './inferRangeSpecStyle.js';
20
19
  import { memoizeFetchMetadata } from './memoizeFetchMetadata.js';
21
20
  import { normalizeRegistryUrl } from './normalizeRegistryUrl.js';
22
- import { BUILTIN_NAMED_REGISTRIES, parseBareSpecifier, parseJsrSpecifierToRegistryPackageSpec, parseNamedRegistrySpecifierToRegistryPackageSpec, } from './parseBareSpecifier.js';
21
+ import { BUILTIN_REGISTRIES_BY_PREFIX, parseBareSpecifier, parseJsrSpecifierToRegistryPackageSpec, parseNamedRegistrySpecifierToRegistryPackageSpec, } from './parseBareSpecifier.js';
23
22
  import { pickPackage, } from './pickPackage.js';
24
23
  import { applyPublishedByPolicy, pickPackageFromMeta, pickVersionByVersionRange } from './pickPackageFromMeta.js';
25
24
  import { failIfTrustDowngraded } from './trustChecks.js';
@@ -64,9 +63,8 @@ export function formatTimeAgo(date) {
64
63
  return `${diffMin} minute${diffMin === 1 ? '' : 's'} ago`;
65
64
  return 'a few seconds ago';
66
65
  }
67
- export { BUILTIN_NAMED_REGISTRIES, fetchMetadataFromFromRegistry, parseBareSpecifier, pickPackageFromMeta, pickVersionByVersionRange, RegistryResponseError, workspacePrefToNpm, };
66
+ export { BUILTIN_REGISTRIES_BY_PREFIX, fetchMetadataFromFromRegistry, parseBareSpecifier, pickPackageFromMeta, pickVersionByVersionRange, RegistryResponseError, workspacePrefToNpm, };
68
67
  export { createNpmResolutionVerifier } from './createNpmResolutionVerifier.js';
69
- export { inferRangeSpecStyle } from './inferRangeSpecStyle.js';
70
68
  export { MINIMUM_RELEASE_AGE_VIOLATION_CODE, TRUST_DOWNGRADE_VIOLATION_CODE, } from './violationCodes.js';
71
69
  export function createNpmResolver(fetchFromRegistry, getAuthHeader, opts) {
72
70
  if (typeof opts.cacheDir !== 'string') {
@@ -90,6 +88,10 @@ export function createNpmResolver(fetchFromRegistry, getAuthHeader, opts) {
90
88
  // using.
91
89
  const ownsMetaCache = opts.metaCache == null;
92
90
  const metaCache = opts.metaCache ?? createDefaultPackageMetaCache();
91
+ // This marker is intentionally resolver-scoped rather than attached to
92
+ // `metaCache`: callers may reuse one metadata cache across installs, and a
93
+ // later install must retry a full-metadata upgrade that previously got 304.
94
+ const releaseAgeUpgradeCheckedPackuments = new WeakSet();
93
95
  // Create peek function if storeDir is provided
94
96
  const storeDir = opts.storeDir;
95
97
  const peekLockerForPeek = new Map();
@@ -116,31 +118,34 @@ export function createNpmResolver(fetchFromRegistry, getAuthHeader, opts) {
116
118
  return request;
117
119
  };
118
120
  }
119
- const namedRegistries = mergeNamedRegistries(opts.namedRegistries);
120
- const namedRegistryNames = new Set(Object.keys(namedRegistries));
121
+ const registriesByPrefix = mergeNamedRegistries(opts.registriesByPrefix);
122
+ const namedRegistryNames = new Set(Object.keys(registriesByPrefix));
121
123
  const ctx = {
122
124
  getAuthHeaderValueByURI: getAuthHeader,
123
125
  pickPackage: pickPackage.bind(null, {
124
126
  fetch,
125
127
  fullMetadata: opts.fullMetadata,
128
+ needsFullMetadataFor: opts.needsFullMetadataFor,
126
129
  filterMetadata: opts.filterMetadata,
127
130
  metaCache,
128
131
  offline: opts.offline,
129
132
  preferOffline: opts.preferOffline,
130
133
  cacheDir: opts.cacheDir,
131
134
  ignoreMissingTimeField: opts.ignoreMissingTimeField,
135
+ releaseAgeUpgradeCheckedPackuments,
132
136
  }),
133
- registries: opts.registries,
134
- namedRegistries,
137
+ registriesByScope: opts.registriesByScope,
138
+ registriesByPrefix,
135
139
  namedRegistryNames,
136
140
  saveWorkspaceProtocol: opts.saveWorkspaceProtocol,
141
+ ignoreMissingTimeField: opts.ignoreMissingTimeField,
137
142
  peekManifestFromStore,
138
143
  warnedHeldBackUpdates: new Set(),
139
144
  };
140
145
  const boundResolveFromNpm = resolveNpm.bind(null, ctx);
141
146
  const boundResolveFromJsr = resolveJsr.bind(null, ctx);
142
147
  const boundResolveFromNamedRegistry = resolveFromNamedRegistry.bind(null, ctx);
143
- const defaultRegistry = opts.registries.default;
148
+ const defaultRegistry = opts.registriesByScope.default;
144
149
  return {
145
150
  resolveFromNpm: boundResolveFromNpm,
146
151
  resolveFromJsr: boundResolveFromJsr,
@@ -306,8 +311,8 @@ function createResolveLatest(resolve, matches) {
306
311
  async function resolveNpm(ctx, wantedDependency, opts) {
307
312
  const defaultTag = opts.defaultTag ?? 'latest';
308
313
  const registry = wantedDependency.alias
309
- ? pickRegistryForPackage(ctx.registries, wantedDependency.alias, wantedDependency.bareSpecifier)
310
- : ctx.registries.default;
314
+ ? pickRegistryForPackage(ctx.registriesByScope, wantedDependency.alias, wantedDependency.bareSpecifier)
315
+ : ctx.registriesByScope.default;
311
316
  if (wantedDependency.bareSpecifier?.startsWith('workspace:')) {
312
317
  if (wantedDependency.bareSpecifier.startsWith('workspace:.'))
313
318
  return null;
@@ -381,6 +386,34 @@ async function resolveNpm(ctx, wantedDependency, opts) {
381
386
  }
382
387
  }
383
388
  }
389
+ // This runs *after* the store peek because a tag-specified dep whose only local copy is a
390
+ // prerelease reaches here with `update: false` (`wantedDepIsLocallyAvailable` ignores
391
+ // prereleases for tags, `pickMatchingLocalVersionOrNull` does not), and the peek must keep
392
+ // winning there. `update` is deliberately absent from the guard: that same helper forces it
393
+ // on for exactly these deps, so excluding it would make this block unreachable.
394
+ if (opts.preferWorkspacePackages === true &&
395
+ workspacePackages != null &&
396
+ opts.projectDir &&
397
+ opts.trustPolicy !== 'no-downgrade' &&
398
+ !opts.updateChecksums &&
399
+ opts.injectWorkspacePackages !== true &&
400
+ !wantedDependency.injected) {
401
+ const workspacePkgsMatchingName = workspacePackages.get(spec.name);
402
+ if (workspacePkgsMatchingName?.size === 1) {
403
+ const localVersion = pickMatchingLocalVersionOrNull(workspacePkgsMatchingName, spec);
404
+ if (localVersion != null) {
405
+ return resolveFromLocalPackage(workspacePkgsMatchingName.get(localVersion), spec, {
406
+ wantedDependency,
407
+ projectDir: opts.projectDir,
408
+ lockfileDir: opts.lockfileDir,
409
+ hardLinkLocalPackages: false,
410
+ saveWorkspaceProtocol: ctx.saveWorkspaceProtocol,
411
+ calcSpecifier: opts.calcSpecifier,
412
+ rangeSpecStyle: opts.rangeSpecStyle,
413
+ });
414
+ }
415
+ }
416
+ }
384
417
  const authHeaderValue = ctx.getAuthHeaderValueByURI(registry, { pkgName: spec.name });
385
418
  let pickResult;
386
419
  try {
@@ -395,6 +428,7 @@ async function resolveNpm(ctx, wantedDependency, opts) {
395
428
  includeLatestTag: opts.update === 'latest',
396
429
  updateChecksums: opts.updateChecksums,
397
430
  optional: wantedDependency.optional,
431
+ trustPolicy: opts.trustPolicy,
398
432
  });
399
433
  }
400
434
  catch (err) { // eslint-disable-line
@@ -450,7 +484,11 @@ async function resolveNpm(ctx, wantedDependency, opts) {
450
484
  throw new NoMatchingVersionError({ wantedDependency, packageMeta: meta, registry });
451
485
  }
452
486
  else if (opts.trustPolicy === 'no-downgrade') {
453
- failIfTrustDowngraded(meta, pickedPackage.version, opts);
487
+ failIfTrustDowngraded(meta, pickedPackage.version, {
488
+ trustPolicyExclude: opts.trustPolicyExclude,
489
+ trustPolicyIgnoreAfter: opts.trustPolicyIgnoreAfter,
490
+ ignoreMissingTimeField: ctx.ignoreMissingTimeField,
491
+ });
454
492
  }
455
493
  const latest = latestAllowedByPolicy(meta, opts);
456
494
  const workspacePkgsMatchingName = workspacePackages?.get(pickedPackage.name);
@@ -526,7 +564,7 @@ async function resolveJsr(ctx, wantedDependency, opts) {
526
564
  const spec = parseJsrSpecifierToRegistryPackageSpec(wantedDependency.bareSpecifier, wantedDependency.alias, opts.defaultTag ?? 'latest');
527
565
  if (spec == null)
528
566
  return null;
529
- const picked = await pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, ctx.registries['@jsr']); // '@jsr' is always defined
567
+ const picked = await pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, ctx.registriesByScope['@jsr']); // '@jsr' is always defined
530
568
  return {
531
569
  ...picked,
532
570
  normalizedBareSpecifier: opts.calcSpecifier
@@ -545,14 +583,14 @@ async function resolveJsr(ctx, wantedDependency, opts) {
545
583
  // another specifier scheme (e.g. `git`, `github`, `jsr`) is silently shadowed
546
584
  // by that scheme's dedicated resolver — no cross-resolver knowledge needed.
547
585
  function mergeNamedRegistries(userDefined) {
548
- const merged = { ...BUILTIN_NAMED_REGISTRIES };
586
+ const merged = { ...BUILTIN_REGISTRIES_BY_PREFIX };
549
587
  if (!userDefined)
550
588
  return merged;
551
589
  for (const [alias, url] of Object.entries(userDefined)) {
552
590
  if (RESERVED_VERSION_PREFIXES.has(alias) || !isWellFormedRegistryName(alias)) {
553
591
  throw new PnpmError('RESERVED_NAMED_REGISTRY_NAME', RESERVED_VERSION_PREFIXES.has(alias)
554
592
  ? `'${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.' });
593
+ : `'${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 registriesByPrefix setting.' });
556
594
  }
557
595
  if (typeof url !== 'string' || !isValidHttpUrl(url)) {
558
596
  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/' });
@@ -573,7 +611,7 @@ function isValidHttpUrl(url) {
573
611
  // Resolves a `<alias>:` specifier from one of the configured named registries.
574
612
  // The `gh:` alias ships as a built-in default pointing at the GitHub Packages
575
613
  // npm registry; additional aliases come from pnpm-workspace.yaml's
576
- // `namedRegistries` field. Auth tokens are looked up by the resolved registry
614
+ // `registriesByPrefix` field. Auth tokens are looked up by the resolved registry
577
615
  // URL, so a `//npm.pkg.github.com/:_authToken=...` entry in `.npmrc` is
578
616
  // picked up automatically for `gh:` specifiers (and analogously for any user-
579
617
  // configured alias).
@@ -583,7 +621,7 @@ async function resolveFromNamedRegistry(ctx, wantedDependency, opts) {
583
621
  const spec = parseNamedRegistrySpecifierToRegistryPackageSpec(wantedDependency.bareSpecifier, ctx.namedRegistryNames, wantedDependency.alias, opts.defaultTag ?? 'latest');
584
622
  if (spec == null)
585
623
  return null;
586
- const registry = ctx.namedRegistries[spec.registryName];
624
+ const registry = ctx.registriesByPrefix[spec.registryName];
587
625
  if (!registry)
588
626
  return null; // defensive: should never trigger because parse checks the alias set
589
627
  const picked = await pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, registry);
@@ -621,6 +659,7 @@ async function pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, registr
621
659
  includeLatestTag: opts.update === 'latest',
622
660
  updateChecksums: opts.updateChecksums,
623
661
  optional: wantedDependency.optional,
662
+ trustPolicy: opts.trustPolicy,
624
663
  });
625
664
  if (pickedPackage == null) {
626
665
  throw new NoMatchingVersionError({ wantedDependency, packageMeta: meta, registry });
@@ -666,14 +705,13 @@ function calcSpecifier({ wantedDependency, spec, version, defaultRangeSpecStyle,
666
705
  return range;
667
706
  return `npm:${spec.name}@${range}`;
668
707
  }
708
+ /** The manifest range `version` is saved as; see {@link calcVersionRange}. */
669
709
  function calcRange(version, wantedDependency, defaultRangeSpecStyle) {
670
- if (semver.parse(version)?.prerelease.length) {
671
- return version;
672
- }
673
- const rangeSpecStyle = (wantedDependency.prevSpecifier ? inferRangeSpecStyle(wantedDependency.prevSpecifier) : undefined) ??
674
- (wantedDependency.bareSpecifier ? inferRangeSpecStyle(wantedDependency.bareSpecifier) : undefined) ??
675
- defaultRangeSpecStyle;
676
- return versionWithRangeSpecStyle(version, rangeSpecStyle ?? 'major');
710
+ return calcVersionRange(version, {
711
+ prevSpecifier: wantedDependency.prevSpecifier,
712
+ bareSpecifier: wantedDependency.bareSpecifier,
713
+ defaultRangeSpecStyle,
714
+ });
677
715
  }
678
716
  function tryResolveFromWorkspace(wantedDependency, opts) {
679
717
  if (!wantedDependency.bareSpecifier?.startsWith('workspace:')) {
@@ -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 { BUILTIN_NAMED_REGISTRIES } from '@pnpm/constants';
12
+ export { BUILTIN_REGISTRIES_BY_PREFIX } from '@pnpm/constants';
13
13
  export interface NamedRegistryPackageSpec extends RegistryPackageSpec {
14
14
  registryName: string;
15
15
  }
@@ -64,7 +64,7 @@ export function parseJsrSpecifierToRegistryPackageSpec(rawSpecifier, alias, defa
64
64
  jsrPkgName: spec.jsrPkgName,
65
65
  };
66
66
  }
67
- export { BUILTIN_NAMED_REGISTRIES } from '@pnpm/constants';
67
+ export { BUILTIN_REGISTRIES_BY_PREFIX } from '@pnpm/constants';
68
68
  // Parses a named-registry specifier of the shape `<alias>:<body>` into a
69
69
  // RegistryPackageSpec. Returns `null` when the specifier does not use one of
70
70
  // the configured aliases, so the caller can fall through to other resolvers.