From 3b40adee3ef26674204c557324ed2cca9d7170d7 Mon Sep 17 00:00:00 2001 From: Hamidreza Hanafi Date: Sat, 14 Feb 2026 10:57:13 -0500 Subject: [PATCH] manifest: normalize legacy npm-prefixed resolution selectors Accept legacy descriptor selectors such as no-deps@npm:1.0.0 by normalizing them to anonymous ranges during resolution parsing. This preserves compatibility with manifests generated by older Yarn flows. Co-authored-by: Cursor --- packages/zpm/src/manifest/resolutions.rs | 23 ++++++++----- .../sources/features/resolutions.test.js | 34 +++++++++++++++++++ 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/packages/zpm/src/manifest/resolutions.rs b/packages/zpm/src/manifest/resolutions.rs index 4294d815..c2bab27a 100644 --- a/packages/zpm/src/manifest/resolutions.rs +++ b/packages/zpm/src/manifest/resolutions.rs @@ -1,7 +1,7 @@ use rkyv::Archive; use serde::{Deserialize, Deserializer}; use zpm_macro_enum::zpm_enum; -use zpm_primitives::{Descriptor, Ident, Locator, Range, RegistrySemverRange}; +use zpm_primitives::{Descriptor, Ident, Locator, Range}; use zpm_utils::{FromFileString, ToFileString}; use crate::{ @@ -103,7 +103,19 @@ impl ResolutionSelector { } } - +fn normalize_legacy_resolution_selector(selector: ResolutionSelector) -> ResolutionSelector { + match selector { + ResolutionSelector::Descriptor(mut params) => { + params.descriptor.range = params.descriptor.range.to_anonymous_range(); + ResolutionSelector::Descriptor(params) + }, + ResolutionSelector::DescriptorIdent(mut params) => { + params.parent_descriptor.range = params.parent_descriptor.range.to_anonymous_range(); + ResolutionSelector::DescriptorIdent(params) + }, + _ => selector, + } +} use serde::{ser::SerializeMap, Serialize, Serializer}; use serde::de::{self, Visitor, MapAccess}; @@ -188,17 +200,12 @@ impl<'de> Visitor<'de> for ResolutionsFieldVisitor { while let Some(key) = map.next_key::()? { let selector = ResolutionSelector::from_file_string(&key) .map_err(|_| de::Error::custom("invalid resolution selector"))?; + let selector = normalize_legacy_resolution_selector(selector); let value_str: String = map.next_value()?; let range = Range::from_file_string(&value_str) .map_err(|_| de::Error::custom("invalid range"))?; - // TODO: Remove this in a future major version; we're keeping it for backwards compatibility with - // the Berry codebase in which `yarn patch` was adding the "npm:" prefix to all descriptors. - if matches!(selector, ResolutionSelector::Descriptor(DescriptorResolutionSelector {descriptor: Descriptor {range: Range::RegistrySemver(RegistrySemverRange {ident: None, ..}), ..}, ..})) { - return Err(de::Error::custom("the 'npm:' prefix is no longer needed")); - } - let is_valid_resolution_descriptor = matches!(selector, | ResolutionSelector::Descriptor(DescriptorResolutionSelector {descriptor: Descriptor {range: Range::AnonymousSemver(_), ..}, ..}) | ResolutionSelector::DescriptorIdent(DescriptorIdentResolutionSelector {parent_descriptor: Descriptor {range: Range::AnonymousSemver(_), ..}, ..}) diff --git a/tests/acceptance-tests/pkg-tests-specs/sources/features/resolutions.test.js b/tests/acceptance-tests/pkg-tests-specs/sources/features/resolutions.test.js index 480734ec..e7707544 100644 --- a/tests/acceptance-tests/pkg-tests-specs/sources/features/resolutions.test.js +++ b/tests/acceptance-tests/pkg-tests-specs/sources/features/resolutions.test.js @@ -109,6 +109,40 @@ describe(`Features`, () => { ), ); + test( + `it should support legacy npm-prefixed descriptor selectors`, + makeTemporaryEnv( + { + dependencies: { + [`one-fixed-dep`]: `1.0.0`, + [`no-deps`]: `1.1.0`, + }, + resolutions: { + [`no-deps@npm:1.0.0`]: `2.0.0`, + }, + }, + async ({run, source}) => { + await run(`install`); + + await expect(source(`require('no-deps')`)).resolves.toMatchObject({ + name: `no-deps`, + version: `1.1.0`, + }); + + await expect(source(`require('one-fixed-dep')`)).resolves.toMatchObject({ + name: `one-fixed-dep`, + version: `1.0.0`, + dependencies: { + [`no-deps`]: { + name: `no-deps`, + version: `2.0.0`, + }, + }, + }); + }, + ), + ); + test( `it should support overriding a packages with another, but only if its parent package has a specific version`, makeTemporaryEnv(