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