@pnpm/resolving.npm-resolver 1103.2.1 → 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 +66 -0
- package/lib/createNpmResolutionVerifier.d.ts +13 -10
- package/lib/createNpmResolutionVerifier.js +41 -23
- package/lib/fetch.js +5 -3
- package/lib/index.d.ts +20 -7
- package/lib/index.js +56 -15
- package/lib/parseBareSpecifier.d.ts +1 -1
- package/lib/parseBareSpecifier.js +1 -1
- package/lib/pickPackage.d.ts +20 -1
- package/lib/pickPackage.js +100 -13
- package/lib/pickPackageFromMeta.d.ts +7 -1
- package/lib/pickPackageFromMeta.js +82 -3
- package/lib/publishTimes.d.ts +24 -0
- package/lib/publishTimes.js +35 -0
- package/lib/trustChecks.d.ts +14 -0
- package/lib/trustChecks.js +5 -0
- package/package.json +19 -19
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,71 @@
|
|
|
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
|
+
|
|
3
69
|
## 1103.2.1
|
|
4
70
|
|
|
5
71
|
### Patch 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 {
|
|
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
|
|
27
|
-
* `
|
|
28
|
-
* passes the entry with a one-time
|
|
29
|
-
* closed. Defaults to `false` so
|
|
30
|
-
* the resolver only when the user
|
|
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
|
-
|
|
48
|
+
registriesByScope: RegistriesByScope;
|
|
46
49
|
/**
|
|
47
|
-
*
|
|
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
|
-
|
|
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 {
|
|
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
|
|
43
|
-
const namedRegistryPrefixes = namedRegistryTarballPrefixes(
|
|
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 =
|
|
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
|
|
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.
|
|
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,
|
|
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
|
|
168
|
+
const sortedRegistriesByPrefix = Object.fromEntries(Object.entries(mergedRegistriesByPrefix).sort(([aliasA], [aliasB]) => aliasA.localeCompare(aliasB)));
|
|
167
169
|
const namedRegistriesRouting = createHash('sha256')
|
|
168
|
-
.update(JSON.stringify(
|
|
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.
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
// `time`
|
|
252
|
-
//
|
|
253
|
-
//
|
|
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
|
-
|
|
256
|
-
|
|
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
|
-
//
|
|
319
|
-
// a
|
|
320
|
-
//
|
|
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(
|
|
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(
|
|
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
|
-
*
|
|
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,
|
|
5
|
+
import type { DependencyManifest, PackageVersionPolicy, RangeSpecStyle, RegistriesByScope, TrustPolicy } from '@pnpm/types';
|
|
6
6
|
import { fetchMetadataFromFromRegistry, type FetchMetadataFromFromRegistryOptions, RegistryResponseError } from './fetch.js';
|
|
7
|
-
import {
|
|
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,7 +18,7 @@ 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 {
|
|
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
23
|
export { MINIMUM_RELEASE_AGE_VIOLATION_CODE, TRUST_DOWNGRADE_VIOLATION_CODE, } from './violationCodes.js';
|
|
24
24
|
export interface ResolverFactoryOptions {
|
|
@@ -26,13 +26,20 @@ export interface ResolverFactoryOptions {
|
|
|
26
26
|
storeDir?: string;
|
|
27
27
|
frozenStore?: boolean;
|
|
28
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;
|
|
29
36
|
filterMetadata?: boolean;
|
|
30
37
|
offline?: boolean;
|
|
31
38
|
preferOffline?: boolean;
|
|
32
39
|
retry?: RetryTimeoutOptions;
|
|
33
40
|
timeout?: number;
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
registriesByScope: RegistriesByScope;
|
|
42
|
+
registriesByPrefix?: Record<string, string>;
|
|
36
43
|
saveWorkspaceProtocol?: boolean | 'rolling';
|
|
37
44
|
preserveAbsolutePaths?: boolean;
|
|
38
45
|
ignoreMissingTimeField?: boolean;
|
|
@@ -83,10 +90,16 @@ export declare function createNpmResolver(fetchFromRegistry: FetchFromRegistry,
|
|
|
83
90
|
export interface ResolveFromNpmContext {
|
|
84
91
|
pickPackage: (spec: RegistryPackageSpec, opts: PickPackageOptions) => ReturnType<typeof pickPackage>;
|
|
85
92
|
getAuthHeaderValueByURI: GetAuthHeader;
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
registriesByScope: RegistriesByScope;
|
|
94
|
+
registriesByPrefix: Record<string, string>;
|
|
88
95
|
namedRegistryNames: ReadonlySet<string>;
|
|
89
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;
|
|
90
103
|
peekManifestFromStore?: (opts: {
|
|
91
104
|
id: PkgResolutionId;
|
|
92
105
|
integrity: string;
|
package/lib/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import { clearMeta, retainsFullMeta } from './clearMeta.js';
|
|
|
18
18
|
import { fetchMetadataFromFromRegistry, RegistryResponseError } from './fetch.js';
|
|
19
19
|
import { memoizeFetchMetadata } from './memoizeFetchMetadata.js';
|
|
20
20
|
import { normalizeRegistryUrl } from './normalizeRegistryUrl.js';
|
|
21
|
-
import {
|
|
21
|
+
import { BUILTIN_REGISTRIES_BY_PREFIX, parseBareSpecifier, parseJsrSpecifierToRegistryPackageSpec, parseNamedRegistrySpecifierToRegistryPackageSpec, } from './parseBareSpecifier.js';
|
|
22
22
|
import { pickPackage, } from './pickPackage.js';
|
|
23
23
|
import { applyPublishedByPolicy, pickPackageFromMeta, pickVersionByVersionRange } from './pickPackageFromMeta.js';
|
|
24
24
|
import { failIfTrustDowngraded } from './trustChecks.js';
|
|
@@ -63,7 +63,7 @@ export function formatTimeAgo(date) {
|
|
|
63
63
|
return `${diffMin} minute${diffMin === 1 ? '' : 's'} ago`;
|
|
64
64
|
return 'a few seconds ago';
|
|
65
65
|
}
|
|
66
|
-
export {
|
|
66
|
+
export { BUILTIN_REGISTRIES_BY_PREFIX, fetchMetadataFromFromRegistry, parseBareSpecifier, pickPackageFromMeta, pickVersionByVersionRange, RegistryResponseError, workspacePrefToNpm, };
|
|
67
67
|
export { createNpmResolutionVerifier } from './createNpmResolutionVerifier.js';
|
|
68
68
|
export { MINIMUM_RELEASE_AGE_VIOLATION_CODE, TRUST_DOWNGRADE_VIOLATION_CODE, } from './violationCodes.js';
|
|
69
69
|
export function createNpmResolver(fetchFromRegistry, getAuthHeader, opts) {
|
|
@@ -88,6 +88,10 @@ export function createNpmResolver(fetchFromRegistry, getAuthHeader, opts) {
|
|
|
88
88
|
// using.
|
|
89
89
|
const ownsMetaCache = opts.metaCache == null;
|
|
90
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();
|
|
91
95
|
// Create peek function if storeDir is provided
|
|
92
96
|
const storeDir = opts.storeDir;
|
|
93
97
|
const peekLockerForPeek = new Map();
|
|
@@ -114,31 +118,34 @@ export function createNpmResolver(fetchFromRegistry, getAuthHeader, opts) {
|
|
|
114
118
|
return request;
|
|
115
119
|
};
|
|
116
120
|
}
|
|
117
|
-
const
|
|
118
|
-
const namedRegistryNames = new Set(Object.keys(
|
|
121
|
+
const registriesByPrefix = mergeNamedRegistries(opts.registriesByPrefix);
|
|
122
|
+
const namedRegistryNames = new Set(Object.keys(registriesByPrefix));
|
|
119
123
|
const ctx = {
|
|
120
124
|
getAuthHeaderValueByURI: getAuthHeader,
|
|
121
125
|
pickPackage: pickPackage.bind(null, {
|
|
122
126
|
fetch,
|
|
123
127
|
fullMetadata: opts.fullMetadata,
|
|
128
|
+
needsFullMetadataFor: opts.needsFullMetadataFor,
|
|
124
129
|
filterMetadata: opts.filterMetadata,
|
|
125
130
|
metaCache,
|
|
126
131
|
offline: opts.offline,
|
|
127
132
|
preferOffline: opts.preferOffline,
|
|
128
133
|
cacheDir: opts.cacheDir,
|
|
129
134
|
ignoreMissingTimeField: opts.ignoreMissingTimeField,
|
|
135
|
+
releaseAgeUpgradeCheckedPackuments,
|
|
130
136
|
}),
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
registriesByScope: opts.registriesByScope,
|
|
138
|
+
registriesByPrefix,
|
|
133
139
|
namedRegistryNames,
|
|
134
140
|
saveWorkspaceProtocol: opts.saveWorkspaceProtocol,
|
|
141
|
+
ignoreMissingTimeField: opts.ignoreMissingTimeField,
|
|
135
142
|
peekManifestFromStore,
|
|
136
143
|
warnedHeldBackUpdates: new Set(),
|
|
137
144
|
};
|
|
138
145
|
const boundResolveFromNpm = resolveNpm.bind(null, ctx);
|
|
139
146
|
const boundResolveFromJsr = resolveJsr.bind(null, ctx);
|
|
140
147
|
const boundResolveFromNamedRegistry = resolveFromNamedRegistry.bind(null, ctx);
|
|
141
|
-
const defaultRegistry = opts.
|
|
148
|
+
const defaultRegistry = opts.registriesByScope.default;
|
|
142
149
|
return {
|
|
143
150
|
resolveFromNpm: boundResolveFromNpm,
|
|
144
151
|
resolveFromJsr: boundResolveFromJsr,
|
|
@@ -304,8 +311,8 @@ function createResolveLatest(resolve, matches) {
|
|
|
304
311
|
async function resolveNpm(ctx, wantedDependency, opts) {
|
|
305
312
|
const defaultTag = opts.defaultTag ?? 'latest';
|
|
306
313
|
const registry = wantedDependency.alias
|
|
307
|
-
? pickRegistryForPackage(ctx.
|
|
308
|
-
: ctx.
|
|
314
|
+
? pickRegistryForPackage(ctx.registriesByScope, wantedDependency.alias, wantedDependency.bareSpecifier)
|
|
315
|
+
: ctx.registriesByScope.default;
|
|
309
316
|
if (wantedDependency.bareSpecifier?.startsWith('workspace:')) {
|
|
310
317
|
if (wantedDependency.bareSpecifier.startsWith('workspace:.'))
|
|
311
318
|
return null;
|
|
@@ -379,6 +386,34 @@ async function resolveNpm(ctx, wantedDependency, opts) {
|
|
|
379
386
|
}
|
|
380
387
|
}
|
|
381
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
|
+
}
|
|
382
417
|
const authHeaderValue = ctx.getAuthHeaderValueByURI(registry, { pkgName: spec.name });
|
|
383
418
|
let pickResult;
|
|
384
419
|
try {
|
|
@@ -393,6 +428,7 @@ async function resolveNpm(ctx, wantedDependency, opts) {
|
|
|
393
428
|
includeLatestTag: opts.update === 'latest',
|
|
394
429
|
updateChecksums: opts.updateChecksums,
|
|
395
430
|
optional: wantedDependency.optional,
|
|
431
|
+
trustPolicy: opts.trustPolicy,
|
|
396
432
|
});
|
|
397
433
|
}
|
|
398
434
|
catch (err) { // eslint-disable-line
|
|
@@ -448,7 +484,11 @@ async function resolveNpm(ctx, wantedDependency, opts) {
|
|
|
448
484
|
throw new NoMatchingVersionError({ wantedDependency, packageMeta: meta, registry });
|
|
449
485
|
}
|
|
450
486
|
else if (opts.trustPolicy === 'no-downgrade') {
|
|
451
|
-
failIfTrustDowngraded(meta, pickedPackage.version,
|
|
487
|
+
failIfTrustDowngraded(meta, pickedPackage.version, {
|
|
488
|
+
trustPolicyExclude: opts.trustPolicyExclude,
|
|
489
|
+
trustPolicyIgnoreAfter: opts.trustPolicyIgnoreAfter,
|
|
490
|
+
ignoreMissingTimeField: ctx.ignoreMissingTimeField,
|
|
491
|
+
});
|
|
452
492
|
}
|
|
453
493
|
const latest = latestAllowedByPolicy(meta, opts);
|
|
454
494
|
const workspacePkgsMatchingName = workspacePackages?.get(pickedPackage.name);
|
|
@@ -524,7 +564,7 @@ async function resolveJsr(ctx, wantedDependency, opts) {
|
|
|
524
564
|
const spec = parseJsrSpecifierToRegistryPackageSpec(wantedDependency.bareSpecifier, wantedDependency.alias, opts.defaultTag ?? 'latest');
|
|
525
565
|
if (spec == null)
|
|
526
566
|
return null;
|
|
527
|
-
const picked = await pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, ctx.
|
|
567
|
+
const picked = await pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, ctx.registriesByScope['@jsr']); // '@jsr' is always defined
|
|
528
568
|
return {
|
|
529
569
|
...picked,
|
|
530
570
|
normalizedBareSpecifier: opts.calcSpecifier
|
|
@@ -543,14 +583,14 @@ async function resolveJsr(ctx, wantedDependency, opts) {
|
|
|
543
583
|
// another specifier scheme (e.g. `git`, `github`, `jsr`) is silently shadowed
|
|
544
584
|
// by that scheme's dedicated resolver — no cross-resolver knowledge needed.
|
|
545
585
|
function mergeNamedRegistries(userDefined) {
|
|
546
|
-
const merged = { ...
|
|
586
|
+
const merged = { ...BUILTIN_REGISTRIES_BY_PREFIX };
|
|
547
587
|
if (!userDefined)
|
|
548
588
|
return merged;
|
|
549
589
|
for (const [alias, url] of Object.entries(userDefined)) {
|
|
550
590
|
if (RESERVED_VERSION_PREFIXES.has(alias) || !isWellFormedRegistryName(alias)) {
|
|
551
591
|
throw new PnpmError('RESERVED_NAMED_REGISTRY_NAME', RESERVED_VERSION_PREFIXES.has(alias)
|
|
552
592
|
? `'${alias}' cannot be used as a named registry alias: it is a reserved dependency specifier prefix.`
|
|
553
|
-
: `'${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
|
|
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.' });
|
|
554
594
|
}
|
|
555
595
|
if (typeof url !== 'string' || !isValidHttpUrl(url)) {
|
|
556
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/' });
|
|
@@ -571,7 +611,7 @@ function isValidHttpUrl(url) {
|
|
|
571
611
|
// Resolves a `<alias>:` specifier from one of the configured named registries.
|
|
572
612
|
// The `gh:` alias ships as a built-in default pointing at the GitHub Packages
|
|
573
613
|
// npm registry; additional aliases come from pnpm-workspace.yaml's
|
|
574
|
-
// `
|
|
614
|
+
// `registriesByPrefix` field. Auth tokens are looked up by the resolved registry
|
|
575
615
|
// URL, so a `//npm.pkg.github.com/:_authToken=...` entry in `.npmrc` is
|
|
576
616
|
// picked up automatically for `gh:` specifiers (and analogously for any user-
|
|
577
617
|
// configured alias).
|
|
@@ -581,7 +621,7 @@ async function resolveFromNamedRegistry(ctx, wantedDependency, opts) {
|
|
|
581
621
|
const spec = parseNamedRegistrySpecifierToRegistryPackageSpec(wantedDependency.bareSpecifier, ctx.namedRegistryNames, wantedDependency.alias, opts.defaultTag ?? 'latest');
|
|
582
622
|
if (spec == null)
|
|
583
623
|
return null;
|
|
584
|
-
const registry = ctx.
|
|
624
|
+
const registry = ctx.registriesByPrefix[spec.registryName];
|
|
585
625
|
if (!registry)
|
|
586
626
|
return null; // defensive: should never trigger because parse checks the alias set
|
|
587
627
|
const picked = await pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, registry);
|
|
@@ -619,6 +659,7 @@ async function pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, registr
|
|
|
619
659
|
includeLatestTag: opts.update === 'latest',
|
|
620
660
|
updateChecksums: opts.updateChecksums,
|
|
621
661
|
optional: wantedDependency.optional,
|
|
662
|
+
trustPolicy: opts.trustPolicy,
|
|
622
663
|
});
|
|
623
664
|
if (pickedPackage == null) {
|
|
624
665
|
throw new NoMatchingVersionError({ wantedDependency, packageMeta: meta, registry });
|
|
@@ -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 {
|
|
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 {
|
|
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.
|
package/lib/pickPackage.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PackageInRegistry, PackageMeta } from '@pnpm/resolving.registry.types';
|
|
2
|
+
import type { TrustPolicy } from '@pnpm/types';
|
|
2
3
|
import { type FetchMetadataNotModifiedResult, type FetchMetadataResult } from './fetch.js';
|
|
3
4
|
import type { RegistryPackageSpec } from './parseBareSpecifier.js';
|
|
4
5
|
import { type PickPackageFromMetaOptions } from './pickPackageFromMeta.js';
|
|
@@ -22,6 +23,7 @@ export interface PickPackageOptions extends PickPackageFromMetaOptions {
|
|
|
22
23
|
dryRun: boolean;
|
|
23
24
|
includeLatestTag?: boolean;
|
|
24
25
|
optional?: boolean;
|
|
26
|
+
trustPolicy?: TrustPolicy;
|
|
25
27
|
/**
|
|
26
28
|
* When true, force a conditional registry request so a stale on-disk
|
|
27
29
|
* packument can't satisfy the call: the on-disk exact-version fast
|
|
@@ -44,12 +46,23 @@ export declare function pickPackage(ctx: {
|
|
|
44
46
|
modified?: string;
|
|
45
47
|
}) => Promise<FetchMetadataResult | FetchMetadataNotModifiedResult>;
|
|
46
48
|
fullMetadata?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Whether a time-based resolution has to read the full metadata of
|
|
51
|
+
* `registry`, because that registry's abbreviated form carries no `time`.
|
|
52
|
+
* Asked per registry: the answer differs between the public registry and a
|
|
53
|
+
* proxy that does carry it, and the mirror path and cache key below are
|
|
54
|
+
* already keyed by registry, so two registries may disagree within one
|
|
55
|
+
* install.
|
|
56
|
+
*/
|
|
57
|
+
needsFullMetadataFor?: (registry: string) => boolean;
|
|
47
58
|
metaCache: PackageMetaCache;
|
|
48
59
|
cacheDir: string;
|
|
49
60
|
offline?: boolean;
|
|
50
61
|
preferOffline?: boolean;
|
|
51
62
|
filterMetadata?: boolean;
|
|
52
63
|
ignoreMissingTimeField?: boolean;
|
|
64
|
+
/** Packuments whose release-age upgrade fetch already answered 304 in this resolver. */
|
|
65
|
+
releaseAgeUpgradeCheckedPackuments?: WeakSet<PackageMeta>;
|
|
53
66
|
}, spec: RegistryPackageSpec, opts: PickPackageOptions): Promise<{
|
|
54
67
|
meta: PackageMeta;
|
|
55
68
|
pickedPackage: PackageInRegistry | null;
|
|
@@ -91,7 +104,13 @@ export declare function getPkgMirrorPath(cacheDir: string, metaDir: string, regi
|
|
|
91
104
|
* there), so a `meta` that carries one is serialized without it.
|
|
92
105
|
*/
|
|
93
106
|
export declare function prepareJsonForDisk(meta: PackageMeta, etag: string | undefined, jsonText?: string): string;
|
|
94
|
-
|
|
107
|
+
/**
|
|
108
|
+
* At most one warning per package per check. `minimumReleaseAge` and
|
|
109
|
+
* `trustPolicy` both go dark on the same missing field, so keying by package
|
|
110
|
+
* alone would let whichever check ran first silence the other and leave the
|
|
111
|
+
* user told about only one of the two skips.
|
|
112
|
+
*/
|
|
113
|
+
export declare function warnMissingTimeFieldOnce(pkgName: string, skippedCheck: 'minimumReleaseAge' | 'trustPolicy'): void;
|
|
95
114
|
interface MetaHeaders {
|
|
96
115
|
etag?: string;
|
|
97
116
|
modified?: string;
|
package/lib/pickPackage.js
CHANGED
|
@@ -12,7 +12,8 @@ import { renameOverwrite } from 'rename-overwrite';
|
|
|
12
12
|
import semver from 'semver';
|
|
13
13
|
import { clearMeta, retainsFullMeta } from './clearMeta.js';
|
|
14
14
|
import { notModifiedWithoutCacheError, } from './fetch.js';
|
|
15
|
-
import { pickLowestVersionByVersionRange, pickPackageFromMeta, pickVersionByVersionRange, } from './pickPackageFromMeta.js';
|
|
15
|
+
import { getDominantLockfileVersion, pickLowestVersionByVersionRange, pickPackageFromMeta, pickStableCachedRangeVersion, pickVersionByVersionRange, } from './pickPackageFromMeta.js';
|
|
16
|
+
import { dropIncompletePublishTimes } from './publishTimes.js';
|
|
16
17
|
import { toRaw } from './toRaw.js';
|
|
17
18
|
/**
|
|
18
19
|
* prevents simultaneous operations on the meta.json
|
|
@@ -39,6 +40,13 @@ async function runLimited(pkgMirror, fn) {
|
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
}
|
|
43
|
+
function canReuseStableCachedRange(spec, opts) {
|
|
44
|
+
return (spec.type === 'range' &&
|
|
45
|
+
!opts.includeLatestTag &&
|
|
46
|
+
!opts.updateChecksums &&
|
|
47
|
+
opts.publishedBy == null &&
|
|
48
|
+
opts.trustPolicy !== 'no-downgrade');
|
|
49
|
+
}
|
|
42
50
|
// When includeLatestTag is set, the "latest" dist-tag is added as a candidate
|
|
43
51
|
// alongside the requested spec, and the higher-versioned pick wins.
|
|
44
52
|
function runPicker(pickerOpts, spec, pickOne) {
|
|
@@ -97,7 +105,7 @@ function pickMatchingVersionFinal(pickerOpts, spec, meta) {
|
|
|
97
105
|
}
|
|
98
106
|
catch (err) {
|
|
99
107
|
if (pickerOpts.ignoreMissingTimeField && isMissingTimeError(err)) {
|
|
100
|
-
warnMissingTimeFieldOnce(meta.name);
|
|
108
|
+
warnMissingTimeFieldOnce(meta.name, 'minimumReleaseAge');
|
|
101
109
|
return pickMatchingVersionFast({
|
|
102
110
|
...pickerOpts,
|
|
103
111
|
publishedBy: undefined,
|
|
@@ -152,7 +160,12 @@ export async function pickPackage(ctx, spec, opts) {
|
|
|
152
160
|
validatePackageName(spec.name);
|
|
153
161
|
// Use full metadata for optional dependencies to get libc field.
|
|
154
162
|
// See: https://github.com/pnpm/pnpm/issues/9950
|
|
155
|
-
|
|
163
|
+
// The per-registry answer is authoritative when the caller can give one: it
|
|
164
|
+
// already folds in the reasons that hold for every registry, so a registry
|
|
165
|
+
// that carries `time` is free to stay on abbreviated metadata while the
|
|
166
|
+
// others do not.
|
|
167
|
+
const policyWantsFullMetadata = ctx.needsFullMetadataFor?.(opts.registry) ?? ctx.fullMetadata === true;
|
|
168
|
+
const fullMetadata = opts.optional === true || policyWantsFullMetadata;
|
|
156
169
|
const metaDir = fullMetadata
|
|
157
170
|
? (ctx.filterMetadata ? FULL_FILTERED_META_DIR : FULL_META_DIR)
|
|
158
171
|
: ABBREVIATED_META_DIR;
|
|
@@ -177,7 +190,19 @@ export async function pickPackage(ctx, spec, opts) {
|
|
|
177
190
|
ctx.metaCache.set(cacheKey, metaForCache);
|
|
178
191
|
}
|
|
179
192
|
const pickedPackage = pickMatchingVersionFinal(pickerOpts, spec, metaForCache);
|
|
180
|
-
|
|
193
|
+
const unverified = unverifiedDiskPackuments.has(metaForCache);
|
|
194
|
+
const stableCachedRangeVersion = unverified &&
|
|
195
|
+
canReuseStableCachedRange(spec, opts)
|
|
196
|
+
? getDominantLockfileVersion(spec.fetchSpec, opts.preferredVersionSelectors)
|
|
197
|
+
: null;
|
|
198
|
+
const unverifiedPickIsSafe = ctx.preferOffline === true ||
|
|
199
|
+
opts.pickLowestVersion === true ||
|
|
200
|
+
spec.type === 'version' ||
|
|
201
|
+
(pickedPackage != null && pickedPackage.version === stableCachedRangeVersion);
|
|
202
|
+
const cacheResultCanReturn = ctx.offline === true ||
|
|
203
|
+
!unverified ||
|
|
204
|
+
(pickedPackage != null && unverifiedPickIsSafe);
|
|
205
|
+
if (cacheResultCanReturn) {
|
|
181
206
|
return {
|
|
182
207
|
meta: metaForCache,
|
|
183
208
|
pickedPackage,
|
|
@@ -271,6 +296,35 @@ export async function pickPackage(ctx, spec, opts) {
|
|
|
271
296
|
}
|
|
272
297
|
}
|
|
273
298
|
}
|
|
299
|
+
const dominantLockfileVersion = canReuseStableCachedRange(spec, opts)
|
|
300
|
+
? getDominantLockfileVersion(spec.fetchSpec, opts.preferredVersionSelectors)
|
|
301
|
+
: null;
|
|
302
|
+
if (dominantLockfileVersion != null) {
|
|
303
|
+
diskMeta = diskMeta ?? await limit(loadMetaCondensed);
|
|
304
|
+
if (diskMeta != null) {
|
|
305
|
+
try {
|
|
306
|
+
const stableVersion = pickStableCachedRangeVersion({
|
|
307
|
+
meta: diskMeta,
|
|
308
|
+
preferredVersionSelectors: opts.preferredVersionSelectors,
|
|
309
|
+
versionRange: spec.fetchSpec,
|
|
310
|
+
});
|
|
311
|
+
if (stableVersion != null) {
|
|
312
|
+
// Strict dominance makes the preferred tier a singleton, so the
|
|
313
|
+
// highest-version picker used by the proof and the normal picker
|
|
314
|
+
// agree even if pickLowestVersion reaches this code in the future.
|
|
315
|
+
const pickedPackage = pickMatchingVersionFast(pickerOpts, spec, diskMeta);
|
|
316
|
+
if (pickedPackage?.version === stableVersion) {
|
|
317
|
+
cacheDiskLoadedMeta(ctx.metaCache, cacheKey, diskMeta);
|
|
318
|
+
return { meta: diskMeta, pickedPackage };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
// Any malformed cached metadata falls through to normal online
|
|
324
|
+
// resolution, matching the neighboring disk fast paths.
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
274
328
|
if (opts.publishedBy && opts.publishedByExclude?.(spec.name) !== true) {
|
|
275
329
|
const mtime = await limit(async () => getFileMtime(pkgMirror));
|
|
276
330
|
if (mtime != null && mtime >= opts.publishedBy) {
|
|
@@ -377,6 +431,7 @@ export async function pickPackage(ctx, spec, opts) {
|
|
|
377
431
|
// and most packages won't have been modified recently enough to need the full
|
|
378
432
|
// document. We only upgrade to full metadata when the package's modification
|
|
379
433
|
// date is recent enough that some versions might not yet be "mature."
|
|
434
|
+
let attemptedReleaseAgeUpgrade = false;
|
|
380
435
|
if (opts.publishedBy &&
|
|
381
436
|
!fullMetadata &&
|
|
382
437
|
meta.time == null &&
|
|
@@ -393,6 +448,7 @@ export async function pickPackage(ctx, spec, opts) {
|
|
|
393
448
|
if (!opts.dryRun) {
|
|
394
449
|
saveMetaBestEffort(pkgMirror, prepareJsonForDisk(resultToSave.meta, resultToSave.etag, resultToSave.jsonText));
|
|
395
450
|
}
|
|
451
|
+
attemptedReleaseAgeUpgrade = true;
|
|
396
452
|
const fullFetchResult = await ctx.fetch(spec.name, {
|
|
397
453
|
authHeaderValue: opts.authHeaderValue,
|
|
398
454
|
fullMetadata: true,
|
|
@@ -405,6 +461,9 @@ export async function pickPackage(ctx, spec, opts) {
|
|
|
405
461
|
}
|
|
406
462
|
}
|
|
407
463
|
meta = condenseMetaForCache(ctx, meta);
|
|
464
|
+
if (attemptedReleaseAgeUpgrade) {
|
|
465
|
+
ctx.releaseAgeUpgradeCheckedPackuments?.add(meta);
|
|
466
|
+
}
|
|
408
467
|
if (!opts.dryRun) {
|
|
409
468
|
// Mirror the raw registry body, unless the retained form is
|
|
410
469
|
// deliberately narrower: `filterMetadata` always mirrors the stripped
|
|
@@ -441,6 +500,7 @@ async function maybeUpgradeAbbreviatedMetaForReleaseAge(ctx, spec, opts, meta) {
|
|
|
441
500
|
if (ctx.offline === true ||
|
|
442
501
|
!opts.publishedBy ||
|
|
443
502
|
meta.time != null ||
|
|
503
|
+
ctx.releaseAgeUpgradeCheckedPackuments?.has(meta) === true ||
|
|
444
504
|
opts.publishedByExclude?.(spec.name) === true) {
|
|
445
505
|
return { meta };
|
|
446
506
|
}
|
|
@@ -468,8 +528,11 @@ async function maybeUpgradeAbbreviatedMetaForReleaseAge(ctx, spec, opts, meta) {
|
|
|
468
528
|
registry: opts.registry,
|
|
469
529
|
});
|
|
470
530
|
if (fullFetchResult.notModified) {
|
|
471
|
-
// Upgrade fetch came back 304:
|
|
472
|
-
// `pickMatchingVersionFinal`
|
|
531
|
+
// Upgrade fetch came back 304: the registry has no fuller form of this
|
|
532
|
+
// document, so keep it and let `pickMatchingVersionFinal` fall through to
|
|
533
|
+
// its warn-and-skip path. Remember the outcome against the packument
|
|
534
|
+
// itself so no other pick in this resolver repeats the request.
|
|
535
|
+
ctx.releaseAgeUpgradeCheckedPackuments?.add(meta);
|
|
473
536
|
return { meta };
|
|
474
537
|
}
|
|
475
538
|
return { meta: fullFetchResult.meta, upgradedFrom: fullFetchResult };
|
|
@@ -480,12 +543,26 @@ async function maybeUpgradeAbbreviatedMetaForReleaseAge(ctx, spec, opts, meta) {
|
|
|
480
543
|
* pre-upgrade abbreviated form without `time`, and every future install
|
|
481
544
|
* would re-trigger the upgrade fetch.
|
|
482
545
|
*/
|
|
546
|
+
/**
|
|
547
|
+
* The document to serve and cache after an upgrade attempt, marked so no
|
|
548
|
+
* later pick in this resolver repeats the request.
|
|
549
|
+
*
|
|
550
|
+
* Marking belongs here rather than in
|
|
551
|
+
* {@link maybeUpgradeAbbreviatedMetaForReleaseAge} because persisting the
|
|
552
|
+
* response to the mirror can hand back a different object, and only the one
|
|
553
|
+
* that reaches the cache is worth remembering. A registry whose full form is
|
|
554
|
+
* no more complete than its abbreviated one answers `200` rather than `304`,
|
|
555
|
+
* so both outcomes have to be marked — otherwise every dependency edge
|
|
556
|
+
* re-asks for the same full document.
|
|
557
|
+
*/
|
|
483
558
|
function upgradeMetaForCache(ctx, upgrade, opts) {
|
|
484
559
|
if (upgrade.upgradedFrom == null)
|
|
485
560
|
return upgrade.meta;
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
561
|
+
const meta = opts.dryRun
|
|
562
|
+
? condenseMetaForCache(ctx, upgrade.meta)
|
|
563
|
+
: persistUpgradedMeta(ctx, opts.pkgMirror, upgrade.upgradedFrom);
|
|
564
|
+
ctx.releaseAgeUpgradeCheckedPackuments?.add(meta);
|
|
565
|
+
return meta;
|
|
489
566
|
}
|
|
490
567
|
// A condensing resolver keeps and mirrors the condensed form — the mirror
|
|
491
568
|
// only has to carry `time` into the next install; otherwise the raw response
|
|
@@ -586,8 +663,17 @@ function isMissingTimeError(err) {
|
|
|
586
663
|
// memory via this Set as they resolve ever more distinct packages.
|
|
587
664
|
const MAX_WARNED_MISSING_TIME = 1024;
|
|
588
665
|
const warnedMissingTimeFor = new Set();
|
|
589
|
-
|
|
590
|
-
|
|
666
|
+
/**
|
|
667
|
+
* At most one warning per package per check. `minimumReleaseAge` and
|
|
668
|
+
* `trustPolicy` both go dark on the same missing field, so keying by package
|
|
669
|
+
* alone would let whichever check ran first silence the other and leave the
|
|
670
|
+
* user told about only one of the two skips.
|
|
671
|
+
*/
|
|
672
|
+
export function warnMissingTimeFieldOnce(pkgName, skippedCheck) {
|
|
673
|
+
// A package name cannot contain ':', so the check name prefix cannot
|
|
674
|
+
// collide with a name that happens to embed it.
|
|
675
|
+
const key = `${skippedCheck}:${pkgName}`;
|
|
676
|
+
if (warnedMissingTimeFor.has(key))
|
|
591
677
|
return;
|
|
592
678
|
if (warnedMissingTimeFor.size >= MAX_WARNED_MISSING_TIME) {
|
|
593
679
|
// Set preserves insertion order, so the first entry is the oldest.
|
|
@@ -595,8 +681,8 @@ export function warnMissingTimeFieldOnce(pkgName) {
|
|
|
595
681
|
if (oldest != null)
|
|
596
682
|
warnedMissingTimeFor.delete(oldest);
|
|
597
683
|
}
|
|
598
|
-
warnedMissingTimeFor.add(
|
|
599
|
-
globalWarn(`The metadata of ${pkgName} is missing the "time" field; skipping the
|
|
684
|
+
warnedMissingTimeFor.add(key);
|
|
685
|
+
globalWarn(`The metadata of ${pkgName} is missing the "time" field; skipping the ${skippedCheck} check for this package.`);
|
|
600
686
|
}
|
|
601
687
|
async function getFileMtime(filePath) {
|
|
602
688
|
try {
|
|
@@ -648,6 +734,7 @@ export async function loadMeta(pkgMirror) {
|
|
|
648
734
|
return null;
|
|
649
735
|
const headers = JSON.parse(data.slice(0, newlineIdx));
|
|
650
736
|
const meta = JSON.parse(data.slice(newlineIdx + 1));
|
|
737
|
+
dropIncompletePublishTimes(meta);
|
|
651
738
|
meta.etag = headers.etag;
|
|
652
739
|
return meta;
|
|
653
740
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PackageInRegistry, PackageMeta, PackageMetaWithTime } from '@pnpm/resolving.registry.types';
|
|
2
|
-
import type
|
|
2
|
+
import { type VersionSelectors } from '@pnpm/resolving.resolver-base';
|
|
3
3
|
import type { PackageVersionPolicy } from '@pnpm/types';
|
|
4
4
|
import type { RegistryPackageSpec } from './parseBareSpecifier.js';
|
|
5
5
|
export interface PickVersionByVersionRangeOptions {
|
|
@@ -41,3 +41,9 @@ export declare function applyPublishedByPolicy(meta: PackageMeta, publishedBy: D
|
|
|
41
41
|
export declare function assertMetaHasTime(meta: PackageMeta): asserts meta is PackageMetaWithTime;
|
|
42
42
|
export declare function pickLowestVersionByVersionRange({ meta, versionRange, preferredVersionSelectors }: PickVersionByVersionRangeOptions): string | null;
|
|
43
43
|
export declare function pickVersionByVersionRange({ meta, versionRange, preferredVersionSelectors }: PickVersionByVersionRangeOptions): string | null;
|
|
44
|
+
/**
|
|
45
|
+
* Returns the cached version only when lockfile preferences prove that no
|
|
46
|
+
* version missing from the cached packument could tie or outrank it.
|
|
47
|
+
*/
|
|
48
|
+
export declare function pickStableCachedRangeVersion({ meta, preferredVersionSelectors, versionRange, }: PickVersionByVersionRangeOptions): string | null;
|
|
49
|
+
export declare function getDominantLockfileVersion(versionRange: string, preferredVersionSelectors?: VersionSelectors): string | null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import util from 'node:util';
|
|
2
2
|
import { PnpmError } from '@pnpm/error';
|
|
3
3
|
import { filterPkgMetadataByPublishDate } from '@pnpm/resolving.registry.pkg-metadata-filter';
|
|
4
|
+
import { EXISTING_VERSION_SELECTOR_WEIGHT, } from '@pnpm/resolving.resolver-base';
|
|
4
5
|
import semver from 'semver';
|
|
5
6
|
export function pickPackageFromMeta(pickVersionByVersionRangeFn, { preferredVersionSelectors, publishedBy, publishedByExclude, }, meta, spec) {
|
|
6
7
|
if (publishedBy) {
|
|
@@ -152,6 +153,86 @@ export function pickVersionByVersionRange({ meta, versionRange, preferredVersion
|
|
|
152
153
|
}
|
|
153
154
|
return maxVersion;
|
|
154
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Returns the cached version only when lockfile preferences prove that no
|
|
158
|
+
* version missing from the cached packument could tie or outrank it.
|
|
159
|
+
*/
|
|
160
|
+
export function pickStableCachedRangeVersion({ meta, preferredVersionSelectors, versionRange, }) {
|
|
161
|
+
const dominantLockfileVersion = getDominantLockfileVersion(versionRange, preferredVersionSelectors);
|
|
162
|
+
if (dominantLockfileVersion == null || meta.versions[dominantLockfileVersion] == null)
|
|
163
|
+
return null;
|
|
164
|
+
try {
|
|
165
|
+
const pickedVersion = pickVersionByVersionRange({ meta, preferredVersionSelectors, versionRange });
|
|
166
|
+
return pickedVersion === dominantLockfileVersion ? dominantLockfileVersion : null;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
export function getDominantLockfileVersion(versionRange, preferredVersionSelectors) {
|
|
173
|
+
if (preferredVersionSelectors == null)
|
|
174
|
+
return null;
|
|
175
|
+
let lockfileVersion;
|
|
176
|
+
for (const [selector, value] of Object.entries(preferredVersionSelectors)) {
|
|
177
|
+
if (selector === versionRange)
|
|
178
|
+
continue;
|
|
179
|
+
const { selectorType, weight } = preferredSelectorInfo(value);
|
|
180
|
+
if (!Number.isSafeInteger(weight) || weight <= 0)
|
|
181
|
+
return null;
|
|
182
|
+
if (selectorType === 'version' &&
|
|
183
|
+
weight >= EXISTING_VERSION_SELECTOR_WEIGHT &&
|
|
184
|
+
semverSatisfiesLoose(selector, versionRange)) {
|
|
185
|
+
if (lockfileVersion != null)
|
|
186
|
+
return null;
|
|
187
|
+
lockfileVersion = selector;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (lockfileVersion == null)
|
|
191
|
+
return null;
|
|
192
|
+
let guaranteedLockfileWeight = 0;
|
|
193
|
+
let maximumOtherVersionWeight = 0;
|
|
194
|
+
for (const [selector, value] of Object.entries(preferredVersionSelectors)) {
|
|
195
|
+
if (selector === versionRange)
|
|
196
|
+
continue;
|
|
197
|
+
const { selectorType, weight } = preferredSelectorInfo(value);
|
|
198
|
+
switch (selectorType) {
|
|
199
|
+
case 'version':
|
|
200
|
+
if (selector === lockfileVersion) {
|
|
201
|
+
guaranteedLockfileWeight += weight;
|
|
202
|
+
}
|
|
203
|
+
else if (weight < EXISTING_VERSION_SELECTOR_WEIGHT &&
|
|
204
|
+
semverSatisfiesLoose(selector, versionRange)) {
|
|
205
|
+
maximumOtherVersionWeight += weight;
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
case 'range':
|
|
209
|
+
if (semverSatisfiesLoose(lockfileVersion, selector)) {
|
|
210
|
+
guaranteedLockfileWeight += weight;
|
|
211
|
+
}
|
|
212
|
+
// Conservatively assume an unseen version can satisfy every preferred
|
|
213
|
+
// range, even when proving range intersection would be more precise.
|
|
214
|
+
maximumOtherVersionWeight += weight;
|
|
215
|
+
break;
|
|
216
|
+
case 'tag':
|
|
217
|
+
// A registry can move a tag between requests. Do not count its current
|
|
218
|
+
// target toward the lockfile version, and assume all tags could move to
|
|
219
|
+
// the same unseen version.
|
|
220
|
+
maximumOtherVersionWeight += weight;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
if (!Number.isSafeInteger(guaranteedLockfileWeight) ||
|
|
224
|
+
!Number.isSafeInteger(maximumOtherVersionWeight))
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
return guaranteedLockfileWeight > maximumOtherVersionWeight
|
|
228
|
+
? lockfileVersion
|
|
229
|
+
: null;
|
|
230
|
+
}
|
|
231
|
+
function preferredSelectorInfo(value) {
|
|
232
|
+
return typeof value === 'string'
|
|
233
|
+
? { selectorType: value, weight: 1 }
|
|
234
|
+
: value;
|
|
235
|
+
}
|
|
155
236
|
function prioritizePreferredVersions(meta, versionRange, preferredVerSelectors) {
|
|
156
237
|
const preferredVerSelectorsArr = Object.entries(preferredVerSelectors ?? {});
|
|
157
238
|
const versionsPrioritizer = new PreferredVersionsPrioritizer();
|
|
@@ -163,9 +244,7 @@ function prioritizePreferredVersions(meta, versionRange, preferredVerSelectors)
|
|
|
163
244
|
}
|
|
164
245
|
// Then apply weights from preferred selectors
|
|
165
246
|
for (const [preferredSelector, preferredSelectorType] of preferredVerSelectorsArr) {
|
|
166
|
-
const { selectorType, weight } =
|
|
167
|
-
? { selectorType: preferredSelectorType, weight: 1 }
|
|
168
|
-
: preferredSelectorType;
|
|
247
|
+
const { selectorType, weight } = preferredSelectorInfo(preferredSelectorType);
|
|
169
248
|
if (preferredSelector === versionRange)
|
|
170
249
|
continue;
|
|
171
250
|
switch (selectorType) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { PackageMeta } from '@pnpm/resolving.registry.types';
|
|
2
|
+
/**
|
|
3
|
+
* Drops `time` unless it carries a publish timestamp for every version the
|
|
4
|
+
* packument lists.
|
|
5
|
+
*
|
|
6
|
+
* Registries may answer with a partial map: npmmirror adds `time` to its
|
|
7
|
+
* abbreviated documents but fills it in only for the versions it has synced
|
|
8
|
+
* since it started recording publish times, leaving the rest out. A partial
|
|
9
|
+
* map is indistinguishable from a complete one at the point of use, so the
|
|
10
|
+
* `minimumReleaseAge` filter reads every absent timestamp as "not mature"
|
|
11
|
+
* and silently drops the version — resolution then falls back to the lowest
|
|
12
|
+
* match.
|
|
13
|
+
*
|
|
14
|
+
* A map that can't decide maturity is worth nothing to the resolver, so it
|
|
15
|
+
* is normalized away where the document is parsed. Every packument past that
|
|
16
|
+
* point then carries either a complete `time` or none at all — the shape the
|
|
17
|
+
* npm registry's own abbreviated documents have, and the one the rest of the
|
|
18
|
+
* resolver is written against.
|
|
19
|
+
*
|
|
20
|
+
* A packument with no versions keeps whatever `time` it has — there is
|
|
21
|
+
* nothing for the map to be incomplete about — and a version whose entry is
|
|
22
|
+
* an empty string counts as absent.
|
|
23
|
+
*/
|
|
24
|
+
export declare function dropIncompletePublishTimes(meta: PackageMeta): void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drops `time` unless it carries a publish timestamp for every version the
|
|
3
|
+
* packument lists.
|
|
4
|
+
*
|
|
5
|
+
* Registries may answer with a partial map: npmmirror adds `time` to its
|
|
6
|
+
* abbreviated documents but fills it in only for the versions it has synced
|
|
7
|
+
* since it started recording publish times, leaving the rest out. A partial
|
|
8
|
+
* map is indistinguishable from a complete one at the point of use, so the
|
|
9
|
+
* `minimumReleaseAge` filter reads every absent timestamp as "not mature"
|
|
10
|
+
* and silently drops the version — resolution then falls back to the lowest
|
|
11
|
+
* match.
|
|
12
|
+
*
|
|
13
|
+
* A map that can't decide maturity is worth nothing to the resolver, so it
|
|
14
|
+
* is normalized away where the document is parsed. Every packument past that
|
|
15
|
+
* point then carries either a complete `time` or none at all — the shape the
|
|
16
|
+
* npm registry's own abbreviated documents have, and the one the rest of the
|
|
17
|
+
* resolver is written against.
|
|
18
|
+
*
|
|
19
|
+
* A packument with no versions keeps whatever `time` it has — there is
|
|
20
|
+
* nothing for the map to be incomplete about — and a version whose entry is
|
|
21
|
+
* an empty string counts as absent.
|
|
22
|
+
*/
|
|
23
|
+
export function dropIncompletePublishTimes(meta) {
|
|
24
|
+
if (meta.time == null)
|
|
25
|
+
return;
|
|
26
|
+
for (const version in meta.versions) {
|
|
27
|
+
if (!Object.hasOwn(meta.versions, version))
|
|
28
|
+
continue;
|
|
29
|
+
if (!Object.hasOwn(meta.time, version) || !meta.time[version]) {
|
|
30
|
+
delete meta.time;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=publishTimes.js.map
|
package/lib/trustChecks.d.ts
CHANGED
|
@@ -4,6 +4,20 @@ type TrustEvidence = 'provenance' | 'trustedPublisher' | 'stagedPublish';
|
|
|
4
4
|
export declare function failIfTrustDowngraded(meta: PackageMeta, version: string, opts?: {
|
|
5
5
|
trustPolicyExclude?: PackageVersionPolicy;
|
|
6
6
|
trustPolicyIgnoreAfter?: number;
|
|
7
|
+
/**
|
|
8
|
+
* The `minimumReleaseAgeIgnoreMissingTime` opt-in, which declares that
|
|
9
|
+
* the registry cannot date its releases. The downgrade check orders
|
|
10
|
+
* history by publish date, so a packument with no `time` map leaves it
|
|
11
|
+
* nothing to order and the check is skipped with a warning rather than
|
|
12
|
+
* aborting the install.
|
|
13
|
+
*
|
|
14
|
+
* Scoped to the whole map being absent, which `dropIncompletePublishTimes`
|
|
15
|
+
* makes the only shape a registry that dates some of its versions can
|
|
16
|
+
* reach here in. A packument that dates every version it lists is instead
|
|
17
|
+
* saying it does not have this one, so that shape keeps failing closed
|
|
18
|
+
* however this flag is set.
|
|
19
|
+
*/
|
|
20
|
+
ignoreMissingTimeField?: boolean;
|
|
7
21
|
}): void;
|
|
8
22
|
export declare function getTrustEvidence(manifest: PackageInRegistry): TrustEvidence | undefined;
|
|
9
23
|
export {};
|
package/lib/trustChecks.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { PnpmError } from '@pnpm/error';
|
|
2
2
|
import semver from 'semver';
|
|
3
|
+
import { warnMissingTimeFieldOnce } from './pickPackage.js';
|
|
3
4
|
import { assertMetaHasTime } from './pickPackageFromMeta.js';
|
|
4
5
|
const TRUST_RANK = {
|
|
5
6
|
stagedPublish: 3,
|
|
@@ -16,6 +17,10 @@ export function failIfTrustDowngraded(meta, version, opts) {
|
|
|
16
17
|
return;
|
|
17
18
|
}
|
|
18
19
|
}
|
|
20
|
+
if (meta.time == null && opts?.ignoreMissingTimeField) {
|
|
21
|
+
warnMissingTimeFieldOnce(meta.name, 'trustPolicy');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
19
24
|
assertMetaHasTime(meta);
|
|
20
25
|
const versionPublishedAt = meta.time[version];
|
|
21
26
|
if (!versionPublishedAt) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/resolving.npm-resolver",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1104.0.0",
|
|
4
4
|
"description": "Resolver for npm-hosted packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pnpm",
|
|
@@ -29,24 +29,24 @@
|
|
|
29
29
|
"!*.map"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@pnpm/config.normalize-registries": "
|
|
33
|
-
"@pnpm/config.pick-registry-for-package": "
|
|
34
|
-
"@pnpm/config.version-policy": "1100.2.
|
|
35
|
-
"@pnpm/constants": "
|
|
36
|
-
"@pnpm/core-loggers": "1100.3.
|
|
32
|
+
"@pnpm/config.normalize-registries": "1101.0.0",
|
|
33
|
+
"@pnpm/config.pick-registry-for-package": "1101.0.0",
|
|
34
|
+
"@pnpm/config.version-policy": "1100.2.1",
|
|
35
|
+
"@pnpm/constants": "1102.0.0",
|
|
36
|
+
"@pnpm/core-loggers": "1100.3.3",
|
|
37
37
|
"@pnpm/crypto.hash": "1100.0.2",
|
|
38
|
-
"@pnpm/deps.path": "
|
|
39
|
-
"@pnpm/error": "1100.1.
|
|
38
|
+
"@pnpm/deps.path": "1101.0.0",
|
|
39
|
+
"@pnpm/error": "1100.1.3",
|
|
40
40
|
"@pnpm/fetching.types": "1100.0.3",
|
|
41
41
|
"@pnpm/fs.graceful-fs": "1100.1.1",
|
|
42
|
-
"@pnpm/pkg-manifest.utils": "1100.4.
|
|
43
|
-
"@pnpm/resolving.jsr-specifier-parser": "1100.0.
|
|
44
|
-
"@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.
|
|
45
|
-
"@pnpm/resolving.registry.types": "1100.1.
|
|
46
|
-
"@pnpm/resolving.resolver-base": "1101.1.
|
|
47
|
-
"@pnpm/store.cafs": "1100.
|
|
48
|
-
"@pnpm/store.index": "1100.2.
|
|
49
|
-
"@pnpm/types": "
|
|
42
|
+
"@pnpm/pkg-manifest.utils": "1100.4.1",
|
|
43
|
+
"@pnpm/resolving.jsr-specifier-parser": "1100.0.6",
|
|
44
|
+
"@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.17",
|
|
45
|
+
"@pnpm/resolving.registry.types": "1100.1.10",
|
|
46
|
+
"@pnpm/resolving.resolver-base": "1101.1.1",
|
|
47
|
+
"@pnpm/store.cafs": "1100.2.0",
|
|
48
|
+
"@pnpm/store.index": "1100.2.5",
|
|
49
|
+
"@pnpm/types": "1102.0.0",
|
|
50
50
|
"@pnpm/workspace.range-resolver": "1100.0.3",
|
|
51
51
|
"@pnpm/workspace.spec-parser": "1100.0.1",
|
|
52
52
|
"@zkochan/retry": "^0.2.0",
|
|
@@ -65,13 +65,13 @@
|
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"@pnpm/logger": "^1100.0.0",
|
|
68
|
-
"@pnpm/worker": "^1100.
|
|
68
|
+
"@pnpm/worker": "^1100.3.0"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@jest/globals": "30.4.1",
|
|
72
72
|
"@pnpm/logger": "1100.0.0",
|
|
73
|
-
"@pnpm/network.fetch": "1100.1.
|
|
74
|
-
"@pnpm/resolving.npm-resolver": "
|
|
73
|
+
"@pnpm/network.fetch": "1100.1.13",
|
|
74
|
+
"@pnpm/resolving.npm-resolver": "1104.0.0",
|
|
75
75
|
"@pnpm/test-fixtures": "1100.0.1",
|
|
76
76
|
"@pnpm/testing.mock-agent": "1101.0.7",
|
|
77
77
|
"@types/normalize-path": "^3.0.2",
|