noxid-cli 0.2.0

The Noxid compiler command line: check, build, test, adapt, and the agent surface
//! `noxid adapt` must ship the project's declared static assets exactly as
//! `noxid build` does. The docs site's browser compiler (a wasm binary), its
//! logos, and its diagrams live under `[assets] directory`; a deployment that
//! dropped them served the page shell for every one of those URLs.

use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::time::{SystemTime, UNIX_EPOCH};

struct Fixture(PathBuf);

impl Fixture {
    fn new() -> Self {
        let nonce = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("clock after Unix epoch")
            .as_nanos();
        let root =
            std::env::temp_dir().join(format!("noxid-adapt-assets-{}-{nonce}", std::process::id()));
        fs::create_dir_all(&root).expect("create fixture root");
        Self(root)
    }

    fn noxid(&self, directory: &Path, args: &[&str]) -> Output {
        Command::new(env!("CARGO_BIN_EXE_noxid"))
            .args(args)
            .current_dir(directory)
            .env_remove("VERCEL")
            .env_remove("NETLIFY")
            .env_remove("RAILWAY_ENVIRONMENT")
            .output()
            .expect("run noxid")
    }
}

impl Drop for Fixture {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.0);
    }
}

fn assert_ok(output: &Output, what: &str) {
    assert!(
        output.status.success(),
        "{what} failed:\nstdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
}

#[test]
fn adapt_ships_declared_static_assets() {
    let fixture = Fixture::new();
    let scaffold = fixture.noxid(
        &fixture.0,
        &["new", "site", "--template", "counter", "--render", "client"],
    );
    assert_ok(&scaffold, "noxid new");
    let project = fixture.0.join("site");

    // Declare a static asset directory with one file in it, the way the
    // documentation site declares its wasm compiler and logos.
    let manifest = project.join("Noxid.toml");
    let mut toml = fs::read_to_string(&manifest).expect("read Noxid.toml");
    if !toml.contains("[assets]") {
        toml.push_str("\n[assets]\ndirectory = \"src/assets\"\n");
        fs::write(&manifest, toml).expect("write Noxid.toml");
    }
    let assets = project.join("src/assets");
    fs::create_dir_all(&assets).expect("create assets dir");
    let svg = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 8 8\"><circle r=\"4\" cx=\"4\" cy=\"4\"/></svg>";
    fs::write(assets.join("mark.svg"), svg).expect("write asset");

    let build = fixture.noxid(&project, &["build", ".", "--out-dir", "built"]);
    assert_ok(&build, "noxid build");
    assert_eq!(
        fs::read_to_string(project.join("built/assets/mark.svg")).expect("built asset"),
        svg,
        "noxid build copies the declared asset"
    );

    let adapt = fixture.noxid(
        &project,
        &["adapt", ".", "--out-dir", "deployed", "--adapter", "node"],
    );
    assert_ok(&adapt, "noxid adapt");
    assert_eq!(
        fs::read_to_string(project.join("deployed/assets/mark.svg")).expect("adapted asset"),
        svg,
        "noxid adapt ships the declared asset byte for byte"
    );
}

#[test]
fn adapt_refuses_an_asset_that_collides_with_generated_output() {
    let fixture = Fixture::new();
    let scaffold = fixture.noxid(
        &fixture.0,
        &["new", "site", "--template", "counter", "--render", "client"],
    );
    assert_ok(&scaffold, "noxid new");
    let project = fixture.0.join("site");
    let manifest = project.join("Noxid.toml");
    let mut toml = fs::read_to_string(&manifest).expect("read Noxid.toml");
    if !toml.contains("[assets]") {
        toml.push_str("\n[assets]\ndirectory = \"src/assets\"\n");
        fs::write(&manifest, toml).expect("write Noxid.toml");
    }
    let assets = project.join("src/assets");
    fs::create_dir_all(&assets).expect("create assets dir");
    // `assets/global.css` is compiler output in every deployment.
    fs::write(assets.join("global.css"), "body{}").expect("write colliding asset");

    let adapt = fixture.noxid(
        &project,
        &["adapt", ".", "--out-dir", "deployed", "--adapter", "node"],
    );
    assert!(!adapt.status.success(), "adapt must refuse the collision");
    let stderr = String::from_utf8_lossy(&adapt.stderr);
    assert!(
        stderr.contains("PROJECT_ASSET_COLLISION"),
        "expected PROJECT_ASSET_COLLISION, got:\n{stderr}"
    );
}