use std::path::Path;
use std::path::PathBuf;
use deno_core::error::AnyError;
use deno_core::serde_json;
pub struct FrameworkDetection {
pub name: &'static str,
pub entrypoint_code: String,
pub include_paths: Vec<String>,
pub build_command: Option<Vec<String>>,
}
impl FrameworkDetection {
pub fn static_asset_dirs(&self) -> &'static [&'static str] {
match self.name {
"Next.js" => &["public", "app", "src/app"],
"Fresh" | "SvelteKit" => &["static"],
_ => &["public"],
}
}
}
pub fn find_framework_favicon(
dir: &Path,
detection: &FrameworkDetection,
target_os: &str,
) -> Option<PathBuf> {
let exts: &[&str] = match target_os {
"macos" => &["icns", "png"],
"windows" => &["ico"],
_ => &["png"],
};
let names = ["icon", "favicon", "apple-touch-icon", "logo"];
for sub in detection.static_asset_dirs() {
let base = dir.join(sub);
if !base.is_dir() {
continue;
}
for name in names {
for ext in exts {
let candidate = base.join(format!("{name}.{ext}"));
if candidate.is_file() {
return Some(candidate);
}
}
}
}
None
}
pub fn detect_framework(
dir: &Path,
) -> Result<Option<FrameworkDetection>, AnyError> {
if has_config_file(dir, "next.config") {
return Ok(Some(detect_nextjs(dir)?));
}
if dir.join("fresh.gen.ts").exists() || dir.join("_fresh").is_dir() {
return Ok(Some(detect_fresh(dir)));
}
if has_config_file(dir, "astro.config") {
return Ok(Some(detect_astro(dir)));
}
if has_config_file(dir, "nuxt.config") {
return Ok(Some(detect_nuxt(dir)));
}
if has_config_file(dir, "svelte.config")
&& let Some(detection) = detect_sveltekit(dir)
{
return Ok(Some(detection));
}
if let Some(deps) = read_package_deps(dir) {
if deps.has("@remix-run/react") || deps.has_dev("@remix-run/dev") {
return Ok(Some(detect_remix(dir)));
}
if deps.has("@solidjs/start") {
return Ok(Some(detect_nitro_framework(dir, "SolidStart")));
}
if deps.has("@tanstack/react-start") || deps.has("@tanstack/solid-start") {
return Ok(Some(detect_nitro_framework(dir, "TanStack Start")));
}
}
let has_vite_dep = read_package_deps(dir)
.map(|deps| deps.has("vite") || deps.has_dev("vite"))
.unwrap_or(false);
if has_config_file(dir, "vite.config") || has_vite_dep {
return Ok(Some(detect_vite(dir)));
}
if let Some(imports) = read_deno_json_imports(dir)
&& imports
.iter()
.any(|i| i.starts_with("fresh") || i.starts_with("@fresh/core"))
{
return Ok(Some(detect_fresh(dir)));
}
Ok(None)
}
fn deno_exe() -> String {
std::env::current_exe()
.map(|p| p.display().to_string())
.unwrap_or_else(|_| "deno".into())
}
fn deno_task_build() -> Vec<String> {
vec![deno_exe(), "task".into(), "build".into()]
}
fn detect_nextjs(dir: &Path) -> Result<FrameworkDetection, AnyError> {
let version = detect_package_version(dir, "next").unwrap_or(15);
let entrypoint = format!(
r#"// @ts-nocheck
import {{ nextStart }} from "npm:next@^{version}/dist/cli/next-start.js";
globalThis.addEventListener("unhandledrejection", (e) => {{
console.error("[entrypoint] Unhandled rejection:", e.reason);
if (e.reason?.stack) console.error("[entrypoint] Stack:", e.reason.stack);
}});
// Guard: skip for forked workers (child_process.fork sets NODE_CHANNEL_FD).
// Workers use override_main_module to run their target script directly.
if (!Deno.env.get("NODE_CHANNEL_FD")) {{
// Use import.meta.dirname so paths resolve against the VFS in the
// compiled binary rather than the runtime CWD.
await nextStart({{ hostname: "0.0.0.0" }}, import.meta.dirname);
}}
"#,
);
let mut include_paths = vec![".next".into()];
if dir.join("public").is_dir() {
include_paths.push("public".into());
}
Ok(FrameworkDetection {
name: "Next.js",
entrypoint_code: entrypoint,
include_paths,
build_command: Some(deno_task_build()),
})
}
fn detect_astro(_dir: &Path) -> FrameworkDetection {
FrameworkDetection {
name: "Astro",
entrypoint_code: "// @ts-nocheck\nimport \"./dist/server/entry.mjs\";\n"
.into(),
include_paths: vec!["dist".into()],
build_command: Some(deno_task_build()),
}
}
fn detect_fresh(dir: &Path) -> FrameworkDetection {
let is_fresh2 = dir.join("_fresh/server.js").exists()
|| read_deno_json_imports(dir)
.map(|imports| imports.iter().any(|i| i.starts_with("@fresh/core")))
.unwrap_or(false);
if is_fresh2 {
let mut include_paths = vec!["_fresh".into()];
if dir.join("static").is_dir() {
include_paths.push("static".into());
}
FrameworkDetection {
name: "Fresh",
entrypoint_code: r#"// @ts-nocheck
const mod = await import("./_fresh/server.js");
Deno.serve(mod.default.fetch);
"#
.into(),
include_paths,
build_command: Some(vec![deno_exe(), "task".into(), "build".into()]),
}
} else {
FrameworkDetection {
name: "Fresh",
entrypoint_code: "// @ts-nocheck\nimport \"./main.ts\";\n".into(),
include_paths: vec![],
build_command: None,
}
}
}
fn detect_remix(dir: &Path) -> FrameworkDetection {
let mut include_paths = vec!["build".into()];
if dir.join("public").is_dir() {
include_paths.push("public".into());
}
FrameworkDetection {
name: "Remix",
entrypoint_code:
"// @ts-nocheck\nimport \"./node_modules/.bin/remix-serve\";\n".into(),
include_paths,
build_command: Some(deno_task_build()),
}
}
fn detect_nuxt(dir: &Path) -> FrameworkDetection {
detect_nitro_framework(dir, "Nuxt")
}
fn detect_sveltekit(dir: &Path) -> Option<FrameworkDetection> {
if dir.join(".deno-deploy/server.ts").exists() {
return Some(FrameworkDetection {
name: "SvelteKit",
entrypoint_code: "// @ts-nocheck\nimport \"./.deno-deploy/server.ts\";\n"
.into(),
include_paths: vec![".deno-deploy".into()],
build_command: Some(deno_task_build()),
});
}
if dir.join(".output/server/index.ts").exists()
|| dir.join(".output/server/index.mjs").exists()
{
let ext = if dir.join(".output/server/index.ts").exists() {
"ts"
} else {
"mjs"
};
return Some(FrameworkDetection {
name: "SvelteKit",
entrypoint_code: format!(
"// @ts-nocheck\nimport \"./.output/server/index.{ext}\";\n"
),
include_paths: vec![".output".into()],
build_command: Some(deno_task_build()),
});
}
if dir.join("build/index.js").exists()
&& dir.join("build/handler.js").exists()
{
return Some(FrameworkDetection {
name: "SvelteKit",
entrypoint_code: "// @ts-nocheck\nimport \"./build/index.js\";\n".into(),
include_paths: sveltekit_build_includes(dir),
build_command: Some(deno_task_build()),
});
}
let config_text =
["svelte.config.js", "svelte.config.ts", "svelte.config.mjs"]
.iter()
.find_map(|f| std::fs::read_to_string(dir.join(f)).ok())?;
if config_text.contains("@deno/svelte-adapter") {
return Some(FrameworkDetection {
name: "SvelteKit",
entrypoint_code: "// @ts-nocheck\nimport \"./.deno-deploy/server.ts\";\n"
.into(),
include_paths: vec![".deno-deploy".into()],
build_command: Some(deno_task_build()),
});
}
if config_text.contains("svelte-adapter-deno") {
return Some(FrameworkDetection {
name: "SvelteKit",
entrypoint_code: "// @ts-nocheck\nimport \"./build/index.js\";\n".into(),
include_paths: vec!["build/client".into()],
build_command: Some(deno_task_build()),
});
}
if config_text.contains("nitro") {
return Some(FrameworkDetection {
name: "SvelteKit",
entrypoint_code:
"// @ts-nocheck\nimport \"./.output/server/index.mjs\";\n".into(),
include_paths: vec![".output".into()],
build_command: Some(deno_task_build()),
});
}
None
}
fn sveltekit_build_includes(dir: &Path) -> Vec<String> {
["client", "static", "prerendered"]
.iter()
.map(|sub| format!("build/{sub}"))
.filter(|rel| dir.join(rel).is_dir())
.collect()
}
fn detect_nitro_framework(
dir: &Path,
name: &'static str,
) -> FrameworkDetection {
let ext = if dir.join(".output/server/index.ts").exists() {
"ts"
} else {
"mjs"
};
FrameworkDetection {
name,
entrypoint_code: format!(
"// @ts-nocheck\nimport \"./.output/server/index.{ext}\";\n"
),
include_paths: vec![".output".into()],
build_command: Some(deno_task_build()),
}
}
fn detect_vite(dir: &Path) -> FrameworkDetection {
if let Some(server_file) = ["server.js", "server.ts", "server.mjs"]
.iter()
.find(|f| dir.join(f).exists())
{
return FrameworkDetection {
name: "Vite",
entrypoint_code: format!("// @ts-nocheck\nimport \"./{server_file}\";\n"),
include_paths: vec!["dist".into()],
build_command: Some(deno_task_build()),
};
}
FrameworkDetection {
name: "Vite",
entrypoint_code: r#"// @ts-nocheck
import { serveDir } from "jsr:@std/http/file-server";
// `vite build` emits a static site into `dist/`. Resolve it against the VFS in
// the compiled binary via import.meta.dirname rather than the runtime CWD.
const fsRoot = import.meta.dirname + "/dist";
Deno.serve(async (req) => {
const res = await serveDir(req, { fsRoot, quiet: true });
// SPA fallback: route unmatched HTML navigations back to index.html so
// client-side routers keep working after a hard refresh.
if (
res.status === 404 &&
req.method === "GET" &&
(req.headers.get("accept") ?? "").includes("text/html")
) {
const index = new Request(new URL("/index.html", req.url), {
headers: req.headers,
});
return await serveDir(index, { fsRoot, quiet: true });
}
return res;
});
"#
.into(),
include_paths: vec!["dist".into()],
build_command: Some(deno_task_build()),
}
}
fn has_config_file(dir: &Path, base_name: &str) -> bool {
["js", "mjs", "ts", "mts", "cjs"]
.iter()
.any(|ext| dir.join(format!("{base_name}.{ext}")).exists())
}
fn read_package_deps(dir: &Path) -> Option<PackageDeps> {
let content = std::fs::read_to_string(dir.join("package.json")).ok()?;
let pkg: serde_json::Value = serde_json::from_str(&content).ok()?;
Some(PackageDeps {
deps: pkg
.get("dependencies")
.cloned()
.unwrap_or(serde_json::Value::Object(Default::default())),
dev_deps: pkg
.get("devDependencies")
.cloned()
.unwrap_or(serde_json::Value::Object(Default::default())),
})
}
struct PackageDeps {
deps: serde_json::Value,
dev_deps: serde_json::Value,
}
impl PackageDeps {
fn has(&self, name: &str) -> bool {
self.deps.get(name).is_some()
}
fn has_dev(&self, name: &str) -> bool {
self.dev_deps.get(name).is_some()
}
}
fn detect_package_version(dir: &Path, package: &str) -> Option<u32> {
let content = std::fs::read_to_string(dir.join("package.json")).ok()?;
let pkg: serde_json::Value = serde_json::from_str(&content).ok()?;
let ver_str = pkg
.get("dependencies")
.and_then(|d| d.get(package))
.or_else(|| pkg.get("devDependencies").and_then(|d| d.get(package)))?
.as_str()?;
ver_str
.chars()
.skip_while(|c: &char| !c.is_ascii_digit())
.take_while(|c: &char| c.is_ascii_digit())
.collect::<String>()
.parse()
.ok()
}
fn read_deno_json_imports(dir: &Path) -> Option<Vec<String>> {
let content = std::fs::read_to_string(dir.join("deno.json"))
.or_else(|_| std::fs::read_to_string(dir.join("deno.jsonc")))
.ok()?;
let config: serde_json::Value =
jsonc_parser::parse_to_serde_value(&content, &Default::default())
.ok()
.flatten()?;
let imports = config.get("imports")?.as_object()?;
Some(imports.keys().cloned().collect())
}
#[cfg(test)]
mod tests {
use std::fs;
use super::*;
fn setup_dir() -> tempfile::TempDir {
tempfile::tempdir().unwrap()
}
#[test]
fn no_framework_empty_dir() {
let dir = setup_dir();
let result = detect_framework(dir.path()).unwrap();
assert!(result.is_none());
}
#[test]
fn detects_nextjs_with_config_js() {
let dir = setup_dir();
fs::write(dir.path().join("next.config.js"), "").unwrap();
fs::write(
dir.path().join("package.json"),
r#"{"dependencies":{"next":"^15.0.0"}}"#,
)
.unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "Next.js");
assert_eq!(det.include_paths, vec![".next"]);
assert!(det.entrypoint_code.contains("next@^15"));
assert!(det.build_command.is_some());
let cmd = det.build_command.unwrap();
assert_eq!(cmd[1..], vec!["task", "build"]);
}
#[test]
fn nextjs_always_builds() {
let dir = setup_dir();
fs::write(dir.path().join("next.config.js"), "").unwrap();
fs::create_dir(dir.path().join(".next")).unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "Next.js");
assert!(det.build_command.is_some());
}
#[test]
fn detects_nextjs_with_config_mjs() {
let dir = setup_dir();
fs::write(dir.path().join("next.config.mjs"), "").unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "Next.js");
}
#[test]
fn detects_nextjs_with_config_ts() {
let dir = setup_dir();
fs::write(dir.path().join("next.config.ts"), "").unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "Next.js");
}
#[test]
fn nextjs_version_from_package_json() {
let dir = setup_dir();
fs::write(dir.path().join("next.config.js"), "").unwrap();
fs::write(
dir.path().join("package.json"),
r#"{"dependencies":{"next":"^14.2.3"}}"#,
)
.unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert!(det.entrypoint_code.contains("next@^14"));
}
#[test]
fn nextjs_defaults_to_v15() {
let dir = setup_dir();
fs::write(dir.path().join("next.config.js"), "").unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert!(det.entrypoint_code.contains("next@^15"));
}
#[test]
fn detects_fresh_gen_ts() {
let dir = setup_dir();
fs::write(dir.path().join("fresh.gen.ts"), "").unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "Fresh");
assert!(det.include_paths.is_empty());
assert!(det.entrypoint_code.contains("main.ts"));
}
#[test]
fn detects_fresh2_with_server_js() {
let dir = setup_dir();
fs::create_dir_all(dir.path().join("_fresh")).unwrap();
fs::write(dir.path().join("_fresh/server.js"), "").unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "Fresh");
assert!(det.entrypoint_code.contains("_fresh/server.js"));
let cmd = det.build_command.unwrap();
assert_eq!(cmd[1..], vec!["task", "build"]);
}
#[test]
fn fresh1_has_no_build_command() {
let dir = setup_dir();
fs::write(dir.path().join("fresh.gen.ts"), "").unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "Fresh");
assert!(det.build_command.is_none());
}
#[test]
fn fresh1_with_deno_json_stays_fresh1() {
let dir = setup_dir();
fs::write(dir.path().join("fresh.gen.ts"), "").unwrap();
fs::write(
dir.path().join("deno.json"),
r#"{"tasks":{"start":"deno run -A main.ts"}}"#,
)
.unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "Fresh");
assert!(det.entrypoint_code.contains("main.ts"));
assert!(det.build_command.is_none());
}
#[test]
fn fresh2_detected_via_fresh_core_import() {
let dir = setup_dir();
fs::write(dir.path().join("fresh.gen.ts"), "").unwrap();
fs::write(
dir.path().join("deno.json"),
r#"{"imports":{"@fresh/core":"jsr:@fresh/core@^2"}}"#,
)
.unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "Fresh");
assert!(det.entrypoint_code.contains("_fresh/server.js"));
assert!(det.build_command.is_some());
}
#[test]
fn detects_astro() {
let dir = setup_dir();
fs::write(dir.path().join("astro.config.mjs"), "").unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "Astro");
assert!(det.entrypoint_code.contains("dist/server/entry.mjs"));
assert_eq!(det.include_paths, vec!["dist"]);
assert!(det.build_command.is_some());
let cmd = det.build_command.unwrap();
assert_eq!(cmd[1..], vec!["task", "build"]);
}
#[test]
fn detects_nuxt() {
let dir = setup_dir();
fs::write(dir.path().join("nuxt.config.ts"), "").unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "Nuxt");
assert_eq!(det.include_paths, vec![".output"]);
let cmd = det.build_command.unwrap();
assert_eq!(cmd[1..], vec!["task", "build"]);
}
#[test]
fn detects_nuxt_with_ts_output() {
let dir = setup_dir();
fs::write(dir.path().join("nuxt.config.ts"), "").unwrap();
fs::create_dir_all(dir.path().join(".output/server")).unwrap();
fs::write(dir.path().join(".output/server/index.ts"), "").unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert!(det.entrypoint_code.contains("index.ts"));
}
#[test]
fn detects_sveltekit_deno_deploy() {
let dir = setup_dir();
fs::write(dir.path().join("svelte.config.js"), "").unwrap();
fs::create_dir_all(dir.path().join(".deno-deploy")).unwrap();
fs::write(dir.path().join(".deno-deploy/server.ts"), "").unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "SvelteKit");
assert!(det.entrypoint_code.contains(".deno-deploy/server.ts"));
assert_eq!(det.include_paths, vec![".deno-deploy"]);
let cmd = det.build_command.unwrap();
assert_eq!(cmd[1..], vec!["task", "build"]);
}
#[test]
fn detects_sveltekit_nitro_from_built_output() {
let dir = setup_dir();
fs::write(dir.path().join("svelte.config.ts"), "").unwrap();
fs::create_dir_all(dir.path().join(".output/server")).unwrap();
fs::write(dir.path().join(".output/server/index.mjs"), "").unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "SvelteKit");
assert_eq!(det.include_paths, vec![".output"]);
assert!(det.build_command.is_some());
}
#[test]
fn detects_sveltekit_adapter_deno_from_config() {
let dir = setup_dir();
fs::write(
dir.path().join("svelte.config.js"),
"import adapter from 'svelte-adapter-deno';\nexport default { kit: { adapter: adapter() } };\n",
)
.unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "SvelteKit");
assert!(det.entrypoint_code.contains("build/index.js"));
assert_eq!(det.include_paths, vec!["build/client"]);
}
#[test]
fn detects_sveltekit_adapter_deno_from_built_output() {
let dir = setup_dir();
fs::write(dir.path().join("svelte.config.js"), "").unwrap();
fs::create_dir_all(dir.path().join("build/client")).unwrap();
fs::create_dir_all(dir.path().join("build/prerendered")).unwrap();
fs::write(dir.path().join("build/index.js"), "").unwrap();
fs::write(dir.path().join("build/handler.js"), "").unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "SvelteKit");
assert!(det.entrypoint_code.contains("build/index.js"));
assert_eq!(det.include_paths, vec!["build/client", "build/prerendered"]);
assert!(det.build_command.is_some());
}
#[test]
fn detects_sveltekit_nitro_from_config() {
let dir = setup_dir();
fs::write(
dir.path().join("svelte.config.js"),
"// uses a nitro-based adapter\nexport default { kit: {} };\n",
)
.unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "SvelteKit");
assert_eq!(det.include_paths, vec![".output"]);
}
#[test]
fn does_not_detect_sveltekit_with_unknown_adapter() {
let dir = setup_dir();
fs::write(
dir.path().join("svelte.config.js"),
"import adapter from '@sveltejs/adapter-vercel';\nexport default { kit: { adapter: adapter() } };\n",
)
.unwrap();
assert!(detect_framework(dir.path()).unwrap().is_none());
}
#[test]
fn detects_remix_from_deps() {
let dir = setup_dir();
fs::write(
dir.path().join("package.json"),
r#"{"dependencies":{"@remix-run/react":"^2.0.0"}}"#,
)
.unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "Remix");
assert_eq!(det.include_paths, vec!["build"]);
let cmd = det.build_command.unwrap();
assert_eq!(cmd[1..], vec!["task", "build"]);
}
#[test]
fn detects_remix_from_dev_deps() {
let dir = setup_dir();
fs::write(
dir.path().join("package.json"),
r#"{"devDependencies":{"@remix-run/dev":"^2.0.0"}}"#,
)
.unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "Remix");
}
#[test]
fn detects_solidstart() {
let dir = setup_dir();
fs::write(
dir.path().join("package.json"),
r#"{"dependencies":{"@solidjs/start":"^1.0.0"}}"#,
)
.unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "SolidStart");
assert_eq!(det.include_paths, vec![".output"]);
let cmd = det.build_command.unwrap();
assert_eq!(cmd[1..], vec!["task", "build"]);
}
#[test]
fn detects_tanstack_start_react() {
let dir = setup_dir();
fs::write(
dir.path().join("package.json"),
r#"{"dependencies":{"@tanstack/react-start":"^1.0.0"}}"#,
)
.unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "TanStack Start");
}
#[test]
fn detects_tanstack_start_solid() {
let dir = setup_dir();
fs::write(
dir.path().join("package.json"),
r#"{"dependencies":{"@tanstack/solid-start":"^1.0.0"}}"#,
)
.unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "TanStack Start");
}
#[test]
fn detects_vite_ssr_with_server_js() {
let dir = setup_dir();
fs::write(dir.path().join("vite.config.js"), "").unwrap();
fs::write(dir.path().join("server.js"), "").unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "Vite");
assert!(det.entrypoint_code.contains("server.js"));
assert_eq!(det.include_paths, vec!["dist"]);
let cmd = det.build_command.unwrap();
assert_eq!(cmd[1..], vec!["task", "build"]);
}
#[test]
fn detects_vite_ssr_with_server_ts() {
let dir = setup_dir();
fs::write(dir.path().join("vite.config.ts"), "").unwrap();
fs::write(dir.path().join("server.ts"), "").unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "Vite");
assert!(det.entrypoint_code.contains("server.ts"));
}
#[test]
fn detects_vite_spa_from_config_without_server_file() {
let dir = setup_dir();
fs::write(dir.path().join("vite.config.js"), "").unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "Vite");
assert!(det.entrypoint_code.contains("serveDir"));
assert!(det.entrypoint_code.contains("/dist"));
assert!(det.entrypoint_code.contains("Deno.serve"));
assert_eq!(det.include_paths, vec!["dist"]);
let cmd = det.build_command.unwrap();
assert_eq!(cmd[1..], vec!["task", "build"]);
}
#[test]
fn detects_vite_from_package_dep() {
let dir = setup_dir();
fs::write(
dir.path().join("package.json"),
r#"{"devDependencies":{"vite":"^5.0.0"}}"#,
)
.unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "Vite");
assert!(det.entrypoint_code.contains("serveDir"));
assert_eq!(det.include_paths, vec!["dist"]);
}
#[test]
fn detects_vite_ssr_when_dep_and_server_present() {
let dir = setup_dir();
fs::write(
dir.path().join("package.json"),
r#"{"dependencies":{"vite":"^5.0.0"}}"#,
)
.unwrap();
fs::write(dir.path().join("server.ts"), "").unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "Vite");
assert!(det.entrypoint_code.contains("server.ts"));
}
#[test]
fn detects_fresh_from_deno_json_imports() {
let dir = setup_dir();
fs::write(
dir.path().join("deno.json"),
r#"{"imports":{"fresh":"jsr:@fresh/core"}}"#,
)
.unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "Fresh");
}
#[test]
fn detects_fresh_from_deno_json_fresh_core() {
let dir = setup_dir();
fs::write(
dir.path().join("deno.json"),
r#"{"imports":{"@fresh/core":"jsr:@fresh/core@^2"}}"#,
)
.unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "Fresh");
}
#[test]
fn detects_fresh_from_deno_jsonc() {
let dir = setup_dir();
fs::write(
dir.path().join("deno.jsonc"),
r#"{"imports":{"fresh":"jsr:@fresh/core"}}"#,
)
.unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "Fresh");
}
#[test]
fn detects_fresh_from_commented_deno_jsonc() {
let dir = setup_dir();
fs::write(
dir.path().join("deno.jsonc"),
"{\n // This is a comment\n \"imports\": {\n \"fresh\": \"jsr:@fresh/core\"\n }\n}\n",
)
.unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "Fresh");
}
#[test]
fn config_file_takes_priority_over_package_json() {
let dir = setup_dir();
fs::write(dir.path().join("next.config.js"), "").unwrap();
fs::write(
dir.path().join("package.json"),
r#"{"dependencies":{"@remix-run/react":"^2.0.0"}}"#,
)
.unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "Next.js");
}
#[test]
fn package_json_takes_priority_over_vite_ssr() {
let dir = setup_dir();
fs::write(dir.path().join("vite.config.js"), "").unwrap();
fs::write(dir.path().join("server.js"), "").unwrap();
fs::write(
dir.path().join("package.json"),
r#"{"dependencies":{"@solidjs/start":"^1.0.0"}}"#,
)
.unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "SolidStart");
}
#[test]
fn config_file_takes_priority_over_deno_json() {
let dir = setup_dir();
fs::write(dir.path().join("astro.config.mjs"), "").unwrap();
fs::write(
dir.path().join("deno.json"),
r#"{"imports":{"fresh":"jsr:@fresh/core"}}"#,
)
.unwrap();
let det = detect_framework(dir.path()).unwrap().unwrap();
assert_eq!(det.name, "Astro");
}
#[test]
fn has_config_file_various_extensions() {
let dir = setup_dir();
assert!(!has_config_file(dir.path(), "next.config"));
fs::write(dir.path().join("next.config.mts"), "").unwrap();
assert!(has_config_file(dir.path(), "next.config"));
}
#[test]
fn detect_package_version_parses_caret() {
let dir = setup_dir();
fs::write(
dir.path().join("package.json"),
r#"{"dependencies":{"next":"^16.1.6"}}"#,
)
.unwrap();
assert_eq!(detect_package_version(dir.path(), "next"), Some(16));
}
#[test]
fn detect_package_version_parses_tilde() {
let dir = setup_dir();
fs::write(
dir.path().join("package.json"),
r#"{"dependencies":{"next":"~15.0.0"}}"#,
)
.unwrap();
assert_eq!(detect_package_version(dir.path(), "next"), Some(15));
}
#[test]
fn detect_package_version_parses_exact() {
let dir = setup_dir();
fs::write(
dir.path().join("package.json"),
r#"{"dependencies":{"next":"14.2.3"}}"#,
)
.unwrap();
assert_eq!(detect_package_version(dir.path(), "next"), Some(14));
}
#[test]
fn detect_package_version_from_dev_deps() {
let dir = setup_dir();
fs::write(
dir.path().join("package.json"),
r#"{"devDependencies":{"next":"^13.0.0"}}"#,
)
.unwrap();
assert_eq!(detect_package_version(dir.path(), "next"), Some(13));
}
#[test]
fn detect_package_version_missing_package() {
let dir = setup_dir();
fs::write(
dir.path().join("package.json"),
r#"{"dependencies":{"react":"^18.0.0"}}"#,
)
.unwrap();
assert_eq!(detect_package_version(dir.path(), "next"), None);
}
#[test]
fn detect_package_version_no_package_json() {
let dir = setup_dir();
assert_eq!(detect_package_version(dir.path(), "next"), None);
}
#[test]
fn read_deno_json_imports_returns_keys() {
let dir = setup_dir();
fs::write(
dir.path().join("deno.json"),
r#"{"imports":{"foo":"jsr:@foo/bar","baz":"npm:baz"}}"#,
)
.unwrap();
let mut imports = read_deno_json_imports(dir.path()).unwrap();
imports.sort();
assert_eq!(imports, vec!["baz", "foo"]);
}
#[test]
fn read_deno_json_imports_no_file() {
let dir = setup_dir();
assert!(read_deno_json_imports(dir.path()).is_none());
}
#[test]
fn read_deno_json_imports_no_imports_key() {
let dir = setup_dir();
fs::write(dir.path().join("deno.json"), r#"{"tasks":{}}"#).unwrap();
assert!(read_deno_json_imports(dir.path()).is_none());
}
fn nextjs_detection() -> FrameworkDetection {
FrameworkDetection {
name: "Next.js",
entrypoint_code: String::new(),
include_paths: vec![],
build_command: None,
}
}
fn fresh_detection() -> FrameworkDetection {
FrameworkDetection {
name: "Fresh",
entrypoint_code: String::new(),
include_paths: vec![],
build_command: None,
}
}
#[test]
fn finds_favicon_in_public_for_linux() {
let dir = setup_dir();
fs::create_dir_all(dir.path().join("public")).unwrap();
fs::write(dir.path().join("public/favicon.png"), "").unwrap();
let det = nextjs_detection();
let p = find_framework_favicon(dir.path(), &det, "linux").unwrap();
assert_eq!(p, dir.path().join("public/favicon.png"));
}
#[test]
fn finds_ico_for_windows_but_not_png() {
let dir = setup_dir();
fs::create_dir_all(dir.path().join("public")).unwrap();
fs::write(dir.path().join("public/favicon.png"), "").unwrap();
let det = nextjs_detection();
assert!(find_framework_favicon(dir.path(), &det, "windows").is_none());
fs::write(dir.path().join("public/favicon.ico"), "").unwrap();
let p = find_framework_favicon(dir.path(), &det, "windows").unwrap();
assert_eq!(p, dir.path().join("public/favicon.ico"));
}
#[test]
fn macos_prefers_icns_over_png() {
let dir = setup_dir();
fs::create_dir_all(dir.path().join("public")).unwrap();
fs::write(dir.path().join("public/icon.png"), "").unwrap();
fs::write(dir.path().join("public/icon.icns"), "").unwrap();
let det = nextjs_detection();
let p = find_framework_favicon(dir.path(), &det, "macos").unwrap();
assert_eq!(p, dir.path().join("public/icon.icns"));
}
#[test]
fn icon_name_preferred_over_favicon() {
let dir = setup_dir();
fs::create_dir_all(dir.path().join("public")).unwrap();
fs::write(dir.path().join("public/favicon.png"), "").unwrap();
fs::write(dir.path().join("public/icon.png"), "").unwrap();
let det = nextjs_detection();
let p = find_framework_favicon(dir.path(), &det, "linux").unwrap();
assert_eq!(p, dir.path().join("public/icon.png"));
}
#[test]
fn nextjs_app_router_favicon_in_app_dir() {
let dir = setup_dir();
fs::create_dir_all(dir.path().join("app")).unwrap();
fs::write(dir.path().join("app/favicon.ico"), "").unwrap();
let det = nextjs_detection();
let p = find_framework_favicon(dir.path(), &det, "windows").unwrap();
assert_eq!(p, dir.path().join("app/favicon.ico"));
}
#[test]
fn fresh_uses_static_dir() {
let dir = setup_dir();
fs::create_dir_all(dir.path().join("static")).unwrap();
fs::write(dir.path().join("static/favicon.png"), "").unwrap();
let det = fresh_detection();
let p = find_framework_favicon(dir.path(), &det, "linux").unwrap();
assert_eq!(p, dir.path().join("static/favicon.png"));
}
#[test]
fn no_favicon_returns_none() {
let dir = setup_dir();
fs::create_dir_all(dir.path().join("public")).unwrap();
let det = nextjs_detection();
assert!(find_framework_favicon(dir.path(), &det, "linux").is_none());
}
#[test]
fn public_takes_priority_over_app_for_nextjs() {
let dir = setup_dir();
fs::create_dir_all(dir.path().join("public")).unwrap();
fs::create_dir_all(dir.path().join("app")).unwrap();
fs::write(dir.path().join("public/favicon.png"), "").unwrap();
fs::write(dir.path().join("app/icon.png"), "").unwrap();
let det = nextjs_detection();
let p = find_framework_favicon(dir.path(), &det, "linux").unwrap();
assert_eq!(p, dir.path().join("public/favicon.png"));
}
}