lobe_core/stack/
detect.rs1use 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}