1use 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 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 let enablers = self.enablers();
54 if enablers.is_empty() {
55 return false;
56 }
57 enablers.iter().any(|enabler| {
58 if enabler.ends_with('/') {
59 deps.iter().any(|d| d.starts_with(enabler))
61 } else {
62 deps.iter().any(|d| d == enabler)
63 }
64 })
65 }
66
67 fn entry_patterns(&self) -> &'static [&'static str] {
69 &[]
70 }
71
72 fn config_patterns(&self) -> &'static [&'static str] {
74 &[]
75 }
76
77 fn always_used(&self) -> &'static [&'static str] {
79 &[]
80 }
81
82 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
84 vec![]
85 }
86
87 fn tooling_dependencies(&self) -> &'static [&'static str] {
90 &[]
91 }
92
93 fn resolve_config(&self, _config_path: &Path, _source: &str, _root: &Path) -> PluginResult {
98 PluginResult::default()
99 }
100}
101
102macro_rules! define_plugin {
137 (
138 struct $name:ident => $display:expr,
139 enablers: $enablers:expr
140 $(, entry_patterns: $entry:expr)?
141 $(, config_patterns: $config:expr)?
142 $(, always_used: $always:expr)?
143 $(, tooling_dependencies: $tooling:expr)?
144 $(, used_exports: [$( ($pat:expr, $exports:expr) ),* $(,)?])?
145 $(,)?
146 ) => {
147 pub struct $name;
148
149 impl Plugin for $name {
150 fn name(&self) -> &'static str {
151 $display
152 }
153
154 fn enablers(&self) -> &'static [&'static str] {
155 $enablers
156 }
157
158 $( fn entry_patterns(&self) -> &'static [&'static str] { $entry } )?
159 $( fn config_patterns(&self) -> &'static [&'static str] { $config } )?
160 $( fn always_used(&self) -> &'static [&'static str] { $always } )?
161 $( fn tooling_dependencies(&self) -> &'static [&'static str] { $tooling } )?
162
163 $(
164 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
165 vec![$( ($pat, $exports) ),*]
166 }
167 )?
168 }
169 };
170}
171
172pub mod config_parser;
173
174mod angular;
175mod astro;
176mod ava;
177mod babel;
178mod biome;
179mod changesets;
180mod commitlint;
181mod cypress;
182mod docusaurus;
183mod drizzle;
184mod eslint;
185mod expo;
186mod graphql_codegen;
187mod jest;
188mod knex;
189mod mocha;
190mod msw;
191mod nestjs;
192mod nextjs;
193mod nuxt;
194mod nx;
195mod playwright;
196mod postcss;
197mod prisma;
198mod react_native;
199mod react_router;
200mod remix;
201mod rollup;
202mod semantic_release;
203mod sentry;
204mod storybook;
205mod stylelint;
206mod tailwind;
207mod tsup;
208mod turborepo;
209mod typescript;
210mod vite;
211mod vitest;
212mod webpack;
213mod wrangler;
214
215pub struct PluginRegistry {
217 plugins: Vec<Box<dyn Plugin>>,
218}
219
220#[derive(Debug, Default)]
222pub struct AggregatedPluginResult {
223 pub entry_patterns: Vec<String>,
225 pub config_patterns: Vec<String>,
227 pub always_used: Vec<String>,
229 pub used_exports: Vec<(String, Vec<String>)>,
231 pub referenced_dependencies: Vec<String>,
233 pub discovered_always_used: Vec<String>,
235 pub setup_files: Vec<PathBuf>,
237 pub tooling_dependencies: Vec<String>,
239 pub script_used_packages: std::collections::HashSet<String>,
241 pub active_plugins: Vec<String>,
243}
244
245impl PluginRegistry {
246 pub fn new() -> Self {
248 let plugins: Vec<Box<dyn Plugin>> = vec![
249 Box::new(nextjs::NextJsPlugin),
251 Box::new(nuxt::NuxtPlugin),
252 Box::new(remix::RemixPlugin),
253 Box::new(astro::AstroPlugin),
254 Box::new(angular::AngularPlugin),
255 Box::new(react_router::ReactRouterPlugin),
256 Box::new(react_native::ReactNativePlugin),
257 Box::new(expo::ExpoPlugin),
258 Box::new(nestjs::NestJsPlugin),
259 Box::new(docusaurus::DocusaurusPlugin),
260 Box::new(vite::VitePlugin),
262 Box::new(webpack::WebpackPlugin),
263 Box::new(rollup::RollupPlugin),
264 Box::new(tsup::TsupPlugin),
265 Box::new(vitest::VitestPlugin),
267 Box::new(jest::JestPlugin),
268 Box::new(playwright::PlaywrightPlugin),
269 Box::new(cypress::CypressPlugin),
270 Box::new(mocha::MochaPlugin),
271 Box::new(ava::AvaPlugin),
272 Box::new(storybook::StorybookPlugin),
273 Box::new(eslint::EslintPlugin),
275 Box::new(biome::BiomePlugin),
276 Box::new(stylelint::StylelintPlugin),
277 Box::new(typescript::TypeScriptPlugin),
279 Box::new(babel::BabelPlugin),
280 Box::new(tailwind::TailwindPlugin),
282 Box::new(postcss::PostCssPlugin),
283 Box::new(prisma::PrismaPlugin),
285 Box::new(drizzle::DrizzlePlugin),
286 Box::new(knex::KnexPlugin),
287 Box::new(turborepo::TurborepoPlugin),
289 Box::new(nx::NxPlugin),
290 Box::new(changesets::ChangesetsPlugin),
291 Box::new(commitlint::CommitlintPlugin),
293 Box::new(semantic_release::SemanticReleasePlugin),
294 Box::new(wrangler::WranglerPlugin),
296 Box::new(sentry::SentryPlugin),
297 Box::new(graphql_codegen::GraphqlCodegenPlugin),
299 Box::new(msw::MswPlugin),
300 ];
301 Self { plugins }
302 }
303
304 pub fn run(
309 &self,
310 pkg: &PackageJson,
311 root: &Path,
312 discovered_files: &[PathBuf],
313 ) -> AggregatedPluginResult {
314 let _span = tracing::info_span!("run_plugins").entered();
315 let mut result = AggregatedPluginResult::default();
316
317 let active: Vec<&dyn Plugin> = self
319 .plugins
320 .iter()
321 .filter(|p| p.is_enabled(pkg, root))
322 .map(|p| p.as_ref())
323 .collect();
324
325 tracing::info!(
326 plugins = active
327 .iter()
328 .map(|p| p.name())
329 .collect::<Vec<_>>()
330 .join(", "),
331 "active plugins"
332 );
333
334 for plugin in &active {
336 result.active_plugins.push(plugin.name().to_string());
337
338 for pat in plugin.entry_patterns() {
339 result.entry_patterns.push((*pat).to_string());
340 }
341 for pat in plugin.config_patterns() {
342 result.config_patterns.push((*pat).to_string());
343 }
344 for pat in plugin.always_used() {
345 result.always_used.push((*pat).to_string());
346 }
347 for (file_pat, exports) in plugin.used_exports() {
348 result.used_exports.push((
349 file_pat.to_string(),
350 exports.iter().map(|s| s.to_string()).collect(),
351 ));
352 }
353 for dep in plugin.tooling_dependencies() {
354 result.tooling_dependencies.push((*dep).to_string());
355 }
356 }
357
358 let config_matchers: Vec<(&dyn Plugin, Vec<globset::GlobMatcher>)> = active
361 .iter()
362 .filter(|p| !p.config_patterns().is_empty())
363 .map(|p| {
364 let matchers: Vec<globset::GlobMatcher> = p
365 .config_patterns()
366 .iter()
367 .filter_map(|pat| globset::Glob::new(pat).ok().map(|g| g.compile_matcher()))
368 .collect();
369 (*p, matchers)
370 })
371 .collect();
372
373 if !config_matchers.is_empty() {
374 let relative_files: Vec<(&PathBuf, String)> = discovered_files
376 .iter()
377 .map(|f| {
378 let rel = f
379 .strip_prefix(root)
380 .unwrap_or(f)
381 .to_string_lossy()
382 .into_owned();
383 (f, rel)
384 })
385 .collect();
386
387 for (plugin, matchers) in &config_matchers {
388 for (abs_path, rel_path) in &relative_files {
389 if matchers.iter().any(|m| m.is_match(rel_path.as_str())) {
390 if let Ok(source) = std::fs::read_to_string(abs_path) {
392 let plugin_result = plugin.resolve_config(abs_path, &source, root);
393 if !plugin_result.is_empty() {
394 tracing::debug!(
395 plugin = plugin.name(),
396 config = rel_path.as_str(),
397 entries = plugin_result.entry_patterns.len(),
398 deps = plugin_result.referenced_dependencies.len(),
399 "resolved config"
400 );
401 result.entry_patterns.extend(plugin_result.entry_patterns);
402 result
403 .referenced_dependencies
404 .extend(plugin_result.referenced_dependencies);
405 result
406 .discovered_always_used
407 .extend(plugin_result.always_used_files);
408 result.setup_files.extend(plugin_result.setup_files);
409 }
410 }
411 }
412 }
413 }
414 }
415
416 result
417 }
418}
419
420impl Default for PluginRegistry {
421 fn default() -> Self {
422 Self::new()
423 }
424}