use crate::core::detection::PackageJson;
#[derive(Debug, Clone)]
pub struct DetectedFramework {
pub name: String,
pub version: Option<String>,
pub confidence: u8,
}
impl DetectedFramework {
pub fn new(name: &str, version: Option<&str>, confidence: u8) -> Self {
Self {
name: name.into(),
version: version.map(|s| s.into()),
confidence,
}
}
}
#[derive(Debug, Clone)]
#[allow(unused)]
pub struct Framework {
pub name: &'static str,
pub package_names: &'static [&'static str],
pub config_files: &'static [&'static str],
pub detections: fn(&PackageJson) -> Option<DetectedFramework>,
}
impl Framework {
pub fn all() -> Vec<Framework> {
vec![
express(),
fastify(),
react(),
nextjs(),
vue(),
nodejs(),
typescript(),
jest(),
vite(),
tailwind(),
commonjs(),
esm(),
]
}
pub fn detect(&self, pkg: &PackageJson) -> Option<DetectedFramework> {
(self.detections)(pkg)
}
}
fn detect_dep(pkg: &PackageJson, name: &str) -> Option<String> {
pkg.dependencies
.get(name)
.cloned()
.or_else(|| pkg.dev_dependencies.get(name).cloned())
}
fn express() -> Framework {
Framework {
name: "Express",
package_names: &["express"],
config_files: &[],
detections: |pkg| {
detect_dep(pkg, "express").map(|v| DetectedFramework::new("Express", Some(&v), 95))
},
}
}
fn fastify() -> Framework {
Framework {
name: "Fastify",
package_names: &["fastify"],
config_files: &[],
detections: |pkg| {
detect_dep(pkg, "fastify").map(|v| DetectedFramework {
name: "Fastify".into(),
version: Some(v),
confidence: 95,
})
},
}
}
fn react() -> Framework {
Framework {
name: "React",
package_names: &["react", "react-dom"],
config_files: &["vite.config.ts", "vite.config.js", "webpack.config.js"],
detections: |pkg| {
let has_react = pkg.dependencies.contains_key("react")
|| pkg.dev_dependencies.contains_key("react");
let has_dom = pkg.dependencies.contains_key("react-dom")
|| pkg.dev_dependencies.contains_key("react-dom");
if has_react || has_dom {
let version = detect_dep(pkg, "react");
Some(DetectedFramework {
name: "React".into(),
version,
confidence: if has_dom { 95 } else { 80 },
})
} else {
None
}
},
}
}
fn nextjs() -> Framework {
Framework {
name: "Next.js",
package_names: &["next"],
config_files: &["next.config.js", "next.config.ts"],
detections: |pkg| {
detect_dep(pkg, "next").map(|v| DetectedFramework {
name: "Next.js".into(),
version: Some(v),
confidence: 95,
})
},
}
}
fn vue() -> Framework {
Framework {
name: "Vue",
package_names: &["vue"],
config_files: &["vite.config.ts", "vite.config.js", "vue.config.js"],
detections: |pkg| {
detect_dep(pkg, "vue").map(|v| DetectedFramework {
name: "Vue".into(),
version: Some(v),
confidence: 95,
})
},
}
}
fn nodejs() -> Framework {
Framework {
name: "Node.js",
package_names: &[],
config_files: &["package.json"],
detections: |pkg| {
if !pkg.dependencies.is_empty() || !pkg.dev_dependencies.is_empty() {
Some(DetectedFramework {
name: "Node.js".into(),
version: None,
confidence: 60,
})
} else {
None
}
},
}
}
fn typescript() -> Framework {
Framework {
name: "TypeScript",
package_names: &["typescript"],
config_files: &["tsconfig.json", "tsconfig.build.json"],
detections: |pkg| {
detect_dep(pkg, "typescript").map(|v| DetectedFramework {
name: "TypeScript".into(),
version: Some(v),
confidence: 90,
})
},
}
}
fn jest() -> Framework {
Framework {
name: "Jest",
package_names: &["jest", "@types/jest"],
config_files: &["jest.config.js", "jest.config.ts", "jest.config.json"],
detections: |pkg| {
detect_dep(pkg, "jest").map(|v| DetectedFramework {
name: "Jest".into(),
version: Some(v),
confidence: 95,
})
},
}
}
fn vite() -> Framework {
Framework {
name: "Vite",
package_names: &["vite"],
config_files: &["vite.config.ts", "vite.config.js"],
detections: |pkg| {
detect_dep(pkg, "vite").map(|v| DetectedFramework {
name: "Vite".into(),
version: Some(v),
confidence: 95,
})
},
}
}
fn tailwind() -> Framework {
Framework {
name: "Tailwind",
package_names: &["tailwindcss", "autoprefixer"],
config_files: &[
"tailwind.config.js",
"tailwind.config.ts",
"postcss.config.js",
],
detections: |pkg| {
if pkg.dependencies.contains_key("tailwindcss")
|| pkg.dev_dependencies.contains_key("tailwindcss")
{
Some(DetectedFramework {
name: "Tailwind".into(),
version: detect_dep(pkg, "tailwindcss"),
confidence: 90,
})
} else {
None
}
},
}
}
fn commonjs() -> Framework {
Framework {
name: "CommonJS",
package_names: &[],
config_files: &[],
detections: |pkg| {
if pkg.typ.as_deref() == Some("module") {
None
} else {
Some(DetectedFramework {
name: "CommonJS".into(),
version: None,
confidence: 70,
})
}
},
}
}
fn esm() -> Framework {
Framework {
name: "ESM",
package_names: &[],
config_files: &[],
detections: |pkg| {
if pkg.typ.as_deref() == Some("module") {
Some(DetectedFramework {
name: "ESM".into(),
version: None,
confidence: 90,
})
} else {
None
}
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_express_detection() {
let pkg = PackageJson {
name: "test".into(),
version: "1.0.0".into(),
dependencies: [("express".into(), "^4.18.0".into())].into(),
dev_dependencies: Default::default(),
scripts: Default::default(),
typ: None,
workspaces: None,
};
let frameworks = Framework::all();
let detected: Vec<_> = frameworks.iter().filter_map(|f| f.detect(&pkg)).collect();
assert!(detected.iter().any(|f| f.name == "Express"));
}
#[test]
fn test_react_detection() {
let pkg = PackageJson {
name: "test".into(),
version: "1.0.0".into(),
dependencies: [
("react".into(), "^18.0.0".into()),
("react-dom".into(), "^18.0.0".into()),
]
.into(),
dev_dependencies: Default::default(),
scripts: Default::default(),
typ: None,
workspaces: None,
};
let frameworks = Framework::all();
let detected: Vec<_> = frameworks.iter().filter_map(|f| f.detect(&pkg)).collect();
assert!(detected.iter().any(|f| f.name == "React"));
}
#[test]
fn test_typescript_detection() {
let pkg = PackageJson {
name: "test".into(),
version: "1.0.0".into(),
dependencies: Default::default(),
dev_dependencies: [("typescript".into(), "^5.0.0".into())].into(),
scripts: Default::default(),
typ: None,
workspaces: None,
};
let frameworks = Framework::all();
let detected: Vec<_> = frameworks.iter().filter_map(|f| f.detect(&pkg)).collect();
assert!(detected.iter().any(|f| f.name == "TypeScript"));
}
}