Skip to main content

fallow_engine/
plugins.rs

1//! Plugin registry helpers and types exposed through the engine boundary.
2
3use std::path::{Path, PathBuf};
4
5use fallow_config::{ExternalPluginDef, PackageJson};
6
7use crate::core_backend;
8
9pub mod registry {
10    use crate::core_backend;
11
12    /// Invalid user-authored regex extracted from a plugin config file.
13    #[derive(Debug, Clone, PartialEq, Eq)]
14    pub struct PluginRegexValidationError {
15        pub(super) inner: core_backend::BackendPluginRegexValidationError,
16    }
17
18    impl From<core_backend::BackendPluginRegexValidationError> for PluginRegexValidationError {
19        fn from(inner: core_backend::BackendPluginRegexValidationError) -> Self {
20            Self { inner }
21        }
22    }
23
24    /// Names of every built-in framework plugin in registry order.
25    #[must_use]
26    pub fn builtin_plugin_names() -> Vec<&'static str> {
27        core_backend::builtin_plugin_names()
28    }
29
30    /// Format plugin regex validation errors for user-facing diagnostics.
31    #[must_use]
32    pub fn format_plugin_regex_errors(errors: &[PluginRegexValidationError]) -> String {
33        let backend_errors = errors
34            .iter()
35            .map(|error| error.inner.clone())
36            .collect::<Vec<_>>();
37        core_backend::format_plugin_regex_errors(&backend_errors)
38    }
39}
40
41/// Aggregated results from all active plugins for a project.
42#[derive(Debug, Clone, Default)]
43pub struct AggregatedPluginResult {
44    inner: core_backend::BackendAggregatedPluginResult,
45}
46
47impl AggregatedPluginResult {
48    pub(crate) const fn as_backend(&self) -> &core_backend::BackendAggregatedPluginResult {
49        &self.inner
50    }
51
52    /// Names of active plugins.
53    #[must_use]
54    pub fn active_plugins(&self) -> &[String] {
55        self.inner.active_plugins()
56    }
57
58    /// Merge active plugin names from another result, preserving insertion order.
59    pub fn merge_active_plugins_from(&mut self, other: &Self) {
60        self.inner.merge_active_plugins_from(&other.inner);
61    }
62}
63
64impl From<core_backend::BackendAggregatedPluginResult> for AggregatedPluginResult {
65    fn from(inner: core_backend::BackendAggregatedPluginResult) -> Self {
66        Self { inner }
67    }
68}
69
70/// Registry of all available plugins.
71pub struct PluginRegistry {
72    inner: core_backend::BackendPluginRegistry,
73}
74
75impl PluginRegistry {
76    /// Create a registry with all built-in plugins and optional external plugins.
77    #[must_use]
78    pub fn new(external: Vec<ExternalPluginDef>) -> Self {
79        Self {
80            inner: core_backend::BackendPluginRegistry::new(external),
81        }
82    }
83
84    /// Hidden directory names that should be traversed before full plugin execution.
85    #[must_use]
86    pub fn discovery_hidden_dirs(&self, pkg: &PackageJson, root: &Path) -> Vec<String> {
87        self.inner.discovery_hidden_dirs(pkg, root)
88    }
89
90    /// Run all plugins against a project.
91    pub fn try_run(
92        &self,
93        pkg: &PackageJson,
94        root: &Path,
95        discovered_files: &[PathBuf],
96    ) -> Result<AggregatedPluginResult, Vec<registry::PluginRegexValidationError>> {
97        self.inner
98            .try_run(pkg, root, discovered_files)
99            .map(Into::into)
100            .map_err(|errors| errors.into_iter().map(Into::into).collect())
101    }
102}
103
104impl Default for PluginRegistry {
105    fn default() -> Self {
106        Self::new(vec![])
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use std::path::PathBuf;
113
114    use super::{AggregatedPluginResult, PluginRegistry};
115
116    #[test]
117    fn plugin_registry_try_run_returns_engine_result() {
118        let registry = PluginRegistry::default();
119        let result = registry
120            .try_run(
121                &fallow_config::PackageJson::default(),
122                &PathBuf::from("/repo"),
123                &[],
124            )
125            .expect("empty package should not produce regex errors");
126
127        assert!(result.active_plugins().is_empty());
128    }
129
130    #[test]
131    fn aggregated_plugin_result_merges_active_plugins() {
132        let mut base = AggregatedPluginResult::default();
133        base.inner.push_active_plugin_for_test("nextjs");
134        let mut incoming = AggregatedPluginResult::default();
135        incoming.inner.push_active_plugin_for_test("nextjs");
136        incoming.inner.push_active_plugin_for_test("vitest");
137
138        base.merge_active_plugins_from(&incoming);
139
140        assert_eq!(base.active_plugins(), ["nextjs", "vitest"]);
141    }
142}