use super::js_package_manager_main as js;
use crate::{
file::path_to_content,
model::{command, file_util, runner_type},
};
use anyhow::{Result, anyhow};
use std::{path::PathBuf, process};
const NPM_LOCKFILE_NAME: &str = "package-lock.json";
#[derive(Clone, Debug, PartialEq)]
pub struct Npm {
pub path: PathBuf,
commands: Vec<command::CommandWithPreview>,
}
impl Npm {
pub fn command_to_run(&self, command: &command::CommandForExec) -> Result<String> {
Ok(format!("npm {}", command.args))
}
pub fn execute(&self, command: &command::CommandForExec) -> Result<()> {
let child = process::Command::new("npm")
.stdin(process::Stdio::inherit())
.args(command.args.split_whitespace().collect::<Vec<&str>>())
.spawn();
match child {
Ok(mut child) => match child.wait() {
Ok(_) => Ok(()),
Err(e) => Err(anyhow!("failed to run: {}", e)),
},
Err(e) => Err(anyhow!("failed to spawn: {}", e)),
}
}
pub fn new(current_dir: PathBuf, cwd_file_names: Vec<String>) -> Option<Npm> {
let package_json_exist = Iterator::find(&mut cwd_file_names.iter(), |&f| f == js::METADATA_FILE_NAME);
let lockfile_exist_in_current_dir = Iterator::find(&mut cwd_file_names.iter(), |&f| f == NPM_LOCKFILE_NAME);
let lockfile_exist_in_ancestors =
file_util::find_file_in_ancestors(current_dir.clone(), vec![NPM_LOCKFILE_NAME]);
match (package_json_exist, lockfile_exist_in_current_dir, lockfile_exist_in_ancestors) {
(None, _, _) => None,
(Some(_), Some(_), _) => Npm::collect_workspace_scripts(current_dir.clone()).map(|commands| Npm {
path: current_dir,
commands,
}),
(Some(_), None, Some(_)) => {
Self::collect_scripts_in_package_json(current_dir.clone()).map(|commands| Npm {
path: current_dir,
commands,
})
}
(Some(_), None, None) => None,
}
}
fn collect_workspace_scripts(current_dir: PathBuf) -> Option<Vec<command::CommandWithPreview>> {
let mut result = Self::collect_scripts_in_package_json(current_dir.clone())?;
let workspace_package_json_paths = match Self::get_workspace_packages() {
Ok(result) => result,
Err(_) => return Some(result),
};
for path in workspace_package_json_paths {
if path == current_dir.join(js::METADATA_FILE_NAME) {
continue;
}
if let Ok(c) = path_to_content::path_to_content(&path)
&& let Some((name, parsing_result)) = js::JsPackageManager::parse_package_json(&c)
{
for (key, _, line_number) in parsing_result {
result.push(command::CommandWithPreview::new(
runner_type::RunnerType::JsPackageManager(runner_type::JsPackageManager::Npm),
format!("run {} --workspace={}", key.as_str(), name.clone()),
path.clone(),
line_number,
));
}
};
}
Some(result)
}
fn collect_scripts_in_package_json(current_dir: PathBuf) -> Option<Vec<command::CommandWithPreview>> {
let parsed_scripts_part_of_package_json =
match path_to_content::path_to_content(¤t_dir.join(js::METADATA_FILE_NAME)) {
Ok(c) => match js::JsPackageManager::parse_package_json(&c) {
Some(result) => result.1,
None => return None,
},
Err(_) => return None,
};
Some(
parsed_scripts_part_of_package_json
.iter()
.map(|(key, _value, line_number)| {
command::CommandWithPreview::new(
runner_type::RunnerType::JsPackageManager(runner_type::JsPackageManager::Npm),
format!("run {}", key),
current_dir.clone().join(js::METADATA_FILE_NAME),
*line_number,
)
})
.collect(),
)
}
fn get_workspace_packages() -> Result<Vec<PathBuf>> {
let output = process::Command::new("npm")
.arg("query")
.arg(".workspace")
.arg("--json")
.output()?;
if !output.status.success() {
return Err(anyhow!("npm query failed"));
}
#[derive(serde::Deserialize, Debug)]
struct WorkspacePackage {
path: String,
}
let output_str = String::from_utf8(output.stdout)?;
let packages: Vec<WorkspacePackage> = serde_json::from_str(&output_str)?;
Ok(packages
.iter()
.map(|pkg| PathBuf::from(&pkg.path).join(js::METADATA_FILE_NAME))
.collect())
}
pub fn to_commands(&self) -> Vec<command::CommandWithPreview> {
self.commands.clone()
}
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_npm_detection() {
let npm_root = PathBuf::from("test_data/npm");
let file_names = vec!["package.json".to_string(), "package-lock.json".to_string()];
let result = Npm::new(npm_root.clone(), file_names);
assert!(result.is_some(), "Should detect npm workspace root");
let empty_dir = std::env::temp_dir();
let file_names = vec!["README.md".to_string()];
let result = Npm::new(empty_dir, file_names);
assert!(result.is_none(), "Should not detect npm without package.json");
let no_lockfile = std::env::temp_dir();
let file_names = vec!["package.json".to_string()];
let result = Npm::new(no_lockfile, file_names);
assert!(result.is_none(), "Should not detect npm without lockfile");
}
#[test]
fn test_command_format() {
let npm = Npm {
path: PathBuf::from("test_data/npm"),
commands: vec![],
};
let command = command::CommandForExec {
runner_type: runner_type::RunnerType::JsPackageManager(runner_type::JsPackageManager::Npm),
args: "run build".to_string(),
};
let result = npm.command_to_run(&command);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "npm run build");
}
#[test]
fn test_workspace_command_format() {
let npm = Npm {
path: PathBuf::from("test_data/npm_monorepo"),
commands: vec![],
};
let command = command::CommandForExec {
runner_type: runner_type::RunnerType::JsPackageManager(runner_type::JsPackageManager::Npm),
args: "run build --workspace=app1".to_string(),
};
let result = npm.command_to_run(&command);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "npm run build --workspace=app1");
}
}