@pnpm/resolving.npm-resolver 1101.0.3 → 1101.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.
package/lib/fetch.js CHANGED
@@ -3,24 +3,71 @@ import { requestRetryLogger } from '@pnpm/core-loggers';
3
3
  import { FetchError, PnpmError, } from '@pnpm/error';
4
4
  import { globalWarn } from '@pnpm/logger';
5
5
  import * as retry from '@zkochan/retry';
6
- // https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
7
- // eslint-disable-next-line regexp/no-super-linear-backtracking, regexp/use-ignore-case
8
- const semverRegex = /(.*)(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
6
+ import semver from 'semver';
9
7
  export class RegistryResponseError extends FetchError {
10
8
  pkgName;
11
9
  constructor(request, response, pkgName) {
12
10
  let hint;
13
11
  if (response.status === 404) {
14
12
  hint = `${pkgName} is not in the npm registry, or you have no permission to fetch it.`;
15
- const matched = pkgName.match(semverRegex);
16
- if (matched != null) {
17
- hint += ` Did you mean ${matched[1]}?`;
13
+ const nameWithoutVersion = stripTrailingSemverSuffix(pkgName);
14
+ if (nameWithoutVersion != null) {
15
+ hint += ` Did you mean ${nameWithoutVersion}?`;
18
16
  }
19
17
  }
20
18
  super(request, response, hint);
21
19
  this.pkgName = pkgName;
22
20
  }
23
21
  }
22
+ /**
23
+ * Detect when a package name accidentally includes a `<version>` suffix
24
+ * (e.g. `lodash@4.17.21` or `lodash4.17.21`) and return the part before the
25
+ * version. Returns `undefined` when no semver suffix is present.
26
+ *
27
+ * Implemented as an O(n) scan to avoid polynomial backtracking on adversarial
28
+ * input (CodeQL: js/polynomial-redos).
29
+ */
30
+ function stripTrailingSemverSuffix(pkgName) {
31
+ // Common case: "name@version" – split on the rightmost '@'.
32
+ // `atIdx > 0` rules out the leading '@' of scoped names like '@scope/foo'.
33
+ const atIdx = pkgName.lastIndexOf('@');
34
+ if (atIdx > 0 && semver.valid(pkgName.slice(atIdx + 1)) != null) {
35
+ return pkgName.slice(0, atIdx);
36
+ }
37
+ // Fallback: detect a trailing "<digits>.<digits>.<digits>" appended to a name
38
+ // with no separator (e.g. "foo1.0.0"). We walk backwards through three
39
+ // digit-blocks separated by dots; this is O(n) and free of regex backtracking.
40
+ let i = pkgName.length;
41
+ i = consumeTrailingDigits(pkgName, i);
42
+ if (i === pkgName.length || i === 0 || pkgName.charCodeAt(i - 1) !== 46 /* '.' */)
43
+ return undefined;
44
+ i--;
45
+ const beforePatch = i;
46
+ i = consumeTrailingDigits(pkgName, i);
47
+ if (i === beforePatch || i === 0 || pkgName.charCodeAt(i - 1) !== 46)
48
+ return undefined;
49
+ i--;
50
+ const beforeMinor = i;
51
+ i = consumeTrailingDigits(pkgName, i);
52
+ if (i === beforeMinor || i === 0)
53
+ return undefined;
54
+ if (semver.valid(pkgName.slice(i)) == null)
55
+ return undefined;
56
+ let prefix = pkgName.slice(0, i);
57
+ if (prefix.endsWith('@'))
58
+ prefix = prefix.slice(0, -1);
59
+ return prefix.length > 0 ? prefix : undefined;
60
+ }
61
+ function consumeTrailingDigits(s, end) {
62
+ let i = end;
63
+ while (i > 0) {
64
+ const c = s.charCodeAt(i - 1);
65
+ if (c < 48 || c > 57)
66
+ break;
67
+ i--;
68
+ }
69
+ return i;
70
+ }
24
71
  export async function fetchMetadataFromFromRegistry(fetchOpts, pkgName, { authHeaderValue, etag: cachedEtag, fullMetadata, modified: cachedModified, registry, }) {
25
72
  const uri = toUri(pkgName, registry);
26
73
  const op = retry.operation(fetchOpts.retry);
package/lib/index.d.ts CHANGED
@@ -4,7 +4,7 @@ import type { PackageMeta } from '@pnpm/resolving.registry.types';
4
4
  import type { DirectoryResolution, PkgResolutionId, PreferredVersions, ResolveResult, TarballResolution, WantedDependency, WorkspacePackages } from '@pnpm/resolving.resolver-base';
5
5
  import type { DependencyManifest, PackageVersionPolicy, PinnedVersion, Registries, TrustPolicy } from '@pnpm/types';
6
6
  import { fetchMetadataFromFromRegistry, type FetchMetadataFromFromRegistryOptions, RegistryResponseError } from './fetch.js';
7
- import { parseBareSpecifier, type RegistryPackageSpec } from './parseBareSpecifier.js';
7
+ import { BUILTIN_NAMED_REGISTRIES, parseBareSpecifier, type RegistryPackageSpec } from './parseBareSpecifier.js';
8
8
  import { type PackageMetaCache, pickPackage, type PickPackageOptions } from './pickPackage.js';
9
9
  import { pickPackageFromMeta, pickVersionByVersionRange } from './pickPackageFromMeta.js';
10
10
  import { workspacePrefToNpm } from './workspacePrefToNpm.js';
@@ -20,7 +20,8 @@ export declare class NoMatchingVersionError extends PnpmError {
20
20
  readonly immatureVersion?: string;
21
21
  constructor(opts: NoMatchingVersionErrorOptions);
22
22
  }
23
- export { fetchMetadataFromFromRegistry, type FetchMetadataFromFromRegistryOptions, type PackageMeta, type PackageMetaCache, parseBareSpecifier, pickPackageFromMeta, pickVersionByVersionRange, type RegistryPackageSpec, RegistryResponseError, workspacePrefToNpm, };
23
+ export declare function formatTimeAgo(date: Date): string | null;
24
+ export { BUILTIN_NAMED_REGISTRIES, fetchMetadataFromFromRegistry, type FetchMetadataFromFromRegistryOptions, type PackageMeta, type PackageMetaCache, parseBareSpecifier, pickPackageFromMeta, pickVersionByVersionRange, type RegistryPackageSpec, RegistryResponseError, workspacePrefToNpm, };
24
25
  export { whichVersionIsPinned } from './whichVersionIsPinned.js';
25
26
  export interface ResolverFactoryOptions {
26
27
  cacheDir: string;
@@ -32,6 +33,7 @@ export interface ResolverFactoryOptions {
32
33
  retry?: RetryTimeoutOptions;
33
34
  timeout?: number;
34
35
  registries: Registries;
36
+ namedRegistries?: Record<string, string>;
35
37
  saveWorkspaceProtocol?: boolean | 'rolling';
36
38
  preserveAbsolutePaths?: boolean;
37
39
  strictPublishedByCheck?: boolean;
@@ -54,6 +56,14 @@ export interface JsrResolveResult extends ResolveResult {
54
56
  resolution: TarballResolution;
55
57
  resolvedVia: 'jsr-registry';
56
58
  }
59
+ export interface NamedRegistryResolveResult extends ResolveResult {
60
+ alias: string;
61
+ /** The named-registry alias that was matched, e.g. `gh` or a user-defined name. */
62
+ registryName: string;
63
+ manifest: DependencyManifest;
64
+ resolution: TarballResolution;
65
+ resolvedVia: 'named-registry';
66
+ }
57
67
  export interface WorkspaceResolveResult extends ResolveResult {
58
68
  manifest: DependencyManifest;
59
69
  resolution: DirectoryResolution;
@@ -61,16 +71,19 @@ export interface WorkspaceResolveResult extends ResolveResult {
61
71
  }
62
72
  export type NpmResolver = (wantedDependency: WantedDependency & {
63
73
  optional?: boolean;
64
- }, opts: ResolveFromNpmOptions) => Promise<NpmResolveResult | JsrResolveResult | WorkspaceResolveResult | null>;
74
+ }, opts: ResolveFromNpmOptions) => Promise<NpmResolveResult | JsrResolveResult | NamedRegistryResolveResult | WorkspaceResolveResult | null>;
65
75
  export declare function createNpmResolver(fetchFromRegistry: FetchFromRegistry, getAuthHeader: GetAuthHeader, opts: ResolverFactoryOptions): {
66
76
  resolveFromNpm: NpmResolver;
67
77
  resolveFromJsr: NpmResolver;
78
+ resolveFromNamedRegistry: NpmResolver;
68
79
  clearCache: () => void;
69
80
  };
70
81
  export interface ResolveFromNpmContext {
71
82
  pickPackage: (spec: RegistryPackageSpec, opts: PickPackageOptions) => ReturnType<typeof pickPackage>;
72
83
  getAuthHeaderValueByURI: (registry: string) => string | undefined;
73
84
  registries: Registries;
85
+ namedRegistries: Record<string, string>;
86
+ namedRegistryNames: ReadonlySet<string>;
74
87
  saveWorkspaceProtocol?: boolean | 'rolling';
75
88
  peekManifestFromStore?: (opts: {
76
89
  id: PkgResolutionId;
package/lib/index.js CHANGED
@@ -13,7 +13,7 @@ import ssri from 'ssri';
13
13
  import versionSelectorType from 'version-selector-type';
14
14
  import { fetchMetadataFromFromRegistry, RegistryResponseError } from './fetch.js';
15
15
  import { normalizeRegistryUrl } from './normalizeRegistryUrl.js';
16
- import { parseBareSpecifier, parseJsrSpecifierToRegistryPackageSpec, } from './parseBareSpecifier.js';
16
+ import { BUILTIN_NAMED_REGISTRIES, parseBareSpecifier, parseJsrSpecifierToRegistryPackageSpec, parseNamedRegistrySpecifierToRegistryPackageSpec, } from './parseBareSpecifier.js';
17
17
  import { pickPackage, } from './pickPackage.js';
18
18
  import { pickPackageFromMeta, pickVersionByVersionRange } from './pickPackageFromMeta.js';
19
19
  import { failIfTrustDowngraded } from './trustChecks.js';
@@ -29,7 +29,7 @@ export class NoMatchingVersionError extends PnpmError {
29
29
  let errorMessage;
30
30
  if (opts.publishedBy && opts.immatureVersion && opts.packageMeta.time) {
31
31
  const time = new Date(opts.packageMeta.time[opts.immatureVersion]);
32
- const releaseAgeText = formatTimeAgo(time);
32
+ const releaseAgeText = formatTimeAgo(time) ?? 'just now';
33
33
  const pkgName = opts.wantedDependency.alias ?? opts.packageMeta.name;
34
34
  errorMessage = `Version ${opts.immatureVersion} (released ${releaseAgeText}) of ${pkgName} does not meet the minimumReleaseAge constraint`;
35
35
  }
@@ -41,25 +41,36 @@ export class NoMatchingVersionError extends PnpmError {
41
41
  this.immatureVersion = opts.immatureVersion;
42
42
  }
43
43
  }
44
- function formatTimeAgo(date) {
45
- const now = Date.now();
46
- const diffMs = now - date.getTime();
47
- // Handle clock skew (future dates) and very recent releases (< 1 minute)
48
- if (diffMs < 60 * 1000) {
49
- return 'just now';
50
- }
51
- const diffMinutes = Math.floor(diffMs / (60 * 1000));
52
- const diffHours = Math.floor(diffMs / (60 * 60 * 1000));
53
- const diffDays = Math.floor(diffMs / (24 * 60 * 60 * 1000));
54
- if (diffHours >= 48) {
55
- return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`;
44
+ export function formatTimeAgo(date) {
45
+ const ts = date.getTime();
46
+ if (isNaN(ts)) {
47
+ return null;
56
48
  }
57
- if (diffMinutes >= 90) {
58
- return `${diffHours} hour${diffHours === 1 ? '' : 's'} ago`;
49
+ const now = Date.now();
50
+ const diffMs = now - ts;
51
+ // Handle clock skew (future dates)
52
+ if (diffMs < 0) {
53
+ return null;
59
54
  }
60
- return `${diffMinutes} minute${diffMinutes === 1 ? '' : 's'} ago`;
55
+ const diffSec = Math.floor(diffMs / 1000);
56
+ const diffMin = Math.floor(diffSec / 60);
57
+ const diffHour = Math.floor(diffMin / 60);
58
+ const diffDay = Math.floor(diffHour / 24);
59
+ const diffMonth = Math.floor(diffDay / 30);
60
+ const diffYear = Math.floor(diffDay / 365);
61
+ if (diffYear > 0)
62
+ return `${diffYear} year${diffYear === 1 ? '' : 's'} ago`;
63
+ if (diffMonth > 0)
64
+ return `${diffMonth} month${diffMonth === 1 ? '' : 's'} ago`;
65
+ if (diffDay > 0)
66
+ return `${diffDay} day${diffDay === 1 ? '' : 's'} ago`;
67
+ if (diffHour > 0)
68
+ return `${diffHour} hour${diffHour === 1 ? '' : 's'} ago`;
69
+ if (diffMin > 0)
70
+ return `${diffMin} minute${diffMin === 1 ? '' : 's'} ago`;
71
+ return 'a few seconds ago';
61
72
  }
62
- export { fetchMetadataFromFromRegistry, parseBareSpecifier, pickPackageFromMeta, pickVersionByVersionRange, RegistryResponseError, workspacePrefToNpm, };
73
+ export { BUILTIN_NAMED_REGISTRIES, fetchMetadataFromFromRegistry, parseBareSpecifier, pickPackageFromMeta, pickVersionByVersionRange, RegistryResponseError, workspacePrefToNpm, };
63
74
  export { whichVersionIsPinned } from './whichVersionIsPinned.js';
64
75
  export function createNpmResolver(fetchFromRegistry, getAuthHeader, opts) {
65
76
  if (typeof opts.cacheDir !== 'string') {
@@ -103,6 +114,8 @@ export function createNpmResolver(fetchFromRegistry, getAuthHeader, opts) {
103
114
  return request;
104
115
  };
105
116
  }
117
+ const namedRegistries = mergeNamedRegistries(opts.namedRegistries);
118
+ const namedRegistryNames = new Set(Object.keys(namedRegistries));
106
119
  const ctx = {
107
120
  getAuthHeaderValueByURI: getAuthHeader,
108
121
  pickPackage: pickPackage.bind(null, {
@@ -117,12 +130,15 @@ export function createNpmResolver(fetchFromRegistry, getAuthHeader, opts) {
117
130
  ignoreMissingTimeField: opts.ignoreMissingTimeField,
118
131
  }),
119
132
  registries: opts.registries,
133
+ namedRegistries,
134
+ namedRegistryNames,
120
135
  saveWorkspaceProtocol: opts.saveWorkspaceProtocol,
121
136
  peekManifestFromStore,
122
137
  };
123
138
  return {
124
139
  resolveFromNpm: resolveNpm.bind(null, ctx),
125
140
  resolveFromJsr: resolveJsr.bind(null, ctx),
141
+ resolveFromNamedRegistry: resolveFromNamedRegistry.bind(null, ctx),
126
142
  clearCache: () => {
127
143
  if ('clear' in metaCache && typeof metaCache.clear === 'function') {
128
144
  metaCache.clear();
@@ -164,7 +180,13 @@ async function resolveNpm(ctx, wantedDependency, opts) {
164
180
  // Fast path: if we have a current resolution with integrity, try to peek the manifest from the store.
165
181
  // This avoids the expensive metadata fetch from the registry.
166
182
  // We do this AFTER ensuring the spec is valid for this resolver to avoids hijacking other resolvers.
167
- if (ctx.peekManifestFromStore && opts.currentPkg?.resolution && !opts.update) {
183
+ // If publishedBy is set (resolutionMode=time-based or minimumReleaseAge is configured), we only take
184
+ // the fast path when publishedAt is already known from the lockfile's `time:` block; otherwise we
185
+ // fall through to a registry fetch so the cutoff isn't computed from missing data.
186
+ if (ctx.peekManifestFromStore &&
187
+ opts.currentPkg?.resolution &&
188
+ !opts.update &&
189
+ (opts.publishedBy == null || opts.currentPkg.publishedAt != null)) {
168
190
  const currentResolution = opts.currentPkg.resolution;
169
191
  // Only use this optimization for tarball resolutions with integrity (npm packages)
170
192
  if ('tarball' in currentResolution && currentResolution.integrity) {
@@ -184,7 +206,7 @@ async function resolveNpm(ctx, wantedDependency, opts) {
184
206
  manifest,
185
207
  resolution: currentResolution,
186
208
  resolvedVia: 'npm-registry',
187
- publishedAt: undefined, // Don't have this without metadata
209
+ publishedAt: opts.currentPkg.publishedAt,
188
210
  };
189
211
  }
190
212
  }
@@ -326,52 +348,117 @@ async function resolveNpm(ctx, wantedDependency, opts) {
326
348
  async function resolveJsr(ctx, wantedDependency, opts) {
327
349
  if (!wantedDependency.bareSpecifier)
328
350
  return null;
329
- const defaultTag = opts.defaultTag ?? 'latest';
330
- const registry = ctx.registries['@jsr']; // '@jsr' is always defined
331
- const spec = parseJsrSpecifierToRegistryPackageSpec(wantedDependency.bareSpecifier, wantedDependency.alias, defaultTag);
351
+ const spec = parseJsrSpecifierToRegistryPackageSpec(wantedDependency.bareSpecifier, wantedDependency.alias, opts.defaultTag ?? 'latest');
352
+ if (spec == null)
353
+ return null;
354
+ const picked = await pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, ctx.registries['@jsr']); // '@jsr' is always defined
355
+ return {
356
+ ...picked,
357
+ normalizedBareSpecifier: opts.calcSpecifier
358
+ ? calcPrefixedSpecifier('jsr:', spec.jsrPkgName, wantedDependency, picked.manifest.version, opts.pinnedVersion)
359
+ : undefined,
360
+ resolvedVia: 'jsr-registry',
361
+ alias: spec.jsrPkgName,
362
+ };
363
+ }
364
+ // Merges user-supplied named-registry aliases (from config) on top of pnpm's
365
+ // built-in defaults (e.g. `gh` → GitHub Packages). User entries take precedence
366
+ // so GHES users can point `gh` at their enterprise host. URLs are validated
367
+ // here so typos like `npm.work.example.com` (no scheme) surface at startup
368
+ // rather than as a confusing 404 during resolution. The named-registry
369
+ // resolver runs last in the resolution chain, so an alias that collides with
370
+ // another specifier scheme (e.g. `git`, `github`, `jsr`) is silently shadowed
371
+ // by that scheme's dedicated resolver — no cross-resolver knowledge needed.
372
+ function mergeNamedRegistries(userDefined) {
373
+ const merged = { ...BUILTIN_NAMED_REGISTRIES };
374
+ if (!userDefined)
375
+ return merged;
376
+ for (const [alias, url] of Object.entries(userDefined)) {
377
+ if (typeof url !== 'string' || !isValidHttpUrl(url)) {
378
+ 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/' });
379
+ }
380
+ merged[alias] = url;
381
+ }
382
+ return merged;
383
+ }
384
+ function isValidHttpUrl(url) {
385
+ try {
386
+ const parsed = new URL(url);
387
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:';
388
+ }
389
+ catch {
390
+ return false;
391
+ }
392
+ }
393
+ // Resolves a `<alias>:` specifier from one of the configured named registries.
394
+ // The `gh:` alias ships as a built-in default pointing at the GitHub Packages
395
+ // npm registry; additional aliases come from pnpm-workspace.yaml's
396
+ // `namedRegistries` field. Auth tokens are looked up by the resolved registry
397
+ // URL, so a `//npm.pkg.github.com/:_authToken=...` entry in `.npmrc` is
398
+ // picked up automatically for `gh:` specifiers (and analogously for any user-
399
+ // configured alias).
400
+ async function resolveFromNamedRegistry(ctx, wantedDependency, opts) {
401
+ if (!wantedDependency.bareSpecifier)
402
+ return null;
403
+ const spec = parseNamedRegistrySpecifierToRegistryPackageSpec(wantedDependency.bareSpecifier, ctx.namedRegistryNames, wantedDependency.alias, opts.defaultTag ?? 'latest');
332
404
  if (spec == null)
333
405
  return null;
406
+ const registry = ctx.namedRegistries[spec.registryName];
407
+ if (!registry)
408
+ return null; // defensive: should never trigger because parse checks the alias set
409
+ const picked = await pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, registry);
410
+ return {
411
+ ...picked,
412
+ normalizedBareSpecifier: opts.calcSpecifier
413
+ ? calcPrefixedSpecifier(`${spec.registryName}:`, spec.name, wantedDependency, picked.manifest.version, opts.pinnedVersion)
414
+ : undefined,
415
+ resolvedVia: 'named-registry',
416
+ registryName: spec.registryName,
417
+ // Exposes the scoped package name so callers that omit an explicit alias
418
+ // (e.g. `pnpm add gh:@acme/foo`) record the dependency under `@acme/foo`.
419
+ alias: spec.name,
420
+ };
421
+ }
422
+ // Shared inner shell for resolvers that pull from a single registry URL with
423
+ // an already-parsed RegistryPackageSpec (jsr, named-registry). Returns the
424
+ // fields common to their result envelopes; each caller adds its own
425
+ // resolvedVia, alias, and normalizedBareSpecifier.
426
+ async function pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, registry) {
334
427
  const authHeaderValue = ctx.getAuthHeaderValueByURI(registry);
335
428
  const { meta, pickedPackage } = await ctx.pickPackage(spec, {
336
429
  pickLowestVersion: opts.pickLowestVersion,
337
430
  publishedBy: opts.publishedBy,
431
+ publishedByExclude: opts.publishedByExclude,
338
432
  authHeaderValue,
339
433
  dryRun: opts.dryRun === true,
340
434
  preferredVersionSelectors: opts.preferredVersions?.[spec.name],
341
435
  registry,
342
436
  includeLatestTag: opts.update === 'latest',
437
+ optional: wantedDependency.optional,
343
438
  });
344
439
  if (pickedPackage == null) {
345
440
  throw new NoMatchingVersionError({ wantedDependency, packageMeta: meta, registry });
346
441
  }
347
- const id = `${pickedPackage.name}@${pickedPackage.version}`;
348
- const resolution = {
349
- integrity: getIntegrity(pickedPackage.dist),
350
- tarball: normalizeRegistryUrl(pickedPackage.dist.tarball),
351
- };
352
442
  return {
353
- id,
443
+ id: `${pickedPackage.name}@${pickedPackage.version}`,
354
444
  latest: meta['dist-tags'].latest,
355
445
  manifest: pickedPackage,
356
- normalizedBareSpecifier: opts.calcSpecifier
357
- ? calcJsrSpecifier({
358
- wantedDependency,
359
- spec,
360
- version: pickedPackage.version,
361
- defaultPinnedVersion: opts.pinnedVersion,
362
- })
363
- : undefined,
364
- resolution,
365
- resolvedVia: 'jsr-registry',
446
+ resolution: {
447
+ integrity: getIntegrity(pickedPackage.dist),
448
+ tarball: normalizeRegistryUrl(pickedPackage.dist.tarball),
449
+ },
366
450
  publishedAt: meta.time?.[pickedPackage.version],
367
- alias: spec.jsrPkgName,
368
451
  };
369
452
  }
370
- function calcJsrSpecifier({ wantedDependency, spec, version, defaultPinnedVersion, }) {
453
+ // Builds a `<prefix><pkgName>@<range>` specifier (or a bare `<prefix><range>`
454
+ // when the dependency alias matches the package name). Shared between the
455
+ // jsr and named-registry resolvers since they only differ in `prefix` and
456
+ // which spec field holds the package name.
457
+ function calcPrefixedSpecifier(prefix, pkgName, wantedDependency, version, defaultPinnedVersion) {
371
458
  const range = calcRange(version, wantedDependency, defaultPinnedVersion);
372
- if (!wantedDependency.alias || spec.jsrPkgName === wantedDependency.alias)
373
- return `jsr:${range}`;
374
- return `jsr:${spec.jsrPkgName}@${range}`;
459
+ if (!wantedDependency.alias || pkgName === wantedDependency.alias)
460
+ return `${prefix}${range}`;
461
+ return `${prefix}${pkgName}@${range}`;
375
462
  }
376
463
  function calcSpecifier({ wantedDependency, spec, version, defaultPinnedVersion, }) {
377
464
  if (wantedDependency.prevSpecifier === wantedDependency.bareSpecifier && wantedDependency.prevSpecifier && versionSelectorType(wantedDependency.prevSpecifier)?.type === 'tag') {
@@ -9,3 +9,8 @@ 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 declare const BUILTIN_NAMED_REGISTRIES: Readonly<Record<string, string>>;
13
+ export interface NamedRegistryPackageSpec extends RegistryPackageSpec {
14
+ registryName: string;
15
+ }
16
+ export declare function parseNamedRegistrySpecifierToRegistryPackageSpec(rawSpecifier: string, knownRegistryNames: ReadonlySet<string>, packageAlias: string | undefined, defaultTag: string): NamedRegistryPackageSpec | null;
@@ -1,18 +1,29 @@
1
+ import { PnpmError } from '@pnpm/error';
1
2
  import { parseJsrSpecifier } from '@pnpm/resolving.jsr-specifier-parser';
2
3
  import { parseNpmTarballUrl } from 'parse-npm-tarball-url';
4
+ import semver from 'semver';
3
5
  import getVersionSelectorType from 'version-selector-type';
4
6
  export function parseBareSpecifier(bareSpecifier, alias, defaultTag, registry) {
5
7
  let name = alias;
6
8
  if (bareSpecifier.startsWith('npm:')) {
7
9
  bareSpecifier = bareSpecifier.slice(4);
8
- const index = bareSpecifier.lastIndexOf('@');
9
- if (index < 1) {
10
- name = bareSpecifier;
11
- bareSpecifier = defaultTag;
10
+ // `npm:<version_selector>` fall back to the outer dependency alias as
11
+ // the package name, mirroring the named-registry shape (e.g. `gh:^1.0.0`).
12
+ // Restricted to semver ranges/versions so unscoped package names like
13
+ // `npm:is-positive` keep their npm package-aliasing meaning.
14
+ if (alias && semver.validRange(bareSpecifier) != null) {
15
+ name = alias;
12
16
  }
13
17
  else {
14
- name = bareSpecifier.slice(0, index);
15
- bareSpecifier = bareSpecifier.slice(index + 1);
18
+ const index = bareSpecifier.lastIndexOf('@');
19
+ if (index < 1) {
20
+ name = bareSpecifier;
21
+ bareSpecifier = defaultTag;
22
+ }
23
+ else {
24
+ name = bareSpecifier.slice(0, index);
25
+ bareSpecifier = bareSpecifier.slice(index + 1);
26
+ }
16
27
  }
17
28
  }
18
29
  if (name) {
@@ -52,4 +63,75 @@ export function parseJsrSpecifierToRegistryPackageSpec(rawSpecifier, alias, defa
52
63
  jsrPkgName: spec.jsrPkgName,
53
64
  };
54
65
  }
66
+ export const BUILTIN_NAMED_REGISTRIES = Object.freeze({
67
+ gh: 'https://npm.pkg.github.com/',
68
+ });
69
+ // Parses a named-registry specifier of the shape `<alias>:<body>` into a
70
+ // RegistryPackageSpec. Returns `null` when the specifier does not use one of
71
+ // the configured aliases, so the caller can fall through to other resolvers.
72
+ // Supported shapes:
73
+ // - `<alias>:[@<owner>/]<name>[@<version_selector>]`
74
+ // - `<alias>:<version_selector>` paired with a package alias
75
+ export function parseNamedRegistrySpecifierToRegistryPackageSpec(rawSpecifier, knownRegistryNames, packageAlias, defaultTag) {
76
+ const colon = rawSpecifier.indexOf(':');
77
+ if (colon <= 0)
78
+ return null;
79
+ const registryName = rawSpecifier.substring(0, colon);
80
+ if (!knownRegistryNames.has(registryName))
81
+ return null;
82
+ const body = rawSpecifier.substring(colon + 1);
83
+ let pkgName;
84
+ let versionSelector;
85
+ if (semver.validRange(body) != null) {
86
+ // `<alias>:<version_selector>` — fall back to the dependency alias as
87
+ // the package name. Unresolvable without one.
88
+ if (!packageAlias)
89
+ return null;
90
+ pkgName = packageAlias;
91
+ versionSelector = body;
92
+ }
93
+ else if (body[0] === '@') {
94
+ // `<alias>:@<owner>/<name>[@<version_selector>]` — scoped package.
95
+ const index = body.lastIndexOf('@');
96
+ if (index === 0) {
97
+ pkgName = body;
98
+ }
99
+ else {
100
+ pkgName = body.substring(0, index);
101
+ versionSelector = body.substring(index + '@'.length);
102
+ }
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
+ else if (packageAlias?.startsWith('@')) {
108
+ // `<alias>:<tag>` paired with a scoped alias — body is a version
109
+ // selector (tag/dist-tag). Mirrors GitHub Packages, where the package
110
+ // is always scoped and a bare body is a tag.
111
+ pkgName = packageAlias;
112
+ versionSelector = body;
113
+ }
114
+ else {
115
+ // `<alias>:<name>[@<version_selector>]` — unscoped package in body.
116
+ const index = body.lastIndexOf('@');
117
+ if (index < 1) {
118
+ pkgName = body;
119
+ }
120
+ else {
121
+ pkgName = body.substring(0, index);
122
+ versionSelector = body.substring(index + '@'.length);
123
+ }
124
+ if (!pkgName)
125
+ return null;
126
+ }
127
+ const selector = getVersionSelectorType(versionSelector ?? defaultTag);
128
+ if (selector == null)
129
+ return null;
130
+ return {
131
+ fetchSpec: selector.normalized,
132
+ name: pkgName,
133
+ type: selector.type,
134
+ registryName,
135
+ };
136
+ }
55
137
  //# sourceMappingURL=parseBareSpecifier.js.map
@@ -123,15 +123,30 @@ 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 registryName = getRegistryName(opts.registry);
127
+ const pkgMirror = path.join(ctx.cacheDir, metaDir, registryName, `${encodePkgName(spec.name)}.jsonl`);
126
128
  const cachedMeta = ctx.metaCache.get(cacheKey);
127
129
  if (cachedMeta != null) {
130
+ // The in-memory cache may hold abbreviated metadata from an earlier call
131
+ // that didn't need `time` (no publishedBy then). If this call has
132
+ // publishedBy and the package was modified recently, upgrade to full
133
+ // metadata so the maturity check runs properly.
134
+ const upgrade = await maybeUpgradeAbbreviatedMetaForReleaseAge(ctx, spec, opts, cachedMeta);
135
+ let metaForCache = upgrade.meta;
136
+ if (upgrade.upgradedFrom != null) {
137
+ // Persist the upgraded meta to disk too: the on-disk mirror still holds
138
+ // the abbreviated form, so without this a fresh process would re-trigger
139
+ // the upgrade fetch on its next install.
140
+ metaForCache = opts.dryRun
141
+ ? upgrade.meta
142
+ : persistUpgradedMeta(ctx, pkgMirror, upgrade.upgradedFrom);
143
+ ctx.metaCache.set(cacheKey, metaForCache);
144
+ }
128
145
  return {
129
- meta: cachedMeta,
130
- pickedPackage: pickMatchingVersionFinal(pickerOpts, spec, cachedMeta),
146
+ meta: metaForCache,
147
+ pickedPackage: pickMatchingVersionFinal(pickerOpts, spec, metaForCache),
131
148
  };
132
149
  }
133
- const registryName = getRegistryName(opts.registry);
134
- const pkgMirror = path.join(ctx.cacheDir, metaDir, registryName, `${encodePkgName(spec.name)}.jsonl`);
135
150
  return runLimited(pkgMirror, async (limit) => {
136
151
  let metaCachedInStore;
137
152
  if (ctx.offline === true || ctx.preferOffline === true || opts.pickLowestVersion) {
@@ -145,6 +160,17 @@ export async function pickPackage(ctx, spec, opts) {
145
160
  throw new PnpmError('NO_OFFLINE_META', `Failed to resolve ${toRaw(spec)} in package mirror ${pkgMirror}`);
146
161
  }
147
162
  if (metaCachedInStore != null) {
163
+ // Disk-cached meta may be abbreviated; upgrade for the maturity check
164
+ // before letting pickMatchingVersionFinal warn-and-skip on missing time.
165
+ const upgrade = await maybeUpgradeAbbreviatedMetaForReleaseAge(ctx, spec, opts, metaCachedInStore);
166
+ metaCachedInStore = upgrade.meta;
167
+ if (upgrade.upgradedFrom != null) {
168
+ // Persist so the next install skips this upgrade fetch entirely.
169
+ if (!opts.dryRun) {
170
+ metaCachedInStore = persistUpgradedMeta(ctx, pkgMirror, upgrade.upgradedFrom);
171
+ }
172
+ ctx.metaCache.set(cacheKey, metaCachedInStore);
173
+ }
148
174
  const pickedPackage = pickMatchingVersionFinal(pickerOpts, spec, metaCachedInStore);
149
175
  if (pickedPackage) {
150
176
  return {
@@ -169,7 +195,7 @@ export async function pickPackage(ctx, spec, opts) {
169
195
  }
170
196
  }
171
197
  catch (err) {
172
- if (ctx.strictPublishedByCheck) {
198
+ if (shouldRethrowFromFastPathCache(err, ctx.strictPublishedByCheck)) {
173
199
  throw err;
174
200
  }
175
201
  }
@@ -190,10 +216,7 @@ export async function pickPackage(ctx, spec, opts) {
190
216
  }
191
217
  }
192
218
  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))) {
219
+ if (shouldRethrowFromFastPathCache(err, ctx.strictPublishedByCheck)) {
197
220
  throw err;
198
221
  }
199
222
  }
@@ -219,6 +242,19 @@ export async function pickPackage(ctx, spec, opts) {
219
242
  if (fetchResult.notModified) {
220
243
  metaCachedInStore = metaCachedInStore ?? await limit(async () => loadMeta(pkgMirror));
221
244
  if (metaCachedInStore != null) {
245
+ // The cached metadata may be abbreviated (no per-version `time`).
246
+ // When minimumReleaseAge is active we need `time` for the maturity check,
247
+ // so upgrade to full metadata via a follow-up fetch when warranted.
248
+ // Without this, repeat installs of recently-modified packages would
249
+ // silently bypass the maturity check via the warn-and-skip fallback.
250
+ const upgrade = await maybeUpgradeAbbreviatedMetaForReleaseAge(ctx, spec, opts, metaCachedInStore);
251
+ metaCachedInStore = upgrade.meta;
252
+ if (upgrade.upgradedFrom != null && !opts.dryRun) {
253
+ // Persist the upgraded full metadata to disk so subsequent installs
254
+ // skip this upgrade fetch entirely (the cached meta will then have
255
+ // `time` populated, so the upgrade condition won't trigger).
256
+ metaCachedInStore = persistUpgradedMeta(ctx, pkgMirror, upgrade.upgradedFrom);
257
+ }
222
258
  ctx.metaCache.set(cacheKey, metaCachedInStore);
223
259
  return {
224
260
  meta: metaCachedInStore,
@@ -307,6 +343,77 @@ export async function pickPackage(ctx, spec, opts) {
307
343
  }
308
344
  });
309
345
  }
346
+ // When `minimumReleaseAge` is active and we have abbreviated metadata (which
347
+ // the npm registry serves by default and which omits per-version `time`),
348
+ // the maturity check can't run on the data we have. If the package has been
349
+ // modified since the maturity cutoff, re-fetch with `fullMetadata: true` so
350
+ // `time` is populated and the check can proceed properly. Without this,
351
+ // `pickMatchingVersionFinal` would fall back to its warn-and-skip path,
352
+ // silently bypassing the minimumReleaseAge guarantee for affected packages.
353
+ //
354
+ // Returns the original meta when no upgrade is needed. When an upgrade
355
+ // happens, returns both the upgraded meta and the underlying fetch result
356
+ // so callers can persist it to disk and avoid re-fetching on next install.
357
+ async function maybeUpgradeAbbreviatedMetaForReleaseAge(ctx, spec, opts, meta) {
358
+ if (ctx.offline === true ||
359
+ !opts.publishedBy ||
360
+ meta.time != null ||
361
+ opts.publishedByExclude?.(spec.name) === true) {
362
+ return { meta };
363
+ }
364
+ const modifiedDate = meta.modified ? new Date(meta.modified) : null;
365
+ const isModifiedValid = modifiedDate != null && !Number.isNaN(modifiedDate.getTime());
366
+ if (isModifiedValid && modifiedDate < opts.publishedBy) {
367
+ // The package was last modified before the maturity cutoff. No individual
368
+ // version can be newer than the cutoff, so the abbreviated form is fine.
369
+ return { meta };
370
+ }
371
+ // When `modified` is missing or malformed we fall through to the upgrade
372
+ // fetch: prefer correctness (run the maturity check on real `time` data)
373
+ // over saving a network call when our cached freshness signal is unusable.
374
+ // Forward etag/modified so the registry can answer 304 if the upgraded
375
+ // representation hasn't actually changed (rare on the npm registry where
376
+ // full and abbreviated have distinct etags, but cheap to support).
377
+ const fullFetchResult = await ctx.fetch(spec.name, {
378
+ authHeaderValue: opts.authHeaderValue,
379
+ fullMetadata: true,
380
+ etag: meta.etag,
381
+ modified: meta.modified,
382
+ registry: opts.registry,
383
+ });
384
+ if (fullFetchResult.notModified) {
385
+ // Upgrade fetch came back 304: keep the abbreviated meta. The downstream
386
+ // `pickMatchingVersionFinal` will fall through to its warn-and-skip path.
387
+ return { meta };
388
+ }
389
+ return { meta: fullFetchResult.meta, upgradedFrom: fullFetchResult };
390
+ }
391
+ // Returns true when a fast-path cache catch should rethrow under
392
+ // strictPublishedByCheck. ERR_PNPM_MISSING_TIME is excluded so callers fall
393
+ // through to the network fetch path, which can upgrade abbreviated cached
394
+ // metadata to full and run the maturity check on real `time` data.
395
+ function shouldRethrowFromFastPathCache(err, strictPublishedByCheck) {
396
+ return strictPublishedByCheck === true && !isMissingTimeError(err);
397
+ }
398
+ // Persists upgraded full metadata to the on-disk cache mirror and returns
399
+ // the meta to store in the in-memory cache. When `filterMetadata` is on, the
400
+ // in-memory and on-disk forms are both stripped via `clearMeta`; otherwise
401
+ // the original raw response body is written and the unstripped meta is kept.
402
+ function persistUpgradedMeta(ctx, pkgMirror, upgradedFrom) {
403
+ const metaForCache = ctx.filterMetadata ? clearMeta(upgradedFrom.meta) : upgradedFrom.meta;
404
+ const jsonForDisk = ctx.filterMetadata
405
+ ? prepareJsonForDisk(metaForCache, upgradedFrom.etag)
406
+ : prepareJsonForDisk(upgradedFrom.meta, upgradedFrom.etag, upgradedFrom.jsonText);
407
+ runLimited(pkgMirror, (l) => l(async () => {
408
+ try {
409
+ await saveMeta(pkgMirror, jsonForDisk);
410
+ }
411
+ catch (err) { // eslint-disable-line
412
+ // We don't care if this file was not written to the cache
413
+ }
414
+ }));
415
+ return metaForCache;
416
+ }
310
417
  function clearMeta(pkg) {
311
418
  const versions = {};
312
419
  for (const [version, info] of Object.entries(pkg.versions)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/resolving.npm-resolver",
3
- "version": "1101.0.3",
3
+ "version": "1101.1.1",
4
4
  "description": "Resolver for npm-hosted packages",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -28,38 +28,38 @@
28
28
  "dependencies": {
29
29
  "@zkochan/retry": "^0.2.0",
30
30
  "encode-registry": "^3.0.1",
31
- "lru-cache": "^11.3.6",
31
+ "lru-cache": "^11.2.7",
32
32
  "normalize-path": "^3.0.0",
33
- "p-limit": "^7.3.0",
33
+ "p-limit": "^7.1.0",
34
34
  "p-memoize": "8.0.0",
35
35
  "parse-npm-tarball-url": "^5.0.0",
36
36
  "path-temp": "^3.0.0",
37
37
  "ramda": "npm:@pnpm/ramda@0.28.1",
38
38
  "rename-overwrite": "^7.0.1",
39
- "semver": "^7.7.4",
39
+ "semver": "^7.7.2",
40
40
  "semver-utils": "^1.1.4",
41
41
  "ssri": "13.0.1",
42
42
  "version-selector-type": "^3.0.0",
43
- "@pnpm/core-loggers": "1100.0.1",
44
- "@pnpm/error": "1100.0.0",
45
- "@pnpm/fetching.types": "1100.0.1",
43
+ "@pnpm/core-loggers": "1100.0.2",
44
+ "@pnpm/constants": "1100.0.0",
46
45
  "@pnpm/crypto.hash": "1100.0.1",
47
46
  "@pnpm/fs.graceful-fs": "1100.1.0",
48
- "@pnpm/resolving.registry.types": "1100.0.2",
49
- "@pnpm/resolving.jsr-specifier-parser": "1100.0.0",
50
- "@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.2",
51
- "@pnpm/resolving.resolver-base": "1100.1.2",
47
+ "@pnpm/error": "1100.0.0",
48
+ "@pnpm/fetching.types": "1100.0.1",
49
+ "@pnpm/resolving.registry.types": "1100.0.3",
50
+ "@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.3",
51
+ "@pnpm/resolving.resolver-base": "1100.1.3",
52
+ "@pnpm/types": "1101.1.0",
52
53
  "@pnpm/store.index": "1100.1.0",
53
- "@pnpm/store.cafs": "1100.1.2",
54
- "@pnpm/types": "1101.0.0",
54
+ "@pnpm/store.cafs": "1100.1.4",
55
55
  "@pnpm/workspace.spec-parser": "1100.0.0",
56
56
  "@pnpm/workspace.range-resolver": "1100.0.1",
57
- "@pnpm/config.pick-registry-for-package": "1100.0.2",
58
- "@pnpm/constants": "1100.0.0"
57
+ "@pnpm/resolving.jsr-specifier-parser": "1100.0.0",
58
+ "@pnpm/config.pick-registry-for-package": "1100.0.3"
59
59
  },
60
60
  "peerDependencies": {
61
- "@pnpm/logger": "^1001.0.1",
62
- "@pnpm/worker": "^1100.1.3"
61
+ "@pnpm/logger": ">=1001.0.0 <1002.0.0",
62
+ "@pnpm/worker": "^1100.1.5"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@jest/globals": "30.3.0",
@@ -69,12 +69,12 @@
69
69
  "@types/ssri": "^7.1.5",
70
70
  "load-json-file": "^7.0.1",
71
71
  "tempy": "3.0.0",
72
- "@pnpm/config.version-policy": "1100.0.2",
72
+ "@pnpm/config.version-policy": "1100.0.3",
73
+ "@pnpm/network.fetch": "1100.0.4",
74
+ "@pnpm/resolving.npm-resolver": "1101.1.1",
73
75
  "@pnpm/logger": "1100.0.0",
74
- "@pnpm/network.fetch": "1100.0.2",
75
- "@pnpm/resolving.npm-resolver": "1101.0.3",
76
76
  "@pnpm/test-fixtures": "1100.0.0",
77
- "@pnpm/testing.mock-agent": "1100.0.2"
77
+ "@pnpm/testing.mock-agent": "1100.0.4"
78
78
  },
79
79
  "engines": {
80
80
  "node": ">=22.13"