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        "size-limit",
126        "svgo",
127        "svgr",
128        "graphql-codegen",
129        "typedoc",
130        "openapi-ts",
131        "plop",
132        "c8",
133        "nyc",
134        "msw",
135        "napi-rs",
136        "opencode",
137        "nodemon",
138        "pm2",
139        "dependency-cruiser",
140        "wuchale",
141        "varlock",
142        "pnpm",
143        "bun",
144    ];
145
146    /// Invalid user-authored regex extracted from a plugin config file.
147    #[derive(Debug, Clone, PartialEq, Eq)]
148    pub struct PluginRegexValidationError {
149        message: String,
150    }
151
152    impl From<core_backend::BackendPluginRegexValidationError> for PluginRegexValidationError {
153        fn from(inner: core_backend::BackendPluginRegexValidationError) -> Self {
154            Self {
155                message: inner.message(),
156            }
157        }
158    }
159
160    /// Names of every built-in framework plugin in registry order.
161    #[must_use]
162    pub fn builtin_plugin_names() -> Vec<&'static str> {
163        BUILTIN_PLUGIN_NAMES.to_vec()
164    }
165
166    /// Format plugin regex validation errors for user-facing diagnostics.
167    #[must_use]
168    pub fn format_plugin_regex_errors(errors: &[PluginRegexValidationError]) -> String {
169        let joined = errors
170            .iter()
171            .map(|error| error.message.as_str())
172            .collect::<Vec<_>>();
173        format!(
174            "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.",
175            joined.join("\n  - ")
176        )
177    }
178}
179
180/// Aggregated results from all active plugins for a project.
181#[derive(Debug, Clone, Default)]
182pub struct AggregatedPluginResult {
183    inner: core_backend::BackendAggregatedPluginResult,
184}
185
186impl AggregatedPluginResult {
187    /// Names of active plugins.
188    #[must_use]
189    pub fn active_plugins(&self) -> &[String] {
190        self.inner.active_plugins()
191    }
192
193    /// Merge active plugin names from another result, preserving insertion order.
194    pub(crate) fn merge_active_plugins_from(&mut self, other: &Self) {
195        self.inner.merge_active_plugins_from(&other.inner);
196    }
197
198    pub(crate) fn backend(&self) -> &core_backend::BackendAggregatedPluginResult {
199        &self.inner
200    }
201}
202
203impl From<core_backend::BackendAggregatedPluginResult> for AggregatedPluginResult {
204    fn from(inner: core_backend::BackendAggregatedPluginResult) -> Self {
205        Self { inner }
206    }
207}
208
209/// Registry of all available plugins.
210pub struct PluginRegistry {
211    inner: core_backend::BackendPluginRegistry,
212}
213
214impl PluginRegistry {
215    /// Create a registry with all built-in plugins and optional external plugins.
216    #[must_use]
217    pub(crate) fn new(external: Vec<ExternalPluginDef>) -> Self {
218        Self {
219            inner: core_backend::BackendPluginRegistry::new(external),
220        }
221    }
222
223    /// Hidden directory names that should be traversed before full plugin execution.
224    #[must_use]
225    pub(crate) fn discovery_hidden_dirs(&self, pkg: &PackageJson, root: &Path) -> Vec<String> {
226        self.inner.discovery_hidden_dirs(pkg, root)
227    }
228
229    /// Run all plugins against a project.
230    pub(crate) fn try_run(
231        &self,
232        pkg: &PackageJson,
233        root: &Path,
234        discovered_files: &[PathBuf],
235    ) -> Result<AggregatedPluginResult, Vec<registry::PluginRegexValidationError>> {
236        self.inner
237            .try_run(pkg, root, discovered_files)
238            .map(Into::into)
239            .map_err(|errors| errors.into_iter().map(Into::into).collect())
240    }
241}
242
243impl Default for PluginRegistry {
244    fn default() -> Self {
245        Self::new(vec![])
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use std::path::PathBuf;
252
253    use super::{AggregatedPluginResult, PluginRegistry};
254
255    #[test]
256    fn plugin_registry_try_run_returns_engine_result() {
257        let registry = PluginRegistry::default();
258        let result = registry
259            .try_run(
260                &fallow_config::PackageJson::default(),
261                &PathBuf::from("/repo"),
262                &[],
263            )
264            .expect("empty package should not produce regex errors");
265
266        assert!(result.active_plugins().is_empty());
267    }
268
269    #[test]
270    fn aggregated_plugin_result_merges_active_plugins() {
271        let mut base = AggregatedPluginResult::default();
272        base.inner.push_active_plugin_for_test("nextjs");
273        let mut incoming = AggregatedPluginResult::default();
274        incoming.inner.push_active_plugin_for_test("nextjs");
275        incoming.inner.push_active_plugin_for_test("vitest");
276
277        base.merge_active_plugins_from(&incoming);
278
279        assert_eq!(base.active_plugins(), ["nextjs", "vitest"]);
280    }
281}