Skip to main content

fallow_core/plugins/
tooling.rs

1//! General tooling dependency detection.
2//!
3//! Known dev dependencies that are tooling (used by CLI/config, not imported in
4//! application code). These complement the per-plugin `tooling_dependencies()`
5//! lists with dependencies that aren't tied to any single plugin.
6
7/// Prefixes of package names that are always dev tooling.
8const GENERAL_TOOLING_PREFIXES: &[&str] = &[
9    "@types/",
10    "husky",
11    "lint-staged",
12    "commitlint",
13    "@commitlint",
14    "stylelint",
15    "@vitest/",
16    "@jest/",
17    "@tapjs/",
18    "@testing-library/",
19    "@playwright/",
20    "@react-native-community/cli",
21    "@react-native/",
22    "secretlint",
23    "@secretlint/",
24    "oxlint",
25    "@semantic-release/",
26    "semantic-release",
27    "@release-it/",
28    "@lerna-lite/",
29    "@changesets/",
30    "@graphql-codegen/",
31    "@biomejs/",
32    "@electron-forge/",
33    "@electron/",
34    "@formatjs/",
35];
36
37/// Exact package names that are always dev tooling.
38const GENERAL_TOOLING_EXACT: &[&str] = &[
39    "typescript",
40    "prettier",
41    "turbo",
42    "concurrently",
43    "cross-env",
44    "rimraf",
45    "npm-run-all",
46    "npm-run-all2",
47    "nodemon",
48    "ts-node",
49    "tsx",
50    "knip",
51    "fallow",
52    "jest",
53    "vitest",
54    "tap",
55    "happy-dom",
56    "jsdom",
57    "vite",
58    "sass",
59    "sass-embedded",
60    "webpack",
61    "webpack-cli",
62    "webpack-dev-server",
63    "esbuild",
64    "rollup",
65    "swc",
66    "@swc/core",
67    "@swc/jest",
68    "terser",
69    "cssnano",
70    "sharp",
71    "release-it",
72    "lerna",
73    "dotenv-cli",
74    "dotenv-flow",
75    "oxfmt",
76    "jscpd",
77    "npm-check-updates",
78    "markdownlint-cli",
79    "npm-package-json-lint",
80    "synp",
81    "flow-bin",
82    "i18next-parser",
83    "i18next-conv",
84    "webpack-bundle-analyzer",
85    "vite-plugin-svgr",
86    "vite-plugin-eslint",
87    "@vitejs/plugin-vue",
88    "@vitejs/plugin-react",
89    "next-sitemap",
90    "tsup",
91    "unbuild",
92    "typedoc",
93    "nx",
94    "@manypkg/cli",
95    "vue-tsc",
96    "@vue/tsconfig",
97    "@tsconfig/node20",
98    "@tsconfig/react-native",
99    "@typescript/native-preview",
100    "tw-animate-css",
101    "@ianvs/prettier-plugin-sort-imports",
102    "prettier-plugin-tailwindcss",
103    "prettier-plugin-organize-imports",
104    "@vitejs/plugin-react-swc",
105    "@vitejs/plugin-legacy",
106    "rolldown",
107    "rolldown-vite",
108    "oxc-transform",
109    "puppeteer",
110    "madge",
111    "patch-package",
112    "electron",
113    "electron-builder",
114    "electron-vite",
115];
116
117/// Lazily-built set for O(1) exact-match lookups.
118fn tooling_exact_set() -> &'static rustc_hash::FxHashSet<&'static str> {
119    static SET: std::sync::OnceLock<rustc_hash::FxHashSet<&'static str>> =
120        std::sync::OnceLock::new();
121    SET.get_or_init(|| GENERAL_TOOLING_EXACT.iter().copied().collect())
122}
123
124/// Check whether a package is a known tooling/dev dependency by name.
125///
126/// This is the single source of truth for general tooling detection.
127/// Per-plugin tooling dependencies are declared via `Plugin::tooling_dependencies()`
128/// and aggregated separately in `AggregatedPluginResult`.
129#[must_use]
130pub fn is_known_tooling_dependency(name: &str) -> bool {
131    GENERAL_TOOLING_PREFIXES.iter().any(|p| name.starts_with(p))
132        || tooling_exact_set().contains(name)
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    // ── Prefix matching ──────────────────────────────────────────
140
141    #[test]
142    fn types_prefix_matches_scoped() {
143        assert!(is_known_tooling_dependency("@types/node"));
144        assert!(is_known_tooling_dependency("@types/react"));
145        assert!(is_known_tooling_dependency("@types/express"));
146    }
147
148    #[test]
149    fn types_prefix_does_not_match_similar_names() {
150        // "type-fest" should NOT match "@types/" prefix
151        assert!(!is_known_tooling_dependency("type-fest"));
152        assert!(!is_known_tooling_dependency("typesafe-actions"));
153    }
154
155    #[test]
156    fn storybook_not_blanket_matched() {
157        // @storybook/ and storybook prefixes removed — handled by StorybookPlugin config parsing
158        assert!(!is_known_tooling_dependency("@storybook/react"));
159        assert!(!is_known_tooling_dependency("@storybook/addon-essentials"));
160        assert!(!is_known_tooling_dependency("storybook"));
161    }
162
163    #[test]
164    fn testing_library_prefix_matches() {
165        assert!(is_known_tooling_dependency("@testing-library/react"));
166        assert!(is_known_tooling_dependency("@testing-library/jest-dom"));
167    }
168
169    #[test]
170    fn babel_not_blanket_matched() {
171        // @babel/ and babel- prefixes removed — handled by BabelPlugin config parsing
172        assert!(!is_known_tooling_dependency("@babel/core"));
173        assert!(!is_known_tooling_dependency("@babel/preset-env"));
174        assert!(!is_known_tooling_dependency("babel-loader"));
175        assert!(!is_known_tooling_dependency("babel-jest"));
176    }
177
178    #[test]
179    fn vitest_prefix_matches() {
180        assert!(is_known_tooling_dependency("@vitest/coverage-v8"));
181        assert!(is_known_tooling_dependency("@vitest/ui"));
182    }
183
184    #[test]
185    fn eslint_not_blanket_matched() {
186        // eslint and @typescript-eslint prefixes removed — handled by EslintPlugin config parsing
187        assert!(!is_known_tooling_dependency("eslint"));
188        assert!(!is_known_tooling_dependency("eslint-plugin-react"));
189        assert!(!is_known_tooling_dependency("eslint-config-next"));
190        assert!(!is_known_tooling_dependency("@typescript-eslint/parser"));
191    }
192
193    #[test]
194    fn biomejs_prefix_matches() {
195        assert!(is_known_tooling_dependency("@biomejs/biome"));
196    }
197
198    // ── Exact matching ───────────────────────────────────────────
199
200    #[test]
201    fn exact_typescript_matches() {
202        assert!(is_known_tooling_dependency("typescript"));
203    }
204
205    #[test]
206    fn exact_prettier_matches() {
207        assert!(is_known_tooling_dependency("prettier"));
208    }
209
210    #[test]
211    fn exact_vitest_matches() {
212        assert!(is_known_tooling_dependency("vitest"));
213    }
214
215    #[test]
216    fn exact_jest_matches() {
217        assert!(is_known_tooling_dependency("jest"));
218    }
219
220    #[test]
221    fn exact_vite_matches() {
222        assert!(is_known_tooling_dependency("vite"));
223    }
224
225    #[test]
226    fn exact_esbuild_matches() {
227        assert!(is_known_tooling_dependency("esbuild"));
228    }
229
230    #[test]
231    fn exact_tsup_matches() {
232        assert!(is_known_tooling_dependency("tsup"));
233    }
234
235    #[test]
236    fn exact_turbo_matches() {
237        assert!(is_known_tooling_dependency("turbo"));
238    }
239
240    // ── Non-tooling dependencies ─────────────────────────────────
241
242    #[test]
243    fn common_runtime_deps_not_tooling() {
244        assert!(!is_known_tooling_dependency("react"));
245        assert!(!is_known_tooling_dependency("react-dom"));
246        assert!(!is_known_tooling_dependency("express"));
247        assert!(!is_known_tooling_dependency("lodash"));
248        assert!(!is_known_tooling_dependency("next"));
249        assert!(!is_known_tooling_dependency("vue"));
250        assert!(!is_known_tooling_dependency("axios"));
251    }
252
253    #[test]
254    fn empty_string_not_tooling() {
255        assert!(!is_known_tooling_dependency(""));
256    }
257
258    #[test]
259    fn near_miss_not_tooling() {
260        // These look similar to tooling but should NOT match
261        assert!(!is_known_tooling_dependency("type-fest"));
262        assert!(!is_known_tooling_dependency("typestyle"));
263        assert!(!is_known_tooling_dependency("prettier-bytes")); // not the exact "prettier"
264        // Note: "prettier-bytes" starts with "prettier" but only prefix matches
265        // check the prefixes list — "prettier" is NOT in GENERAL_TOOLING_PREFIXES,
266        // it's in GENERAL_TOOLING_EXACT. So "prettier-bytes" should not match.
267    }
268
269    #[test]
270    fn sass_variants_are_tooling() {
271        assert!(is_known_tooling_dependency("sass"));
272        assert!(is_known_tooling_dependency("sass-embedded"));
273    }
274
275    #[test]
276    fn prettier_plugins_are_tooling() {
277        assert!(is_known_tooling_dependency(
278            "@ianvs/prettier-plugin-sort-imports"
279        ));
280        assert!(is_known_tooling_dependency("prettier-plugin-tailwindcss"));
281    }
282
283    // ── Additional prefix matching ────────────────────────────────
284
285    #[test]
286    fn electron_forge_prefix_matches() {
287        assert!(is_known_tooling_dependency("@electron-forge/cli"));
288        assert!(is_known_tooling_dependency(
289            "@electron-forge/maker-squirrel"
290        ));
291    }
292
293    #[test]
294    fn electron_prefix_matches() {
295        assert!(is_known_tooling_dependency("@electron/rebuild"));
296        assert!(is_known_tooling_dependency("@electron/notarize"));
297    }
298
299    #[test]
300    fn formatjs_prefix_matches() {
301        assert!(is_known_tooling_dependency("@formatjs/cli"));
302        assert!(is_known_tooling_dependency("@formatjs/intl"));
303    }
304
305    #[test]
306    fn rollup_not_blanket_matched() {
307        // @rollup/ prefix removed — handled by RollupPlugin config parsing
308        assert!(!is_known_tooling_dependency("@rollup/plugin-commonjs"));
309        assert!(!is_known_tooling_dependency("@rollup/plugin-node-resolve"));
310        assert!(!is_known_tooling_dependency("@rollup/plugin-typescript"));
311    }
312
313    #[test]
314    fn semantic_release_prefix_matches() {
315        assert!(is_known_tooling_dependency("@semantic-release/github"));
316        assert!(is_known_tooling_dependency("@semantic-release/npm"));
317        assert!(is_known_tooling_dependency("semantic-release"));
318    }
319
320    #[test]
321    fn release_it_prefix_matches() {
322        assert!(is_known_tooling_dependency(
323            "@release-it/conventional-changelog"
324        ));
325    }
326
327    #[test]
328    fn lerna_lite_prefix_matches() {
329        assert!(is_known_tooling_dependency("@lerna-lite/cli"));
330        assert!(is_known_tooling_dependency("@lerna-lite/publish"));
331    }
332
333    #[test]
334    fn changesets_prefix_matches() {
335        assert!(is_known_tooling_dependency("@changesets/cli"));
336        assert!(is_known_tooling_dependency("@changesets/changelog-github"));
337    }
338
339    #[test]
340    fn graphql_codegen_prefix_matches() {
341        assert!(is_known_tooling_dependency("@graphql-codegen/cli"));
342        assert!(is_known_tooling_dependency(
343            "@graphql-codegen/typescript-operations"
344        ));
345    }
346
347    #[test]
348    fn secretlint_prefix_matches() {
349        assert!(is_known_tooling_dependency("secretlint"));
350        assert!(is_known_tooling_dependency(
351            "@secretlint/secretlint-rule-preset-recommend"
352        ));
353    }
354
355    #[test]
356    fn oxlint_prefix_matches() {
357        assert!(is_known_tooling_dependency("oxlint"));
358    }
359
360    #[test]
361    fn react_native_community_prefix_matches() {
362        assert!(is_known_tooling_dependency("@react-native-community/cli"));
363        assert!(is_known_tooling_dependency(
364            "@react-native-community/cli-platform-android"
365        ));
366    }
367
368    #[test]
369    fn react_native_prefix_matches() {
370        assert!(is_known_tooling_dependency("@react-native/metro-config"));
371        assert!(is_known_tooling_dependency(
372            "@react-native/typescript-config"
373        ));
374    }
375
376    #[test]
377    fn jest_prefix_matches() {
378        assert!(is_known_tooling_dependency("@jest/globals"));
379        assert!(is_known_tooling_dependency("@jest/types"));
380    }
381
382    #[test]
383    fn playwright_prefix_matches() {
384        assert!(is_known_tooling_dependency("@playwright/test"));
385    }
386
387    #[test]
388    fn tapjs_prefix_matches() {
389        assert!(is_known_tooling_dependency("@tapjs/test"));
390        assert!(is_known_tooling_dependency("@tapjs/snapshot"));
391    }
392
393    // ── Additional exact matching ─────────────────────────────────
394
395    #[test]
396    fn exact_tap_matches() {
397        assert!(is_known_tooling_dependency("tap"));
398    }
399
400    #[test]
401    fn exact_rolldown_matches() {
402        assert!(is_known_tooling_dependency("rolldown"));
403        assert!(is_known_tooling_dependency("rolldown-vite"));
404    }
405
406    #[test]
407    fn exact_electron_matches() {
408        assert!(is_known_tooling_dependency("electron"));
409        assert!(is_known_tooling_dependency("electron-builder"));
410        assert!(is_known_tooling_dependency("electron-vite"));
411    }
412
413    #[test]
414    fn exact_sharp_matches() {
415        assert!(is_known_tooling_dependency("sharp"));
416    }
417
418    #[test]
419    fn exact_puppeteer_matches() {
420        assert!(is_known_tooling_dependency("puppeteer"));
421    }
422
423    #[test]
424    fn exact_madge_matches() {
425        assert!(is_known_tooling_dependency("madge"));
426    }
427
428    #[test]
429    fn exact_patch_package_matches() {
430        assert!(is_known_tooling_dependency("patch-package"));
431    }
432
433    #[test]
434    fn exact_nx_matches() {
435        assert!(is_known_tooling_dependency("nx"));
436    }
437
438    #[test]
439    fn exact_vue_tsc_matches() {
440        assert!(is_known_tooling_dependency("vue-tsc"));
441    }
442
443    #[test]
444    fn exact_tsconfig_packages_match() {
445        assert!(is_known_tooling_dependency("@tsconfig/node20"));
446        assert!(is_known_tooling_dependency("@tsconfig/react-native"));
447        assert!(is_known_tooling_dependency("@vue/tsconfig"));
448    }
449
450    #[test]
451    fn exact_vitejs_plugins_match() {
452        assert!(is_known_tooling_dependency("@vitejs/plugin-vue"));
453        assert!(is_known_tooling_dependency("@vitejs/plugin-react"));
454        assert!(is_known_tooling_dependency("@vitejs/plugin-react-swc"));
455        assert!(is_known_tooling_dependency("@vitejs/plugin-legacy"));
456    }
457
458    #[test]
459    fn exact_oxc_transform_matches() {
460        assert!(is_known_tooling_dependency("oxc-transform"));
461    }
462
463    #[test]
464    fn exact_typescript_native_preview_matches() {
465        assert!(is_known_tooling_dependency("@typescript/native-preview"));
466    }
467
468    #[test]
469    fn exact_tw_animate_css_matches() {
470        assert!(is_known_tooling_dependency("tw-animate-css"));
471    }
472
473    #[test]
474    fn exact_manypkg_cli_matches() {
475        assert!(is_known_tooling_dependency("@manypkg/cli"));
476    }
477
478    #[test]
479    fn exact_swc_variants_match() {
480        assert!(is_known_tooling_dependency("@swc/core"));
481        assert!(is_known_tooling_dependency("@swc/jest"));
482    }
483
484    // ── Negative tests for near-misses ────────────────────────────
485
486    #[test]
487    fn runtime_deps_with_similar_names_not_tooling() {
488        // These are NOT tooling — they don't match any prefix or exact entry
489        assert!(!is_known_tooling_dependency("react-scripts"));
490        assert!(!is_known_tooling_dependency("express-validator"));
491        assert!(!is_known_tooling_dependency("sass-loader")); // "sass" is exact, not prefix
492    }
493
494    #[test]
495    fn postcss_not_blanket_matched() {
496        // postcss, autoprefixer, tailwindcss, @tailwindcss prefixes removed —
497        // handled by PostCssPlugin and TailwindPlugin config parsing
498        assert!(!is_known_tooling_dependency("postcss-modules"));
499        assert!(!is_known_tooling_dependency("postcss-import"));
500        assert!(!is_known_tooling_dependency("autoprefixer"));
501        assert!(!is_known_tooling_dependency("tailwindcss"));
502        assert!(!is_known_tooling_dependency("@tailwindcss/typography"));
503    }
504
505    #[test]
506    fn tooling_exact_set_is_deterministic() {
507        // Calling the lazy set multiple times returns the same result
508        let set1 = tooling_exact_set();
509        let set2 = tooling_exact_set();
510        assert_eq!(set1.len(), set2.len());
511        assert!(set1.contains("typescript"));
512    }
513}