fallow-core 3.32.0

Internal detector backend for fallow-engine and fallow-api
Documentation
//! Tsup TypeScript library bundler plugin.
//!
//! Detects Tsup projects and marks config files as always used.
//! Parses tsup config to extract referenced dependencies.

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

const ENABLERS: &[&str] = &["tsup"];

const CONFIG_PATTERNS: &[&str] = &["tsup.config.{ts,mts,cts,js,cjs,mjs,json}"];

const ALWAYS_USED: &[&str] = &["tsup.config.{ts,mts,cts,js,cjs,mjs,json}"];

const TOOLING_DEPENDENCIES: &[&str] = &["tsup"];

define_plugin! {
    struct TsupPlugin => "tsup",
    enablers: ENABLERS,
    config_patterns: CONFIG_PATTERNS,
    always_used: ALWAYS_USED,
    tooling_dependencies: TOOLING_DEPENDENCIES,
    package_json_config_key: "tsup",
    resolve_config(config_path, source, root) {
        let mut result = PluginResult::default();
        super::add_import_referenced_dependencies(&mut result, source, config_path);

        let entries = config_parser::extract_config_array_or_object_string_or_array(source, config_path, &["entry"]);
        result.extend_config_dir_entry_patterns(entries, config_path, root);

        result
    }
}

#[cfg(test)]
mod tests {
    use std::path::Path;

    use super::*;

    #[test]
    fn resolve_config_entry_array() {
        let source = r#"
            export default {
                entry: ["src/index.ts", "src/cli.ts"]
            };
        "#;
        let plugin = TsupPlugin;
        let result = plugin.resolve_config(
            Path::new("/project/tsup.config.ts"),
            source,
            Path::new("/project"),
        );
        assert_eq!(result.entry_patterns, vec!["src/index.ts", "src/cli.ts"]);
    }

    #[test]
    fn resolve_config_entry_single() {
        let source = r#"
            export default {
                entry: ["src/index.ts"]
            };
        "#;
        let plugin = TsupPlugin;
        let result = plugin.resolve_config(
            Path::new("/project/tsup.config.ts"),
            source,
            Path::new("/project"),
        );
        assert_eq!(result.entry_patterns, vec!["src/index.ts"]);
    }

    #[test]
    fn resolve_config_imports() {
        let source = r#"
            import { defineConfig } from 'tsup';
            import react from '@vitejs/plugin-react';
            export default defineConfig({
                entry: ["src/index.ts"]
            });
        "#;
        let plugin = TsupPlugin;
        let result = plugin.resolve_config(
            Path::new("/project/tsup.config.ts"),
            source,
            Path::new("/project"),
        );
        assert!(result.referenced_dependencies.contains(&"tsup".to_string()));
        assert!(
            result
                .referenced_dependencies
                .contains(&"@vitejs/plugin-react".to_string())
        );
        assert_eq!(result.entry_patterns, vec!["src/index.ts"]);
    }

    #[test]
    fn resolve_config_empty() {
        let source = r"export default {};";
        let plugin = TsupPlugin;
        let result = plugin.resolve_config(
            Path::new("/project/tsup.config.ts"),
            source,
            Path::new("/project"),
        );
        assert!(result.entry_patterns.is_empty());
        assert!(result.referenced_dependencies.is_empty());
    }

    #[test]
    fn resolve_config_no_entry() {
        let source = r#"
            export default {
                format: ["cjs", "esm"]
            };
        "#;
        let plugin = TsupPlugin;
        let result = plugin.resolve_config(
            Path::new("/project/tsup.config.ts"),
            source,
            Path::new("/project"),
        );
        assert!(result.entry_patterns.is_empty());
    }

    #[test]
    fn resolve_config_define_config() {
        let source = r#"
            import { defineConfig } from 'tsup';
            export default defineConfig({
                entry: ["src/main.ts", "src/worker.ts"]
            });
        "#;
        let plugin = TsupPlugin;
        let result = plugin.resolve_config(
            Path::new("/project/tsup.config.ts"),
            source,
            Path::new("/project"),
        );
        assert_eq!(result.entry_patterns, vec!["src/main.ts", "src/worker.ts"]);
        assert!(result.referenced_dependencies.contains(&"tsup".to_string()));
    }

    #[test]
    fn resolve_config_entry_string() {
        let source = r#"
            import { defineConfig } from 'tsup';
            export default defineConfig({
                entry: "src/index.ts"
            });
        "#;
        let plugin = TsupPlugin;
        let result = plugin.resolve_config(
            Path::new("/project/tsup.config.ts"),
            source,
            Path::new("/project"),
        );
        assert_eq!(result.entry_patterns, vec!["src/index.ts"]);
    }

    #[test]
    fn resolve_config_entry_object_map() {
        let source = r#"
            import { defineConfig } from 'tsup';
            export default defineConfig({
                entry: { main: "src/main.ts", worker: "src/worker.ts" }
            });
        "#;
        let plugin = TsupPlugin;
        let result = plugin.resolve_config(
            Path::new("/project/tsup.config.ts"),
            source,
            Path::new("/project"),
        );
        assert_eq!(result.entry_patterns, vec!["src/main.ts", "src/worker.ts"]);
    }

    #[test]
    fn resolve_config_nested_entry_resolves_from_config_directory() {
        let source = r#"
            export default {
                entry: ["src/cli.ts", "./src/index.ts", "src/bin/*.ts", "!src/bin/skip.ts"]
            };
        "#;
        let plugin = TsupPlugin;
        let result = plugin.resolve_config(
            Path::new("/project/packages/lib/tsup.config.ts"),
            source,
            Path::new("/project"),
        );
        assert_eq!(
            result.entry_patterns,
            vec![
                "packages/lib/src/cli.ts",
                "packages/lib/src/index.ts",
                "packages/lib/src/bin/*.ts",
            ]
        );
        assert!(
            result
                .entry_patterns
                .iter()
                .all(|rule| rule.exclude_globs == ["packages/lib/src/bin/skip.ts"])
        );
    }

    #[test]
    fn resolve_config_entry_resolves_from_workspace_root() {
        let source = r#"
            export default {
                entry: ["src/cli.ts"]
            };
        "#;
        let plugin = TsupPlugin;
        let result = plugin.resolve_config(
            Path::new("/project/packages/lib/tsup.config.ts"),
            source,
            Path::new("/project/packages/lib"),
        );
        assert_eq!(result.entry_patterns, vec!["src/cli.ts"]);
    }

    #[test]
    fn resolve_config_entry_from_each_config_in_array() {
        let source = r#"
            import { defineConfig } from 'tsup';
            const shared = { format: ["esm"] };
            export default defineConfig([
                { entry: ["src/main.ts"] },
                { ...shared, entry: { worker: "src/worker.ts" } },
            ]);
        "#;
        let plugin = TsupPlugin;
        let result = plugin.resolve_config(
            Path::new("/project/tsup.config.ts"),
            source,
            Path::new("/project"),
        );
        assert_eq!(result.entry_patterns, vec!["src/main.ts", "src/worker.ts"]);
    }

    #[test]
    fn resolve_config_entry_from_plain_exported_array() {
        let source = r#"
            export default [
                { entry: "src/main.ts" },
                { entry: ["src/worker.ts"] },
            ];
        "#;
        let plugin = TsupPlugin;
        let result = plugin.resolve_config(
            Path::new("/project/tsup.config.ts"),
            source,
            Path::new("/project"),
        );
        assert_eq!(result.entry_patterns, vec!["src/main.ts", "src/worker.ts"]);
    }

    #[test]
    fn resolve_config_json_entry_object_map() {
        let source = r#"{ "entry": { "index": "src/index.ts", "cli": "src/cli.ts" } }"#;
        let result = TsupPlugin.resolve_config(
            Path::new("/project/tsup.config.json"),
            source,
            Path::new("/project"),
        );
        assert_eq!(result.entry_patterns, vec!["src/index.ts", "src/cli.ts"]);
    }

    #[test]
    fn reads_package_json_config_key() {
        assert_eq!(TsupPlugin.package_json_config_key(), Some("tsup"));
    }
}