1use crate::buildsystem::{BuildSystem, DependencyCategory, Error};
2use crate::dependencies::node::NodePackageDependency;
3use crate::dependencies::BinaryDependency;
4use crate::dependency::Dependency;
5use crate::installer::{Error as InstallerError, InstallationScope, Installer};
6use crate::session::Session;
7use serde::Deserialize;
8use std::collections::HashMap;
9use std::path::PathBuf;
10
11#[derive(Debug)]
12#[allow(dead_code)]
13pub struct Node {
17 path: PathBuf,
18 package: NodePackage,
19}
20
21#[derive(Debug, Deserialize)]
22struct NodePackage {
23 #[serde(default)]
24 dependencies: HashMap<String, String>,
25 #[serde(rename = "devDependencies", default)]
26 dev_dependencies: HashMap<String, String>,
27 #[serde(default)]
28 scripts: HashMap<String, String>,
29}
30
31impl Node {
32 pub fn new(path: PathBuf) -> Result<Self, Box<dyn std::error::Error>> {
34 let package_path = path.join("package.json");
35
36 let package_content = std::fs::read_to_string(&package_path)?;
37
38 let package: NodePackage = serde_json::from_str(&package_content)?;
39
40 Ok(Self { path, package })
41 }
42
43 fn setup(&self, session: &dyn Session, installer: &dyn Installer) -> Result<(), Error> {
44 let binary_req = BinaryDependency::new("npm");
45 if !binary_req.present(session) {
46 installer.install(&binary_req, InstallationScope::Global)?;
47 }
48 Ok(())
49 }
50
51 pub fn probe(path: &std::path::Path) -> Option<Box<dyn BuildSystem>> {
55 let package_json_path = path.join("package.json");
56 if package_json_path.exists() {
57 log::debug!("Found package.json, attempting to parse as node package.");
58 match Self::new(path.to_path_buf()) {
59 Ok(node_system) => return Some(Box::new(node_system)),
60 Err(e) => {
61 log::debug!("Failed to parse package.json: {}", e);
62 return None;
63 }
64 }
65 }
66 None
67 }
68}
69
70impl BuildSystem for Node {
71 fn get_declared_dependencies(
72 &self,
73 _session: &dyn Session,
74 _fixers: Option<&[&dyn crate::fix_build::BuildFixer<InstallerError>]>,
75 ) -> Result<Vec<(DependencyCategory, Box<dyn Dependency>)>, Error> {
76 let mut dependencies: Vec<(DependencyCategory, Box<dyn Dependency>)> = vec![];
77
78 for (name, _version) in self.package.dependencies.iter() {
79 dependencies.push((
81 DependencyCategory::Universal,
82 Box::new(NodePackageDependency::new(name)),
83 ));
84 }
85
86 for (name, _version) in self.package.dev_dependencies.iter() {
87 dependencies.push((
89 DependencyCategory::Build,
90 Box::new(NodePackageDependency::new(name)),
91 ));
92 }
93
94 Ok(dependencies)
95 }
96
97 fn name(&self) -> &str {
98 "node"
99 }
100
101 fn dist(
102 &self,
103 session: &dyn Session,
104 installer: &dyn crate::installer::Installer,
105 target_directory: &std::path::Path,
106 quiet: bool,
107 ) -> Result<std::ffi::OsString, crate::buildsystem::Error> {
108 self.setup(session, installer)?;
109 let dc = crate::dist_catcher::DistCatcher::new(vec![
110 session.external_path(std::path::Path::new("."))
111 ]);
112 session
113 .command(vec!["npm", "pack"])
114 .quiet(quiet)
115 .run_detecting_problems()?;
116 Ok(dc.copy_single(target_directory).unwrap().unwrap())
117 }
118
119 fn test(
120 &self,
121 session: &dyn crate::session::Session,
122 installer: &dyn crate::installer::Installer,
123 ) -> Result<(), crate::buildsystem::Error> {
124 self.setup(session, installer)?;
125 if let Some(test_script) = self.package.scripts.get("test") {
126 session
127 .command(vec!["bash", "-c", test_script])
128 .run_detecting_problems()?;
129 } else {
130 log::info!("No test command defined in package.json");
131 }
132 Ok(())
133 }
134
135 fn build(
136 &self,
137 session: &dyn crate::session::Session,
138 installer: &dyn crate::installer::Installer,
139 ) -> Result<(), crate::buildsystem::Error> {
140 self.setup(session, installer)?;
141 if let Some(build_script) = self.package.scripts.get("build") {
142 session
143 .command(vec!["bash", "-c", build_script])
144 .run_detecting_problems()?;
145 } else {
146 log::info!("No build command defined in package.json");
147 }
148 Ok(())
149 }
150
151 fn clean(
152 &self,
153 session: &dyn crate::session::Session,
154 installer: &dyn crate::installer::Installer,
155 ) -> Result<(), crate::buildsystem::Error> {
156 self.setup(session, installer)?;
157 if let Some(clean_script) = self.package.scripts.get("clean") {
158 session
159 .command(vec!["bash", "-c", clean_script])
160 .run_detecting_problems()?;
161 } else {
162 log::info!("No clean command defined in package.json");
163 }
164 Ok(())
165 }
166
167 fn install(
168 &self,
169 _session: &dyn crate::session::Session,
170 _installer: &dyn crate::installer::Installer,
171 _install_target: &crate::buildsystem::InstallTarget,
172 ) -> Result<(), crate::buildsystem::Error> {
173 Err(Error::Unimplemented)
174 }
175
176 fn get_declared_outputs(
177 &self,
178 _session: &dyn crate::session::Session,
179 _fixers: Option<&[&dyn crate::fix_build::BuildFixer<crate::installer::Error>]>,
180 ) -> Result<Vec<Box<dyn crate::output::Output>>, crate::buildsystem::Error> {
181 Err(Error::Unimplemented)
182 }
183
184 fn as_any(&self) -> &dyn std::any::Any {
185 self
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use tempfile::TempDir;
193
194 #[test]
195 fn test_node_detection_minimal_package() {
196 let temp_dir = TempDir::new().unwrap();
197 let project_dir = temp_dir.path();
198
199 std::fs::write(
201 project_dir.join("package.json"),
202 r#"{"name": "test-package", "version": "1.0.0"}"#,
203 )
204 .unwrap();
205
206 let result = Node::probe(project_dir);
207
208 match result {
209 Some(bs) => {
210 assert_eq!(bs.name(), "node");
211 }
212 None => {
213 panic!("Should detect node buildsystem with minimal package.json");
214 }
215 }
216 }
217
218 #[test]
219 fn test_node_detection_complex_package() {
220 let temp_dir = TempDir::new().unwrap();
221 let project_dir = temp_dir.path();
222
223 std::fs::write(
225 project_dir.join("package.json"),
226 r#"{
227 "name": "test-nodejs-package",
228 "version": "1.2.3",
229 "dependencies": {
230 "express": "^4.18.0",
231 "lodash": "^4.17.21"
232 },
233 "devDependencies": {
234 "jest": "^28.0.0"
235 },
236 "scripts": {
237 "test": "jest",
238 "build": "webpack"
239 }
240}"#,
241 )
242 .unwrap();
243
244 let result = Node::probe(project_dir);
245
246 assert!(
247 result.is_some(),
248 "Should detect node buildsystem with complex package.json"
249 );
250 }
251
252 #[test]
253 fn test_detect_buildsystems_integration() {
254 use crate::buildsystem::detect_buildsystems;
255
256 let temp_dir = TempDir::new().unwrap();
257 let project_dir = temp_dir.path();
258
259 std::fs::write(
261 project_dir.join("package.json"),
262 r#"{"name": "test-package", "version": "1.0.0"}"#,
263 )
264 .unwrap();
265
266 let buildsystems = detect_buildsystems(project_dir);
267
268 assert!(
269 !buildsystems.is_empty(),
270 "Should detect at least one buildsystem"
271 );
272
273 let has_node = buildsystems.iter().any(|bs| bs.name() == "node");
274 assert!(
275 has_node,
276 "Should detect node buildsystem. Found: {:?}",
277 buildsystems.iter().map(|bs| bs.name()).collect::<Vec<_>>()
278 );
279 }
280
281 #[test]
282 fn test_scoped_package_detection() {
283 let temp_dir = TempDir::new().unwrap();
284 let project_dir = temp_dir.path();
285
286 std::fs::write(
288 project_dir.join("package.json"),
289 r#"{"name": "@myorg/test-package", "version": "1.0.0"}"#,
290 )
291 .unwrap();
292
293 let result = Node::probe(project_dir);
294 assert!(
295 result.is_some(),
296 "Should detect node buildsystem with scoped package name"
297 );
298 }
299}