@pnpm/resolving.npm-resolver 1102.1.0 → 1102.1.1

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.
@@ -0,0 +1,16 @@
1
+ import type { PackageMeta } from '@pnpm/resolving.registry.types';
2
+ /**
3
+ * Reduces a package metadata document to the abbreviated field set that the
4
+ * resolver actually reads, dropping install-irrelevant fields (scripts,
5
+ * exports, readme, custom `_`-prefixed fields, etc.).
6
+ *
7
+ * Used in two places:
8
+ * - The network layer (`fetch.ts`) normalizes a registry that ignored the
9
+ * abbreviated `Accept` header and returned a full document.
10
+ * - The resolver (`pickPackage.ts`) narrows a deliberately-fetched full
11
+ * document into the `filterMetadata` cache slot.
12
+ *
13
+ * Null-safe on `versions` so it can be called on an unpublished package (no
14
+ * versions), which the abbreviated path can reach.
15
+ */
16
+ export declare function clearMeta(pkg: PackageMeta): PackageMeta;
@@ -0,0 +1,55 @@
1
+ import { pick } from 'ramda';
2
+ // The list taken from https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
3
+ // with the addition of 'libc'
4
+ const ABBREVIATED_VERSION_FIELDS = [
5
+ 'name',
6
+ 'version',
7
+ 'bin',
8
+ 'directories',
9
+ 'devDependencies',
10
+ 'optionalDependencies',
11
+ 'dependencies',
12
+ 'peerDependencies',
13
+ 'dist',
14
+ 'engines',
15
+ 'peerDependenciesMeta',
16
+ 'cpu',
17
+ 'os',
18
+ 'libc',
19
+ 'deprecated',
20
+ 'bundleDependencies',
21
+ 'bundledDependencies',
22
+ 'hasInstallScript',
23
+ '_npmUser',
24
+ ];
25
+ /**
26
+ * Reduces a package metadata document to the abbreviated field set that the
27
+ * resolver actually reads, dropping install-irrelevant fields (scripts,
28
+ * exports, readme, custom `_`-prefixed fields, etc.).
29
+ *
30
+ * Used in two places:
31
+ * - The network layer (`fetch.ts`) normalizes a registry that ignored the
32
+ * abbreviated `Accept` header and returned a full document.
33
+ * - The resolver (`pickPackage.ts`) narrows a deliberately-fetched full
34
+ * document into the `filterMetadata` cache slot.
35
+ *
36
+ * Null-safe on `versions` so it can be called on an unpublished package (no
37
+ * versions), which the abbreviated path can reach.
38
+ */
39
+ export function clearMeta(pkg) {
40
+ // A null prototype so that a registry-controlled version key named
41
+ // `__proto__` becomes a regular own property instead of mutating the
42
+ // prototype of the map (js/prototype-polluting-assignment).
43
+ const versions = Object.create(null);
44
+ for (const [version, info] of Object.entries(pkg.versions ?? {})) {
45
+ versions[version] = pick(ABBREVIATED_VERSION_FIELDS, info);
46
+ }
47
+ return {
48
+ name: pkg.name,
49
+ 'dist-tags': pkg['dist-tags'],
50
+ versions,
51
+ time: pkg.time,
52
+ modified: pkg.modified,
53
+ };
54
+ }
55
+ //# sourceMappingURL=clearMeta.js.map
package/lib/fetch.js CHANGED
@@ -5,6 +5,15 @@ import { FetchError, PnpmError, redactUrlCredentials, } 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
+ import { clearMeta } from './clearMeta.js';
9
+ /**
10
+ * Content type of an abbreviated (install-oriented) package metadata document.
11
+ * A spec-compliant registry echoes this in the response `Content-Type` when it
12
+ * honors the abbreviated `Accept` header. Its absence signals that the registry
13
+ * ignored the header and served the full document instead.
14
+ * https://github.com/npm/registry/blob/main/docs/responses/package-metadata.md
15
+ */
16
+ const ABBREVIATED_META_CONTENT_TYPE = 'application/vnd.npm.install-v1+json';
8
17
  export class RegistryResponseError extends FetchError {
9
18
  pkgName;
10
19
  constructor(request, response, pkgName) {
@@ -125,8 +134,7 @@ export async function fetchMetadataFromFromRegistry(fetchOpts, pkgName, { authHe
125
134
  globalWarn(`Request took ${elapsedMs}ms: ${uri}`);
126
135
  }
127
136
  resolve({
128
- meta,
129
- jsonText,
137
+ ...normalizeAbbreviatedResponse({ fullMetadata, meta, jsonText, response }),
130
138
  etag: response.headers.get('etag') ?? undefined,
131
139
  });
132
140
  }
@@ -156,6 +164,38 @@ export async function fetchMetadataFromFromRegistry(fetchOpts, pkgName, { authHe
156
164
  });
157
165
  });
158
166
  }
167
+ /**
168
+ * When the resolver asked for abbreviated metadata but the registry ignored the
169
+ * `Accept` header and returned the full document (detected via the response
170
+ * `Content-Type`), strip it down to the abbreviated field set so downstream
171
+ * consumers — the in-memory cache, the on-disk mirror, and the resolver — never
172
+ * carry the megabytes of install-irrelevant data (scripts, exports, readme,
173
+ * custom fields) that a full document contains.
174
+ *
175
+ * Registries that honor the header (e.g. the npm registry) echo the abbreviated
176
+ * `Content-Type`, so this is a no-op for them: no re-serialization, no field
177
+ * stripping — the happy path pays nothing.
178
+ */
179
+ function normalizeAbbreviatedResponse({ fullMetadata, meta, jsonText, response }) {
180
+ if (fullMetadata)
181
+ return { meta, jsonText };
182
+ if (parseMediaType(response.headers.get('content-type')) === ABBREVIATED_META_CONTENT_TYPE)
183
+ return { meta, jsonText };
184
+ const normalized = clearMeta(meta);
185
+ return { meta: normalized, jsonText: JSON.stringify(normalized) };
186
+ }
187
+ /**
188
+ * Extracts the media type from a `Content-Type` header value, dropping
189
+ * parameters such as `; charset=utf-8`. Media types are case-insensitive
190
+ * (RFC 9110 §8.3.1), so the result is lowercased for comparison.
191
+ */
192
+ function parseMediaType(contentType) {
193
+ if (contentType == null)
194
+ return undefined;
195
+ const semicolonIndex = contentType.indexOf(';');
196
+ const mediaType = semicolonIndex === -1 ? contentType : contentType.slice(0, semicolonIndex);
197
+ return mediaType.trim().toLowerCase();
198
+ }
159
199
  function toUri(pkgName, registry) {
160
200
  let encodedName;
161
201
  if (pkgName[0] === '@') {
package/lib/index.d.ts CHANGED
@@ -94,6 +94,8 @@ export interface ResolveFromNpmContext {
94
94
  name?: string;
95
95
  version?: string;
96
96
  }) => Promise<DependencyManifest | undefined>;
97
+ /** Deduplicates the held-back-update warning per `(name, picked, preferred)`. */
98
+ warnedHeldBackUpdates: Set<string>;
97
99
  }
98
100
  export type ResolveFromNpmOptions = {
99
101
  alwaysTryWorkspacePackages?: boolean;
@@ -109,6 +111,7 @@ export type ResolveFromNpmOptions = {
109
111
  preferredVersions?: PreferredVersions;
110
112
  preferWorkspacePackages?: boolean;
111
113
  update?: false | 'compatible' | 'latest';
114
+ updateRequested?: boolean;
112
115
  updateChecksums?: boolean;
113
116
  injectWorkspacePackages?: boolean;
114
117
  calcSpecifier?: boolean;
package/lib/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import path from 'node:path';
2
2
  import { pickRegistryForPackage } from '@pnpm/config.pick-registry-for-package';
3
3
  import { PnpmError } from '@pnpm/error';
4
+ import { globalWarn } from '@pnpm/logger';
5
+ import { EXISTING_VERSION_SELECTOR_WEIGHT, } from '@pnpm/resolving.resolver-base';
4
6
  import { storeIndexKey } from '@pnpm/store.index';
5
7
  import { readPkgFromCafs, } from '@pnpm/worker';
6
8
  import { resolveWorkspaceRange } from '@pnpm/workspace.range-resolver';
@@ -130,6 +132,7 @@ export function createNpmResolver(fetchFromRegistry, getAuthHeader, opts) {
130
132
  namedRegistryNames,
131
133
  saveWorkspaceProtocol: opts.saveWorkspaceProtocol,
132
134
  peekManifestFromStore,
135
+ warnedHeldBackUpdates: new Set(),
133
136
  };
134
137
  const boundResolveFromNpm = resolveNpm.bind(null, ctx);
135
138
  const boundResolveFromJsr = resolveJsr.bind(null, ctx);
@@ -150,6 +153,96 @@ export function createNpmResolver(fetchFromRegistry, getAuthHeader, opts) {
150
153
  },
151
154
  };
152
155
  }
156
+ /**
157
+ * The preferred-version selectors to hand the package picker for `pkgName`.
158
+ *
159
+ * When this package is the user's update target (`updateRequested`), the
160
+ * lockfile's contribution to its selectors is removed so the target
161
+ * re-resolves exactly the way a fresh install would after its lockfile
162
+ * entries were deleted. Everything a fresh install applies is preserved:
163
+ * manifest pins, the versions propagated down the dependency chain, and the
164
+ * negative-weight `range` penalties that `pnpm audit --fix` injects to steer
165
+ * resolution away from vulnerable versions.
166
+ */
167
+ function preferredVersionSelectorsFor(opts, pkgName) {
168
+ const selectors = opts.preferredVersions?.[pkgName];
169
+ if (!opts.updateRequested)
170
+ return selectors;
171
+ return stripLockfileVersionPins(selectors);
172
+ }
173
+ /**
174
+ * Remove the lockfile-derived part of the selectors: the concrete pins
175
+ * `getPreferredVersionsFromLockfileAndManifests` seeds at
176
+ * `EXISTING_VERSION_SELECTOR_WEIGHT` — added onto the manifest weight when a
177
+ * manifest entry pins the same version, so the lockfile weight is subtracted
178
+ * rather than the selector dropped, leaving the manifest contribution in
179
+ * effect. Selectors a fresh install would also apply (manifest pins,
180
+ * chain-propagated versions, `range`/`tag` selectors) pass through unchanged.
181
+ * Returns `undefined` when nothing remains.
182
+ */
183
+ function stripLockfileVersionPins(selectors) {
184
+ if (selectors == null)
185
+ return undefined;
186
+ let kept;
187
+ for (const [selector, value] of Object.entries(selectors)) {
188
+ let keptValue = value;
189
+ if (typeof value !== 'string' && value.selectorType === 'version' && value.weight >= EXISTING_VERSION_SELECTOR_WEIGHT) {
190
+ const manifestWeight = value.weight - EXISTING_VERSION_SELECTOR_WEIGHT;
191
+ if (manifestWeight <= 0)
192
+ continue;
193
+ keptValue = { selectorType: 'version', weight: manifestWeight };
194
+ }
195
+ // Null-prototype: selector keys come from manifests and the lockfile,
196
+ // and a dist-tag named `__proto__` is a valid selector key.
197
+ kept ??= Object.create(null);
198
+ kept[selector] = keptValue;
199
+ }
200
+ return kept;
201
+ }
202
+ /**
203
+ * During a targeted update the picker still honors the preferred versions a
204
+ * fresh install would apply (manifest pins and versions propagated down the
205
+ * dependency chain), so the target can legitimately settle below the highest
206
+ * version its range admits. Surface that once per package: reaching the
207
+ * newer version everywhere is an override's job, not an update's.
208
+ *
209
+ * The baseline for "held back" is the pick with only the non-pin selectors
210
+ * applied — `range`/`tag` selectors such as the `pnpm audit --fix`
211
+ * vulnerability penalties steer the baseline too, so the warning never
212
+ * recommends a version those selectors avoid.
213
+ *
214
+ * The recommended override is scoped to the declared range being resolved
215
+ * (`name@<range>`), so applying it can never violate any consumer's range:
216
+ * only declarations of exactly this range match the selector, and the
217
+ * recommended version satisfies it by construction.
218
+ */
219
+ function warnOnceOnHeldBackUpdate(ctx, opts, spec, meta, pickedVersion) {
220
+ if (!opts.updateRequested || spec.type !== 'range')
221
+ return;
222
+ const selectors = preferredVersionSelectorsFor(opts, spec.name);
223
+ if (selectors == null)
224
+ return;
225
+ let nonPinSelectors;
226
+ for (const [selector, value] of Object.entries(selectors)) {
227
+ if ((typeof value === 'string' ? value : value.selectorType) === 'version')
228
+ continue;
229
+ // Null-prototype for the same reason as in `stripLockfileVersionPins`.
230
+ nonPinSelectors ??= Object.create(null);
231
+ nonPinSelectors[selector] = value;
232
+ }
233
+ const preferred = pickVersionByVersionRange({
234
+ meta,
235
+ versionRange: spec.fetchSpec,
236
+ preferredVersionSelectors: nonPinSelectors,
237
+ });
238
+ if (preferred == null || preferred === pickedVersion)
239
+ return;
240
+ const key = `${spec.name}@${spec.fetchSpec}:${pickedVersion}<${preferred}`;
241
+ if (ctx.warnedHeldBackUpdates.has(key))
242
+ return;
243
+ ctx.warnedHeldBackUpdates.add(key);
244
+ globalWarn(`"${spec.name}@${spec.fetchSpec}" was updated to ${pickedVersion}, not ${preferred}, to match the version preferred by your manifests and already installed dependencies. To use ${preferred}, add an override to pnpm-workspace.yaml: overrides: { "${spec.name}@${spec.fetchSpec}": "${preferred}" }`);
245
+ }
153
246
  function isNpmSpec(query, defaultRegistry) {
154
247
  const { alias, bareSpecifier } = query.wantedDependency;
155
248
  if (!bareSpecifier)
@@ -285,7 +378,7 @@ async function resolveNpm(ctx, wantedDependency, opts) {
285
378
  publishedByExclude: opts.publishedByExclude,
286
379
  authHeaderValue,
287
380
  dryRun: opts.dryRun === true,
288
- preferredVersionSelectors: opts.preferredVersions?.[spec.name],
381
+ preferredVersionSelectors: preferredVersionSelectorsFor(opts, spec.name),
289
382
  registry,
290
383
  includeLatestTag: opts.update === 'latest',
291
384
  updateChecksums: opts.updateChecksums,
@@ -306,8 +399,13 @@ async function resolveNpm(ctx, wantedDependency, opts) {
306
399
  pinnedVersion: opts.pinnedVersion,
307
400
  });
308
401
  }
309
- catch {
310
- // ignore
402
+ catch (workspaceErr) {
403
+ // When the registry doesn't have the package and the workspace has it
404
+ // only at non-matching versions, the mismatch error (which lists the
405
+ // available workspace versions) is more actionable than the raw 404.
406
+ if (err.code === 'ERR_PNPM_FETCH_404' && workspaceErr.code === 'ERR_PNPM_NO_MATCHING_VERSION_INSIDE_WORKSPACE') {
407
+ throw workspaceErr;
408
+ }
311
409
  }
312
410
  }
313
411
  throw err;
@@ -328,8 +426,13 @@ async function resolveNpm(ctx, wantedDependency, opts) {
328
426
  pinnedVersion: opts.pinnedVersion,
329
427
  });
330
428
  }
331
- catch {
332
- // ignore
429
+ catch (workspaceErr) {
430
+ // Neither the registry nor the workspace has a matching version; the
431
+ // workspace mismatch error carries the available local versions,
432
+ // which is the actionable detail here.
433
+ if (workspaceErr.code === 'ERR_PNPM_NO_MATCHING_VERSION_INSIDE_WORKSPACE') {
434
+ throw workspaceErr;
435
+ }
333
436
  }
334
437
  }
335
438
  throw new NoMatchingVersionError({ wantedDependency, packageMeta: meta, registry });
@@ -370,6 +473,7 @@ async function resolveNpm(ctx, wantedDependency, opts) {
370
473
  };
371
474
  }
372
475
  }
476
+ warnOnceOnHeldBackUpdate(ctx, opts, spec, meta, pickedPackage.version);
373
477
  const id = `${pickedPackage.name}@${pickedPackage.version}`;
374
478
  const resolution = {
375
479
  integrity: getIntegrity(pickedPackage.dist),
@@ -489,7 +593,7 @@ async function pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, registr
489
593
  publishedByExclude: opts.publishedByExclude,
490
594
  authHeaderValue,
491
595
  dryRun: opts.dryRun === true,
492
- preferredVersionSelectors: opts.preferredVersions?.[spec.name],
596
+ preferredVersionSelectors: preferredVersionSelectorsFor(opts, spec.name),
493
597
  registry,
494
598
  includeLatestTag: opts.update === 'latest',
495
599
  updateChecksums: opts.updateChecksums,
@@ -498,6 +602,7 @@ async function pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, registr
498
602
  if (pickedPackage == null) {
499
603
  throw new NoMatchingVersionError({ wantedDependency, packageMeta: meta, registry });
500
604
  }
605
+ warnOnceOnHeldBackUpdate(ctx, opts, spec, meta, pickedPackage.version);
501
606
  const resolution = {
502
607
  integrity: getIntegrity(pickedPackage.dist),
503
608
  tarball: normalizeRegistryUrl(pickedPackage.dist.tarball),
@@ -2,6 +2,7 @@ import { PnpmError } from '@pnpm/error';
2
2
  import { parseJsrSpecifier } from '@pnpm/resolving.jsr-specifier-parser';
3
3
  import { parseNpmTarballUrl } from 'parse-npm-tarball-url';
4
4
  import semver from 'semver';
5
+ import validateNpmPackageName from 'validate-npm-package-name';
5
6
  import getVersionSelectorType from 'version-selector-type';
6
7
  export function parseBareSpecifier(bareSpecifier, alias, defaultTag, registry) {
7
8
  let name = alias;
@@ -69,6 +70,9 @@ export const BUILTIN_NAMED_REGISTRIES = Object.freeze({
69
70
  // Parses a named-registry specifier of the shape `<alias>:<body>` into a
70
71
  // RegistryPackageSpec. Returns `null` when the specifier does not use one of
71
72
  // the configured aliases, so the caller can fall through to other resolvers.
73
+ // Throws INVALID_NAMED_REGISTRY_PACKAGE_NAME when the alias matches but the
74
+ // package name is malformed (missing or empty scope/name segments, path
75
+ // separators inside the name).
72
76
  // Supported shapes:
73
77
  // - `<alias>:[@<owner>/]<name>[@<version_selector>]`
74
78
  // - `<alias>:<version_selector>` paired with a package alias
@@ -100,9 +104,6 @@ export function parseNamedRegistrySpecifierToRegistryPackageSpec(rawSpecifier, k
100
104
  pkgName = body.substring(0, index);
101
105
  versionSelector = body.substring(index + '@'.length);
102
106
  }
103
- if (pkgName.indexOf('/') === -1 || pkgName.endsWith('/')) {
104
- throw new PnpmError('INVALID_NAMED_REGISTRY_PACKAGE_NAME', `The package name '${pkgName}' in named registry '${registryName}:' is invalid`);
105
- }
106
107
  }
107
108
  else if (packageAlias?.startsWith('@')) {
108
109
  // `<alias>:<tag>` paired with a scoped alias — body is a version
@@ -124,6 +125,11 @@ export function parseNamedRegistrySpecifierToRegistryPackageSpec(rawSpecifier, k
124
125
  if (!pkgName)
125
126
  return null;
126
127
  }
128
+ // The name is used in registry URLs and metadata cache file paths, so
129
+ // anything that is not a valid npm package name must never make it through.
130
+ if (!validateNpmPackageName(pkgName).validForOldPackages) {
131
+ throw new PnpmError('INVALID_NAMED_REGISTRY_PACKAGE_NAME', `The package name '${pkgName}' in named registry '${registryName}:' is invalid`);
132
+ }
127
133
  const selector = getVersionSelectorType(versionSelector ?? defaultTag);
128
134
  if (selector == null)
129
135
  return null;
@@ -3,6 +3,14 @@ import type { FetchMetadataNotModifiedResult, FetchMetadataResult } from './fetc
3
3
  import type { RegistryPackageSpec } from './parseBareSpecifier.js';
4
4
  import { type PickPackageFromMetaOptions } from './pickPackageFromMeta.js';
5
5
  export interface PackageMetaCache {
6
+ /**
7
+ * Must return the same object reference that `set` stored for the key: the
8
+ * resolver tracks whether a cached packument was validated against the
9
+ * registry by object identity (see `unverifiedDiskPackuments`). In a cache
10
+ * that clones or deserializes on read, that provenance is lost and recovery
11
+ * degrades — a stale disk-promoted entry that can't satisfy a spec fails
12
+ * the pick instead of falling through to the registry.
13
+ */
6
14
  get: (key: string) => PackageMeta | undefined;
7
15
  set: (key: string, meta: PackageMeta) => void;
8
16
  has: (key: string) => boolean;
@@ -8,9 +8,9 @@ import { globalWarn, logger } from '@pnpm/logger';
8
8
  import getRegistryName from 'encode-registry';
9
9
  import pLimit, {} from 'p-limit';
10
10
  import { fastPathTemp as pathTemp } from 'path-temp';
11
- import { pick } from 'ramda';
12
11
  import { renameOverwrite } from 'rename-overwrite';
13
12
  import semver from 'semver';
13
+ import { clearMeta } from './clearMeta.js';
14
14
  import { pickLowestVersionByVersionRange, pickPackageFromMeta, pickVersionByVersionRange, } from './pickPackageFromMeta.js';
15
15
  import { toRaw } from './toRaw.js';
16
16
  /**
@@ -104,6 +104,29 @@ function pickMatchingVersionFinal(pickerOpts, spec, meta) {
104
104
  throw err;
105
105
  }
106
106
  }
107
+ /**
108
+ * Packuments promoted into the in-memory cache straight from the on-disk
109
+ * mirror, without registry validation. The mirror may predate versions the
110
+ * registry has, so when a cache hit on such an entry can't satisfy the
111
+ * requested spec (and the resolver isn't offline), `pickPackage` falls
112
+ * through to the regular flow — a conditional registry request — instead of
113
+ * failing the pick, exactly as it would have before the entry was promoted.
114
+ * Network-fetched and 304-revalidated packuments are never in this set, so
115
+ * hits on them keep returning directly even when the pick fails (the caller
116
+ * then falls back to workspace packages or reports no matching version).
117
+ */
118
+ const unverifiedDiskPackuments = new WeakSet();
119
+ /**
120
+ * Promote a packument parsed from the on-disk mirror into the in-memory
121
+ * cache, so repeat resolutions of the same package (common across a large
122
+ * dependency graph) don't re-read and re-parse the mirror. The entry is
123
+ * remembered as disk-sourced (see {@link unverifiedDiskPackuments}) because it
124
+ * never went through registry validation.
125
+ */
126
+ function cacheDiskLoadedMeta(metaCache, cacheKey, meta) {
127
+ unverifiedDiskPackuments.add(meta);
128
+ metaCache.set(cacheKey, meta);
129
+ }
107
130
  export async function pickPackage(ctx, spec, opts) {
108
131
  opts = opts || {};
109
132
  const pickerOpts = {
@@ -147,21 +170,31 @@ export async function pickPackage(ctx, spec, opts) {
147
170
  : persistUpgradedMeta(ctx, pkgMirror, upgrade.upgradedFrom);
148
171
  ctx.metaCache.set(cacheKey, metaForCache);
149
172
  }
150
- return {
151
- meta: metaForCache,
152
- pickedPackage: pickMatchingVersionFinal(pickerOpts, spec, metaForCache),
153
- };
173
+ const pickedPackage = pickMatchingVersionFinal(pickerOpts, spec, metaForCache);
174
+ if (pickedPackage != null || ctx.offline === true || !unverifiedDiskPackuments.has(metaForCache)) {
175
+ return {
176
+ meta: metaForCache,
177
+ pickedPackage,
178
+ };
179
+ }
180
+ // Disk-promoted meta that can't satisfy the spec: fall through and
181
+ // revalidate against the registry (see unverifiedDiskPackuments).
154
182
  }
155
183
  return runLimited(pkgMirror, async (limit) => {
156
184
  let metaCachedInStore;
157
185
  if (ctx.offline === true || ctx.preferOffline === true || opts.pickLowestVersion) {
158
186
  metaCachedInStore = await limit(async () => loadMeta(pkgMirror));
159
187
  if (ctx.offline) {
160
- if (metaCachedInStore != null)
188
+ if (metaCachedInStore != null) {
189
+ // maybeUpgradeAbbreviatedMetaForReleaseAge short-circuits when
190
+ // offline, so a later in-memory cache hit returns this same meta
191
+ // without any network access.
192
+ cacheDiskLoadedMeta(ctx.metaCache, cacheKey, metaCachedInStore);
161
193
  return {
162
194
  meta: metaCachedInStore,
163
195
  pickedPackage: pickMatchingVersionFinal(pickerOpts, spec, metaCachedInStore),
164
196
  };
197
+ }
165
198
  throw new PnpmError('NO_OFFLINE_META', `Failed to resolve ${toRaw(spec)} in package mirror ${pkgMirror}`);
166
199
  }
167
200
  if (metaCachedInStore != null) {
@@ -178,6 +211,14 @@ export async function pickPackage(ctx, spec, opts) {
178
211
  }
179
212
  const pickedPackage = pickMatchingVersionFinal(pickerOpts, spec, metaCachedInStore);
180
213
  if (pickedPackage) {
214
+ // A cache hit re-runs maybeUpgradeAbbreviatedMetaForReleaseAge, so
215
+ // serving this meta from memory can't bypass the release-age
216
+ // upgrade. When the upgrade branch above already cached the
217
+ // registry-validated upgraded meta, don't overwrite it with a
218
+ // disk-sourced marking.
219
+ if (upgrade.upgradedFrom == null) {
220
+ cacheDiskLoadedMeta(ctx.metaCache, cacheKey, metaCachedInStore);
221
+ }
181
222
  return {
182
223
  meta: metaCachedInStore,
183
224
  pickedPackage,
@@ -193,7 +234,7 @@ export async function pickPackage(ctx, spec, opts) {
193
234
  try {
194
235
  const pickedPackage = pickMatchingVersionFast(pickerOpts, spec, metaCachedInStore);
195
236
  if (pickedPackage) {
196
- ctx.metaCache.set(cacheKey, metaCachedInStore);
237
+ cacheDiskLoadedMeta(ctx.metaCache, cacheKey, metaCachedInStore);
197
238
  return {
198
239
  meta: metaCachedInStore,
199
240
  pickedPackage,
@@ -431,41 +472,6 @@ function persistUpgradedMeta(ctx, pkgMirror, upgradedFrom) {
431
472
  }));
432
473
  return metaForCache;
433
474
  }
434
- function clearMeta(pkg) {
435
- const versions = {};
436
- for (const [version, info] of Object.entries(pkg.versions)) {
437
- // The list taken from https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
438
- // with the addition of 'libc'
439
- versions[version] = pick([
440
- 'name',
441
- 'version',
442
- 'bin',
443
- 'directories',
444
- 'devDependencies',
445
- 'optionalDependencies',
446
- 'dependencies',
447
- 'peerDependencies',
448
- 'dist',
449
- 'engines',
450
- 'peerDependenciesMeta',
451
- 'cpu',
452
- 'os',
453
- 'libc',
454
- 'deprecated',
455
- 'bundleDependencies',
456
- 'bundledDependencies',
457
- 'hasInstallScript',
458
- '_npmUser',
459
- ], info);
460
- }
461
- return {
462
- name: pkg.name,
463
- 'dist-tags': pkg['dist-tags'],
464
- versions,
465
- time: pkg.time,
466
- modified: pkg.modified,
467
- };
468
- }
469
475
  export function encodePkgName(pkgName) {
470
476
  if (pkgName !== pkgName.toLowerCase()) {
471
477
  return `${pkgName}_${createHexHash(pkgName)}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/resolving.npm-resolver",
3
- "version": "1102.1.0",
3
+ "version": "1102.1.1",
4
4
  "description": "Resolver for npm-hosted packages",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -42,28 +42,29 @@
42
42
  "semver": "^7.8.4",
43
43
  "semver-utils": "^1.1.4",
44
44
  "ssri": "13.0.1",
45
+ "validate-npm-package-name": "7.0.2",
45
46
  "version-selector-type": "^3.0.0",
46
47
  "@pnpm/config.pick-registry-for-package": "1100.0.9",
47
- "@pnpm/constants": "1100.0.0",
48
48
  "@pnpm/config.version-policy": "1100.1.6",
49
+ "@pnpm/constants": "1100.0.0",
49
50
  "@pnpm/core-loggers": "1100.2.1",
50
51
  "@pnpm/crypto.hash": "1100.0.1",
52
+ "@pnpm/error": "1100.0.1",
51
53
  "@pnpm/fetching.types": "1100.0.2",
52
- "@pnpm/resolving.jsr-specifier-parser": "1100.0.1",
53
54
  "@pnpm/fs.graceful-fs": "1100.1.0",
55
+ "@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.9",
56
+ "@pnpm/resolving.jsr-specifier-parser": "1100.0.2",
54
57
  "@pnpm/resolving.registry.types": "1100.1.3",
55
- "@pnpm/resolving.resolver-base": "1100.5.0",
58
+ "@pnpm/store.cafs": "1100.1.12",
59
+ "@pnpm/resolving.resolver-base": "1100.5.1",
56
60
  "@pnpm/store.index": "1100.2.1",
57
- "@pnpm/store.cafs": "1100.1.11",
58
61
  "@pnpm/types": "1101.3.2",
59
62
  "@pnpm/workspace.range-resolver": "1100.0.2",
60
- "@pnpm/error": "1100.0.1",
61
- "@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.9",
62
63
  "@pnpm/workspace.spec-parser": "1100.0.0"
63
64
  },
64
65
  "peerDependencies": {
65
66
  "@pnpm/logger": "^1100.0.0",
66
- "@pnpm/worker": "^1100.2.2"
67
+ "@pnpm/worker": "^1100.2.3"
67
68
  },
68
69
  "devDependencies": {
69
70
  "@jest/globals": "30.4.1",
@@ -71,13 +72,14 @@
71
72
  "@types/ramda": "0.31.1",
72
73
  "@types/semver": "7.7.1",
73
74
  "@types/ssri": "^7.1.5",
75
+ "@types/validate-npm-package-name": "^4.0.2",
74
76
  "load-json-file": "^7.0.1",
75
77
  "tempy": "3.0.0",
76
- "@pnpm/network.fetch": "1100.1.4",
77
- "@pnpm/resolving.npm-resolver": "1102.1.0",
78
+ "@pnpm/logger": "1100.0.0",
79
+ "@pnpm/resolving.npm-resolver": "1102.1.1",
78
80
  "@pnpm/test-fixtures": "1100.0.0",
79
- "@pnpm/testing.mock-agent": "1101.0.4",
80
- "@pnpm/logger": "1100.0.0"
81
+ "@pnpm/network.fetch": "1100.1.4",
82
+ "@pnpm/testing.mock-agent": "1101.0.4"
81
83
  },
82
84
  "engines": {
83
85
  "node": ">=22.13"