use rustc_hash::FxHashSet;
const CATALOGUE_TOML: &str = include_str!("../../data/tooling.toml");
#[cfg(test)]
const FRAMEWORK_PLUGIN_FAMILY_PREFIXES: &[&str] = &[
"vite-plugin-",
"prettier-plugin-",
"eslint-plugin-",
"rollup-plugin-",
];
#[cfg(test)]
const FRAMEWORK_PLUGIN_SCOPED_PREFIXES: &[&str] = &["@rollup/plugin-"];
#[derive(serde::Deserialize)]
struct ToolingCatalogue {
#[serde(default)]
prefix: Vec<PrefixEntry>,
#[serde(default)]
exact: Vec<ExactEntry>,
}
#[derive(serde::Deserialize)]
struct PrefixEntry {
pattern: String,
#[expect(
dead_code,
reason = "documentation field, surfaced via the catalogue source"
)]
#[serde(default)]
notes: Option<String>,
}
#[derive(serde::Deserialize)]
struct ExactEntry {
name: String,
#[expect(
dead_code,
reason = "documentation field, surfaced via the catalogue source"
)]
#[serde(default)]
ecosystem: Option<String>,
}
struct Catalogue {
prefixes: Vec<String>,
exact: FxHashSet<String>,
}
fn catalogue() -> &'static Catalogue {
static CATALOGUE: std::sync::OnceLock<Catalogue> = std::sync::OnceLock::new();
CATALOGUE.get_or_init(|| {
let parsed: ToolingCatalogue = toml::from_str(CATALOGUE_TOML).expect(
"embedded crates/core/data/tooling.toml must parse; run \
`cargo test -p fallow-core catalogue_parses` to see the error",
);
Catalogue {
prefixes: parsed.prefix.into_iter().map(|p| p.pattern).collect(),
exact: parsed.exact.into_iter().map(|e| e.name).collect(),
}
})
}
#[must_use]
pub fn is_known_tooling_dependency(name: &str) -> bool {
let catalogue = catalogue();
catalogue
.prefixes
.iter()
.any(|p| name.starts_with(p.as_str()))
|| catalogue.exact.contains(name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn types_prefix_matches_scoped() {
assert!(is_known_tooling_dependency("@types/node"));
assert!(is_known_tooling_dependency("@types/react"));
assert!(is_known_tooling_dependency("@types/express"));
}
#[test]
fn types_prefix_does_not_match_similar_names() {
assert!(!is_known_tooling_dependency("type-fest"));
assert!(!is_known_tooling_dependency("typesafe-actions"));
}
#[test]
fn storybook_not_blanket_matched() {
assert!(!is_known_tooling_dependency("@storybook/react"));
assert!(!is_known_tooling_dependency("@storybook/addon-essentials"));
assert!(!is_known_tooling_dependency("storybook"));
}
#[test]
fn testing_library_prefix_matches() {
assert!(is_known_tooling_dependency("@testing-library/react"));
assert!(is_known_tooling_dependency("@testing-library/jest-dom"));
}
#[test]
fn babel_not_blanket_matched() {
assert!(!is_known_tooling_dependency("@babel/core"));
assert!(!is_known_tooling_dependency("@babel/preset-env"));
assert!(!is_known_tooling_dependency("babel-loader"));
assert!(!is_known_tooling_dependency("babel-jest"));
}
#[test]
fn vitest_prefix_matches() {
assert!(is_known_tooling_dependency("@vitest/coverage-v8"));
assert!(is_known_tooling_dependency("@vitest/ui"));
}
#[test]
fn eslint_not_blanket_matched() {
assert!(!is_known_tooling_dependency("eslint"));
assert!(!is_known_tooling_dependency("eslint-plugin-react"));
assert!(!is_known_tooling_dependency("eslint-config-next"));
assert!(!is_known_tooling_dependency("@typescript-eslint/parser"));
}
#[test]
fn biomejs_prefix_matches() {
assert!(is_known_tooling_dependency("@biomejs/biome"));
}
#[test]
fn exact_typescript_matches() {
assert!(is_known_tooling_dependency("typescript"));
}
#[test]
fn exact_prettier_matches() {
assert!(is_known_tooling_dependency("prettier"));
}
#[test]
fn exact_vitest_matches() {
assert!(is_known_tooling_dependency("vitest"));
}
#[test]
fn exact_jest_matches() {
assert!(is_known_tooling_dependency("jest"));
}
#[test]
fn exact_vite_matches() {
assert!(is_known_tooling_dependency("vite"));
}
#[test]
fn exact_esbuild_matches() {
assert!(is_known_tooling_dependency("esbuild"));
}
#[test]
fn exact_tsup_matches() {
assert!(is_known_tooling_dependency("tsup"));
}
#[test]
fn exact_turbo_matches() {
assert!(is_known_tooling_dependency("turbo"));
}
#[test]
fn common_runtime_deps_not_tooling() {
assert!(!is_known_tooling_dependency("react"));
assert!(!is_known_tooling_dependency("react-dom"));
assert!(!is_known_tooling_dependency("express"));
assert!(!is_known_tooling_dependency("lodash"));
assert!(!is_known_tooling_dependency("next"));
assert!(!is_known_tooling_dependency("vue"));
assert!(!is_known_tooling_dependency("axios"));
}
#[test]
fn empty_string_not_tooling() {
assert!(!is_known_tooling_dependency(""));
}
#[test]
fn near_miss_not_tooling() {
assert!(!is_known_tooling_dependency("type-fest"));
assert!(!is_known_tooling_dependency("typestyle"));
assert!(!is_known_tooling_dependency("prettier-bytes")); }
#[test]
fn sass_variants_are_tooling() {
assert!(is_known_tooling_dependency("sass"));
assert!(is_known_tooling_dependency("sass-embedded"));
}
#[test]
fn framework_plugin_packages_no_longer_exact_matched() {
assert!(!is_known_tooling_dependency("vite-plugin-svgr"));
assert!(!is_known_tooling_dependency("vite-plugin-eslint"));
assert!(!is_known_tooling_dependency("prettier-plugin-tailwindcss"));
assert!(!is_known_tooling_dependency(
"prettier-plugin-organize-imports"
));
assert!(!is_known_tooling_dependency(
"@ianvs/prettier-plugin-sort-imports"
));
}
#[test]
fn electron_forge_prefix_matches() {
assert!(is_known_tooling_dependency("@electron-forge/cli"));
assert!(is_known_tooling_dependency(
"@electron-forge/maker-squirrel"
));
}
#[test]
fn electron_prefix_matches() {
assert!(is_known_tooling_dependency("@electron/rebuild"));
assert!(is_known_tooling_dependency("@electron/notarize"));
}
#[test]
fn formatjs_prefix_matches() {
assert!(is_known_tooling_dependency("@formatjs/cli"));
assert!(is_known_tooling_dependency("@formatjs/intl"));
}
#[test]
fn rollup_not_blanket_matched() {
assert!(!is_known_tooling_dependency("@rollup/plugin-commonjs"));
assert!(!is_known_tooling_dependency("@rollup/plugin-node-resolve"));
assert!(!is_known_tooling_dependency("@rollup/plugin-typescript"));
}
#[test]
fn semantic_release_prefix_matches() {
assert!(is_known_tooling_dependency("@semantic-release/github"));
assert!(is_known_tooling_dependency("@semantic-release/npm"));
assert!(is_known_tooling_dependency("semantic-release"));
}
#[test]
fn release_it_prefix_matches() {
assert!(is_known_tooling_dependency(
"@release-it/conventional-changelog"
));
}
#[test]
fn lerna_lite_prefix_matches() {
assert!(is_known_tooling_dependency("@lerna-lite/cli"));
assert!(is_known_tooling_dependency("@lerna-lite/publish"));
}
#[test]
fn changesets_prefix_matches() {
assert!(is_known_tooling_dependency("@changesets/cli"));
assert!(is_known_tooling_dependency("@changesets/changelog-github"));
}
#[test]
fn graphql_codegen_prefix_matches() {
assert!(is_known_tooling_dependency("@graphql-codegen/cli"));
assert!(is_known_tooling_dependency(
"@graphql-codegen/typescript-operations"
));
}
#[test]
fn secretlint_prefix_matches() {
assert!(is_known_tooling_dependency("secretlint"));
assert!(is_known_tooling_dependency(
"@secretlint/secretlint-rule-preset-recommend"
));
}
#[test]
fn oxlint_prefix_matches() {
assert!(is_known_tooling_dependency("oxlint"));
}
#[test]
fn react_native_community_prefix_matches() {
assert!(is_known_tooling_dependency("@react-native-community/cli"));
assert!(is_known_tooling_dependency(
"@react-native-community/cli-platform-android"
));
}
#[test]
fn react_native_prefix_matches() {
assert!(is_known_tooling_dependency("@react-native/metro-config"));
assert!(is_known_tooling_dependency(
"@react-native/typescript-config"
));
}
#[test]
fn jest_prefix_matches() {
assert!(is_known_tooling_dependency("@jest/globals"));
assert!(is_known_tooling_dependency("@jest/types"));
}
#[test]
fn playwright_prefix_matches() {
assert!(is_known_tooling_dependency("@playwright/test"));
assert!(is_known_tooling_dependency("playwright"));
}
#[test]
fn tapjs_prefix_matches() {
assert!(is_known_tooling_dependency("@tapjs/test"));
assert!(is_known_tooling_dependency("@tapjs/snapshot"));
}
#[test]
fn exact_tap_matches() {
assert!(is_known_tooling_dependency("tap"));
}
#[test]
fn exact_rolldown_matches() {
assert!(is_known_tooling_dependency("rolldown"));
assert!(is_known_tooling_dependency("rolldown-vite"));
}
#[test]
fn exact_electron_matches() {
assert!(is_known_tooling_dependency("electron"));
assert!(is_known_tooling_dependency("electron-builder"));
assert!(is_known_tooling_dependency("electron-vite"));
}
#[test]
fn exact_sharp_matches() {
assert!(is_known_tooling_dependency("sharp"));
}
#[test]
fn exact_puppeteer_matches() {
assert!(is_known_tooling_dependency("puppeteer"));
}
#[test]
fn exact_madge_matches() {
assert!(is_known_tooling_dependency("madge"));
}
#[test]
fn exact_patch_package_matches() {
assert!(is_known_tooling_dependency("patch-package"));
}
#[test]
fn exact_nx_matches() {
assert!(is_known_tooling_dependency("nx"));
}
#[test]
fn exact_vue_tsc_matches() {
assert!(is_known_tooling_dependency("vue-tsc"));
}
#[test]
fn exact_tsconfig_packages_match() {
assert!(is_known_tooling_dependency("@tsconfig/node20"));
assert!(is_known_tooling_dependency("@tsconfig/react-native"));
assert!(is_known_tooling_dependency("@vue/tsconfig"));
}
#[test]
fn exact_vitejs_plugins_match() {
assert!(is_known_tooling_dependency("@vitejs/plugin-vue"));
assert!(is_known_tooling_dependency("@vitejs/plugin-react"));
assert!(is_known_tooling_dependency("@vitejs/plugin-react-swc"));
assert!(is_known_tooling_dependency("@vitejs/plugin-legacy"));
}
#[test]
fn exact_oxc_transform_matches() {
assert!(is_known_tooling_dependency("oxc-transform"));
}
#[test]
fn exact_typescript_native_preview_matches() {
assert!(is_known_tooling_dependency("@typescript/native-preview"));
}
#[test]
fn exact_tw_animate_css_matches() {
assert!(is_known_tooling_dependency("tw-animate-css"));
}
#[test]
fn exact_manypkg_cli_matches() {
assert!(is_known_tooling_dependency("@manypkg/cli"));
}
#[test]
fn exact_swc_variants_match() {
assert!(is_known_tooling_dependency("@swc/core"));
assert!(is_known_tooling_dependency("@swc/jest"));
}
#[test]
fn runtime_deps_with_similar_names_not_tooling() {
assert!(!is_known_tooling_dependency("react-scripts"));
assert!(!is_known_tooling_dependency("express-validator"));
assert!(!is_known_tooling_dependency("sass-loader")); }
#[test]
fn postcss_not_blanket_matched() {
assert!(!is_known_tooling_dependency("postcss-modules"));
assert!(!is_known_tooling_dependency("postcss-import"));
assert!(!is_known_tooling_dependency("autoprefixer"));
assert!(!is_known_tooling_dependency("tailwindcss"));
assert!(!is_known_tooling_dependency("@tailwindcss/typography"));
}
#[test]
fn catalogue_is_deterministic() {
assert_eq!(
is_known_tooling_dependency("typescript"),
is_known_tooling_dependency("typescript")
);
assert!(is_known_tooling_dependency("typescript"));
}
#[test]
fn catalogue_parses() {
let cat = catalogue();
assert!(!cat.prefixes.is_empty(), "catalogue must have prefixes");
assert!(!cat.exact.is_empty(), "catalogue must have exact entries");
assert!(cat.exact.contains("typescript"));
assert!(cat.prefixes.iter().any(|p| p == "@types/"));
}
#[test]
fn catalogue_has_no_empty_or_whitespace_prefixes() {
for prefix in &catalogue().prefixes {
assert!(
!prefix.trim().is_empty(),
"catalogue prefix must be non-empty / non-whitespace; got {prefix:?}"
);
}
}
#[test]
fn catalogue_has_no_duplicate_entries() {
let parsed: ToolingCatalogue = toml::from_str(CATALOGUE_TOML).unwrap();
let mut seen_exact = FxHashSet::default();
for entry in &parsed.exact {
assert!(
seen_exact.insert(entry.name.as_str()),
"duplicate exact catalogue entry: {:?}",
entry.name
);
}
let mut seen_prefix = FxHashSet::default();
for entry in &parsed.prefix {
assert!(
seen_prefix.insert(entry.pattern.as_str()),
"duplicate prefix catalogue entry: {:?}",
entry.pattern
);
}
}
#[test]
fn catalogue_rejects_framework_plugin_exact_entries() {
let parsed: ToolingCatalogue = toml::from_str(CATALOGUE_TOML).unwrap();
for entry in &parsed.exact {
let tail = entry
.name
.strip_prefix('@')
.and_then(|rest| rest.split_once('/'))
.map(|(_scope, tail)| tail);
for bad in FRAMEWORK_PLUGIN_FAMILY_PREFIXES {
assert!(
!entry.name.starts_with(bad) && !tail.is_some_and(|t| t.starts_with(bad)),
"exact catalogue entry {:?} is a framework plugin ({bad}); \
credit it in the relevant plugin's config parser instead of the catalogue",
entry.name,
);
}
for bad in FRAMEWORK_PLUGIN_SCOPED_PREFIXES {
assert!(
!entry.name.starts_with(bad),
"exact catalogue entry {:?} is a framework plugin ({bad}); \
credit it in the relevant plugin's config parser instead of the catalogue",
entry.name,
);
}
}
}
}