use std::path::{Path, PathBuf};
use vize_carton::append;
use vize_carton::cstr;
use vize_carton::String;
#[derive(Debug, thiserror::Error)]
pub enum TsgoError {
#[error("{0}")]
TsgoNotFound(#[from] TsgoNotFoundError),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("SFC parse error: {0}")]
SfcParse(String),
#[error("Path error: {path}")]
PathError { path: PathBuf },
#[error("tsgo error (exit code {exit_code}): {message}")]
TsgoExecution { exit_code: i32, message: String },
#[error("JSON parse error: {0}")]
JsonParse(#[from] serde_json::Error),
#[error("Virtual project not initialized. Call scan_project() first.")]
NotInitialized,
#[error("Failed to strip prefix from path: {0}")]
StripPrefix(#[from] std::path::StripPrefixError),
#[error("Directory walk error: {0}")]
WalkDir(#[from] walkdir::Error),
}
pub type TsgoResult<T> = Result<T, TsgoError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PackageManager {
Pnpm,
Npm,
Yarn,
Bun,
}
#[derive(Debug)]
pub struct TsgoNotFoundError {
detected_pm: Option<PackageManager>,
}
impl TsgoNotFoundError {
pub fn new(project_root: &Path) -> Self {
let detected_pm = detect_package_manager(project_root);
Self { detected_pm }
}
pub fn detected_package_manager(&self) -> Option<PackageManager> {
self.detected_pm
}
pub fn display_message(&self) -> String {
let mut msg = String::default();
msg.push_str("error: tsgo not found\n\n");
msg.push_str("vize check requires '@typescript/native-preview' to be installed.\n\n");
if let Some(pm) = self.detected_pm {
msg.push_str("To install, run:\n\n");
append!(msg, " {}\n", self.install_command(pm));
} else {
msg.push_str("To install, run one of the following:\n\n");
append!(
msg,
" {} # npm\n",
self.install_command(PackageManager::Npm)
);
append!(
msg,
" {} # pnpm\n",
self.install_command(PackageManager::Pnpm)
);
append!(
msg,
" {} # yarn\n",
self.install_command(PackageManager::Yarn)
);
append!(
msg,
" {} # bun\n",
self.install_command(PackageManager::Bun)
);
}
msg
}
fn install_command(&self, pm: PackageManager) -> String {
match pm {
PackageManager::Npm => cstr!("npm install -D @typescript/native-preview"),
PackageManager::Pnpm => cstr!("pnpm add -D @typescript/native-preview"),
PackageManager::Yarn => cstr!("yarn add -D @typescript/native-preview"),
PackageManager::Bun => cstr!("bun add -D @typescript/native-preview"),
}
}
}
impl std::fmt::Display for TsgoNotFoundError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.display_message())
}
}
impl std::error::Error for TsgoNotFoundError {}
pub fn detect_package_manager(project_root: &Path) -> Option<PackageManager> {
if project_root.join("pnpm-lock.yaml").exists() {
return Some(PackageManager::Pnpm);
}
if project_root.join("bun.lockb").exists() || project_root.join("bun.lock").exists() {
return Some(PackageManager::Bun);
}
if project_root.join("yarn.lock").exists() {
return Some(PackageManager::Yarn);
}
if project_root.join("package-lock.json").exists() {
return Some(PackageManager::Npm);
}
let pkg_json = project_root.join("package.json");
if let Ok(content) = std::fs::read_to_string(&pkg_json) {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
if let Some(pm) = json.get("packageManager").and_then(|v| v.as_str()) {
if pm.starts_with("pnpm") {
return Some(PackageManager::Pnpm);
}
if pm.starts_with("yarn") {
return Some(PackageManager::Yarn);
}
if pm.starts_with("bun") {
return Some(PackageManager::Bun);
}
if pm.starts_with("npm") {
return Some(PackageManager::Npm);
}
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::{PackageManager, TsgoNotFoundError};
#[test]
fn test_tsgo_not_found_error_message() {
let error = TsgoNotFoundError {
detected_pm: Some(PackageManager::Pnpm),
};
let msg = error.display_message();
assert!(msg.contains("pnpm add -D @typescript/native-preview"));
}
#[test]
fn test_tsgo_not_found_no_pm() {
let error = TsgoNotFoundError { detected_pm: None };
let msg = error.display_message();
assert!(msg.contains("npm install"));
assert!(msg.contains("pnpm add"));
assert!(msg.contains("yarn add"));
assert!(msg.contains("bun add"));
}
}