fallow-core 3.35.0

Internal detector backend for fallow-engine and fallow-api
Documentation
#![expect(
    clippy::disallowed_types,
    reason = "ScriptCatalog takes the serde-deserialized std HashMap"
)]

use std::collections::HashMap;
use std::path::Path;

use rustc_hash::{FxHashMap, FxHashSet};

use super::super::{
    BinaryConfigFile, IgnoredCommandEntries, ScriptAnalysis, ScriptCatalog,
    analyze_commands_with_context, referenced_package_scripts,
};
use super::{staged_commands, task_commands};

fn analyze(command: &str, scripts: &[(&str, &str)], declared: &[&str]) -> ScriptAnalysis {
    let scripts: HashMap<String, String> = scripts
        .iter()
        .map(|(name, body)| ((*name).to_string(), (*body).to_string()))
        .collect();
    let declared: FxHashSet<String> = declared.iter().map(|name| (*name).to_string()).collect();
    analyze_commands_with_context(
        &[command.to_string()],
        Path::new("/nonexistent"),
        &FxHashMap::default(),
        &declared,
        &ScriptCatalog::from_scripts(&scripts),
        IgnoredCommandEntries::NONE,
    )
}

#[test]
fn every_vite_plus_binary_credits_vite_plus() {
    for command in [
        "vp test",
        "vp lint",
        "vp pack",
        "vp config",
        "vpr build",
        "vpx some-tool",
        "vp exec some-tool",
    ] {
        let result = analyze(command, &[], &["vite-plus"]);
        assert!(
            result.used_packages.contains("vite-plus"),
            "`{command}` did not credit vite-plus: {:?}",
            result.used_packages
        );
    }
}

#[test]
fn lint_and_format_targets_are_not_entries() {
    for command in [
        "vp lint scripts/a.ts",
        "vp lint --fix scripts/a.ts",
        "vp fmt scripts/a.ts",
        "vp format scripts/a.ts",
        "vp check --fix scripts/a.ts",
    ] {
        let result = analyze(command, &[], &["vite-plus"]);
        assert!(
            result.entry_files.is_empty(),
            "`{command}` produced entries: {:?}",
            result.entry_files
        );
    }
}

#[test]
fn subcommands_without_file_arguments_make_no_entry() {
    for command in [
        "vp config scripts/a.ts",
        "vp run scripts/a.ts",
        "vp install a.ts",
    ] {
        let result = analyze(command, &[], &["vite-plus"]);
        assert!(
            result.entry_files.is_empty(),
            "`{command}` produced entries: {:?}",
            result.entry_files
        );
    }
}

#[test]
fn test_config_argument_goes_to_vitest() {
    let result = analyze("vp test -c vitest.e2e.config.ts", &[], &["vite-plus"]);
    assert_eq!(
        result.binary_config_files,
        [BinaryConfigFile {
            binary: "vitest".to_string(),
            path: "vitest.e2e.config.ts".to_string(),
        }]
    );
}

#[test]
fn pack_file_arguments_are_entries() {
    let result = analyze("vp pack src/cli.ts", &[], &["vite-plus"]);
    assert_eq!(result.entry_files, ["src/cli.ts"]);
}

#[test]
fn exec_and_vpx_run_the_child_command() {
    for command in [
        "vp exec tsx scripts/gen.ts",
        "vp exec -- tsx scripts/gen.ts",
        "vp dlx tsx scripts/gen.ts",
        "vpx tsx scripts/gen.ts",
    ] {
        let result = analyze(command, &[], &["vite-plus", "tsx"]);
        assert!(
            result.used_packages.contains("tsx"),
            "`{command}` did not credit tsx: {:?}",
            result.used_packages
        );
        assert!(
            result
                .entry_files
                .iter()
                .any(|file| file == "scripts/gen.ts"),
            "`{command}` did not make the entry: {:?}",
            result.entry_files
        );
    }
}

#[test]
fn exec_in_other_packages_makes_no_entry_here() {
    for command in [
        "vp exec -r tsx scripts/gen.ts",
        "vp exec --filter web tsx scripts/gen.ts",
        "vp exec --filter=web tsx scripts/gen.ts",
    ] {
        let result = analyze(command, &[], &["vite-plus", "tsx"]);
        assert!(
            result.entry_files.is_empty(),
            "`{command}` produced entries: {:?}",
            result.entry_files
        );
    }
}

#[test]
fn run_and_vpr_call_a_declared_script() {
    let scripts: HashMap<String, String> =
        HashMap::from([("gen".to_string(), "tsx scripts/gen.ts".to_string())]);
    let catalog = ScriptCatalog::from_scripts(&scripts);
    for command in [
        "vp run gen",
        "vp run --cache gen",
        "vp run --concurrency-limit 2 gen",
        "vpr gen",
        "vpr --verbose gen",
    ] {
        let names = referenced_package_scripts(command, &catalog);
        assert!(
            names.contains("gen"),
            "`{command}` did not call the script: {names:?}"
        );
    }
    assert!(referenced_package_scripts("vp run other", &catalog).is_empty());
}

fn write_config(source: &str) -> tempfile::TempDir {
    let temp = tempfile::tempdir().expect("tempdir");
    std::fs::write(temp.path().join("vite.config.ts"), source).expect("write config");
    temp
}

#[test]
fn staged_block_gives_string_and_array_commands() {
    let temp = write_config(
        r"
        import { defineConfig } from 'vite-plus';
        export default defineConfig({
            staged: {
                '*.ts': 'vp check --fix',
                '*.md': ['prettier --write', 'markdownlint'],
                '*.rs': (files) => `cargo fmt -- ${files.join(' ')}`,
            },
        });
        ",
    );
    assert_eq!(
        staged_commands(temp.path()),
        ["vp check --fix", "prettier --write", "markdownlint"]
    );
}

#[test]
fn run_tasks_give_commands_and_skip_a_task_with_cwd() {
    let temp = write_config(
        r"
        export default {
            run: {
                tasks: {
                    build: { command: 'tsc -p tsconfig.build.json', cache: true },
                    both: { command: ['node a.js', 'node b.js'] },
                    short: 'node scripts/short.ts',
                    elsewhere: { command: 'node a.js', cwd: 'subfolder' },
                },
            },
        };
        ",
    );
    assert_eq!(
        task_commands(temp.path()),
        [
            (
                "build".to_string(),
                "tsc -p tsconfig.build.json".to_string()
            ),
            ("both".to_string(), "node a.js && node b.js".to_string()),
            ("short".to_string(), "node scripts/short.ts".to_string()),
        ]
    );
}

#[test]
fn no_config_gives_no_commands() {
    let temp = tempfile::tempdir().expect("tempdir");
    assert!(staged_commands(temp.path()).is_empty());
    assert!(task_commands(temp.path()).is_empty());
}