morph-cli 0.1.0

AST-based codebase migration and codemod tool for JavaScript and TypeScript projects.
Documentation
use std::path::Path;
use std::process::Command;

pub fn detect_prettier_config(path: &Path) -> Option<String> {
    let mut current = Some(path.to_path_buf());

    while let Some(p) = current {
        for config_file in &[
            ".prettierrc",
            ".prettierrc.json",
            ".prettierrc.js",
            ".prettierrc.cjs",
            "prettier.config.js",
            "prettier.config.cjs",
        ] {
            let config_path = p.join(config_file);
            if config_path.exists() {
                return Some(config_path.to_string_lossy().to_string());
            }
        }
        if let Ok(content) = std::fs::read_to_string(p.join("package.json"))
            && content.contains("prettier")
        {
            return Some("package.json".to_string());
        }
        current = p.parent().map(|p| p.to_path_buf());
    }
    None
}

pub fn prettier_available() -> bool {
    Command::new("prettier")
        .arg("--version")
        .output()
        .map(|o| o.status.success())
        .unwrap_or(false)
}

pub fn format_with_prettier(source: &str, path: &Path) -> Result<String, String> {
    if !prettier_available() {
        return Err("Prettier not available".to_string());
    }

    let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("js");

    let parser = match ext {
        "js" | "jsx" | "mjs" => "babel",
        "ts" | "tsx" => "typescript",
        "json" => "json",
        "md" => "markdown",
        "css" | "scss" | "less" => "css",
        "html" | "vue" | "svelte" => "html",
        _ => "babel",
    };

    let mut cmd = Command::new("prettier");
    cmd.arg("--parser").arg(parser).arg("--write").arg("-");

    if detect_prettier_config(path).is_some() {
        cmd.arg("--config").arg("auto");
    }

    let mut child = cmd
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .map_err(|e| e.to_string())?;

    use std::io::Write;
    if let Some(mut stdin) = child.stdin.take() {
        stdin
            .write_all(source.as_bytes())
            .map_err(|e| e.to_string())?;
    }

    let output = child.wait_with_output().map_err(|e| e.to_string())?;

    if output.status.success() {
        String::from_utf8(output.stdout).map_err(|e| e.to_string())
    } else {
        Err(String::from_utf8_lossy(&output.stderr).to_string())
    }
}

pub fn format_with_fallback(source: &str) -> String {
    source.to_string()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_prettier_available() {
        let available = prettier_available();
        assert!(available || !available);
    }

    #[test]
    fn test_detect_prettier_config_none() {
        let config = detect_prettier_config(Path::new("/tmp/nonexistent/file.js"));
        assert!(config.is_none() || config.is_some());
    }

    #[test]
    fn test_format_with_fallback() {
        let result = format_with_fallback("const x = 1;");
        assert_eq!(result, "const x = 1;");
    }
}