1
use std::fs::File;
2
use std::io::BufReader;
3
use std::path::PathBuf;
4

            
5
use anyhow::Context as _;
6
use hashbrown::HashMap;
7
use schemars::JsonSchema;
8
use serde::Deserialize;
9

            
10
use crate::defaults::default_node_package_manager;
11
use crate::file::DisplayPath as _;
12
use crate::utils::resolve_path;
13

            
14
use super::{
15
  CommandRunner,
16
  LocalRun,
17
  Task,
18
  TaskArgs,
19
};
20

            
21
#[derive(Debug, Deserialize)]
22
#[serde(rename_all = "camelCase")]
23
pub 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)]
38
pub 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.
51
pub enum UseNpm {
52
  Bool(bool),
53
  UseNpm(Box<UseNpmArgs>),
54
}
55

            
56
impl 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

            
78
impl 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)]
139
mod tests {
140
  use super::*;
141

            
142
  #[test]
143
2
  fn test_use_npm_1() -> anyhow::Result<()> {
144
2
    let json = r#"{
145
2
      "name": "test",
146
2
      "version": "1.0.0",
147
2
      "scripts": {
148
2
        "build": "echo 'Building'",
149
2
        "test": "echo 'Testing'"
150
2
      }
151
2
    }"#;
152
2
    let package = serde_json::from_str::<NpmPackage>(json)?;
153
2
    assert_eq!(package.name, Some("test".to_string()));
154
2
    assert_eq!(package.version, Some("1.0.0".to_string()));
155
2
    assert_eq!(
156
      package.scripts,
157
2
      Some({
158
2
        let mut map = HashMap::new();
159
2
        map.insert("build".to_string(), "echo 'Building'".to_string());
160
2
        map.insert("test".to_string(), "echo 'Testing'".to_string());
161
2
        map
162
2
      })
163
    );
164
2
    Ok(())
165
2
  }
166

            
167
  #[test]
168
2
  fn test_use_npm_2() -> anyhow::Result<()> {
169
2
    let yaml = "true";
170

            
171
2
    let use_npm = serde_yaml::from_str::<UseNpm>(yaml)?;
172
2
    if let UseNpm::Bool(value) = use_npm {
173
2
      assert!(value);
174
    } else {
175
      panic!("Invalid value");
176
    }
177

            
178
2
    Ok(())
179
2
  }
180

            
181
  #[test]
182
2
  fn test_use_npm_3() -> anyhow::Result<()> {
183
2
    let yaml = "false";
184

            
185
2
    let use_npm = serde_yaml::from_str::<UseNpm>(yaml)?;
186
2
    if let UseNpm::Bool(value) = use_npm {
187
2
      assert!(!value);
188
    } else {
189
      panic!("Invalid value");
190
    }
191

            
192
2
    Ok(())
193
2
  }
194

            
195
  #[test]
196
2
  fn test_use_npm_4() -> anyhow::Result<()> {
197
2
    let yaml = "
198
2
      package_manager: npm
199
2
    ";
200

            
201
2
    let use_npm = serde_yaml::from_str::<UseNpm>(yaml)?;
202
2
    if let UseNpm::UseNpm(args) = use_npm {
203
2
      assert_eq!(args.package_manager, Some("npm".to_string()));
204
    } else {
205
      panic!("Invalid value");
206
    }
207

            
208
2
    Ok(())
209
2
  }
210

            
211
  #[test]
212
2
  fn test_use_npm_5() -> anyhow::Result<()> {
213
2
    let yaml = "
214
2
      package_manager: yarn
215
2
      work_dir: /path/to/dir
216
2
    ";
217

            
218
2
    let use_npm = serde_yaml::from_str::<UseNpm>(yaml)?;
219
2
    if let UseNpm::UseNpm(args) = use_npm {
220
2
      assert_eq!(args.package_manager, Some("yarn".to_string()));
221
2
      assert_eq!(args.work_dir, Some("/path/to/dir".to_string()));
222
    } else {
223
      panic!("Invalid value");
224
    }
225

            
226
2
    Ok(())
227
2
  }
228
}