use std::path::Path;
pub fn detect_frameworks(project_root: &Path) -> Vec<String> {
let mut found: Vec<&'static str> = Vec::new();
if let Some(text) = read_manifest(project_root, "package.json") {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) {
let mut seen_keys: Vec<String> = Vec::new();
for field in ["dependencies", "devDependencies"] {
if let Some(obj) = v.get(field).and_then(serde_json::Value::as_object) {
for k in obj.keys() {
seen_keys.push(k.clone());
}
}
}
for key in &seen_keys {
if let Some(name) = match key.as_str() {
"next" => Some("Next.js"),
"react" => Some("React"),
"vue" => Some("Vue"),
"svelte" => Some("Svelte"),
"@angular/core" => Some("Angular"),
"vite" => Some("Vite"),
_ => None,
} {
push_unique(&mut found, name);
}
}
}
}
if let Some(text) = read_manifest(project_root, "Cargo.toml") {
if cargo_has_dep(&text, "axum") {
push_unique(&mut found, "Axum");
}
if cargo_has_dep(&text, "actix-web") {
push_unique(&mut found, "Actix-Web");
}
if cargo_has_dep(&text, "rocket") {
push_unique(&mut found, "Rocket");
}
}
if read_manifest(project_root, "pubspec.yaml").is_some() {
push_unique(&mut found, "Flutter");
}
if let Some(text) = read_manifest(project_root, "Gemfile") {
if gem_has(&text, "rails") {
push_unique(&mut found, "Rails");
}
}
let py_text = read_manifest(project_root, "pyproject.toml")
.or_else(|| read_manifest(project_root, "requirements.txt"))
.unwrap_or_default();
if !py_text.is_empty() {
if python_has(&py_text, "django") {
push_unique(&mut found, "Django");
}
if python_has(&py_text, "fastapi") {
push_unique(&mut found, "FastAPI");
}
if python_has(&py_text, "flask") {
push_unique(&mut found, "Flask");
}
}
let java_text = read_manifest(project_root, "pom.xml")
.or_else(|| read_manifest(project_root, "build.gradle"))
.or_else(|| read_manifest(project_root, "build.gradle.kts"))
.unwrap_or_default();
if !java_text.is_empty() && java_text.contains("spring-boot") {
push_unique(&mut found, "Spring Boot");
}
if let Some(text) = read_manifest(project_root, "composer.json") {
if text.contains("laravel/framework") {
push_unique(&mut found, "Laravel");
}
}
let mut out: Vec<String> = found.iter().map(|s| s.to_string()).collect();
out.sort();
out.dedup();
out
}
fn read_manifest(project_root: &Path, name: &str) -> Option<String> {
std::fs::read_to_string(project_root.join(name)).ok()
}
fn push_unique(out: &mut Vec<&'static str>, s: &'static str) {
if !out.contains(&s) {
out.push(s);
}
}
fn cargo_has_dep(text: &str, name: &str) -> bool {
for line in text.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix(name) {
if rest.starts_with(|c: char| c.is_whitespace() || c == '=') {
return true;
}
}
}
false
}
fn gem_has(text: &str, name: &str) -> bool {
let needle_single = format!("gem '{name}'");
let needle_double = format!("gem \"{name}\"");
text.contains(&needle_single) || text.contains(&needle_double)
}
fn python_has(text: &str, name: &str) -> bool {
text.to_ascii_lowercase()
.contains(&name.to_ascii_lowercase())
}
#[cfg(test)]
mod tests {
use super::*;
fn write_manifest(dir: &Path, name: &str, contents: &str) {
std::fs::write(dir.join(name), contents).expect("write manifest");
}
#[test]
fn detect_frameworks_finds_nextjs_and_react_in_package_json() {
let tmp = tempfile::tempdir().unwrap();
write_manifest(
tmp.path(),
"package.json",
r#"{ "dependencies": { "next": "14", "react": "18" } }"#,
);
let fws = detect_frameworks(tmp.path());
assert!(fws.contains(&"Next.js".to_string()), "got {fws:?}");
assert!(fws.contains(&"React".to_string()), "got {fws:?}");
}
#[test]
fn detect_frameworks_finds_vite_in_dev_dependencies() {
let tmp = tempfile::tempdir().unwrap();
write_manifest(
tmp.path(),
"package.json",
r#"{ "devDependencies": { "vite": "^5" } }"#,
);
let fws = detect_frameworks(tmp.path());
assert_eq!(fws, vec!["Vite".to_string()]);
}
#[test]
fn detect_frameworks_finds_axum_in_cargo_toml() {
let tmp = tempfile::tempdir().unwrap();
write_manifest(
tmp.path(),
"Cargo.toml",
"[dependencies]\naxum = \"0.7\"\nserde = \"1\"\n",
);
let fws = detect_frameworks(tmp.path());
assert!(fws.contains(&"Axum".to_string()), "got {fws:?}");
}
#[test]
fn detect_frameworks_finds_django_and_fastapi_in_requirements() {
let tmp = tempfile::tempdir().unwrap();
write_manifest(
tmp.path(),
"requirements.txt",
"Django==4.2\nfastapi==0.110\nrequests==2.31\n",
);
let fws = detect_frameworks(tmp.path());
assert!(fws.contains(&"Django".to_string()), "got {fws:?}");
assert!(fws.contains(&"FastAPI".to_string()), "got {fws:?}");
}
#[test]
fn detect_frameworks_finds_rails_in_gemfile() {
let tmp = tempfile::tempdir().unwrap();
write_manifest(
tmp.path(),
"Gemfile",
"source 'https://rubygems.org'\ngem 'rails', '~> 7.0'\n",
);
let fws = detect_frameworks(tmp.path());
assert_eq!(fws, vec!["Rails".to_string()]);
}
#[test]
fn detect_frameworks_finds_flutter_via_pubspec_presence() {
let tmp = tempfile::tempdir().unwrap();
write_manifest(tmp.path(), "pubspec.yaml", "name: my_app\n");
let fws = detect_frameworks(tmp.path());
assert_eq!(fws, vec!["Flutter".to_string()]);
}
#[test]
fn detect_frameworks_finds_spring_boot_in_pom() {
let tmp = tempfile::tempdir().unwrap();
write_manifest(
tmp.path(),
"pom.xml",
"<project><dependencies><dependency><groupId>org.springframework.boot</groupId>\
<artifactId>spring-boot-starter-web</artifactId></dependency></dependencies></project>",
);
let fws = detect_frameworks(tmp.path());
assert_eq!(fws, vec!["Spring Boot".to_string()]);
}
#[test]
fn detect_frameworks_finds_laravel_in_composer_json() {
let tmp = tempfile::tempdir().unwrap();
write_manifest(
tmp.path(),
"composer.json",
r#"{ "require": { "laravel/framework": "^10.0" } }"#,
);
let fws = detect_frameworks(tmp.path());
assert_eq!(fws, vec!["Laravel".to_string()]);
}
#[test]
fn detect_frameworks_returns_sorted_deduplicated() {
let tmp = tempfile::tempdir().unwrap();
write_manifest(
tmp.path(),
"package.json",
r#"{
"dependencies": { "react": "18", "next": "14" },
"devDependencies": { "react": "18", "vite": "5" }
}"#,
);
let fws = detect_frameworks(tmp.path());
let mut expected = vec![
"Next.js".to_string(),
"React".to_string(),
"Vite".to_string(),
];
expected.sort();
assert_eq!(fws, expected);
}
#[test]
fn detect_frameworks_returns_empty_for_empty_project() {
let tmp = tempfile::tempdir().unwrap();
let fws = detect_frameworks(tmp.path());
assert!(fws.is_empty(), "got {fws:?}");
}
#[test]
fn cargo_has_dep_avoids_prefix_collisions() {
let toml = "[dependencies]\naxum-extra = \"0.9\"\n";
assert!(!cargo_has_dep(toml, "axum"));
let toml2 = "[dependencies]\naxum = \"0.7\"\n";
assert!(cargo_has_dep(toml2, "axum"));
}
}