Skip to main content

lobe_core/stack/
detect.rs

1use std::fs;
2use std::path::Path;
3
4use crate::error::Result;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum StackKind {
8    NextJs,
9    Express,
10    Go,
11    Rust,
12    Nginx,
13}
14
15pub fn detect_stack_from_entries(entries: &[&str]) -> Vec<StackKind> {
16    let mut stacks = Vec::new();
17
18    if entries.iter().any(|entry| {
19        matches!(
20            *entry,
21            "next.config.js" | "next.config.mjs" | "next.config.ts"
22        )
23    }) {
24        stacks.push(StackKind::NextJs);
25    }
26
27    if entries.contains(&"package.json")
28        && entries
29            .iter()
30            .any(|entry| matches!(*entry, "server.js" | "server.ts" | "app.js" | "app.ts"))
31    {
32        stacks.push(StackKind::Express);
33    }
34
35    if entries.contains(&"go.mod") {
36        stacks.push(StackKind::Go);
37    }
38
39    if entries.contains(&"Cargo.toml") {
40        stacks.push(StackKind::Rust);
41    }
42
43    if entries.contains(&"nginx.conf") {
44        stacks.push(StackKind::Nginx);
45    }
46
47    stacks
48}
49
50pub fn detect_stack_in_dir<P: AsRef<Path>>(dir: P) -> Result<Vec<StackKind>> {
51    let entries = fs::read_dir(dir)?
52        .map(|entry| entry.map(|value| value.file_name().to_string_lossy().into_owned()))
53        .collect::<std::result::Result<Vec<_>, _>>()?;
54
55    let names = entries.iter().map(String::as_str).collect::<Vec<_>>();
56
57    Ok(detect_stack_from_entries(&names))
58}
59
60#[cfg(test)]
61mod tests {
62    use std::fs;
63    use std::path::PathBuf;
64    use std::time::{SystemTime, UNIX_EPOCH};
65
66    use super::{detect_stack_from_entries, detect_stack_in_dir, StackKind};
67
68    #[test]
69    fn detect_stack_from_entries_finds_multiple_known_stacks() {
70        let stacks = detect_stack_from_entries(&[
71            "package.json",
72            "next.config.js",
73            "go.mod",
74            "Cargo.toml",
75            "nginx.conf",
76        ]);
77
78        assert!(stacks.contains(&StackKind::NextJs));
79        assert!(stacks.contains(&StackKind::Go));
80        assert!(stacks.contains(&StackKind::Rust));
81        assert!(stacks.contains(&StackKind::Nginx));
82    }
83
84    #[test]
85    fn detect_stack_from_entries_uses_server_files_for_express() {
86        let stacks = detect_stack_from_entries(&["package.json", "server.ts"]);
87
88        assert_eq!(stacks, vec![StackKind::Express]);
89    }
90
91    #[test]
92    fn detect_stack_in_dir_reads_directory_entries() {
93        let dir = unique_temp_dir();
94        fs::create_dir_all(&dir).expect("temp dir should be created");
95        fs::write(dir.join("package.json"), "{}").expect("package.json should be written");
96        fs::write(dir.join("next.config.ts"), "").expect("next config should be written");
97
98        let stacks = detect_stack_in_dir(&dir).expect("stack detection should succeed");
99
100        assert_eq!(stacks, vec![StackKind::NextJs]);
101
102        fs::remove_dir_all(&dir).expect("temp dir should be removed");
103    }
104
105    fn unique_temp_dir() -> PathBuf {
106        let suffix = SystemTime::now()
107            .duration_since(UNIX_EPOCH)
108            .expect("time should move forward")
109            .as_nanos();
110
111        std::env::temp_dir().join(format!("lobe-stack-detect-{suffix}"))
112    }
113}