morph_cli/core/format/
prettier.rs1use 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}