fallow_core/plugins/
tooling.rs1use rustc_hash::FxHashSet;
13
14const CATALOGUE_TOML: &str = include_str!("../../data/tooling.toml");
17
18#[cfg(test)]
26const FRAMEWORK_PLUGIN_FAMILY_PREFIXES: &[&str] = &[
27 "vite-plugin-",
28 "prettier-plugin-",
29 "eslint-plugin-",
30 "rollup-plugin-",
31];
32
33#[cfg(test)]
38const FRAMEWORK_PLUGIN_SCOPED_PREFIXES: &[&str] = &["@rollup/plugin-"];
39
40#[derive(serde::Deserialize)]
41struct ToolingCatalogue {
42 #[serde(default)]
43 prefix: Vec<PrefixEntry>,
44 #[serde(default)]
45 exact: Vec<ExactEntry>,
46}
47
48#[derive(serde::Deserialize)]
49struct PrefixEntry {
50 pattern: String,
54 #[expect(
56 dead_code,
57 reason = "documentation field, surfaced via the catalogue source"
58 )]
59 #[serde(default)]
60 notes: Option<String>,
61}
62
63#[derive(serde::Deserialize)]
64struct ExactEntry {
65 name: String,
67 #[expect(
69 dead_code,
70 reason = "documentation field, surfaced via the catalogue source"
71 )]
72 #[serde(default)]
73 ecosystem: Option<String>,
74}
75
76struct Catalogue {
78 prefixes: Vec<String>,
79 exact: FxHashSet<String>,
80}
81
82#[expect(
86 clippy::expect_used,
87 reason = "embedded tooling catalogue is compile-time data pinned by catalogue_parses"
88)]
89fn catalogue() -> &'static Catalogue {
90 static CATALOGUE: std::sync::OnceLock<Catalogue> = std::sync::OnceLock::new();
91 CATALOGUE.get_or_init(|| {
92 let parsed: ToolingCatalogue = toml::from_str(CATALOGUE_TOML).expect(
93 "embedded crates/core/data/tooling.toml must parse; run \
94 `cargo test -p fallow-core catalogue_parses` to see the error",
95 );
96 Catalogue {
97 prefixes: parsed.prefix.into_iter().map(|p| p.pattern).collect(),
98 exact: parsed.exact.into_iter().map(|e| e.name).collect(),
99 }
100 })
101}
102
103#[must_use]
109pub fn is_known_tooling_dependency(name: &str) -> bool {
110 let catalogue = catalogue();
111 catalogue
112 .prefixes
113 .iter()
114 .any(|p| name.starts_with(p.as_str()))
115 || catalogue.exact.contains(name)
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn types_prefix_matches_scoped() {
124 assert!(is_known_tooling_dependency("@types/node"));
125 assert!(is_known_tooling_dependency("@types/react"));
126 assert!(is_known_tooling_dependency("@types/express"));
127 }
128
129 #[test]
130 fn types_prefix_does_not_match_similar_names() {
131 assert!(!is_known_tooling_dependency("type-fest"));
132 assert!(!is_known_tooling_dependency("typesafe-actions"));
133 }
134
135 #[test]
136 fn storybook_not_blanket_matched() {
137 assert!(!is_known_tooling_dependency("@storybook/react"));
138 assert!(!is_known_tooling_dependency("@storybook/addon-essentials"));
139 assert!(!is_known_tooling_dependency("storybook"));
140 }
141
142 #[test]
143 fn testing_library_prefix_matches() {
144 assert!(is_known_tooling_dependency("@testing-library/react"));
145 assert!(is_known_tooling_dependency("@testing-library/jest-dom"));
146 }
147
148 #[test]
149 fn babel_not_blanket_matched() {
150 assert!(!is_known_tooling_dependency("@babel/core"));
151 assert!(!is_known_tooling_dependency("@babel/preset-env"));
152 assert!(!is_known_tooling_dependency("babel-loader"));
153 assert!(!is_known_tooling_dependency("babel-jest"));
154 }
155
156 #[test]
157 fn vitest_prefix_matches() {
158 assert!(is_known_tooling_dependency("@vitest/coverage-v8"));
159 assert!(is_known_tooling_dependency("@vitest/ui"));
160 }
161
162 #[test]
163 fn eslint_not_blanket_matched() {
164 assert!(!is_known_tooling_dependency("eslint"));
165 assert!(!is_known_tooling_dependency("eslint-plugin-react"));
166 assert!(!is_known_tooling_dependency("eslint-config-next"));
167 assert!(!is_known_tooling_dependency("@typescript-eslint/parser"));
168 }
169
170 #[test]
171 fn biomejs_prefix_matches() {
172 assert!(is_known_tooling_dependency("@biomejs/biome"));
173 }
174
175 #[test]
176 fn exact_typescript_matches() {
177 assert!(is_known_tooling_dependency("typescript"));
178 }
179
180 #[test]
181 fn exact_prettier_matches() {
182 assert!(is_known_tooling_dependency("prettier"));
183 }
184
185 #[test]
186 fn exact_vitest_matches() {
187 assert!(is_known_tooling_dependency("vitest"));
188 }
189
190 #[test]
191 fn exact_jest_matches() {
192 assert!(is_known_tooling_dependency("jest"));
193 }
194
195 #[test]
196 fn exact_vite_matches() {
197 assert!(is_known_tooling_dependency("vite"));
198 }
199
200 #[test]
201 fn exact_esbuild_matches() {
202 assert!(is_known_tooling_dependency("esbuild"));
203 }
204
205 #[test]
206 fn exact_tsup_matches() {
207 assert!(is_known_tooling_dependency("tsup"));
208 }
209
210 #[test]
211 fn exact_turbo_matches() {
212 assert!(is_known_tooling_dependency("turbo"));
213 }
214
215 #[test]
216 fn common_runtime_deps_not_tooling() {
217 assert!(!is_known_tooling_dependency("react"));
218 assert!(!is_known_tooling_dependency("react-dom"));
219 assert!(!is_known_tooling_dependency("express"));
220 assert!(!is_known_tooling_dependency("lodash"));
221 assert!(!is_known_tooling_dependency("next"));
222 assert!(!is_known_tooling_dependency("vue"));
223 assert!(!is_known_tooling_dependency("axios"));
224 }
225
226 #[test]
227 fn empty_string_not_tooling() {
228 assert!(!is_known_tooling_dependency(""));
229 }
230
231 #[test]
232 fn near_miss_not_tooling() {
233 assert!(!is_known_tooling_dependency("type-fest"));
234 assert!(!is_known_tooling_dependency("typestyle"));
235 assert!(!is_known_tooling_dependency("prettier-bytes")); }
237
238 #[test]
239 fn sass_variants_are_tooling() {
240 assert!(is_known_tooling_dependency("sass"));
241 assert!(is_known_tooling_dependency("sass-embedded"));
242 }
243
244 #[test]
245 fn framework_plugin_packages_no_longer_exact_matched() {
246 assert!(!is_known_tooling_dependency("vite-plugin-svgr"));
247 assert!(!is_known_tooling_dependency("vite-plugin-eslint"));
248 assert!(!is_known_tooling_dependency("prettier-plugin-tailwindcss"));
249 assert!(!is_known_tooling_dependency(
250 "prettier-plugin-organize-imports"
251 ));
252 assert!(!is_known_tooling_dependency(
253 "@ianvs/prettier-plugin-sort-imports"
254 ));
255 }
256
257 #[test]
258 fn electron_forge_prefix_matches() {
259 assert!(is_known_tooling_dependency("@electron-forge/cli"));
260 assert!(is_known_tooling_dependency(
261 "@electron-forge/maker-squirrel"
262 ));
263 }
264
265 #[test]
266 fn electron_prefix_matches() {
267 assert!(is_known_tooling_dependency("@electron/rebuild"));
268 assert!(is_known_tooling_dependency("@electron/notarize"));
269 }
270
271 #[test]
272 fn formatjs_prefix_matches() {
273 assert!(is_known_tooling_dependency("@formatjs/cli"));
274 assert!(is_known_tooling_dependency("@formatjs/intl"));
275 }
276
277 #[test]
278 fn rollup_not_blanket_matched() {
279 assert!(!is_known_tooling_dependency("@rollup/plugin-commonjs"));
280 assert!(!is_known_tooling_dependency("@rollup/plugin-node-resolve"));
281 assert!(!is_known_tooling_dependency("@rollup/plugin-typescript"));
282 }
283
284 #[test]
285 fn semantic_release_prefix_matches() {
286 assert!(is_known_tooling_dependency("@semantic-release/github"));
287 assert!(is_known_tooling_dependency("@semantic-release/npm"));
288 assert!(is_known_tooling_dependency("semantic-release"));
289 }
290
291 #[test]
292 fn release_it_prefix_matches() {
293 assert!(is_known_tooling_dependency(
294 "@release-it/conventional-changelog"
295 ));
296 }
297
298 #[test]
299 fn lerna_lite_prefix_matches() {
300 assert!(is_known_tooling_dependency("@lerna-lite/cli"));
301 assert!(is_known_tooling_dependency("@lerna-lite/publish"));
302 }
303
304 #[test]
305 fn changesets_prefix_matches() {
306 assert!(is_known_tooling_dependency("@changesets/cli"));
307 assert!(is_known_tooling_dependency("@changesets/changelog-github"));
308 }
309
310 #[test]
311 fn graphql_codegen_prefix_matches() {
312 assert!(is_known_tooling_dependency("@graphql-codegen/cli"));
313 assert!(is_known_tooling_dependency(
314 "@graphql-codegen/typescript-operations"
315 ));
316 }
317
318 #[test]
319 fn secretlint_prefix_matches() {
320 assert!(is_known_tooling_dependency("secretlint"));
321 assert!(is_known_tooling_dependency(
322 "@secretlint/secretlint-rule-preset-recommend"
323 ));
324 }
325
326 #[test]
327 fn oxlint_prefix_matches() {
328 assert!(is_known_tooling_dependency("oxlint"));
329 }
330
331 #[test]
332 fn react_native_community_prefix_matches() {
333 assert!(is_known_tooling_dependency("@react-native-community/cli"));
334 assert!(is_known_tooling_dependency(
335 "@react-native-community/cli-platform-android"
336 ));
337 }
338
339 #[test]
340 fn react_native_prefix_matches() {
341 assert!(is_known_tooling_dependency("@react-native/metro-config"));
342 assert!(is_known_tooling_dependency(
343 "@react-native/typescript-config"
344 ));
345 }
346
347 #[test]
348 fn jest_prefix_matches() {
349 assert!(is_known_tooling_dependency("@jest/globals"));
350 assert!(is_known_tooling_dependency("@jest/types"));
351 }
352
353 #[test]
354 fn playwright_prefix_matches() {
355 assert!(is_known_tooling_dependency("@playwright/test"));
356 assert!(is_known_tooling_dependency("playwright"));
357 }
358
359 #[test]
360 fn tapjs_prefix_matches() {
361 assert!(is_known_tooling_dependency("@tapjs/test"));
362 assert!(is_known_tooling_dependency("@tapjs/snapshot"));
363 }
364
365 #[test]
366 fn exact_tap_matches() {
367 assert!(is_known_tooling_dependency("tap"));
368 }
369
370 #[test]
371 fn exact_rolldown_matches() {
372 assert!(is_known_tooling_dependency("rolldown"));
373 assert!(is_known_tooling_dependency("rolldown-vite"));
374 }
375
376 #[test]
377 fn exact_electron_matches() {
378 assert!(is_known_tooling_dependency("electron"));
379 assert!(is_known_tooling_dependency("electron-builder"));
380 assert!(is_known_tooling_dependency("electron-vite"));
381 }
382
383 #[test]
384 fn exact_sharp_matches() {
385 assert!(is_known_tooling_dependency("sharp"));
386 }
387
388 #[test]
389 fn exact_puppeteer_matches() {
390 assert!(is_known_tooling_dependency("puppeteer"));
391 }
392
393 #[test]
394 fn exact_madge_matches() {
395 assert!(is_known_tooling_dependency("madge"));
396 }
397
398 #[test]
399 fn exact_patch_package_matches() {
400 assert!(is_known_tooling_dependency("patch-package"));
401 }
402
403 #[test]
404 fn exact_nx_matches() {
405 assert!(is_known_tooling_dependency("nx"));
406 }
407
408 #[test]
409 fn exact_vue_tsc_matches() {
410 assert!(is_known_tooling_dependency("vue-tsc"));
411 }
412
413 #[test]
414 fn exact_tsconfig_packages_match() {
415 assert!(is_known_tooling_dependency("@tsconfig/node20"));
416 assert!(is_known_tooling_dependency("@tsconfig/react-native"));
417 assert!(is_known_tooling_dependency("@vue/tsconfig"));
418 }
419
420 #[test]
421 fn exact_vitejs_plugins_match() {
422 assert!(is_known_tooling_dependency("@vitejs/plugin-vue"));
423 assert!(is_known_tooling_dependency("@vitejs/plugin-react"));
424 assert!(is_known_tooling_dependency("@vitejs/plugin-react-swc"));
425 assert!(is_known_tooling_dependency("@vitejs/plugin-legacy"));
426 }
427
428 #[test]
429 fn exact_oxc_transform_matches() {
430 assert!(is_known_tooling_dependency("oxc-transform"));
431 }
432
433 #[test]
434 fn exact_typescript_native_preview_matches() {
435 assert!(is_known_tooling_dependency("@typescript/native-preview"));
436 }
437
438 #[test]
439 fn exact_tw_animate_css_matches() {
440 assert!(is_known_tooling_dependency("tw-animate-css"));
441 }
442
443 #[test]
444 fn exact_manypkg_cli_matches() {
445 assert!(is_known_tooling_dependency("@manypkg/cli"));
446 }
447
448 #[test]
449 fn exact_swc_variants_match() {
450 assert!(is_known_tooling_dependency("@swc/core"));
451 assert!(is_known_tooling_dependency("@swc/jest"));
452 }
453
454 #[test]
455 fn runtime_deps_with_similar_names_not_tooling() {
456 assert!(!is_known_tooling_dependency("react-scripts"));
457 assert!(!is_known_tooling_dependency("express-validator"));
458 assert!(!is_known_tooling_dependency("sass-loader")); }
460
461 #[test]
462 fn postcss_not_blanket_matched() {
463 assert!(!is_known_tooling_dependency("postcss-modules"));
464 assert!(!is_known_tooling_dependency("postcss-import"));
465 assert!(!is_known_tooling_dependency("autoprefixer"));
466 assert!(!is_known_tooling_dependency("tailwindcss"));
467 assert!(!is_known_tooling_dependency("@tailwindcss/typography"));
468 }
469
470 #[test]
471 fn catalogue_is_deterministic() {
472 assert_eq!(
473 is_known_tooling_dependency("typescript"),
474 is_known_tooling_dependency("typescript")
475 );
476 assert!(is_known_tooling_dependency("typescript"));
477 }
478
479 #[test]
480 fn catalogue_parses() {
481 let cat = catalogue();
482 assert!(!cat.prefixes.is_empty(), "catalogue must have prefixes");
483 assert!(!cat.exact.is_empty(), "catalogue must have exact entries");
484 assert!(cat.exact.contains("typescript"));
485 assert!(cat.prefixes.iter().any(|p| p == "@types/"));
486 }
487
488 #[test]
489 fn catalogue_has_no_empty_or_whitespace_prefixes() {
490 for prefix in &catalogue().prefixes {
491 assert!(
492 !prefix.trim().is_empty(),
493 "catalogue prefix must be non-empty / non-whitespace; got {prefix:?}"
494 );
495 }
496 }
497
498 #[test]
499 fn catalogue_has_no_duplicate_entries() {
500 let parsed: ToolingCatalogue = toml::from_str(CATALOGUE_TOML).unwrap();
501
502 let mut seen_exact = FxHashSet::default();
503 for entry in &parsed.exact {
504 assert!(
505 seen_exact.insert(entry.name.as_str()),
506 "duplicate exact catalogue entry: {:?}",
507 entry.name
508 );
509 }
510
511 let mut seen_prefix = FxHashSet::default();
512 for entry in &parsed.prefix {
513 assert!(
514 seen_prefix.insert(entry.pattern.as_str()),
515 "duplicate prefix catalogue entry: {:?}",
516 entry.pattern
517 );
518 }
519 }
520
521 #[test]
522 fn catalogue_rejects_framework_plugin_exact_entries() {
523 let parsed: ToolingCatalogue = toml::from_str(CATALOGUE_TOML).unwrap();
524 for entry in &parsed.exact {
525 let tail = entry
526 .name
527 .strip_prefix('@')
528 .and_then(|rest| rest.split_once('/'))
529 .map(|(_scope, tail)| tail);
530 for bad in FRAMEWORK_PLUGIN_FAMILY_PREFIXES {
531 assert!(
532 !entry.name.starts_with(bad) && !tail.is_some_and(|t| t.starts_with(bad)),
533 "exact catalogue entry {:?} is a framework plugin ({bad}); \
534 credit it in the relevant plugin's config parser instead of the catalogue",
535 entry.name,
536 );
537 }
538 for bad in FRAMEWORK_PLUGIN_SCOPED_PREFIXES {
539 assert!(
540 !entry.name.starts_with(bad),
541 "exact catalogue entry {:?} is a framework plugin ({bad}); \
542 credit it in the relevant plugin's config parser instead of the catalogue",
543 entry.name,
544 );
545 }
546 }
547 }
548}