mini-build
Builds the directory a static server serves: CSS bundling and minification, JS bundling and minification, and byte-identical asset mirroring — all delegated to external CLI tools. No HTTP, no serving, no runtime.
Status: pre-release, extracted from
mini-static0.28.x.
[]
= "0.1"
Scope
Produce the directory a static server serves. That sentence decides what belongs here. An asset folder mirrored byte-for-byte is in scope even though nothing transforms it, because the output directory is not complete without it. Serving that directory, watching it for a browser's benefit, and choosing its cache headers are all somebody else's job.
This crate has no runtime dependencies — cargo tree shows the crate alone. It is
synchronous: a build spawns subprocesses and waits on them, which the standard library
already expresses, and an async runtime would be weight with nothing to buy.
Why it is separate
This pipeline lived inside mini-static until 0.29.0. Splitting it apart gained three
things beyond a smaller server:
- The server can be read-only. Previously one process both wrote into and served from the same directory — the output dir defaulted to the served root.
- Live-reload got simpler. The output dir was deliberately never watched, because watching it would feed each pipeline its own writes back into its trigger. With the builder in a separate process that loop is impossible, so the server can just watch what it serves.
- Build performance became measurable. A static server's benchmarks are microsecond
request latencies; a build is milliseconds over a file tree. Neither is visible in the
other's suite. See
benches/build.rs.
Usage
use ;
use Path;
Every path is canonicalized and checked as it is registered, so a misconfiguration is reported while the builder is being assembled rather than partway through a build that has already written files. Overlap is rejected in both directions: a source folder inside the output dir would have the build read its own writes, and an output dir inside a source folder would have it write into its own inputs.
Tool availability is checked before any file is written — a build that will fail for want
of esbuild fails before it half-populates the output dir. A tool configured for neither
bundling nor minifying spawns no process, so its binary is not required.
Watch mode
use Builder;
use Path;
watch builds once and then keeps the output dir in sync until the handle drops. The
initial build's failures come back through the Result; later rebuild failures go to the
callback, because by then there is no call left to return from. The callback is required
rather than optional: a rebuild that fails silently leaves the output dir holding stale
bytes while you believe it is current.
Dropping the handle stops the watcher and joins its thread. A watcher that outlives its handle is a leak that shows up as builds running after you thought they had stopped.
External tools
This crate does not install or manage the tools it invokes:
bundle and minify are independent toggles per language, and all four combinations are
valid: passthrough copy, per-file minify, bundle only, or bundle and minify. *.min.css
and *.min.js bypass minification — running a minifier over already-minified input is
wasted work at best and a correctness risk at worst.
Per-file mode degrades to a raw copy when a tool rejects a file, rather than failing the build: one malformed source should not take a whole site offline. Bundle failures do propagate, since a partial bundle is worse than an unchanged one. Every invocation is bounded by a 30-second timeout, after which the child is killed rather than abandoned.
Performance
benches/build.rs measures whole-tree wall clock, and process startup is what a build
spends its time on — about 19 ms per spawn, of which only ~5 ms is the tool itself and the
rest is std::process::Command on this platform.
Inputs are therefore batched one invocation per source directory rather than one per file, in bundle mode and per-file mode alike. Fifty scattered files dropped from 1155 ms to 195 ms bundling, and from 1128 ms to 192 ms minifying per file — and, more to the point, stopped scaling with file count at all: ten files and fifty now cost the same, because both sit in the same eight directories. The same fifty files in a single directory cost 48 ms, which is the shape a hand-authored site actually has.
Per-file mode still promises that a malformed source degrades to a raw copy while its neighbours are minified normally. A failed batch therefore rebuilds its group one file at a time rather than giving up on the directory — the slow path costs a spawn per file, but only in the case that was already going wrong.
Batching per directory rather than all at once is a correctness requirement, not caution:
lightningcss --output-dir distinguishes its results by basename, so one/shared.css and
two/shared.css batched together would silently leave one of them out of the build.
Still on the table, each to be argued against the recorded baseline: staging inputs under
unique names so a whole tree is one invocation rather than one per directory, a persistent
esbuild process, and skipping unchanged files.