asset-oven is a lazy, content-hashed asset middleware for Bun — local asset paths written straight into server-rendered HTML just work, no bundler config, no manifest, no build step to remember to run.
I'd previously written a small bundler for Bun + Hono that I've been using on other projects for a while, but recently I've needed to work on more static sites and enjoyed using Hono's server-side JSX for templating, so I ended up writing asset-oven as a weekend project with some help from Claude.
import { Hono } from "hono";
import { builder } from "asset-oven/hono";
const app = new Hono();
app.use(builder({ root: import.meta.dir, publicPath: "/assets" }));
app.get("/", (c) => c.html(`<script src="/client.ts" type="module"></script>`));
export default app;On the first request that renders /client.ts, asset-oven bundles it with Bun.build, content-hashes the output, rewrites the tag to /assets/client-a1b2c3d4.js, and serves it with a long-lived immutable cache header — all inline in the response, no separate build command.
bun add asset-ovenPick the adapter for your server:
// Hono
import { builder } from "asset-oven/hono";
app.use(builder({ root: import.meta.dir, publicPath: "/assets" }));// plain Bun.serve
import { withAssets } from "asset-oven/bun";
const fetch = withAssets(myFetchHandler, {
root: import.meta.dir,
publicPath: "/assets",
});
Bun.serve({ fetch });Then just reference assets by their on-disk path in your HTML — <script src="/client.ts">, <link rel="stylesheet" href="/styles.css">, <img src="/logo.png"> — and asset-oven handles the rest.
- Zero-config asset paths 📝 — write
src="/client.ts"in your JSX/HTML like it's a static file; no imports, no manifest to look up. - Lazy build + hash + cache ⚡ — assets are built on first request, not ahead of time; concurrent requests for the same not-yet-built asset are deduped into a single build.
- Nested assets just resolve 🔗 — a JS
importof an image or a CSSurl()background produces its own hashed URL automatically, viaBun.build's own dependency graph. - Real static-file serving 📦 — Range requests,
HEAD, and immutable long-lived caching headers, so video/audio scrubbing and CDN caching work correctly. - Two thin adapters 🔌 — a Hono middleware (
asset-oven/hono) and a plainBun.servefetch wrapper (asset-oven/bun), both backed by the same framework-agnosticAssetBuildercore. - Sensible defaults, fully overridable ⚙️ — cache-retry timing defaults off
NODE_ENV, and the rawBun.buildconfig (minify,sourcemap, plugins,define, etc.) can be passed straight through.
This repo is a Bun workspace monorepo — the library lives in asset-oven/, with three runnable examples under examples/:
- examples/hono-basic/ — a Hono app using the
asset-oven/honomiddleware - examples/bun-serve/ — a plain
Bun.serveapp using theasset-oven/bunwithAssetswrapper - examples/nested-assets/ — a JS
importand a CSSurl()each resolving to their own hashed asset URL
bun install
cd examples/hono-basic && bun run devSwap in examples/bun-serve or examples/nested-assets for the other two.
AssetBuilderOptions, accepted by both adapters and the AssetBuilder core:
| Option | Default | Description |
|---|---|---|
root |
process.cwd() |
Directory local asset paths are resolved against. |
publicPath |
"" |
Prefix prepended to every hashed asset URL. |
cacheDir |
a random os.tmpdir() subdir |
Output directory for built/copied assets; wiped on boot. |
build |
{} |
Merged into every Bun.build call — e.g. minify, sourcemap, plugins, define. |
negativeCacheTtl |
60000ms in production, else 5000ms |
How long a failed/missing asset lookup is cached before retry. |
cd asset-oven && bun testMIT
This project uses AI-assisted development tools. All output has been reviewed by a human (me). The overall structure of the project and original prototyping was written by a human.
