corsa 0.7.0

Production-oriented Rust bindings, orchestration layers, and Node integration for typescript-go
Documentation
use std::{env, path::PathBuf};

use corsa::fast::{CompactString, SmallVec};

const HELP: &str = "\
usage: cargo run -p corsa --bin bench_tooling_compare -- [options]

options:
  --tsgo PATH                tsgo executable (default: .cache/tsgo)
  --node CMD                 node executable or command name (default: node)
  --dataset PATH             tsconfig path to benchmark (repeatable)
  --json-output PATH         write machine-readable benchmark JSON
  --suite SUITE              project-check | workflow | both (default: both)
  --iterations N             timed iterations per row (default: 10)
  --warmup-iterations N      untimed warmup iterations (default: 2)
  --timeout-ms N             per-process timeout in milliseconds (default: 60000)
  --help                     show this message
";

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Suite {
    ProjectCheck,
    Workflow,
}

#[derive(Clone, Debug)]
pub struct Cli {
    pub root_dir: PathBuf,
    pub tsgo_path: PathBuf,
    pub node_command: CompactString,
    pub dataset_paths: SmallVec<[PathBuf; 4]>,
    pub json_output_path: Option<PathBuf>,
    pub suites: SmallVec<[Suite; 2]>,
    pub iterations: usize,
    pub warmup_iterations: usize,
    pub timeout_ms: u64,
}

pub fn parse() -> Result<Option<Cli>, CompactString> {
    let root_dir = discover_root_dir()?;
    let mut tsgo_path = default_tsgo_path(&root_dir);
    let mut node_command = CompactString::from("node");
    let mut dataset_paths = SmallVec::<[PathBuf; 4]>::new();
    let mut json_output_path = None;
    let mut suites = both_suites();
    let mut iterations = 10_usize;
    let mut warmup_iterations = 2_usize;
    let mut timeout_ms = 60_000_u64;
    let mut args = env::args_os().skip(1);
    while let Some(argument) = args.next() {
        let argument = CompactString::from(argument.to_string_lossy().as_ref());
        match argument.as_str() {
            "--help" | "-h" => {
                println!("{HELP}");
                return Ok(None);
            }
            "--tsgo" => {
                tsgo_path = read_path(&mut args, &argument, &root_dir)?;
            }
            "--node" => {
                node_command = read_value(&mut args, &argument)?;
            }
            "--dataset" => {
                dataset_paths.push(read_path(&mut args, &argument, &root_dir)?);
            }
            "--json-output" => {
                json_output_path = Some(read_path(&mut args, &argument, &root_dir)?);
            }
            "--suite" => {
                suites = parse_suite(read_value(&mut args, &argument)?)?;
            }
            "--iterations" => {
                iterations = parse_usize(read_value(&mut args, &argument)?, &argument)?;
            }
            "--warmup-iterations" => {
                warmup_iterations = parse_usize(read_value(&mut args, &argument)?, &argument)?;
            }
            "--timeout-ms" => {
                timeout_ms = parse_u64(read_value(&mut args, &argument)?, &argument)?;
            }
            _ => return Err(argument),
        }
    }
    if dataset_paths.is_empty() {
        dataset_paths = default_datasets(&root_dir);
    }
    if dataset_paths.is_empty() {
        return Err(CompactString::from(
            "no datasets found; pass --dataset PATH explicitly",
        ));
    }
    if iterations == 0 {
        return Err(CompactString::from("--iterations must be > 0"));
    }
    if !tsgo_path.exists() {
        return Err(CompactString::from(tsgo_path.display().to_string()));
    }
    Ok(Some(Cli {
        root_dir,
        tsgo_path,
        node_command,
        dataset_paths,
        json_output_path,
        suites,
        iterations,
        warmup_iterations,
        timeout_ms,
    }))
}

fn discover_root_dir() -> Result<PathBuf, CompactString> {
    let cwd = env::current_dir().map_err(|error| CompactString::from(error.to_string()))?;
    for candidate in cwd.ancestors() {
        if candidate.join("pnpm-workspace.yaml").exists()
            && candidate.join("vite.config.ts").exists()
        {
            return Ok(candidate.to_path_buf());
        }
    }
    Ok(cwd)
}

fn both_suites() -> SmallVec<[Suite; 2]> {
    let mut suites = SmallVec::<[Suite; 2]>::new();
    suites.push(Suite::ProjectCheck);
    suites.push(Suite::Workflow);
    suites
}

fn default_tsgo_path(root_dir: &std::path::Path) -> PathBuf {
    let candidates = [
        root_dir.join(".cache/tsgo"),
        root_dir.join(".cache/tsgo.exe"),
        root_dir.join("ref/typescript-go/.cache/tsgo"),
        root_dir.join("ref/typescript-go/.cache/tsgo.exe"),
        root_dir.join("ref/typescript-go/built/local/tsgo"),
        root_dir.join("ref/typescript-go/built/local/tsgo.exe"),
    ];
    for candidate in candidates {
        if candidate.exists() {
            return candidate;
        }
    }
    root_dir.join(if cfg!(windows) {
        ".cache/tsgo.exe"
    } else {
        ".cache/tsgo"
    })
}

fn default_datasets(root_dir: &std::path::Path) -> SmallVec<[PathBuf; 4]> {
    let base = root_dir.join("ref/typescript-go");
    let candidates = [
        base.join("_packages/ast/tsconfig.json"),
        base.join("_packages/native-preview/tsconfig.json"),
        base.join("_packages/api/tsconfig.json"),
        base.join("_extension/tsconfig.json"),
    ];
    let mut datasets = SmallVec::<[PathBuf; 4]>::new();
    for path in candidates {
        if path.exists() {
            datasets.push(path);
        }
    }
    datasets
}

fn read_path(
    args: &mut impl Iterator<Item = std::ffi::OsString>,
    flag: &CompactString,
    root_dir: &std::path::Path,
) -> Result<PathBuf, CompactString> {
    let value = PathBuf::from(read_value(args, flag)?.as_str());
    if value.is_absolute() {
        Ok(value)
    } else {
        Ok(root_dir.join(value))
    }
}

fn read_value(
    args: &mut impl Iterator<Item = std::ffi::OsString>,
    flag: &CompactString,
) -> Result<CompactString, CompactString> {
    let Some(value) = args.next() else {
        return Err(CompactString::from(flag.as_str()));
    };
    Ok(CompactString::from(value.to_string_lossy().as_ref()))
}

fn parse_suite(value: CompactString) -> Result<SmallVec<[Suite; 2]>, CompactString> {
    match value.as_str() {
        "project-check" => {
            let mut suites = SmallVec::<[Suite; 2]>::new();
            suites.push(Suite::ProjectCheck);
            Ok(suites)
        }
        "workflow" => {
            let mut suites = SmallVec::<[Suite; 2]>::new();
            suites.push(Suite::Workflow);
            Ok(suites)
        }
        "both" => Ok(both_suites()),
        _ => Err(value),
    }
}

fn parse_usize(value: CompactString, _flag: &CompactString) -> Result<usize, CompactString> {
    value
        .parse::<usize>()
        .map_err(|_| CompactString::from(value.as_str()))
}

fn parse_u64(value: CompactString, _flag: &CompactString) -> Result<u64, CompactString> {
    value
        .parse::<u64>()
        .map_err(|_| CompactString::from(value.as_str()))
}

#[cfg(test)]
mod tests {
    use super::{Suite, both_suites, parse_suite, parse_u64, parse_usize};

    #[test]
    fn parse_suite_supports_all_variants() {
        assert_eq!(parse_suite("both".into()).unwrap(), both_suites());
        assert_eq!(
            parse_suite("project-check".into()).unwrap().as_slice(),
            &[Suite::ProjectCheck]
        );
        assert_eq!(
            parse_suite("workflow".into()).unwrap().as_slice(),
            &[Suite::Workflow]
        );
    }

    #[test]
    fn numeric_parsers_reject_invalid_values() {
        assert_eq!(
            parse_usize("10".into(), &"--iterations".into()).unwrap(),
            10
        );
        assert_eq!(parse_u64("20".into(), &"--timeout-ms".into()).unwrap(), 20);
        assert!(parse_usize("nope".into(), &"--iterations".into()).is_err());
        assert!(parse_u64("nope".into(), &"--timeout-ms".into()).is_err());
    }
}