tower-serve-embedded 0.2.0

Embed content-hashed, cache-busted static web assets into your binary and serve them as a tower::Service, with a compile-time asset! macro for SSR templates.
Documentation
# tower-serve-embedded

Embed content-hashed, cache-busted static web assets (CSS, JS, images) into your Rust binary —
and serve them from any [`tower`](https://crates.io/crates/tower)/[`axum`](https://crates.io/crates/axum)
stack.

It's in the same family as [`rust-embed`](https://crates.io/crates/rust-embed), but tuned for
*serving* web assets:

- **Content hashing + cache busting.** Every ordinary file is exposed at a hashed URL that mirrors
  its location in your project — `assets/css/style.css``/assets/css/style.9f3a1c2b.css` — so it
  can be served `immutable` with a one-year cache and still update the instant its content changes.
  Already-versioned assets can be marked with `immutable_dir` and served at their plain,
  non-hashed URL.
- **A compile-time `asset!` macro.** `asset!("assets/css/style.css")` expands to the generated
  served URL as a `&'static str` for SSR templates (e.g. [`maud`]https://maud.lambda.xyz). Typos
  are compile errors.
- **A `tower::Service`** that serves the embedded assets with `Content-Type`, `ETag`/`304`, and
  `Cache-Control`. Drop it into axum or any other `tower`-based stack.

Design goals: **simple** (point it at a directory, done), **hot-reloadable** in development (via
`build.rs` + `cargo:rerun-if-changed`, so `cargo watch` rebuilds on change), and **no proc
macros** — all codegen happens in a build script, so IDE support stays sharp.

There's no URL-prefix to configure: an asset's path under your crate root *is* its URL.

## Crates

| Crate                        | Where it goes          | Purpose                                            |
| ---------------------------- | ---------------------- | -------------------------------------------------- |
| `tower-serve-embedded`       | `[dependencies]`       | Runtime types, the `asset!` macro, the tower service. |
| `tower-serve-embedded-build` | `[build-dependencies]` | Walks the asset dir, hashes files, generates code. |

## Quickstart

```toml
# Cargo.toml
[dependencies]
tower-serve-embedded = "0.1"

[build-dependencies]
tower-serve-embedded-build = "0.1"
```

```rust
// build.rs
fn main() {
    tower_serve_embedded_build::Builder::new("assets") // dir relative to your crate root
        // .immutable_dir("lib")                        // already-versioned deps: serve as-is, don't re-hash
        .emit()
        .unwrap();
}
```

```rust
// src/main.rs (or any module)
tower_serve_embedded::embed!(); // brings `ASSETS` and the `asset!` macro into scope
```

Call `embed!()` at the crate root if you want to use the macro from other modules as
`crate::asset!(...)`.

### Reference assets in templates

Refer to assets by their path **relative to the crate root**. `asset!` resolves at compile time
to the full served URL: cache-busted for ordinary assets, plain for `immutable_dir` assets.

```rust
let css = asset!("assets/css/style.css"); // "/assets/css/style.9f3a1c2b.css"
```

It drops straight into any engine that interpolates `&str`, e.g. `maud`:

```rust
html! {
    link rel="stylesheet" href=(asset!("assets/css/style.css"));
    script src=(asset!("assets/js/app.js")) defer {}
}
```

Need a name that isn't known at compile time? Use the runtime equivalent:

```rust
let url: Option<&'static str> = ASSETS.url("assets/css/style.css");
```

### Serve them

Each generated asset URL is already the full path, so mount the service as a **fallback** — no
nesting or prefix to keep in sync:

```rust
use axum::{Router, routing::get};

let app = Router::new()
    .route("/", get(index))
    .fallback_service(ASSETS.service());
```

The service handles `GET`/`HEAD`, returns `304 Not Modified` for matching `If-None-Match`, and
sets `Content-Type`, a strong `ETag`, and
`Cache-Control: public, max-age=31536000, immutable` for generated routes. Ordinary assets are
served only at their cache-busted URL; assets under
[`immutable_dir`](#immutable-already-versioned-assets) are served only at their plain, non-hashed
URL. It's a plain `tower::Service`, so wrap it in any `tower` layer (compression, tracing, …) you
like.

> Prefer not to use the fallback slot? Scope it to the asset directory instead — the service
> still matches the full path, so route it without stripping the prefix:
> `Router::route_service("/assets/{*path}", ASSETS.service())`.

### Immutable, already-versioned assets

Some assets are *already* immutable because their name encodes a version — vendored libraries like
`assets/lib/htmx-1.9.10.min.js`, or anything you pull from a CDN. Re-hashing those just produces an
uglier URL. Mark their directory with `immutable_dir`:

```rust
// build.rs
tower_serve_embedded_build::Builder::new("assets")
    .immutable_dir("lib") // relative to the embedded dir
    .emit()
    .unwrap();
```

Files under it are still embedded and served, but only at their original URL
(`/assets/lib/htmx-1.9.10.min.js`) — no extra hash — with the one-year `immutable` cache. `asset!`
and `ASSETS.url(..)` return that plain path:

```rust
let htmx = asset!("assets/lib/htmx-1.9.10.min.js"); // "/assets/lib/htmx-1.9.10.min.js"
```

## Hot reloading

The build script emits `cargo:rerun-if-changed` for the asset directory and every file, so:

```sh
cargo watch -x run
```

Editing an ordinary asset re-runs the build script, recomputes the hash, and rebuilds — the new
cache-busted URL flows through `asset!` automatically. Dev and release behave identically; there's
no separate "debug" embedding mode to reason about.

Two mechanisms cooperate here, and it's worth knowing they're distinct:

- `cargo watch` is a filesystem watcher; its job is to **invoke cargo** when a file changes. With
  no `-w` flags it watches the whole crate (both `src` and `assets`, ignoring `target`), so you
  don't need to enumerate paths.
- The build script's `cargo:rerun-if-changed` lines are a **cargo** signal that decides whether to
  re-run the build script *once cargo is invoked*. They don't watch the filesystem, so they don't
  replace `cargo watch` — they're what makes the rebuild actually pick up the new bytes and hash.

## Notes

- Hashing is **BLAKE3**, truncated to 16 hex chars (64 bits) for filenames and ETags.
- Hidden files/directories (names starting with `.`) and symlinks are skipped.
- One asset set per module: `embed!()` defines `ASSETS` and a crate-local `asset!` macro at the
  invocation site. Put it at the crate root if you want `crate::asset!(...)` everywhere in your
  crate.
- Ordinary assets are served only at their cache-busted URL (`immutable`); `immutable_dir` assets
  are served only at their plain URL (`immutable`). A stable, always-fresh HTML entry point (SPA
  `index.html`) is intentionally out of scope — keep that route in your app.

## Examples

Runnable, self-contained setups live in [`examples/`](examples), each with its own `build.rs`,
`assets/`, and `main.rs`:

| Example                   | Run                          | How it serves assets                                          |
| ------------------------- | ---------------------------- | ------------------------------------------------------------ |
| [`axum`]examples/axum   | `cargo run -p axum-example`  | Mounts `ASSETS.service()` (a `tower::Service`) as a fallback. |
| [`actix`]examples/actix | `cargo run -p actix-example` | Uses the framework-agnostic data API (`ASSETS.resolve`).      |
| [`warp`]examples/warp   | `cargo run -p warp-example`  | Uses the framework-agnostic data API (`ASSETS.resolve`).      |

axum is `tower`-based, so it mounts the service directly. actix-web and warp aren't, so they look
assets up with [`Assets::resolve`] and build the response by hand — the `asset!` macro and the
`build.rs` embedding step are identical across all three.

[`Assets::resolve`]: https://docs.rs/tower-serve-embedded/latest/tower_serve_embedded/struct.Assets.html#method.resolve

## License

MIT OR Apache-2.0.