Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

crates/nornir-viz-web/src/main.rs

//! **nornir-viz-web** entry points.
//!
//! - **wasm** (`wasm32-unknown-unknown`): `wasm_start` mounts [`NornirVizApp`] on
//!   the `<canvas id="nornir_viz">` in `index.html` via eframe's glow/WebGL
//!   `WebRunner`. `window.__nornir_viz_state()` returns the live `state_json`.
//! - **native** (dev + the serve path):
//!   - `nornir-viz-web` (no args) — open the viz in a native eframe window.
//!   - `nornir-viz-web serve --dir <dist> [--addr 127.0.0.1:8080]` — "nornir viz
//!     --> server nornir": serve the built wasm bundle as a static website over
//!     axum (feature `serve`). This is the nornir server that hosts the viz site.
//!
//! Build the wasm bundle with `./build-wasm.sh` (wasm-bindgen), then serve `dist/`.

// ─────────────────────────── native ───────────────────────────
/// The pure-Rust git-hash build identity — `v<pkg> (<git-sha>)`, mirroring
/// `nornir::VERSION_LINE`. `NORNIR_VIZ_WEB_GIT_HASH` is stamped by this crate's
/// own `build.rs` (this crate is not a member of the main `nornir` crate, so it
/// cannot read `nornir::VERSION_LINE`).
#[cfg(not(target_arch = "wasm32"))]
const VERSION_LINE: &str = concat!(
    "v",
    env!("CARGO_PKG_VERSION"),
    " (",
    env!("NORNIR_VIZ_WEB_GIT_HASH"),
    ")"
);

#[cfg(not(target_arch = "wasm32"))]
fn main() {
    let args: Vec<String> = std::env::args().collect();
    match args.get(1).map(String::as_str) {
        // Build-version LAW: `--version`/`-V` prints the git-hash identity and
        // exits (CLI parity with the rest of the suite; never a build without it).
        Some("--version") | Some("-V") => {
            println!("nornir-viz-web {VERSION_LINE}");
        }
        Some("serve") => {
            #[cfg(feature = "serve")]
            {
                serve::run(&args[2..]);
            }
            #[cfg(not(feature = "serve"))]
            {
                eprintln!(
                    "the `serve` subcommand needs the `serve` feature:\n  \
                     cargo run -p nornir-viz-web --features serve -- serve --dir dist"
                );
                std::process::exit(2);
            }
        }
        _ => native_viz(),
    }
}

/// Open the viz in a native eframe (wgpu) window — the dev counterpart of the
/// browser bundle (same [`NornirVizApp`], so what you see natively is what ships).
#[cfg(not(target_arch = "wasm32"))]
fn native_viz() {
    let native_options = eframe::NativeOptions {
        // LAW #4 — WebGPU, never OpenGL. Explicit so the native preview and the wasm
        // bundle are provably the same backend family: "what you see natively is what
        // ships" is only true if both name their renderer.
        renderer: eframe::Renderer::Wgpu,
        viewport: eframe::egui::ViewportBuilder::default()
            .with_inner_size([1200.0, 780.0])
            .with_title("nornir · viz (web preview)"),
        ..Default::default()
    };
    let _ = eframe::run_native(
        "nornir-viz-web",
        native_options,
        Box::new(|_cc| Ok(Box::new(nornir_viz_web::NornirVizApp::new()))),
    );
}

/// The static-file server: "nornir viz --> server nornir" — axum + tower-http
/// `ServeDir` hosting the built wasm bundle (`index.html` + `.js` + `.wasm`).
#[cfg(all(not(target_arch = "wasm32"), feature = "serve"))]
mod serve {
    use std::net::SocketAddr;
    use std::path::PathBuf;

    use axum::Router;
    use tower_http::services::ServeDir;

    /// Parse `--dir <path>` (default `dist`) and `--addr <ip:port>` (default
    /// `127.0.0.1:8080`) and serve the directory as a website until Ctrl-C.
    pub fn run(args: &[String]) {
        let mut dir = PathBuf::from("dist");
        let mut addr: SocketAddr = "127.0.0.1:8080".parse().unwrap();
        let mut it = args.iter();
        while let Some(a) = it.next() {
            match a.as_str() {
                "--dir" => {
                    if let Some(v) = it.next() {
                        dir = PathBuf::from(v);
                    }
                }
                "--addr" => {
                    if let Some(v) = it.next() {
                        addr = v.parse().unwrap_or(addr);
                    }
                }
                _ => {}
            }
        }
        if !dir.join("index.html").exists() {
            eprintln!(
                "no `{}` — build the wasm bundle first:\n  ./crates/nornir-viz-web/build-wasm.sh",
                dir.join("index.html").display()
            );
            std::process::exit(2);
        }

        let rt = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .unwrap();
        rt.block_on(async move {
            // `ServeDir` serves the bundle; the fallback re-serves index.html so any
            // deep-link path lands on the SPA. Correct MIME for `.wasm` is set by
            // tower-http from the extension (application/wasm).
            let app = Router::new()
                .fallback_service(ServeDir::new(&dir).append_index_html_on_directories(true));
            let listener = tokio::net::TcpListener::bind(addr).await.expect("bind");
            eprintln!(
                "nornir viz website serving {} at http://{addr}/",
                dir.display()
            );
            axum::serve(listener, app).await.expect("serve");
        });
    }
}

// On wasm the bin must still link a `main`, but eframe drives the canvas from
// `wasm_start`, so `main` does nothing.
#[cfg(target_arch = "wasm32")]
fn main() {}

// ─────────────────────────── wasm ───────────────────────────
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen::prelude::wasm_bindgen(start)]
pub fn wasm_start() {
    use wasm_bindgen::JsCast as _;

    // Pretty panics in the browser console (opaque otherwise).
    console_error_panic_hook::set_once();

    // LAW #4 — WebGPU, never WebGL. `WebOptions` carries the same `renderer` field
    // `NativeOptions` does, and leaving it to `Renderer::default()` means the browser
    // backend is decided by whichever eframe feature the manifest happens to carry. On
    // wasm32 `glow` means WebGL2, so naming this is what makes the page's backend a
    // statement in the source rather than a side effect of feature resolution.
    let web_options = eframe::WebOptions {
        renderer: eframe::Renderer::Wgpu,
        ..Default::default()
    };
    wasm_bindgen_futures::spawn_local(async {
        let document = web_sys::window()
            .expect("no `window`")
            .document()
            .expect("no `document`");
        let canvas = document
            .get_element_by_id("nornir_viz")
            .expect("missing <canvas id=\"nornir_viz\">")
            .dyn_into::<web_sys::HtmlCanvasElement>()
            .expect("#nornir_viz is not a <canvas>");

        let start_result = eframe::WebRunner::new()
            .start(
                canvas,
                web_options,
                Box::new(|_cc| Ok(Box::new(nornir_viz_web::NornirVizApp::new()))),
            )
            .await;
        if let Err(e) = start_result {
            log::error!("eframe WebRunner failed to start: {e:?}");
        }
    });
}

/// `window.__nornir_viz_state()` — return the live app's whole `state_json` as a
/// string, so a headless browser drive / the console can read what actually
/// rendered in the shipped wasm bundle.
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen::prelude::wasm_bindgen(js_name = __nornir_viz_state)]
pub fn nornir_viz_state() -> String {
    nornir_viz_web::wasm_state::latest()
}