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 7cb3dfcbd0..da496ce51e 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 @@ -398,6 +398,8 @@ Forward a command to the package manager Commands: approve-builds Approve dependency lifecycle scripts (install/postinstall) to run prune Remove unnecessary packages + patch Prepare a package for local patching + patch-commit Commit a prepared package patch pack Create a tarball of the package list List installed packages [alias: ls] view, info, show View package information from the registry diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_patch/npm/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_patch/npm/package.json new file mode 100644 index 0000000000..3d874a76f0 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_patch/npm/package.json @@ -0,0 +1,7 @@ +{ + "name": "pm-patch-npm", + "version": "1.0.0", + "private": true, + "license": "MIT", + "packageManager": "npm@11.11.1" +} diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_patch/pnpm/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_patch/pnpm/package.json new file mode 100644 index 0000000000..b532b946a6 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_patch/pnpm/package.json @@ -0,0 +1,7 @@ +{ + "name": "pm-patch-pnpm", + "version": "1.0.0", + "private": true, + "license": "MIT", + "packageManager": "pnpm@10.34.4" +} diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_patch/snapshots.toml b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_patch/snapshots.toml new file mode 100644 index 0000000000..15d3ae2930 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_patch/snapshots.toml @@ -0,0 +1,26 @@ +[[case]] +name = "pm_patch_npm" +vp = "local" +cwd = "npm" +steps = [ + { argv = ["vp", "pm", "patch", "placeholder"], comment = "npm warns and exits successfully because patch is unsupported" }, + { argv = ["vp", "pm", "patch-commit", "placeholder"], comment = "npm warns and exits successfully because patch-commit is unsupported" }, +] + +[[case]] +name = "pm_patch_pnpm" +vp = "local" +cwd = "pnpm" +steps = [ + { argv = ["vp", "pm", "patch", "placeholder", "--", "--version"], comment = "pnpm receives the patch command" }, + { argv = ["vp", "pm", "patch-commit", "placeholder", "--", "--version"], comment = "pnpm receives the patch-commit command" }, +] + +[[case]] +name = "pm_patch_yarn" +vp = "local" +cwd = "yarn" +steps = [ + { argv = ["vp", "pm", "patch", "placeholder"], comment = "Yarn Classic warns and exits successfully because patch is unsupported" }, + { argv = ["vp", "pm", "patch-commit", "placeholder"], comment = "Yarn Classic warns and exits successfully because patch-commit is unsupported" }, +] diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_patch/snapshots/pm_patch_npm.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_patch/snapshots/pm_patch_npm.md new file mode 100644 index 0000000000..2881c445ae --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_patch/snapshots/pm_patch_npm.md @@ -0,0 +1,17 @@ +# pm_patch_npm + +## `vp pm patch placeholder` + +npm warns and exits successfully because patch is unsupported + +``` +warn: npm does not have a 'patch' command. +``` + +## `vp pm patch-commit placeholder` + +npm warns and exits successfully because patch-commit is unsupported + +``` +warn: npm does not have a 'patch-commit' command. +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_patch/snapshots/pm_patch_pnpm.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_patch/snapshots/pm_patch_pnpm.md new file mode 100644 index 0000000000..cff49de0a3 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_patch/snapshots/pm_patch_pnpm.md @@ -0,0 +1,17 @@ +# pm_patch_pnpm + +## `vp pm patch placeholder -- --version` + +pnpm receives the patch command + +``` +10.34.4 +``` + +## `vp pm patch-commit placeholder -- --version` + +pnpm receives the patch-commit command + +``` +10.34.4 +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_patch/snapshots/pm_patch_yarn.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_patch/snapshots/pm_patch_yarn.md new file mode 100644 index 0000000000..f039f1eb4f --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_patch/snapshots/pm_patch_yarn.md @@ -0,0 +1,17 @@ +# pm_patch_yarn + +## `vp pm patch placeholder` + +Yarn Classic warns and exits successfully because patch is unsupported + +``` +warn: yarn classic does not have a 'patch' command. +``` + +## `vp pm patch-commit placeholder` + +Yarn Classic warns and exits successfully because patch-commit is unsupported + +``` +warn: yarn classic does not have a 'patch-commit' command. +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_patch/yarn/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_patch/yarn/package.json new file mode 100644 index 0000000000..041153f2aa --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pm_patch/yarn/package.json @@ -0,0 +1,7 @@ +{ + "name": "pm-patch-yarn", + "version": "1.0.0", + "private": true, + "license": "MIT", + "packageManager": "yarn@1.22.22" +} diff --git a/crates/vite_pm_cli/src/cli.rs b/crates/vite_pm_cli/src/cli.rs index ed7d80766d..5238ff2fa3 100644 --- a/crates/vite_pm_cli/src/cli.rs +++ b/crates/vite_pm_cli/src/cli.rs @@ -12,9 +12,9 @@ use crate::{ resolution::{ AddArgs, ApproveBuildsArgs, AuditArgs, CacheArgs, ConfigCommand, DedupeArgs, DeprecateArgs, DistTagCommand, DlxArgs, FundArgs, InstallArgs, LinkArgs, ListArgs, LoginArgs, LogoutArgs, - OutdatedArgs, OutdatedFormat, OwnerCommand, PackArgs, PingArgs, PruneArgs, PublishArgs, - RebuildArgs, RemoveArgs, Resolution, SearchArgs, StageCommand, TokenCommand, UnlinkArgs, - UpdateArgs, VersionArgs, ViewArgs, WhoamiArgs, WhyArgs, + OutdatedArgs, OutdatedFormat, OwnerCommand, PackArgs, PatchArgs, PatchCommitArgs, PingArgs, + PruneArgs, PublishArgs, RebuildArgs, RemoveArgs, Resolution, SearchArgs, StageCommand, + TokenCommand, UnlinkArgs, UpdateArgs, VersionArgs, ViewArgs, WhoamiArgs, WhyArgs, resolve_for_manager as resolve_args_for_manager, }, }; @@ -79,6 +79,13 @@ pub enum PmCommand { /// Remove unnecessary packages Prune(PruneArgs), + /// Prepare a package for local patching + Patch(PatchArgs), + + /// Commit a prepared package patch + #[command(name = "patch-commit")] + PatchCommit(PatchCommitArgs), + /// Create a tarball of the package Pack(PackArgs), @@ -311,6 +318,8 @@ impl PmCommand { match self { Self::ApproveBuilds(args) => resolve_args_for_manager(manager, args), Self::Prune(args) => resolve_args_for_manager(manager, args), + Self::Patch(args) => resolve_args_for_manager(manager, args), + Self::PatchCommit(args) => resolve_args_for_manager(manager, args), Self::Pack(args) => resolve_args_for_manager(manager, args), Self::List(args) => resolve_args_for_manager(manager, args), Self::View(args) => resolve_args_for_manager(manager, args), diff --git a/crates/vite_pm_cli/src/dispatch.rs b/crates/vite_pm_cli/src/dispatch.rs index 45711e7f39..801b84ccb0 100644 --- a/crates/vite_pm_cli/src/dispatch.rs +++ b/crates/vite_pm_cli/src/dispatch.rs @@ -88,6 +88,8 @@ fn pm_manager_policy(command: &PmCommand) -> ManagerPolicy { match command { PmCommand::ApproveBuilds(_) | PmCommand::Prune(_) + | PmCommand::Patch(_) + | PmCommand::PatchCommit(_) | PmCommand::Pack(_) | PmCommand::List(_) | PmCommand::Version(_) diff --git a/crates/vite_pm_cli/src/resolution/commands/mod.rs b/crates/vite_pm_cli/src/resolution/commands/mod.rs index 9e126104f7..15b313688e 100644 --- a/crates/vite_pm_cli/src/resolution/commands/mod.rs +++ b/crates/vite_pm_cli/src/resolution/commands/mod.rs @@ -16,6 +16,8 @@ mod logout; mod outdated; mod owner; mod pack; +mod patch; +mod patch_commit; mod ping; mod prune; mod publish; @@ -50,6 +52,8 @@ pub use logout::LogoutArgs; pub use outdated::{OutdatedArgs, OutdatedFormat}; pub use owner::OwnerCommand; pub use pack::PackArgs; +pub use patch::PatchArgs; +pub use patch_commit::PatchCommitArgs; pub use ping::PingArgs; pub use prune::PruneArgs; pub use publish::PublishArgs; diff --git a/crates/vite_pm_cli/src/resolution/commands/patch.rs b/crates/vite_pm_cli/src/resolution/commands/patch.rs new file mode 100644 index 0000000000..a331a18d6f --- /dev/null +++ b/crates/vite_pm_cli/src/resolution/commands/patch.rs @@ -0,0 +1,148 @@ +use vite_pm_cli_macros::pm_args; + +use crate::resolution::{ + Bun, CommandBuilder, CommandResolution, DiagnosticKind, Diagnostics, Npm, Pnpm, Resolve, Yarn, +}; + +#[pm_args] +#[derive(clap::Args, Clone, Debug, Default, PartialEq, Eq)] +pub struct PatchArgs { + /// Package to patch + pub(crate) package: String, + + /// Additional arguments + #[arg(last = true, allow_hyphen_values = true)] + pub(crate) pass_through_args: Vec, +} + +impl Resolve for Pnpm { + fn resolve(&self, args: &PatchArgs, _diag: &mut Diagnostics) -> CommandResolution { + let mut cmd = CommandBuilder::new("pnpm"); + cmd.arg("patch").arg(&args.package).extend(args.pass_through_args.iter()); + cmd.into() + } +} + +impl Resolve for Npm { + fn resolve(&self, _args: &PatchArgs, diag: &mut Diagnostics) -> CommandResolution { + diag.warn(DiagnosticKind::UnsupportedCommandNoop, "npm does not have a 'patch' command."); + CommandResolution::Noop + } +} + +impl Resolve for Yarn { + fn resolve(&self, args: &PatchArgs, diag: &mut Diagnostics) -> CommandResolution { + if !self.is_berry() { + diag.warn( + DiagnosticKind::UnsupportedCommandNoop, + "yarn classic does not have a 'patch' command.", + ); + return CommandResolution::Noop; + } + let mut cmd = CommandBuilder::new("yarn"); + cmd.arg("patch").arg(&args.package).extend(args.pass_through_args.iter()); + cmd.into() + } +} + +impl Resolve for Bun { + fn resolve(&self, args: &PatchArgs, _diag: &mut Diagnostics) -> CommandResolution { + let mut cmd = CommandBuilder::new("bun"); + cmd.arg("patch").arg(&args.package).extend(args.pass_through_args.iter()); + cmd.into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::resolution::{ + resolve, + test_utils::{bun, npm, parse_args, pnpm, yarn}, + }; + + fn patch_args(package: &str) -> PatchArgs { + PatchArgs { package: package.to_string(), ..Default::default() } + } + + #[test] + fn test_pnpm_patch() { + let CommandResolution::Run(command) = + resolve(&pnpm("10.0.0"), patch_args("left-pad")).outcome + else { + panic!("expected command resolution"); + }; + + assert_eq!(command.program, "pnpm"); + assert_eq!(command.args, vec!["patch", "left-pad"]); + } + + #[test] + fn test_yarn_berry_patch() { + let CommandResolution::Run(command) = + resolve(&yarn("4.0.0"), patch_args("left-pad")).outcome + else { + panic!("expected command resolution"); + }; + + assert_eq!(command.program, "yarn"); + assert_eq!(command.args, vec!["patch", "left-pad"]); + } + + #[test] + fn test_bun_patch() { + let CommandResolution::Run(command) = + resolve(&bun("1.3.11"), patch_args("left-pad")).outcome + else { + panic!("expected command resolution"); + }; + + assert_eq!(command.program, "bun"); + assert_eq!(command.args, vec!["patch", "left-pad"]); + } + + #[test] + fn test_npm_patch_not_supported() { + let result = resolve(&npm("11.0.0"), patch_args("left-pad")); + + assert_eq!(result.outcome, CommandResolution::Noop); + assert_eq!(result.diagnostics.len(), 1); + assert_eq!(result.diagnostics[0].kind, DiagnosticKind::UnsupportedCommandNoop); + assert_eq!(result.diagnostics[0].message, "npm does not have a 'patch' command."); + } + + #[test] + fn test_yarn_classic_patch_not_supported() { + let result = resolve(&yarn("1.22.22"), patch_args("left-pad")); + + assert_eq!(result.outcome, CommandResolution::Noop); + assert_eq!(result.diagnostics.len(), 1); + assert_eq!(result.diagnostics[0].kind, DiagnosticKind::UnsupportedCommandNoop); + assert_eq!(result.diagnostics[0].message, "yarn classic does not have a 'patch' command."); + } + + #[test] + fn test_patch_with_pass_through_args() { + let CommandResolution::Run(command) = resolve( + &pnpm("10.0.0"), + PatchArgs { + package: "left-pad".to_string(), + pass_through_args: vec!["--edit-dir".to_string(), ".patches".to_string()], + }, + ) + .outcome + else { + panic!("expected command resolution"); + }; + + assert_eq!(command.args, vec!["patch", "left-pad", "--edit-dir", ".patches"]); + } + + #[test] + fn parser_captures_package_and_pass_through_args() { + let args = parse_args::(["left-pad", "--", "--edit-dir", ".patches"]).unwrap(); + + assert_eq!(args.package, "left-pad"); + assert_eq!(args.pass_through_args, vec!["--edit-dir".to_string(), ".patches".to_string()]); + } +} diff --git a/crates/vite_pm_cli/src/resolution/commands/patch_commit.rs b/crates/vite_pm_cli/src/resolution/commands/patch_commit.rs new file mode 100644 index 0000000000..277667ae01 --- /dev/null +++ b/crates/vite_pm_cli/src/resolution/commands/patch_commit.rs @@ -0,0 +1,166 @@ +use vite_pm_cli_macros::pm_args; + +use crate::resolution::{ + Bun, CommandBuilder, CommandResolution, DiagnosticKind, Diagnostics, Npm, Pnpm, Resolve, Yarn, +}; + +#[pm_args] +#[derive(clap::Args, Clone, Debug, Default, PartialEq, Eq)] +pub struct PatchCommitArgs { + /// Patch directory to commit + pub(crate) patch_dir: String, + + /// Additional arguments + #[arg(last = true, allow_hyphen_values = true)] + pub(crate) pass_through_args: Vec, +} + +impl Resolve for Pnpm { + fn resolve(&self, args: &PatchCommitArgs, _diag: &mut Diagnostics) -> CommandResolution { + let mut cmd = CommandBuilder::new("pnpm"); + cmd.arg("patch-commit").arg(&args.patch_dir).extend(args.pass_through_args.iter()); + cmd.into() + } +} + +impl Resolve for Npm { + fn resolve(&self, _args: &PatchCommitArgs, diag: &mut Diagnostics) -> CommandResolution { + diag.warn( + DiagnosticKind::UnsupportedCommandNoop, + "npm does not have a 'patch-commit' command.", + ); + CommandResolution::Noop + } +} + +impl Resolve for Yarn { + fn resolve(&self, args: &PatchCommitArgs, diag: &mut Diagnostics) -> CommandResolution { + if !self.is_berry() { + diag.warn( + DiagnosticKind::UnsupportedCommandNoop, + "yarn classic does not have a 'patch-commit' command.", + ); + return CommandResolution::Noop; + } + let mut cmd = CommandBuilder::new("yarn"); + // Without --save, yarn patch-commit only prints the patch to stdout. + cmd.arg("patch-commit") + .arg("--save") + .arg(&args.patch_dir) + .extend(args.pass_through_args.iter()); + cmd.into() + } +} + +impl Resolve for Bun { + fn resolve(&self, args: &PatchCommitArgs, _diag: &mut Diagnostics) -> CommandResolution { + let mut cmd = CommandBuilder::new("bun"); + cmd.arg("patch").arg("--commit").arg(&args.patch_dir).extend(args.pass_through_args.iter()); + cmd.into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::resolution::{ + resolve, + test_utils::{bun, npm, parse_args, pnpm, yarn}, + }; + + fn patch_commit_args(patch_dir: &str) -> PatchCommitArgs { + PatchCommitArgs { patch_dir: patch_dir.to_string(), ..Default::default() } + } + + #[test] + fn test_pnpm_patch_commit() { + let CommandResolution::Run(command) = + resolve(&pnpm("10.0.0"), patch_commit_args("patches/left-pad")).outcome + else { + panic!("expected command resolution"); + }; + + assert_eq!(command.program, "pnpm"); + assert_eq!(command.args, vec!["patch-commit", "patches/left-pad"]); + } + + #[test] + fn test_yarn_berry_patch_commit() { + let CommandResolution::Run(command) = + resolve(&yarn("4.0.0"), patch_commit_args("patches/left-pad")).outcome + else { + panic!("expected command resolution"); + }; + + assert_eq!(command.program, "yarn"); + assert_eq!(command.args, vec!["patch-commit", "--save", "patches/left-pad"]); + } + + #[test] + fn test_bun_patch_commit_uses_flag() { + let CommandResolution::Run(command) = + resolve(&bun("1.3.11"), patch_commit_args("patches/left-pad")).outcome + else { + panic!("expected command resolution"); + }; + + assert_eq!(command.program, "bun"); + assert_eq!(command.args, vec!["patch", "--commit", "patches/left-pad"]); + } + + #[test] + fn test_npm_patch_commit_not_supported() { + let result = resolve(&npm("11.0.0"), patch_commit_args("patches/left-pad")); + + assert_eq!(result.outcome, CommandResolution::Noop); + assert_eq!(result.diagnostics.len(), 1); + assert_eq!(result.diagnostics[0].kind, DiagnosticKind::UnsupportedCommandNoop); + assert_eq!(result.diagnostics[0].message, "npm does not have a 'patch-commit' command."); + } + + #[test] + fn test_yarn_classic_patch_commit_not_supported() { + let result = resolve(&yarn("1.22.22"), patch_commit_args("patches/left-pad")); + + assert_eq!(result.outcome, CommandResolution::Noop); + assert_eq!(result.diagnostics.len(), 1); + assert_eq!(result.diagnostics[0].kind, DiagnosticKind::UnsupportedCommandNoop); + assert_eq!( + result.diagnostics[0].message, + "yarn classic does not have a 'patch-commit' command." + ); + } + + #[test] + fn test_patch_commit_with_pass_through_args() { + let CommandResolution::Run(command) = resolve( + &bun("1.3.11"), + PatchCommitArgs { + patch_dir: "patches/left-pad".to_string(), + pass_through_args: vec!["--patches-dir".to_string(), ".patches".to_string()], + }, + ) + .outcome + else { + panic!("expected command resolution"); + }; + + assert_eq!( + command.args, + vec!["patch", "--commit", "patches/left-pad", "--patches-dir", ".patches"] + ); + } + + #[test] + fn parser_captures_patch_dir_and_pass_through_args() { + let args = + parse_args::(["patches/left-pad", "--", "--patches-dir", ".patches"]) + .unwrap(); + + assert_eq!(args.patch_dir, "patches/left-pad"); + assert_eq!( + args.pass_through_args, + vec!["--patches-dir".to_string(), ".patches".to_string()] + ); + } +} diff --git a/crates/vite_pm_cli/src/resolution/mod.rs b/crates/vite_pm_cli/src/resolution/mod.rs index 08b120bbbe..202acdbd0a 100644 --- a/crates/vite_pm_cli/src/resolution/mod.rs +++ b/crates/vite_pm_cli/src/resolution/mod.rs @@ -46,9 +46,9 @@ pub(crate) use commands::SaveDependencyArgs; pub use commands::{ AddArgs, ApproveBuildsArgs, AuditArgs, CacheArgs, ConfigCommand, DedupeArgs, DeprecateArgs, DistTagCommand, DlxArgs, FundArgs, InstallArgs, LinkArgs, ListArgs, LoginArgs, LogoutArgs, - OutdatedArgs, OutdatedFormat, OwnerCommand, PackArgs, PingArgs, PruneArgs, PublishArgs, - RebuildArgs, RemoveArgs, SearchArgs, StageCommand, TokenCommand, UnlinkArgs, UpdateArgs, - VersionArgs, ViewArgs, WhoamiArgs, WhyArgs, + OutdatedArgs, OutdatedFormat, OwnerCommand, PackArgs, PatchArgs, PatchCommitArgs, PingArgs, + PruneArgs, PublishArgs, RebuildArgs, RemoveArgs, SearchArgs, StageCommand, TokenCommand, + UnlinkArgs, UpdateArgs, VersionArgs, ViewArgs, WhoamiArgs, WhyArgs, }; use diagnostics::DiagnosticKind; /// Diagnostics emitted while unsupported arguments and fallbacks are resolved.