@pnpm/resolving.npm-resolver 1100.0.0 → 1100.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/lib/index.d.ts CHANGED
@@ -35,7 +35,12 @@ export interface ResolverFactoryOptions {
35
35
  saveWorkspaceProtocol?: boolean | 'rolling';
36
36
  preserveAbsolutePaths?: boolean;
37
37
  strictPublishedByCheck?: boolean;
38
+ ignoreMissingTimeField?: boolean;
38
39
  fetchWarnTimeoutMs?: number;
40
+ /** Pre-populated metadata cache. When provided, the resolver uses this
41
+ * instead of creating a new LRU cache. Useful for servers that keep
42
+ * metadata in SQLite or persist it across requests. */
43
+ metaCache?: PackageMetaCache;
39
44
  }
40
45
  export interface NpmResolveResult extends ResolveResult {
41
46
  latest?: string;
package/lib/index.js CHANGED
@@ -74,7 +74,7 @@ export function createNpmResolver(fetchFromRegistry, getAuthHeader, opts) {
74
74
  const fetch = pMemoize(fetchMetadataFromFromRegistry.bind(null, fetchOpts), {
75
75
  cacheKey: (...args) => JSON.stringify(args),
76
76
  });
77
- const metaCache = new LRUCache({
77
+ const metaCache = opts.metaCache ?? new LRUCache({
78
78
  max: 10000,
79
79
  ttl: 120 * 1000, // 2 minutes
80
80
  });
@@ -114,6 +114,7 @@ export function createNpmResolver(fetchFromRegistry, getAuthHeader, opts) {
114
114
  preferOffline: opts.preferOffline,
115
115
  cacheDir: opts.cacheDir,
116
116
  strictPublishedByCheck: opts.strictPublishedByCheck,
117
+ ignoreMissingTimeField: opts.ignoreMissingTimeField,
117
118
  }),
118
119
  registries: opts.registries,
119
120
  saveWorkspaceProtocol: opts.saveWorkspaceProtocol,
@@ -123,7 +124,9 @@ export function createNpmResolver(fetchFromRegistry, getAuthHeader, opts) {
123
124
  resolveFromNpm: resolveNpm.bind(null, ctx),
124
125
  resolveFromJsr: resolveJsr.bind(null, ctx),
125
126
  clearCache: () => {
126
- metaCache.clear();
127
+ if ('clear' in metaCache && typeof metaCache.clear === 'function') {
128
+ metaCache.clear();
129
+ }
127
130
  pMemoizeClear(fetch);
128
131
  },
129
132
  };
@@ -198,7 +201,7 @@ async function resolveNpm(ctx, wantedDependency, opts) {
198
201
  dryRun: opts.dryRun === true,
199
202
  preferredVersionSelectors: opts.preferredVersions?.[spec.name],
200
203
  registry,
201
- updateToLatest: opts.update === 'latest',
204
+ includeLatestTag: opts.update === 'latest',
202
205
  optional: wantedDependency.optional,
203
206
  });
204
207
  }
@@ -336,7 +339,7 @@ async function resolveJsr(ctx, wantedDependency, opts) {
336
339
  dryRun: opts.dryRun === true,
337
340
  preferredVersionSelectors: opts.preferredVersions?.[spec.name],
338
341
  registry,
339
- updateToLatest: opts.update === 'latest',
342
+ includeLatestTag: opts.update === 'latest',
340
343
  });
341
344
  if (pickedPackage == null) {
342
345
  throw new NoMatchingVersionError({ wantedDependency, packageMeta: meta, registry });
@@ -12,7 +12,7 @@ export interface PickPackageOptions extends PickPackageFromMetaOptions {
12
12
  pickLowestVersion?: boolean;
13
13
  registry: string;
14
14
  dryRun: boolean;
15
- updateToLatest?: boolean;
15
+ includeLatestTag?: boolean;
16
16
  optional?: boolean;
17
17
  }
18
18
  export declare function pickPackage(ctx: {
@@ -30,6 +30,7 @@ export declare function pickPackage(ctx: {
30
30
  preferOffline?: boolean;
31
31
  filterMetadata?: boolean;
32
32
  strictPublishedByCheck?: boolean;
33
+ ignoreMissingTimeField?: boolean;
33
34
  }, spec: RegistryPackageSpec, opts: PickPackageOptions): Promise<{
34
35
  meta: PackageMeta;
35
36
  pickedPackage: PackageInRegistry | null;
@@ -4,7 +4,7 @@ import { ABBREVIATED_META_DIR, FULL_FILTERED_META_DIR, FULL_META_DIR } from '@pn
4
4
  import { createHexHash } from '@pnpm/crypto.hash';
5
5
  import { PnpmError } from '@pnpm/error';
6
6
  import gfs from '@pnpm/fs.graceful-fs';
7
- import { logger } from '@pnpm/logger';
7
+ 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';
@@ -38,42 +38,82 @@ async function runLimited(pkgMirror, fn) {
38
38
  }
39
39
  }
40
40
  }
41
- const pickPackageFromMetaUsingTimeStrict = pickPackageFromMeta.bind(null, pickVersionByVersionRange);
42
- function pickPackageFromMetaUsingTime(opts, spec, meta) {
43
- const pickedPackage = pickPackageFromMeta(pickVersionByVersionRange, opts, spec, meta);
44
- if (pickedPackage)
45
- return pickedPackage;
46
- return pickPackageFromMeta(pickLowestVersionByVersionRange, {
47
- preferredVersionSelectors: opts.preferredVersionSelectors,
48
- }, spec, meta);
41
+ // When includeLatestTag is set, the "latest" dist-tag is added as a candidate
42
+ // alongside the requested spec, and the higher-versioned pick wins.
43
+ function runPicker(pickerOpts, spec, pickOne) {
44
+ const currentPkg = pickOne(spec);
45
+ if (!pickerOpts.includeLatestTag)
46
+ return currentPkg;
47
+ const latestPkg = pickOne({ ...spec, type: 'tag', fetchSpec: 'latest' });
48
+ return pickMax(latestPkg, currentPkg);
49
+ }
50
+ // Returns whichever pick has the higher version, treating null as "no match".
51
+ function pickMax(a, b) {
52
+ if (!a)
53
+ return b;
54
+ if (!b)
55
+ return a;
56
+ return semver.lt(a.version, b.version) ? b : a;
57
+ }
58
+ const pickHighest = pickPackageFromMeta.bind(null, pickVersionByVersionRange);
59
+ const pickLowest = pickPackageFromMeta.bind(null, pickLowestVersionByVersionRange);
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.
63
+ function pickRespectingMinReleaseAge(pickerOpts, spec, meta) {
64
+ return runPicker(pickerOpts, spec, (targetSpec) => {
65
+ const highest = pickHighest(pickerOpts, meta, targetSpec);
66
+ if (highest || pickerOpts.strictPublishedByCheck)
67
+ return highest;
68
+ return pickLowest({
69
+ preferredVersionSelectors: pickerOpts.preferredVersionSelectors,
70
+ }, meta, targetSpec);
71
+ });
72
+ }
73
+ // When minimumReleaseAge is not active: pick by pickLowestVersion preference.
74
+ function pickIgnoringReleaseAge(pickerOpts, spec, meta) {
75
+ const pickVersion = pickerOpts.pickLowestVersion ? pickLowest : pickHighest;
76
+ return runPicker(pickerOpts, spec, (targetSpec) => pickVersion(pickerOpts, meta, targetSpec));
77
+ }
78
+ // Used in shortcut/fall-through paths: if it fails (including with
79
+ // ERR_PNPM_MISSING_TIME), the caller falls through to the next path — e.g.
80
+ // the network fetch that can upgrade abbreviated metadata to full.
81
+ function pickMatchingVersionFast(pickerOpts, spec, meta) {
82
+ return pickerOpts.publishedBy
83
+ ? pickRespectingMinReleaseAge(pickerOpts, spec, meta)
84
+ : pickIgnoringReleaseAge(pickerOpts, spec, meta);
85
+ }
86
+ // Used at terminal return sites where no further fallback path exists. When
87
+ // metadata lacks the per-version `time` field and ignoreMissingTimeField is
88
+ // enabled, skip the minimumReleaseAge filter with a warning instead of
89
+ // failing hard.
90
+ function pickMatchingVersionFinal(pickerOpts, spec, meta) {
91
+ try {
92
+ return pickMatchingVersionFast(pickerOpts, spec, meta);
93
+ }
94
+ catch (err) {
95
+ if (pickerOpts.ignoreMissingTimeField && isMissingTimeError(err)) {
96
+ warnMissingTimeFieldOnce(meta.name);
97
+ return pickMatchingVersionFast({
98
+ ...pickerOpts,
99
+ publishedBy: undefined,
100
+ publishedByExclude: undefined,
101
+ }, spec, meta);
102
+ }
103
+ throw err;
104
+ }
49
105
  }
50
106
  export async function pickPackage(ctx, spec, opts) {
51
107
  opts = opts || {};
52
- const pickPackageFromMetaBySpec = (opts.publishedBy
53
- ? (ctx.strictPublishedByCheck ? pickPackageFromMetaUsingTimeStrict : pickPackageFromMetaUsingTime)
54
- : (pickPackageFromMeta.bind(null, opts.pickLowestVersion ? pickLowestVersionByVersionRange : pickVersionByVersionRange))).bind(null, {
108
+ const pickerOpts = {
55
109
  preferredVersionSelectors: opts.preferredVersionSelectors,
56
110
  publishedBy: opts.publishedBy,
57
111
  publishedByExclude: opts.publishedByExclude,
58
- });
59
- let _pickPackageFromMeta;
60
- if (opts.updateToLatest) {
61
- _pickPackageFromMeta = (meta) => {
62
- const latestStableSpec = { ...spec, type: 'tag', fetchSpec: 'latest' };
63
- const latestStable = pickPackageFromMetaBySpec(latestStableSpec, meta);
64
- const current = pickPackageFromMetaBySpec(spec, meta);
65
- if (!latestStable)
66
- return current;
67
- if (!current)
68
- return latestStable;
69
- if (semver.lt(latestStable.version, current.version))
70
- return current;
71
- return latestStable;
72
- };
73
- }
74
- else {
75
- _pickPackageFromMeta = pickPackageFromMetaBySpec.bind(null, spec);
76
- }
112
+ pickLowestVersion: opts.pickLowestVersion,
113
+ includeLatestTag: opts.includeLatestTag,
114
+ strictPublishedByCheck: ctx.strictPublishedByCheck,
115
+ ignoreMissingTimeField: ctx.ignoreMissingTimeField,
116
+ };
77
117
  validatePackageName(spec.name);
78
118
  // Use full metadata for optional dependencies to get libc field.
79
119
  // See: https://github.com/pnpm/pnpm/issues/9950
@@ -87,7 +127,7 @@ export async function pickPackage(ctx, spec, opts) {
87
127
  if (cachedMeta != null) {
88
128
  return {
89
129
  meta: cachedMeta,
90
- pickedPackage: _pickPackageFromMeta(cachedMeta),
130
+ pickedPackage: pickMatchingVersionFinal(pickerOpts, spec, cachedMeta),
91
131
  };
92
132
  }
93
133
  const registryName = getRegistryName(opts.registry);
@@ -100,12 +140,12 @@ export async function pickPackage(ctx, spec, opts) {
100
140
  if (metaCachedInStore != null)
101
141
  return {
102
142
  meta: metaCachedInStore,
103
- pickedPackage: _pickPackageFromMeta(metaCachedInStore),
143
+ pickedPackage: pickMatchingVersionFinal(pickerOpts, spec, metaCachedInStore),
104
144
  };
105
145
  throw new PnpmError('NO_OFFLINE_META', `Failed to resolve ${toRaw(spec)} in package mirror ${pkgMirror}`);
106
146
  }
107
147
  if (metaCachedInStore != null) {
108
- const pickedPackage = _pickPackageFromMeta(metaCachedInStore);
148
+ const pickedPackage = pickMatchingVersionFinal(pickerOpts, spec, metaCachedInStore);
109
149
  if (pickedPackage) {
110
150
  return {
111
151
  meta: metaCachedInStore,
@@ -114,13 +154,13 @@ export async function pickPackage(ctx, spec, opts) {
114
154
  }
115
155
  }
116
156
  }
117
- if (!opts.updateToLatest && spec.type === 'version') {
157
+ if (!opts.includeLatestTag && spec.type === 'version') {
118
158
  metaCachedInStore = metaCachedInStore ?? await limit(async () => loadMeta(pkgMirror));
119
159
  // use the cached meta only if it has the required package version
120
160
  // otherwise it is probably out of date
121
161
  if ((metaCachedInStore?.versions?.[spec.fetchSpec]) != null) {
122
162
  try {
123
- const pickedPackage = _pickPackageFromMeta(metaCachedInStore);
163
+ const pickedPackage = pickMatchingVersionFast(pickerOpts, spec, metaCachedInStore);
124
164
  if (pickedPackage) {
125
165
  return {
126
166
  meta: metaCachedInStore,
@@ -141,7 +181,7 @@ export async function pickPackage(ctx, spec, opts) {
141
181
  metaCachedInStore = metaCachedInStore ?? await limit(async () => loadMeta(pkgMirror));
142
182
  if (metaCachedInStore != null) {
143
183
  try {
144
- const pickedPackage = _pickPackageFromMeta(metaCachedInStore);
184
+ const pickedPackage = pickMatchingVersionFast(pickerOpts, spec, metaCachedInStore);
145
185
  if (pickedPackage) {
146
186
  return {
147
187
  meta: metaCachedInStore,
@@ -182,7 +222,7 @@ export async function pickPackage(ctx, spec, opts) {
182
222
  ctx.metaCache.set(cacheKey, metaCachedInStore);
183
223
  return {
184
224
  meta: metaCachedInStore,
185
- pickedPackage: _pickPackageFromMeta(metaCachedInStore),
225
+ pickedPackage: pickMatchingVersionFinal(pickerOpts, spec, metaCachedInStore),
186
226
  };
187
227
  }
188
228
  throw new PnpmError('CACHE_MISSING_AFTER_304', `Metadata cache for ${spec.name} is unreadable after receiving 304 Not Modified`);
@@ -250,7 +290,7 @@ export async function pickPackage(ctx, spec, opts) {
250
290
  ctx.metaCache.set(cacheKey, meta);
251
291
  return {
252
292
  meta,
253
- pickedPackage: _pickPackageFromMeta(meta),
293
+ pickedPackage: pickMatchingVersionFinal(pickerOpts, spec, meta),
254
294
  };
255
295
  }
256
296
  catch (err) { // eslint-disable-line
@@ -262,7 +302,7 @@ export async function pickPackage(ctx, spec, opts) {
262
302
  logger.debug({ message: `Using cached meta from ${pkgMirror}` });
263
303
  return {
264
304
  meta,
265
- pickedPackage: _pickPackageFromMeta(meta),
305
+ pickedPackage: pickMatchingVersionFinal(pickerOpts, spec, meta),
266
306
  };
267
307
  }
268
308
  });
@@ -325,6 +365,22 @@ function isMissingTimeError(err) {
325
365
  'code' in err &&
326
366
  err.code === 'ERR_PNPM_MISSING_TIME');
327
367
  }
368
+ // Cap the size so long-lived processes (daemons, store servers) can't leak
369
+ // memory via this Set as they resolve ever more distinct packages.
370
+ const MAX_WARNED_MISSING_TIME = 1024;
371
+ const warnedMissingTimeFor = new Set();
372
+ function warnMissingTimeFieldOnce(pkgName) {
373
+ if (warnedMissingTimeFor.has(pkgName))
374
+ return;
375
+ if (warnedMissingTimeFor.size >= MAX_WARNED_MISSING_TIME) {
376
+ // Set preserves insertion order, so the first entry is the oldest.
377
+ const oldest = warnedMissingTimeFor.values().next().value;
378
+ if (oldest != null)
379
+ warnedMissingTimeFor.delete(oldest);
380
+ }
381
+ warnedMissingTimeFor.add(pkgName);
382
+ globalWarn(`The metadata of ${pkgName} is missing the "time" field; skipping the minimumReleaseAge check for this package.`);
383
+ }
328
384
  async function getFileMtime(filePath) {
329
385
  try {
330
386
  const stat = await fs.stat(filePath);
@@ -14,7 +14,7 @@ export interface PickPackageFromMetaOptions {
14
14
  publishedBy?: Date;
15
15
  publishedByExclude?: PackageVersionPolicy;
16
16
  }
17
- export declare function pickPackageFromMeta(pickVersionByVersionRangeFn: PickVersionByVersionRange, { preferredVersionSelectors, publishedBy, publishedByExclude }: PickPackageFromMetaOptions, spec: RegistryPackageSpec, meta: PackageMeta): PackageInRegistry | null;
17
+ export declare function pickPackageFromMeta(pickVersionByVersionRangeFn: PickVersionByVersionRange, { preferredVersionSelectors, publishedBy, publishedByExclude }: PickPackageFromMetaOptions, meta: PackageMeta, spec: RegistryPackageSpec): PackageInRegistry | null;
18
18
  export declare function assertMetaHasTime(meta: PackageMeta): asserts meta is PackageMetaWithTime;
19
19
  export declare function pickLowestVersionByVersionRange({ meta, versionRange, preferredVersionSelectors }: PickVersionByVersionRangeOptions): string | null;
20
20
  export declare function pickVersionByVersionRange({ meta, versionRange, preferredVersionSelectors }: PickVersionByVersionRangeOptions): string | null;
@@ -2,7 +2,7 @@ import util from 'node:util';
2
2
  import { PnpmError } from '@pnpm/error';
3
3
  import { filterPkgMetadataByPublishDate } from '@pnpm/resolving.registry.pkg-metadata-filter';
4
4
  import semver from 'semver';
5
- export function pickPackageFromMeta(pickVersionByVersionRangeFn, { preferredVersionSelectors, publishedBy, publishedByExclude, }, spec, meta) {
5
+ export function pickPackageFromMeta(pickVersionByVersionRangeFn, { preferredVersionSelectors, publishedBy, publishedByExclude, }, meta, spec) {
6
6
  if (publishedBy) {
7
7
  const excludeResult = publishedByExclude?.(meta.name) ?? false;
8
8
  if (excludeResult !== true) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/resolving.npm-resolver",
3
- "version": "1100.0.0",
3
+ "version": "1100.1.0",
4
4
  "description": "Resolver for npm-hosted packages",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -41,25 +41,25 @@
41
41
  "ssri": "13.0.1",
42
42
  "version-selector-type": "^3.0.0",
43
43
  "@pnpm/constants": "1100.0.0",
44
- "@pnpm/config.pick-registry-for-package": "1100.0.0",
45
- "@pnpm/core-loggers": "1100.0.0",
44
+ "@pnpm/config.pick-registry-for-package": "1100.0.1",
46
45
  "@pnpm/crypto.hash": "1100.0.0",
47
46
  "@pnpm/error": "1100.0.0",
48
- "@pnpm/fetching.types": "1100.0.0",
49
47
  "@pnpm/fs.graceful-fs": "1100.0.0",
48
+ "@pnpm/fetching.types": "1100.0.0",
50
49
  "@pnpm/resolving.jsr-specifier-parser": "1100.0.0",
51
- "@pnpm/resolving.registry.types": "1100.0.0",
52
- "@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.0",
53
- "@pnpm/resolving.resolver-base": "1100.0.0",
54
- "@pnpm/types": "1100.0.0",
50
+ "@pnpm/core-loggers": "1100.0.1",
51
+ "@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.1",
52
+ "@pnpm/resolving.resolver-base": "1100.1.0",
53
+ "@pnpm/store.cafs": "1100.0.2",
55
54
  "@pnpm/store.index": "1100.0.0",
56
- "@pnpm/store.cafs": "1100.0.0",
55
+ "@pnpm/resolving.registry.types": "1100.0.1",
56
+ "@pnpm/types": "1101.0.0",
57
57
  "@pnpm/workspace.spec-parser": "1100.0.0",
58
58
  "@pnpm/workspace.range-resolver": "1100.0.0"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "@pnpm/logger": ">=1001.0.0 <1002.0.0",
62
- "@pnpm/worker": "^1100.0.0"
62
+ "@pnpm/worker": "^1100.0.2"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@types/normalize-path": "^3.0.2",
@@ -68,12 +68,12 @@
68
68
  "@types/ssri": "^7.1.5",
69
69
  "load-json-file": "^7.0.1",
70
70
  "tempy": "3.0.0",
71
- "@pnpm/config.version-policy": "1100.0.0",
71
+ "@pnpm/config.version-policy": "1100.0.1",
72
72
  "@pnpm/logger": "1100.0.0",
73
- "@pnpm/network.fetch": "1100.0.0",
74
- "@pnpm/testing.mock-agent": "1100.0.0",
75
- "@pnpm/resolving.npm-resolver": "1100.0.0",
76
- "@pnpm/test-fixtures": "1100.0.0"
73
+ "@pnpm/resolving.npm-resolver": "1100.1.0",
74
+ "@pnpm/network.fetch": "1100.0.1",
75
+ "@pnpm/test-fixtures": "1100.0.0",
76
+ "@pnpm/testing.mock-agent": "1100.0.1"
77
77
  },
78
78
  "engines": {
79
79
  "node": ">=22.13"