Skip to main content

fallow_config/
framework.rs

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