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    "@babel/",
28    "babel-",
29    "@react-native-community/cli",
30    "@react-native/",
31    "secretlint",
32    "@secretlint/",
33    "oxlint",
34    "@semantic-release/",
35    "semantic-release",
36    "@release-it/",
37    "@lerna-lite/",
38    "@changesets/",
39    "@graphql-codegen/",
40    "@rollup/",
41    "@biomejs/",
42    "@electron-forge/",
43    "@electron/",
44    "@formatjs/",
45];
46
47/// Exact package names that are always dev tooling.
48const GENERAL_TOOLING_EXACT: &[&str] = &[
49    "typescript",
50    "prettier",
51    "turbo",
52    "concurrently",
53    "cross-env",
54    "rimraf",
55    "npm-run-all",
56    "npm-run-all2",
57    "nodemon",
58    "ts-node",
59    "tsx",
60    "knip",
61    "fallow",
62    "jest",
63    "vitest",
64    "happy-dom",
65    "jsdom",
66    "vite",
67    "sass",
68    "sass-embedded",
69    "webpack",
70    "webpack-cli",
71    "webpack-dev-server",
72    "esbuild",
73    "rollup",
74    "swc",
75    "@swc/core",
76    "@swc/jest",
77    "terser",
78    "cssnano",
79    "sharp",
80    "release-it",
81    "lerna",
82    "dotenv-cli",
83    "dotenv-flow",
84    "oxfmt",
85    "jscpd",
86    "npm-check-updates",
87    "markdownlint-cli",
88    "npm-package-json-lint",
89    "synp",
90    "flow-bin",
91    "i18next-parser",
92    "i18next-conv",
93    "webpack-bundle-analyzer",
94    "vite-plugin-svgr",
95    "vite-plugin-eslint",
96    "@vitejs/plugin-vue",
97    "@vitejs/plugin-react",
98    "next-sitemap",
99    "tsup",
100    "unbuild",
101    "typedoc",
102    "nx",
103    "@manypkg/cli",
104    "vue-tsc",
105    "@vue/tsconfig",
106    "@tsconfig/node20",
107    "@tsconfig/react-native",
108    "@typescript/native-preview",
109    "tw-animate-css",
110    "@ianvs/prettier-plugin-sort-imports",
111    "prettier-plugin-tailwindcss",
112    "prettier-plugin-organize-imports",
113    "@vitejs/plugin-react-swc",
114    "@vitejs/plugin-legacy",
115    "rolldown",
116    "rolldown-vite",
117    "oxc-transform",
118    "puppeteer",
119    "madge",
120    "patch-package",
121    "electron",
122    "electron-builder",
123    "electron-vite",
124];
125
126/// Lazily-built set for O(1) exact-match lookups.
127fn tooling_exact_set() -> &'static rustc_hash::FxHashSet<&'static str> {
128    static SET: std::sync::OnceLock<rustc_hash::FxHashSet<&'static str>> =
129        std::sync::OnceLock::new();
130    SET.get_or_init(|| GENERAL_TOOLING_EXACT.iter().copied().collect())
131}
132
133/// Check whether a package is a known tooling/dev dependency by name.
134///
135/// This is the single source of truth for general tooling detection.
136/// Per-plugin tooling dependencies are declared via `Plugin::tooling_dependencies()`
137/// and aggregated separately in `AggregatedPluginResult`.
138pub fn is_known_tooling_dependency(name: &str) -> bool {
139    GENERAL_TOOLING_PREFIXES.iter().any(|p| name.starts_with(p))
140        || tooling_exact_set().contains(name)
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    // ── Prefix matching ──────────────────────────────────────────
148
149    #[test]
150    fn types_prefix_matches_scoped() {
151        assert!(is_known_tooling_dependency("@types/node"));
152        assert!(is_known_tooling_dependency("@types/react"));
153        assert!(is_known_tooling_dependency("@types/express"));
154    }
155
156    #[test]
157    fn types_prefix_does_not_match_similar_names() {
158        // "type-fest" should NOT match "@types/" prefix
159        assert!(!is_known_tooling_dependency("type-fest"));
160        assert!(!is_known_tooling_dependency("typesafe-actions"));
161    }
162
163    #[test]
164    fn storybook_prefix_matches() {
165        assert!(is_known_tooling_dependency("@storybook/react"));
166        assert!(is_known_tooling_dependency("@storybook/addon-essentials"));
167        assert!(is_known_tooling_dependency("storybook"));
168    }
169
170    #[test]
171    fn testing_library_prefix_matches() {
172        assert!(is_known_tooling_dependency("@testing-library/react"));
173        assert!(is_known_tooling_dependency("@testing-library/jest-dom"));
174    }
175
176    #[test]
177    fn babel_prefix_matches() {
178        assert!(is_known_tooling_dependency("@babel/core"));
179        assert!(is_known_tooling_dependency("babel-loader"));
180        assert!(is_known_tooling_dependency("babel-jest"));
181    }
182
183    #[test]
184    fn vitest_prefix_matches() {
185        assert!(is_known_tooling_dependency("@vitest/coverage-v8"));
186        assert!(is_known_tooling_dependency("@vitest/ui"));
187    }
188
189    #[test]
190    fn eslint_prefix_matches() {
191        assert!(is_known_tooling_dependency("eslint"));
192        assert!(is_known_tooling_dependency("eslint-plugin-react"));
193        assert!(is_known_tooling_dependency("eslint-config-next"));
194    }
195
196    #[test]
197    fn biomejs_prefix_matches() {
198        assert!(is_known_tooling_dependency("@biomejs/biome"));
199    }
200
201    // ── Exact matching ───────────────────────────────────────────
202
203    #[test]
204    fn exact_typescript_matches() {
205        assert!(is_known_tooling_dependency("typescript"));
206    }
207
208    #[test]
209    fn exact_prettier_matches() {
210        assert!(is_known_tooling_dependency("prettier"));
211    }
212
213    #[test]
214    fn exact_vitest_matches() {
215        assert!(is_known_tooling_dependency("vitest"));
216    }
217
218    #[test]
219    fn exact_jest_matches() {
220        assert!(is_known_tooling_dependency("jest"));
221    }
222
223    #[test]
224    fn exact_vite_matches() {
225        assert!(is_known_tooling_dependency("vite"));
226    }
227
228    #[test]
229    fn exact_esbuild_matches() {
230        assert!(is_known_tooling_dependency("esbuild"));
231    }
232
233    #[test]
234    fn exact_tsup_matches() {
235        assert!(is_known_tooling_dependency("tsup"));
236    }
237
238    #[test]
239    fn exact_turbo_matches() {
240        assert!(is_known_tooling_dependency("turbo"));
241    }
242
243    // ── Non-tooling dependencies ─────────────────────────────────
244
245    #[test]
246    fn common_runtime_deps_not_tooling() {
247        assert!(!is_known_tooling_dependency("react"));
248        assert!(!is_known_tooling_dependency("react-dom"));
249        assert!(!is_known_tooling_dependency("express"));
250        assert!(!is_known_tooling_dependency("lodash"));
251        assert!(!is_known_tooling_dependency("next"));
252        assert!(!is_known_tooling_dependency("vue"));
253        assert!(!is_known_tooling_dependency("axios"));
254    }
255
256    #[test]
257    fn empty_string_not_tooling() {
258        assert!(!is_known_tooling_dependency(""));
259    }
260
261    #[test]
262    fn near_miss_not_tooling() {
263        // These look similar to tooling but should NOT match
264        assert!(!is_known_tooling_dependency("type-fest"));
265        assert!(!is_known_tooling_dependency("typestyle"));
266        assert!(!is_known_tooling_dependency("prettier-bytes")); // not the exact "prettier"
267        // Note: "prettier-bytes" starts with "prettier" but only prefix matches
268        // check the prefixes list — "prettier" is NOT in GENERAL_TOOLING_PREFIXES,
269        // it's in GENERAL_TOOLING_EXACT. So "prettier-bytes" should not match.
270    }
271
272    #[test]
273    fn sass_variants_are_tooling() {
274        assert!(is_known_tooling_dependency("sass"));
275        assert!(is_known_tooling_dependency("sass-embedded"));
276    }
277
278    #[test]
279    fn prettier_plugins_are_tooling() {
280        assert!(is_known_tooling_dependency(
281            "@ianvs/prettier-plugin-sort-imports"
282        ));
283        assert!(is_known_tooling_dependency("prettier-plugin-tailwindcss"));
284    }
285}