use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Framework {
SvelteKit,
NextJs,
Nuxt,
Remix,
Astro,
}
impl std::fmt::Display for Framework {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
impl Framework {
pub fn detect_from_root(root: &Path) -> Option<Self> {
if root.join("svelte.config.js").exists() || root.join("svelte.config.ts").exists() {
return Some(Framework::SvelteKit);
}
if root.join("next.config.js").exists()
|| root.join("next.config.ts").exists()
|| root.join("next.config.mjs").exists()
{
return Some(Framework::NextJs);
}
if root.join("nuxt.config.js").exists()
|| root.join("nuxt.config.ts").exists()
|| root.join("nuxt.config.mjs").exists()
{
return Some(Framework::Nuxt);
}
if root.join("remix.config.js").exists() || root.join("remix.config.ts").exists() {
return Some(Framework::Remix);
}
if root.join("astro.config.js").exists()
|| root.join("astro.config.ts").exists()
|| root.join("astro.config.mjs").exists()
{
return Some(Framework::Astro);
}
None
}
pub fn name(&self) -> &'static str {
match self {
Framework::SvelteKit => "SvelteKit",
Framework::NextJs => "Next.js",
Framework::Nuxt => "Nuxt",
Framework::Remix => "Remix",
Framework::Astro => "Astro",
}
}
fn route_handler_exports(&self) -> HashSet<&'static str> {
match self {
Framework::SvelteKit => {
vec![
"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD",
"load", ]
.into_iter()
.collect()
}
Framework::NextJs => {
vec!["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
.into_iter()
.collect()
}
Framework::Nuxt => {
vec!["default", "get", "post", "put", "patch", "delete"]
.into_iter()
.collect()
}
Framework::Remix => {
vec!["loader", "action", "default", "headers", "meta", "links"]
.into_iter()
.collect()
}
Framework::Astro => {
vec!["get", "post", "put", "patch", "del", "all"]
.into_iter()
.collect()
}
}
}
fn is_route_file(&self, path: &str) -> bool {
let path_lower = path.to_lowercase();
let has_segment = |segment: &str| {
let with_slashes = format!("/{}/", segment);
let at_start = format!("{}/", segment);
path_lower.contains(&with_slashes) || path_lower.starts_with(&at_start)
};
let ends_with_file = |prefix: &str| {
let pattern = format!("/{prefix}.");
path_lower.contains(&pattern) || path_lower.starts_with(&format!("{prefix}."))
};
match self {
Framework::SvelteKit => {
has_segment("routes")
&& (path_lower.contains("+server.")
|| path_lower.contains("+page.")
|| path_lower.contains("+layout."))
}
Framework::NextJs => {
(has_segment("app") && ends_with_file("route"))
|| (has_segment("pages")
&& !path_lower.contains("_app.")
&& !path_lower.contains("_document."))
}
Framework::Nuxt => {
(has_segment("server") && has_segment("api")) || has_segment("pages")
}
Framework::Remix => {
has_segment("routes")
}
Framework::Astro => {
has_segment("pages")
}
}
}
fn is_conventional_export(&self, symbol_name: &str, file_path: &str) -> bool {
if !self.is_route_file(file_path) {
return false;
}
self.route_handler_exports().contains(symbol_name)
}
}
pub fn detect_frameworks(root: &Path) -> Vec<Framework> {
let mut frameworks = Vec::new();
if let Some(fw) = Framework::detect_from_root(root) {
frameworks.push(fw);
}
for subdir_name in &["apps", "packages"] {
let subdir = root.join(subdir_name);
if subdir.is_dir()
&& let Ok(entries) = std::fs::read_dir(&subdir)
{
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir()
&& let Some(fw) = Framework::detect_from_root(&path)
&& !frameworks.contains(&fw)
{
frameworks.push(fw);
}
}
}
}
frameworks
}
pub fn is_framework_convention(
symbol_name: &str,
file_path: &str,
frameworks: &[Framework],
) -> bool {
frameworks
.iter()
.any(|fw| fw.is_conventional_export(symbol_name, file_path))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_sveltekit_detection() {
let temp = TempDir::new().unwrap();
fs::write(temp.path().join("svelte.config.js"), "export default {}").unwrap();
let detected = Framework::detect_from_root(temp.path());
assert_eq!(detected, Some(Framework::SvelteKit));
}
#[test]
fn test_nextjs_detection() {
let temp = TempDir::new().unwrap();
fs::write(temp.path().join("next.config.js"), "module.exports = {}").unwrap();
let detected = Framework::detect_from_root(temp.path());
assert_eq!(detected, Some(Framework::NextJs));
}
#[test]
fn test_nuxt_detection() {
let temp = TempDir::new().unwrap();
fs::write(temp.path().join("nuxt.config.ts"), "export default {}").unwrap();
let detected = Framework::detect_from_root(temp.path());
assert_eq!(detected, Some(Framework::Nuxt));
}
#[test]
fn test_no_framework_detection() {
let temp = TempDir::new().unwrap();
let detected = Framework::detect_from_root(temp.path());
assert_eq!(detected, None);
}
#[test]
fn test_sveltekit_route_handlers() {
let framework = Framework::SvelteKit;
let handlers = framework.route_handler_exports();
assert!(handlers.contains("GET"));
assert!(handlers.contains("POST"));
assert!(handlers.contains("PUT"));
assert!(handlers.contains("DELETE"));
assert!(handlers.contains("load"));
}
#[test]
fn test_sveltekit_is_route_file() {
let framework = Framework::SvelteKit;
assert!(framework.is_route_file("src/routes/api/users/+server.ts"));
assert!(framework.is_route_file("src/routes/blog/+page.ts"));
assert!(framework.is_route_file("src/routes/+layout.svelte"));
assert!(!framework.is_route_file("src/lib/utils.ts"));
assert!(!framework.is_route_file("src/components/Button.svelte"));
}
#[test]
fn test_nextjs_is_route_file() {
let framework = Framework::NextJs;
assert!(framework.is_route_file("app/api/users/route.ts"));
assert!(framework.is_route_file("app/dashboard/route.js"));
assert!(framework.is_route_file("pages/index.tsx"));
assert!(!framework.is_route_file("components/Button.tsx"));
assert!(!framework.is_route_file("lib/utils.ts"));
}
#[test]
fn test_sveltekit_conventional_export() {
let framework = Framework::SvelteKit;
assert!(framework.is_conventional_export("GET", "src/routes/api/users/+server.ts"));
assert!(framework.is_conventional_export("POST", "src/routes/api/posts/+server.js"));
assert!(framework.is_conventional_export("load", "src/routes/blog/+page.ts"));
assert!(!framework.is_conventional_export("GET", "src/lib/api.ts"));
assert!(!framework.is_conventional_export("myCustomFunction", "src/routes/api/+server.ts"));
}
#[test]
fn test_is_framework_convention_helper() {
let frameworks = vec![Framework::SvelteKit, Framework::NextJs];
assert!(is_framework_convention(
"GET",
"src/routes/api/users/+server.ts",
&frameworks
));
assert!(is_framework_convention(
"GET",
"app/api/users/route.ts",
&frameworks
));
assert!(!is_framework_convention(
"GET",
"src/lib/utils.ts",
&frameworks
));
assert!(!is_framework_convention(
"GET",
"src/routes/api/users/+server.ts",
&[]
));
}
#[test]
fn test_framework_names() {
assert_eq!(Framework::SvelteKit.name(), "SvelteKit");
assert_eq!(Framework::NextJs.name(), "Next.js");
assert_eq!(Framework::Nuxt.name(), "Nuxt");
assert_eq!(Framework::Remix.name(), "Remix");
assert_eq!(Framework::Astro.name(), "Astro");
}
}