@pnpm/resolving.npm-resolver 1101.1.0 → 1101.2.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.
@@ -58,12 +58,13 @@ function pickMax(a, b) {
58
58
  const pickHighest = pickPackageFromMeta.bind(null, pickVersionByVersionRange);
59
59
  const pickLowest = pickPackageFromMeta.bind(null, pickLowestVersionByVersionRange);
60
60
  // When minimumReleaseAge is active: try the highest mature version; if none
61
- // and strictPublishedByCheck is off, fall back to the lowest version in range
62
- // without applying the maturity filter.
61
+ // satisfies the range, fall back to the lowest version regardless of maturity
62
+ // so the resolver can report the violation inline and let the install layer
63
+ // (or other caller) decide what to do — never throw at this layer.
63
64
  function pickRespectingMinReleaseAge(pickerOpts, spec, meta) {
64
65
  return runPicker(pickerOpts, spec, (targetSpec) => {
65
66
  const highest = pickHighest(pickerOpts, meta, targetSpec);
66
- if (highest || pickerOpts.strictPublishedByCheck)
67
+ if (highest)
67
68
  return highest;
68
69
  return pickLowest({
69
70
  preferredVersionSelectors: pickerOpts.preferredVersionSelectors,
@@ -111,7 +112,6 @@ export async function pickPackage(ctx, spec, opts) {
111
112
  publishedByExclude: opts.publishedByExclude,
112
113
  pickLowestVersion: opts.pickLowestVersion,
113
114
  includeLatestTag: opts.includeLatestTag,
114
- strictPublishedByCheck: ctx.strictPublishedByCheck,
115
115
  ignoreMissingTimeField: ctx.ignoreMissingTimeField,
116
116
  };
117
117
  validatePackageName(spec.name);
@@ -123,15 +123,29 @@ export async function pickPackage(ctx, spec, opts) {
123
123
  : ABBREVIATED_META_DIR;
124
124
  // Cache key includes fullMetadata to avoid returning abbreviated metadata when full metadata is requested.
125
125
  const cacheKey = fullMetadata ? `${spec.name}:full` : spec.name;
126
+ const pkgMirror = getPkgMirrorPath(ctx.cacheDir, metaDir, opts.registry, spec.name);
126
127
  const cachedMeta = ctx.metaCache.get(cacheKey);
127
128
  if (cachedMeta != null) {
129
+ // The in-memory cache may hold abbreviated metadata from an earlier call
130
+ // that didn't need `time` (no publishedBy then). If this call has
131
+ // publishedBy and the package was modified recently, upgrade to full
132
+ // metadata so the maturity check runs properly.
133
+ const upgrade = await maybeUpgradeAbbreviatedMetaForReleaseAge(ctx, spec, opts, cachedMeta);
134
+ let metaForCache = upgrade.meta;
135
+ if (upgrade.upgradedFrom != null) {
136
+ // Persist the upgraded meta to disk too: the on-disk mirror still holds
137
+ // the abbreviated form, so without this a fresh process would re-trigger
138
+ // the upgrade fetch on its next install.
139
+ metaForCache = opts.dryRun
140
+ ? upgrade.meta
141
+ : persistUpgradedMeta(ctx, pkgMirror, upgrade.upgradedFrom);
142
+ ctx.metaCache.set(cacheKey, metaForCache);
143
+ }
128
144
  return {
129
- meta: cachedMeta,
130
- pickedPackage: pickMatchingVersionFinal(pickerOpts, spec, cachedMeta),
145
+ meta: metaForCache,
146
+ pickedPackage: pickMatchingVersionFinal(pickerOpts, spec, metaForCache),
131
147
  };
132
148
  }
133
- const registryName = getRegistryName(opts.registry);
134
- const pkgMirror = path.join(ctx.cacheDir, metaDir, registryName, `${encodePkgName(spec.name)}.jsonl`);
135
149
  return runLimited(pkgMirror, async (limit) => {
136
150
  let metaCachedInStore;
137
151
  if (ctx.offline === true || ctx.preferOffline === true || opts.pickLowestVersion) {
@@ -145,6 +159,17 @@ export async function pickPackage(ctx, spec, opts) {
145
159
  throw new PnpmError('NO_OFFLINE_META', `Failed to resolve ${toRaw(spec)} in package mirror ${pkgMirror}`);
146
160
  }
147
161
  if (metaCachedInStore != null) {
162
+ // Disk-cached meta may be abbreviated; upgrade for the maturity check
163
+ // before letting pickMatchingVersionFinal warn-and-skip on missing time.
164
+ const upgrade = await maybeUpgradeAbbreviatedMetaForReleaseAge(ctx, spec, opts, metaCachedInStore);
165
+ metaCachedInStore = upgrade.meta;
166
+ if (upgrade.upgradedFrom != null) {
167
+ // Persist so the next install skips this upgrade fetch entirely.
168
+ if (!opts.dryRun) {
169
+ metaCachedInStore = persistUpgradedMeta(ctx, pkgMirror, upgrade.upgradedFrom);
170
+ }
171
+ ctx.metaCache.set(cacheKey, metaCachedInStore);
172
+ }
148
173
  const pickedPackage = pickMatchingVersionFinal(pickerOpts, spec, metaCachedInStore);
149
174
  if (pickedPackage) {
150
175
  return {
@@ -168,10 +193,11 @@ export async function pickPackage(ctx, spec, opts) {
168
193
  };
169
194
  }
170
195
  }
171
- catch (err) {
172
- if (ctx.strictPublishedByCheck) {
173
- throw err;
174
- }
196
+ catch {
197
+ // Swallow fast-path errors (e.g. ERR_PNPM_MISSING_TIME from
198
+ // abbreviated meta) and fall through to the network fetch, which
199
+ // can upgrade to full metadata and run the maturity check on
200
+ // real `time` data.
175
201
  }
176
202
  }
177
203
  }
@@ -189,13 +215,8 @@ export async function pickPackage(ctx, spec, opts) {
189
215
  };
190
216
  }
191
217
  }
192
- catch (err) {
193
- // Don't rethrow ERR_PNPM_MISSING_TIME from cached abbreviated metadata
194
- // let the code fall through to the network fetch path which will get full metadata.
195
- if (ctx.strictPublishedByCheck &&
196
- !(isMissingTimeError(err))) {
197
- throw err;
198
- }
218
+ catch {
219
+ // Same as above fall through to the network fetch.
199
220
  }
200
221
  }
201
222
  }
@@ -219,6 +240,19 @@ export async function pickPackage(ctx, spec, opts) {
219
240
  if (fetchResult.notModified) {
220
241
  metaCachedInStore = metaCachedInStore ?? await limit(async () => loadMeta(pkgMirror));
221
242
  if (metaCachedInStore != null) {
243
+ // The cached metadata may be abbreviated (no per-version `time`).
244
+ // When minimumReleaseAge is active we need `time` for the maturity check,
245
+ // so upgrade to full metadata via a follow-up fetch when warranted.
246
+ // Without this, repeat installs of recently-modified packages would
247
+ // silently bypass the maturity check via the warn-and-skip fallback.
248
+ const upgrade = await maybeUpgradeAbbreviatedMetaForReleaseAge(ctx, spec, opts, metaCachedInStore);
249
+ metaCachedInStore = upgrade.meta;
250
+ if (upgrade.upgradedFrom != null && !opts.dryRun) {
251
+ // Persist the upgraded full metadata to disk so subsequent installs
252
+ // skip this upgrade fetch entirely (the cached meta will then have
253
+ // `time` populated, so the upgrade condition won't trigger).
254
+ metaCachedInStore = persistUpgradedMeta(ctx, pkgMirror, upgrade.upgradedFrom);
255
+ }
222
256
  ctx.metaCache.set(cacheKey, metaCachedInStore);
223
257
  return {
224
258
  meta: metaCachedInStore,
@@ -307,6 +341,70 @@ export async function pickPackage(ctx, spec, opts) {
307
341
  }
308
342
  });
309
343
  }
344
+ // When `minimumReleaseAge` is active and we have abbreviated metadata (which
345
+ // the npm registry serves by default and which omits per-version `time`),
346
+ // the maturity check can't run on the data we have. If the package has been
347
+ // modified since the maturity cutoff, re-fetch with `fullMetadata: true` so
348
+ // `time` is populated and the check can proceed properly. Without this,
349
+ // `pickMatchingVersionFinal` would fall back to its warn-and-skip path,
350
+ // silently bypassing the minimumReleaseAge guarantee for affected packages.
351
+ //
352
+ // Returns the original meta when no upgrade is needed. When an upgrade
353
+ // happens, returns both the upgraded meta and the underlying fetch result
354
+ // so callers can persist it to disk and avoid re-fetching on next install.
355
+ async function maybeUpgradeAbbreviatedMetaForReleaseAge(ctx, spec, opts, meta) {
356
+ if (ctx.offline === true ||
357
+ !opts.publishedBy ||
358
+ meta.time != null ||
359
+ opts.publishedByExclude?.(spec.name) === true) {
360
+ return { meta };
361
+ }
362
+ const modifiedDate = meta.modified ? new Date(meta.modified) : null;
363
+ const isModifiedValid = modifiedDate != null && !Number.isNaN(modifiedDate.getTime());
364
+ if (isModifiedValid && modifiedDate < opts.publishedBy) {
365
+ // The package was last modified before the maturity cutoff. No individual
366
+ // version can be newer than the cutoff, so the abbreviated form is fine.
367
+ return { meta };
368
+ }
369
+ // When `modified` is missing or malformed we fall through to the upgrade
370
+ // fetch: prefer correctness (run the maturity check on real `time` data)
371
+ // over saving a network call when our cached freshness signal is unusable.
372
+ // Forward etag/modified so the registry can answer 304 if the upgraded
373
+ // representation hasn't actually changed (rare on the npm registry where
374
+ // full and abbreviated have distinct etags, but cheap to support).
375
+ const fullFetchResult = await ctx.fetch(spec.name, {
376
+ authHeaderValue: opts.authHeaderValue,
377
+ fullMetadata: true,
378
+ etag: meta.etag,
379
+ modified: meta.modified,
380
+ registry: opts.registry,
381
+ });
382
+ if (fullFetchResult.notModified) {
383
+ // Upgrade fetch came back 304: keep the abbreviated meta. The downstream
384
+ // `pickMatchingVersionFinal` will fall through to its warn-and-skip path.
385
+ return { meta };
386
+ }
387
+ return { meta: fullFetchResult.meta, upgradedFrom: fullFetchResult };
388
+ }
389
+ // Persists upgraded full metadata to the on-disk cache mirror and returns
390
+ // the meta to store in the in-memory cache. When `filterMetadata` is on, the
391
+ // in-memory and on-disk forms are both stripped via `clearMeta`; otherwise
392
+ // the original raw response body is written and the unstripped meta is kept.
393
+ function persistUpgradedMeta(ctx, pkgMirror, upgradedFrom) {
394
+ const metaForCache = ctx.filterMetadata ? clearMeta(upgradedFrom.meta) : upgradedFrom.meta;
395
+ const jsonForDisk = ctx.filterMetadata
396
+ ? prepareJsonForDisk(metaForCache, upgradedFrom.etag)
397
+ : prepareJsonForDisk(upgradedFrom.meta, upgradedFrom.etag, upgradedFrom.jsonText);
398
+ runLimited(pkgMirror, (l) => l(async () => {
399
+ try {
400
+ await saveMeta(pkgMirror, jsonForDisk);
401
+ }
402
+ catch (err) { // eslint-disable-line
403
+ // We don't care if this file was not written to the cache
404
+ }
405
+ }));
406
+ return metaForCache;
407
+ }
310
408
  function clearMeta(pkg) {
311
409
  const versions = {};
312
410
  for (const [version, info] of Object.entries(pkg.versions)) {
@@ -342,18 +440,25 @@ function clearMeta(pkg) {
342
440
  modified: pkg.modified,
343
441
  };
344
442
  }
345
- function encodePkgName(pkgName) {
443
+ export function encodePkgName(pkgName) {
346
444
  if (pkgName !== pkgName.toLowerCase()) {
347
445
  return `${pkgName}_${createHexHash(pkgName)}`;
348
446
  }
349
447
  return pkgName;
350
448
  }
449
+ /**
450
+ * Path of the on-disk JSONL document where pnpm mirrors a package's registry
451
+ * metadata. `metaDir` selects between abbreviated and full caches.
452
+ */
453
+ export function getPkgMirrorPath(cacheDir, metaDir, registry, pkgName) {
454
+ return path.join(cacheDir, metaDir, getRegistryName(registry), `${encodePkgName(pkgName)}.jsonl`);
455
+ }
351
456
  /**
352
457
  * Formats metadata for disk storage as two-line NDJSON:
353
458
  * Line 1: cache headers (etag, modified) — small, fast to read
354
459
  * Line 2: the full registry metadata JSON — unchanged from the registry response
355
460
  */
356
- function prepareJsonForDisk(meta, etag, jsonText) {
461
+ export function prepareJsonForDisk(meta, etag, jsonText) {
357
462
  const modified = meta.modified ?? meta.time?.modified;
358
463
  const headers = JSON.stringify({ etag, modified });
359
464
  const body = jsonText ?? JSON.stringify(meta);
@@ -369,7 +474,7 @@ function isMissingTimeError(err) {
369
474
  // memory via this Set as they resolve ever more distinct packages.
370
475
  const MAX_WARNED_MISSING_TIME = 1024;
371
476
  const warnedMissingTimeFor = new Set();
372
- function warnMissingTimeFieldOnce(pkgName) {
477
+ export function warnMissingTimeFieldOnce(pkgName) {
373
478
  if (warnedMissingTimeFor.has(pkgName))
374
479
  return;
375
480
  if (warnedMissingTimeFor.size >= MAX_WARNED_MISSING_TIME) {
@@ -396,7 +501,7 @@ async function getFileMtime(filePath) {
396
501
  * parsing the full metadata (which can be megabytes for popular packages)
397
502
  * when we only need conditional-request headers.
398
503
  */
399
- async function loadMetaHeaders(pkgMirror) {
504
+ export async function loadMetaHeaders(pkgMirror) {
400
505
  let fh;
401
506
  try {
402
507
  fh = await fs.open(pkgMirror, 'r');
@@ -423,7 +528,7 @@ async function loadMetaHeaders(pkgMirror) {
423
528
  * Line 1: cache headers (etag, modified)
424
529
  * Line 2: registry metadata JSON
425
530
  */
426
- async function loadMeta(pkgMirror) {
531
+ export async function loadMeta(pkgMirror) {
427
532
  try {
428
533
  const data = await gfs.readFile(pkgMirror, 'utf8');
429
534
  const newlineIdx = data.indexOf('\n');
@@ -439,7 +544,7 @@ async function loadMeta(pkgMirror) {
439
544
  }
440
545
  }
441
546
  const createdDirs = new Set();
442
- async function saveMeta(pkgMirror, json) {
547
+ export async function saveMeta(pkgMirror, json) {
443
548
  const dir = path.dirname(pkgMirror);
444
549
  if (!createdDirs.has(dir)) {
445
550
  await fs.mkdir(dir, { recursive: true });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Violation codes the npm resolver attaches to
3
+ * `ResolutionPolicyViolation.code` when an inline policy check rejects
4
+ * a pick. Exported so downstream code (the install command, the strict
5
+ * resolver wrapper, tests) references one source of truth instead of
6
+ * re-typing the string.
7
+ *
8
+ * Lives in its own module — both `index.ts` and `createNpmResolutionVerifier.ts`
9
+ * import it, so keeping the constants here avoids a cycle.
10
+ */
11
+ export declare const MINIMUM_RELEASE_AGE_VIOLATION_CODE = "MINIMUM_RELEASE_AGE_VIOLATION";
12
+ export declare const TRUST_DOWNGRADE_VIOLATION_CODE = "TRUST_DOWNGRADE";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Violation codes the npm resolver attaches to
3
+ * `ResolutionPolicyViolation.code` when an inline policy check rejects
4
+ * a pick. Exported so downstream code (the install command, the strict
5
+ * resolver wrapper, tests) references one source of truth instead of
6
+ * re-typing the string.
7
+ *
8
+ * Lives in its own module — both `index.ts` and `createNpmResolutionVerifier.ts`
9
+ * import it, so keeping the constants here avoids a cycle.
10
+ */
11
+ export const MINIMUM_RELEASE_AGE_VIOLATION_CODE = 'MINIMUM_RELEASE_AGE_VIOLATION';
12
+ export const TRUST_DOWNGRADE_VIOLATION_CODE = 'TRUST_DOWNGRADE';
13
+ //# sourceMappingURL=violationCodes.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/resolving.npm-resolver",
3
- "version": "1101.1.0",
3
+ "version": "1101.2.0",
4
4
  "description": "Resolver for npm-hosted packages",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -41,25 +41,26 @@
41
41
  "ssri": "13.0.1",
42
42
  "version-selector-type": "^3.0.0",
43
43
  "@pnpm/config.pick-registry-for-package": "1100.0.3",
44
+ "@pnpm/config.version-policy": "1100.1.0",
45
+ "@pnpm/constants": "1100.0.0",
46
+ "@pnpm/core-loggers": "1100.1.0",
44
47
  "@pnpm/crypto.hash": "1100.0.1",
45
48
  "@pnpm/error": "1100.0.0",
46
49
  "@pnpm/fetching.types": "1100.0.1",
47
- "@pnpm/resolving.jsr-specifier-parser": "1100.0.0",
48
50
  "@pnpm/fs.graceful-fs": "1100.1.0",
51
+ "@pnpm/resolving.jsr-specifier-parser": "1100.0.0",
49
52
  "@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.3",
50
- "@pnpm/store.cafs": "1100.1.3",
51
- "@pnpm/store.index": "1100.1.0",
52
53
  "@pnpm/resolving.registry.types": "1100.0.3",
54
+ "@pnpm/resolving.resolver-base": "1100.2.0",
55
+ "@pnpm/store.cafs": "1100.1.5",
56
+ "@pnpm/store.index": "1100.1.0",
53
57
  "@pnpm/types": "1101.1.0",
54
- "@pnpm/resolving.resolver-base": "1100.1.3",
55
- "@pnpm/constants": "1100.0.0",
56
58
  "@pnpm/workspace.range-resolver": "1100.0.1",
57
- "@pnpm/workspace.spec-parser": "1100.0.0",
58
- "@pnpm/core-loggers": "1100.0.2"
59
+ "@pnpm/workspace.spec-parser": "1100.0.0"
59
60
  },
60
61
  "peerDependencies": {
61
62
  "@pnpm/logger": ">=1001.0.0 <1002.0.0",
62
- "@pnpm/worker": "^1100.1.4"
63
+ "@pnpm/worker": "^1100.1.6"
63
64
  },
64
65
  "devDependencies": {
65
66
  "@jest/globals": "30.3.0",
@@ -69,12 +70,11 @@
69
70
  "@types/ssri": "^7.1.5",
70
71
  "load-json-file": "^7.0.1",
71
72
  "tempy": "3.0.0",
72
- "@pnpm/config.version-policy": "1100.0.3",
73
73
  "@pnpm/logger": "1100.0.0",
74
- "@pnpm/resolving.npm-resolver": "1101.1.0",
75
- "@pnpm/network.fetch": "1100.0.3",
74
+ "@pnpm/resolving.npm-resolver": "1101.2.0",
75
+ "@pnpm/network.fetch": "1100.0.5",
76
76
  "@pnpm/test-fixtures": "1100.0.0",
77
- "@pnpm/testing.mock-agent": "1100.0.3"
77
+ "@pnpm/testing.mock-agent": "1100.0.5"
78
78
  },
79
79
  "engines": {
80
80
  "node": ">=22.13"