fallow_config/
fixability.rs1use std::path::{Path, PathBuf};
2
3use crate::FallowConfig;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum ConfigFixPlan {
8 Edit {
10 config_path: PathBuf,
12 },
13 BlockedMonorepo {
16 workspace_root: PathBuf,
18 },
19 BlockedNoCreate {
21 target: PathBuf,
23 },
24 Create {
26 target: PathBuf,
28 },
29}
30
31#[must_use]
33pub fn classify_config_fix_plan(
34 root: &Path,
35 explicit: Option<&PathBuf>,
36 no_create_config: bool,
37) -> ConfigFixPlan {
38 if let Some(existing) = resolve_existing_config_path(root, explicit) {
39 return ConfigFixPlan::Edit {
40 config_path: existing,
41 };
42 }
43 let target = root.join(".fallowrc.json");
44 if let Some(workspace_root) = find_workspace_root_above(root) {
45 return ConfigFixPlan::BlockedMonorepo { workspace_root };
46 }
47 if no_create_config {
48 return ConfigFixPlan::BlockedNoCreate { target };
49 }
50 ConfigFixPlan::Create { target }
51}
52
53#[must_use]
56pub fn is_config_fixable(root: &Path, explicit: Option<&PathBuf>) -> bool {
57 matches!(
58 classify_config_fix_plan(root, explicit, false),
59 ConfigFixPlan::Edit { .. } | ConfigFixPlan::Create { .. }
60 )
61}
62
63fn resolve_existing_config_path(root: &Path, explicit: Option<&PathBuf>) -> Option<PathBuf> {
64 if let Some(path) = explicit {
65 let absolute = if path.is_absolute() {
66 path.clone()
67 } else {
68 std::env::current_dir().map_or_else(|_| path.clone(), |cwd| cwd.join(path))
69 };
70 if absolute.exists() {
71 return Some(absolute);
72 }
73 return None;
74 }
75 FallowConfig::find_config_path(root)
76}
77
78fn find_workspace_root_above(start: &Path) -> Option<PathBuf> {
79 let mut current = start.parent()?;
80 loop {
81 if has_workspace_marker(current) {
82 return Some(current.to_path_buf());
83 }
84 current = current.parent()?;
85 }
86}
87
88fn has_workspace_marker(dir: &Path) -> bool {
89 const SENTINELS: &[&str] = &[
90 "pnpm-workspace.yaml",
91 "turbo.json",
92 "lerna.json",
93 "rush.json",
94 ];
95 for name in SENTINELS {
96 if dir.join(name).exists() {
97 return true;
98 }
99 }
100 if crate::workspace::load_root_deno_workspace_patterns(dir)
101 .ok()
102 .flatten()
103 .is_some_and(|(_path, patterns)| !patterns.is_empty())
104 {
105 return true;
106 }
107 let pkg_path = dir.join("package.json");
108 if !pkg_path.exists() {
109 return false;
110 }
111 let Ok(content) = std::fs::read_to_string(&pkg_path) else {
112 return false;
113 };
114 let Ok(value) = serde_json::from_str::<serde_json::Value>(&content) else {
115 return false;
116 };
117 value
118 .get("workspaces")
119 .is_some_and(|v| v.is_array() || v.is_object())
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn config_fixable_true_when_config_exists() {
128 let dir = tempfile::tempdir().unwrap();
129 std::fs::write(dir.path().join(".fallowrc.json"), "{}").unwrap();
130 assert!(is_config_fixable(dir.path(), None));
131 }
132
133 #[test]
134 fn config_fixable_true_when_can_create_at_root() {
135 let dir = tempfile::tempdir().unwrap();
136 assert!(is_config_fixable(dir.path(), None));
137 }
138
139 #[test]
140 fn config_fixable_false_when_monorepo_subpackage() {
141 let dir = tempfile::tempdir().unwrap();
142 std::fs::write(
143 dir.path().join("pnpm-workspace.yaml"),
144 "packages:\n - packages/*\n",
145 )
146 .unwrap();
147 let sub = dir.path().join("packages/app");
148 std::fs::create_dir_all(&sub).unwrap();
149 assert!(!is_config_fixable(&sub, None));
150 }
151}