Skip to main content

morph_cli/core/format/
prettier.rs

1use std::path::Path;
2use std::process::Command;
3
4pub fn detect_prettier_config(path: &Path) -> Option<String> {
5    let mut current = Some(path.to_path_buf());
6
7    while let Some(p) = current {
8        for config_file in &[
9            ".prettierrc",
10            ".prettierrc.json",
11            ".prettierrc.js",
12            ".prettierrc.cjs",
13            "prettier.config.js",
14            "prettier.config.cjs",
15        ] {
16            let config_path = p.join(config_file);
17            if config_path.exists() {
18                return Some(config_path.to_string_lossy().to_string());
19            }
20        }
21        if let Ok(content) = std::fs::read_to_string(p.join("package.json"))
22            && content.contains("prettier")
23        {
24            return Some("package.json".to_string());
25        }
26        current = p.parent().map(|p| p.to_path_buf());
27    }
28    None
29}
30
31pub fn prettier_available() -> bool {
32    Command::new("prettier")
33        .arg("--version")
34        .output()
35        .map(|o| o.status.success())
36        .unwrap_or(false)
37}
38
39pub fn format_with_prettier(source: &str, path: &Path) -> Result<String, String> {
40    if !prettier_available() {
41        return Err("Prettier not available".to_string());
42    }
43
44    let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("js");
45
46    let parser = match ext {
47        "js" | "jsx" | "mjs" => "babel",
48        "ts" | "tsx" => "typescript",
49        "json" => "json",
50        "md" => "markdown",
51        "css" | "scss" | "less" => "css",
52        "html" | "vue" | "svelte" => "html",
53        _ => "babel",
54    };
55
56    let mut cmd = Command::new("prettier");
57    cmd.arg("--parser").arg(parser).arg("--write").arg("-");
58
59    if detect_prettier_config(path).is_some() {
60        cmd.arg("--config").arg("auto");
61    }
62
63    let mut child = cmd
64        .stdin(std::process::Stdio::piped())
65        .stdout(std::process::Stdio::piped())
66        .stderr(std::process::Stdio::piped())
67        .spawn()
68        .map_err(|e| e.to_string())?;
69
70    use std::io::Write;
71    if let Some(mut stdin) = child.stdin.take() {
72        stdin
73            .write_all(source.as_bytes())
74            .map_err(|e| e.to_string())?;
75    }
76
77    let output = child.wait_with_output().map_err(|e| e.to_string())?;
78
79    if output.status.success() {
80        String::from_utf8(output.stdout).map_err(|e| e.to_string())
81    } else {
82        Err(String::from_utf8_lossy(&output.stderr).to_string())
83    }
84}
85
86pub fn format_with_fallback(source: &str) -> String {
87    source.to_string()
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn test_prettier_available() {
96        let available = prettier_available();
97        assert!(available || !available);
98    }
99
100    #[test]
101    fn test_detect_prettier_config_none() {
102        let config = detect_prettier_config(Path::new("/tmp/nonexistent/file.js"));
103        assert!(config.is_none() || config.is_some());
104    }
105
106    #[test]
107    fn test_format_with_fallback() {
108        let result = format_with_fallback("const x = 1;");
109        assert_eq!(result, "const x = 1;");
110    }
111}