Skip to content

Commit f0ae621

Browse files
liangmiQwQfengmk2
andauthored
feat(cli): spawn npm for global installation in parallel and refine output (#1597)
Close #1476 This PR introduced parallel installation to for global packages installation and updates to improve installation speed. It used `futures::stream::FuturesUnordered` to manage parallel tasks, it also added `--concurrency` option (5 as default) for `vp add`, `vp install`, `vp update` with `--global`. In order to suit the parallel installation, this PR modified output format for these commands, with a progress bar and more format on the final results. It’s a pretty big change, but since it’s tightly coupled with the parallel logic, I’m not sure if it belongs in a separate PR. https://github.com/user-attachments/assets/ed2f8082-9d76-48f2-a3c4-f4fcab0b910c 🤖 Generated with OpenAI Codex --------- Co-authored-by: MK (fengmk2) <fengmk2@gmail.com>
1 parent 65f850b commit f0ae621

36 files changed

Lines changed: 709 additions & 393 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/vite_global_cli/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ chrono = { workspace = true }
1616
clap = { workspace = true, features = ["derive"] }
1717
clap_complete = { workspace = true, features = ["unstable-dynamic"] }
1818
directories = { workspace = true }
19+
futures = { workspace = true }
1920
serde = { workspace = true }
2021
serde_json = { workspace = true }
2122
node-semver = { workspace = true }
@@ -25,6 +26,8 @@ tracing = { workspace = true }
2526
owo-colors = { workspace = true }
2627
oxc_resolver = { workspace = true }
2728
crossterm = { workspace = true }
29+
indexmap = { workspace = true }
30+
indicatif = { workspace = true }
2831
vite_error = { workspace = true }
2932
vite_install = { workspace = true }
3033
vite_js_runtime = { workspace = true }

crates/vite_global_cli/src/cli.rs

Lines changed: 66 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,20 @@ use std::{ffi::OsStr, process::ExitStatus};
88
use clap::{CommandFactory, FromArgMatches, Parser, Subcommand};
99
use clap_complete::ArgValueCompleter;
1010
use tokio::runtime::Runtime;
11-
use vite_path::{AbsolutePath, AbsolutePathBuf};
11+
use vite_path::AbsolutePathBuf;
1212
use vite_pm_cli::PackageManagerCommand;
13+
use vite_shared::output;
1314

14-
use crate::{commands, error::Error, help};
15+
use crate::{
16+
commands::{
17+
self,
18+
env::{global_install, package_metadata::PackageMetadata},
19+
},
20+
error::Error,
21+
help,
22+
};
23+
24+
const DEFAULT_GLOBAL_INSTALL_CONCURRENCY: usize = 5;
1525

1626
#[derive(Clone, Copy, Debug)]
1727
pub struct RenderOptions {
@@ -532,19 +542,24 @@ async fn run_package_manager_command(
532542
) -> Result<ExitStatus, Error> {
533543
match command {
534544
PackageManagerCommand::Install {
535-
global: true, packages: Some(pkgs), node, force, ..
536-
} if !pkgs.is_empty() => managed_install(&pkgs, node.as_deref(), force).await,
545+
global: true,
546+
packages: Some(pkgs),
547+
node,
548+
force,
549+
concurrency,
550+
..
551+
} if !pkgs.is_empty() => managed_install(&pkgs, node.as_deref(), force, concurrency).await,
537552

538-
PackageManagerCommand::Add { global: true, ref packages, ref node, .. } => {
539-
managed_install(packages, node.as_deref(), false).await
540-
}
553+
PackageManagerCommand::Add {
554+
global: true, ref packages, ref node, concurrency, ..
555+
} => managed_install(packages, node.as_deref(), false, concurrency).await,
541556

542557
PackageManagerCommand::Remove { global: true, ref packages, dry_run, .. } => {
543558
managed_uninstall(packages, dry_run).await
544559
}
545560

546-
PackageManagerCommand::Update { global: true, ref packages, .. } => {
547-
managed_update(&cwd, packages).await
561+
PackageManagerCommand::Update { global: true, ref packages, concurrency, .. } => {
562+
managed_update(packages, concurrency).await
548563
}
549564

550565
// `pm list -g` lists vite-plus-managed globals, not the underlying PM's.
@@ -562,20 +577,28 @@ async fn run_package_manager_command(
562577
}
563578
}
564579

565-
// snap-test fixtures expect bare lines (no "error:"/"info:" prefix), so
566-
// these helpers use `output::raw_stderr`/`output::raw` rather than the
567-
// prefixed `output::error`/`output::info`.
568580
async fn managed_install(
569581
packages: &[String],
570582
node: Option<&str>,
571583
force: bool,
584+
concurrency: Option<usize>,
572585
) -> Result<ExitStatus, Error> {
573-
for package in packages {
574-
if let Err(e) = crate::commands::env::global_install::install(package, node, force).await {
575-
vite_shared::output::raw_stderr(&format!("Failed to install {package}: {e}"));
576-
return Ok(exit_status(1));
577-
}
586+
if let Err((package_name, error)) = crate::commands::env::global_install::install(
587+
packages,
588+
node,
589+
force,
590+
concurrency.unwrap_or(DEFAULT_GLOBAL_INSTALL_CONCURRENCY),
591+
false,
592+
)
593+
.await
594+
{
595+
output::error(&format!(
596+
"Failed to install {}: {error}",
597+
package_name.as_deref().unwrap_or("global packages")
598+
));
599+
return Ok(exit_status(1));
578600
}
601+
579602
Ok(ExitStatus::default())
580603
}
581604

@@ -589,21 +612,14 @@ async fn managed_uninstall(packages: &[String], dry_run: bool) -> Result<ExitSta
589612
Ok(ExitStatus::default())
590613
}
591614

592-
fn is_global_package_up_to_date(
593-
installed_version: &str,
594-
registry_version: &str,
595-
installed_node_version: &str,
596-
target_node_version: &str,
597-
) -> bool {
615+
fn is_global_package_up_to_date(installed_version: &str, registry_version: &str) -> bool {
598616
installed_version.trim() == registry_version.trim()
599-
&& installed_node_version.trim() == target_node_version.trim()
600617
}
601618

602-
async fn managed_update(cwd: &AbsolutePath, packages: &[String]) -> Result<ExitStatus, Error> {
603-
use crate::commands::env::{
604-
config::resolve_version, global_install, package_metadata::PackageMetadata,
605-
};
606-
619+
async fn managed_update(
620+
packages: &[String],
621+
concurrency: Option<usize>,
622+
) -> Result<ExitStatus, Error> {
607623
let all_packages = if packages.is_empty() {
608624
let all = PackageMetadata::list_all().await?;
609625
if all.is_empty() {
@@ -615,27 +631,14 @@ async fn managed_update(cwd: &AbsolutePath, packages: &[String]) -> Result<ExitS
615631
None
616632
};
617633

618-
let target_node_version = resolve_version(cwd).await?.version;
619634
let mut to_update: Vec<String> = Vec::new();
620635
let mut skipped = 0usize;
621636

622637
if let Some(all) = all_packages {
623638
for metadata in all {
624-
if metadata.platform.node.trim() != target_node_version.trim() {
625-
to_update.push(metadata.name.clone());
626-
continue;
627-
}
628-
629-
match global_install::latest_package_version(&metadata.name, Some(&target_node_version))
630-
.await
631-
{
639+
match global_install::latest_package_version(&metadata.name).await {
632640
Ok(latest_version)
633-
if is_global_package_up_to_date(
634-
&metadata.version,
635-
&latest_version,
636-
&metadata.platform.node,
637-
&target_node_version,
638-
) =>
641+
if is_global_package_up_to_date(&metadata.version, &latest_version) =>
639642
{
640643
vite_shared::output::raw(&format!(
641644
"{} is already up to date (v{}).",
@@ -662,21 +665,9 @@ async fn managed_update(cwd: &AbsolutePath, packages: &[String]) -> Result<ExitS
662665

663666
let (package_name, _) = global_install::parse_package_spec(package);
664667
if let Some(metadata) = PackageMetadata::load(&package_name).await? {
665-
if metadata.platform.node.trim() != target_node_version.trim() {
666-
to_update.push(package.clone());
667-
continue;
668-
}
669-
670-
match global_install::latest_package_version(package, Some(&target_node_version))
671-
.await
672-
{
668+
match global_install::latest_package_version(package).await {
673669
Ok(latest_version)
674-
if is_global_package_up_to_date(
675-
&metadata.version,
676-
&latest_version,
677-
&metadata.platform.node,
678-
&target_node_version,
679-
) =>
670+
if is_global_package_up_to_date(&metadata.version, &latest_version) =>
680671
{
681672
vite_shared::output::raw(&format!(
682673
"{} is already up to date (v{}).",
@@ -704,11 +695,21 @@ async fn managed_update(cwd: &AbsolutePath, packages: &[String]) -> Result<ExitS
704695
return Ok(ExitStatus::default());
705696
}
706697

707-
for package in &to_update {
708-
if let Err(e) = global_install::install(package, Some(&target_node_version), false).await {
709-
vite_shared::output::raw_stderr(&format!("Failed to update {package}: {e}"));
710-
return Ok(exit_status(1));
711-
}
698+
// Call reinstall logic
699+
if let Err((package_name, error)) = global_install::install(
700+
&to_update,
701+
None,
702+
false,
703+
concurrency.unwrap_or(DEFAULT_GLOBAL_INSTALL_CONCURRENCY),
704+
true,
705+
)
706+
.await
707+
{
708+
output::error(&format!(
709+
"Failed to update {}: {error}",
710+
package_name.as_deref().unwrap_or("global packages")
711+
));
712+
return Ok(exit_status(1));
712713
}
713714
Ok(ExitStatus::default())
714715
}
@@ -961,17 +962,12 @@ mod tests {
961962

962963
#[test]
963964
fn skips_global_update_when_registry_and_node_versions_match() {
964-
assert!(is_global_package_up_to_date("5.9.3", "5.9.3", "20.18.0", "20.18.0"));
965+
assert!(is_global_package_up_to_date("5.9.3", "5.9.3"));
965966
}
966967

967968
#[test]
968969
fn updates_global_package_when_registry_version_differs_from_installed_version() {
969-
assert!(!is_global_package_up_to_date("5.9.2", "5.9.3", "20.18.0", "20.18.0"));
970-
}
971-
972-
#[test]
973-
fn updates_global_package_when_node_version_differs_from_target_version() {
974-
assert!(!is_global_package_up_to_date("5.9.3", "5.9.3", "20.18.0", "24.15.0"));
970+
assert!(!is_global_package_up_to_date("5.9.2", "5.9.3"));
975971
}
976972

977973
#[test]

0 commit comments

Comments
 (0)