Skip to main content

fallow_config/
framework.rs

1use serde::{Deserialize, Serialize};
2
3/// Declarative framework detection and entry point configuration.
4///
5/// Users can define custom framework presets via `fallow.toml` to add
6/// project-specific entry points, always-used files, and used export rules.
7/// Built-in framework support is provided by the plugin system in fallow-core.
8#[derive(Debug, Clone, Deserialize, Serialize)]
9pub struct FrameworkPreset {
10    /// Unique name for this framework.
11    pub name: String,
12
13    /// How to detect if this framework is in use.
14    #[serde(default)]
15    pub detection: Option<FrameworkDetection>,
16
17    /// Glob patterns for files that are entry points.
18    #[serde(default)]
19    pub entry_points: Vec<FrameworkEntryPattern>,
20
21    /// Files that are always considered "used".
22    #[serde(default)]
23    pub always_used: Vec<String>,
24
25    /// Exports that are always considered used in matching files.
26    #[serde(default)]
27    pub used_exports: Vec<FrameworkUsedExport>,
28}
29
30/// How to detect if a framework is in use.
31#[derive(Debug, Clone, Deserialize, Serialize)]
32#[serde(tag = "type", rename_all = "snake_case")]
33pub enum FrameworkDetection {
34    /// Framework detected if this package is in dependencies.
35    Dependency { package: String },
36    /// Framework detected if this file pattern matches.
37    FileExists { pattern: String },
38    /// All conditions must be true.
39    All { conditions: Vec<FrameworkDetection> },
40    /// Any condition must be true.
41    Any { conditions: Vec<FrameworkDetection> },
42}
43
44/// Entry point pattern from a framework.
45#[derive(Debug, Clone, Deserialize, Serialize)]
46pub struct FrameworkEntryPattern {
47    /// Glob pattern for entry point files.
48    pub pattern: String,
49}
50
51/// Exports considered used for files matching a pattern.
52#[derive(Debug, Clone, Deserialize, Serialize)]
53pub struct FrameworkUsedExport {
54    /// Files matching this glob pattern.
55    pub file_pattern: String,
56    /// These exports are always considered used.
57    pub exports: Vec<String>,
58}
59
60/// Resolved framework rule (after loading custom presets).
61#[derive(Debug, Clone)]
62pub struct FrameworkRule {
63    pub name: String,
64    pub detection: Option<FrameworkDetection>,
65    pub entry_points: Vec<FrameworkEntryPattern>,
66    pub always_used: Vec<String>,
67    pub used_exports: Vec<FrameworkUsedExport>,
68}
69
70impl From<FrameworkPreset> for FrameworkRule {
71    fn from(preset: FrameworkPreset) -> Self {
72        Self {
73            name: preset.name,
74            detection: preset.detection,
75            entry_points: preset.entry_points,
76            always_used: preset.always_used,
77            used_exports: preset.used_exports,
78        }
79    }
80}
81
82/// Load user-defined framework rules from fallow.toml.
83///
84/// Built-in framework support is handled by the plugin system in fallow-core.
85/// This function only processes custom user-defined presets.
86pub fn resolve_framework_rules(custom: &[FrameworkPreset]) -> Vec<FrameworkRule> {
87    custom
88        .iter()
89        .map(|p| FrameworkRule::from(p.clone()))
90        .collect()
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn resolve_framework_rules_empty() {
99        let rules = resolve_framework_rules(&[]);
100        assert!(rules.is_empty());
101    }
102
103    #[test]
104    fn resolve_framework_rules_with_custom() {
105        let custom = vec![FrameworkPreset {
106            name: "custom".to_string(),
107            detection: None,
108            entry_points: vec![FrameworkEntryPattern {
109                pattern: "src/custom/**/*.ts".to_string(),
110            }],
111            always_used: vec![],
112            used_exports: vec![],
113        }];
114        let rules = resolve_framework_rules(&custom);
115        assert_eq!(rules.len(), 1);
116        assert_eq!(rules[0].name, "custom");
117    }
118
119    #[test]
120    fn framework_preset_to_rule() {
121        let preset = FrameworkPreset {
122            name: "test".to_string(),
123            detection: Some(FrameworkDetection::Dependency {
124                package: "test-pkg".to_string(),
125            }),
126            entry_points: vec![FrameworkEntryPattern {
127                pattern: "src/**/*.test.ts".to_string(),
128            }],
129            always_used: vec!["setup.ts".to_string()],
130            used_exports: vec![FrameworkUsedExport {
131                file_pattern: "src/**/*.test.ts".to_string(),
132                exports: vec!["default".to_string()],
133            }],
134        };
135        let rule: FrameworkRule = preset.into();
136        assert_eq!(rule.name, "test");
137        assert!(rule.detection.is_some());
138        assert_eq!(rule.entry_points.len(), 1);
139        assert_eq!(rule.always_used, vec!["setup.ts"]);
140        assert_eq!(rule.used_exports.len(), 1);
141    }
142
143    #[test]
144    fn framework_detection_deserialize_dependency() {
145        let json = r#"{"type": "dependency", "package": "next"}"#;
146        let detection: FrameworkDetection = serde_json::from_str(json).unwrap();
147        assert!(
148            matches!(detection, FrameworkDetection::Dependency { package } if package == "next")
149        );
150    }
151
152    #[test]
153    fn framework_detection_deserialize_file_exists() {
154        let json = r#"{"type": "file_exists", "pattern": "tsconfig.json"}"#;
155        let detection: FrameworkDetection = serde_json::from_str(json).unwrap();
156        assert!(
157            matches!(detection, FrameworkDetection::FileExists { pattern } if pattern == "tsconfig.json")
158        );
159    }
160
161    #[test]
162    fn framework_detection_deserialize_all() {
163        let json = r#"{"type": "all", "conditions": [{"type": "dependency", "package": "a"}, {"type": "dependency", "package": "b"}]}"#;
164        let detection: FrameworkDetection = serde_json::from_str(json).unwrap();
165        assert!(
166            matches!(detection, FrameworkDetection::All { conditions } if conditions.len() == 2)
167        );
168    }
169
170    #[test]
171    fn framework_detection_deserialize_any() {
172        let json = r#"{"type": "any", "conditions": [{"type": "dependency", "package": "a"}]}"#;
173        let detection: FrameworkDetection = serde_json::from_str(json).unwrap();
174        assert!(
175            matches!(detection, FrameworkDetection::Any { conditions } if conditions.len() == 1)
176        );
177    }
178}