use log::{debug, info};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum JavaScriptRuntime {
Bun,
Node,
Deno,
Unknown,
}
impl JavaScriptRuntime {
pub fn as_str(&self) -> &str {
match self {
JavaScriptRuntime::Bun => "bun",
JavaScriptRuntime::Node => "node",
JavaScriptRuntime::Deno => "deno",
JavaScriptRuntime::Unknown => "unknown",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PackageManager {
Bun,
Npm,
Yarn,
Pnpm,
Unknown,
}
impl PackageManager {
pub fn as_str(&self) -> &str {
match self {
PackageManager::Bun => "bun",
PackageManager::Npm => "npm",
PackageManager::Yarn => "yarn",
PackageManager::Pnpm => "pnpm",
PackageManager::Unknown => "unknown",
}
}
pub fn lockfile_name(&self) -> &str {
match self {
PackageManager::Bun => "bun.lockb",
PackageManager::Npm => "package-lock.json",
PackageManager::Yarn => "yarn.lock",
PackageManager::Pnpm => "pnpm-lock.yaml",
PackageManager::Unknown => "",
}
}
pub fn audit_command(&self) -> &str {
match self {
PackageManager::Bun => "bun audit",
PackageManager::Npm => "npm audit",
PackageManager::Yarn => "yarn audit",
PackageManager::Pnpm => "pnpm audit",
PackageManager::Unknown => "",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeDetectionResult {
pub runtime: JavaScriptRuntime,
pub package_manager: PackageManager,
pub detected_lockfiles: Vec<String>,
pub has_package_json: bool,
pub has_engines_field: bool,
pub confidence: DetectionConfidence,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum DetectionConfidence {
High, Medium, Low, }
pub struct RuntimeDetector {
project_path: PathBuf,
}
impl RuntimeDetector {
pub fn new(project_path: PathBuf) -> Self {
Self { project_path }
}
pub fn detect_js_runtime_and_package_manager(&self) -> RuntimeDetectionResult {
debug!(
"Detecting JavaScript runtime and package manager for project: {}",
self.project_path.display()
);
let mut detected_lockfiles = Vec::new();
let has_package_json = self.project_path.join("package.json").exists();
debug!("Has package.json: {}", has_package_json);
let lockfile_detection = self.detect_by_lockfiles(&mut detected_lockfiles);
if let Some((runtime, manager)) = lockfile_detection {
info!(
"Detected {} runtime with {} package manager via lockfile",
runtime.as_str(),
manager.as_str()
);
return RuntimeDetectionResult {
runtime,
package_manager: manager,
detected_lockfiles,
has_package_json,
has_engines_field: false,
confidence: DetectionConfidence::High,
};
}
let engines_result = self.detect_by_engines_field();
if let Some((runtime, manager)) = engines_result {
info!(
"Detected {} runtime with {} package manager via engines field",
runtime.as_str(),
manager.as_str()
);
return RuntimeDetectionResult {
runtime,
package_manager: manager,
detected_lockfiles,
has_package_json,
has_engines_field: true,
confidence: DetectionConfidence::High,
};
}
if self.has_bun_specific_files() {
info!("Detected Bun-specific files, assuming Bun runtime");
return RuntimeDetectionResult {
runtime: JavaScriptRuntime::Bun,
package_manager: PackageManager::Bun,
detected_lockfiles,
has_package_json,
has_engines_field: false,
confidence: DetectionConfidence::Medium,
};
}
if has_package_json {
debug!(
"Package.json exists but no specific runtime detected, defaulting to Node.js with npm"
);
RuntimeDetectionResult {
runtime: JavaScriptRuntime::Node,
package_manager: PackageManager::Npm,
detected_lockfiles,
has_package_json,
has_engines_field: false,
confidence: DetectionConfidence::Low,
}
} else {
debug!("No package.json found, not a JavaScript project");
RuntimeDetectionResult {
runtime: JavaScriptRuntime::Unknown,
package_manager: PackageManager::Unknown,
detected_lockfiles,
has_package_json,
has_engines_field: false,
confidence: DetectionConfidence::Low,
}
}
}
pub fn detect_all_package_managers(&self) -> Vec<PackageManager> {
let mut managers = Vec::new();
if self.project_path.join("bun.lockb").exists() {
managers.push(PackageManager::Bun);
}
if self.project_path.join("pnpm-lock.yaml").exists() {
managers.push(PackageManager::Pnpm);
}
if self.project_path.join("yarn.lock").exists() {
managers.push(PackageManager::Yarn);
}
if self.project_path.join("package-lock.json").exists() {
managers.push(PackageManager::Npm);
}
managers
}
pub fn is_bun_project(&self) -> bool {
let result = self.detect_js_runtime_and_package_manager();
matches!(result.runtime, JavaScriptRuntime::Bun)
|| matches!(result.package_manager, PackageManager::Bun)
}
pub fn is_js_project(&self) -> bool {
self.project_path.join("package.json").exists()
|| self.project_path.join("bun.lockb").exists()
|| self.project_path.join("package-lock.json").exists()
|| self.project_path.join("yarn.lock").exists()
|| self.project_path.join("pnpm-lock.yaml").exists()
}
fn detect_by_lockfiles(
&self,
detected_lockfiles: &mut Vec<String>,
) -> Option<(JavaScriptRuntime, PackageManager)> {
if self.project_path.join("bun.lockb").exists() {
detected_lockfiles.push("bun.lockb".to_string());
debug!("Found bun.lockb, using Bun runtime and package manager");
return Some((JavaScriptRuntime::Bun, PackageManager::Bun));
}
if self.project_path.join("pnpm-lock.yaml").exists() {
detected_lockfiles.push("pnpm-lock.yaml".to_string());
debug!("Found pnpm-lock.yaml, using Node.js runtime with pnpm");
return Some((JavaScriptRuntime::Node, PackageManager::Pnpm));
}
if self.project_path.join("yarn.lock").exists() {
detected_lockfiles.push("yarn.lock".to_string());
debug!("Found yarn.lock, using Node.js runtime with Yarn");
return Some((JavaScriptRuntime::Node, PackageManager::Yarn));
}
if self.project_path.join("package-lock.json").exists() {
detected_lockfiles.push("package-lock.json".to_string());
debug!("Found package-lock.json, using Node.js runtime with npm");
return Some((JavaScriptRuntime::Node, PackageManager::Npm));
}
None
}
fn detect_by_engines_field(&self) -> Option<(JavaScriptRuntime, PackageManager)> {
let package_json_path = self.project_path.join("package.json");
if !package_json_path.exists() {
return None;
}
match self.read_package_json() {
Ok(package_json) => {
if let Some(engines) = package_json.get("engines") {
debug!("Found engines field in package.json: {:?}", engines);
if engines.get("bun").is_some() {
debug!("Found bun engine specification");
return Some((JavaScriptRuntime::Bun, PackageManager::Bun));
}
if engines.get("deno").is_some() {
debug!("Found deno engine specification");
return Some((JavaScriptRuntime::Deno, PackageManager::Unknown));
}
if engines.get("node").is_some() {
debug!("Found node engine specification, using npm as default");
return Some((JavaScriptRuntime::Node, PackageManager::Npm));
}
}
if let Some(package_manager) = package_json
.get("packageManager")
.and_then(|pm| pm.as_str())
{
debug!("Found packageManager field: {}", package_manager);
if package_manager.starts_with("bun") {
return Some((JavaScriptRuntime::Bun, PackageManager::Bun));
} else if package_manager.starts_with("pnpm") {
return Some((JavaScriptRuntime::Node, PackageManager::Pnpm));
} else if package_manager.starts_with("yarn") {
return Some((JavaScriptRuntime::Node, PackageManager::Yarn));
} else if package_manager.starts_with("npm") {
return Some((JavaScriptRuntime::Node, PackageManager::Npm));
}
}
}
Err(e) => {
debug!("Failed to read package.json: {}", e);
}
}
None
}
fn has_bun_specific_files(&self) -> bool {
if self.project_path.join("bunfig.toml").exists() {
debug!("Found bunfig.toml");
return true;
}
if self.project_path.join(".bunfig.toml").exists() {
debug!("Found .bunfig.toml");
return true;
}
if let Ok(package_json) = self.read_package_json()
&& let Some(scripts) = package_json.get("scripts").and_then(|s| s.as_object())
{
for script in scripts.values() {
if let Some(script_str) = script.as_str()
&& (script_str.contains("bun ") || script_str.starts_with("bun"))
{
debug!("Found Bun command in scripts: {}", script_str);
return true;
}
}
}
false
}
fn read_package_json(&self) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
let package_json_path = self.project_path.join("package.json");
let content = fs::read_to_string(package_json_path)?;
let json: serde_json::Value = serde_json::from_str(&content)?;
Ok(json)
}
pub fn get_audit_commands(&self) -> Vec<String> {
let result = self.detect_js_runtime_and_package_manager();
let mut commands = Vec::new();
commands.push(result.package_manager.audit_command().to_string());
let all_managers = self.detect_all_package_managers();
for manager in all_managers {
let cmd = manager.audit_command().to_string();
if !commands.contains(&cmd) {
commands.push(cmd);
}
}
commands
}
pub fn get_detection_summary(&self) -> String {
let result = self.detect_js_runtime_and_package_manager();
let confidence_str = match result.confidence {
DetectionConfidence::High => "high confidence",
DetectionConfidence::Medium => "medium confidence",
DetectionConfidence::Low => "low confidence (default)",
};
let mut summary = format!(
"Detected {} runtime with {} package manager ({})",
result.runtime.as_str(),
result.package_manager.as_str(),
confidence_str
);
if !result.detected_lockfiles.is_empty() {
summary.push_str(&format!(
" - Lock files: {}",
result.detected_lockfiles.join(", ")
));
}
if result.has_engines_field {
summary.push_str(" - Engines field present");
}
summary
}
}