From 641db235583d118ec0ee189dc9064c18d04c0284 Mon Sep 17 00:00:00 2001 From: yii Date: Tue, 7 Jul 2026 14:46:41 +0800 Subject: [PATCH 1/6] feat(pm): add ci subcommand --- .../cli_helper_message/snapshots.toml | 1 + .../snapshots/cli_helper_message.md | 21 +++ crates/vite_install/src/commands/ci.rs | 135 ++++++++++++++++++ crates/vite_install/src/commands/mod.rs | 1 + crates/vite_pm_cli/src/cli.rs | 25 ++++ crates/vite_pm_cli/src/handlers.rs | 7 + 6 files changed, 190 insertions(+) create mode 100644 crates/vite_install/src/commands/ci.rs diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cli_helper_message/snapshots.toml b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cli_helper_message/snapshots.toml index e5ec623e3f..1497a40d28 100644 --- a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cli_helper_message/snapshots.toml +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cli_helper_message/snapshots.toml @@ -15,6 +15,7 @@ steps = [ { argv = ["vp", "why", "-h"], comment = "show why help message" }, { argv = ["vp", "info", "-h"], comment = "show info help message" }, { argv = ["vp", "pm", "-h"], comment = "show pm help message" }, + { argv = ["vp", "pm", "ci", "-h"], comment = "show pm ci help message" }, { argv = ["vp", "env"], comment = "show env help message" }, { argv = ["vp", "upgrade", "-h"], comment = "show upgrade help message" }, ] diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cli_helper_message/snapshots/cli_helper_message.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cli_helper_message/snapshots/cli_helper_message.md index 7a51254a8e..ea58921e1a 100644 --- a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cli_helper_message/snapshots/cli_helper_message.md +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cli_helper_message/snapshots/cli_helper_message.md @@ -396,6 +396,7 @@ Usage: vp pm Forward a command to the package manager Commands: + ci Clean install dependencies for CI environments approve-builds Approve dependency lifecycle scripts (install/postinstall) to run prune Remove unnecessary packages pack Create a tarball of the package @@ -424,6 +425,26 @@ Options: Documentation: https://viteplus.dev/guide/install ``` +## `vp pm ci -h` + +show pm ci help message + +``` +VITE+ - The Unified Toolchain for the Web + +Usage: vp pm ci [-- ...] + +Clean install dependencies for CI environments + +Arguments: + [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager + +Options: + -h, --help Print help + +Documentation: https://viteplus.dev/guide/install +``` + ## `vp env` show env help message diff --git a/crates/vite_install/src/commands/ci.rs b/crates/vite_install/src/commands/ci.rs new file mode 100644 index 0000000000..f19fb130c8 --- /dev/null +++ b/crates/vite_install/src/commands/ci.rs @@ -0,0 +1,135 @@ +use std::{collections::HashMap, process::ExitStatus}; + +use vite_command::run_command; +use vite_error::Error; +use vite_path::AbsolutePath; + +use crate::package_manager::{ + PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, +}; + +/// Options for the ci command. +#[derive(Debug, Default)] +pub struct CiCommandOptions<'a> { + pub pass_through_args: Option<&'a [String]>, +} + +impl PackageManager { + /// Run the ci command with the package manager. + #[must_use] + pub async fn run_ci_command( + &self, + options: &CiCommandOptions<'_>, + cwd: impl AsRef, + ) -> Result { + let resolve_command = self.resolve_ci_command(options); + run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) + .await + } + + /// Resolve the ci command. + #[must_use] + pub fn resolve_ci_command(&self, options: &CiCommandOptions<'_>) -> ResolveCommandResult { + let bin_name: String; + let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); + let mut args: Vec = Vec::new(); + + match self.client { + PackageManagerType::Npm => { + bin_name = "npm".into(); + args.push("ci".into()); + } + PackageManagerType::Pnpm => { + bin_name = "pnpm".into(); + args.push("ci".into()); + } + PackageManagerType::Bun => { + bin_name = "bun".into(); + args.push("ci".into()); + } + PackageManagerType::Yarn => { + bin_name = "yarn".into(); + args.push("install".into()); + args.push("--immutable".into()); + } + } + + if let Some(pass_through_args) = options.pass_through_args { + args.extend_from_slice(pass_through_args); + } + + ResolveCommandResult { bin_path: bin_name, args, envs } + } +} + +#[cfg(test)] +mod tests { + use tempfile::{TempDir, tempdir}; + use vite_path::AbsolutePathBuf; + use vite_str::Str; + + use super::*; + + fn create_temp_dir() -> TempDir { + tempdir().expect("Failed to create temp directory") + } + + fn create_mock_package_manager(pm_type: PackageManagerType, version: &str) -> PackageManager { + let temp_dir = create_temp_dir(); + let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let install_dir = temp_dir_path.join("install"); + + PackageManager { + client: pm_type, + package_name: pm_type.to_string().into(), + version: Str::from(version), + hash: None, + bin_name: pm_type.to_string().into(), + workspace_root: temp_dir_path.clone(), + is_monorepo: false, + install_dir, + } + } + + #[test] + fn test_npm_ci() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm.resolve_ci_command(&CiCommandOptions::default()); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["ci"]); + } + + #[test] + fn test_pnpm_ci() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "11.0.0"); + let result = pm.resolve_ci_command(&CiCommandOptions::default()); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["ci"]); + } + + #[test] + fn test_bun_ci() { + let pm = create_mock_package_manager(PackageManagerType::Bun, "1.3.11"); + let result = pm.resolve_ci_command(&CiCommandOptions::default()); + assert_eq!(result.bin_path, "bun"); + assert_eq!(result.args, vec!["ci"]); + } + + #[test] + fn test_yarn_ci_uses_immutable_install() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); + let result = pm.resolve_ci_command(&CiCommandOptions::default()); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["install", "--immutable"]); + } + + #[test] + fn test_ci_pass_through_args() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "11.0.0"); + let pass_through = vec!["--ignore-scripts".to_string()]; + let result = + pm.resolve_ci_command(&CiCommandOptions { pass_through_args: Some(&pass_through) }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["ci", "--ignore-scripts"]); + } +} diff --git a/crates/vite_install/src/commands/mod.rs b/crates/vite_install/src/commands/mod.rs index e59115a3f0..bd70617d1c 100644 --- a/crates/vite_install/src/commands/mod.rs +++ b/crates/vite_install/src/commands/mod.rs @@ -2,6 +2,7 @@ pub mod add; pub mod approve_builds; pub mod audit; pub mod cache; +pub mod ci; pub mod config; pub mod dedupe; pub mod deprecate; diff --git a/crates/vite_pm_cli/src/cli.rs b/crates/vite_pm_cli/src/cli.rs index 2e2c94ee86..d690b06120 100644 --- a/crates/vite_pm_cli/src/cli.rs +++ b/crates/vite_pm_cli/src/cli.rs @@ -562,6 +562,13 @@ impl PackageManagerCommand { /// Package manager subcommands (`vp pm `). #[derive(Subcommand, Debug, Clone)] pub enum PmCommands { + /// Clean install dependencies for CI environments + Ci { + /// Additional arguments to pass through to the package manager + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + /// Approve dependency lifecycle scripts (install/postinstall) to run #[command(name = "approve-builds")] ApproveBuilds { @@ -1372,6 +1379,24 @@ mod tests { assert_eq!(error.kind(), clap::error::ErrorKind::ArgumentConflict); } + #[test] + fn ci_parses() { + let command = parse_pm_command(&["vp", "pm", "ci"]).expect("ci should parse"); + + assert!(matches!(command, PackageManagerCommand::Pm(PmCommands::Ci { .. }))); + } + + #[test] + fn ci_pass_through_args_capture() { + let command = parse_pm_command(&["vp", "pm", "ci", "--", "--ignore-scripts"]) + .expect("pass-through args should parse"); + + let PackageManagerCommand::Pm(PmCommands::Ci { pass_through_args }) = command else { + panic!("expected Ci variant"); + }; + assert_eq!(pass_through_args, Some(vec!["--ignore-scripts".to_string()])); + } + #[test] fn approve_builds_all_conflicts_with_deny_packages() { let error = parse_pm_command(&["vp", "pm", "approve-builds", "--all", "!core-js"]) diff --git a/crates/vite_pm_cli/src/handlers.rs b/crates/vite_pm_cli/src/handlers.rs index 1670c5ec4d..f481d4341a 100644 --- a/crates/vite_pm_cli/src/handlers.rs +++ b/crates/vite_pm_cli/src/handlers.rs @@ -11,6 +11,7 @@ use vite_install::{ approve_builds::ApproveBuildsCommandOptions, audit::AuditCommandOptions, cache::CacheCommandOptions, + ci::CiCommandOptions, config::ConfigCommandOptions, dedupe::DedupeCommandOptions, deprecate::DeprecateCommandOptions, @@ -172,6 +173,7 @@ pub async fn run_pm_subcommand( let needs_project = matches!( command, PmCommands::ApproveBuilds { .. } + | PmCommands::Ci { .. } | PmCommands::Prune { .. } | PmCommands::Pack { .. } | PmCommands::List { .. } @@ -189,6 +191,11 @@ pub async fn run_pm_subcommand( }; match command { + PmCommands::Ci { pass_through_args } => { + let options = CiCommandOptions { pass_through_args: pass_through_args.as_deref() }; + Ok(pm.run_ci_command(&options, cwd).await?) + } + PmCommands::ApproveBuilds { packages, all, pass_through_args } => { let options = ApproveBuildsCommandOptions { packages: &packages, From f400017c86ed70fc6cb070c86b5ecd4fd35d77be Mon Sep 17 00:00:00 2001 From: Yii Date: Tue, 7 Jul 2026 17:04:14 +0800 Subject: [PATCH 2/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Yii --- crates/vite_install/src/commands/ci.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/vite_install/src/commands/ci.rs b/crates/vite_install/src/commands/ci.rs index f19fb130c8..d4989570b1 100644 --- a/crates/vite_install/src/commands/ci.rs +++ b/crates/vite_install/src/commands/ci.rs @@ -50,7 +50,11 @@ impl PackageManager { PackageManagerType::Yarn => { bin_name = "yarn".into(); args.push("install".into()); - args.push("--immutable".into()); + if self.is_yarn_berry() { + args.push("--immutable".into()); + } else { + args.push("--frozen-lockfile".into()); + } } } From 4fee930e7953559e822690841b0210e186f1fa20 Mon Sep 17 00:00:00 2001 From: yii Date: Tue, 7 Jul 2026 17:23:41 +0800 Subject: [PATCH 3/6] fix(pm): match pnpm ci semantics --- crates/vite_install/src/commands/ci.rs | 113 ++++++++++++++++--------- 1 file changed, 74 insertions(+), 39 deletions(-) diff --git a/crates/vite_install/src/commands/ci.rs b/crates/vite_install/src/commands/ci.rs index d4989570b1..f13c5e7d9c 100644 --- a/crates/vite_install/src/commands/ci.rs +++ b/crates/vite_install/src/commands/ci.rs @@ -22,47 +22,64 @@ impl PackageManager { options: &CiCommandOptions<'_>, cwd: impl AsRef, ) -> Result { - let resolve_command = self.resolve_ci_command(options); - run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) - .await + let cwd = cwd.as_ref(); + + let mut last_status = None; + for resolve_command in self.resolve_ci_commands(options) { + let status = run_command( + &resolve_command.bin_path, + &resolve_command.args, + &resolve_command.envs, + cwd, + ) + .await?; + + if !status.success() { + return Ok(status); + } + last_status = Some(status); + } + + Ok(last_status.unwrap_or_default()) } /// Resolve the ci command. #[must_use] - pub fn resolve_ci_command(&self, options: &CiCommandOptions<'_>) -> ResolveCommandResult { - let bin_name: String; + pub fn resolve_ci_commands(&self, options: &CiCommandOptions<'_>) -> Vec { let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); - let mut args: Vec = Vec::new(); + let command = |bin_path: &str, mut args: Vec| { + if let Some(pass_through_args) = options.pass_through_args { + args.extend_from_slice(pass_through_args); + } + + ResolveCommandResult { bin_path: bin_path.into(), args, envs: envs.clone() } + }; match self.client { - PackageManagerType::Npm => { - bin_name = "npm".into(); - args.push("ci".into()); - } + PackageManagerType::Npm => vec![command("npm", vec!["ci".into()])], PackageManagerType::Pnpm => { - bin_name = "pnpm".into(); - args.push("ci".into()); - } - PackageManagerType::Bun => { - bin_name = "bun".into(); - args.push("ci".into()); + // pnpm documents `ci` as `clean` followed by + // `install --frozen-lockfile`; keep the steps split for parity. + // See https://pnpm.io/cli/ci. + let clean = ResolveCommandResult { + bin_path: "pnpm".into(), + args: vec!["clean".into()], + envs: envs.clone(), + }; + + vec![clean, command("pnpm", vec!["install".into(), "--frozen-lockfile".into()])] } + PackageManagerType::Bun => vec![command("bun", vec!["ci".into()])], PackageManagerType::Yarn => { - bin_name = "yarn".into(); - args.push("install".into()); + let mut args = vec!["install".into()]; if self.is_yarn_berry() { args.push("--immutable".into()); } else { args.push("--frozen-lockfile".into()); } + vec![command("yarn", args)] } } - - if let Some(pass_through_args) = options.pass_through_args { - args.extend_from_slice(pass_through_args); - } - - ResolveCommandResult { bin_path: bin_name, args, envs } } } @@ -98,33 +115,48 @@ mod tests { #[test] fn test_npm_ci() { let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let result = pm.resolve_ci_command(&CiCommandOptions::default()); - assert_eq!(result.bin_path, "npm"); - assert_eq!(result.args, vec!["ci"]); + let result = pm.resolve_ci_commands(&CiCommandOptions::default()); + assert_eq!(result.len(), 1); + assert_eq!(result[0].bin_path, "npm"); + assert_eq!(result[0].args, vec!["ci"]); } #[test] fn test_pnpm_ci() { let pm = create_mock_package_manager(PackageManagerType::Pnpm, "11.0.0"); - let result = pm.resolve_ci_command(&CiCommandOptions::default()); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["ci"]); + let result = pm.resolve_ci_commands(&CiCommandOptions::default()); + assert_eq!(result.len(), 2); + assert_eq!(result[0].bin_path, "pnpm"); + assert_eq!(result[0].args, vec!["clean"]); + assert_eq!(result[1].bin_path, "pnpm"); + assert_eq!(result[1].args, vec!["install", "--frozen-lockfile"]); } #[test] fn test_bun_ci() { let pm = create_mock_package_manager(PackageManagerType::Bun, "1.3.11"); - let result = pm.resolve_ci_command(&CiCommandOptions::default()); - assert_eq!(result.bin_path, "bun"); - assert_eq!(result.args, vec!["ci"]); + let result = pm.resolve_ci_commands(&CiCommandOptions::default()); + assert_eq!(result.len(), 1); + assert_eq!(result[0].bin_path, "bun"); + assert_eq!(result[0].args, vec!["ci"]); } #[test] fn test_yarn_ci_uses_immutable_install() { let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); - let result = pm.resolve_ci_command(&CiCommandOptions::default()); - assert_eq!(result.bin_path, "yarn"); - assert_eq!(result.args, vec!["install", "--immutable"]); + let result = pm.resolve_ci_commands(&CiCommandOptions::default()); + assert_eq!(result.len(), 1); + assert_eq!(result[0].bin_path, "yarn"); + assert_eq!(result[0].args, vec!["install", "--immutable"]); + } + + #[test] + fn test_yarn_classic_ci_uses_frozen_lockfile_install() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.22"); + let result = pm.resolve_ci_commands(&CiCommandOptions::default()); + assert_eq!(result.len(), 1); + assert_eq!(result[0].bin_path, "yarn"); + assert_eq!(result[0].args, vec!["install", "--frozen-lockfile"]); } #[test] @@ -132,8 +164,11 @@ mod tests { let pm = create_mock_package_manager(PackageManagerType::Pnpm, "11.0.0"); let pass_through = vec!["--ignore-scripts".to_string()]; let result = - pm.resolve_ci_command(&CiCommandOptions { pass_through_args: Some(&pass_through) }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["ci", "--ignore-scripts"]); + pm.resolve_ci_commands(&CiCommandOptions { pass_through_args: Some(&pass_through) }); + assert_eq!(result.len(), 2); + assert_eq!(result[0].bin_path, "pnpm"); + assert_eq!(result[0].args, vec!["clean"]); + assert_eq!(result[1].bin_path, "pnpm"); + assert_eq!(result[1].args, vec!["install", "--frozen-lockfile", "--ignore-scripts"]); } } From 408cd040b2f55d345f523f63f774551437ad7a6e Mon Sep 17 00:00:00 2001 From: yii Date: Tue, 7 Jul 2026 17:32:35 +0800 Subject: [PATCH 4/6] fix(pm): share ci command envs --- crates/vite_install/src/commands/ci.rs | 43 +++++++++++++++++++------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/crates/vite_install/src/commands/ci.rs b/crates/vite_install/src/commands/ci.rs index f13c5e7d9c..b3ecb11ffa 100644 --- a/crates/vite_install/src/commands/ci.rs +++ b/crates/vite_install/src/commands/ci.rs @@ -47,29 +47,36 @@ impl PackageManager { #[must_use] pub fn resolve_ci_commands(&self, options: &CiCommandOptions<'_>) -> Vec { let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); - let command = |bin_path: &str, mut args: Vec| { + let command = |bin_path: &str, args: Vec| ResolveCommandResult { + bin_path: bin_path.into(), + args, + envs: envs.clone(), + }; + let command_with_pass_through = |bin_path: &str, mut args: Vec| { if let Some(pass_through_args) = options.pass_through_args { args.extend_from_slice(pass_through_args); } - ResolveCommandResult { bin_path: bin_path.into(), args, envs: envs.clone() } + command(bin_path, args) }; match self.client { - PackageManagerType::Npm => vec![command("npm", vec!["ci".into()])], + PackageManagerType::Npm => vec![command_with_pass_through("npm", vec!["ci".into()])], PackageManagerType::Pnpm => { // pnpm documents `ci` as `clean` followed by // `install --frozen-lockfile`; keep the steps split for parity. // See https://pnpm.io/cli/ci. - let clean = ResolveCommandResult { - bin_path: "pnpm".into(), - args: vec!["clean".into()], - envs: envs.clone(), - }; - - vec![clean, command("pnpm", vec!["install".into(), "--frozen-lockfile".into()])] + let clean = command("pnpm", vec!["clean".into()]); + + vec![ + clean, + command_with_pass_through( + "pnpm", + vec!["install".into(), "--frozen-lockfile".into()], + ), + ] } - PackageManagerType::Bun => vec![command("bun", vec!["ci".into()])], + PackageManagerType::Bun => vec![command_with_pass_through("bun", vec!["ci".into()])], PackageManagerType::Yarn => { let mut args = vec!["install".into()]; if self.is_yarn_berry() { @@ -77,7 +84,7 @@ impl PackageManager { } else { args.push("--frozen-lockfile".into()); } - vec![command("yarn", args)] + vec![command_with_pass_through("yarn", args)] } } } @@ -112,10 +119,17 @@ mod tests { } } + fn assert_path_envs(commands: &[ResolveCommandResult]) { + for command in commands { + assert!(command.envs.contains_key("PATH")); + } + } + #[test] fn test_npm_ci() { let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); let result = pm.resolve_ci_commands(&CiCommandOptions::default()); + assert_path_envs(&result); assert_eq!(result.len(), 1); assert_eq!(result[0].bin_path, "npm"); assert_eq!(result[0].args, vec!["ci"]); @@ -125,6 +139,7 @@ mod tests { fn test_pnpm_ci() { let pm = create_mock_package_manager(PackageManagerType::Pnpm, "11.0.0"); let result = pm.resolve_ci_commands(&CiCommandOptions::default()); + assert_path_envs(&result); assert_eq!(result.len(), 2); assert_eq!(result[0].bin_path, "pnpm"); assert_eq!(result[0].args, vec!["clean"]); @@ -136,6 +151,7 @@ mod tests { fn test_bun_ci() { let pm = create_mock_package_manager(PackageManagerType::Bun, "1.3.11"); let result = pm.resolve_ci_commands(&CiCommandOptions::default()); + assert_path_envs(&result); assert_eq!(result.len(), 1); assert_eq!(result[0].bin_path, "bun"); assert_eq!(result[0].args, vec!["ci"]); @@ -145,6 +161,7 @@ mod tests { fn test_yarn_ci_uses_immutable_install() { let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); let result = pm.resolve_ci_commands(&CiCommandOptions::default()); + assert_path_envs(&result); assert_eq!(result.len(), 1); assert_eq!(result[0].bin_path, "yarn"); assert_eq!(result[0].args, vec!["install", "--immutable"]); @@ -154,6 +171,7 @@ mod tests { fn test_yarn_classic_ci_uses_frozen_lockfile_install() { let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.22"); let result = pm.resolve_ci_commands(&CiCommandOptions::default()); + assert_path_envs(&result); assert_eq!(result.len(), 1); assert_eq!(result[0].bin_path, "yarn"); assert_eq!(result[0].args, vec!["install", "--frozen-lockfile"]); @@ -165,6 +183,7 @@ mod tests { let pass_through = vec!["--ignore-scripts".to_string()]; let result = pm.resolve_ci_commands(&CiCommandOptions { pass_through_args: Some(&pass_through) }); + assert_path_envs(&result); assert_eq!(result.len(), 2); assert_eq!(result[0].bin_path, "pnpm"); assert_eq!(result[0].args, vec!["clean"]); From 3d4aaddca74273e9151653f7339f038cd8acd336 Mon Sep 17 00:00:00 2001 From: yii Date: Thu, 9 Jul 2026 12:37:02 +0800 Subject: [PATCH 5/6] test(pm): snapshot pm ci delegation --- .../cli_snapshots/fixtures/pm_ci/package.json | 4 ++ .../fixtures/pm_ci/scripts/setup-fake-pms.cjs | 38 +++++++++++ .../fixtures/pm_ci/snapshots.toml | 26 ++++++++ .../pm_ci/snapshots/pm_ci_package_managers.md | 64 +++++++++++++++++++ 4 files changed, 132 insertions(+) create mode 100644 crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_ci/package.json create mode 100644 crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_ci/scripts/setup-fake-pms.cjs create mode 100644 crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_ci/snapshots.toml create mode 100644 crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_ci/snapshots/pm_ci_package_managers.md diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_ci/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_ci/package.json new file mode 100644 index 0000000000..00f56075c1 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_ci/package.json @@ -0,0 +1,4 @@ +{ + "name": "pm-ci-snapshot", + "private": true +} diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_ci/scripts/setup-fake-pms.cjs b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_ci/scripts/setup-fake-pms.cjs new file mode 100644 index 0000000000..adaaac242b --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_ci/scripts/setup-fake-pms.cjs @@ -0,0 +1,38 @@ +const { chmodSync, mkdirSync, writeFileSync } = require('node:fs'); +const { join } = require('node:path'); + +const vpHome = process.env.VP_HOME; +if (!vpHome) { + throw new Error('VP_HOME is required'); +} + +const packageManagers = [ + ['pnpm', '11.0.0'], + ['npm', '10.5.0'], + ['yarn', '1.22.22'], + ['yarn', '4.0.0'], + ['bun', '1.2.0'], +]; + +for (const [name, version] of packageManagers) { + const binDir = join(vpHome, 'package_manager', name, version, name, 'bin'); + mkdirSync(binDir, { recursive: true }); + + const fakePm = join(binDir, 'fake-pm.cjs'); + writeFileSync( + fakePm, + `const args = process.argv.slice(2); +console.log(${JSON.stringify(name)} + (args.length ? ' ' + args.join(' ') : '')); +`, + ); + + const unixShim = join(binDir, name); + writeFileSync(unixShim, "#!/usr/bin/env node\nrequire('./fake-pm.cjs');\n"); + chmodSync(unixShim, 0o755); + + writeFileSync(join(binDir, `${name}.cmd`), `@echo off\r\nnode "%~dp0fake-pm.cjs" %*\r\n`); + writeFileSync( + join(binDir, `${name}.ps1`), + 'node "$PSScriptRoot/fake-pm.cjs" @args\nexit $LASTEXITCODE\n', + ); +} diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_ci/snapshots.toml b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_ci/snapshots.toml new file mode 100644 index 0000000000..955c79e899 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_ci/snapshots.toml @@ -0,0 +1,26 @@ +[[case]] +name = "pm_ci_package_managers" +vp = "global" +comment = """ +Covers `vp pm ci` command delegation for each supported package manager. +Fake managed package-manager installs live under VP_HOME, so the case records +the exact argv Vite+ delegates without hitting the network or real installers. +""" +steps = [ + { argv = ["node", "scripts/setup-fake-pms.cjs"], snapshot = false }, + + { argv = ["vpt", "json-edit", "package.json", "packageManager", "pnpm@11.0.0"], snapshot = false }, + { argv = ["vp", "pm", "ci"], comment = "pnpm uses the documented clean + frozen install flow" }, + + { argv = ["vpt", "json-edit", "package.json", "packageManager", "npm@10.5.0"], snapshot = false }, + { argv = ["vp", "pm", "ci"], comment = "npm keeps native ci delegation" }, + + { argv = ["vpt", "json-edit", "package.json", "packageManager", "yarn@1.22.22"], snapshot = false }, + { argv = ["vp", "pm", "ci"], comment = "Yarn Classic uses frozen-lockfile install" }, + + { argv = ["vpt", "json-edit", "package.json", "packageManager", "yarn@4.0.0"], snapshot = false }, + { argv = ["vp", "pm", "ci"], comment = "Yarn Berry uses immutable install" }, + + { argv = ["vpt", "json-edit", "package.json", "packageManager", "bun@1.2.0"], snapshot = false }, + { argv = ["vp", "pm", "ci"], comment = "Bun keeps native ci delegation" }, +] diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_ci/snapshots/pm_ci_package_managers.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_ci/snapshots/pm_ci_package_managers.md new file mode 100644 index 0000000000..303b194627 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_ci/snapshots/pm_ci_package_managers.md @@ -0,0 +1,64 @@ +# pm_ci_package_managers + +Covers `vp pm ci` command delegation for each supported package manager. +Fake managed package-manager installs live under VP_HOME, so the case records +the exact argv Vite+ delegates without hitting the network or real installers. + +## `node scripts/setup-fake-pms.cjs` + + +## `vpt json-edit package.json packageManager pnpm@11.0.0` + + +## `vp pm ci` + +pnpm uses the documented clean + frozen install flow + +``` +pnpm clean +pnpm install --frozen-lockfile +``` + +## `vpt json-edit package.json packageManager npm@10.5.0` + + +## `vp pm ci` + +npm keeps native ci delegation + +``` +npm ci +``` + +## `vpt json-edit package.json packageManager yarn@1.22.22` + + +## `vp pm ci` + +Yarn Classic uses frozen-lockfile install + +``` +yarn install --frozen-lockfile +``` + +## `vpt json-edit package.json packageManager yarn@4.0.0` + + +## `vp pm ci` + +Yarn Berry uses immutable install + +``` +yarn install --immutable +``` + +## `vpt json-edit package.json packageManager bun@1.2.0` + + +## `vp pm ci` + +Bun keeps native ci delegation + +``` +bun ci +``` From 016c289fec74e4964d346e96acca20a3bce8ea1f Mon Sep 17 00:00:00 2001 From: yii Date: Thu, 9 Jul 2026 13:59:27 +0800 Subject: [PATCH 6/6] fix(pm): avoid new ci subcommands --- .../fixtures/pm_ci/snapshots.toml | 6 +- .../pm_ci/snapshots/pm_ci_package_managers.md | 9 +- crates/vite_install/src/commands/ci.rs | 118 ++++++------------ 3 files changed, 45 insertions(+), 88 deletions(-) diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_ci/snapshots.toml b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_ci/snapshots.toml index 955c79e899..7c0ae8a5c3 100644 --- a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_ci/snapshots.toml +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_ci/snapshots.toml @@ -10,10 +10,10 @@ steps = [ { argv = ["node", "scripts/setup-fake-pms.cjs"], snapshot = false }, { argv = ["vpt", "json-edit", "package.json", "packageManager", "pnpm@11.0.0"], snapshot = false }, - { argv = ["vp", "pm", "ci"], comment = "pnpm uses the documented clean + frozen install flow" }, + { argv = ["vp", "pm", "ci"], comment = "pnpm uses frozen-lockfile install" }, { argv = ["vpt", "json-edit", "package.json", "packageManager", "npm@10.5.0"], snapshot = false }, - { argv = ["vp", "pm", "ci"], comment = "npm keeps native ci delegation" }, + { argv = ["vp", "pm", "ci"], comment = "npm keeps native ci delegation because npm has no frozen-lockfile install flag" }, { argv = ["vpt", "json-edit", "package.json", "packageManager", "yarn@1.22.22"], snapshot = false }, { argv = ["vp", "pm", "ci"], comment = "Yarn Classic uses frozen-lockfile install" }, @@ -22,5 +22,5 @@ steps = [ { argv = ["vp", "pm", "ci"], comment = "Yarn Berry uses immutable install" }, { argv = ["vpt", "json-edit", "package.json", "packageManager", "bun@1.2.0"], snapshot = false }, - { argv = ["vp", "pm", "ci"], comment = "Bun keeps native ci delegation" }, + { argv = ["vp", "pm", "ci"], comment = "Bun uses frozen-lockfile install" }, ] diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_ci/snapshots/pm_ci_package_managers.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_ci/snapshots/pm_ci_package_managers.md index 303b194627..5b2726e769 100644 --- a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_ci/snapshots/pm_ci_package_managers.md +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_ci/snapshots/pm_ci_package_managers.md @@ -12,10 +12,9 @@ the exact argv Vite+ delegates without hitting the network or real installers. ## `vp pm ci` -pnpm uses the documented clean + frozen install flow +pnpm uses frozen-lockfile install ``` -pnpm clean pnpm install --frozen-lockfile ``` @@ -24,7 +23,7 @@ pnpm install --frozen-lockfile ## `vp pm ci` -npm keeps native ci delegation +npm keeps native ci delegation because npm has no frozen-lockfile install flag ``` npm ci @@ -57,8 +56,8 @@ yarn install --immutable ## `vp pm ci` -Bun keeps native ci delegation +Bun uses frozen-lockfile install ``` -bun ci +bun install --frozen-lockfile ``` diff --git a/crates/vite_install/src/commands/ci.rs b/crates/vite_install/src/commands/ci.rs index b3ecb11ffa..11bb8f6fcc 100644 --- a/crates/vite_install/src/commands/ci.rs +++ b/crates/vite_install/src/commands/ci.rs @@ -22,61 +22,31 @@ impl PackageManager { options: &CiCommandOptions<'_>, cwd: impl AsRef, ) -> Result { - let cwd = cwd.as_ref(); - - let mut last_status = None; - for resolve_command in self.resolve_ci_commands(options) { - let status = run_command( - &resolve_command.bin_path, - &resolve_command.args, - &resolve_command.envs, - cwd, - ) - .await?; - - if !status.success() { - return Ok(status); - } - last_status = Some(status); - } - - Ok(last_status.unwrap_or_default()) + let resolve_command = self.resolve_ci_command(options); + run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) + .await } /// Resolve the ci command. #[must_use] - pub fn resolve_ci_commands(&self, options: &CiCommandOptions<'_>) -> Vec { + pub fn resolve_ci_command(&self, options: &CiCommandOptions<'_>) -> ResolveCommandResult { let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); - let command = |bin_path: &str, args: Vec| ResolveCommandResult { - bin_path: bin_path.into(), - args, - envs: envs.clone(), - }; - let command_with_pass_through = |bin_path: &str, mut args: Vec| { + let command = |bin_path: &str, mut args: Vec| { if let Some(pass_through_args) = options.pass_through_args { args.extend_from_slice(pass_through_args); } - command(bin_path, args) + ResolveCommandResult { bin_path: bin_path.into(), args, envs: envs.clone() } }; match self.client { - PackageManagerType::Npm => vec![command_with_pass_through("npm", vec!["ci".into()])], + PackageManagerType::Npm => command("npm", vec!["ci".into()]), PackageManagerType::Pnpm => { - // pnpm documents `ci` as `clean` followed by - // `install --frozen-lockfile`; keep the steps split for parity. - // See https://pnpm.io/cli/ci. - let clean = command("pnpm", vec!["clean".into()]); - - vec![ - clean, - command_with_pass_through( - "pnpm", - vec!["install".into(), "--frozen-lockfile".into()], - ), - ] + command("pnpm", vec!["install".into(), "--frozen-lockfile".into()]) + } + PackageManagerType::Bun => { + command("bun", vec!["install".into(), "--frozen-lockfile".into()]) } - PackageManagerType::Bun => vec![command_with_pass_through("bun", vec!["ci".into()])], PackageManagerType::Yarn => { let mut args = vec!["install".into()]; if self.is_yarn_berry() { @@ -84,7 +54,7 @@ impl PackageManager { } else { args.push("--frozen-lockfile".into()); } - vec![command_with_pass_through("yarn", args)] + command("yarn", args) } } } @@ -119,62 +89,53 @@ mod tests { } } - fn assert_path_envs(commands: &[ResolveCommandResult]) { - for command in commands { - assert!(command.envs.contains_key("PATH")); - } + fn assert_path_env(command: &ResolveCommandResult) { + assert!(command.envs.contains_key("PATH")); } #[test] fn test_npm_ci() { let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let result = pm.resolve_ci_commands(&CiCommandOptions::default()); - assert_path_envs(&result); - assert_eq!(result.len(), 1); - assert_eq!(result[0].bin_path, "npm"); - assert_eq!(result[0].args, vec!["ci"]); + let result = pm.resolve_ci_command(&CiCommandOptions::default()); + assert_path_env(&result); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["ci"]); } #[test] fn test_pnpm_ci() { let pm = create_mock_package_manager(PackageManagerType::Pnpm, "11.0.0"); - let result = pm.resolve_ci_commands(&CiCommandOptions::default()); - assert_path_envs(&result); - assert_eq!(result.len(), 2); - assert_eq!(result[0].bin_path, "pnpm"); - assert_eq!(result[0].args, vec!["clean"]); - assert_eq!(result[1].bin_path, "pnpm"); - assert_eq!(result[1].args, vec!["install", "--frozen-lockfile"]); + let result = pm.resolve_ci_command(&CiCommandOptions::default()); + assert_path_env(&result); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["install", "--frozen-lockfile"]); } #[test] fn test_bun_ci() { let pm = create_mock_package_manager(PackageManagerType::Bun, "1.3.11"); - let result = pm.resolve_ci_commands(&CiCommandOptions::default()); - assert_path_envs(&result); - assert_eq!(result.len(), 1); - assert_eq!(result[0].bin_path, "bun"); - assert_eq!(result[0].args, vec!["ci"]); + let result = pm.resolve_ci_command(&CiCommandOptions::default()); + assert_path_env(&result); + assert_eq!(result.bin_path, "bun"); + assert_eq!(result.args, vec!["install", "--frozen-lockfile"]); } #[test] fn test_yarn_ci_uses_immutable_install() { let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); - let result = pm.resolve_ci_commands(&CiCommandOptions::default()); - assert_path_envs(&result); - assert_eq!(result.len(), 1); - assert_eq!(result[0].bin_path, "yarn"); - assert_eq!(result[0].args, vec!["install", "--immutable"]); + let result = pm.resolve_ci_command(&CiCommandOptions::default()); + assert_path_env(&result); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["install", "--immutable"]); } #[test] fn test_yarn_classic_ci_uses_frozen_lockfile_install() { let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.22"); - let result = pm.resolve_ci_commands(&CiCommandOptions::default()); - assert_path_envs(&result); - assert_eq!(result.len(), 1); - assert_eq!(result[0].bin_path, "yarn"); - assert_eq!(result[0].args, vec!["install", "--frozen-lockfile"]); + let result = pm.resolve_ci_command(&CiCommandOptions::default()); + assert_path_env(&result); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["install", "--frozen-lockfile"]); } #[test] @@ -182,12 +143,9 @@ mod tests { let pm = create_mock_package_manager(PackageManagerType::Pnpm, "11.0.0"); let pass_through = vec!["--ignore-scripts".to_string()]; let result = - pm.resolve_ci_commands(&CiCommandOptions { pass_through_args: Some(&pass_through) }); - assert_path_envs(&result); - assert_eq!(result.len(), 2); - assert_eq!(result[0].bin_path, "pnpm"); - assert_eq!(result[0].args, vec!["clean"]); - assert_eq!(result[1].bin_path, "pnpm"); - assert_eq!(result[1].args, vec!["install", "--frozen-lockfile", "--ignore-scripts"]); + pm.resolve_ci_command(&CiCommandOptions { pass_through_args: Some(&pass_through) }); + assert_path_env(&result); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["install", "--frozen-lockfile", "--ignore-scripts"]); } }