How to Use the asset! Macro for Static File Bundling and Caching in Topcoat
The asset! macro embeds a compile-time binary descriptor into your executable that the Topcoat bundler extracts to create content-hashed, cache-friendly static files served under /_topcoat/assets.
The asset! macro in the tokio-rs/topcoat repository transforms static files and remote URLs into type-safe, zero-cost handles that are bundled and cached at build time. Unlike traditional file serving that relies on runtime filesystem access, Topcoat's approach embeds asset metadata directly into your binary through the RawAsset encoding system, enabling deterministic content hashing and immutable CDN deployments.
Understanding the asset! Macro Architecture
The asset! macro expands into three components at compile time: a constant Asset handle, an EncodedAsset byte array, and a pre-computed AssetId. This architecture ensures assets cannot be dead-code eliminated while keeping the runtime handle lightweight.
Asset and AssetId
In crates/topcoat-asset/src/asset.rs (lines 17-27), the Asset struct defines a thin handle containing the embedded byte slice and providing an id() method to retrieve the stable AssetId. The AssetId (lines 78-90) is a deterministic 64-bit hash derived from the crate name, source file path, asset path, and options, guaranteeing stability across builds when declarations remain unchanged.
RawAsset Encoding
The RawAsset::encode and RawAsset::decode functions (lines 31-41 and 44-55) manage a fixed-size 2048-byte binary payload containing the ID, path, crate context, and options. The bundler locates these declarations by scanning the binary for a unique scrambled prefix, allowing it to extract every asset definition without parsing source code.
The Bundler
The Bundler implementation in crates/topcoat-asset/src/bundler.rs walks the compiled binary, finds every encoded asset descriptor, resolves source paths (local or remote), copies or downloads files into the bundle directory, and produces a manifest.toml mapping AssetId to bundled filenames. This process runs via the topcoat asset bundle command.
Declaring Assets with the asset! Macro
Local File Paths
Local paths resolve relative to the declaration context:
./or../→ relative to the source file containing theasset!call- Other relative paths → anchored to
CARGO_MANIFEST_DIR - Absolute paths → used verbatim
use topcoat::{asset::{Asset, asset}, view::view};
// Relative to CARGO_MANIFEST_DIR
const LOGO: Asset = asset!("assets/logo.png");
// Relative to this source file (src/ui/header.rs)
const HEADER_BG: Asset = asset!("./images/header.jpg");
Remote URLs
Remote assets specified with http:// or https:// are downloaded once during bundling and cached in <target>/topcoat/cache/assets for subsequent builds. The bundler treats cached downloads as local files after initial retrieval.
const DATATABLE_JS: Asset = asset!(
"https://cdn.jsdelivr.net/npm/datatable@1.10.24/datatable.min.js"
);
Asset Options
The macro accepts optional named arguments compiled into the binary descriptor:
rename: "name"→ Replaces the file stem in the bundled filenameextension: "ext"→ Forces a specific extension for files without onechecksum: "sha256:<hex>"→ Verifies source integrity; fails the bundle if mismatchedcontent_type: "mime/type"→ Overrides automatic MIME type detection
const VERIFIED_JS: Asset = asset!(
"https://cdn.example.com/lib.js",
rename: "library",
checksum: "sha256:9f5b2c3d4e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b",
content_type: "application/javascript"
);
The Bundling Workflow
The end-to-end asset pipeline follows four stages:
- Declaration → Developers call
asset!anywhere in the codebase, creating embedded descriptors incrates/topcoat-asset/src/asset.rs(lines 63-66) - Compilation → The Rust compiler embeds the
RawAssetbinary descriptors into the final executable - Bundling → Running
topcoat asset bundlescans the binary, resolves all paths, downloads remote files, computes content hashes, and writes files to<target>/assetswith hashed names likelogo-1a2b3c4d5e6f7a8b.png - Verification → The bundler generates
manifest.tomlmappingAssetIdvalues to content-hashed filenames, ensuring the bundle matches the exact binary that produced it
Serving Assets in Production
Local Serving
Attach the bundle to your router using RouterBuilderAssetExt::assets, which registers the route prefix /_topcoat/assets and makes each Asset render as its public URL.
use topcoat::{
asset::{AssetBundle, RouterBuilderAssetExt},
router::{Router, RouterBuilderDiscoverExt},
};
pub fn router() -> Router {
Router::builder()
.discover()
.assets(AssetBundle::load().unwrap())
.build()
}
If the bundle resides outside the executable directory, use AssetBundle::load_dir("dist/assets") to specify the path.
CDN Hosting
For external hosting, use AssetConfig::hosted_at to generate URLs pointing to your CDN while still loading the local bundle for ID-to-filename resolution.
use topcoat::{
asset::{AssetBundle, AssetConfig, RouterBuilderAssetExt},
router::Router,
};
let router = Router::builder()
.assets(AssetConfig::hosted_at(
"https://cdn.example.com/assets",
AssetBundle::load().unwrap(),
))
.build();
This configuration emits URLs like https://cdn.example.com/assets/logo-1a2b3c4d5e6f7a8b.png. You must manually upload the bundle files from <target>/assets to your CDN.
Summary
- The
asset!macro creates compile-time asset descriptors that survive dead-code elimination and provide zero-cost runtime handles. - Path resolution supports relative paths (to source file or manifest), absolute paths, and remote URLs with local caching.
- Asset options enable renaming, extension overrides, SHA-256 verification, and content-type specification.
- The Bundler extracts descriptors from the binary, hashes content, and produces cache-friendly filenames.
- Router integration via
RouterBuilderAssetExt::assetsserves files locally or generates CDN URLs while maintaining theAssetIdmapping.
Frequently Asked Questions
What happens if I deploy a new binary without updating the asset bundle?
The application will panic at runtime when attempting to render an asset. Because AssetId values are derived from the declaration location and options in crates/topcoat-asset/src/asset.rs (lines 78-90), the binary and bundle share a strict coupling. This early failure prevents broken image links or stale JavaScript in production.
How does Topcoat handle cache invalidation for static assets?
The bundler automatically embeds a content hash into every filename (e.g., app-5e6f7a8b9c0d1e2f.css). Since the hash changes only when the file contents change, you can serve these assets with immutable, long-lived HTTP cache headers. When content changes, the AssetId maps to a new hashed filename, forcing clients to fetch the updated version.
Can I use the asset! macro with files outside my crate directory?
Yes, but with limitations. Absolute paths are used verbatim, and relative paths resolve to CARGO_MANIFEST_DIR. However, for reproducible builds and team development, it is recommended to keep assets within the crate directory or use remote URLs for external dependencies. The bundler caches remote downloads in <target>/topcoat/cache/assets to avoid repeated network requests.
What is the performance impact of using many asset! declarations?
Runtime performance impact is zero. Each Asset is a const handle containing a static byte slice reference. The compile-time binary size increases by 2048 bytes per asset (the RawAsset descriptor size), and the bundler scans this embedded metadata during the build process, not at runtime. Memory overhead consists only of the Asset handle (typically a pointer and length) when used in views.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →