fix(add): resolve a version-less dependency to the tag setting (#17260)

pnpm v12 resolved `pnpm add <pkg>` without a version, and a version-less
`npm:` or `jsr:` dependency, to the `latest` dist-tag even when the `tag`
setting named another one. pnpm v11 reads `tag` from pnpm-workspace.yaml,
the global config, `PNPM_CONFIG_TAG`, and `--config.tag`, and threads it
as `defaultTag` into `parseWantedDependencies` (installSome.ts) and the
resolution context (createResolutionContext.ts).

`Config` now carries `tag`, read from the same layers, with
`Config::default_tag` falling back to `latest`. A brand-new `add` with no
version resolves a non-default tag the way an explicit `add <pkg>@<tag>`
does, so the picked version is also locked. A `jsr:` add and the install
resolver take the same default. `dlx`, `add --config`, `update --latest`,
and `outdated` keep `latest`, as they do in v11.

Tested with e2e tests for the yaml setting, `--config.tag`, and a
version-less `npm:` alias, each seen to fail with `default_tag` forced to
`latest`, plus config unit tests for apply, override, reset, and the
resolved projection.

Part of pnpm/tasks#191

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Zoltan KochanandClaude Opus 5.5 authored and GitHub committed 2026-10-23 09:46:18 +02:00
1 parent be2f02ffc8
commit 6d736a02a8
25 files changed
+371 -66

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
"pacquet": patch
---
`pnpm add <pkg>` without a version now installs the version under the dist-tag set by the `tag` setting. An `npm:` alias or a `jsr:` dependency without a version resolves that tag too. The setting is read from `pnpm-workspace.yaml`, the global config, `PNPM_CONFIG_TAG`, and `--config.tag`, and defaults to `latest`.
@@ -407,6 +407,9 @@ pub(super) fn configure_cache_install(
// with `patchedDependencies` fail on a patch file missing under the
// cache dir.
config.patched_dependencies = None;
// pnpm's dlx resolves a version-less package to `latest` whatever the
// caller's `tag` setting says, and the cache key holds no tag.
config.tag = None;
// Build a *fresh* allow-list for the throwaway install — the dlx
// packages themselves plus the CLI `--allow-build` entries — rather
// than inheriting the caller project's `allow_builds` /
@@ -3,7 +3,8 @@ use super::{
add_with_settings, assert_eq, bravo_dep_mature_up_to_1_0_1_minimum_release_age,
exec_pacquet_in_temp_cwd, prod_spec, set_minimum_release_age,
};
use assert_cmd::{assert::OutputAssertExt, cargo::CommandCargoExt};
use crate::_utils::pacquet_in;
use assert_cmd::assert::OutputAssertExt;
#[test]
fn save_prefix_defaults_to_caret() {
@@ -382,10 +383,123 @@ fn readd_with_a_new_specifier_keeps_the_picked_version() {
drop((root, npmrc_info)); // cleanup
}
fn pacquet_in(workspace: &std::path::Path) -> std::process::Command {
std::process::Command::cargo_bin("pnpm")
.expect("find the pnpm binary")
.with_current_dir(workspace)
/// The `tag` setting names the dist-tag a version-less `add` resolves.
#[test]
fn add_without_a_version_resolves_the_configured_tag() {
let CommandTempCwd { root, workspace, npmrc_info, .. } =
CommandTempCwd::init().add_mocked_registry_with_own_storage();
let name = "@pnpm.e2e/dep-of-pkg-with-1-dep";
npmrc_info.set_dist_tag(name, "100.0.0", "dev");
crate::_utils::append_workspace_yaml_key(&workspace, "tag", "dev");
pacquet_in(&workspace)
.with_args(["add", name, "--lockfile-only"])
.assert()
.success();
assert_eq!(prod_spec(&workspace, name), "^100.0.0");
assert_eq!(locked_version(&workspace, name), "100.0.0");
drop((root, npmrc_info)); // cleanup
}
#[test]
fn add_without_a_version_resolves_the_tag_from_a_config_override() {
let CommandTempCwd { root, workspace, npmrc_info, .. } =
CommandTempCwd::init().add_mocked_registry_with_own_storage();
let name = "@pnpm.e2e/dep-of-pkg-with-1-dep";
npmrc_info.set_dist_tag(name, "100.0.0", "dev");
pacquet_in(&workspace)
.with_args(["add", name, "--lockfile-only", "--config.tag=dev"])
.assert()
.success();
assert_eq!(prod_spec(&workspace, name), "^100.0.0");
assert_eq!(locked_version(&workspace, name), "100.0.0");
drop((root, npmrc_info)); // cleanup
}
/// An `npm:` alias with no version resolves the `tag` setting too.
#[test]
fn install_resolves_a_versionless_npm_alias_to_the_configured_tag() {
let CommandTempCwd { root, workspace, npmrc_info, .. } =
CommandTempCwd::init().add_mocked_registry_with_own_storage();
let name = "@pnpm.e2e/dep-of-pkg-with-1-dep";
npmrc_info.set_dist_tag(name, "100.0.0", "dev");
crate::_utils::append_workspace_yaml_key(&workspace, "tag", "dev");
std::fs::write(
workspace.join("package.json"),
serde_json::json!({ "dependencies": { "alias": format!("npm:{name}") } }).to_string(),
)
.expect("write package.json");
pacquet_in(&workspace)
.with_args(["install", "--lockfile-only"])
.assert()
.success();
assert_eq!(locked_version(&workspace, "alias"), format!("{name}@100.0.0"));
drop((root, npmrc_info)); // cleanup
}
/// pnpm reads `<pkg>@` as `<pkg>`, so the empty version resolves the tag.
#[test]
fn add_with_an_empty_version_resolves_the_configured_tag() {
let CommandTempCwd { root, workspace, npmrc_info, .. } =
CommandTempCwd::init().add_mocked_registry_with_own_storage();
let name = "@pnpm.e2e/dep-of-pkg-with-1-dep";
npmrc_info.set_dist_tag(name, "100.0.0", "dev");
crate::_utils::append_workspace_yaml_key(&workspace, "tag", "dev");
pacquet_in(&workspace)
.with_args(["add", &format!("{name}@"), "--lockfile-only"])
.assert()
.success();
assert_eq!(prod_spec(&workspace, name), "^100.0.0");
assert_eq!(locked_version(&workspace, name), "100.0.0");
drop((root, npmrc_info)); // cleanup
}
/// An empty specifier in the manifest resolves the tag, as in pnpm.
#[test]
fn install_resolves_an_empty_specifier_to_the_configured_tag() {
let CommandTempCwd { root, workspace, npmrc_info, .. } =
CommandTempCwd::init().add_mocked_registry_with_own_storage();
let name = "@pnpm.e2e/dep-of-pkg-with-1-dep";
npmrc_info.set_dist_tag(name, "100.0.0", "dev");
crate::_utils::append_workspace_yaml_key(&workspace, "tag", "dev");
std::fs::write(
workspace.join("package.json"),
serde_json::json!({ "dependencies": { (name): "" } }).to_string(),
)
.expect("write package.json");
pacquet_in(&workspace)
.with_args(["install", "--lockfile-only"])
.assert()
.success();
assert_eq!(locked_version(&workspace, name), "100.0.0");
drop((root, npmrc_info)); // cleanup
}
#[test]
fn add_without_a_version_resolves_the_tag_from_the_environment() {
let CommandTempCwd { root, workspace, npmrc_info, .. } =
CommandTempCwd::init().add_mocked_registry_with_own_storage();
let name = "@pnpm.e2e/dep-of-pkg-with-1-dep";
npmrc_info.set_dist_tag(name, "100.0.0", "dev");
pacquet_in(&workspace)
.with_env("PNPM_CONFIG_TAG", "dev")
.with_args(["add", name, "--lockfile-only"])
.assert()
.success();
assert_eq!(prod_spec(&workspace, name), "^100.0.0");
assert_eq!(locked_version(&workspace, name), "100.0.0");
drop((root, npmrc_info)); // cleanup
}
fn locked_version(workspace: &std::path::Path, name: &str) -> String {
+31 -2
View File
@@ -16,8 +16,13 @@ use tempfile::TempDir;
/// `npm.jsr.io`; point it at the mocked registry, which serves the
/// `@jsr/pnpm-e2e__bar` fixture up to 2.0.0.
fn setup() -> (TempDir, std::path::PathBuf, AddMockedRegistry) {
let CommandTempCwd { root, workspace, npmrc_info, .. } =
CommandTempCwd::init().add_mocked_registry();
route_jsr_to_the_mocked_registry(CommandTempCwd::init().add_mocked_registry())
}
fn route_jsr_to_the_mocked_registry(
cwd: CommandTempCwd<AddMockedRegistry>,
) -> (TempDir, std::path::PathBuf, AddMockedRegistry) {
let CommandTempCwd { root, workspace, npmrc_info, .. } = cwd;
let npmrc = fs::read_to_string(&npmrc_info.npmrc_path).expect("read the harness .npmrc");
let jsr_registry = npmrc_info.mock_instance.url();
fs::write(&npmrc_info.npmrc_path, format!("{npmrc}@jsr:registry={jsr_registry}\n"))
@@ -81,3 +86,27 @@ fn add_keeps_the_range_operator_a_jsr_selector_asks_for() {
drop((root, anchor));
}
/// A `jsr:` selector with no version resolves the `tag` setting.
#[test]
fn add_resolves_a_versionless_jsr_selector_to_the_configured_tag() {
let (root, workspace, anchor) = route_jsr_to_the_mocked_registry(
CommandTempCwd::init().add_mocked_registry_with_own_storage(),
);
anchor.set_dist_tag("@jsr/pnpm-e2e__bar", "1.0.0", "dev");
_utils::append_workspace_yaml_key(&workspace, "tag", "dev");
pacquet_in(&workspace)
.with_args(["add", "jsr:@pnpm-e2e/bar", "--lockfile-only"])
.assert()
.success();
assert_eq!(
dependency_spec(&workspace, "dependencies", "@pnpm-e2e/bar").as_deref(),
Some("jsr:^1.0.0"),
);
let lockfile = read_lockfile(&workspace.join(Lockfile::FILE_NAME));
let entry = root_dependency(&lockfile, "@pnpm-e2e/bar");
assert_eq!(entry.version.to_string(), "@jsr/pnpm-e2e__bar@1.0.0");
drop((root, anchor));
}
+1 -1
View File
@@ -524,7 +524,7 @@ fn jsr_sources_and_direct_jsr_registry_packages_use_their_own_metadata_policy()
(
"application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*",
abbreviated,
2 - usize::from(direct),
1,
),
] {
requests.push(
+21
View File
@@ -533,3 +533,24 @@ fn dlx_recovers_ignored_builds() {
drop(root);
}
}
/// pnpm's dlx resolves a version-less package to `latest`, so a caller's
/// `tag` that names no version of the package must not fail the run.
#[test]
#[cfg_attr(not(unix), ignore = "dlx bin execution is only exercised on Unix")]
fn dlx_ignores_the_caller_projects_tag_setting() {
let CommandTempCwd { pacquet, root, workspace, .. } =
CommandTempCwd::init().add_mocked_registry();
append_workspace_yaml_key(&workspace, "tag", "no-such-tag");
let output = pacquet
.with_args(["dlx", "@pnpm.e2e/hello-world-js-bin"])
.assert()
.success()
.get_output()
.stdout
.clone();
assert!(String::from_utf8_lossy(&output).contains("Hello world!"));
drop(root);
}
+1
View File
@@ -415,6 +415,7 @@ impl WorkspaceSettings {
enum_field!(settings, reader, resolution_mode, "RESOLUTION_MODE", ResolutionMode);
enum_field!(settings, reader, catalog_mode, "CATALOG_MODE", CatalogMode);
string_field!(settings, reader, save_catalog_name, "SAVE_CATALOG_NAME");
string_field!(settings, reader, tag, "TAG");
if let Some(save_prefix) = reader.value_allow_empty("SAVE_PREFIX") {
settings.save_prefix = Some(save_prefix);
}
@@ -418,3 +418,14 @@ fn publish_wait_timeout_reads_from_environment() {
}
assert_eq!(WorkspaceSettings::from_pnpm_config_env::<Env>().publish_wait_timeout, Some(600000));
}
#[test]
fn tag_parses_from_env() {
struct EnvTag;
impl EnvVar for EnvTag {
fn var(name: &str) -> Option<String> {
(name == "PNPM_CONFIG_TAG").then(|| "next".to_owned())
}
}
assert_eq!(WorkspaceSettings::from_pnpm_config_env::<EnvTag>().tag.as_deref(), Some("next"));
}
+14
View File
@@ -1714,6 +1714,11 @@ pub struct Config {
/// `undefined`).
pub save_catalog_name: Option<String>,
/// The dist-tag a dependency with no version resolves to, as in
/// `pnpm add foo` or an `npm:foo` alias. The `tag` setting. Unset means
/// `latest`; read it through [`Config::default_tag`].
pub tag: Option<String>,
/// The range operator `pnpm add` prepends to a resolved version
/// when saving it: `^` (the default), `~`, or `""` for an exact
/// pin. The `savePrefix` setting, overridden per-invocation by
@@ -1866,6 +1871,15 @@ impl Config {
self.engine_strict && !self.force
}
/// [`tag`](Self::tag), or `latest` when it is unset or empty.
#[must_use]
pub fn default_tag(&self) -> &str {
self.tag
.as_deref()
.filter(|tag| !tag.is_empty())
.unwrap_or("latest")
}
#[must_use]
pub fn new() -> Self {
Self::default()
@@ -132,6 +132,7 @@ impl WorkspaceSettings {
self.prefer_symlinked_executables.take(),
);
overlay_some(&mut config.save_catalog_name, self.save_catalog_name.take());
overlay_some(&mut config.tag, self.tag.take());
overlay_some(&mut config.init_author_name, self.init_author_name.take());
overlay_some(&mut config.init_author_email, self.init_author_email.take());
overlay_some(&mut config.init_author_url, self.init_author_url.take());
@@ -101,6 +101,7 @@ impl WorkspaceSettings {
pub(super) fn substitute_env_scalars<Sys: EnvVar>(&mut self) {
substitute_optional_string::<Sys>(&mut self.scope);
substitute_optional_string::<Sys>(&mut self.tag);
substitute_optional_string::<Sys>(&mut self.store_dir);
substitute_optional_string::<Sys>(&mut self.state_dir);
substitute_optional_string::<Sys>(&mut self.modules_dir);
@@ -157,7 +157,7 @@ impl WorkspaceSettings {
minimum_release_age_ignore_missing_time, minimum_release_age_strict,
trust_lockfile, trust_policy, trust_policy_exclude, trust_policy_exclude_prune,
trust_policy_ignore_after, init_author_name, init_author_email, init_author_url,
init_license, init_version, pm_on_fail, versioning, save_catalog_name,
init_license, init_version, pm_on_fail, versioning, save_catalog_name, tag,
save_prefix, tag_version_prefix, pipeline_base, child_concurrency, workspace_concurrency, catalogs,
allow_builds, concurrency_groups, boot_config,
});
@@ -185,6 +185,7 @@ impl WorkspaceSettings {
pm_on_fail: config.pm_on_fail,
versioning: Some(config.versioning.clone()),
save_catalog_name: config.save_catalog_name.clone(),
tag: config.tag.clone(),
save_prefix: config.save_prefix.clone(),
tag_version_prefix: Some(config.tag_version_prefix.clone()),
pipeline_base: config.pipeline_base.clone(),
@@ -840,6 +840,11 @@ pub struct WorkspaceSettings {
/// [`Config::save_catalog_name`]: crate::Config::save_catalog_name
pub save_catalog_name: Option<String>,
/// `tag` from `pnpm-workspace.yaml`. See [`Config::tag`].
///
/// [`Config::tag`]: crate::Config::tag
pub tag: Option<String>,
/// `savePrefix` from `pnpm-workspace.yaml`. See
/// [`Config::save_prefix`].
///
@@ -71,6 +71,8 @@ mod workspace_settings;
mod reporting;
mod tag;
mod tag_version_prefix;
mod integrity;
@@ -33,6 +33,7 @@ fn from_resolved_reports_every_setting() {
save_prefix: Some("~".to_string()),
tag_version_prefix: "release-".to_string(),
save_catalog_name: Some("default".to_string()),
tag: Some("next".to_string()),
pipeline_base: Some("origin/main".to_string()),
remote_cache: Some(Box::default()),
init_author_name: Some("Example".to_string()),
@@ -0,0 +1,40 @@
use super::{BTreeMap, Config, Path, WorkspaceSettings, assert_eq};
#[test]
fn tag_defaults_to_latest_and_applies_from_yaml() {
let mut config = Config::default();
assert_eq!(config.tag, None);
assert_eq!(config.default_tag(), "latest");
let settings: WorkspaceSettings = serde_saphyr::from_str("tag: next\n").unwrap();
settings.apply_to(&mut config, Path::new("/workspace"));
assert_eq!(config.default_tag(), "next");
assert_eq!(WorkspaceSettings::from_resolved(&config).tag.as_deref(), Some("next"));
}
#[test]
fn an_empty_tag_falls_back_to_latest() {
let mut config = Config::default();
let settings: WorkspaceSettings = serde_saphyr::from_str("tag: ''\n").unwrap();
settings.apply_to(&mut config, Path::new("/workspace"));
assert_eq!(config.default_tag(), "latest");
}
#[test]
fn tag_is_read_from_a_config_override() {
let values = BTreeMap::from([("tag".to_string(), "beta".to_string())]);
let mut config = Config::default();
WorkspaceSettings::from_string_values(&values).apply_to(&mut config, Path::new("/workspace"));
assert_eq!(config.default_tag(), "beta");
}
#[test]
fn tag_can_be_reset() {
let mut config = Config { tag: Some("next".to_string()), ..Config::default() };
assert!(WorkspaceSettings::reset_setting_to_default::<crate::Host>(
&mut config,
&Config::default(),
"tag",
Path::new("/workspace"),
));
assert_eq!(config.tag, None);
}
@@ -259,6 +259,7 @@ fn create_config(
minimum_release_age_exclude_prune: false,
catalogs: None,
save_catalog_name: None,
tag: None,
save_prefix: None,
save_exact: false,
save_peer: false,
+39 -18
View File
@@ -108,29 +108,26 @@ pub(super) fn seed_picked_versions(
seed: (&mut HashSet<String>, &mut PreferredVersions),
) {
let (dropped_pins, preferred) = seed;
for PickedVersion { name, version, specifier } in picked_versions {
let (Ok(package), Ok(semver)) = (name.parse::<PkgName>(), version.parse::<Version>())
else {
for picked in picked_versions {
let Some(lock) = locked.unsettled_pick(picked) else {
continue;
};
let direct = locked.direct(&package);
let pinned_elsewhere = direct.is_some_and(|(locked, _)| {
!locked.is_some_and(|locked| same_build(locked, &semver))
});
let settled = match direct {
Some(_) if pinned_elsewhere => false,
Some((_, locked_specifier)) if locked_specifier == specifier => true,
_ => locked.only_version_of(&package, &semver),
};
if settled {
continue;
}
crate::install_with_fresh_lockfile::prefer_requested_version(preferred, name, version);
if pinned_elsewhere {
dropped_pins.insert(name.clone());
crate::install_with_fresh_lockfile::prefer_requested_version(
preferred,
&picked.name,
&picked.version,
);
if lock == PickLock::PinnedElsewhere {
dropped_pins.insert(picked.alias.clone());
}
}
}
/// How the importer locks a pick the install has to move to.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PickLock {
Unpinned,
PinnedElsewhere,
}
/// What the wanted lockfile holds for the importer an add rewrites.
pub(super) struct LockedVersions<'l> {
lockfile: Option<&'l Lockfile>,
@@ -150,6 +147,30 @@ impl<'l> LockedVersions<'l> {
Self { lockfile, importer }
}
/// `None` when the fast lockfile update already lands on `picked`.
fn unsettled_pick(&self, picked: &PickedVersion) -> Option<PickLock> {
let PickedVersion { alias, name, version, specifier } = picked;
let (Ok(alias), Ok(name), Ok(version)) =
(alias.parse::<PkgName>(), name.parse::<PkgName>(), version.parse::<Version>())
else {
return None;
};
let direct = self.direct(&alias);
let pinned_elsewhere = direct.is_some_and(|(locked, _)| {
!locked.is_some_and(|locked| same_build(locked, &version))
});
let settled = match direct {
Some(_) if pinned_elsewhere => false,
Some((_, locked_specifier)) if locked_specifier == specifier => true,
_ => self.only_version_of(&name, &version),
};
match (settled, pinned_elsewhere) {
(true, _) => None,
(false, false) => Some(PickLock::Unpinned),
(false, true) => Some(PickLock::PinnedElsewhere),
}
}
/// The version and specifier the importer locks `name` at as a direct
/// dependency.
fn direct(&self, name: &PkgName) -> Option<(Option<&'l Version>, &'l str)> {
@@ -222,9 +222,11 @@ pub(super) struct PreparedManifest {
pub(super) updated_catalogs: Catalogs,
pub(super) picked_versions: Vec<PickedVersion>,
}
/// The version an explicit registry specifier for `name` resolved to, and
/// The version an explicit registry specifier for `alias` resolved to, and
/// the specifier the manifest saves for it.
pub(super) struct PickedVersion {
pub(super) alias: String,
/// The registry name the version belongs to.
pub(super) name: String,
pub(super) version: String,
pub(super) specifier: String,
@@ -290,12 +292,13 @@ impl PreparedManifest {
let mut picked_versions = Vec::new();
for dependency in dependencies {
merge_catalogs(&mut updated_catalogs, &dependency.updated_catalogs);
if let Some(version) = dependency.picked_version
&& dependency.manifest_specifier != version
if let Some(picked) = dependency.picked
&& dependency.manifest_specifier != picked.version
{
picked_versions.push(PickedVersion {
name: dependency.package_name,
version,
alias: dependency.package_name,
name: picked.name,
version: picked.version,
specifier: dependency.manifest_specifier,
});
}
@@ -27,7 +27,14 @@ pub(super) struct ResolvedAddedDependency {
pub(super) types_selector: Option<String>,
/// The version an explicit registry specifier resolved to, which the
/// install must lock.
pub(super) picked_version: Option<String>,
pub(super) picked: Option<PickedPackage>,
}
/// A registry package version picked while resolving an added selector.
pub(super) struct PickedPackage {
/// The registry name, which differs from the dependency's alias for a
/// `jsr:` selector.
pub(super) name: String,
pub(super) version: String,
}
pub(super) async fn resolve_added_dependency(
package_selector: &str,
@@ -37,10 +44,7 @@ pub(super) async fn resolve_added_dependency(
let selector = AddSelector::parse(package_selector, manifest, inputs).await?;
let package_name = selector.package_name(package_selector);
let prev_specifier = declared_specifier(manifest, package_name);
let SaveSpecifier {
specifier: bare_specifier,
picked_version,
} = bare_save_specifier(
let SaveSpecifier { specifier: bare_specifier, picked } = bare_save_specifier(
package_selector,
&selector,
prev_specifier.as_deref(),
@@ -65,7 +69,7 @@ pub(super) async fn resolve_added_dependency(
updated_catalogs,
warnings,
types_selector,
picked_version,
picked,
})
}
@@ -147,6 +151,8 @@ impl AddSelector {
(None, Some(protocol)) => protocol.explicit_spec(package_selector),
(None, None) => split_name_spec(package_selector).1,
}
// pnpm reads `foo@` as `foo`, which resolves the `tag` setting.
.filter(|spec| !spec.is_empty())
}
}
/// The dependency's current specifier, so a re-add keeps the
@@ -190,7 +196,8 @@ pub(super) fn declared_specifier(manifest: &PackageManifest, package_name: &str)
/// `catalog:` reference — naming no version asks for whatever the
/// workspace agreed on, which is the entry, not the `latest` range
/// it happens to resolve to today;
/// - any other brand-new dependency fetches and pins the `latest` range.
/// - any other brand-new dependency fetches and pins the range of the
/// configured `tag`, `latest` by default.
pub(super) async fn bare_save_specifier(
package_selector: &str,
selector: &AddSelector,
@@ -224,8 +231,7 @@ pub(super) async fn bare_save_specifier(
}
if let Some(ProtocolSelector::Jsr(jsr)) = selector.protocol.as_ref() {
return Ok(resolve_jsr_save_specifier(jsr, manifest, inputs).await?
.unwrap_or_else(|| package_selector.to_string())
.into());
.unwrap_or_else(|| package_selector.to_string().into()));
}
registry_save_specifier(package_name, explicit_spec, prev_specifier, manifest, inputs).await
}
@@ -238,33 +244,57 @@ async fn registry_save_specifier(
inputs: &AddResolveInputs<'_, '_>,
) -> Result<SaveSpecifier, AddError> {
match (explicit_spec, prev_specifier) {
(Some(spec), prev) => Ok(
match resolve_explicit_registry_spec(package_name, spec, prev, manifest, inputs).await?
{
Some(picked) => {
SaveSpecifier { specifier: picked.range, picked_version: Some(picked.version) }
}
None => normalized_save_specifier(spec).into(),
},
),
(Some(spec), prev) => {
explicit_registry_save_specifier(package_name, spec, prev, manifest, inputs).await
}
(None, Some(prev)) => Ok(prev.to_string().into()),
(None, None) => match cataloged_specifier(package_name, inputs) {
Some(specifier) => Ok(specifier.into()),
None => {
pick_latest_range(package_name, manifest, inputs).await.map(SaveSpecifier::from)
}
None => match inputs.add.config.default_tag() {
// The default tag keeps the shared picker every plain add uses.
"latest" => {
pick_latest_range(package_name, manifest, inputs).await.map(SaveSpecifier::from)
}
tag => {
explicit_registry_save_specifier(package_name, tag, None, manifest, inputs)
.await
}
},
},
}
}
/// [`registry_save_specifier`] for a registry selector that names `spec`.
async fn explicit_registry_save_specifier(
package_name: &str,
spec: &str,
prev_specifier: Option<&str>,
manifest: &PackageManifest,
inputs: &AddResolveInputs<'_, '_>,
) -> Result<SaveSpecifier, AddError> {
Ok(
match resolve_explicit_registry_spec(package_name, spec, prev_specifier, manifest, inputs)
.await?
{
Some(picked) => SaveSpecifier {
specifier: picked.range,
picked: Some(PickedPackage {
name: package_name.to_string(),
version: picked.version,
}),
},
None => normalized_save_specifier(spec).into(),
},
)
}
/// What [`bare_save_specifier`] records, and the version it resolved an
/// explicit registry specifier to.
pub(super) struct SaveSpecifier {
specifier: String,
picked_version: Option<String>,
picked: Option<PickedPackage>,
}
impl From<String> for SaveSpecifier {
fn from(specifier: String) -> Self {
Self { specifier, picked_version: None }
Self { specifier, picked: None }
}
}
/// The `catalog:` reference for a dependency the catalog already lists, or
@@ -380,10 +410,11 @@ pub(super) async fn resolve_jsr_save_specifier(
spec: &JsrSpec,
manifest: &PackageManifest,
inputs: &AddResolveInputs<'_, '_>,
) -> Result<Option<String>, AddError> {
) -> Result<Option<SaveSpecifier>, AddError> {
let inputs =
AddResolveInputs { add: super::AddOptions { save_types: false, ..inputs.add }, ..*inputs };
let version_selector = spec.version_selector.as_deref().unwrap_or("latest");
let version_selector =
spec.version_selector.as_deref().unwrap_or_else(|| inputs.add.config.default_tag());
let range = resolve_explicit_registry_spec(
&spec.npm_pkg_name,
version_selector,
@@ -395,7 +426,10 @@ pub(super) async fn resolve_jsr_save_specifier(
&inputs,
)
.await?;
Ok(range.map(|picked| format!("jsr:{}", picked.range)))
Ok(range.map(|picked| SaveSpecifier {
specifier: format!("jsr:{}", picked.range),
picked: Some(PickedPackage { name: spec.npm_pkg_name.clone(), version: picked.version }),
}))
}
/// Split a `pacquet add` argument into its package name and optional
/// `@<version>` part. The version separator is the first `@` at or after
@@ -103,7 +103,7 @@ impl SharedResolveOptions<'_> {
},
version: pnpm_resolving_resolver_base::VersionSelectionOptions {
preferred_versions,
default_tag: Some("latest".to_string()),
default_tag: Some(self.config.default_tag().to_string()),
..Default::default()
},
refresh: pnpm_resolving_resolver_base::ResolutionRefreshOptions {
@@ -101,7 +101,10 @@ pub(super) fn wanted_spec(
default_tag: &str,
registry: &str,
) -> Option<RegistryPackageSpec> {
if let Some(bare) = wanted_dependency.bare_specifier.as_deref() {
if let Some(bare) = wanted_dependency.bare_specifier
.as_deref()
.filter(|bare| !bare.is_empty())
{
return parse_bare_specifier(
bare,
wanted_dependency.alias.as_deref(),
-3
View File
@@ -59,9 +59,6 @@ If you `pnpm add` a package and you don't provide a specific version, then it
will install the package at the version registered under the tag from this
setting.
This also sets the tag that is added to the `package@version` specified by the
`pnpm tag` command if no explicit tag is given.
### globalDir
* Default:
-3
View File
@@ -34,9 +34,6 @@ If you `pnpm add` a package and you don't provide a specific version, then it
will install the package at the version registered under the tag from this
setting.
This also sets the tag that is added to the `package@version` specified by the
`pnpm tag` command if no explicit tag is given.
### globalDir
* Default: