@pnpm/resolving.npm-resolver 1103.0.0 → 1103.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/lib/clearMeta.js +15 -2
- package/lib/createNpmResolutionVerifier.js +34 -29
- package/lib/index.js +11 -0
- package/lib/parseBareSpecifier.d.ts +1 -1
- package/lib/parseBareSpecifier.js +1 -3
- package/lib/pickPackage.js +21 -5
- package/lib/pickPackageFromMeta.js +77 -30
- package/lib/violationCodes.d.ts +1 -0
- package/lib/violationCodes.js +1 -0
- package/package.json +19 -17
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,84 @@
|
|
|
1
1
|
# @pnpm/npm-resolver
|
|
2
2
|
|
|
3
|
+
## 1103.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- **Security fix.** Affects projects using `namedRegistries` on pnpm 11.1.0–11.19.x. It is **semi-breaking** for those projects — see "If you use named registries" below.
|
|
8
|
+
|
|
9
|
+
The lockfile recorded no marker for which registry a package came from. Packages were keyed by `name@version` alone, and entry lookup went through `refToRelative(ref, name)`, so a dependency you declared against one registry could be satisfied by an entry that was actually resolved from another. When two registries served the same name and version, both collapsed onto a single `packages:` entry and whichever resolved first decided the tarball every consumer got.
|
|
10
|
+
|
|
11
|
+
That is a package-substitution risk: a package you expect from your private registry could be installed from a different registry that publishes the same name and version, and the lockfile recorded nothing that would let you tell.
|
|
12
|
+
|
|
13
|
+
Packages resolved from a named registry are now recorded under registry-qualified keys (`<name>@<registryName>:<version>`, e.g. `foo@work:1.0.0`), so each registry gets its own entry and the lockfile pins which one a dependency came from.
|
|
14
|
+
|
|
15
|
+
The lockfile format version is unchanged. Registry-qualified keys appear only for packages resolved from a named registry, so a project that does not use `namedRegistries` sees no difference, and older pnpm versions keep reading the file.
|
|
16
|
+
|
|
17
|
+
### If you use named registries
|
|
18
|
+
|
|
19
|
+
Your next non-frozen install re-keys those entries, which shows up as a lockfile diff. Commit it — that diff is the fix being applied. Review it: an entry that moves to a registry you did not expect is worth investigating.
|
|
20
|
+
|
|
21
|
+
Everyone working on the project should be on this version or newer before you do. An older pnpm reads the re-keyed lockfile fine — frozen installs are unaffected — but it does not produce registry-qualified keys itself, so any install that updates the lockfile writes those entries back to the old shape, and the next install on a current pnpm re-qualifies them. The result is a lockfile that flips back and forth, and while it is in the old shape the project is exposed again. Because the lockfile format version is deliberately unchanged, pnpm cannot detect this and warn you about it.
|
|
22
|
+
|
|
23
|
+
There is no setting to keep the old behavior: the old shape is the vulnerability.
|
|
24
|
+
|
|
25
|
+
Tarball URLs that follow the standard registry layout are no longer written to the lockfile for named-registry packages; they are recomputed from the `namedRegistries` setting on demand.
|
|
26
|
+
|
|
27
|
+
To use named registries, map your aliases in `pnpm-workspace.yaml`:
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
namedRegistries:
|
|
31
|
+
work: https://npm.enterprise.example.com/
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### New built-in `npmjs:` alias
|
|
35
|
+
|
|
36
|
+
`npmjs:` now resolves to `https://registry.npmjs.org/` with no configuration, alongside the existing `gh:` alias for GitHub Packages. It pins a dependency to the public registry even when `registry` points elsewhere, such as an internal proxy:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{ "dependencies": { "left-pad": "npmjs:^1.3.0" } }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`npm:` cannot do this — it is the alias protocol (`npm:<name>@<range>`) and resolves through whatever `registry` points at.
|
|
43
|
+
|
|
44
|
+
**If you mirror or proxy npmjs, point the alias at your mirror:**
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
namedRegistries:
|
|
48
|
+
npmjs: https://npm.internal.example.com/
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Built-in registry URLs are also the prefixes a lockfile's recorded tarball URL is matched against when pnpm verifies a package. Without the override, an entry whose tarball URL is on `registry.npmjs.org` is verified against the public registry rather than your mirror. This only affects lockfiles that record such URLs — a canonical URL for your configured registry is omitted from the lockfile and unaffected — and only when a tarball-URL, `minimumReleaseAge`, or `trustPolicy` check runs. Overriding the alias is the same escape hatch GHES users already have for `gh`.
|
|
52
|
+
|
|
53
|
+
Every alias the lockfile references must stay in `namedRegistries`: reading an entry whose alias is gone fails with `ERR_PNPM_MISSING_NAMED_REGISTRY` rather than silently falling back to the default registry, since that would fetch a different package. Renaming an alias re-resolves the packages that used it.
|
|
54
|
+
|
|
55
|
+
Named registry aliases that shadow a reserved dependency specifier prefix (`file`, `link`, `workspace`, `runtime`, `npm`, `jsr`, ...) are now rejected with `ERR_PNPM_RESERVED_NAMED_REGISTRY_NAME` instead of being silently shadowed by the corresponding resolver.
|
|
56
|
+
|
|
57
|
+
`pnpm licenses` and `pnpm sbom` now keep the two artifacts apart as well: license records carry the registry alias, and SBOM components carry the purl `repository_url` qualifier.
|
|
58
|
+
|
|
59
|
+
### Patch Changes
|
|
60
|
+
|
|
61
|
+
- Fixed the order in which pnpm matches a lockfile's recorded tarball URL against known registry URLs. Two registry URLs of equal length were previously ordered arbitrarily, so which one a tarball URL matched could differ between runs.
|
|
62
|
+
|
|
63
|
+
- Dependency resolution is faster: package metadata is now filtered once per packument instead of once per dependency edge when `minimumReleaseAge` is active, and parsed semver versions and ranges are reused instead of re-parsed on every comparison.
|
|
64
|
+
|
|
65
|
+
- Updated dependencies:
|
|
66
|
+
- @pnpm/config.normalize-registries@1100.1.0
|
|
67
|
+
- @pnpm/config.pick-registry-for-package@1100.1.0
|
|
68
|
+
- @pnpm/config.version-policy@1100.1.12
|
|
69
|
+
- @pnpm/constants@1101.0.0
|
|
70
|
+
- @pnpm/core-loggers@1100.3.2
|
|
71
|
+
- @pnpm/deps.path@1100.1.0
|
|
72
|
+
- @pnpm/error@1100.1.1
|
|
73
|
+
- @pnpm/pkg-manifest.utils@1100.3.1
|
|
74
|
+
- @pnpm/resolving.jsr-specifier-parser@1100.0.4
|
|
75
|
+
- @pnpm/resolving.registry.pkg-metadata-filter@1100.0.15
|
|
76
|
+
- @pnpm/resolving.registry.types@1100.1.9
|
|
77
|
+
- @pnpm/resolving.resolver-base@1101.1.0
|
|
78
|
+
- @pnpm/store.cafs@1100.1.18
|
|
79
|
+
- @pnpm/store.index@1100.2.3
|
|
80
|
+
- @pnpm/types@1101.9.0
|
|
81
|
+
|
|
3
82
|
## 1103.0.0
|
|
4
83
|
|
|
5
84
|
### Major Changes
|
package/lib/clearMeta.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { pick } from 'ramda';
|
|
2
1
|
// The list taken from https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
|
|
3
2
|
// with the addition of 'libc'
|
|
4
3
|
const ABBREVIATED_VERSION_FIELDS = [
|
|
@@ -48,7 +47,7 @@ export function clearMeta(pkg) {
|
|
|
48
47
|
// prototype of the map (js/prototype-polluting-assignment).
|
|
49
48
|
const versions = Object.create(null);
|
|
50
49
|
for (const [version, info] of Object.entries(pkg.versions ?? {})) {
|
|
51
|
-
versions[version] =
|
|
50
|
+
versions[version] = pickAbbreviatedVersionFields(info);
|
|
52
51
|
}
|
|
53
52
|
const condensed = {
|
|
54
53
|
name: pkg.name,
|
|
@@ -64,6 +63,20 @@ export function clearMeta(pkg) {
|
|
|
64
63
|
condensedPackuments.set(condensed, condensed);
|
|
65
64
|
return condensed;
|
|
66
65
|
}
|
|
66
|
+
// Hand-rolled rather than delegated to a generic field picker because it runs
|
|
67
|
+
// for every version of every packument an install parses. Testing the value
|
|
68
|
+
// for `undefined` is equivalent to testing for the key's presence: version
|
|
69
|
+
// objects come from JSON, which cannot encode undefined.
|
|
70
|
+
function pickAbbreviatedVersionFields(info) {
|
|
71
|
+
const picked = {};
|
|
72
|
+
for (const field of ABBREVIATED_VERSION_FIELDS) {
|
|
73
|
+
const value = info[field];
|
|
74
|
+
if (value !== undefined) {
|
|
75
|
+
picked[field] = value;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return picked;
|
|
79
|
+
}
|
|
67
80
|
/**
|
|
68
81
|
* Whether a resolver keeps full packuments rather than condensing them with
|
|
69
82
|
* {@link clearMeta}: only `fullMetadata` without `filterMetadata`, whose
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { normalizeNamedRegistries } from '@pnpm/config.normalize-registries';
|
|
3
|
+
import { namedRegistryTarballPrefixes, pickRegistryForPackage } from '@pnpm/config.pick-registry-for-package';
|
|
2
4
|
import { createPackageVersionPolicy } from '@pnpm/config.version-policy';
|
|
3
5
|
import { FULL_META_DIR } from '@pnpm/constants';
|
|
4
6
|
import { PnpmError } from '@pnpm/error';
|
|
@@ -7,10 +9,9 @@ import semver from 'semver';
|
|
|
7
9
|
import { fetchAttestationPublishedAt } from './fetchAttestationPublishedAt.js';
|
|
8
10
|
import { fetchAbbreviatedMetadataCached, fetchFullMetadataCached, } from './fetchFullMetadataCached.js';
|
|
9
11
|
import { normalizeRegistryUrl } from './normalizeRegistryUrl.js';
|
|
10
|
-
import { BUILTIN_NAMED_REGISTRIES } from './parseBareSpecifier.js';
|
|
11
12
|
import { getPkgMetaCacheKey, getPkgMirrorPath, loadMeta, warnMissingTimeFieldOnce } from './pickPackage.js';
|
|
12
13
|
import { failIfTrustDowngraded } from './trustChecks.js';
|
|
13
|
-
import { MINIMUM_RELEASE_AGE_VIOLATION_CODE, MISSING_TARBALL_INTEGRITY_VIOLATION_CODE, TARBALL_URL_MISMATCH_VIOLATION_CODE, TRUST_DOWNGRADE_VIOLATION_CODE, } from './violationCodes.js';
|
|
14
|
+
import { MINIMUM_RELEASE_AGE_VIOLATION_CODE, MISSING_NAMED_REGISTRY_VIOLATION_CODE, MISSING_TARBALL_INTEGRITY_VIOLATION_CODE, TARBALL_URL_MISMATCH_VIOLATION_CODE, TRUST_DOWNGRADE_VIOLATION_CODE, } from './violationCodes.js';
|
|
14
15
|
/**
|
|
15
16
|
* Returns a `ResolutionVerifier` for npm-registry-resolved lockfile
|
|
16
17
|
* entries. It always binds each entry's recorded tarball URL to the
|
|
@@ -38,30 +39,8 @@ export function createNpmResolutionVerifier(opts) {
|
|
|
38
39
|
const trustExcludePolicy = opts.trustPolicyExclude?.length
|
|
39
40
|
? createExcludePolicy(opts.trustPolicyExclude, 'trustPolicyExclude')
|
|
40
41
|
: undefined;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// `https://npm/team-b/`) route to the longest matching prefix — matching
|
|
44
|
-
// only `origin` would silently send lookups to the wrong one. Built-in
|
|
45
|
-
// aliases (`gh:` → npm.pkg.github.com, etc.) are merged in alongside the
|
|
46
|
-
// user-defined ones so the verifier recognizes the same set of named
|
|
47
|
-
// registries the resolver does; otherwise a package resolved via `gh:`
|
|
48
|
-
// would land in the lockfile with a tarball URL the verifier can't route.
|
|
49
|
-
const namedRegistryPrefixes = Object.values({
|
|
50
|
-
...BUILTIN_NAMED_REGISTRIES,
|
|
51
|
-
...(opts.namedRegistries ?? {}),
|
|
52
|
-
})
|
|
53
|
-
.map((url) => {
|
|
54
|
-
const parsed = tryParseUrl(url);
|
|
55
|
-
if (!parsed)
|
|
56
|
-
return null;
|
|
57
|
-
// Ensure trailing slash so prefix matching against tarball URLs (which
|
|
58
|
-
// always include the package path under the registry root) does not
|
|
59
|
-
// accidentally match a sibling registry whose URL shares a prefix string.
|
|
60
|
-
const pathname = parsed.pathname.endsWith('/') ? parsed.pathname : `${parsed.pathname}/`;
|
|
61
|
-
return `${parsed.origin}${pathname}`;
|
|
62
|
-
})
|
|
63
|
-
.filter((value) => value != null)
|
|
64
|
-
.sort((a, b) => b.length - a.length);
|
|
42
|
+
const mergedNamedRegistries = normalizeNamedRegistries(opts.namedRegistries);
|
|
43
|
+
const namedRegistryPrefixes = namedRegistryTarballPrefixes(mergedNamedRegistries);
|
|
65
44
|
// Per-install dedup of every network/disk fetch the verifier issues.
|
|
66
45
|
// The maturity check uses the layered `fetchPublishedAt` lookup; the
|
|
67
46
|
// trust check uses an attestation fast-path before falling back to
|
|
@@ -85,7 +64,7 @@ export function createNpmResolutionVerifier(opts) {
|
|
|
85
64
|
const minimumReleaseAge = opts.minimumReleaseAge ?? 0;
|
|
86
65
|
const trustPolicy = opts.trustPolicy;
|
|
87
66
|
const trustPolicyIgnoreAfter = opts.trustPolicyIgnoreAfter;
|
|
88
|
-
const verify = async (resolution, { name, version, nonSemverVersion }) => {
|
|
67
|
+
const verify = async (resolution, { name, version, nonSemverVersion, registryName }) => {
|
|
89
68
|
if (!isRegistryTarballResolution(resolution))
|
|
90
69
|
return { ok: true };
|
|
91
70
|
// Network-free structural checks must run before registry metadata shortcuts.
|
|
@@ -120,7 +99,26 @@ export function createNpmResolutionVerifier(opts) {
|
|
|
120
99
|
};
|
|
121
100
|
}
|
|
122
101
|
const tarballUrl = typeof rawTarball === 'string' ? rawTarball : undefined;
|
|
123
|
-
|
|
102
|
+
let registry;
|
|
103
|
+
if (registryName != null) {
|
|
104
|
+
// Registry-qualified entries name their registry in the dep path, so
|
|
105
|
+
// routing does not depend on a recorded tarball URL (canonical URLs
|
|
106
|
+
// are omitted from the lockfile in the 12.0 format).
|
|
107
|
+
const namedRegistry = mergedNamedRegistries[registryName];
|
|
108
|
+
if (!namedRegistry) {
|
|
109
|
+
// Fail closed: without the registry URL, none of the metadata-backed
|
|
110
|
+
// checks below can vouch for this entry.
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
code: MISSING_NAMED_REGISTRY_VIOLATION_CODE,
|
|
114
|
+
reason: `was resolved from the named registry '${registryName}:', which is not present in the namedRegistries setting`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
registry = namedRegistry;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
registry = pickRegistryForVersion(opts.registries, namedRegistryPrefixes, name, tarballUrl);
|
|
121
|
+
}
|
|
124
122
|
// A registry entry that pins an explicit tarball URL must point at the
|
|
125
123
|
// artifact the registry's own metadata lists. Otherwise a trusted
|
|
126
124
|
// `name@version` could front bytes from an attacker-chosen URL (with a
|
|
@@ -164,6 +162,10 @@ export function createNpmResolutionVerifier(opts) {
|
|
|
164
162
|
// stays trusted after its exclude entry has been pulled.
|
|
165
163
|
const sortedMinAgeExcludes = [...new Set(opts.minimumReleaseAgeExclude ?? [])].sort();
|
|
166
164
|
const sortedTrustExcludes = [...new Set(opts.trustPolicyExclude ?? [])].sort();
|
|
165
|
+
const sortedNamedRegistries = Object.fromEntries(Object.entries(mergedNamedRegistries).sort(([aliasA], [aliasB]) => aliasA.localeCompare(aliasB)));
|
|
166
|
+
const namedRegistriesRouting = createHash('sha256')
|
|
167
|
+
.update(JSON.stringify(sortedNamedRegistries))
|
|
168
|
+
.digest('hex');
|
|
167
169
|
return {
|
|
168
170
|
verify,
|
|
169
171
|
policy: {
|
|
@@ -175,6 +177,7 @@ export function createNpmResolutionVerifier(opts) {
|
|
|
175
177
|
tarballUrlBinding: true,
|
|
176
178
|
// Same cache identity rule for the missing-integrity structural check.
|
|
177
179
|
integrityRequired: true,
|
|
180
|
+
namedRegistriesRouting,
|
|
178
181
|
minimumReleaseAge,
|
|
179
182
|
minimumReleaseAgeExclude: sortedMinAgeExcludes,
|
|
180
183
|
trustPolicy: trustPolicy ?? null,
|
|
@@ -190,6 +193,8 @@ export function createNpmResolutionVerifier(opts) {
|
|
|
190
193
|
// without the flag cannot prove they rejected unverifiable tarballs.
|
|
191
194
|
if (cached.integrityRequired !== true)
|
|
192
195
|
return false;
|
|
196
|
+
if (cached.namedRegistriesRouting !== namedRegistriesRouting)
|
|
197
|
+
return false;
|
|
193
198
|
// Maturity: a previously cached run under a larger cutoff
|
|
194
199
|
// (stricter window) is trustworthy under a smaller current one —
|
|
195
200
|
// its set of accepted versions is a subset of today's. The
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { pickRegistryForPackage } from '@pnpm/config.pick-registry-for-package';
|
|
3
|
+
import { isWellFormedRegistryName, RESERVED_VERSION_PREFIXES } from '@pnpm/deps.path';
|
|
3
4
|
import { PnpmError } from '@pnpm/error';
|
|
4
5
|
import { globalWarn } from '@pnpm/logger';
|
|
5
6
|
import { rangeSpecGranularity, versionWithRangeSpecStyle } from '@pnpm/pkg-manifest.utils';
|
|
@@ -539,6 +540,11 @@ function mergeNamedRegistries(userDefined) {
|
|
|
539
540
|
if (!userDefined)
|
|
540
541
|
return merged;
|
|
541
542
|
for (const [alias, url] of Object.entries(userDefined)) {
|
|
543
|
+
if (RESERVED_VERSION_PREFIXES.has(alias) || !isWellFormedRegistryName(alias)) {
|
|
544
|
+
throw new PnpmError('RESERVED_NAMED_REGISTRY_NAME', RESERVED_VERSION_PREFIXES.has(alias)
|
|
545
|
+
? `'${alias}' cannot be used as a named registry alias: it is a reserved dependency specifier prefix.`
|
|
546
|
+
: `'${alias}' cannot be used as a named registry alias: aliases must start with a letter and contain only letters, digits, ".", "_", and "-".`, { hint: 'Rename the entry in the namedRegistries setting.' });
|
|
547
|
+
}
|
|
542
548
|
if (typeof url !== 'string' || !isValidHttpUrl(url)) {
|
|
543
549
|
throw new PnpmError('INVALID_NAMED_REGISTRY_URL', `The named registry alias '${alias}' is mapped to '${String(url)}', which is not a valid http(s) URL.`, { hint: 'Provide a URL that starts with http:// or https://, e.g. https://npm.pkg.example.com/' });
|
|
544
550
|
}
|
|
@@ -574,6 +580,11 @@ async function resolveFromNamedRegistry(ctx, wantedDependency, opts) {
|
|
|
574
580
|
const picked = await pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, registry);
|
|
575
581
|
return {
|
|
576
582
|
...picked,
|
|
583
|
+
// Qualifying the id with the registry alias is what keeps the same
|
|
584
|
+
// name@version resolved from two registries distinct in the lockfile.
|
|
585
|
+
// Without it they collapse onto one entry and whichever resolved first
|
|
586
|
+
// decides the tarball both consumers get.
|
|
587
|
+
id: `${picked.manifest.name}@${spec.registryName}:${picked.manifest.version}`,
|
|
577
588
|
normalizedBareSpecifier: opts.calcSpecifier
|
|
578
589
|
? calcPrefixedSpecifier(`${spec.registryName}:`, spec.name, wantedDependency, picked.manifest.version, opts.rangeSpecStyle)
|
|
579
590
|
: undefined,
|
|
@@ -9,7 +9,7 @@ export interface JsrRegistryPackageSpec extends RegistryPackageSpec {
|
|
|
9
9
|
jsrPkgName: string;
|
|
10
10
|
}
|
|
11
11
|
export declare function parseJsrSpecifierToRegistryPackageSpec(rawSpecifier: string, alias: string | undefined, defaultTag: string): JsrRegistryPackageSpec | null;
|
|
12
|
-
export
|
|
12
|
+
export { BUILTIN_NAMED_REGISTRIES } from '@pnpm/constants';
|
|
13
13
|
export interface NamedRegistryPackageSpec extends RegistryPackageSpec {
|
|
14
14
|
registryName: string;
|
|
15
15
|
}
|
|
@@ -64,9 +64,7 @@ export function parseJsrSpecifierToRegistryPackageSpec(rawSpecifier, alias, defa
|
|
|
64
64
|
jsrPkgName: spec.jsrPkgName,
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
|
-
export
|
|
68
|
-
gh: 'https://npm.pkg.github.com/',
|
|
69
|
-
});
|
|
67
|
+
export { BUILTIN_NAMED_REGISTRIES } from '@pnpm/constants';
|
|
70
68
|
// Parses a named-registry specifier of the shape `<alias>:<body>` into a
|
|
71
69
|
// RegistryPackageSpec. Returns `null` when the specifier does not use one of
|
|
72
70
|
// the configured aliases, so the caller can fall through to other resolvers.
|
package/lib/pickPackage.js
CHANGED
|
@@ -191,13 +191,29 @@ export async function pickPackage(ctx, spec, opts) {
|
|
|
191
191
|
};
|
|
192
192
|
let diskMeta;
|
|
193
193
|
if (ctx.offline === true || ctx.preferOffline === true || opts.pickLowestVersion) {
|
|
194
|
-
|
|
194
|
+
// Concurrent offline picks of one package all miss the pre-queue cache
|
|
195
|
+
// check and queue behind this limiter, so the check is repeated inside
|
|
196
|
+
// the queue and the promotion happens before the limiter releases —
|
|
197
|
+
// otherwise every queued pick re-reads and re-parses the mirror. Serving
|
|
198
|
+
// a queued pick from the cache is equivalent to it having arrived after
|
|
199
|
+
// the first caller cached it: offline entries are always disk-sourced
|
|
200
|
+
// and maybeUpgradeAbbreviatedMetaForReleaseAge short-circuits when
|
|
201
|
+
// offline, so an in-memory hit returns this same meta with no network
|
|
202
|
+
// access.
|
|
203
|
+
diskMeta = await limit(async () => {
|
|
204
|
+
if (ctx.offline !== true)
|
|
205
|
+
return loadMetaCondensed();
|
|
206
|
+
const cached = ctx.metaCache.get(cacheKey);
|
|
207
|
+
if (cached != null)
|
|
208
|
+
return cached;
|
|
209
|
+
const meta = await loadMetaCondensed();
|
|
210
|
+
if (meta != null) {
|
|
211
|
+
cacheDiskLoadedMeta(ctx.metaCache, cacheKey, meta);
|
|
212
|
+
}
|
|
213
|
+
return meta;
|
|
214
|
+
});
|
|
195
215
|
if (ctx.offline) {
|
|
196
216
|
if (diskMeta != null) {
|
|
197
|
-
// maybeUpgradeAbbreviatedMetaForReleaseAge short-circuits when
|
|
198
|
-
// offline, so a later in-memory cache hit returns this same meta
|
|
199
|
-
// without any network access.
|
|
200
|
-
cacheDiskLoadedMeta(ctx.metaCache, cacheKey, diskMeta);
|
|
201
217
|
return {
|
|
202
218
|
meta: diskMeta,
|
|
203
219
|
pickedPackage: pickMatchingVersionFinal(pickerOpts, spec, diskMeta),
|
|
@@ -90,36 +90,11 @@ function parseModifiedDate(modified) {
|
|
|
90
90
|
return null;
|
|
91
91
|
return date;
|
|
92
92
|
}
|
|
93
|
-
const semverRangeCache = new Map();
|
|
94
|
-
// This is a performance optimization; working with string-ish semver
|
|
95
|
-
// causes lots of allocations and repeated work, but caching the Range
|
|
96
|
-
// and ensuring we give it a SemVer instance greatly speeds things up.
|
|
97
|
-
function semverSatisfiesLoose(version, range) {
|
|
98
|
-
let semverRange = semverRangeCache.get(range);
|
|
99
|
-
if (semverRange === undefined) {
|
|
100
|
-
try {
|
|
101
|
-
semverRange = new semver.Range(range, true);
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
semverRange = null;
|
|
105
|
-
}
|
|
106
|
-
semverRangeCache.set(range, semverRange);
|
|
107
|
-
}
|
|
108
|
-
if (semverRange) {
|
|
109
|
-
try {
|
|
110
|
-
return semverRange.test(new semver.SemVer(version, true));
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
93
|
export function pickLowestVersionByVersionRange({ meta, versionRange, preferredVersionSelectors }) {
|
|
119
94
|
if (preferredVersionSelectors != null && Object.keys(preferredVersionSelectors).length > 0) {
|
|
120
95
|
const prioritizedPreferredVersions = prioritizePreferredVersions(meta, versionRange, preferredVersionSelectors);
|
|
121
96
|
for (const preferredVersions of prioritizedPreferredVersions) {
|
|
122
|
-
const preferredVersion =
|
|
97
|
+
const preferredVersion = minSatisfyingLoose(preferredVersions, versionRange);
|
|
123
98
|
if (preferredVersion) {
|
|
124
99
|
return preferredVersion;
|
|
125
100
|
}
|
|
@@ -128,7 +103,7 @@ export function pickLowestVersionByVersionRange({ meta, versionRange, preferredV
|
|
|
128
103
|
if (versionRange === '*') {
|
|
129
104
|
return Object.keys(meta.versions).sort(semver.compare)[0];
|
|
130
105
|
}
|
|
131
|
-
return
|
|
106
|
+
return minSatisfyingLoose(Object.keys(meta.versions), versionRange);
|
|
132
107
|
}
|
|
133
108
|
export function pickVersionByVersionRange({ meta, versionRange, preferredVersionSelectors }) {
|
|
134
109
|
const latest = meta['dist-tags'].latest;
|
|
@@ -138,7 +113,7 @@ export function pickVersionByVersionRange({ meta, versionRange, preferredVersion
|
|
|
138
113
|
if (preferredVersions.includes(latest) && semverSatisfiesLoose(latest, versionRange)) {
|
|
139
114
|
return latest;
|
|
140
115
|
}
|
|
141
|
-
const preferredVersion =
|
|
116
|
+
const preferredVersion = maxSatisfyingLoose(preferredVersions, versionRange);
|
|
142
117
|
if (preferredVersion) {
|
|
143
118
|
return preferredVersion;
|
|
144
119
|
}
|
|
@@ -150,13 +125,13 @@ export function pickVersionByVersionRange({ meta, versionRange, preferredVersion
|
|
|
150
125
|
// E.g.: 1.0.0-beta.1. See issue: https://github.com/pnpm/pnpm/issues/865
|
|
151
126
|
return latest;
|
|
152
127
|
}
|
|
153
|
-
const maxVersion =
|
|
128
|
+
const maxVersion = maxSatisfyingLoose(versions, versionRange);
|
|
154
129
|
// if the selected version is deprecated, try to find a non-deprecated one that satisfies the range
|
|
155
130
|
if (maxVersion && meta.versions[maxVersion].deprecated && versions.length > 1) {
|
|
156
131
|
const nonDeprecatedVersions = versions.map((version) => meta.versions[version])
|
|
157
132
|
.filter((versionMeta) => !versionMeta.deprecated)
|
|
158
133
|
.map((versionMeta) => versionMeta.version);
|
|
159
|
-
const maxNonDeprecatedVersion =
|
|
134
|
+
const maxNonDeprecatedVersion = maxSatisfyingLoose(nonDeprecatedVersions, versionRange);
|
|
160
135
|
if (maxNonDeprecatedVersion)
|
|
161
136
|
return maxNonDeprecatedVersion;
|
|
162
137
|
}
|
|
@@ -224,4 +199,76 @@ class PreferredVersionsPrioritizer {
|
|
|
224
199
|
.map((weight) => versionsByWeight[parseInt(weight, 10)]);
|
|
225
200
|
}
|
|
226
201
|
}
|
|
202
|
+
function semverSatisfiesLoose(version, range) {
|
|
203
|
+
const semverRange = parseRangeLoose(range);
|
|
204
|
+
if (semverRange == null)
|
|
205
|
+
return false;
|
|
206
|
+
const parsedVersion = parseSemverLoose(version);
|
|
207
|
+
return parsedVersion != null && semverRange.test(parsedVersion);
|
|
208
|
+
}
|
|
209
|
+
// semver's own maxSatisfying/minSatisfying re-parse the range and every
|
|
210
|
+
// version string on each call, which dominates resolution time on large
|
|
211
|
+
// packuments; these reuse the parse caches instead.
|
|
212
|
+
function maxSatisfyingLoose(versions, range) {
|
|
213
|
+
return findSatisfyingLoose(versions, range, (candidate, best) => candidate.compare(best) > 0);
|
|
214
|
+
}
|
|
215
|
+
function minSatisfyingLoose(versions, range) {
|
|
216
|
+
return findSatisfyingLoose(versions, range, (candidate, best) => candidate.compare(best) < 0);
|
|
217
|
+
}
|
|
218
|
+
function findSatisfyingLoose(versions, range, isBetter) {
|
|
219
|
+
const semverRange = parseRangeLoose(range);
|
|
220
|
+
if (semverRange == null)
|
|
221
|
+
return null;
|
|
222
|
+
let bestVersion = null;
|
|
223
|
+
let bestParsed = null;
|
|
224
|
+
for (const version of versions) {
|
|
225
|
+
const parsed = parseSemverLoose(version);
|
|
226
|
+
if (parsed == null || !semverRange.test(parsed))
|
|
227
|
+
continue;
|
|
228
|
+
if (bestParsed == null || isBetter(parsed, bestParsed)) {
|
|
229
|
+
bestVersion = version;
|
|
230
|
+
bestParsed = parsed;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return bestVersion;
|
|
234
|
+
}
|
|
235
|
+
function parseRangeLoose(range) {
|
|
236
|
+
let semverRange = semverRangeCache.get(range);
|
|
237
|
+
if (semverRange === undefined) {
|
|
238
|
+
try {
|
|
239
|
+
semverRange = new semver.Range(range, true);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
semverRange = null;
|
|
243
|
+
}
|
|
244
|
+
if (semverRangeCache.size >= SEMVER_CACHE_MAX_SIZE)
|
|
245
|
+
semverRangeCache.clear();
|
|
246
|
+
semverRangeCache.set(range, semverRange);
|
|
247
|
+
}
|
|
248
|
+
return semverRange;
|
|
249
|
+
}
|
|
250
|
+
function parseSemverLoose(version) {
|
|
251
|
+
let parsed = semverInstanceCache.get(version);
|
|
252
|
+
if (parsed === undefined) {
|
|
253
|
+
try {
|
|
254
|
+
parsed = new semver.SemVer(version, true);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
parsed = null;
|
|
258
|
+
}
|
|
259
|
+
if (semverInstanceCache.size >= SEMVER_CACHE_MAX_SIZE)
|
|
260
|
+
semverInstanceCache.clear();
|
|
261
|
+
semverInstanceCache.set(version, parsed);
|
|
262
|
+
}
|
|
263
|
+
return parsed;
|
|
264
|
+
}
|
|
265
|
+
// Working with string-ish semver causes lots of allocations and repeated
|
|
266
|
+
// work, and a dependency graph tests the same ranges and versions over and
|
|
267
|
+
// over, so both parses are cached. Parse failures are cached as null, so a
|
|
268
|
+
// malformed version string is never re-parsed either. The caches are dropped
|
|
269
|
+
// wholesale once they grow past this size, so a long-lived process (daemon,
|
|
270
|
+
// store server) can't retain them without bound.
|
|
271
|
+
const SEMVER_CACHE_MAX_SIZE = 50_000;
|
|
272
|
+
const semverRangeCache = new Map();
|
|
273
|
+
const semverInstanceCache = new Map();
|
|
227
274
|
//# sourceMappingURL=pickPackageFromMeta.js.map
|
package/lib/violationCodes.d.ts
CHANGED
|
@@ -12,3 +12,4 @@ export declare const MINIMUM_RELEASE_AGE_VIOLATION_CODE = "MINIMUM_RELEASE_AGE_V
|
|
|
12
12
|
export declare const TRUST_DOWNGRADE_VIOLATION_CODE = "TRUST_DOWNGRADE";
|
|
13
13
|
export declare const TARBALL_URL_MISMATCH_VIOLATION_CODE = "TARBALL_URL_MISMATCH";
|
|
14
14
|
export declare const MISSING_TARBALL_INTEGRITY_VIOLATION_CODE = "MISSING_TARBALL_INTEGRITY";
|
|
15
|
+
export declare const MISSING_NAMED_REGISTRY_VIOLATION_CODE = "MISSING_NAMED_REGISTRY";
|
package/lib/violationCodes.js
CHANGED
|
@@ -12,4 +12,5 @@ export const MINIMUM_RELEASE_AGE_VIOLATION_CODE = 'MINIMUM_RELEASE_AGE_VIOLATION
|
|
|
12
12
|
export const TRUST_DOWNGRADE_VIOLATION_CODE = 'TRUST_DOWNGRADE';
|
|
13
13
|
export const TARBALL_URL_MISMATCH_VIOLATION_CODE = 'TARBALL_URL_MISMATCH';
|
|
14
14
|
export const MISSING_TARBALL_INTEGRITY_VIOLATION_CODE = 'MISSING_TARBALL_INTEGRITY';
|
|
15
|
+
export const MISSING_NAMED_REGISTRY_VIOLATION_CODE = 'MISSING_NAMED_REGISTRY';
|
|
15
16
|
//# sourceMappingURL=violationCodes.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/resolving.npm-resolver",
|
|
3
|
-
"version": "1103.
|
|
3
|
+
"version": "1103.1.0",
|
|
4
4
|
"description": "Resolver for npm-hosted packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pnpm",
|
|
@@ -29,22 +29,24 @@
|
|
|
29
29
|
"!*.map"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@pnpm/config.
|
|
33
|
-
"@pnpm/config.
|
|
34
|
-
"@pnpm/
|
|
35
|
-
"@pnpm/
|
|
32
|
+
"@pnpm/config.normalize-registries": "1100.1.0",
|
|
33
|
+
"@pnpm/config.pick-registry-for-package": "1100.1.0",
|
|
34
|
+
"@pnpm/config.version-policy": "1100.1.12",
|
|
35
|
+
"@pnpm/constants": "1101.0.0",
|
|
36
|
+
"@pnpm/core-loggers": "1100.3.2",
|
|
36
37
|
"@pnpm/crypto.hash": "1100.0.2",
|
|
37
|
-
"@pnpm/
|
|
38
|
+
"@pnpm/deps.path": "1100.1.0",
|
|
39
|
+
"@pnpm/error": "1100.1.1",
|
|
38
40
|
"@pnpm/fetching.types": "1100.0.3",
|
|
39
41
|
"@pnpm/fs.graceful-fs": "1100.1.1",
|
|
40
|
-
"@pnpm/pkg-manifest.utils": "1100.3.
|
|
41
|
-
"@pnpm/resolving.jsr-specifier-parser": "1100.0.
|
|
42
|
-
"@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.
|
|
43
|
-
"@pnpm/resolving.registry.types": "1100.1.
|
|
44
|
-
"@pnpm/resolving.resolver-base": "1101.
|
|
45
|
-
"@pnpm/store.cafs": "1100.1.
|
|
46
|
-
"@pnpm/store.index": "1100.2.
|
|
47
|
-
"@pnpm/types": "1101.
|
|
42
|
+
"@pnpm/pkg-manifest.utils": "1100.3.1",
|
|
43
|
+
"@pnpm/resolving.jsr-specifier-parser": "1100.0.4",
|
|
44
|
+
"@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.15",
|
|
45
|
+
"@pnpm/resolving.registry.types": "1100.1.9",
|
|
46
|
+
"@pnpm/resolving.resolver-base": "1101.1.0",
|
|
47
|
+
"@pnpm/store.cafs": "1100.1.18",
|
|
48
|
+
"@pnpm/store.index": "1100.2.3",
|
|
49
|
+
"@pnpm/types": "1101.9.0",
|
|
48
50
|
"@pnpm/workspace.range-resolver": "1100.0.3",
|
|
49
51
|
"@pnpm/workspace.spec-parser": "1100.0.1",
|
|
50
52
|
"@zkochan/retry": "^0.2.0",
|
|
@@ -64,13 +66,13 @@
|
|
|
64
66
|
},
|
|
65
67
|
"peerDependencies": {
|
|
66
68
|
"@pnpm/logger": "^1100.0.0",
|
|
67
|
-
"@pnpm/worker": "^1100.2.
|
|
69
|
+
"@pnpm/worker": "^1100.2.10"
|
|
68
70
|
},
|
|
69
71
|
"devDependencies": {
|
|
70
72
|
"@jest/globals": "30.4.1",
|
|
71
73
|
"@pnpm/logger": "1100.0.0",
|
|
72
|
-
"@pnpm/network.fetch": "1100.1.
|
|
73
|
-
"@pnpm/resolving.npm-resolver": "1103.
|
|
74
|
+
"@pnpm/network.fetch": "1100.1.11",
|
|
75
|
+
"@pnpm/resolving.npm-resolver": "1103.1.0",
|
|
74
76
|
"@pnpm/test-fixtures": "1100.0.1",
|
|
75
77
|
"@pnpm/testing.mock-agent": "1101.0.7",
|
|
76
78
|
"@types/normalize-path": "^3.0.2",
|