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::ToUtf8 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 pub name: Option<String>,
26
27 pub version: Option<String>,
29
30 pub scripts: Option<HashMap<String, String>>,
32
33 pub package_manager: Option<String>,
35}
36
37#[derive(Debug, Deserialize, JsonSchema)]
38pub struct UseNpmArgs {
39 #[serde(default)]
41 pub package_manager: Option<String>,
42
43 #[serde(default)]
45 pub work_dir: Option<String>,
46}
47
48#[derive(Debug, Deserialize, JsonSchema)]
49#[serde(untagged)]
50pub 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.to_utf8()?))?;
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}