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
9/// External-plugin dry-run primitives for the CLI's `plugin-check` command.
10pub use crate::core_backend::{
11    CheckWarning, ManifestResult, RuleReport, WarningKind, check_manifest_entries,
12    is_external_plugin_active,
13};
14
15/// Built-in plugin name roster and plugin-regex validation diagnostics.
16pub mod registry {
17    use crate::core_backend;
18
19    const BUILTIN_PLUGIN_NAMES: &[&str] = &[
20        "nextjs",
21        "nuxt",
22        "pinia",
23        "remix",
24        "astro",
25        "browser-extension",
26        "wxt",
27        "angular",
28        "react-router",
29        "redwoodsdk",
30        "tanstack-router",
31        "react-native",
32        "expo",
33        "expo-router",
34        "firebase",
35        "nestjs",
36        "adonis",
37        "docusaurus",
38        "gatsby",
39        "sveltekit",
40        "nitro",
41        "capacitor",
42        "ionic",
43        "sanity",
44        "supabase",
45        "vitepress",
46        "rspress",
47        "next-intl",
48        "relay",
49        "electron",
50        "i18next",
51        "qwik",
52        "convex",
53        "lit",
54        "lexical",
55        "obsidian",
56        "content-collections",
57        "contentlayer",
58        "fumadocs",
59        "mintlify",
60        "velite",
61        "ember",
62        "vite",
63        "vscode",
64        "webpack",
65        "rollup",
66        "rolldown",
67        "rspack",
68        "rsbuild",
69        "tsup",
70        "tsdown",
71        "pkg-utils",
72        "parcel",
73        "vitest",
74        "jest",
75        "playwright",
76        "cypress",
77        "mocha",
78        "ava",
79        "tap",
80        "tsd",
81        "k6",
82        "storybook",
83        "stryker",
84        "karma",
85        "cucumber",
86        "webdriverio",
87        "eslint",
88        "biome",
89        "stylelint",
90        "prettier",
91        "oxlint",
92        "markdownlint",
93        "cspell",
94        "remark",
95        "typescript",
96        "babel",
97        "swc",
98        "tailwind",
99        "postcss",
100        "unocss",
101        "pandacss",
102        "prisma",
103        "drizzle",
104        "knex",
105        "typeorm",
106        "kysely",
107        "turborepo",
108        "nx",
109        "changesets",
110        "syncpack",
111        "commitlint",
112        "commitizen",
113        "commit-and-tag-version",
114        "semantic-release",
115        "danger",
116        "hardhat",
117        "vercel",
118        "wrangler",
119        "opennext-cloudflare",
120        "sentry",
121        "husky",
122        "lint-staged",
123        "lefthook",
124        "simple-git-hooks",
125        "svgo",
126        "svgr",
127        "graphql-codegen",
128        "typedoc",
129        "openapi-ts",
130        "plop",
131        "c8",
132        "nyc",
133        "msw",
134        "napi-rs",
135        "opencode",
136        "nodemon",
137        "pm2",
138        "dependency-cruiser",
139        "wuchale",
140        "varlock",
141        "pnpm",
142        "bun",
143    ];
144
145    /// Invalid user-authored regex extracted from a plugin config file.
146    #[derive(Debug, Clone, PartialEq, Eq)]
147    pub struct PluginRegexValidationError {
148        message: String,
149    }
150
151    impl From<core_backend::BackendPluginRegexValidationError> for PluginRegexValidationError {
152        fn from(inner: core_backend::BackendPluginRegexValidationError) -> Self {
153            Self {
154                message: inner.message(),
155            }
156        }
157    }
158
159    /// Names of every built-in framework plugin in registry order.
160    #[must_use]
161    pub fn builtin_plugin_names() -> Vec<&'static str> {
162        BUILTIN_PLUGIN_NAMES.to_vec()
163    }
164
165    /// Format plugin regex validation errors for user-facing diagnostics.
166    #[must_use]
167    pub fn format_plugin_regex_errors(errors: &[PluginRegexValidationError]) -> String {
168        let joined = errors
169            .iter()
170            .map(|error| error.message.as_str())
171            .collect::<Vec<_>>();
172        format!(
173            "invalid plugin regex configuration:\n  - {}\n\nRewrite the plugin config with Rust-compatible regex syntax, or remove unsupported constructs such as JavaScript lookahead and lookbehind.",
174            joined.join("\n  - ")
175        )
176    }
177}
178
179/// Aggregated results from all active plugins for a project.
180#[derive(Debug, Clone, Default)]
181pub struct AggregatedPluginResult {
182    inner: core_backend::BackendAggregatedPluginResult,
183}
184
185impl AggregatedPluginResult {
186    /// Names of active plugins.
187    #[must_use]
188    pub fn active_plugins(&self) -> &[String] {
189        self.inner.active_plugins()
190    }
191
192    /// Merge active plugin names from another result, preserving insertion order.
193    pub(crate) fn merge_active_plugins_from(&mut self, other: &Self) {
194        self.inner.merge_active_plugins_from(&other.inner);
195    }
196
197    pub(crate) fn backend(&self) -> &core_backend::BackendAggregatedPluginResult {
198        &self.inner
199    }
200}
201
202impl From<core_backend::BackendAggregatedPluginResult> for AggregatedPluginResult {
203    fn from(inner: core_backend::BackendAggregatedPluginResult) -> Self {
204        Self { inner }
205    }
206}
207
208/// Registry of all available plugins.
209pub struct PluginRegistry {
210    inner: core_backend::BackendPluginRegistry,
211}
212
213impl PluginRegistry {
214    /// Create a registry with all built-in plugins and optional external plugins.
215    #[must_use]
216    pub(crate) fn new(external: Vec<ExternalPluginDef>) -> Self {
217        Self {
218            inner: core_backend::BackendPluginRegistry::new(external),
219        }
220    }
221
222    /// Hidden directory names that should be traversed before full plugin execution.
223    #[must_use]
224    pub(crate) fn discovery_hidden_dirs(&self, pkg: &PackageJson, root: &Path) -> Vec<String> {
225        self.inner.discovery_hidden_dirs(pkg, root)
226    }
227
228    /// Run all plugins against a project.
229    pub(crate) fn try_run(
230        &self,
231        pkg: &PackageJson,
232        root: &Path,
233        discovered_files: &[PathBuf],
234    ) -> Result<AggregatedPluginResult, Vec<registry::PluginRegexValidationError>> {
235        self.inner
236            .try_run(pkg, root, discovered_files)
237            .map(Into::into)
238            .map_err(|errors| errors.into_iter().map(Into::into).collect())
239    }
240}
241
242impl Default for PluginRegistry {
243    fn default() -> Self {
244        Self::new(vec![])
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use std::path::PathBuf;
251
252    use super::{AggregatedPluginResult, PluginRegistry};
253
254    #[test]
255    fn plugin_registry_try_run_returns_engine_result() {
256        let registry = PluginRegistry::default();
257        let result = registry
258            .try_run(
259                &fallow_config::PackageJson::default(),
260                &PathBuf::from("/repo"),
261                &[],
262            )
263            .expect("empty package should not produce regex errors");
264
265        assert!(result.active_plugins().is_empty());
266    }
267
268    #[test]
269    fn aggregated_plugin_result_merges_active_plugins() {
270        let mut base = AggregatedPluginResult::default();
271        base.inner.push_active_plugin_for_test("nextjs");
272        let mut incoming = AggregatedPluginResult::default();
273        incoming.inner.push_active_plugin_for_test("nextjs");
274        incoming.inner.push_active_plugin_for_test("vitest");
275
276        base.merge_active_plugins_from(&incoming);
277
278        assert_eq!(base.active_plugins(), ["nextjs", "vitest"]);
279    }
280}