use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
struct TempDirectory(PathBuf);
impl TempDirectory {
fn new() -> Self {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"noxid-runtime-pruner-{}-{nonce}-{counter}",
std::process::id()
));
fs::create_dir_all(&path).unwrap();
Self(path)
}
}
impl Drop for TempDirectory {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn walk_javascript(directory: &Path, visit: &mut impl FnMut(&Path)) {
let mut entries = fs::read_dir(directory)
.unwrap_or_else(|error| panic!("cannot read {}: {error}", directory.display()))
.collect::<Result<Vec<_>, _>>()
.unwrap();
entries.sort_by_key(|entry| entry.file_name());
for entry in entries {
let path = entry.path();
let file_type = entry.file_type().unwrap();
if file_type.is_dir() {
walk_javascript(&path, visit);
} else if file_type.is_file()
&& path
.extension()
.is_some_and(|value| matches!(value.to_str(), Some("js" | "mjs" | "ts")))
{
visit(&path);
}
}
}
fn named_runtime_imports(source: &str) -> BTreeSet<String> {
let mut imports = BTreeSet::new();
let mut remaining = source;
while let Some(start) = remaining.find("import {") {
remaining = &remaining[start + "import {".len()..];
let Some(close) = remaining.find('}') else {
break;
};
let bindings = &remaining[..close];
let tail = &remaining[close + 1..];
let Some(statement_end) = tail.find(';') else {
break;
};
let statement = &tail[..statement_end];
if statement.contains("noxid-runtime.js") {
for binding in bindings
.split(',')
.map(str::trim)
.filter(|name| !name.is_empty())
{
let imported = binding.split_ascii_whitespace().next().unwrap();
imports.insert(imported.to_string());
}
}
remaining = &tail[statement_end + 1..];
}
imports
}
fn runtime_exports(source: &str) -> BTreeSet<String> {
source
.lines()
.filter_map(|line| {
let declaration = line.trim_start().strip_prefix("export ")?;
let declaration = declaration.strip_prefix("async ").unwrap_or(declaration);
let name = ["function ", "class ", "const ", "let "]
.into_iter()
.find_map(|prefix| declaration.strip_prefix(prefix))?
.split(|character: char| !character.is_ascii_alphanumeric() && character != '_')
.next()?;
(!name.is_empty()).then(|| name.to_string())
})
.collect()
}
fn bundled_runtime_exports(source: &str, known: &BTreeSet<String>) -> BTreeSet<String> {
known
.iter()
.filter(|name| {
source.contains(&format!(",\"{name}\",()=>"))
|| source.contains(&format!(",'{name}',()=>"))
})
.cloned()
.collect()
}
fn assert_runtime_imports_survive(label: &str, output: &Path, require_imports: bool) {
let known = runtime_exports(noxid_runtime::RUNTIME_JS);
let mut requested_runtime = BTreeSet::new();
let mut runtime_modules = Vec::new();
walk_javascript(output, &mut |path| {
if path
.file_name()
.is_some_and(|value| value == "noxid-runtime.js")
{
return;
}
let source = fs::read_to_string(path).unwrap();
let module_imports = named_runtime_imports(&source);
let bundled_exports = bundled_runtime_exports(&source, &known);
if !module_imports.is_empty() || !bundled_exports.is_empty() {
runtime_modules.push(path.strip_prefix(output).unwrap().display().to_string());
requested_runtime.extend(module_imports);
requested_runtime.extend(bundled_exports);
}
});
if require_imports {
assert!(
!requested_runtime.is_empty(),
"{label} emitted neither named noxid-runtime.js imports nor Farm-bundled runtime exports"
);
}
let requested = requested_runtime
.iter()
.map(String::as_str)
.collect::<Vec<_>>();
let pruned = noxid_runtime::javascript_for(requested.iter().copied());
let exports = runtime_exports(&pruned);
for imported in &requested_runtime {
assert!(
exports.contains(imported),
"{label} retains runtime export `{imported}` in [{}], but javascript_for({requested:?}) drops its export",
runtime_modules.join(", ")
);
}
}
fn run_noxid(project: &Path, arguments: &[&str], out_dir: &Path) {
let (command, options) = arguments.split_first().expect("noxid command");
let output = Command::new(env!("CARGO_BIN_EXE_noxid"))
.arg(command)
.arg(project)
.args(options)
.arg("--out-dir")
.arg(out_dir)
.env_remove("DATABASE_URL")
.env_remove("VERCEL")
.env_remove("NETLIFY")
.env_remove("RAILWAY_ENVIRONMENT")
.output()
.unwrap();
assert!(
output.status.success(),
"example project {} failed to run `noxid {}`:\nstdout:\n{}\nstderr:\n{}",
project.display(),
arguments.join(" "),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn feature_markers_are_flat_and_balanced() {
let mut open: Option<&str> = None;
for (index, line) in noxid_runtime::RUNTIME_JS.lines().enumerate() {
let line_number = index + 1;
if let Some(feature) = line.trim().strip_prefix("// noxid-runtime:feature-start:") {
assert!(
open.is_none(),
"runtime feature `{feature}` starts at line {line_number} inside feature `{}`",
open.unwrap()
);
open = Some(feature);
} else if let Some(feature) = line.trim().strip_prefix("// noxid-runtime:feature-end:") {
assert_eq!(
open.take(),
Some(feature),
"runtime feature `{feature}` ends at line {line_number} without its matching flat start"
);
}
}
assert_eq!(open, None, "runtime feature `{}` never ends", open.unwrap());
}
#[test]
fn every_example_project_build_and_adapter_runtime_import_survives_pruning() {
let repository = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let examples = repository.join("examples");
let temp = TempDirectory::new();
let mut projects = fs::read_dir(&examples)
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap()
.into_iter()
.map(|entry| entry.path())
.filter(|path| path.is_dir() && path.join("Noxid.toml").is_file())
.collect::<Vec<_>>();
projects.sort();
assert!(
!projects.is_empty(),
"expected compile-tested example projects"
);
for project in &projects {
let name = project.file_name().unwrap().to_string_lossy();
let built = temp.0.join("build").join(name.as_ref());
run_noxid(project, &["build"], &built);
let ships_client_runtime = name != "uploads";
assert_runtime_imports_survive(
&format!("example project {name} build"),
&built,
ships_client_runtime,
);
let adapted = temp.0.join("adapt-node").join(name.as_ref());
run_noxid(project, &["adapt", "--adapter", "node"], &adapted);
assert_runtime_imports_survive(
&format!("example project {name} node adaptation"),
&adapted,
ships_client_runtime && name != "attachments",
);
if name == "attachments" {
let mut attachment_runtime = false;
walk_javascript(&adapted, &mut |path| {
attachment_runtime |= fs::read_to_string(path)
.unwrap()
.contains("@formkit/auto-animate returned an invalid attachment controller");
});
assert!(
attachment_runtime,
"attachments node adaptation dropped its Farm-minified attachment runtime"
);
}
}
let portable = examples.join("stream-ssr");
for adapter in ["node", "deno", "cloudflare", "vercel", "netlify"] {
let adapted = temp.0.join("adapt-matrix").join(adapter);
run_noxid(&portable, &["adapt", "--adapter", adapter], &adapted);
assert!(
adapted.join("stream-ssr/server/handler.js").is_file(),
"stream-ssr {adapter} adaptation omitted its server handler"
);
assert_runtime_imports_survive(&format!("stream-ssr {adapter} adaptation"), &adapted, true);
}
}