1pub mod config_parser;
13
14mod angular;
15mod astro;
16mod ava;
17mod babel;
18mod biome;
19mod changesets;
20mod commitlint;
21mod cypress;
22mod docusaurus;
23mod drizzle;
24mod eslint;
25mod expo;
26mod graphql_codegen;
27mod jest;
28mod knex;
29mod mocha;
30mod msw;
31mod nestjs;
32mod nextjs;
33mod nuxt;
34mod nx;
35mod playwright;
36mod postcss;
37mod prisma;
38mod react_native;
39mod react_router;
40mod remix;
41mod rollup;
42mod semantic_release;
43mod sentry;
44mod storybook;
45mod stylelint;
46mod tailwind;
47mod tsup;
48mod turborepo;
49mod typescript;
50mod vite;
51mod vitest;
52mod webpack;
53mod wrangler;
54
55use std::path::{Path, PathBuf};
56
57use fallow_config::PackageJson;
58
59#[derive(Debug, Default)]
61pub struct PluginResult {
62 pub entry_patterns: Vec<String>,
64 pub referenced_dependencies: Vec<String>,
66 pub always_used_files: Vec<String>,
68 pub setup_files: Vec<PathBuf>,
70}
71
72impl PluginResult {
73 pub fn is_empty(&self) -> bool {
74 self.entry_patterns.is_empty()
75 && self.referenced_dependencies.is_empty()
76 && self.always_used_files.is_empty()
77 && self.setup_files.is_empty()
78 }
79}
80
81pub trait Plugin: Send + Sync {
83 fn name(&self) -> &'static str;
85
86 fn enablers(&self) -> &'static [&'static str] {
89 &[]
90 }
91
92 fn is_enabled(&self, pkg: &PackageJson, _root: &Path) -> bool {
95 let deps = pkg.all_dependency_names();
96 let enablers = self.enablers();
97 if enablers.is_empty() {
98 return false;
99 }
100 enablers.iter().any(|enabler| {
101 if enabler.ends_with('/') {
102 deps.iter().any(|d| d.starts_with(enabler))
104 } else {
105 deps.iter().any(|d| d == enabler)
106 }
107 })
108 }
109
110 fn entry_patterns(&self) -> &'static [&'static str] {
112 &[]
113 }
114
115 fn config_patterns(&self) -> &'static [&'static str] {
117 &[]
118 }
119
120 fn always_used(&self) -> &'static [&'static str] {
122 &[]
123 }
124
125 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
127 vec![]
128 }
129
130 fn tooling_dependencies(&self) -> &'static [&'static str] {
133 &[]
134 }
135
136 fn resolve_config(&self, _config_path: &Path, _source: &str, _root: &Path) -> PluginResult {
141 PluginResult::default()
142 }
143}
144
145pub struct PluginRegistry {
147 plugins: Vec<Box<dyn Plugin>>,
148}
149
150#[derive(Debug, Default)]
152pub struct AggregatedPluginResult {
153 pub entry_patterns: Vec<String>,
155 pub config_patterns: Vec<String>,
157 pub always_used: Vec<String>,
159 pub used_exports: Vec<(String, Vec<String>)>,
161 pub referenced_dependencies: Vec<String>,
163 pub discovered_always_used: Vec<String>,
165 pub setup_files: Vec<PathBuf>,
167 pub tooling_dependencies: Vec<String>,
169 pub active_plugins: Vec<String>,
171}
172
173impl PluginRegistry {
174 pub fn new() -> Self {
176 let plugins: Vec<Box<dyn Plugin>> = vec![
177 Box::new(nextjs::NextJsPlugin),
179 Box::new(nuxt::NuxtPlugin),
180 Box::new(remix::RemixPlugin),
181 Box::new(astro::AstroPlugin),
182 Box::new(angular::AngularPlugin),
183 Box::new(react_router::ReactRouterPlugin),
184 Box::new(react_native::ReactNativePlugin),
185 Box::new(expo::ExpoPlugin),
186 Box::new(nestjs::NestJsPlugin),
187 Box::new(docusaurus::DocusaurusPlugin),
188 Box::new(vite::VitePlugin),
190 Box::new(webpack::WebpackPlugin),
191 Box::new(rollup::RollupPlugin),
192 Box::new(tsup::TsupPlugin),
193 Box::new(vitest::VitestPlugin),
195 Box::new(jest::JestPlugin),
196 Box::new(playwright::PlaywrightPlugin),
197 Box::new(cypress::CypressPlugin),
198 Box::new(mocha::MochaPlugin),
199 Box::new(ava::AvaPlugin),
200 Box::new(storybook::StorybookPlugin),
201 Box::new(eslint::EslintPlugin),
203 Box::new(biome::BiomePlugin),
204 Box::new(stylelint::StylelintPlugin),
205 Box::new(typescript::TypeScriptPlugin),
207 Box::new(babel::BabelPlugin),
208 Box::new(tailwind::TailwindPlugin),
210 Box::new(postcss::PostCssPlugin),
211 Box::new(prisma::PrismaPlugin),
213 Box::new(drizzle::DrizzlePlugin),
214 Box::new(knex::KnexPlugin),
215 Box::new(turborepo::TurborepoPlugin),
217 Box::new(nx::NxPlugin),
218 Box::new(changesets::ChangesetsPlugin),
219 Box::new(commitlint::CommitlintPlugin),
221 Box::new(semantic_release::SemanticReleasePlugin),
222 Box::new(wrangler::WranglerPlugin),
224 Box::new(sentry::SentryPlugin),
225 Box::new(graphql_codegen::GraphqlCodegenPlugin),
227 Box::new(msw::MswPlugin),
228 ];
229 Self { plugins }
230 }
231
232 pub fn run(
237 &self,
238 pkg: &PackageJson,
239 root: &Path,
240 discovered_files: &[PathBuf],
241 ) -> AggregatedPluginResult {
242 let _span = tracing::info_span!("run_plugins").entered();
243 let mut result = AggregatedPluginResult::default();
244
245 let active: Vec<&dyn Plugin> = self
247 .plugins
248 .iter()
249 .filter(|p| p.is_enabled(pkg, root))
250 .map(|p| p.as_ref())
251 .collect();
252
253 tracing::info!(
254 plugins = active
255 .iter()
256 .map(|p| p.name())
257 .collect::<Vec<_>>()
258 .join(", "),
259 "active plugins"
260 );
261
262 for plugin in &active {
264 result.active_plugins.push(plugin.name().to_string());
265
266 for pat in plugin.entry_patterns() {
267 result.entry_patterns.push((*pat).to_string());
268 }
269 for pat in plugin.config_patterns() {
270 result.config_patterns.push((*pat).to_string());
271 }
272 for pat in plugin.always_used() {
273 result.always_used.push((*pat).to_string());
274 }
275 for (file_pat, exports) in plugin.used_exports() {
276 result.used_exports.push((
277 file_pat.to_string(),
278 exports.iter().map(|s| s.to_string()).collect(),
279 ));
280 }
281 for dep in plugin.tooling_dependencies() {
282 result.tooling_dependencies.push((*dep).to_string());
283 }
284 }
285
286 let config_matchers: Vec<(&dyn Plugin, Vec<globset::GlobMatcher>)> = active
289 .iter()
290 .filter(|p| !p.config_patterns().is_empty())
291 .map(|p| {
292 let matchers: Vec<globset::GlobMatcher> = p
293 .config_patterns()
294 .iter()
295 .filter_map(|pat| globset::Glob::new(pat).ok().map(|g| g.compile_matcher()))
296 .collect();
297 (*p, matchers)
298 })
299 .collect();
300
301 if !config_matchers.is_empty() {
302 let relative_files: Vec<(&PathBuf, String)> = discovered_files
304 .iter()
305 .map(|f| {
306 let rel = f
307 .strip_prefix(root)
308 .unwrap_or(f)
309 .to_string_lossy()
310 .into_owned();
311 (f, rel)
312 })
313 .collect();
314
315 for (plugin, matchers) in &config_matchers {
316 for (abs_path, rel_path) in &relative_files {
317 if matchers.iter().any(|m| m.is_match(rel_path.as_str())) {
318 if let Ok(source) = std::fs::read_to_string(abs_path) {
320 let plugin_result = plugin.resolve_config(abs_path, &source, root);
321 if !plugin_result.is_empty() {
322 tracing::debug!(
323 plugin = plugin.name(),
324 config = rel_path.as_str(),
325 entries = plugin_result.entry_patterns.len(),
326 deps = plugin_result.referenced_dependencies.len(),
327 "resolved config"
328 );
329 result.entry_patterns.extend(plugin_result.entry_patterns);
330 result
331 .referenced_dependencies
332 .extend(plugin_result.referenced_dependencies);
333 result
334 .discovered_always_used
335 .extend(plugin_result.always_used_files);
336 result.setup_files.extend(plugin_result.setup_files);
337 }
338 }
339 }
340 }
341 }
342 }
343
344 result
345 }
346}
347
348impl Default for PluginRegistry {
349 fn default() -> Self {
350 Self::new()
351 }
352}