fallow-core 2.40.0

Core analysis engine for the fallow TypeScript/JavaScript codebase analyzer
Documentation
//! Tailwind CSS plugin.
//!
//! Detects Tailwind projects and marks config files as always used.
//! Parses tailwind.config to extract content globs and plugin dependencies.

use super::config_parser;
use super::{Plugin, PluginResult};

const ENABLERS: &[&str] = &["tailwindcss", "@tailwindcss/postcss"];

const CONFIG_PATTERNS: &[&str] = &["tailwind.config.{ts,js,cjs,mjs}"];

const ALWAYS_USED: &[&str] = &["tailwind.config.{ts,js,cjs,mjs}"];

const TOOLING_DEPENDENCIES: &[&str] = &["tailwindcss", "@tailwindcss/postcss"];

define_plugin! {
    struct TailwindPlugin => "tailwind",
    enablers: ENABLERS,
    config_patterns: CONFIG_PATTERNS,
    always_used: ALWAYS_USED,
    tooling_dependencies: TOOLING_DEPENDENCIES,
    resolve_config(config_path, source, _root) {
        let mut result = PluginResult::default();

        // Extract import sources as referenced dependencies
        let imports = config_parser::extract_imports(source, config_path);
        for imp in &imports {
            let dep = crate::resolve::extract_package_name(imp);
            result.referenced_dependencies.push(dep);
        }

        // content -> file globs that Tailwind scans for class usage
        // e.g. content: ["./src/**/*.{js,ts,jsx,tsx}", "./index.html"]
        let content = config_parser::extract_config_string_array(source, config_path, &["content"]);
        result.always_used_files.extend(content);

        // plugins as require() calls: plugins: [require("@tailwindcss/typography")]
        let require_deps =
            config_parser::extract_config_require_strings(source, config_path, "plugins");
        for dep in &require_deps {
            result
                .referenced_dependencies
                .push(crate::resolve::extract_package_name(dep));
        }

        // plugins as shallow strings (less common): plugins: ["@tailwindcss/typography"]
        let plugin_strings =
            config_parser::extract_config_shallow_strings(source, config_path, "plugins");
        for plugin in &plugin_strings {
            result
                .referenced_dependencies
                .push(crate::resolve::extract_package_name(plugin));
        }

        // presets -> referenced dependencies
        let presets = config_parser::extract_config_shallow_strings(source, config_path, "presets");
        for preset in &presets {
            result
                .referenced_dependencies
                .push(crate::resolve::extract_package_name(preset));
        }

        result
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn resolve_config_content_globs() {
        let source = r#"
            module.exports = {
                content: ["./src/**/*.{js,ts,jsx,tsx}", "./index.html"]
            };
        "#;
        let plugin = TailwindPlugin;
        let result = plugin.resolve_config(
            std::path::Path::new("tailwind.config.js"),
            source,
            std::path::Path::new("/project"),
        );
        assert_eq!(
            result.always_used_files,
            vec!["./src/**/*.{js,ts,jsx,tsx}", "./index.html"]
        );
    }

    #[test]
    fn resolve_config_plugins_require() {
        let source = r#"
            module.exports = {
                plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")]
            };
        "#;
        let plugin = TailwindPlugin;
        let result = plugin.resolve_config(
            std::path::Path::new("tailwind.config.js"),
            source,
            std::path::Path::new("/project"),
        );
        let deps = &result.referenced_dependencies;
        assert!(deps.contains(&"@tailwindcss/typography".to_string()));
        assert!(deps.contains(&"@tailwindcss/forms".to_string()));
    }

    #[test]
    fn resolve_config_plugins_string_array() {
        let source = r#"
            module.exports = {
                plugins: ["@tailwindcss/typography", "@tailwindcss/forms"]
            };
        "#;
        let plugin = TailwindPlugin;
        let result = plugin.resolve_config(
            std::path::Path::new("tailwind.config.js"),
            source,
            std::path::Path::new("/project"),
        );
        let deps = &result.referenced_dependencies;
        assert!(deps.contains(&"@tailwindcss/typography".to_string()));
        assert!(deps.contains(&"@tailwindcss/forms".to_string()));
    }

    #[test]
    fn resolve_config_presets() {
        let source = r#"
            module.exports = {
                presets: ["@acme/tailwind-preset", "my-preset"]
            };
        "#;
        let plugin = TailwindPlugin;
        let result = plugin.resolve_config(
            std::path::Path::new("tailwind.config.js"),
            source,
            std::path::Path::new("/project"),
        );
        let deps = &result.referenced_dependencies;
        assert!(deps.contains(&"@acme/tailwind-preset".to_string()));
        assert!(deps.contains(&"my-preset".to_string()));
    }

    #[test]
    fn resolve_config_imports() {
        let source = r#"
            import defaultTheme from 'tailwindcss/defaultTheme';
            import forms from '@tailwindcss/forms';
            module.exports = {
                content: ["./src/**/*.tsx"]
            };
        "#;
        let plugin = TailwindPlugin;
        let result = plugin.resolve_config(
            std::path::Path::new("tailwind.config.ts"),
            source,
            std::path::Path::new("/project"),
        );
        let deps = &result.referenced_dependencies;
        assert!(deps.contains(&"tailwindcss".to_string()));
        assert!(deps.contains(&"@tailwindcss/forms".to_string()));
    }

    #[test]
    fn resolve_config_combined() {
        let source = r#"
            import defaultTheme from 'tailwindcss/defaultTheme';
            module.exports = {
                content: ["./src/**/*.tsx"],
                plugins: [require("@tailwindcss/typography")],
                presets: ["my-preset"]
            };
        "#;
        let plugin = TailwindPlugin;
        let result = plugin.resolve_config(
            std::path::Path::new("tailwind.config.js"),
            source,
            std::path::Path::new("/project"),
        );
        assert_eq!(result.always_used_files, vec!["./src/**/*.tsx"]);
        let deps = &result.referenced_dependencies;
        assert!(deps.contains(&"tailwindcss".to_string()));
        assert!(deps.contains(&"@tailwindcss/typography".to_string()));
        assert!(deps.contains(&"my-preset".to_string()));
    }

    #[test]
    fn resolve_config_empty() {
        let source = r"module.exports = {};";
        let plugin = TailwindPlugin;
        let result = plugin.resolve_config(
            std::path::Path::new("tailwind.config.js"),
            source,
            std::path::Path::new("/project"),
        );
        assert!(result.always_used_files.is_empty());
        assert!(result.referenced_dependencies.is_empty());
    }

    #[test]
    fn resolve_config_scoped_package_in_plugins() {
        let source = r#"
            module.exports = {
                plugins: [require("@scope/plugin/nested")]
            };
        "#;
        let plugin = TailwindPlugin;
        let result = plugin.resolve_config(
            std::path::Path::new("tailwind.config.js"),
            source,
            std::path::Path::new("/project"),
        );
        assert!(
            result
                .referenced_dependencies
                .contains(&"@scope/plugin".to_string())
        );
    }
}