use std::fs;
use std::path::Path;
#[derive(Debug, PartialEq)]
pub struct NightlyStatus {
pub toolchain_nightly: bool,
pub leptos_nightly_feature: bool,
}
impl NightlyStatus {
pub fn is_ok(&self) -> bool {
self.toolchain_nightly && self.leptos_nightly_feature
}
pub fn missing_items(&self) -> Vec<&'static str> {
let mut items = Vec::new();
if !self.toolchain_nightly {
items.push("rust-toolchain.toml with channel = \"nightly\"");
}
if !self.leptos_nightly_feature {
items.push("leptos with features = [\"nightly\"] in Cargo.toml");
}
items
}
}
pub fn check_nightly_setup(dir: &Path) -> NightlyStatus {
NightlyStatus {
toolchain_nightly: has_nightly_toolchain(dir),
leptos_nightly_feature: has_leptos_nightly_feature(dir),
}
}
fn has_nightly_toolchain(dir: &Path) -> bool {
if let Ok(content) = fs::read_to_string(dir.join("rust-toolchain.toml")) {
return parse_toolchain_toml_channel(&content).as_deref().is_some_and(is_nightly_channel);
}
if let Ok(content) = fs::read_to_string(dir.join("rust-toolchain")) {
return content
.lines()
.find(|l| !l.trim().is_empty())
.is_some_and(|l| is_nightly_channel(l.trim()));
}
false
}
fn is_nightly_channel(channel: &str) -> bool {
channel == "nightly" || channel.starts_with("nightly-")
}
fn has_leptos_nightly_feature(dir: &Path) -> bool {
use cargo_toml::Manifest;
let path = dir.join("Cargo.toml");
let Ok(manifest) = Manifest::from_path(&path) else {
return false;
};
if leptos_features_contain_nightly(&manifest.dependencies) {
return true;
}
if let Some(ws) = &manifest.workspace {
if leptos_features_contain_nightly(&ws.dependencies) {
return true;
}
}
false
}
fn parse_toolchain_toml_channel(content: &str) -> Option<String> {
#[derive(serde::Deserialize)]
struct ToolchainFile {
toolchain: ToolchainSection,
}
#[derive(serde::Deserialize)]
struct ToolchainSection {
channel: Option<String>,
}
toml::from_str::<ToolchainFile>(content).ok().and_then(|f| f.toolchain.channel)
}
fn leptos_features_contain_nightly(deps: &cargo_toml::DepsSet) -> bool {
deps.get("leptos")
.map(|dep| dep.req_features().iter().any(|f| f == "nightly"))
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use std::fs;
use tempfile::TempDir;
use super::*;
fn write(dir: &TempDir, name: &str, content: &str) {
fs::write(dir.path().join(name), content).unwrap();
}
#[test]
fn toolchain_toml_nightly_is_detected() {
let dir = TempDir::new().unwrap();
write(&dir, "rust-toolchain.toml", "[toolchain]\nchannel = \"nightly\"\n");
assert!(has_nightly_toolchain(dir.path()));
}
#[test]
fn toolchain_toml_stable_returns_false() {
let dir = TempDir::new().unwrap();
write(&dir, "rust-toolchain.toml", "[toolchain]\nchannel = \"stable\"\n");
assert!(!has_nightly_toolchain(dir.path()));
}
#[test]
fn toolchain_toml_with_targets_nightly_is_detected() {
let dir = TempDir::new().unwrap();
write(
&dir,
"rust-toolchain.toml",
"[toolchain]\nchannel = \"nightly\"\ntargets = [\"wasm32-unknown-unknown\"]\n",
);
assert!(has_nightly_toolchain(dir.path()));
}
#[test]
fn legacy_toolchain_file_nightly_is_detected() {
let dir = TempDir::new().unwrap();
write(&dir, "rust-toolchain", "nightly");
assert!(has_nightly_toolchain(dir.path()));
}
#[test]
fn legacy_toolchain_file_stable_returns_false() {
let dir = TempDir::new().unwrap();
write(&dir, "rust-toolchain", "stable");
assert!(!has_nightly_toolchain(dir.path()));
}
#[test]
fn missing_toolchain_file_returns_false() {
let dir = TempDir::new().unwrap();
assert!(!has_nightly_toolchain(dir.path()));
}
#[test]
fn toml_toolchain_takes_priority_over_legacy() {
let dir = TempDir::new().unwrap();
write(&dir, "rust-toolchain.toml", "[toolchain]\nchannel = \"stable\"\n");
write(&dir, "rust-toolchain", "nightly");
assert!(!has_nightly_toolchain(dir.path()));
}
#[test]
fn leptos_nightly_feature_in_dependencies_is_detected() {
let dir = TempDir::new().unwrap();
write(
&dir,
"Cargo.toml",
"[package]\nname = \"test\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nleptos = { version = \"0.8\", features = [\"nightly\"] }\n",
);
assert!(has_leptos_nightly_feature(dir.path()));
}
#[test]
fn leptos_without_nightly_feature_returns_false() {
let dir = TempDir::new().unwrap();
write(
&dir,
"Cargo.toml",
"[package]\nname = \"test\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nleptos = { version = \"0.8\", features = [\"csr\"] }\n",
);
assert!(!has_leptos_nightly_feature(dir.path()));
}
#[test]
fn leptos_nightly_feature_in_workspace_dependencies_is_detected() {
let dir = TempDir::new().unwrap();
write(
&dir,
"Cargo.toml",
"[workspace]\nmembers = []\n\n[workspace.dependencies]\nleptos = { version = \"0.8\", features = [\"nightly\"] }\n",
);
assert!(has_leptos_nightly_feature(dir.path()));
}
#[test]
fn leptos_nightly_among_multiple_features_is_detected() {
let dir = TempDir::new().unwrap();
write(
&dir,
"Cargo.toml",
"[package]\nname = \"test\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nleptos = { version = \"0.8\", features = [\"csr\", \"nightly\", \"experimental-islands\"] }\n",
);
assert!(has_leptos_nightly_feature(dir.path()));
}
#[test]
fn missing_cargo_toml_returns_false() {
let dir = TempDir::new().unwrap();
assert!(!has_leptos_nightly_feature(dir.path()));
}
#[test]
fn cargo_toml_without_leptos_returns_false() {
let dir = TempDir::new().unwrap();
write(
&dir,
"Cargo.toml",
"[package]\nname = \"test\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nserde = \"1\"\n",
);
assert!(!has_leptos_nightly_feature(dir.path()));
}
#[test]
fn status_is_ok_when_both_are_set() {
let status = NightlyStatus { toolchain_nightly: true, leptos_nightly_feature: true };
assert!(status.is_ok());
assert!(status.missing_items().is_empty());
}
#[test]
fn status_not_ok_when_toolchain_missing() {
let status = NightlyStatus { toolchain_nightly: false, leptos_nightly_feature: true };
assert!(!status.is_ok());
assert_eq!(status.missing_items(), vec!["rust-toolchain.toml with channel = \"nightly\""]);
}
#[test]
fn status_not_ok_when_leptos_feature_missing() {
let status = NightlyStatus { toolchain_nightly: true, leptos_nightly_feature: false };
assert!(!status.is_ok());
assert_eq!(
status.missing_items(),
vec!["leptos with features = [\"nightly\"] in Cargo.toml"]
);
}
#[test]
fn status_lists_both_missing_items() {
let status = NightlyStatus { toolchain_nightly: false, leptos_nightly_feature: false };
assert!(!status.is_ok());
assert_eq!(status.missing_items().len(), 2);
}
#[test]
fn dated_nightly_toolchain_toml_is_detected() {
let dir = TempDir::new().unwrap();
write(&dir, "rust-toolchain.toml", "[toolchain]\nchannel = \"nightly-2024-01-01\"\n");
assert!(has_nightly_toolchain(dir.path()));
}
#[test]
fn dated_nightly_legacy_toolchain_is_detected() {
let dir = TempDir::new().unwrap();
write(&dir, "rust-toolchain", "nightly-2024-01-01");
assert!(has_nightly_toolchain(dir.path()));
}
#[test]
fn toolchain_toml_missing_channel_key_returns_false() {
let dir = TempDir::new().unwrap();
write(&dir, "rust-toolchain.toml", "[toolchain]\ntargets = [\"wasm32-unknown-unknown\"]\n");
assert!(!has_nightly_toolchain(dir.path()));
}
#[test]
fn malformed_toolchain_toml_returns_false() {
let dir = TempDir::new().unwrap();
write(&dir, "rust-toolchain.toml", "not valid toml ][[[");
assert!(!has_nightly_toolchain(dir.path()));
}
#[test]
fn leptos_simple_string_dep_returns_false() {
let dir = TempDir::new().unwrap();
write(
&dir,
"Cargo.toml",
"[package]\nname = \"test\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nleptos = \"0.8\"\n",
);
assert!(!has_leptos_nightly_feature(dir.path()));
}
#[test]
fn malformed_cargo_toml_returns_false() {
let dir = TempDir::new().unwrap();
write(&dir, "Cargo.toml", "not valid toml ][[[");
assert!(!has_leptos_nightly_feature(dir.path()));
}
#[test]
fn check_nightly_setup_full_project() {
let dir = TempDir::new().unwrap();
write(&dir, "rust-toolchain.toml", "[toolchain]\nchannel = \"nightly\"\n");
write(
&dir,
"Cargo.toml",
"[package]\nname = \"test\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nleptos = { version = \"0.8\", features = [\"nightly\"] }\n",
);
let status = check_nightly_setup(dir.path());
assert!(status.is_ok());
}
}