diff --git a/README.md b/README.md index c50f3b7..c323028 100644 --- a/README.md +++ b/README.md @@ -58,12 +58,20 @@ jobs: ## Autofix -Most issues can be automatically fixed by using the `--fix` flag. Note that autofix is disabled in CI environments (when `$CI` is set): +Most issues can be automatically fixed by using the `--fix` (or `-f`) flag. Sherif will automatically run your package manager's `install` command (see [No-install mode](#no-install-mode) to disable this behavior) to update the lockfile. Note that autofix is disabled in CI environments (when `$CI` is set): ```bash sherif --fix ``` +### No-install mode + +If you don't want Sherif to run your packager manager's `install` command after running autofix, you can use the `--no-install` flag: + +```bash +sherif --fix --no-install +``` + ## Rules You can ignore a specific rule by using `--ignore-rule ` (or `-r `): diff --git a/fixtures/install/apps/abc/package.json b/fixtures/install/apps/abc/package.json new file mode 100644 index 0000000..91e63fd --- /dev/null +++ b/fixtures/install/apps/abc/package.json @@ -0,0 +1,3 @@ +{ + "name": "abc" +} diff --git a/fixtures/install/apps/def/package.json b/fixtures/install/apps/def/package.json new file mode 100644 index 0000000..b68df89 --- /dev/null +++ b/fixtures/install/apps/def/package.json @@ -0,0 +1,3 @@ +{ + "name": "def" +} diff --git a/fixtures/install/package-lock.json b/fixtures/install/package-lock.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/fixtures/install/package-lock.json @@ -0,0 +1 @@ +{} diff --git a/fixtures/install/package.json b/fixtures/install/package.json new file mode 100644 index 0000000..6043060 --- /dev/null +++ b/fixtures/install/package.json @@ -0,0 +1,6 @@ +{ + "name": "install", + "workspaces": [ + "apps/*" + ] +} diff --git a/src/args.rs b/src/args.rs index a91867c..35c8f60 100644 --- a/src/args.rs +++ b/src/args.rs @@ -8,9 +8,13 @@ pub struct Args { pub path: PathBuf, /// Fix the issues automatically, if possible. - #[arg(long)] + #[arg(long, short)] pub fix: bool, + /// Don't run your package manager's install command when autofixing. + #[arg(long)] + pub no_install: bool, + /// Ignore the `multiple-dependency-versions` rule for the given dependency name and/or version. #[arg(long, short)] pub ignore_dependency: Vec, diff --git a/src/collect.rs b/src/collect.rs index 6a69d1b..f858b08 100644 --- a/src/collect.rs +++ b/src/collect.rs @@ -342,6 +342,7 @@ mod test { let args = Args { path: "unknown".into(), fix: false, + no_install: true, ignore_rule: Vec::new(), ignore_package: Vec::new(), ignore_dependency: Vec::new(), @@ -361,6 +362,7 @@ mod test { let args = Args { path: "fixtures/empty".into(), fix: false, + no_install: true, ignore_rule: Vec::new(), ignore_package: Vec::new(), ignore_dependency: Vec::new(), @@ -380,6 +382,7 @@ mod test { let args = Args { path: "fixtures/basic".into(), fix: false, + no_install: true, ignore_rule: Vec::new(), ignore_package: Vec::new(), ignore_dependency: Vec::new(), @@ -405,6 +408,7 @@ mod test { let args = Args { path: "fixtures/pnpm".into(), fix: false, + no_install: true, ignore_rule: Vec::new(), ignore_package: Vec::new(), ignore_dependency: Vec::new(), @@ -430,6 +434,7 @@ mod test { let args = Args { path: "fixtures/yarn-nohoist".into(), fix: false, + no_install: true, ignore_rule: Vec::new(), ignore_package: Vec::new(), ignore_dependency: Vec::new(), @@ -455,6 +460,7 @@ mod test { let args = Args { path: "fixtures/no-workspace-pnpm".into(), fix: false, + no_install: true, ignore_rule: Vec::new(), ignore_package: Vec::new(), ignore_dependency: Vec::new(), @@ -474,6 +480,7 @@ mod test { let args = Args { path: "fixtures/without-package-json".into(), fix: false, + no_install: true, ignore_rule: Vec::new(), ignore_package: Vec::new(), ignore_dependency: Vec::new(), @@ -500,6 +507,7 @@ mod test { let args = Args { path: "fixtures/ignore-paths".into(), fix: false, + no_install: true, ignore_rule: Vec::new(), ignore_package: Vec::new(), ignore_dependency: Vec::new(), @@ -534,6 +542,7 @@ mod test { let args = Args { path: "fixtures/root-issues".into(), fix: false, + no_install: true, ignore_rule: Vec::new(), ignore_package: Vec::new(), ignore_dependency: Vec::new(), @@ -568,6 +577,7 @@ mod test { fn collect_root_issues_fixed() { let args = Args { fix: false, + no_install: true, path: "fixtures/root-issues-fixed".into(), ignore_rule: Vec::new(), ignore_package: Vec::new(), @@ -586,6 +596,7 @@ mod test { let args = Args { path: "fixtures/dependencies".into(), fix: false, + no_install: true, ignore_rule: Vec::new(), ignore_package: Vec::new(), ignore_dependency: Vec::new(), @@ -618,6 +629,7 @@ mod test { let args = Args { path: "fixtures/dependencies".into(), fix: false, + no_install: false, ignore_rule: Vec::new(), ignore_package: Vec::new(), ignore_dependency: vec!["next@4.5.6".to_string()], @@ -646,6 +658,7 @@ mod test { let args = Args { path: "fixtures/dependencies-star".into(), fix: false, + no_install: true, ignore_rule: Vec::new(), ignore_package: Vec::new(), ignore_dependency: Vec::new(), @@ -674,6 +687,7 @@ mod test { let args = Args { path: "fixtures/dependencies-nested-star".into(), fix: false, + no_install: false, ignore_rule: Vec::new(), ignore_package: Vec::new(), ignore_dependency: Vec::new(), @@ -705,6 +719,7 @@ mod test { let args = Args { path: "fixtures/pnpm-glob".into(), fix: false, + no_install: true, ignore_rule: Vec::new(), ignore_package: Vec::new(), ignore_dependency: Vec::new(), @@ -723,6 +738,7 @@ mod test { let args = Args { path: "fixtures/unordered".into(), fix: false, + no_install: false, ignore_rule: Vec::new(), ignore_package: Vec::new(), ignore_dependency: Vec::new(), diff --git a/src/install.rs b/src/install.rs new file mode 100644 index 0000000..e3355fc --- /dev/null +++ b/src/install.rs @@ -0,0 +1,128 @@ +use crate::printer::get_render_config; +use anyhow::{anyhow, Result}; +use colored::Colorize; +use inquire::Select; +use std::{fmt::Display, fs, process::Command, process::Stdio}; + +const PACKAGE_MANAGERS: [&str; 4] = ["npm", "yarn", "pnpm", "bun"]; + +#[derive(Debug, PartialEq)] +enum PackageManager { + Npm, + Yarn, + Pnpm, + Bun, +} + +impl PackageManager { + pub fn resolve() -> Result { + if fs::metadata("package-lock.json").is_ok() { + return Ok(PackageManager::Npm); + } else if fs::metadata("yarn.lock").is_ok() { + return Ok(PackageManager::Yarn); + } else if fs::metadata("pnpm-lock.yaml").is_ok() { + return Ok(PackageManager::Pnpm); + } else if fs::metadata("bun.lockb").is_ok() { + return Ok(PackageManager::Bun); + } + + let package_manager = + Select::new("Select a package manager to use", PACKAGE_MANAGERS.to_vec()) + .with_render_config(get_render_config()) + .with_help_message("Enter to select") + .prompt(); + + match package_manager { + Ok("npm") => Ok(PackageManager::Npm), + Ok("yarn") => Ok(PackageManager::Yarn), + Ok("pnpm") => Ok(PackageManager::Pnpm), + Ok("bun") => Ok(PackageManager::Bun), + _ => Err(anyhow!("No package manager selected")), + } + } +} + +impl Display for PackageManager { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PackageManager::Npm => write!(f, "npm"), + PackageManager::Yarn => write!(f, "yarn"), + PackageManager::Pnpm => write!(f, "pnpm"), + PackageManager::Bun => write!(f, "bun"), + } + } +} + +pub fn install() -> Result<()> { + let package_manager = PackageManager::resolve()?; + + println!( + " {}", + format!("Note: running install command using {}...", package_manager).bright_black(), + ); + println!(); + + let mut command = Command::new(package_manager.to_string()) + .arg("install") + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn()?; + + let status = command.wait()?; + if !status.success() { + return Err(anyhow!("Install command failed")); + } + + println!(); + Ok(()) +} + +#[cfg(test)] +mod test { + use crate::{args::Args, collect::collect_packages}; + use serde_json::Value; + use std::fs; + + #[test] + fn test_detect_package_manager() { + use super::*; + use std::fs; + + fs::File::create("package-lock.json").unwrap(); + assert_eq!(PackageManager::resolve().unwrap(), PackageManager::Npm); + + fs::remove_file("package-lock.json").unwrap(); + fs::File::create("yarn.lock").unwrap(); + assert_eq!(PackageManager::resolve().unwrap(), PackageManager::Yarn); + + fs::remove_file("yarn.lock").unwrap(); + fs::File::create("pnpm-lock.yaml").unwrap(); + assert_eq!(PackageManager::resolve().unwrap(), PackageManager::Pnpm); + + fs::remove_file("pnpm-lock.yaml").unwrap(); + } + + #[test] + fn test_install_run() { + let args = Args { + path: "fixtures/install".into(), + fix: false, + no_install: false, + ignore_rule: Vec::new(), + ignore_package: Vec::new(), + ignore_dependency: Vec::new(), + }; + + let _ = collect_packages(&args); + + std::env::set_current_dir("fixtures/install").unwrap(); + super::install().unwrap(); + + // Test if the previously empty package-lock.json now contains the "install" name to indicate that the install command was run + let file = fs::File::open("package-lock.json"); + let json: Result = serde_json::from_reader(file.unwrap()); + assert_eq!(json.unwrap()["name"], "install"); + + std::env::set_current_dir("../../").unwrap(); + } +} diff --git a/src/main.rs b/src/main.rs index c40873a..60288e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ use std::time::Instant; mod args; mod collect; +mod install; mod json; mod packages; mod plural; @@ -59,6 +60,14 @@ fn main() { let errors = issues.len_by_level(IssueLevel::Error); let fixed = issues.len_by_level(IssueLevel::Fixed); + // Only run the install command if we allow it and we fixed some issues. + if args.fix && !args.no_install && fixed > 0 { + if let Err(error) = install::install() { + print_error("Failed to install packages", error.to_string().as_str()); + std::process::exit(1); + } + } + if let Err(error) = print_issues(issues) { print_error("Failed to print issues", error.to_string().as_str()); std::process::exit(1); diff --git a/src/printer.rs b/src/printer.rs index 561ff60..92e3b21 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -4,6 +4,7 @@ use crate::{ }; use anyhow::Result; use colored::Colorize; +use inquire::ui::{Color, RenderConfig, StyleSheet, Styled}; use std::io::Write; use std::time::Instant; @@ -71,7 +72,17 @@ pub fn print_footer( ); println!( "{}", - " Note: use `-i` to ignore dependencies, `-r` to ignore rules, `-p` to ignore packages, and `--fix` to autofix fixable issues." + " Note: use `-i` to ignore dependencies, `-r` to ignore rules, `-p` to ignore packages, and `-f` to autofix fixable issues." .bright_black() ); } + +pub fn get_render_config() -> RenderConfig { + let mut render_config = RenderConfig::default_colored() + .with_prompt_prefix(Styled::new("✓").with_fg(Color::DarkGrey)) + .with_help_message(StyleSheet::new().with_fg(Color::DarkGrey)) + .with_highlighted_option_prefix(Styled::new(" → ").with_fg(Color::LightCyan)) + .with_canceled_prompt_indicator(Styled::new("✗").with_fg(Color::LightRed)); + render_config.answered_prompt_prefix = Styled::new("✓").with_fg(Color::LightGreen); + render_config +} diff --git a/src/rules/multiple_dependency_versions.rs b/src/rules/multiple_dependency_versions.rs index 6666765..8e66c42 100644 --- a/src/rules/multiple_dependency_versions.rs +++ b/src/rules/multiple_dependency_versions.rs @@ -1,12 +1,9 @@ use super::{Issue, IssueLevel, PackageType}; -use crate::{json, packages::semversion::SemVersion}; +use crate::{json, packages::semversion::SemVersion, printer::get_render_config}; use anyhow::Result; use colored::Colorize; use indexmap::IndexMap; -use inquire::{ - ui::{Color, RenderConfig, StyleSheet, Styled}, - Select, -}; +use inquire::Select; use std::{borrow::Cow, fs, path::PathBuf}; #[derive(Debug)] @@ -128,15 +125,8 @@ impl Issue for MultipleDependencyVersionsIssue { .collect::>(); versions.dedup(); - let mut render_config = RenderConfig::default_colored() - .with_prompt_prefix(Styled::new("✓").with_fg(Color::DarkGrey)) - .with_help_message(StyleSheet::new().with_fg(Color::DarkGrey)) - .with_highlighted_option_prefix(Styled::new(" → ").with_fg(Color::LightCyan)) - .with_canceled_prompt_indicator(Styled::new("✗").with_fg(Color::LightRed)); - render_config.answered_prompt_prefix = Styled::new("✓").with_fg(Color::LightGreen); - let select = Select::new(&message, versions) - .with_render_config(render_config) + .with_render_config(get_render_config()) .with_help_message("Enter to select, Esc to skip") .prompt_skippable()?;