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