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 "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 #[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 #[must_use]
162 pub fn builtin_plugin_names() -> Vec<&'static str> {
163 BUILTIN_PLUGIN_NAMES.to_vec()
164 }
165
166 #[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#[derive(Debug, Clone, Default)]
182pub struct AggregatedPluginResult {
183 inner: core_backend::BackendAggregatedPluginResult,
184}
185
186impl AggregatedPluginResult {
187 #[must_use]
189 pub fn active_plugins(&self) -> &[String] {
190 self.inner.active_plugins()
191 }
192
193 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
209pub struct PluginRegistry {
211 inner: core_backend::BackendPluginRegistry,
212}
213
214impl PluginRegistry {
215 #[must_use]
217 pub(crate) fn new(external: Vec<ExternalPluginDef>) -> Self {
218 Self {
219 inner: core_backend::BackendPluginRegistry::new(external),
220 }
221 }
222
223 #[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 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}