syncable_cli/analyzer/runtime/
javascript.rs1use std::path::PathBuf;
2use std::fs;
3use serde::{Deserialize, Serialize};
4use log::{debug, info};
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!("Detecting JavaScript runtime and package manager for project: {}", self.project_path.display());
100
101 let mut detected_lockfiles = Vec::new();
102 let has_package_json = self.project_path.join("package.json").exists();
103
104 debug!("Has package.json: {}", has_package_json);
105
106 let lockfile_detection = self.detect_by_lockfiles(&mut detected_lockfiles);
108 if let Some((runtime, manager)) = lockfile_detection {
109 info!("Detected {} runtime with {} package manager via lockfile", runtime.as_str(), manager.as_str());
110 return RuntimeDetectionResult {
111 runtime,
112 package_manager: manager,
113 detected_lockfiles,
114 has_package_json,
115 has_engines_field: false,
116 confidence: DetectionConfidence::High,
117 };
118 }
119
120 let engines_result = self.detect_by_engines_field();
122 if let Some((runtime, manager)) = engines_result {
123 info!("Detected {} runtime with {} package manager via engines field", runtime.as_str(), manager.as_str());
124 return RuntimeDetectionResult {
125 runtime,
126 package_manager: manager,
127 detected_lockfiles,
128 has_package_json,
129 has_engines_field: true,
130 confidence: DetectionConfidence::High,
131 };
132 }
133
134 if self.has_bun_specific_files() {
136 info!("Detected Bun-specific files, assuming Bun runtime");
137 return RuntimeDetectionResult {
138 runtime: JavaScriptRuntime::Bun,
139 package_manager: PackageManager::Bun,
140 detected_lockfiles,
141 has_package_json,
142 has_engines_field: false,
143 confidence: DetectionConfidence::Medium,
144 };
145 }
146
147 if has_package_json {
149 debug!("Package.json exists but no specific runtime detected, defaulting to Node.js with npm");
150 RuntimeDetectionResult {
151 runtime: JavaScriptRuntime::Node,
152 package_manager: PackageManager::Npm,
153 detected_lockfiles,
154 has_package_json,
155 has_engines_field: false,
156 confidence: DetectionConfidence::Low,
157 }
158 } else {
159 debug!("No package.json found, not a JavaScript project");
160 RuntimeDetectionResult {
161 runtime: JavaScriptRuntime::Unknown,
162 package_manager: PackageManager::Unknown,
163 detected_lockfiles,
164 has_package_json,
165 has_engines_field: false,
166 confidence: DetectionConfidence::Low,
167 }
168 }
169 }
170
171 pub fn detect_all_package_managers(&self) -> Vec<PackageManager> {
173 let mut managers = Vec::new();
174
175 if self.project_path.join("bun.lockb").exists() {
176 managers.push(PackageManager::Bun);
177 }
178 if self.project_path.join("pnpm-lock.yaml").exists() {
179 managers.push(PackageManager::Pnpm);
180 }
181 if self.project_path.join("yarn.lock").exists() {
182 managers.push(PackageManager::Yarn);
183 }
184 if self.project_path.join("package-lock.json").exists() {
185 managers.push(PackageManager::Npm);
186 }
187
188 managers
189 }
190
191 pub fn is_bun_project(&self) -> bool {
193 let result = self.detect_js_runtime_and_package_manager();
194 matches!(result.runtime, JavaScriptRuntime::Bun) ||
195 matches!(result.package_manager, PackageManager::Bun)
196 }
197
198 pub fn is_js_project(&self) -> bool {
200 self.project_path.join("package.json").exists() ||
201 self.project_path.join("bun.lockb").exists() ||
202 self.project_path.join("package-lock.json").exists() ||
203 self.project_path.join("yarn.lock").exists() ||
204 self.project_path.join("pnpm-lock.yaml").exists()
205 }
206
207 fn detect_by_lockfiles(&self, detected_lockfiles: &mut Vec<String>) -> Option<(JavaScriptRuntime, PackageManager)> {
209 if self.project_path.join("bun.lockb").exists() {
211 detected_lockfiles.push("bun.lockb".to_string());
212 debug!("Found bun.lockb, using Bun runtime and package manager");
213 return Some((JavaScriptRuntime::Bun, PackageManager::Bun));
214 }
215
216 if self.project_path.join("pnpm-lock.yaml").exists() {
218 detected_lockfiles.push("pnpm-lock.yaml".to_string());
219 debug!("Found pnpm-lock.yaml, using Node.js runtime with pnpm");
220 return Some((JavaScriptRuntime::Node, PackageManager::Pnpm));
221 }
222
223 if self.project_path.join("yarn.lock").exists() {
225 detected_lockfiles.push("yarn.lock".to_string());
226 debug!("Found yarn.lock, using Node.js runtime with Yarn");
227 return Some((JavaScriptRuntime::Node, PackageManager::Yarn));
228 }
229
230 if self.project_path.join("package-lock.json").exists() {
232 detected_lockfiles.push("package-lock.json".to_string());
233 debug!("Found package-lock.json, using Node.js runtime with npm");
234 return Some((JavaScriptRuntime::Node, PackageManager::Npm));
235 }
236
237 None
238 }
239
240 fn detect_by_engines_field(&self) -> Option<(JavaScriptRuntime, PackageManager)> {
242 let package_json_path = self.project_path.join("package.json");
243 if !package_json_path.exists() {
244 return None;
245 }
246
247 match self.read_package_json() {
248 Ok(package_json) => {
249 if let Some(engines) = package_json.get("engines") {
250 debug!("Found engines field in package.json: {:?}", engines);
251
252 if engines.get("bun").is_some() {
254 debug!("Found bun engine specification");
255 return Some((JavaScriptRuntime::Bun, PackageManager::Bun));
256 }
257
258 if engines.get("deno").is_some() {
260 debug!("Found deno engine specification");
261 return Some((JavaScriptRuntime::Deno, PackageManager::Unknown));
262 }
263
264 if engines.get("node").is_some() {
266 debug!("Found node engine specification, using npm as default");
267 return Some((JavaScriptRuntime::Node, PackageManager::Npm));
268 }
269 }
270
271 if let Some(package_manager) = package_json.get("packageManager").and_then(|pm| pm.as_str()) {
273 debug!("Found packageManager field: {}", package_manager);
274
275 if package_manager.starts_with("bun") {
276 return Some((JavaScriptRuntime::Bun, PackageManager::Bun));
277 } else if package_manager.starts_with("pnpm") {
278 return Some((JavaScriptRuntime::Node, PackageManager::Pnpm));
279 } else if package_manager.starts_with("yarn") {
280 return Some((JavaScriptRuntime::Node, PackageManager::Yarn));
281 } else if package_manager.starts_with("npm") {
282 return Some((JavaScriptRuntime::Node, PackageManager::Npm));
283 }
284 }
285 }
286 Err(e) => {
287 debug!("Failed to read package.json: {}", e);
288 }
289 }
290
291 None
292 }
293
294 fn has_bun_specific_files(&self) -> bool {
296 if self.project_path.join("bunfig.toml").exists() {
298 debug!("Found bunfig.toml");
299 return true;
300 }
301
302 if self.project_path.join(".bunfig.toml").exists() {
304 debug!("Found .bunfig.toml");
305 return true;
306 }
307
308 if let Ok(package_json) = self.read_package_json() {
310 if let Some(scripts) = package_json.get("scripts").and_then(|s| s.as_object()) {
311 for script in scripts.values() {
312 if let Some(script_str) = script.as_str() {
313 if script_str.contains("bun ") || script_str.starts_with("bun") {
314 debug!("Found Bun command in scripts: {}", script_str);
315 return true;
316 }
317 }
318 }
319 }
320 }
321
322 false
323 }
324
325 fn read_package_json(&self) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
327 let package_json_path = self.project_path.join("package.json");
328 let content = fs::read_to_string(package_json_path)?;
329 let json: serde_json::Value = serde_json::from_str(&content)?;
330 Ok(json)
331 }
332
333 pub fn get_audit_commands(&self) -> Vec<String> {
335 let result = self.detect_js_runtime_and_package_manager();
336 let mut commands = Vec::new();
337
338 commands.push(result.package_manager.audit_command().to_string());
340
341 let all_managers = self.detect_all_package_managers();
343 for manager in all_managers {
344 let cmd = manager.audit_command().to_string();
345 if !commands.contains(&cmd) {
346 commands.push(cmd);
347 }
348 }
349
350 commands
351 }
352
353 pub fn get_detection_summary(&self) -> String {
355 let result = self.detect_js_runtime_and_package_manager();
356
357 let confidence_str = match result.confidence {
358 DetectionConfidence::High => "high confidence",
359 DetectionConfidence::Medium => "medium confidence",
360 DetectionConfidence::Low => "low confidence (default)",
361 };
362
363 let mut summary = format!(
364 "Detected {} runtime with {} package manager ({})",
365 result.runtime.as_str(),
366 result.package_manager.as_str(),
367 confidence_str
368 );
369
370 if !result.detected_lockfiles.is_empty() {
371 summary.push_str(&format!(" - Lock files: {}", result.detected_lockfiles.join(", ")));
372 }
373
374 if result.has_engines_field {
375 summary.push_str(" - Engines field present");
376 }
377
378 summary
379 }
380}