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