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