Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,27 @@ name = "command_outdated_global"
vp = "global"
skip-platforms = ["windows"]
steps = [
{ argv = ["vp", "install", "-g", "testnpm2@1.0.0"], comment = "should prepare global outdated package", snapshot = false, continue-on-failure = true },
{ argv = ["vp", "install", "-g", "testnpm2@1.0.0"], comment = "should prepare a version-pinned global package", snapshot = false, continue-on-failure = true },
{ argv = ["vp", "outdated", "definitely-not-installed-vite-plus-snap-pkg", "-g", "--format", "json"], comment = "should support empty global json output", continue-on-failure = true },
{ argv = ["vp", "outdated", "testnpm2", "-g", "--format", "json"], comment = "should report a pinned package without a wanted update", continue-on-failure = true },
{ argv = ["vp", "outdated", "testnpm2", "-g", "--format", "list"], comment = "should render the newer latest as a hint in list format", continue-on-failure = true },
{ tty = false, argv = ["vp", "update", "-g"], comment = "should not move a pinned package to latest", continue-on-failure = true },
{ argv = ["vpt", "json-edit", "$VP_HOME/packages/testnpm2.json", "versionSpec", "no-such-tag"], comment = "should warn and skip when the recorded version spec no longer resolves", snapshot = false },
{ tty = false, argv = ["vp", "update", "-g"], continue-on-failure = true },
{ argv = ["vpt", "json-edit", "$VP_HOME/packages/testnpm2.json", "versionSpec", "null"], comment = "should follow latest again once the recorded version spec is cleared", snapshot = false },
{ argv = ["vp", "outdated", "testnpm2", "-g", "--format", "json"], comment = "should support global json output", continue-on-failure = true },
{ argv = ["vp", "outdated", "testnpm2", "-g", "--format", "list", "--concurrency", "5"], comment = "should support global list output", continue-on-failure = true },
{ argv = ["vpt", "json-edit", "$VP_HOME/packages/testnpm2.json", "versionSpec", "1.0.0"], comment = "should override a recorded version spec with --latest", snapshot = false },
{ tty = false, argv = ["vp", "update", "-g", "--latest"], continue-on-failure = true },
{ argv = ["vpt", "grep-file", "$VP_HOME/packages/testnpm2.json", "versionSpec"], comment = "should clear the recorded version spec after --latest (grep-file prints missing)", continue-on-failure = true },
{ argv = ["vpt", "json-edit", "$VP_HOME/packages/testnpm2.json", "versionSpec", "1.0.1"], comment = "should clear a recorded version spec with --latest even without a reinstall", snapshot = false },
{ tty = false, argv = ["vp", "update", "-g", "--latest"], continue-on-failure = true },
{ argv = ["vpt", "grep-file", "$VP_HOME/packages/testnpm2.json", "versionSpec"], comment = "should have removed the pin from the up-to-date package (grep-file prints missing)", continue-on-failure = true },
{ argv = ["vpt", "json-edit", "$VP_HOME/packages/testnpm2.json", "versionSpec", "1.0.0"], comment = "should persist an explicit spec switch without a reinstall", snapshot = false },
{ tty = false, argv = ["vp", "update", "-g", "testnpm2@1.0.1"], continue-on-failure = true },
{ argv = ["vpt", "grep-file", "$VP_HOME/packages/testnpm2.json", 'versionSpec": "1.0.1'], continue-on-failure = true },
{ tty = false, argv = ["vp", "update", "-g", "testnpm2@no-such-tag"], comment = "should not persist an explicit spec that fails to resolve", continue-on-failure = true },
{ argv = ["vpt", "grep-file", "$VP_HOME/packages/testnpm2.json", 'versionSpec": "1.0.1'], continue-on-failure = true },
]
after = [
{ argv = ["vp", "remove", "-g", "testnpm2"], continue-on-failure = true },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## `vp install -g testnpm2@1.0.0`

should prepare global outdated package
should prepare a version-pinned global package


## `vp outdated definitely-not-installed-vite-plus-snap-pkg -g --format json`
Expand All @@ -13,6 +13,62 @@ should support empty global json output
{}
```

## `vp outdated testnpm2 -g --format json`

should report a pinned package without a wanted update

**Exit code:** 1

```
{
"testnpm2": {
"current": "1.0.0",
"wanted": "1.0.0",
"latest": "1.0.1",
"dependent": "global",
"location": "<home>/.vite-plus/packages/testnpm2/<uuid>/lib/node_modules/testnpm2"
}
}
```

## `vp outdated testnpm2 -g --format list`

should render the newer latest as a hint in list format

**Exit code:** 1

```
testnpm2 (global)
1.0.0 => 1.0.0 (latest: 1.0.1)
```

## `vp update -g`

should not move a pinned package to latest

```
All global packages are up to date.
```

## `vpt json-edit $VP_HOME/packages/testnpm2.json versionSpec no-such-tag`

should warn and skip when the recorded version spec no longer resolves


## `vp update -g`

**Exit code:** 1

```
All global packages are up to date.
warn: npm view failed for testnpm2@no-such-tag: npm error code E404; skipping
```

## `vpt json-edit $VP_HOME/packages/testnpm2.json versionSpec null`

should follow latest again once the recorded version spec is cleared


## `vp outdated testnpm2 -g --format json`

should support global json output
Expand Down Expand Up @@ -41,3 +97,82 @@ should support global list output
testnpm2 (global)
1.0.0 => 1.0.1
```

## `vpt json-edit $VP_HOME/packages/testnpm2.json versionSpec 1.0.0`

should override a recorded version spec with --latest


## `vp update -g --latest`

```
info: Updating 1 global package with Node.js <version>
✓ Updated testnpm2 to 1.0.1
```

## `vpt grep-file $VP_HOME/packages/testnpm2.json versionSpec`

should clear the recorded version spec after --latest (grep-file prints missing)

**Exit code:** 1

```
<home>/.vite-plus/packages/testnpm2.json: missing "versionSpec"
pattern not found
```

## `vpt json-edit $VP_HOME/packages/testnpm2.json versionSpec 1.0.1`

should clear a recorded version spec with --latest even without a reinstall


## `vp update -g --latest`

```
All global packages are up to date.
```

## `vpt grep-file $VP_HOME/packages/testnpm2.json versionSpec`

should have removed the pin from the up-to-date package (grep-file prints missing)

**Exit code:** 1

```
<home>/.vite-plus/packages/testnpm2.json: missing "versionSpec"
pattern not found
```

## `vpt json-edit $VP_HOME/packages/testnpm2.json versionSpec 1.0.0`

should persist an explicit spec switch without a reinstall


## `vp update -g testnpm2@1.0.1`

```
All global packages are up to date.
```

## `vpt grep-file $VP_HOME/packages/testnpm2.json 'versionSpec": "1.0.1'`

```
<home>/.vite-plus/packages/testnpm2.json: found "versionSpec\": \"1.0.1"
```

## `vp update -g testnpm2@no-such-tag`

should not persist an explicit spec that fails to resolve

**Exit code:** 1

```
All global packages are up to date.
warn: npm view failed for testnpm2@no-such-tag: npm error code E404; skipping
```

## `vpt grep-file $VP_HOME/packages/testnpm2.json 'versionSpec": "1.0.1'`

```
<home>/.vite-plus/packages/testnpm2.json: found "versionSpec\": \"1.0.1"
```
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,19 @@ steps = [
after = [
{ tty = false, argv = ["vp", "remove", "-g", "testnpm2"], continue-on-failure = true },
]

[[case]]
name = "command_update_node_mismatch_install_failure"
vp = "global"
local-registry = true
skip-platforms = ["windows"]
steps = [
{ tty = false, argv = ["vp", "install", "-g", "--node", "20", "semver@7.7.2"], snapshot = false },
{ argv = ["vpt", "json-edit", "$VP_HOME/bins/semver.json", "package", "conflicting-package"], snapshot = false },
{ tty = false, argv = ["vp", "update", "-g", "semver@=7.7.2", "--reinstall-node-mismatch"], comment = "should keep the recorded spec when the reinstall fails", continue-on-failure = true },
{ argv = ["vpt", "grep-file", "$VP_HOME/packages/semver.json", 'versionSpec": "7.7.2'], comment = "should keep metadata from the successful install", continue-on-failure = true },
]
after = [
{ argv = ["vpt", "json-edit", "$VP_HOME/bins/semver.json", "package", "semver"], continue-on-failure = true },
{ tty = false, argv = ["vp", "remove", "-g", "semver"], continue-on-failure = true },
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# command_update_node_mismatch_install_failure

## `vp install -g --node 20 semver@7.7.2`


## `vpt json-edit $VP_HOME/bins/semver.json package conflicting-package`


## `vp update -g semver@=7.7.2 --reinstall-node-mismatch`

should keep the recorded spec when the reinstall fails

**Exit code:** 1

```
info: Updating 1 global package with Node.js <version>
error: Failed to update semver: Executable 'semver' is already installed by conflicting-package

Please remove conflicting-package before installing semver, or use --force to auto-replace
```

## `vpt grep-file $VP_HOME/packages/semver.json 'versionSpec": "7.7.2'`

should keep metadata from the successful install

```
<home>/.vite-plus/packages/semver.json: found "versionSpec\": \"7.7.2"
```
88 changes: 77 additions & 11 deletions crates/vite_global_cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,7 @@ async fn run_package_manager_command(
}
Some(ManagedGlobalCommand::Update {
packages,
latest,
concurrency,
reinstall_node_mismatch,
ignore_node_mismatch,
Expand All @@ -582,6 +583,7 @@ async fn run_package_manager_command(
}
return managed_update(
packages,
latest,
concurrency,
reinstall_node_mismatch,
ignore_node_mismatch,
Expand Down Expand Up @@ -664,13 +666,22 @@ struct NodeMismatchPackage {

async fn managed_update(
packages: &[String],
latest: bool,
concurrency: Option<usize>,
reinstall_node_mismatch: bool,
ignore_node_mismatch: bool,
) -> Result<ExitStatus, Error> {
let concurrency = concurrency.unwrap_or(DEFAULT_GLOBAL_INSTALL_CONCURRENCY);
let mut to_update: Vec<String> = Vec::new();
let mut node_mismatches: Vec<NodeMismatchPackage> = Vec::new();
// Recorded version-spec changes this update implies: `--latest` clears
// specs, an explicit `pkg@spec` argument replaces the stored one.
// Reinstalls record the new spec on their own, but packages already at
// the target version are not reinstalled and must be rewritten here.
// Entries are `(package name, new spec, registry query spec)`; the query
// spec ties each rewrite to its lookup so failed resolutions never
// persist a policy the update could not act on.
let mut spec_rewrites: Vec<(String, Option<String>, String)> = Vec::new();
let current_node_version;

let packages = if packages.is_empty() {
Expand All @@ -682,10 +693,13 @@ async fn managed_update(
current_node_version = get_current_node_version().await?;

for metadata in &all {
if latest && metadata.version_spec.is_some() {
spec_rewrites.push((metadata.name.clone(), None, metadata.name.clone()));
}
if !is_same_node_version(&metadata.platform.node, &current_node_version) {
node_mismatches.push(NodeMismatchPackage {
name: metadata.name.clone(),
spec: metadata.name.clone(),
spec: if latest { metadata.name.clone() } else { metadata.update_spec() },
installed_node: metadata.platform.node.clone(),
});
}
Expand All @@ -704,12 +718,32 @@ async fn managed_update(
}

// It is not a local package, so `parse_package_spec` there won't return `Err()`
let (package_name, _) = global::parse_package_spec(package).unwrap();
let (package_name, version_spec) = global::parse_package_spec(package).unwrap();
if let Some(metadata) = PackageMetadata::load(&package_name).await? {
if version_spec.is_some() {
// An explicit spec replaces the recorded one even when
// the installed version already satisfies it.
let new_spec = global::update_version_spec(package);
if new_spec != metadata.version_spec {
spec_rewrites.push((package_name.clone(), new_spec, package.clone()));
}
} else if latest && metadata.version_spec.is_some() {
// `--latest` applies to bare names only; explicit specs win.
spec_rewrites.push((package_name.clone(), None, package_name.clone()));
}
if !is_same_node_version(&metadata.platform.node, &current_node_version) {
// Match the spec `get_outdated_packages` resolves for this
// package, so the dedup against outdated results holds.
let spec = if version_spec.is_some() {
package.clone()
} else if latest {
package_name.clone()
} else {
metadata.update_spec()
};
node_mismatches.push(NodeMismatchPackage {
name: package_name,
spec: package.clone(),
spec,
installed_node: metadata.platform.node,
});
}
Expand All @@ -722,16 +756,31 @@ async fn managed_update(
Some(managed_specs)
};

let outdated = global::outdated::get_outdated_packages(
let report = global::outdated::get_outdated_packages(
&packages.unwrap_or_default(),
concurrency * 3,
true,
latest,
global::outdated::LookupMode::WantedOnly,
)
.await?;
to_update.extend(outdated.into_iter().map(|package| package.spec.unwrap_or(package.name)));

let to_update_set = to_update.iter().map(String::as_str).collect::<HashSet<_>>();
node_mismatches.retain(|package| !to_update_set.contains(package.spec.as_str()));
for (_, message) in &report.failures {
output::warn(&format!("{message}; skipping"));
}
// Skipped lookups make the update incomplete; keep going but exit
// nonzero so scripts can tell.
let incomplete = !report.failures.is_empty();
to_update.extend(
report
.outdated
.into_iter()
// A newer `latest` alone (e.g. a version-pinned package) is not
// updatable; only a newer wanted version is.
.filter(|package| package.wanted != package.current)
.map(|package| package.spec.unwrap_or(package.name)),
Comment thread
TheAlexLichter marked this conversation as resolved.
);

let outdated_specs = to_update.iter().map(String::as_str).collect::<HashSet<_>>();
node_mismatches.retain(|package| !outdated_specs.contains(package.spec.as_str()));

if should_reinstall_node_mismatches(
&node_mismatches,
Expand All @@ -742,9 +791,26 @@ async fn managed_update(
to_update.extend(node_mismatches.into_iter().map(|package| package.spec));
}

// Installs save the new spec only after they succeed.
let to_update_set = to_update.iter().map(String::as_str).collect::<HashSet<_>>();
let failed_specs =
report.failures.iter().map(|(spec, _)| spec.as_str()).collect::<HashSet<_>>();
for (package_name, new_spec, query_spec) in &spec_rewrites {
Comment thread
fengmk2 marked this conversation as resolved.
if failed_specs.contains(query_spec.as_str()) || to_update_set.contains(query_spec.as_str())
{
continue;
}
if let Some(mut metadata) = PackageMetadata::load(package_name).await?
&& metadata.version_spec != *new_spec
{
metadata.version_spec = new_spec.clone();
metadata.save().await?;
}
}

if to_update.is_empty() {
vite_shared::output::raw("All global packages are up to date.");
return Ok(ExitStatus::default());
return Ok(if incomplete { exit_status(1) } else { ExitStatus::default() });
}

// Call reinstall logic
Expand All @@ -766,7 +832,7 @@ async fn managed_update(
));
return Ok(exit_status(1));
}
Ok(ExitStatus::default())
Ok(if incomplete { exit_status(1) } else { ExitStatus::default() })
}

async fn get_current_node_version() -> Result<String, Error> {
Expand Down
Loading
Loading