1use std::path::{Path, PathBuf};
4
5use fallow_config::{ExternalPluginDef, PackageJson};
6
7use crate::core_backend;
8
9pub use crate::core_backend::{
11 CheckWarning, ManifestResult, RuleReport, WarningKind, check_manifest_entries,
12 is_external_plugin_active,
13};
14
15pub 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 #[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 #[must_use]
161 pub fn builtin_plugin_names() -> Vec<&'static str> {
162 BUILTIN_PLUGIN_NAMES.to_vec()
163 }
164
165 #[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#[derive(Debug, Clone, Default)]
181pub struct AggregatedPluginResult {
182 inner: core_backend::BackendAggregatedPluginResult,
183}
184
185impl AggregatedPluginResult {
186 #[must_use]
188 pub fn active_plugins(&self) -> &[String] {
189 self.inner.active_plugins()
190 }
191
192 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
208pub struct PluginRegistry {
210 inner: core_backend::BackendPluginRegistry,
211}
212
213impl PluginRegistry {
214 #[must_use]
216 pub(crate) fn new(external: Vec<ExternalPluginDef>) -> Self {
217 Self {
218 inner: core_backend::BackendPluginRegistry::new(external),
219 }
220 }
221
222 #[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 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}