mini-static 0.18.0

A secure, async static file server with streaming, traversal protection, and connection limits.
Documentation
//! End-to-end tests against the real `lightningcss`/`esbuild` binaries.
//!
//! Not part of the default `cargo test` run — every test here is `#[ignore]`d because
//! it depends on external tools this crate does not install or manage. Run with:
//!
//! ```sh
//! npm install -g lightningcss-cli esbuild
//! cargo test -p mini-static --test real_tools -- --ignored
//! ```

use std::fs;
use std::time::Duration;

use mini_static::{CssOptions, CssTool, JsOptions, JsTool, Server};
use tempfile::TempDir;

#[tokio::test]
#[ignore = "requires lightningcss on PATH"]
async fn css_bundle_and_minify_produces_minified_concatenated_output() {
    let src = TempDir::new().unwrap();
    let out = TempDir::new().unwrap();
    fs::write(
        src.path().join("reset.css"),
        "* {\n  margin: 0;\n  padding: 0;\n}\n",
    )
    .unwrap();
    fs::write(
        src.path().join("theme.css"),
        "body {\n  background: white;\n}\n",
    )
    .unwrap();

    let server = Server::new(out.path())
        .unwrap()
        .with_source_folder(src.path())
        .unwrap()
        .with_css_tool(
            CssTool::LightningCss,
            CssOptions::new().bundle(true).minify(true),
        );
    let (_port, handle) = server.run_ephemeral().await.unwrap();

    tokio::time::sleep(Duration::from_millis(300)).await;

    let content = fs::read_to_string(out.path().join("styles.css")).unwrap();
    assert!(content.contains("margin"), "expected reset CSS in bundle");
    assert!(
        content.contains("background"),
        "expected theme CSS in bundle"
    );
    assert!(
        !content.contains('\n'),
        "minified output should not contain source formatting newlines, got: {content}"
    );

    handle.shutdown().await;
}

#[tokio::test]
#[ignore = "requires lightningcss on PATH"]
async fn prune_output_removes_stale_css_bundle_when_no_css_sources_remain() {
    let src = TempDir::new().unwrap();
    let out = TempDir::new().unwrap();
    fs::write(out.path().join("styles.css"), "/* stale */").unwrap();

    let server = Server::new(out.path())
        .unwrap()
        .with_source_folder(src.path())
        .unwrap()
        .with_css_tool(CssTool::LightningCss, CssOptions::new().bundle(true))
        .with_prune_output();
    let (_port, handle) = server.run_ephemeral().await.unwrap();

    tokio::time::sleep(Duration::from_millis(200)).await;

    assert!(
        !out.path().join("styles.css").exists(),
        "prune is opt-in: with_prune_output should delete the stale bundle produced by no css sources"
    );

    handle.shutdown().await;
}

#[tokio::test]
#[ignore = "requires esbuild on PATH"]
async fn js_bundle_and_minify_resolves_the_entrys_module_graph() {
    let src = TempDir::new().unwrap();
    let out = TempDir::new().unwrap();
    fs::write(
        src.path().join("util.js"),
        "export function greet(name) { return 'hello ' + name; }\n",
    )
    .unwrap();
    let entry = src.path().join("main.js");
    fs::write(
        &entry,
        "import { greet } from './util.js';\nconsole.log(greet('world'));\n",
    )
    .unwrap();

    let server = Server::new(out.path())
        .unwrap()
        .with_source_folder(src.path())
        .unwrap()
        .with_js_tool(
            JsTool::Esbuild,
            JsOptions::new()
                .bundle_entry(&entry, "bundle.js")
                .minify(true),
        )
        .unwrap();
    let (_port, handle) = server.run_ephemeral().await.unwrap();

    tokio::time::sleep(Duration::from_millis(300)).await;

    let content = fs::read_to_string(out.path().join("bundle.js")).unwrap();
    assert!(
        content.contains("hello"),
        "bundle should inline the imported module's code, got: {content}"
    );
    assert!(
        !content.contains("import "),
        "a bundled entry should have no remaining import statements, got: {content}"
    );

    handle.shutdown().await;
}

#[tokio::test]
#[ignore = "requires esbuild on PATH"]
async fn malformed_js_source_degrades_to_raw_output() {
    let src = TempDir::new().unwrap();
    let out = TempDir::new().unwrap();
    let malformed = b"function( {{{ !!!";
    fs::write(src.path().join("broken.js"), malformed).unwrap();

    let server = Server::new(out.path())
        .unwrap()
        .with_source_folder(src.path())
        .unwrap()
        .with_js_tool(JsTool::Esbuild, JsOptions::new().minify(true))
        .unwrap();
    let (_port, handle) = server.run_ephemeral().await.unwrap();

    tokio::time::sleep(Duration::from_millis(300)).await;

    let out_js = fs::read(out.path().join("broken.js")).unwrap();
    assert_eq!(
        out_js, malformed,
        "a file esbuild cannot parse must still be mirrored (raw) so the page stays in sync"
    );

    handle.shutdown().await;
}