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