@pnpm/resolving.npm-resolver 1104.0.0 → 1104.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/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # @pnpm/npm-resolver
2
2
 
3
+ ## 1104.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Added explicit registry revision selection with `<version>+rN` and `pnpm update --patches` for refreshing revision artifacts without changing package versions. Registry-backed lockfile policy checks recognize historical revisions, and pnpr now preserves safe revision histories from upstream registries.
8
+
9
+ - Added support for registry replacement tarballs using standard integrity values, explicit revision fields, registry routing from the `registries` setting, non-redirecting integrity-addressed URLs, canonical safe-integer revision numbers, and pnpr proxying for immutable upstream revision artifacts.
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies:
14
+ - @pnpm/config.normalize-registries@1101.0.1
15
+ - @pnpm/config.pick-registry-for-package@1101.0.1
16
+ - @pnpm/config.version-policy@1100.2.2
17
+ - @pnpm/core-loggers@1100.3.4
18
+ - @pnpm/crypto.hash@1100.0.3
19
+ - @pnpm/deps.path@1101.0.1
20
+ - @pnpm/fs.graceful-fs@1100.2.0
21
+ - @pnpm/pkg-manifest.utils@1100.4.2
22
+ - @pnpm/resolving.registry.pkg-metadata-filter@1100.0.18
23
+ - @pnpm/resolving.registry.types@1100.2.0
24
+ - @pnpm/resolving.resolver-base@1101.2.0
25
+ - @pnpm/resolving.tarball-url@1101.1.0
26
+ - @pnpm/store.cafs@1100.3.0
27
+ - @pnpm/store.index@1100.3.0
28
+ - @pnpm/types@1102.1.0
29
+
3
30
  ## 1104.0.0
4
31
 
5
32
  ### Major Changes
@@ -5,13 +5,14 @@ import { createPackageVersionPolicy } from '@pnpm/config.version-policy';
5
5
  import { FULL_META_DIR } from '@pnpm/constants';
6
6
  import { PnpmError } from '@pnpm/error';
7
7
  import { isGitHostedTarballUrl, } from '@pnpm/resolving.resolver-base';
8
+ import { isIntegrityAddressedRegistryTarballUrl, isValidTarballRevision, } from '@pnpm/resolving.tarball-url';
8
9
  import semver from 'semver';
9
10
  import { fetchAttestationPublishedAt } from './fetchAttestationPublishedAt.js';
10
11
  import { fetchAbbreviatedMetadataCached, fetchFullMetadataCached, } from './fetchFullMetadataCached.js';
11
12
  import { normalizeRegistryUrl } from './normalizeRegistryUrl.js';
12
13
  import { getPkgMetaCacheKey, getPkgMirrorPath, loadMeta, warnMissingTimeFieldOnce } from './pickPackage.js';
13
14
  import { failIfTrustDowngraded } from './trustChecks.js';
14
- import { MINIMUM_RELEASE_AGE_VIOLATION_CODE, MISSING_NAMED_REGISTRY_VIOLATION_CODE, MISSING_TARBALL_INTEGRITY_VIOLATION_CODE, TARBALL_URL_MISMATCH_VIOLATION_CODE, TRUST_DOWNGRADE_VIOLATION_CODE, } from './violationCodes.js';
15
+ import { MINIMUM_RELEASE_AGE_VIOLATION_CODE, MISSING_NAMED_REGISTRY_VIOLATION_CODE, MISSING_TARBALL_INTEGRITY_VIOLATION_CODE, TARBALL_REVISION_MISMATCH_VIOLATION_CODE, TARBALL_URL_MISMATCH_VIOLATION_CODE, TRUST_DOWNGRADE_VIOLATION_CODE, } from './violationCodes.js';
15
16
  /**
16
17
  * Returns a `ResolutionVerifier` for npm-registry-resolved lockfile
17
18
  * entries. It always binds each entry's recorded tarball URL to the
@@ -113,7 +114,7 @@ export function createNpmResolutionVerifier(opts) {
113
114
  return {
114
115
  ok: false,
115
116
  code: MISSING_NAMED_REGISTRY_VIOLATION_CODE,
116
- reason: `was resolved from the named registry '${registryName}:', which is not present in the registriesByPrefix setting`,
117
+ reason: `has registry prefix '${registryName}:', which is not declared by the registries setting`,
117
118
  };
118
119
  }
119
120
  registry = namedRegistry;
@@ -129,13 +130,14 @@ export function createNpmResolutionVerifier(opts) {
129
130
  // narrowed by their exclude lists, since it guards integrity rather
130
131
  // than maturity/trust. Registry entries with no tarball URL reconstruct
131
132
  // it from name+version+registry, so they're inherently bound.
132
- if (typeof tarballUrl === 'string') {
133
- const urlViolation = await runTarballUrlCheck(lookupContext, registry, name, version, tarballUrl);
134
- if (urlViolation)
135
- return urlViolation;
136
- }
133
+ const rawRevision = resolution.revision;
137
134
  const ageApplies = ageCheckActive && !isExcluded(excludePolicy, name, version);
138
135
  const trustApplies = trustCheckActive && !isExcluded(trustExcludePolicy, name, version);
136
+ if (tarballUrl != null || (rawRevision != null && (ageApplies || trustApplies))) {
137
+ const artifactViolation = await runRegistryArtifactCheck(lookupContext, registry, name, version, integrity, rawRevision, tarballUrl);
138
+ if (artifactViolation)
139
+ return artifactViolation;
140
+ }
139
141
  if (!ageApplies && !trustApplies)
140
142
  return { ok: true };
141
143
  if (ageApplies) {
@@ -178,6 +180,7 @@ export function createNpmResolutionVerifier(opts) {
178
180
  // applies the binding — otherwise an upgrade could keep trusting a
179
181
  // lockfile that was only ever age/trust-checked.
180
182
  tarballUrlBinding: true,
183
+ revisionHistoryBinding: true,
181
184
  // Same cache identity rule for the missing-integrity structural check.
182
185
  integrityRequired: true,
183
186
  namedRegistriesRouting,
@@ -193,6 +196,8 @@ export function createNpmResolutionVerifier(opts) {
193
196
  // didn't record it can't be trusted to have enforced it.
194
197
  if (cached.tarballUrlBinding !== true)
195
198
  return false;
199
+ if (cached.revisionHistoryBinding !== true)
200
+ return false;
196
201
  // The missing-integrity check is also unconditional; older cache records
197
202
  // without the flag cannot prove they rejected unverifiable tarballs.
198
203
  if (cached.integrityRequired !== true)
@@ -309,7 +314,7 @@ async function runAgeCheck(context, registry, name, version, cutoff, ignoreMissi
309
314
  * a tampered lockfile could smuggle a malicious URL past the check by
310
315
  * pointing it at a `name@version` the registry can't vouch for.
311
316
  */
312
- async function runTarballUrlCheck(context, registry, name, version, lockfileTarball) {
317
+ async function runRegistryArtifactCheck(context, registry, name, version, lockfileIntegrity, rawRevision, lockfileTarball) {
313
318
  const { meta, error } = await fetchAbbreviatedMeta(context, registry, name);
314
319
  if (error != null) {
315
320
  // Couldn't reach the registry to verify (auth/network/5xx). Propagate the
@@ -319,17 +324,107 @@ async function runTarballUrlCheck(context, registry, name, version, lockfileTarb
319
324
  // error — still fail-closed, the entry never reaches the filesystem.
320
325
  throw error;
321
326
  }
322
- const registryTarball = meta?.versionTarballs?.get(version);
323
- if (registryTarball != null && sameTarballUrl(lockfileTarball, registryTarball)) {
324
- return undefined;
327
+ const artifact = meta?.versionArtifacts?.get(version);
328
+ if (artifact == null) {
329
+ if (lockfileTarball == null && rawRevision == null)
330
+ return undefined;
331
+ return {
332
+ ok: false,
333
+ code: lockfileTarball == null
334
+ ? TARBALL_REVISION_MISMATCH_VIOLATION_CODE
335
+ : TARBALL_URL_MISMATCH_VIOLATION_CODE,
336
+ reason: "could not be verified against the registry's published metadata",
337
+ };
325
338
  }
326
- return {
327
- ok: false,
328
- code: TARBALL_URL_MISMATCH_VIOLATION_CODE,
329
- reason: registryTarball == null
330
- ? "could not be verified against the registry's published metadata"
331
- : `has a tarball URL (${lockfileTarball}) that does not match the registry's published metadata (${registryTarball})`,
332
- };
339
+ const metadataRevision = artifact.current.revision;
340
+ const revisionAware = rawRevision != null || metadataRevision !== undefined || artifact.revisions.length > 0;
341
+ if (!revisionAware) {
342
+ if (lockfileTarball == null)
343
+ return undefined;
344
+ if (typeof artifact.current.tarball === 'string' && sameTarballUrl(lockfileTarball, artifact.current.tarball)) {
345
+ return undefined;
346
+ }
347
+ return {
348
+ ok: false,
349
+ code: TARBALL_URL_MISMATCH_VIOLATION_CODE,
350
+ reason: typeof artifact.current.tarball !== 'string'
351
+ ? "could not be verified against the registry's published metadata"
352
+ : `has a tarball URL (${lockfileTarball}) that does not match the registry's published metadata (${artifact.current.tarball})`,
353
+ };
354
+ }
355
+ if (rawRevision != null && !isValidTarballRevision(rawRevision)) {
356
+ return {
357
+ ok: false,
358
+ code: TARBALL_REVISION_MISMATCH_VIOLATION_CODE,
359
+ reason: `has an invalid revision (${String(rawRevision)})`,
360
+ };
361
+ }
362
+ let currentRevision = 0;
363
+ if (metadataRevision !== undefined) {
364
+ if (!isValidTarballRevision(metadataRevision)) {
365
+ return {
366
+ ok: false,
367
+ code: TARBALL_REVISION_MISMATCH_VIOLATION_CODE,
368
+ reason: `registry metadata has an invalid current revision (${String(metadataRevision)})`,
369
+ };
370
+ }
371
+ currentRevision = metadataRevision;
372
+ const currentHistory = artifact.revisions.filter(candidate => candidate.revision === currentRevision);
373
+ if (currentHistory.length !== 1 ||
374
+ currentHistory[0].integrity !== artifact.current.integrity ||
375
+ typeof currentHistory[0].tarball !== 'string' ||
376
+ typeof artifact.current.tarball !== 'string' ||
377
+ typeof artifact.current.integrity !== 'string' ||
378
+ !isIntegrityAddressedRegistryTarballUrl(normalizeRegistryUrl(artifact.current.tarball), artifact.current.integrity, registry) ||
379
+ !sameTarballUrl(currentHistory[0].tarball, artifact.current.tarball)) {
380
+ return {
381
+ ok: false,
382
+ code: TARBALL_REVISION_MISMATCH_VIOLATION_CODE,
383
+ reason: `registry metadata revision ${currentRevision} does not have exactly one matching history entry`,
384
+ };
385
+ }
386
+ }
387
+ const revision = typeof rawRevision === 'number' ? rawRevision : 0;
388
+ const currentMatches = currentRevision === revision;
389
+ const historicalCandidates = artifact.revisions.filter(candidate => candidate.revision === revision);
390
+ if (historicalCandidates.length > 1) {
391
+ return {
392
+ ok: false,
393
+ code: TARBALL_REVISION_MISMATCH_VIOLATION_CODE,
394
+ reason: `revision ${revision} is advertised more than once in the registry's history`,
395
+ };
396
+ }
397
+ const historical = historicalCandidates[0];
398
+ const selected = currentMatches ? artifact.current : historical;
399
+ if (selected == null ||
400
+ selected.integrity !== lockfileIntegrity ||
401
+ (currentMatches && historical != null && historical.integrity !== lockfileIntegrity)) {
402
+ return {
403
+ ok: false,
404
+ code: TARBALL_REVISION_MISMATCH_VIOLATION_CODE,
405
+ reason: `has revision ${revision} with an integrity that does not match the registry's current or historical metadata`,
406
+ };
407
+ }
408
+ if (revision > 0 || !currentMatches) {
409
+ if (typeof selected.tarball !== 'string' ||
410
+ !isIntegrityAddressedRegistryTarballUrl(normalizeRegistryUrl(selected.tarball), lockfileIntegrity, registry)) {
411
+ return {
412
+ ok: false,
413
+ code: TARBALL_REVISION_MISMATCH_VIOLATION_CODE,
414
+ reason: `has revision ${revision} that is not addressed by its complete sha512 integrity`,
415
+ };
416
+ }
417
+ }
418
+ if (lockfileTarball != null && (typeof selected.tarball !== 'string' || !sameTarballUrl(lockfileTarball, selected.tarball))) {
419
+ return {
420
+ ok: false,
421
+ code: TARBALL_URL_MISMATCH_VIOLATION_CODE,
422
+ reason: typeof selected.tarball !== 'string'
423
+ ? "could not be verified against the registry's published metadata"
424
+ : `has a tarball URL (${lockfileTarball}) that does not match the registry's published metadata (${selected.tarball})`,
425
+ };
426
+ }
427
+ return undefined;
333
428
  }
334
429
  function sameTarballUrl(a, b) {
335
430
  return canonicalTarballUrl(a) === canonicalTarballUrl(b);
@@ -549,7 +644,7 @@ async function tryAbbreviatedModifiedShortcut(context, registry, name, version)
549
644
  // publish time — but only for versions the registry currently lists.
550
645
  // An unpublished or never-published pin would otherwise pass the gate
551
646
  // on a stale package-level timestamp.
552
- if (!meta?.versionTarballs?.has(version))
647
+ if (!meta?.versionArtifacts?.has(version))
553
648
  return undefined;
554
649
  return modified;
555
650
  }
@@ -633,16 +728,29 @@ function validateSharedMeta(meta, name) {
633
728
  // lockfile (see #11860). The full document is GC-able as soon as this
634
729
  // closure returns; only the short tarball-URL strings are retained.
635
730
  function projectAbbreviatedMeta(meta) {
636
- let versionTarballs;
731
+ let versionArtifacts;
637
732
  if (meta.versions) {
638
- versionTarballs = new Map();
733
+ versionArtifacts = new Map();
639
734
  for (const [version, manifest] of Object.entries(meta.versions)) {
640
- versionTarballs.set(version, manifest.dist?.tarball);
735
+ versionArtifacts.set(version, {
736
+ current: {
737
+ revision: manifest.dist?.revision,
738
+ integrity: manifest.dist?.integrity,
739
+ tarball: manifest.dist?.tarball,
740
+ },
741
+ revisions: Array.isArray(manifest.dist?.revisions)
742
+ ? manifest.dist.revisions.map(revision => ({
743
+ revision: revision.revision,
744
+ integrity: revision.integrity,
745
+ tarball: revision.tarball,
746
+ }))
747
+ : [],
748
+ });
641
749
  }
642
750
  }
643
751
  return {
644
752
  modified: meta.modified,
645
- versionTarballs,
753
+ versionArtifacts,
646
754
  };
647
755
  }
648
756
  function readLocalMetaTime(context, registry, name) {
package/lib/index.d.ts CHANGED
@@ -123,6 +123,7 @@ export type ResolveFromNpmOptions = {
123
123
  preferredVersions?: PreferredVersions;
124
124
  preferWorkspacePackages?: boolean;
125
125
  update?: false | 'compatible' | 'latest';
126
+ updatePatches?: boolean;
126
127
  updateRequested?: boolean;
127
128
  updateChecksums?: boolean;
128
129
  injectWorkspacePackages?: boolean;
package/lib/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import path from 'node:path';
2
2
  import { pickRegistryForPackage } from '@pnpm/config.pick-registry-for-package';
3
3
  import { isWellFormedRegistryName, RESERVED_VERSION_PREFIXES } from '@pnpm/deps.path';
4
- import { PnpmError } from '@pnpm/error';
4
+ import { PnpmError, redactUrlForDisplay } from '@pnpm/error';
5
5
  import { globalWarn } from '@pnpm/logger';
6
6
  import { calcVersionRange, inferRangeSpecStyle, rangeSpecGranularity, versionWithRangeSpecStyle } from '@pnpm/pkg-manifest.utils';
7
7
  import { EXISTING_VERSION_SELECTOR_WEIGHT, } from '@pnpm/resolving.resolver-base';
8
+ import { isIntegrityAddressedRegistryTarballUrl, isValidTarballRevision, } from '@pnpm/resolving.tarball-url';
8
9
  import { storeIndexKey } from '@pnpm/store.index';
9
10
  import { readPkgFromCafs, } from '@pnpm/worker';
10
11
  import { resolveWorkspaceRange } from '@pnpm/workspace.range-resolver';
@@ -332,12 +333,15 @@ async function resolveNpm(ctx, wantedDependency, opts) {
332
333
  return resolvedFromWorkspace;
333
334
  }
334
335
  }
335
- const workspacePackages = opts.alwaysTryWorkspacePackages !== false ? opts.workspacePackages : undefined;
336
+ const canKeepWorkspaceResolution = opts.currentPkg == null || opts.currentPkg.resolution.type === 'directory';
336
337
  const spec = wantedDependency.bareSpecifier
337
338
  ? parseBareSpecifier(wantedDependency.bareSpecifier, wantedDependency.alias, defaultTag, registry)
338
339
  : defaultTagForAlias(wantedDependency.alias, defaultTag);
339
340
  if (spec == null)
340
341
  return null;
342
+ const workspacePackages = spec.revision == null && (!opts.updatePatches || canKeepWorkspaceResolution) && opts.alwaysTryWorkspacePackages !== false
343
+ ? opts.workspacePackages
344
+ : undefined;
341
345
  // Fast path: if we have a current resolution with integrity, try to peek the manifest from the store.
342
346
  // This avoids the expensive metadata fetch from the registry.
343
347
  // We do this AFTER ensuring the spec is valid for this resolver to avoids hijacking other resolvers.
@@ -347,10 +351,12 @@ async function resolveNpm(ctx, wantedDependency, opts) {
347
351
  if (ctx.peekManifestFromStore &&
348
352
  opts.currentPkg?.resolution &&
349
353
  !opts.update &&
354
+ !opts.updatePatches &&
355
+ spec.revision == null &&
350
356
  (opts.publishedBy == null || opts.currentPkg.publishedAt != null)) {
351
357
  const currentResolution = opts.currentPkg.resolution;
352
358
  // Only use this optimization for tarball resolutions with integrity (npm packages)
353
- if ('tarball' in currentResolution && currentResolution.integrity) {
359
+ if ('tarball' in currentResolution && typeof currentResolution.integrity === 'string') {
354
360
  const manifest = await ctx.peekManifestFromStore({
355
361
  id: opts.currentPkg.id,
356
362
  integrity: currentResolution.integrity,
@@ -398,7 +404,7 @@ async function resolveNpm(ctx, wantedDependency, opts) {
398
404
  !opts.updateChecksums &&
399
405
  opts.injectWorkspacePackages !== true &&
400
406
  !wantedDependency.injected) {
401
- const workspacePkgsMatchingName = workspacePackages.get(spec.name);
407
+ const workspacePkgsMatchingName = spec.revision == null ? workspacePackages.get(spec.name) : undefined;
402
408
  if (workspacePkgsMatchingName?.size === 1) {
403
409
  const localVersion = pickMatchingLocalVersionOrNull(workspacePkgsMatchingName, spec);
404
410
  if (localVersion != null) {
@@ -426,7 +432,7 @@ async function resolveNpm(ctx, wantedDependency, opts) {
426
432
  preferredVersionSelectors: preferredVersionSelectorsFor(opts, spec.name),
427
433
  registry,
428
434
  includeLatestTag: opts.update === 'latest',
429
- updateChecksums: opts.updateChecksums,
435
+ updateChecksums: opts.updateChecksums || opts.updatePatches,
430
436
  optional: wantedDependency.optional,
431
437
  trustPolicy: opts.trustPolicy,
432
438
  });
@@ -491,7 +497,7 @@ async function resolveNpm(ctx, wantedDependency, opts) {
491
497
  });
492
498
  }
493
499
  const latest = latestAllowedByPolicy(meta, opts);
494
- const workspacePkgsMatchingName = workspacePackages?.get(pickedPackage.name);
500
+ const workspacePkgsMatchingName = spec.revision == null ? workspacePackages?.get(pickedPackage.name) : undefined;
495
501
  if (workspacePkgsMatchingName && opts.projectDir) {
496
502
  const matchedPkg = workspacePkgsMatchingName.get(pickedPackage.version);
497
503
  if (matchedPkg) {
@@ -525,11 +531,9 @@ async function resolveNpm(ctx, wantedDependency, opts) {
525
531
  }
526
532
  }
527
533
  warnOnceOnHeldBackUpdate(ctx, opts, spec, meta, pickedPackage.version);
534
+ const selectedPackage = selectPackageRevision(pickedPackage, spec, registry);
528
535
  const id = `${pickedPackage.name}@${pickedPackage.version}`;
529
- const resolution = {
530
- integrity: getIntegrity(pickedPackage.dist),
531
- tarball: normalizeRegistryUrl(pickedPackage.dist.tarball),
532
- };
536
+ const resolution = createRegistryTarballResolution(selectedPackage.dist, registry);
533
537
  let normalizedBareSpecifier;
534
538
  if (opts.calcSpecifier) {
535
539
  normalizedBareSpecifier = spec.normalizedBareSpecifier ?? calcSpecifier({
@@ -543,7 +547,7 @@ async function resolveNpm(ctx, wantedDependency, opts) {
543
547
  return {
544
548
  id,
545
549
  latest,
546
- manifest: pickedPackage,
550
+ manifest: selectedPackage,
547
551
  resolution,
548
552
  resolvedVia: 'npm-registry',
549
553
  publishedAt,
@@ -568,7 +572,14 @@ async function resolveJsr(ctx, wantedDependency, opts) {
568
572
  return {
569
573
  ...picked,
570
574
  normalizedBareSpecifier: opts.calcSpecifier
571
- ? calcPrefixedSpecifier('jsr:', spec.jsrPkgName, wantedDependency, picked.manifest.version, opts.rangeSpecStyle)
575
+ ? calcPrefixedSpecifier({
576
+ prefix: 'jsr:',
577
+ pkgName: spec.jsrPkgName,
578
+ wantedDependency,
579
+ version: picked.manifest.version,
580
+ revision: spec.revision,
581
+ defaultRangeSpecStyle: opts.rangeSpecStyle,
582
+ })
572
583
  : undefined,
573
584
  resolvedVia: 'jsr-registry',
574
585
  alias: spec.jsrPkgName,
@@ -590,7 +601,7 @@ function mergeNamedRegistries(userDefined) {
590
601
  if (RESERVED_VERSION_PREFIXES.has(alias) || !isWellFormedRegistryName(alias)) {
591
602
  throw new PnpmError('RESERVED_NAMED_REGISTRY_NAME', RESERVED_VERSION_PREFIXES.has(alias)
592
603
  ? `'${alias}' cannot be used as a named registry alias: it is a reserved dependency specifier prefix.`
593
- : `'${alias}' cannot be used as a named registry alias: aliases must start with a letter and contain only letters, digits, ".", "_", and "-".`, { hint: 'Rename the entry in the registriesByPrefix setting.' });
604
+ : `'${alias}' cannot be used as a named registry alias: aliases must start with a letter and contain only letters, digits, ".", "_", and "-".`, { hint: 'Change the prefix on the corresponding registries entry.' });
594
605
  }
595
606
  if (typeof url !== 'string' || !isValidHttpUrl(url)) {
596
607
  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/' });
@@ -633,7 +644,14 @@ async function resolveFromNamedRegistry(ctx, wantedDependency, opts) {
633
644
  // decides the tarball both consumers get.
634
645
  id: `${picked.manifest.name}@${spec.registryName}:${picked.manifest.version}`,
635
646
  normalizedBareSpecifier: opts.calcSpecifier
636
- ? calcPrefixedSpecifier(`${spec.registryName}:`, spec.name, wantedDependency, picked.manifest.version, opts.rangeSpecStyle)
647
+ ? calcPrefixedSpecifier({
648
+ prefix: `${spec.registryName}:`,
649
+ pkgName: spec.name,
650
+ wantedDependency,
651
+ version: picked.manifest.version,
652
+ revision: spec.revision,
653
+ defaultRangeSpecStyle: opts.rangeSpecStyle,
654
+ })
637
655
  : undefined,
638
656
  resolvedVia: 'named-registry',
639
657
  registryName: spec.registryName,
@@ -657,7 +675,7 @@ async function pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, registr
657
675
  preferredVersionSelectors: preferredVersionSelectorsFor(opts, spec.name),
658
676
  registry,
659
677
  includeLatestTag: opts.update === 'latest',
660
- updateChecksums: opts.updateChecksums,
678
+ updateChecksums: opts.updateChecksums || opts.updatePatches,
661
679
  optional: wantedDependency.optional,
662
680
  trustPolicy: opts.trustPolicy,
663
681
  });
@@ -665,15 +683,13 @@ async function pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, registr
665
683
  throw new NoMatchingVersionError({ wantedDependency, packageMeta: meta, registry });
666
684
  }
667
685
  warnOnceOnHeldBackUpdate(ctx, opts, spec, meta, pickedPackage.version);
668
- const resolution = {
669
- integrity: getIntegrity(pickedPackage.dist),
670
- tarball: normalizeRegistryUrl(pickedPackage.dist.tarball),
671
- };
686
+ const selectedPackage = selectPackageRevision(pickedPackage, spec, registry);
687
+ const resolution = createRegistryTarballResolution(selectedPackage.dist, registry);
672
688
  const publishedAt = meta.time?.[pickedPackage.version];
673
689
  return {
674
690
  id: `${pickedPackage.name}@${pickedPackage.version}`,
675
691
  latest: latestAllowedByPolicy(meta, opts),
676
- manifest: pickedPackage,
692
+ manifest: selectedPackage,
677
693
  resolution,
678
694
  publishedAt,
679
695
  policyViolation: detectMinReleaseAgeViolation({
@@ -690,13 +706,25 @@ async function pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, registr
690
706
  // when the dependency alias matches the package name). Shared between the
691
707
  // jsr and named-registry resolvers since they only differ in `prefix` and
692
708
  // which spec field holds the package name.
693
- function calcPrefixedSpecifier(prefix, pkgName, wantedDependency, version, defaultRangeSpecStyle) {
694
- const range = calcRange(version, wantedDependency, defaultRangeSpecStyle);
695
- if (!wantedDependency.alias || pkgName === wantedDependency.alias)
696
- return `${prefix}${range}`;
697
- return `${prefix}${pkgName}@${range}`;
709
+ function calcPrefixedSpecifier(opts) {
710
+ if (opts.revision != null) {
711
+ const target = `${opts.version}+r${opts.revision}`;
712
+ if (!opts.wantedDependency.alias || opts.pkgName === opts.wantedDependency.alias)
713
+ return `${opts.prefix}${target}`;
714
+ return `${opts.prefix}${opts.pkgName}@${target}`;
715
+ }
716
+ const range = calcRange(opts.version, opts.wantedDependency, opts.defaultRangeSpecStyle);
717
+ if (!opts.wantedDependency.alias || opts.pkgName === opts.wantedDependency.alias)
718
+ return `${opts.prefix}${range}`;
719
+ return `${opts.prefix}${opts.pkgName}@${range}`;
698
720
  }
699
721
  function calcSpecifier({ wantedDependency, spec, version, defaultRangeSpecStyle, }) {
722
+ if (spec.revision != null) {
723
+ const target = `${version}+r${spec.revision}`;
724
+ if (!wantedDependency.alias || spec.name === wantedDependency.alias)
725
+ return target;
726
+ return `npm:${spec.name}@${target}`;
727
+ }
700
728
  if (wantedDependency.prevSpecifier === wantedDependency.bareSpecifier && wantedDependency.prevSpecifier && versionSelectorType(wantedDependency.prevSpecifier)?.type === 'tag') {
701
729
  return wantedDependency.prevSpecifier;
702
730
  }
@@ -916,6 +944,123 @@ function getIntegrity(dist) {
916
944
  }
917
945
  return integrity.toString();
918
946
  }
947
+ function createRegistryTarballResolution(dist, registry) {
948
+ const integrity = getIntegrity(dist);
949
+ const tarball = normalizeRegistryUrl(dist.tarball);
950
+ if (dist.revision == null) {
951
+ return { integrity, tarball };
952
+ }
953
+ if (!isValidTarballRevision(dist.revision)) {
954
+ throw new PnpmError('MALFORMED_METADATA', `Tarball "${redactUrlForDisplay(dist.tarball)}" has an invalid revision in its metadata: ${String(dist.revision)}`);
955
+ }
956
+ if (integrity == null ||
957
+ !isIntegrityAddressedRegistryTarballUrl(tarball, integrity, registry)) {
958
+ throw new PnpmError('MALFORMED_METADATA', `Tarball "${redactUrlForDisplay(dist.tarball)}" has revision ${dist.revision} but is not addressed by its complete integrity.`);
959
+ }
960
+ return {
961
+ integrity,
962
+ revision: dist.revision,
963
+ tarball,
964
+ };
965
+ }
966
+ const REVISION_MANIFEST_FIELDS = [
967
+ 'bin',
968
+ 'bundleDependencies',
969
+ 'bundledDependencies',
970
+ 'cpu',
971
+ 'dependencies',
972
+ 'engines',
973
+ 'hasInstallScript',
974
+ 'libc',
975
+ 'optionalDependencies',
976
+ 'os',
977
+ 'peerDependencies',
978
+ 'peerDependenciesMeta',
979
+ ];
980
+ function selectPackageRevision(pickedPackage, spec, registry) {
981
+ validateCurrentPackageRevision(pickedPackage, registry);
982
+ if (spec.revision == null)
983
+ return pickedPackage;
984
+ const revisions = pickedPackage.dist.revisions;
985
+ if (revisions == null) {
986
+ if (spec.revision === 0 && pickedPackage.dist.revision == null)
987
+ return pickedPackage;
988
+ throw new PnpmError('NO_MATCHING_REVISION', `No revision ${spec.revision} is advertised for ${pickedPackage.name}@${pickedPackage.version}`);
989
+ }
990
+ if (!Array.isArray(revisions)) {
991
+ throw malformedRevisionHistory(pickedPackage, 'the revisions field is not an array');
992
+ }
993
+ const matches = revisions.filter((entry) => isRevisionNumber(entry?.revision) && entry.revision === spec.revision);
994
+ if (matches.length === 0) {
995
+ throw new PnpmError('NO_MATCHING_REVISION', `No revision ${spec.revision} is advertised for ${pickedPackage.name}@${pickedPackage.version}`);
996
+ }
997
+ if (matches.length !== 1) {
998
+ throw malformedRevisionHistory(pickedPackage, `revision ${spec.revision} is advertised more than once`);
999
+ }
1000
+ const selectedRevision = matches[0];
1001
+ validatePackageRevision(pickedPackage, selectedRevision, registry);
1002
+ const selectedPackage = { ...pickedPackage };
1003
+ for (const field of REVISION_MANIFEST_FIELDS) {
1004
+ delete selectedPackage[field];
1005
+ const value = selectedRevision.manifest[field];
1006
+ if (value !== undefined) {
1007
+ selectedPackage[field] = value;
1008
+ }
1009
+ }
1010
+ selectedPackage.dist = {
1011
+ ...pickedPackage.dist,
1012
+ integrity: selectedRevision.integrity,
1013
+ tarball: selectedRevision.tarball,
1014
+ };
1015
+ if (spec.revision === 0) {
1016
+ delete selectedPackage.dist.revision;
1017
+ }
1018
+ else {
1019
+ selectedPackage.dist.revision = spec.revision;
1020
+ }
1021
+ return selectedPackage;
1022
+ }
1023
+ function validateCurrentPackageRevision(pickedPackage, registry) {
1024
+ const revision = pickedPackage.dist.revision;
1025
+ if (revision == null)
1026
+ return;
1027
+ if (!isValidTarballRevision(revision)) {
1028
+ throw malformedRevisionHistory(pickedPackage, `current revision ${String(revision)} is not a canonical positive safe integer`);
1029
+ }
1030
+ const revisions = pickedPackage.dist.revisions;
1031
+ if (!Array.isArray(revisions)) {
1032
+ throw malformedRevisionHistory(pickedPackage, 'the current revision has no revision history');
1033
+ }
1034
+ const matches = revisions.filter(entry => entry?.revision === revision);
1035
+ if (matches.length !== 1) {
1036
+ throw malformedRevisionHistory(pickedPackage, `current revision ${revision} does not have exactly one history entry`);
1037
+ }
1038
+ const current = matches[0];
1039
+ validatePackageRevision(pickedPackage, current, registry);
1040
+ if (pickedPackage.dist.integrity !== current.integrity ||
1041
+ normalizeRegistryUrl(pickedPackage.dist.tarball) !== normalizeRegistryUrl(current.tarball)) {
1042
+ throw malformedRevisionHistory(pickedPackage, `revision ${revision} does not match the current artifact`);
1043
+ }
1044
+ }
1045
+ function validatePackageRevision(pickedPackage, revision, registry) {
1046
+ if (!isRevisionNumber(revision.revision)) {
1047
+ throw malformedRevisionHistory(pickedPackage, `revision ${String(revision.revision)} is not a canonical safe integer`);
1048
+ }
1049
+ if (typeof revision.integrity !== 'string' ||
1050
+ typeof revision.tarball !== 'string' ||
1051
+ !isIntegrityAddressedRegistryTarballUrl(normalizeRegistryUrl(revision.tarball), revision.integrity, registry)) {
1052
+ throw malformedRevisionHistory(pickedPackage, `revision ${revision.revision} is not addressed by its complete sha512 integrity`);
1053
+ }
1054
+ if (revision.manifest == null || typeof revision.manifest !== 'object' || Array.isArray(revision.manifest)) {
1055
+ throw malformedRevisionHistory(pickedPackage, `revision ${revision.revision} has an invalid manifest`);
1056
+ }
1057
+ }
1058
+ function isRevisionNumber(revision) {
1059
+ return revision === 0 || isValidTarballRevision(revision);
1060
+ }
1061
+ function malformedRevisionHistory(pickedPackage, reason) {
1062
+ return new PnpmError('MALFORMED_METADATA', `The revision history for ${pickedPackage.name}@${pickedPackage.version} is invalid: ${reason}.`);
1063
+ }
919
1064
  /**
920
1065
  * Construct the LRU `PackageMetaCache` instance the resolver uses by
921
1066
  * default. Exported so the install layer can build one cache and hand
@@ -2,6 +2,7 @@ export interface RegistryPackageSpec {
2
2
  type: 'tag' | 'version' | 'range';
3
3
  name: string;
4
4
  fetchSpec: string;
5
+ revision?: number;
5
6
  normalizedBareSpecifier?: string;
6
7
  }
7
8
  export declare function parseBareSpecifier(bareSpecifier: string, alias: string | undefined, defaultTag: string, registry: string): RegistryPackageSpec | null;
@@ -31,9 +31,8 @@ export function parseBareSpecifier(bareSpecifier, alias, defaultTag, registry) {
31
31
  const selector = getVersionSelectorType(bareSpecifier);
32
32
  if (selector != null) {
33
33
  return {
34
- fetchSpec: selector.normalized,
34
+ ...parseRevisionSelector(selector, bareSpecifier),
35
35
  name,
36
- type: selector.type,
37
36
  };
38
37
  }
39
38
  }
@@ -58,9 +57,8 @@ export function parseJsrSpecifierToRegistryPackageSpec(rawSpecifier, alias, defa
58
57
  if (selector == null)
59
58
  return null;
60
59
  return {
61
- fetchSpec: selector.normalized,
60
+ ...parseRevisionSelector(selector, spec.versionSelector ?? defaultTag),
62
61
  name: spec.npmPkgName,
63
- type: selector.type,
64
62
  jsrPkgName: spec.jsrPkgName,
65
63
  };
66
64
  }
@@ -132,10 +130,36 @@ export function parseNamedRegistrySpecifierToRegistryPackageSpec(rawSpecifier, k
132
130
  if (selector == null)
133
131
  return null;
134
132
  return {
135
- fetchSpec: selector.normalized,
133
+ ...parseRevisionSelector(selector, versionSelector ?? defaultTag),
136
134
  name: pkgName,
137
- type: selector.type,
138
135
  registryName,
139
136
  };
140
137
  }
138
+ function parseRevisionSelector(selector, rawSelector) {
139
+ if (selector.type !== 'version') {
140
+ return { fetchSpec: selector.normalized, type: selector.type };
141
+ }
142
+ const normalizedInput = rawSelector.trim();
143
+ const buildIndex = normalizedInput.indexOf('+');
144
+ if (buildIndex === -1) {
145
+ return { fetchSpec: selector.normalized, type: selector.type };
146
+ }
147
+ const build = normalizedInput.slice(buildIndex + 1);
148
+ if (build.length < 2 || build[0] !== 'r' || build.includes('.')) {
149
+ return { fetchSpec: selector.normalized, type: selector.type };
150
+ }
151
+ const digits = build.slice(1);
152
+ if (![...digits].every((char) => char >= '0' && char <= '9')) {
153
+ return { fetchSpec: selector.normalized, type: selector.type };
154
+ }
155
+ const revision = Number(digits);
156
+ if ((digits.length > 1 && digits[0] === '0') || !Number.isSafeInteger(revision)) {
157
+ throw new PnpmError('INVALID_REVISION_SPEC', `Invalid registry revision in version specifier "${rawSelector}"`);
158
+ }
159
+ return {
160
+ fetchSpec: selector.normalized,
161
+ revision,
162
+ type: selector.type,
163
+ };
164
+ }
141
165
  //# sourceMappingURL=parseBareSpecifier.js.map
@@ -11,5 +11,6 @@
11
11
  export declare const MINIMUM_RELEASE_AGE_VIOLATION_CODE = "MINIMUM_RELEASE_AGE_VIOLATION";
12
12
  export declare const TRUST_DOWNGRADE_VIOLATION_CODE = "TRUST_DOWNGRADE";
13
13
  export declare const TARBALL_URL_MISMATCH_VIOLATION_CODE = "TARBALL_URL_MISMATCH";
14
+ export declare const TARBALL_REVISION_MISMATCH_VIOLATION_CODE = "TARBALL_REVISION_MISMATCH";
14
15
  export declare const MISSING_TARBALL_INTEGRITY_VIOLATION_CODE = "MISSING_TARBALL_INTEGRITY";
15
16
  export declare const MISSING_NAMED_REGISTRY_VIOLATION_CODE = "MISSING_NAMED_REGISTRY";
@@ -11,6 +11,7 @@
11
11
  export const MINIMUM_RELEASE_AGE_VIOLATION_CODE = 'MINIMUM_RELEASE_AGE_VIOLATION';
12
12
  export const TRUST_DOWNGRADE_VIOLATION_CODE = 'TRUST_DOWNGRADE';
13
13
  export const TARBALL_URL_MISMATCH_VIOLATION_CODE = 'TARBALL_URL_MISMATCH';
14
+ export const TARBALL_REVISION_MISMATCH_VIOLATION_CODE = 'TARBALL_REVISION_MISMATCH';
14
15
  export const MISSING_TARBALL_INTEGRITY_VIOLATION_CODE = 'MISSING_TARBALL_INTEGRITY';
15
16
  export const MISSING_NAMED_REGISTRY_VIOLATION_CODE = 'MISSING_NAMED_REGISTRY';
16
17
  //# sourceMappingURL=violationCodes.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/resolving.npm-resolver",
3
- "version": "1104.0.0",
3
+ "version": "1104.1.0",
4
4
  "description": "Resolver for npm-hosted packages",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -29,24 +29,25 @@
29
29
  "!*.map"
30
30
  ],
31
31
  "dependencies": {
32
- "@pnpm/config.normalize-registries": "1101.0.0",
33
- "@pnpm/config.pick-registry-for-package": "1101.0.0",
34
- "@pnpm/config.version-policy": "1100.2.1",
32
+ "@pnpm/config.normalize-registries": "1101.0.1",
33
+ "@pnpm/config.pick-registry-for-package": "1101.0.1",
34
+ "@pnpm/config.version-policy": "1100.2.2",
35
35
  "@pnpm/constants": "1102.0.0",
36
- "@pnpm/core-loggers": "1100.3.3",
37
- "@pnpm/crypto.hash": "1100.0.2",
38
- "@pnpm/deps.path": "1101.0.0",
36
+ "@pnpm/core-loggers": "1100.3.4",
37
+ "@pnpm/crypto.hash": "1100.0.3",
38
+ "@pnpm/deps.path": "1101.0.1",
39
39
  "@pnpm/error": "1100.1.3",
40
40
  "@pnpm/fetching.types": "1100.0.3",
41
- "@pnpm/fs.graceful-fs": "1100.1.1",
42
- "@pnpm/pkg-manifest.utils": "1100.4.1",
41
+ "@pnpm/fs.graceful-fs": "1100.2.0",
42
+ "@pnpm/pkg-manifest.utils": "1100.4.2",
43
43
  "@pnpm/resolving.jsr-specifier-parser": "1100.0.6",
44
- "@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.17",
45
- "@pnpm/resolving.registry.types": "1100.1.10",
46
- "@pnpm/resolving.resolver-base": "1101.1.1",
47
- "@pnpm/store.cafs": "1100.2.0",
48
- "@pnpm/store.index": "1100.2.5",
49
- "@pnpm/types": "1102.0.0",
44
+ "@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.18",
45
+ "@pnpm/resolving.registry.types": "1100.2.0",
46
+ "@pnpm/resolving.resolver-base": "1101.2.0",
47
+ "@pnpm/resolving.tarball-url": "1101.1.0",
48
+ "@pnpm/store.cafs": "1100.3.0",
49
+ "@pnpm/store.index": "1100.3.0",
50
+ "@pnpm/types": "1102.1.0",
50
51
  "@pnpm/workspace.range-resolver": "1100.0.3",
51
52
  "@pnpm/workspace.spec-parser": "1100.0.1",
52
53
  "@zkochan/retry": "^0.2.0",
@@ -65,13 +66,13 @@
65
66
  },
66
67
  "peerDependencies": {
67
68
  "@pnpm/logger": "^1100.0.0",
68
- "@pnpm/worker": "^1100.3.0"
69
+ "@pnpm/worker": "^1100.4.0"
69
70
  },
70
71
  "devDependencies": {
71
72
  "@jest/globals": "30.4.1",
72
73
  "@pnpm/logger": "1100.0.0",
73
- "@pnpm/network.fetch": "1100.1.13",
74
- "@pnpm/resolving.npm-resolver": "1104.0.0",
74
+ "@pnpm/network.fetch": "1100.1.14",
75
+ "@pnpm/resolving.npm-resolver": "1104.1.0",
75
76
  "@pnpm/test-fixtures": "1100.0.1",
76
77
  "@pnpm/testing.mock-agent": "1101.0.7",
77
78
  "@types/normalize-path": "^3.0.2",