@pnpm/resolving.npm-resolver 1103.1.0 → 1103.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @pnpm/npm-resolver
2
2
 
3
+ ## 1103.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Lockfile verification now honors offline mode by using cached registry metadata instead of reaching the registry. When the required metadata is not available locally, verification reports the same `ERR_PNPM_NO_OFFLINE_META` condition used by offline resolution.
8
+
9
+ ### Patch Changes
10
+
11
+ - The held-back-update warning printed by `pnpm update` no longer fires when `minimumReleaseAge` is the actual reason a newer version was not picked. The warning's baseline now applies the same maturity cutoff as the pick itself, so it no longer wrongly attributes the hold-back to "your manifests and already installed dependencies" or recommends an override that would defeat the age gate. See pnpm/pnpm#13071.
12
+
13
+ - Updated dependencies:
14
+ - @pnpm/resolving.registry.pkg-metadata-filter@1100.0.16
15
+
3
16
  ## 1103.1.0
4
17
 
5
18
  ### Minor Changes
@@ -58,6 +58,11 @@ export interface CreateNpmResolutionVerifierOptions {
58
58
  fetchOpts: FetchMetadataFromFromRegistryOptions;
59
59
  getAuthHeaderValueByURI: GetAuthHeader;
60
60
  cacheDir?: FetchFullMetadataCachedOptions['cacheDir'];
61
+ /**
62
+ * When true, verifier metadata lookups must use the local mirror
63
+ * only and never reach the registry or attestation endpoint.
64
+ */
65
+ offline?: boolean;
61
66
  /**
62
67
  * Per-install LRU shared with the npm resolver's `pickPackage`
63
68
  * (`{ get, set }` over `PackageMeta`). When provided, the verifier
@@ -53,6 +53,7 @@ export function createNpmResolutionVerifier(opts) {
53
53
  fetchOpts: opts.fetchOpts,
54
54
  getAuthHeaderValueByURI: opts.getAuthHeaderValueByURI,
55
55
  cacheDir: opts.cacheDir,
56
+ offline: opts.offline === true,
56
57
  cutoffMs: cutoff,
57
58
  sharedMetaCache: opts.metaCache,
58
59
  abbreviatedMetaCache: new Map(),
@@ -391,6 +392,7 @@ function fetchFullMetaForTrust(context, registry, name) {
391
392
  registry,
392
393
  authHeaderValue: context.getAuthHeaderValueByURI(registry, { pkgName: name }),
393
394
  cacheDir: context.cacheDir,
395
+ offline: context.offline,
394
396
  }).then(projectTrustMeta);
395
397
  }
396
398
  context.fullMetaForTrustCache.set(cacheKey, cachedPromise);
@@ -485,12 +487,14 @@ async function resolvePublishedAt(context, registry, name, version) {
485
487
  const localTime = await readLocalMetaTime(context, registry, name);
486
488
  if (localTime?.[version])
487
489
  return localTime[version];
488
- const attestationTime = await fetchAttestationPublishedAt(context.fetchOpts, name, version, {
489
- registry,
490
- authHeaderValue: context.getAuthHeaderValueByURI(registry, { pkgName: name }),
491
- });
492
- if (attestationTime != null)
493
- return attestationTime;
490
+ if (!context.offline) {
491
+ const attestationTime = await fetchAttestationPublishedAt(context.fetchOpts, name, version, {
492
+ registry,
493
+ authHeaderValue: context.getAuthHeaderValueByURI(registry, { pkgName: name }),
494
+ });
495
+ if (attestationTime != null)
496
+ return attestationTime;
497
+ }
494
498
  const fullMetaTime = await fetchFullMetaTime(context, registry, name);
495
499
  return fullMetaTime?.[version];
496
500
  }
@@ -555,6 +559,7 @@ function fetchAbbreviatedMeta(context, registry, name) {
555
559
  registry,
556
560
  authHeaderValue: context.getAuthHeaderValueByURI(registry, { pkgName: name }),
557
561
  cacheDir: context.cacheDir,
562
+ offline: context.offline,
558
563
  }).then((meta) => ({ meta: projectAbbreviatedMeta(meta) }), (error) => ({ error }));
559
564
  }
560
565
  context.abbreviatedMetaCache.set(cacheKey, cachedPromise);
@@ -646,6 +651,7 @@ function fetchFullMetaTime(context, registry, name) {
646
651
  registry,
647
652
  authHeaderValue: context.getAuthHeaderValueByURI(registry, { pkgName: name }),
648
653
  cacheDir: context.cacheDir,
654
+ offline: context.offline,
649
655
  }).then((meta) => meta.time);
650
656
  context.fullMetaCache.set(cacheKey, cachedPromise);
651
657
  }
@@ -10,6 +10,10 @@ export interface FetchMetadataCachedOptions {
10
10
  * back. Omit to disable caching — every call re-fetches.
11
11
  */
12
12
  cacheDir?: string;
13
+ /**
14
+ * Use only pnpm's on-disk metadata mirror and never reach the registry.
15
+ */
16
+ offline?: boolean;
13
17
  }
14
18
  export type FetchFullMetadataCachedOptions = FetchMetadataCachedOptions;
15
19
  /**
@@ -1,4 +1,5 @@
1
1
  import { ABBREVIATED_META_DIR, FULL_META_DIR } from '@pnpm/constants';
2
+ import { PnpmError } from '@pnpm/error';
2
3
  import { fetchMetadataFromFromRegistry, } from './fetch.js';
3
4
  import { getPkgMirrorPath, loadMeta, loadMetaHeaders, prepareJsonForDisk, saveMeta } from './pickPackage.js';
4
5
  /**
@@ -28,6 +29,14 @@ async function fetchMetadataCached(fetchOpts, pkgName, opts) {
28
29
  const pkgMirror = opts.cacheDir != null
29
30
  ? getPkgMirrorPath(opts.cacheDir, opts.metaDir, opts.registry, pkgName)
30
31
  : null;
32
+ if (opts.offline === true) {
33
+ if (pkgMirror != null) {
34
+ const cached = await loadMeta(pkgMirror);
35
+ if (cached != null)
36
+ return cached;
37
+ }
38
+ throw new PnpmError('NO_OFFLINE_META', `Failed to resolve ${pkgName} in package mirror ${pkgMirror ?? ''}`);
39
+ }
31
40
  const cacheHeaders = pkgMirror != null ? await loadMetaHeaders(pkgMirror) : null;
32
41
  const conditional = await fetchMetadataFromFromRegistry(fetchOpts, pkgName, {
33
42
  registry: opts.registry,
package/lib/index.js CHANGED
@@ -21,7 +21,7 @@ import { memoizeFetchMetadata } from './memoizeFetchMetadata.js';
21
21
  import { normalizeRegistryUrl } from './normalizeRegistryUrl.js';
22
22
  import { BUILTIN_NAMED_REGISTRIES, parseBareSpecifier, parseJsrSpecifierToRegistryPackageSpec, parseNamedRegistrySpecifierToRegistryPackageSpec, } from './parseBareSpecifier.js';
23
23
  import { pickPackage, } from './pickPackage.js';
24
- import { pickPackageFromMeta, pickVersionByVersionRange } from './pickPackageFromMeta.js';
24
+ import { applyPublishedByPolicy, pickPackageFromMeta, pickVersionByVersionRange } from './pickPackageFromMeta.js';
25
25
  import { failIfTrustDowngraded } from './trustChecks.js';
26
26
  import { MINIMUM_RELEASE_AGE_VIOLATION_CODE } from './violationCodes.js';
27
27
  import { workspacePrefToNpm } from './workspacePrefToNpm.js';
@@ -212,7 +212,10 @@ function stripLockfileVersionPins(selectors) {
212
212
  * The baseline for "held back" is the pick with only the non-pin selectors
213
213
  * applied — `range`/`tag` selectors such as the `pnpm audit --fix`
214
214
  * vulnerability penalties steer the baseline too, so the warning never
215
- * recommends a version those selectors avoid.
215
+ * recommends a version those selectors avoid. The baseline also honors the
216
+ * `publishedBy` maturity cutoff the actual pick applied: a version blocked
217
+ * by `minimumReleaseAge` is not an update the manifests held back, and
218
+ * recommending an override for it would defeat the age gate.
216
219
  *
217
220
  * The recommended override is scoped to the declared range being resolved
218
221
  * (`name@<range>`), so applying it can never violate any consumer's range:
@@ -233,8 +236,14 @@ function warnOnceOnHeldBackUpdate(ctx, opts, spec, meta, pickedVersion) {
233
236
  nonPinSelectors ??= Object.create(null);
234
237
  nonPinSelectors[selector] = value;
235
238
  }
239
+ // `needsFullMetadata` is not this caller's problem: the pick already
240
+ // succeeded on this metadata, which for an abbreviated packument means
241
+ // every version cleared the cutoff, so `meta` is the filtered view.
242
+ const baselineMeta = opts.publishedBy != null
243
+ ? applyPublishedByPolicy(meta, opts.publishedBy, opts.publishedByExclude).meta
244
+ : meta;
236
245
  const preferred = pickVersionByVersionRange({
237
- meta,
246
+ meta: baselineMeta,
238
247
  versionRange: spec.fetchSpec,
239
248
  preferredVersionSelectors: nonPinSelectors,
240
249
  });
@@ -15,6 +15,29 @@ export interface PickPackageFromMetaOptions {
15
15
  publishedByExclude?: PackageVersionPolicy;
16
16
  }
17
17
  export declare function pickPackageFromMeta(pickVersionByVersionRangeFn: PickVersionByVersionRange, { preferredVersionSelectors, publishedBy, publishedByExclude, }: PickPackageFromMetaOptions, meta: PackageMeta, spec: RegistryPackageSpec): PackageInRegistry | null;
18
+ export interface PublishedByView {
19
+ /** The metadata the cutoff leaves visible. `meta` itself when nothing is filtered out. */
20
+ meta: PackageMeta;
21
+ /**
22
+ * The cutoff could not be applied: the metadata is abbreviated, so there
23
+ * are no per-version timestamps to filter on. Whether that is fatal is the
24
+ * caller's call — the pick needs full metadata to honor the cutoff, while a
25
+ * caller reasoning about a pick that already succeeded knows the versions
26
+ * cleared the cutoff some other way.
27
+ */
28
+ needsFullMetadata: boolean;
29
+ }
30
+ /**
31
+ * Narrows `meta` to the versions the `publishedBy` cutoff admits, honoring
32
+ * `publishedByExclude`: a package the policy excludes wholesale keeps its
33
+ * unfiltered metadata, and versions the policy names explicitly stay in
34
+ * regardless of their age.
35
+ *
36
+ * Every consumer of the cutoff goes through here so they agree on what the
37
+ * policy admits — a baseline that filters differently from the pick would
38
+ * misreport why a version was chosen.
39
+ */
40
+ export declare function applyPublishedByPolicy(meta: PackageMeta, publishedBy: Date, publishedByExclude?: PackageVersionPolicy): PublishedByView;
18
41
  export declare function assertMetaHasTime(meta: PackageMeta): asserts meta is PackageMetaWithTime;
19
42
  export declare function pickLowestVersionByVersionRange({ meta, versionRange, preferredVersionSelectors }: PickVersionByVersionRangeOptions): string | null;
20
43
  export declare function pickVersionByVersionRange({ meta, versionRange, preferredVersionSelectors }: PickVersionByVersionRangeOptions): string | null;
@@ -4,28 +4,20 @@ import { filterPkgMetadataByPublishDate } from '@pnpm/resolving.registry.pkg-met
4
4
  import semver from 'semver';
5
5
  export function pickPackageFromMeta(pickVersionByVersionRangeFn, { preferredVersionSelectors, publishedBy, publishedByExclude, }, meta, spec) {
6
6
  if (publishedBy) {
7
- const excludeResult = publishedByExclude?.(meta.name) ?? false;
8
- if (excludeResult !== true) {
9
- if (meta.time != null) {
10
- // Full metadata with per-version timestamps: filter normally
7
+ const view = applyPublishedByPolicy(meta, publishedBy, publishedByExclude);
8
+ meta = view.meta;
9
+ if (view.needsFullMetadata) {
10
+ const modifiedDate = parseModifiedDate(meta.modified);
11
+ if (modifiedDate == null || modifiedDate > publishedBy) {
12
+ // The package was modified after the cutoff (or carries no usable
13
+ // `modified`), so which of its versions are mature is unknowable
14
+ // from abbreviated metadata. The error tells the caller to refetch.
11
15
  assertMetaHasTime(meta);
12
- const trustedVersions = Array.isArray(excludeResult) ? excludeResult : undefined;
13
- meta = filterPkgMetadataByPublishDate(meta, publishedBy, trustedVersions);
14
- }
15
- else {
16
- const modifiedDate = parseModifiedDate(meta.modified);
17
- if (modifiedDate == null || modifiedDate > publishedBy) {
18
- // Abbreviated metadata without per-version timestamps, and the package
19
- // was recently modified (or has no/invalid modified field). We cannot determine
20
- // which individual versions are mature enough — need full metadata.
21
- assertMetaHasTime(meta);
22
- }
23
- // else: meta.modified <= publishedBy — every version was published at or
24
- // before the cutoff (modified is an upper bound on per-version time), so
25
- // they all pass the per-version `<=` maturity filter and no filtering is
26
- // needed. Inclusive at the boundary on purpose so this branch matches the
27
- // per-version filter in `filterPkgMetadataByPublishDate`.
28
16
  }
17
+ // else: `modified` is an upper bound on every per-version timestamp, so
18
+ // `modified <= publishedBy` means they all pass the maturity filter and
19
+ // nothing would be dropped. Inclusive at the boundary on purpose, to
20
+ // match the per-version `<=` in `filterPkgMetadataByPublishDate`.
29
21
  }
30
22
  }
31
23
  if ((!meta.versions || Object.keys(meta.versions).length === 0) && !publishedBy) {
@@ -77,6 +69,29 @@ export function pickPackageFromMeta(pickVersionByVersionRangeFn, { preferredVers
77
69
  throw new PnpmError('MALFORMED_METADATA', `Received malformed metadata for "${spec.name}"`, { hint: 'This might mean that the package was unpublished from the registry', cause: err });
78
70
  }
79
71
  }
72
+ /**
73
+ * Narrows `meta` to the versions the `publishedBy` cutoff admits, honoring
74
+ * `publishedByExclude`: a package the policy excludes wholesale keeps its
75
+ * unfiltered metadata, and versions the policy names explicitly stay in
76
+ * regardless of their age.
77
+ *
78
+ * Every consumer of the cutoff goes through here so they agree on what the
79
+ * policy admits — a baseline that filters differently from the pick would
80
+ * misreport why a version was chosen.
81
+ */
82
+ export function applyPublishedByPolicy(meta, publishedBy, publishedByExclude) {
83
+ const excludeResult = publishedByExclude?.(meta.name) ?? false;
84
+ if (excludeResult === true)
85
+ return { meta, needsFullMetadata: false };
86
+ if (meta.time == null)
87
+ return { meta, needsFullMetadata: true };
88
+ assertMetaHasTime(meta);
89
+ const trustedVersions = Array.isArray(excludeResult) ? excludeResult : undefined;
90
+ return {
91
+ meta: filterPkgMetadataByPublishDate(meta, publishedBy, trustedVersions),
92
+ needsFullMetadata: false,
93
+ };
94
+ }
80
95
  export function assertMetaHasTime(meta) {
81
96
  if (meta.time == null) {
82
97
  throw new PnpmError('MISSING_TIME', `The metadata of ${meta.name} is missing the "time" field`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/resolving.npm-resolver",
3
- "version": "1103.1.0",
3
+ "version": "1103.2.0",
4
4
  "description": "Resolver for npm-hosted packages",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -41,7 +41,7 @@
41
41
  "@pnpm/fs.graceful-fs": "1100.1.1",
42
42
  "@pnpm/pkg-manifest.utils": "1100.3.1",
43
43
  "@pnpm/resolving.jsr-specifier-parser": "1100.0.4",
44
- "@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.15",
44
+ "@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.16",
45
45
  "@pnpm/resolving.registry.types": "1100.1.9",
46
46
  "@pnpm/resolving.resolver-base": "1101.1.0",
47
47
  "@pnpm/store.cafs": "1100.1.18",
@@ -72,12 +72,12 @@
72
72
  "@jest/globals": "30.4.1",
73
73
  "@pnpm/logger": "1100.0.0",
74
74
  "@pnpm/network.fetch": "1100.1.11",
75
- "@pnpm/resolving.npm-resolver": "1103.1.0",
75
+ "@pnpm/resolving.npm-resolver": "1103.2.0",
76
76
  "@pnpm/test-fixtures": "1100.0.1",
77
77
  "@pnpm/testing.mock-agent": "1101.0.7",
78
78
  "@types/normalize-path": "^3.0.2",
79
79
  "@types/ramda": "0.32.0",
80
- "@types/semver": "7.7.1",
80
+ "@types/semver": "7.8.0",
81
81
  "@types/ssri": "^7.1.5",
82
82
  "@types/validate-npm-package-name": "^4.0.2",
83
83
  "load-json-file": "^7.0.1",