1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
use mini_static::{CssOptions, CssTool, JsOptions, JsTool, Server};
use std::env;
use std::path::Path;
use std::time::Duration;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let port = env::var("PORT")
.unwrap_or_else(|_| "8080".to_string())
.parse::<u16>()?;
let root = env::var("ROOT").unwrap_or_else(|_| "./public".to_string());
let root_path = Path::new(&root);
// `with_immutable_assets` opts fingerprinted filenames (`vendor.a1b2c3.js`) into a
// year-long, cacheable-forever `Cache-Control`, since a content change would produce
// a new filename rather than mutating this one. Everything else keeps the default
// `no-cache` (see `Server::with_immutable_assets`).
//
// `with_spa_root("#app")` demos spa-mode navigation (see `public/spa-demo/`) —
// unlike the CSS/JS bundling below, this has no external-tool dependency to gate
// on, so it's simply always on here.
let server = Server::new(root_path)?
.with_immutable_assets(|path| {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.contains(".a1b2c3."))
})
.with_spa_root("#app");
// CSS/JS bundling demo (see `Server::with_css_tool`/`Server::with_js_tool`): both
// are opt-in and delegate to an external CLI tool mini-static does not install —
// `lightningcss` and `esbuild` must already be on `PATH`. Bundles every `.css`
// under `css-src/` (with `@import` resolution) into `public/style-bundle.css`, and
// bundles `js-src/main.js`'s module graph into `public/bundle.js`.
let css_src = Path::new("./css-src");
let js_src = Path::new("./js-src");
let mut server = server;
let has_css_bundle = if css_src.exists() {
match server.clone().with_source_folder(css_src) {
Ok(s) => {
server = s.with_css_tool(
CssTool::LightningCss,
CssOptions::new()
.bundle(true)
.minify(true)
.bundle_output_name("style-bundle.css"),
);
true
}
Err(e) => {
eprintln!("warning: CSS bundling configuration failed: {}", e);
false
}
}
} else {
false
};
let has_js_bundle = if js_src.join("main.js").exists() {
match server.clone().with_source_folder(js_src) {
Ok(s) => match s.with_js_tool(
JsTool::Esbuild,
JsOptions::new()
.bundle_entry(&js_src.join("main.js"), "bundle.js")
.minify(true),
) {
Ok(s) => {
server = s;
true
}
Err(e) => {
eprintln!("warning: JS bundling configuration failed: {}", e);
false
}
},
Err(e) => {
eprintln!("warning: JS bundling configuration failed: {}", e);
false
}
}
} else {
false
};
// Live-reload (background file watcher, SSE stream, injected reload script — see
// `Server::with_live_reload`) is only enabled in debug builds, matching the
// convention `mini-unified`'s `add_reload_route` uses: a release build never pays
// for the watcher or ships the injected script.
#[cfg(debug_assertions)]
{
server = server.with_live_reload();
}
// Bind to all interfaces (0.0.0.0) so the server is accessible from outside
// (e.g., from the host when running in Docker)
let (_port, handle) = server.run_all(port, Duration::from_secs(30)).await?;
println!("mini-static listening on 0.0.0.0:{}", port);
println!("serving files from: {}", root);
println!("immutable caching enabled for *.a1b2c3.* (see vendor.a1b2c3.js)");
println!("precompressed sidecar demo: bundle.js / bundle.js.gz");
println!("spa-mode demo (root #app): http://127.0.0.1:{port}/spa-demo/");
if has_css_bundle {
println!(
"css bundling enabled (lightningcss): {} -> {}",
css_src.display(),
root_path.join("style-bundle.css").display()
);
}
if has_js_bundle {
println!(
"js bundling enabled (esbuild): {} -> {}",
js_src.join("main.js").display(),
root_path.join("bundle.js").display()
);
}
#[cfg(debug_assertions)]
println!("live-reload enabled at {}", mini_static::LIVE_RELOAD_PATH);
// Keep the server running until interrupted
tokio::signal::ctrl_c().await?;
println!("shutting down...");
handle.shutdown().await;
Ok(())
}