fallow_core/plugins/
mod.rs1use std::path::{Path, PathBuf};
13
14use fallow_config::PackageJson;
15
16#[derive(Debug, Default)]
18pub struct PluginResult {
19 pub entry_patterns: Vec<String>,
21 pub referenced_dependencies: Vec<String>,
23 pub always_used_files: Vec<String>,
25 pub setup_files: Vec<PathBuf>,
27}
28
29impl PluginResult {
30 pub const fn is_empty(&self) -> bool {
31 self.entry_patterns.is_empty()
32 && self.referenced_dependencies.is_empty()
33 && self.always_used_files.is_empty()
34 && self.setup_files.is_empty()
35 }
36}
37
38pub trait Plugin: Send + Sync {
40 fn name(&self) -> &'static str;
42
43 fn enablers(&self) -> &'static [&'static str] {
46 &[]
47 }
48
49 fn is_enabled(&self, pkg: &PackageJson, root: &Path) -> bool {
52 let deps = pkg.all_dependency_names();
53 self.is_enabled_with_deps(&deps, root)
54 }
55
56 fn is_enabled_with_deps(&self, deps: &[String], _root: &Path) -> bool {
59 let enablers = self.enablers();
60 if enablers.is_empty() {
61 return false;
62 }
63 enablers.iter().any(|enabler| {
64 if enabler.ends_with('/') {
65 deps.iter().any(|d| d.starts_with(enabler))
67 } else {
68 deps.iter().any(|d| d == enabler)
69 }
70 })
71 }
72
73 fn entry_patterns(&self) -> &'static [&'static str] {
75 &[]
76 }
77
78 fn config_patterns(&self) -> &'static [&'static str] {
80 &[]
81 }
82
83 fn always_used(&self) -> &'static [&'static str] {
85 &[]
86 }
87
88 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
90 vec![]
91 }
92
93 fn tooling_dependencies(&self) -> &'static [&'static str] {
96 &[]
97 }
98
99 fn virtual_module_prefixes(&self) -> &'static [&'static str] {
104 &[]
105 }
106
107 fn path_aliases(&self, _root: &Path) -> Vec<(&'static str, String)> {
117 vec![]
118 }
119
120 fn resolve_config(&self, _config_path: &Path, _source: &str, _root: &Path) -> PluginResult {
125 PluginResult::default()
126 }
127
128 fn package_json_config_key(&self) -> Option<&'static str> {
133 None
134 }
135}
136
137macro_rules! define_plugin {
183 (
186 struct $name:ident => $display:expr,
187 enablers: $enablers:expr
188 $(, entry_patterns: $entry:expr)?
189 $(, config_patterns: $config:expr)?
190 $(, always_used: $always:expr)?
191 $(, tooling_dependencies: $tooling:expr)?
192 $(, virtual_module_prefixes: $virtual:expr)?
193 $(, used_exports: [$( ($pat:expr, $exports:expr) ),* $(,)?])?
194 , resolve_config: imports_only
195 $(,)?
196 ) => {
197 pub struct $name;
198
199 impl Plugin for $name {
200 fn name(&self) -> &'static str {
201 $display
202 }
203
204 fn enablers(&self) -> &'static [&'static str] {
205 $enablers
206 }
207
208 $( fn entry_patterns(&self) -> &'static [&'static str] { $entry } )?
209 $( fn config_patterns(&self) -> &'static [&'static str] { $config } )?
210 $( fn always_used(&self) -> &'static [&'static str] { $always } )?
211 $( fn tooling_dependencies(&self) -> &'static [&'static str] { $tooling } )?
212 $( fn virtual_module_prefixes(&self) -> &'static [&'static str] { $virtual } )?
213
214 $(
215 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
216 vec![$( ($pat, $exports) ),*]
217 }
218 )?
219
220 fn resolve_config(
221 &self,
222 config_path: &std::path::Path,
223 source: &str,
224 _root: &std::path::Path,
225 ) -> PluginResult {
226 let mut result = PluginResult::default();
227 let imports = crate::plugins::config_parser::extract_imports(source, config_path);
228 for imp in &imports {
229 let dep = crate::resolve::extract_package_name(imp);
230 result.referenced_dependencies.push(dep);
231 }
232 result
233 }
234 }
235 };
236
237 (
239 struct $name:ident => $display:expr,
240 enablers: $enablers:expr
241 $(, entry_patterns: $entry:expr)?
242 $(, config_patterns: $config:expr)?
243 $(, always_used: $always:expr)?
244 $(, tooling_dependencies: $tooling:expr)?
245 $(, virtual_module_prefixes: $virtual:expr)?
246 $(, used_exports: [$( ($pat:expr, $exports:expr) ),* $(,)?])?
247 $(,)?
248 ) => {
249 pub struct $name;
250
251 impl Plugin for $name {
252 fn name(&self) -> &'static str {
253 $display
254 }
255
256 fn enablers(&self) -> &'static [&'static str] {
257 $enablers
258 }
259
260 $( fn entry_patterns(&self) -> &'static [&'static str] { $entry } )?
261 $( fn config_patterns(&self) -> &'static [&'static str] { $config } )?
262 $( fn always_used(&self) -> &'static [&'static str] { $always } )?
263 $( fn tooling_dependencies(&self) -> &'static [&'static str] { $tooling } )?
264 $( fn virtual_module_prefixes(&self) -> &'static [&'static str] { $virtual } )?
265
266 $(
267 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
268 vec![$( ($pat, $exports) ),*]
269 }
270 )?
271 }
272 };
273}
274
275pub mod config_parser;
276pub mod registry;
277mod tooling;
278
279pub use registry::{AggregatedPluginResult, PluginRegistry};
280pub use tooling::is_known_tooling_dependency;
281
282mod angular;
283mod astro;
284mod ava;
285mod babel;
286mod biome;
287mod bun;
288mod c8;
289mod capacitor;
290mod changesets;
291mod commitizen;
292mod commitlint;
293mod cspell;
294mod cucumber;
295mod cypress;
296mod dependency_cruiser;
297mod docusaurus;
298mod drizzle;
299mod electron;
300mod eslint;
301mod expo;
302mod gatsby;
303mod graphql_codegen;
304mod husky;
305mod i18next;
306mod jest;
307mod karma;
308mod knex;
309mod kysely;
310mod lefthook;
311mod lint_staged;
312mod markdownlint;
313mod mocha;
314mod msw;
315mod nestjs;
316mod next_intl;
317mod nextjs;
318mod nitro;
319mod nodemon;
320mod nuxt;
321mod nx;
322mod nyc;
323mod openapi_ts;
324mod oxlint;
325mod parcel;
326mod playwright;
327mod plop;
328mod pm2;
329mod postcss;
330mod prettier;
331mod prisma;
332mod react_native;
333mod react_router;
334mod relay;
335mod remark;
336mod remix;
337mod rolldown;
338mod rollup;
339mod rsbuild;
340mod rspack;
341mod sanity;
342mod semantic_release;
343mod sentry;
344mod simple_git_hooks;
345mod storybook;
346mod stylelint;
347mod sveltekit;
348mod svgo;
349mod svgr;
350mod swc;
351mod syncpack;
352mod tailwind;
353mod tanstack_router;
354mod tsdown;
355mod tsup;
356mod turborepo;
357mod typedoc;
358mod typeorm;
359mod typescript;
360mod vite;
361mod vitepress;
362mod vitest;
363mod webdriverio;
364mod webpack;
365mod wrangler;
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370 use std::path::Path;
371
372 #[test]
375 fn is_enabled_with_deps_exact_match() {
376 let plugin = nextjs::NextJsPlugin;
377 let deps = vec!["next".to_string()];
378 assert!(plugin.is_enabled_with_deps(&deps, Path::new("/project")));
379 }
380
381 #[test]
382 fn is_enabled_with_deps_no_match() {
383 let plugin = nextjs::NextJsPlugin;
384 let deps = vec!["react".to_string()];
385 assert!(!plugin.is_enabled_with_deps(&deps, Path::new("/project")));
386 }
387
388 #[test]
389 fn is_enabled_with_deps_empty_deps() {
390 let plugin = nextjs::NextJsPlugin;
391 let deps: Vec<String> = vec![];
392 assert!(!plugin.is_enabled_with_deps(&deps, Path::new("/project")));
393 }
394
395 #[test]
398 fn plugin_result_is_empty_when_default() {
399 let r = PluginResult::default();
400 assert!(r.is_empty());
401 }
402
403 #[test]
404 fn plugin_result_not_empty_with_entry_patterns() {
405 let r = PluginResult {
406 entry_patterns: vec!["*.ts".to_string()],
407 ..Default::default()
408 };
409 assert!(!r.is_empty());
410 }
411
412 #[test]
413 fn plugin_result_not_empty_with_referenced_deps() {
414 let r = PluginResult {
415 referenced_dependencies: vec!["lodash".to_string()],
416 ..Default::default()
417 };
418 assert!(!r.is_empty());
419 }
420
421 #[test]
422 fn plugin_result_not_empty_with_setup_files() {
423 let r = PluginResult {
424 setup_files: vec![PathBuf::from("/setup.ts")],
425 ..Default::default()
426 };
427 assert!(!r.is_empty());
428 }
429}