Skip to main content

mk_lib/schema/
use_npm.rs

1use std::fs::File;
2use std::io::BufReader;
3use std::path::PathBuf;
4
5use anyhow::Context as _;
6use hashbrown::HashMap;
7use schemars::JsonSchema;
8use serde::Deserialize;
9
10use crate::defaults::default_node_package_manager;
11use crate::file::DisplayPath as _;
12use crate::utils::resolve_path;
13
14use super::{
15  CommandRunner,
16  LocalRun,
17  Task,
18  TaskArgs,
19};
20
21#[derive(Debug, Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub struct NpmPackage {
24  /// The name of the package
25  pub name: Option<String>,
26
27  /// The version of the package
28  pub version: Option<String>,
29
30  /// The path to the package
31  pub scripts: Option<HashMap<String, String>>,
32
33  /// The package manager to use
34  pub package_manager: Option<String>,
35}
36
37#[derive(Debug, Deserialize, JsonSchema)]
38pub struct UseNpmArgs {
39  /// The package manager to use
40  #[serde(default)]
41  pub package_manager: Option<String>,
42
43  /// The working directory to run the command in
44  #[serde(default)]
45  pub work_dir: Option<String>,
46}
47
48#[derive(Debug, Deserialize, JsonSchema)]
49#[serde(untagged)]
50/// Enable npm scripts as tasks. Either `true` or an object with optional settings.
51pub enum UseNpm {
52  Bool(bool),
53  UseNpm(Box<UseNpmArgs>),
54}
55
56impl UseNpm {
57  pub fn capture(&self) -> anyhow::Result<HashMap<String, Task>> {
58    self.capture_in_dir(&PathBuf::from("."))
59  }
60
61  pub fn capture_in_dir(&self, base_dir: &std::path::Path) -> anyhow::Result<HashMap<String, Task>> {
62    match self {
63      UseNpm::Bool(true) => self.capture_tasks_in_dir(base_dir),
64      UseNpm::UseNpm(args) => args.capture_tasks_in_dir(base_dir),
65      _ => Ok(HashMap::new()),
66    }
67  }
68
69  fn capture_tasks_in_dir(&self, base_dir: &std::path::Path) -> anyhow::Result<HashMap<String, Task>> {
70    UseNpmArgs {
71      package_manager: None,
72      work_dir: None,
73    }
74    .capture_tasks_in_dir(base_dir)
75  }
76}
77
78impl UseNpmArgs {
79  pub fn capture_tasks(&self) -> anyhow::Result<HashMap<String, Task>> {
80    self.capture_tasks_in_dir(&PathBuf::from("."))
81  }
82
83  pub fn capture_tasks_in_dir(&self, base_dir: &std::path::Path) -> anyhow::Result<HashMap<String, Task>> {
84    let resolved_work_dir = self
85      .work_dir
86      .as_ref()
87      .map(|work_dir| resolve_path(base_dir, work_dir));
88    let path = self
89      .work_dir
90      .as_ref()
91      .map(|_| resolved_work_dir.clone().unwrap().join("package.json"))
92      .unwrap_or_else(|| base_dir.join("package.json"));
93
94    if !path.exists() || !path.is_file() {
95      return Ok(HashMap::new());
96    }
97
98    let file = File::open(&path).context(format!("Failed to open file - {}", path.display_lossy()))?;
99    let reader = BufReader::new(file);
100
101    let package: NpmPackage = serde_json::from_reader(reader)?;
102    let package_manager: &str = &self
103      .package_manager
104      .clone()
105      .unwrap_or_else(default_node_package_manager);
106
107    assert!(!package_manager.is_empty());
108
109    let tasks: HashMap<String, Task> = package
110      .scripts
111      .unwrap_or_default()
112      .into_iter()
113      .map(|(k, _)| {
114        let command = format!("{package_manager} run {k}");
115        let task = Task::Task(Box::new(TaskArgs {
116          commands: vec![CommandRunner::LocalRun(LocalRun {
117            command,
118            shell: None,
119            test: None,
120            work_dir: resolved_work_dir
121              .as_ref()
122              .map(|work_dir| work_dir.to_string_lossy().into_owned()),
123            interactive: Some(true),
124            retrigger: None,
125            ignore_errors: None,
126            save_output_as: None,
127            verbose: None,
128          })],
129          ..Default::default()
130        }));
131        (k, task)
132      })
133      .collect();
134    Ok(tasks)
135  }
136}
137
138#[cfg(test)]
139mod tests {
140  use super::*;
141
142  #[test]
143  fn test_use_npm_1() -> anyhow::Result<()> {
144    let json = r#"{
145      "name": "test",
146      "version": "1.0.0",
147      "scripts": {
148        "build": "echo 'Building'",
149        "test": "echo 'Testing'"
150      }
151    }"#;
152    let package = serde_json::from_str::<NpmPackage>(json)?;
153    assert_eq!(package.name, Some("test".to_string()));
154    assert_eq!(package.version, Some("1.0.0".to_string()));
155    assert_eq!(
156      package.scripts,
157      Some({
158        let mut map = HashMap::new();
159        map.insert("build".to_string(), "echo 'Building'".to_string());
160        map.insert("test".to_string(), "echo 'Testing'".to_string());
161        map
162      })
163    );
164    Ok(())
165  }
166
167  #[test]
168  fn test_use_npm_2() -> anyhow::Result<()> {
169    let yaml = "true";
170
171    let use_npm = serde_yaml::from_str::<UseNpm>(yaml)?;
172    if let UseNpm::Bool(value) = use_npm {
173      assert!(value);
174    } else {
175      panic!("Invalid value");
176    }
177
178    Ok(())
179  }
180
181  #[test]
182  fn test_use_npm_3() -> anyhow::Result<()> {
183    let yaml = "false";
184
185    let use_npm = serde_yaml::from_str::<UseNpm>(yaml)?;
186    if let UseNpm::Bool(value) = use_npm {
187      assert!(!value);
188    } else {
189      panic!("Invalid value");
190    }
191
192    Ok(())
193  }
194
195  #[test]
196  fn test_use_npm_4() -> anyhow::Result<()> {
197    let yaml = "
198      package_manager: npm
199    ";
200
201    let use_npm = serde_yaml::from_str::<UseNpm>(yaml)?;
202    if let UseNpm::UseNpm(args) = use_npm {
203      assert_eq!(args.package_manager, Some("npm".to_string()));
204    } else {
205      panic!("Invalid value");
206    }
207
208    Ok(())
209  }
210
211  #[test]
212  fn test_use_npm_5() -> anyhow::Result<()> {
213    let yaml = "
214      package_manager: yarn
215      work_dir: /path/to/dir
216    ";
217
218    let use_npm = serde_yaml::from_str::<UseNpm>(yaml)?;
219    if let UseNpm::UseNpm(args) = use_npm {
220      assert_eq!(args.package_manager, Some("yarn".to_string()));
221      assert_eq!(args.work_dir, Some("/path/to/dir".to_string()));
222    } else {
223      panic!("Invalid value");
224    }
225
226    Ok(())
227  }
228}