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