syncable_cli/analyzer/runtime/
javascript.rs1use log::{debug, info};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub enum JavaScriptRuntime {
9 Bun,
10 Node,
11 Deno,
12 Unknown,
13}
14
15impl JavaScriptRuntime {
16 pub fn as_str(&self) -> &str {
17 match self {
18 JavaScriptRuntime::Bun => "bun",
19 JavaScriptRuntime::Node => "node",
20 JavaScriptRuntime::Deno => "deno",
21 JavaScriptRuntime::Unknown => "unknown",
22 }
23 }
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub enum PackageManager {
29 Bun,
30 Npm,
31 Yarn,
32 Pnpm,
33 Unknown,
34}
35
36impl PackageManager {
37 pub fn as_str(&self) -> &str {
38 match self {
39 PackageManager::Bun => "bun",
40 PackageManager::Npm => "npm",
41 PackageManager::Yarn => "yarn",
42 PackageManager::Pnpm => "pnpm",
43 PackageManager::Unknown => "unknown",
44 }
45 }
46
47 pub fn lockfile_name(&self) -> &str {
48 match self {
49 PackageManager::Bun => "bun.lockb",
50 PackageManager::Npm => "package-lock.json",
51 PackageManager::Yarn => "yarn.lock",
52 PackageManager::Pnpm => "pnpm-lock.yaml",
53 PackageManager::Unknown => "",
54 }
55 }
56
57 pub fn audit_command(&self) -> &str {
58 match self {
59 PackageManager::Bun => "bun audit",
60 PackageManager::Npm => "npm audit",
61 PackageManager::Yarn => "yarn audit",
62 PackageManager::Pnpm => "pnpm audit",
63 PackageManager::Unknown => "",
64 }
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct RuntimeDetectionResult {
71 pub runtime: JavaScriptRuntime,
72 pub package_manager: PackageManager,
73 pub detected_lockfiles: Vec<String>,
74 pub has_package_json: bool,
75 pub has_engines_field: bool,
76 pub confidence: DetectionConfidence,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
81pub enum DetectionConfidence {
82 High, Medium, Low, }
86
87pub struct RuntimeDetector {
89 project_path: PathBuf,
90}
91
92impl RuntimeDetector {
93 pub fn new(project_path: PathBuf) -> Self {
94 Self { project_path }
95 }
96
97 pub fn detect_js_runtime_and_package_manager(&self) -> RuntimeDetectionResult {
99 debug!(
100 "Detecting JavaScript runtime and package manager for project: {}",
101 self.project_path.display()
102 );
103
104 let mut detected_lockfiles = Vec::new();
105 let has_package_json = self.project_path.join("package.json").exists();
106
107 debug!("Has package.json: {}", has_package_json);
108
109 let lockfile_detection = self.detect_by_lockfiles(&mut detected_lockfiles);
111 if let Some((runtime, manager)) = lockfile_detection {
112 info!(
113 "Detected {} runtime with {} package manager via lockfile",
114 runtime.as_str(),
115 manager.as_str()
116 );
117 return RuntimeDetectionResult {
118 runtime,
119 package_manager: manager,
120 detected_lockfiles,
121 has_package_json,
122 has_engines_field: false,
123 confidence: DetectionConfidence::High,
124 };
125 }
126
127 let engines_result = self.detect_by_engines_field();
129 if let Some((runtime, manager)) = engines_result {
130 info!(
131 "Detected {} runtime with {} package manager via engines field",
132 runtime.as_str(),
133 manager.as_str()
134 );
135 return RuntimeDetectionResult {
136 runtime,
137 package_manager: manager,
138 detected_lockfiles,
139 has_package_json,
140 has_engines_field: true,
141 confidence: DetectionConfidence::High,
142 };
143 }
144
145 if self.has_bun_specific_files() {
147 info!("Detected Bun-specific files, assuming Bun runtime");
148 return RuntimeDetectionResult {
149 runtime: JavaScriptRuntime::Bun,
150 package_manager: PackageManager::Bun,
151 detected_lockfiles,
152 has_package_json,
153 has_engines_field: false,
154 confidence: DetectionConfidence::Medium,
155 };
156 }
157
158 if has_package_json {
160 debug!(
161 "Package.json exists but no specific runtime detected, defaulting to Node.js with npm"
162 );
163 RuntimeDetectionResult {
164 runtime: JavaScriptRuntime::Node,
165 package_manager: PackageManager::Npm,
166 detected_lockfiles,
167 has_package_json,
168 has_engines_field: false,
169 confidence: DetectionConfidence::Low,
170 }
171 } else {
172 debug!("No package.json found, not a JavaScript project");
173 RuntimeDetectionResult {
174 runtime: JavaScriptRuntime::Unknown,
175 package_manager: PackageManager::Unknown,
176 detected_lockfiles,
177 has_package_json,
178 has_engines_field: false,
179 confidence: DetectionConfidence::Low,
180 }
181 }
182 }
183
184 pub fn detect_all_package_managers(&self) -> Vec<PackageManager> {
186 let mut managers = Vec::new();
187
188 if self.project_path.join("bun.lockb").exists() {
189 managers.push(PackageManager::Bun);
190 }
191 if self.project_path.join("pnpm-lock.yaml").exists() {
192 managers.push(PackageManager::Pnpm);
193 }
194 if self.project_path.join("yarn.lock").exists() {
195 managers.push(PackageManager::Yarn);
196 }
197 if self.project_path.join("package-lock.json").exists() {
198 managers.push(PackageManager::Npm);
199 }
200
201 managers
202 }
203
204 pub fn is_bun_project(&self) -> bool {
206 let result = self.detect_js_runtime_and_package_manager();
207 matches!(result.runtime, JavaScriptRuntime::Bun)
208 || matches!(result.package_manager, PackageManager::Bun)
209 }
210
211 pub fn is_js_project(&self) -> bool {
213 self.project_path.join("package.json").exists()
214 || self.project_path.join("bun.lockb").exists()
215 || self.project_path.join("package-lock.json").exists()
216 || self.project_path.join("yarn.lock").exists()
217 || self.project_path.join("pnpm-lock.yaml").exists()
218 }
219
220 fn detect_by_lockfiles(
222 &self,
223 detected_lockfiles: &mut Vec<String>,
224 ) -> Option<(JavaScriptRuntime, PackageManager)> {
225 if self.project_path.join("bun.lockb").exists() {
227 detected_lockfiles.push("bun.lockb".to_string());
228 debug!("Found bun.lockb, using Bun runtime and package manager");
229 return Some((JavaScriptRuntime::Bun, PackageManager::Bun));
230 }
231
232 if self.project_path.join("pnpm-lock.yaml").exists() {
234 detected_lockfiles.push("pnpm-lock.yaml".to_string());
235 debug!("Found pnpm-lock.yaml, using Node.js runtime with pnpm");
236 return Some((JavaScriptRuntime::Node, PackageManager::Pnpm));
237 }
238
239 if self.project_path.join("yarn.lock").exists() {
241 detected_lockfiles.push("yarn.lock".to_string());
242 debug!("Found yarn.lock, using Node.js runtime with Yarn");
243 return Some((JavaScriptRuntime::Node, PackageManager::Yarn));
244 }
245
246 if self.project_path.join("package-lock.json").exists() {
248 detected_lockfiles.push("package-lock.json".to_string());
249 debug!("Found package-lock.json, using Node.js runtime with npm");
250 return Some((JavaScriptRuntime::Node, PackageManager::Npm));
251 }
252
253 None
254 }
255
256 fn detect_by_engines_field(&self) -> Option<(JavaScriptRuntime, PackageManager)> {
258 let package_json_path = self.project_path.join("package.json");
259 if !package_json_path.exists() {
260 return None;
261 }
262
263 match self.read_package_json() {
264 Ok(package_json) => {
265 if let Some(engines) = package_json.get("engines") {
266 debug!("Found engines field in package.json: {:?}", engines);
267
268 if engines.get("bun").is_some() {
270 debug!("Found bun engine specification");
271 return Some((JavaScriptRuntime::Bun, PackageManager::Bun));
272 }
273
274 if engines.get("deno").is_some() {
276 debug!("Found deno engine specification");
277 return Some((JavaScriptRuntime::Deno, PackageManager::Unknown));
278 }
279
280 if engines.get("node").is_some() {
282 debug!("Found node engine specification, using npm as default");
283 return Some((JavaScriptRuntime::Node, PackageManager::Npm));
284 }
285 }
286
287 if let Some(package_manager) = package_json
289 .get("packageManager")
290 .and_then(|pm| pm.as_str())
291 {
292 debug!("Found packageManager field: {}", package_manager);
293
294 if package_manager.starts_with("bun") {
295 return Some((JavaScriptRuntime::Bun, PackageManager::Bun));
296 } else if package_manager.starts_with("pnpm") {
297 return Some((JavaScriptRuntime::Node, PackageManager::Pnpm));
298 } else if package_manager.starts_with("yarn") {
299 return Some((JavaScriptRuntime::Node, PackageManager::Yarn));
300 } else if package_manager.starts_with("npm") {
301 return Some((JavaScriptRuntime::Node, PackageManager::Npm));
302 }
303 }
304 }
305 Err(e) => {
306 debug!("Failed to read package.json: {}", e);
307 }
308 }
309
310 None
311 }
312
313 fn has_bun_specific_files(&self) -> bool {
315 if self.project_path.join("bunfig.toml").exists() {
317 debug!("Found bunfig.toml");
318 return true;
319 }
320
321 if self.project_path.join(".bunfig.toml").exists() {
323 debug!("Found .bunfig.toml");
324 return true;
325 }
326
327 if let Ok(package_json) = self.read_package_json()
329 && let Some(scripts) = package_json.get("scripts").and_then(|s| s.as_object())
330 {
331 for script in scripts.values() {
332 if let Some(script_str) = script.as_str()
333 && (script_str.contains("bun ") || script_str.starts_with("bun"))
334 {
335 debug!("Found Bun command in scripts: {}", script_str);
336 return true;
337 }
338 }
339 }
340
341 false
342 }
343
344 fn read_package_json(&self) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
346 let package_json_path = self.project_path.join("package.json");
347 let content = fs::read_to_string(package_json_path)?;
348 let json: serde_json::Value = serde_json::from_str(&content)?;
349 Ok(json)
350 }
351
352 pub fn get_audit_commands(&self) -> Vec<String> {
354 let result = self.detect_js_runtime_and_package_manager();
355 let mut commands = Vec::new();
356
357 commands.push(result.package_manager.audit_command().to_string());
359
360 let all_managers = self.detect_all_package_managers();
362 for manager in all_managers {
363 let cmd = manager.audit_command().to_string();
364 if !commands.contains(&cmd) {
365 commands.push(cmd);
366 }
367 }
368
369 commands
370 }
371
372 pub fn get_detection_summary(&self) -> String {
374 let result = self.detect_js_runtime_and_package_manager();
375
376 let confidence_str = match result.confidence {
377 DetectionConfidence::High => "high confidence",
378 DetectionConfidence::Medium => "medium confidence",
379 DetectionConfidence::Low => "low confidence (default)",
380 };
381
382 let mut summary = format!(
383 "Detected {} runtime with {} package manager ({})",
384 result.runtime.as_str(),
385 result.package_manager.as_str(),
386 confidence_str
387 );
388
389 if !result.detected_lockfiles.is_empty() {
390 summary.push_str(&format!(
391 " - Lock files: {}",
392 result.detected_lockfiles.join(", ")
393 ));
394 }
395
396 if result.has_engines_field {
397 summary.push_str(" - Engines field present");
398 }
399
400 summary
401 }
402}