1use std::path::{Path, PathBuf};
4
5use fallow_config::{EntryPointRole, 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 {
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 #[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 #[must_use]
160 pub fn builtin_plugin_names() -> Vec<&'static str> {
161 BUILTIN_PLUGIN_NAMES.to_vec()
162 }
163
164 #[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#[derive(Debug, Clone, Default)]
180pub struct AggregatedPluginResult {
181 inner: core_backend::BackendAggregatedPluginResult,
182}
183
184impl AggregatedPluginResult {
185 #[must_use]
187 pub fn active_plugins(&self) -> &[String] {
188 self.inner.active_plugins()
189 }
190
191 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
245pub struct PluginRegistry {
247 inner: core_backend::BackendPluginRegistry,
248}
249
250impl PluginRegistry {
251 #[must_use]
253 pub fn new(external: Vec<ExternalPluginDef>) -> Self {
254 Self {
255 inner: core_backend::BackendPluginRegistry::new(external),
256 }
257 }
258
259 #[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 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}