lobe-core 0.1.0

Local HTTP performance profiling engine — the shared library behind the Lobe CLI. Captures DNS/TCP/TLS/TTFB/download phases per request with grounded network baselines.
Documentation
use std::fs;
use std::path::Path;

use crate::error::Result;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StackKind {
    NextJs,
    Express,
    Go,
    Rust,
    Nginx,
}

pub fn detect_stack_from_entries(entries: &[&str]) -> Vec<StackKind> {
    let mut stacks = Vec::new();

    if entries.iter().any(|entry| {
        matches!(
            *entry,
            "next.config.js" | "next.config.mjs" | "next.config.ts"
        )
    }) {
        stacks.push(StackKind::NextJs);
    }

    if entries.contains(&"package.json")
        && entries
            .iter()
            .any(|entry| matches!(*entry, "server.js" | "server.ts" | "app.js" | "app.ts"))
    {
        stacks.push(StackKind::Express);
    }

    if entries.contains(&"go.mod") {
        stacks.push(StackKind::Go);
    }

    if entries.contains(&"Cargo.toml") {
        stacks.push(StackKind::Rust);
    }

    if entries.contains(&"nginx.conf") {
        stacks.push(StackKind::Nginx);
    }

    stacks
}

pub fn detect_stack_in_dir<P: AsRef<Path>>(dir: P) -> Result<Vec<StackKind>> {
    let entries = fs::read_dir(dir)?
        .map(|entry| entry.map(|value| value.file_name().to_string_lossy().into_owned()))
        .collect::<std::result::Result<Vec<_>, _>>()?;

    let names = entries.iter().map(String::as_str).collect::<Vec<_>>();

    Ok(detect_stack_from_entries(&names))
}

#[cfg(test)]
mod tests {
    use std::fs;
    use std::path::PathBuf;
    use std::time::{SystemTime, UNIX_EPOCH};

    use super::{detect_stack_from_entries, detect_stack_in_dir, StackKind};

    #[test]
    fn detect_stack_from_entries_finds_multiple_known_stacks() {
        let stacks = detect_stack_from_entries(&[
            "package.json",
            "next.config.js",
            "go.mod",
            "Cargo.toml",
            "nginx.conf",
        ]);

        assert!(stacks.contains(&StackKind::NextJs));
        assert!(stacks.contains(&StackKind::Go));
        assert!(stacks.contains(&StackKind::Rust));
        assert!(stacks.contains(&StackKind::Nginx));
    }

    #[test]
    fn detect_stack_from_entries_uses_server_files_for_express() {
        let stacks = detect_stack_from_entries(&["package.json", "server.ts"]);

        assert_eq!(stacks, vec![StackKind::Express]);
    }

    #[test]
    fn detect_stack_in_dir_reads_directory_entries() {
        let dir = unique_temp_dir();
        fs::create_dir_all(&dir).expect("temp dir should be created");
        fs::write(dir.join("package.json"), "{}").expect("package.json should be written");
        fs::write(dir.join("next.config.ts"), "").expect("next config should be written");

        let stacks = detect_stack_in_dir(&dir).expect("stack detection should succeed");

        assert_eq!(stacks, vec![StackKind::NextJs]);

        fs::remove_dir_all(&dir).expect("temp dir should be removed");
    }

    fn unique_temp_dir() -> PathBuf {
        let suffix = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("time should move forward")
            .as_nanos();

        std::env::temp_dir().join(format!("lobe-stack-detect-{suffix}"))
    }
}