use std::path::Path;
use super::{Runtime, WasmTargetHint};
pub fn detect_runtime(context_path: impl AsRef<Path>) -> Option<Runtime> {
let path = context_path.as_ref();
if let Some(hint) = detect_wasm_hint(path) {
return Some(Runtime::Wasm(hint));
}
if path.join("package.json").exists() {
if path.join("bun.lockb").exists() {
return Some(Runtime::Bun);
}
if path.join("deno.json").exists() || path.join("deno.jsonc").exists() {
return Some(Runtime::Deno);
}
return Some(Runtime::Node20);
}
if path.join("deno.json").exists()
|| path.join("deno.jsonc").exists()
|| path.join("deno.lock").exists()
{
return Some(Runtime::Deno);
}
if path.join("Cargo.toml").exists() {
return Some(Runtime::Rust);
}
if path.join("pyproject.toml").exists()
|| path.join("requirements.txt").exists()
|| path.join("setup.py").exists()
|| path.join("Pipfile").exists()
|| path.join("poetry.lock").exists()
{
return Some(Runtime::Python312);
}
if path.join("go.mod").exists() {
return Some(Runtime::Go);
}
None
}
pub fn detect_runtime_with_version(context_path: impl AsRef<Path>) -> Option<Runtime> {
let path = context_path.as_ref();
let base_runtime = detect_runtime(path)?;
match base_runtime {
Runtime::Node20 | Runtime::Node22 => {
if let Some(version) = read_node_version(path) {
if version.starts_with("22") || version.starts_with("v22") {
return Some(Runtime::Node22);
}
if version.starts_with("20") || version.starts_with("v20") {
return Some(Runtime::Node20);
}
}
if let Some(version) = read_package_node_version(path) {
if version.contains("22") {
return Some(Runtime::Node22);
}
}
Some(Runtime::Node20)
}
Runtime::Python312 | Runtime::Python313 => {
if let Some(version) = read_python_version(path) {
if version.starts_with("3.13") {
return Some(Runtime::Python313);
}
}
Some(Runtime::Python312)
}
other => Some(other),
}
}
fn detect_wasm_hint(path: &Path) -> Option<WasmTargetHint> {
if path.join("cargo-component.toml").exists() {
return Some(WasmTargetHint::Component);
}
let cargo_toml = path.join("Cargo.toml");
let cargo_has_component_metadata = if cargo_toml.exists() {
cargo_toml_has_component_metadata(&cargo_toml)
} else {
false
};
if cargo_has_component_metadata {
return Some(WasmTargetHint::Component);
}
if path.join("componentize-py.config").exists() {
return Some(WasmTargetHint::Component);
}
if path.join("package.json").exists() && package_json_uses_jco(&path.join("package.json")) {
return Some(WasmTargetHint::Component);
}
if cargo_toml.exists() && cargo_config_targets_wasip(path) {
return Some(WasmTargetHint::Module);
}
None
}
fn cargo_toml_has_component_metadata(cargo_toml: &Path) -> bool {
let Ok(content) = std::fs::read_to_string(cargo_toml) else {
return false;
};
content.lines().any(|line| {
let trimmed = line.trim();
trimmed == "[package.metadata.component]"
|| trimmed.starts_with("[package.metadata.component.")
})
}
const JCO_DEP_SECTIONS: &[&str] = &[
"dependencies",
"devDependencies",
"peerDependencies",
"optionalDependencies",
];
fn package_json_uses_jco(package_json: &Path) -> bool {
let Ok(content) = std::fs::read_to_string(package_json) else {
return false;
};
let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) else {
return false;
};
for section in JCO_DEP_SECTIONS {
if let Some(obj) = json.get(section).and_then(|v| v.as_object()) {
if obj.contains_key("jco") || obj.contains_key("@bytecodealliance/jco") {
return true;
}
}
}
false
}
fn cargo_config_targets_wasip(path: &Path) -> bool {
let config = path.join(".cargo").join("config.toml");
let Ok(content) = std::fs::read_to_string(&config) else {
return false;
};
content.lines().any(|line| {
let trimmed = line.trim();
trimmed.contains("wasm32-wasip1") || trimmed.contains("wasm32-wasip2")
})
}
fn read_node_version(path: &Path) -> Option<String> {
for filename in &[".nvmrc", ".node-version"] {
let version_file = path.join(filename);
if version_file.exists() {
if let Ok(content) = std::fs::read_to_string(&version_file) {
let version = content.trim().to_string();
if !version.is_empty() {
return Some(version);
}
}
}
}
None
}
fn read_package_node_version(path: &Path) -> Option<String> {
let package_json = path.join("package.json");
if package_json.exists() {
if let Ok(content) = std::fs::read_to_string(&package_json) {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
if let Some(engines) = json.get("engines") {
if let Some(node) = engines.get("node") {
if let Some(version) = node.as_str() {
return Some(version.to_string());
}
}
}
}
}
}
None
}
fn read_python_version(path: &Path) -> Option<String> {
let python_version = path.join(".python-version");
if python_version.exists() {
if let Ok(content) = std::fs::read_to_string(&python_version) {
let version = content.trim().to_string();
if !version.is_empty() {
return Some(version);
}
}
}
let pyproject = path.join("pyproject.toml");
if pyproject.exists() {
if let Ok(content) = std::fs::read_to_string(&pyproject) {
for line in content.lines() {
let line = line.trim();
if line.starts_with("requires-python") {
if let Some(version) = line.split('=').nth(1) {
let version = version.trim().trim_matches('"').trim_matches('\'');
return Some(version.to_string());
}
}
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn create_temp_dir() -> TempDir {
TempDir::new().expect("Failed to create temp directory")
}
#[test]
fn test_detect_nodejs_project() {
let dir = create_temp_dir();
fs::write(dir.path().join("package.json"), "{}").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Node20));
}
#[test]
fn test_detect_bun_project() {
let dir = create_temp_dir();
fs::write(dir.path().join("package.json"), "{}").unwrap();
fs::write(dir.path().join("bun.lockb"), "").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Bun));
}
#[test]
fn test_detect_deno_project() {
let dir = create_temp_dir();
fs::write(dir.path().join("deno.json"), "{}").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Deno));
}
#[test]
fn test_detect_deno_with_package_json() {
let dir = create_temp_dir();
fs::write(dir.path().join("package.json"), "{}").unwrap();
fs::write(dir.path().join("deno.json"), "{}").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Deno));
}
#[test]
fn test_detect_rust_project() {
let dir = create_temp_dir();
fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Rust));
}
#[test]
fn test_detect_python_requirements() {
let dir = create_temp_dir();
fs::write(dir.path().join("requirements.txt"), "flask==2.0").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Python312));
}
#[test]
fn test_detect_python_pyproject() {
let dir = create_temp_dir();
fs::write(dir.path().join("pyproject.toml"), "[project]").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Python312));
}
#[test]
fn test_detect_go_project() {
let dir = create_temp_dir();
fs::write(dir.path().join("go.mod"), "module example.com/app").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Go));
}
#[test]
fn test_detect_no_runtime() {
let dir = create_temp_dir();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, None);
}
#[test]
fn test_detect_node22_from_nvmrc() {
let dir = create_temp_dir();
fs::write(dir.path().join("package.json"), "{}").unwrap();
fs::write(dir.path().join(".nvmrc"), "22.0.0").unwrap();
let runtime = detect_runtime_with_version(dir.path());
assert_eq!(runtime, Some(Runtime::Node22));
}
#[test]
fn test_detect_node22_from_package_engines() {
let dir = create_temp_dir();
let package_json = r#"{"engines": {"node": ">=22.0.0"}}"#;
fs::write(dir.path().join("package.json"), package_json).unwrap();
let runtime = detect_runtime_with_version(dir.path());
assert_eq!(runtime, Some(Runtime::Node22));
}
#[test]
fn test_detect_python313_from_version_file() {
let dir = create_temp_dir();
fs::write(dir.path().join("requirements.txt"), "flask").unwrap();
fs::write(dir.path().join(".python-version"), "3.13.0").unwrap();
let runtime = detect_runtime_with_version(dir.path());
assert_eq!(runtime, Some(Runtime::Python313));
}
#[test]
fn test_detect_wasm_cargo_component_toml() {
let dir = create_temp_dir();
fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"foo\"").unwrap();
fs::write(dir.path().join("cargo-component.toml"), "").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Wasm(WasmTargetHint::Component)));
}
#[test]
fn test_detect_wasm_cargo_metadata_component() {
let dir = create_temp_dir();
let toml = r#"
[package]
name = "foo"
version = "0.1.0"
[package.metadata.component]
package = "zlayer:example"
"#;
fs::write(dir.path().join("Cargo.toml"), toml).unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Wasm(WasmTargetHint::Component)));
}
#[test]
fn test_detect_wasm_componentize_py() {
let dir = create_temp_dir();
fs::write(dir.path().join("pyproject.toml"), "[project]").unwrap();
fs::write(dir.path().join("componentize-py.config"), "").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Wasm(WasmTargetHint::Component)));
}
#[test]
fn test_detect_wasm_jco_in_package_json() {
let dir = create_temp_dir();
let pkg = r#"{
"name": "foo",
"devDependencies": { "@bytecodealliance/jco": "^1.0.0" }
}"#;
fs::write(dir.path().join("package.json"), pkg).unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Wasm(WasmTargetHint::Component)));
}
#[test]
fn test_detect_wasm_cargo_config_wasip1_module() {
let dir = create_temp_dir();
fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"foo\"").unwrap();
fs::create_dir_all(dir.path().join(".cargo")).unwrap();
fs::write(
dir.path().join(".cargo").join("config.toml"),
"[build]\ntarget = \"wasm32-wasip1\"\n",
)
.unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Wasm(WasmTargetHint::Module)));
}
#[test]
fn test_plain_rust_project_is_not_wasm() {
let dir = create_temp_dir();
fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"foo\"").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Rust));
}
#[test]
fn test_plain_node_project_is_not_wasm() {
let dir = create_temp_dir();
fs::write(
dir.path().join("package.json"),
r#"{"dependencies":{"express":"^4"}}"#,
)
.unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Node20));
}
}