@aphrody/m3-bun
The one Bun-native tool for M3 apps: a single Bun plugin and the m3-bun CLI. It builds, minifies, serves with HMR,
compiles to one executable, packages and generates deploy configs. One Bun file server serves the UI files and the API
from the same process. Full guide: docs/guides/M3-BUN.md.
m3-bun dev # HMR dev server: UI and API in one process (bunfig [serve.static] plugins)
m3-bun build [--minify none|basic|full] [--graph[=<registry>]]
m3-bun graph [file] # recorded builds (bun:graph), or what depends on a file
m3-bun serve # production server of the build directory
m3-bun compile [--target linux-x64-musl|windows-x64|darwin-arm64|...]
m3-bun package [--rootfs] [--deploy]
m3-bun deploy docker systemd nginx vercel fly cloudflare coolify vscode windows linux-image # or: all
m3-bun smoke # run the executable (yolo runtime when installed), GET /, /healthz, /api/m3/info
Inside the Aphrody checkout the same commands run as yolo m3 bun <command>.
The plugin
@aphrody/m3-bun/plugin is @aphrody/m3's plugin (Tailwind v4, Sass, m3:* token sheets, ?url) plus what a
standalone checkout needs: the token sheets from @aphrody/m3-tokens when the Rust library is absent, the font sheets from
@aphrody/m3-fonts, m3:icons/[style/]name[?fill=1&wght=300] (Material Symbols SVG markup from the forked catalogue, inlined
at build time) and m3:optional/<specifier> (a module that may not be installed: its namespace and resolved: true, or
null and resolved: false).
# bunfig.toml
[serve.static]
plugins = ["@aphrody/m3-bun/plugin"]
// m3.config.ts
export default defineM3Config({ entry: "src/index.html", theme: { seed: "#6750a4" } });
export const bun = defineM3Bun({ api: "src/api/index.ts", port: 3000, compile: { bytecode: true } });
The fork's Bun plugins compose behind options of m3Bun (src/plugins.ts): yaml, svelte (on by default), mdx
(@aphrody/bun-mdx-rs, native onBeforeParse), n2b and oxc. Each one is imported on the first module its hooks
match. The m3 packages are modules too (src/plugins-m3.ts): m3:icon/<style>/<name> (@aphrody/m3-icons),
m3:theme/{tokens,tailwind,spaceui}.css (@aphrody/m3-theme) and m3:assets/<path>[?url] (@aphrody/assets).
Build graph (bun:graph)
m3-bun build --graph (or bun: { graph: true } in m3.config.ts) records the build in the bun:graph SQLite registry of
the Aphrody fork of Bun, .m3-bun/graph.sqlite by default: the app is a repository, the build a run with its duration
sample and one artifact (path, SHA-256, bytes) per output file, and the Bun.build metafile a graph snapshot. Its nodes
are the inputs (virtual m3: modules included), the outputs and the external imports; its edges the imports, keyed by
import kind, and bundled-into from an input to each output it contributed to. m3-bun graph prints the bun:graphx
markdown report; m3-bun graph src/main.ts lists the modules that import the file, transitively, and the outputs it went
into. Runtimes without bun:graph fail with an explicit error.
One process, UI and API
src/api exports Bun routes (defineM3Api); Bun.serve runs them natively next to the file server (cache headers,
ETag, brotli and gzip, SPA fallback, /healthz, /api/m3/info, security headers, traversal guard). routesToFetch turns the
same routes into one fetch handler for Cloudflare Workers and Vercel functions.
The server's optional parts load on first use (src/server-*.ts), on Bun APIs only:
| Module | What it does |
|---|---|
| HTML | HTMLRewriter: CSP nonce on every script, and the request's theme |
| Markdown | Bun.markdown pages |
| Crawl | sitemap.xml, robots.txt and llms.txt |
| Realtime | WebSocket topics |
| Data | bun:sqlite with migrations |
| Secrets | Bun.secrets |
| Auth | Bun.password and sessions |
| Cache | Bun.RedisClient |
| MCP | @aphrody/m3-mcp tools |
| AG-UI | @aphrody/m3-ai |
| Next | @aphrody/next-bun behind a proxy |
| m3 routes | @aphrody/m3/server routes |
m3-bun dev --inspect [--inspector-ui] runs the dev server in a child Bun with its inspector on, relays its console and
exceptions, and serves the WebKit Web Inspector (@aphrody/web-inspector-bun). API load and handler errors go to Bun's
error overlay.
Tools
m3-bun lint (eslint-plugin-m3 through oxlint), migrate (m3-codemods, --next for m3-next-migrate), create
(scaffold), design (m3-design), og (canvas Open Graph image) and lib (m3-config library build). Each tool package
loads on first use (src/tools-*.ts). If a package is missing, the error names it.
Deploy targets also include tauri, webos, lambda, os-themes, release, next and windows-inspect.
Compile: bytecode and the limits
m3-bun compile embeds the built files and the API in a generated entry and links it with the Bun runtime. It passes
bytecode: true to Bun.build (with minify as configured and sourcemap: "none"): JavaScriptCore bytecode is stored in
the executable, so start-up skips parsing. Measured on the fixture app (medians, same host, interleaved runs):
--version 4.3 ms instead of 7.5 ms, first /healthz 5.8 ms instead of 9.3 ms; the executable grows by about 1.5 MB.
Bytecode output is CommonJS, so the generated entry has no top-level await. compile: { bytecode: false } turns it off.
The executable is not static: glibc targets link libc, libm, libdl, libpthread and the loader, musl targets link musl and
libstdc++. Images carry those libraries (m3-bun package --rootfs, the linux-image target); the docker target
compiles and runs on ghcr.io/aphrody-labs/alpine:3.24-runtime (musl, libstdc++, CA certificates, tzdata). Windows
metadata (compile.windows) is applied only when compiling on Windows; elsewhere the report says so.
m3 compile of @aphrody/m3 runs this compiler (an optional peer): compiling an HTML entry directly yields an HTML
file, not an executable.
Deploy targets are files; nothing here applies, pushes or reloads anything.