biu is a zero-config, high-performance bundler for HTML files with
TypeScript/JavaScript, powered by Bun. It handles module
splitting with custom import suffixes and provides built-in minification for
HTML, CSS, and JS.
Project repository https://github.com/mindon/biu
Easy to use, just art run biu --serve within a directory with a src/
containing your HTML and TS/JS files, biu take care of everything else. (default
preview on http://localhost:3000 )
The oututput directory ./dist is all static html and JS files ready for deployment.
- Zero-config: Automatically scans and bundles your project.
- Smart Module Splitting: TS/JS files whose basename appears in any HTML are built as independent modules; others are automatically inlined.
- Force Inline (
??): Use the??import suffix to force inline bundling. - Minification: Built-in minification for HTML, CSS/SCSS, and TypeScript/JavaScript (including HTML template literals).
- Content Hashing: Output filenames include content hash for cache busting.
- Static Directory: Copy unprocessed static assets directly to the output.
- Watch Mode: Live rebuilds on file changes with debounce.
- Dev Server: Built-in static file server with SPA fallback.
- Backend (API Routes): If a
./backenddirectory exists, biu mounts it as a Next.js-style file-system router (powered byBun.FileSystemRouter). Each.ts/.jsfile under it becomes a route, and named exportsGET/POST/PUT/DELETE/... (or adefaulthandler) serve matching HTTP methods. Routes are hot-reloaded on file changes — no restart. - CDN Caching (offline-ready):
--cdn-cacherecursively downloads every CDN URL referenced from HTML / CSS / JS / TS /<script type="importmap">to a local fs cache, rewrites build outputs to load them from<outDir>/cdn/, and injects a runtime shim that intercepts dynamicdocument.createElement('script'|'link')/fetch/XHRcross-origin loads.--offlinefails loud on cache misses; in--servemode the dev server exposes a lazy/_cdn/<host>/<path>proxy that fills the cache on demand. - Dependency Management: Auto-install, interactive confirm, or record-only
modes for npm dependencies (
--depends). - Post-build Scripts: Run custom
.sh/.ts/.jsscripts after each build. - Self-compile: Build a standalone binary with a single command.
- Fast: Built on the lightning-fast Bun runtime.
biu.ts ← Main entry (~95 lines), orchestrates all modules
src/
├── constants.ts ← Version, USAGE, extension sets, env vars
├── utils.ts ← Utilities (contentHash, recursive scan)
├── cli.ts ← CLI argument parsing & DependsMode types
├── deps.ts ← Dependency detection, install & record
├── styles.ts ← CSS/SCSS compilation & minification
├── assets.ts ← Static asset processing & copying
├── plugins.ts ← Bun build plugins (base + main)
├── html.ts ← HTML processing & minification
├── importmaps.ts ← <script type="importmap"> handling
├── cdn.ts ← CDN URL extraction, fs cache, rewrite pipeline
├── cdn-shim.ts ← Runtime shim injected into HTML for dynamic loads
├── builder.ts ← Core build logic (deps, modules, path rewriting)
├── post-build.ts ← Post-build script execution
├── server.ts ← Watch mode, dev server, backend router, CDN proxy
├── *_test.ts ← Test files for each module
plugins/
└── minify-html-literals/ ← HTML template literal minifier (vendored)
demo-project/ ← Example project for testing
| Module | Responsibility | Dependencies |
|---|---|---|
constants |
Constants & config | — |
utils |
Base utilities | — |
cli |
Argument parsing | constants |
styles |
CSS processing | utils |
html |
HTML processing | styles |
assets |
Asset processing | utils |
plugins |
Build plugins | minify-html-literals |
post-build |
Post-build scripts | — |
importmaps |
Importmap handling | utils |
cdn-shim |
Runtime CDN shim source | — |
cdn |
CDN cache pipeline | utils, cdn-shim |
server |
Watch / Serve / Backend | constants, utils |
deps |
Dependency mgmt | cli |
builder |
Core build | utils, constants, styles, assets, plugins, html, deps, cdn |
biu.ts |
Entry point | cli, constants, builder, assets, post-build, server, cdn |
If a prebuilt binary has been published to the GitHub Releases of
mindon/biu, you can install it
without cloning the repo:
macOS / Linux / WSL:
curl -fsSL https://mindon.dev/biu/install | bashWindows (PowerShell):
powershell -c "irm https://mindon.dev/biu/install.ps1 | iex"The installers download biu-<target>.zip from GitHub Releases, extract the
biu (or biu.exe) binary into $BIU_INSTALL/bin (default ~/.biu/bin /
%USERPROFILE%\.biu\bin) and add it to your PATH. Supported targets:
| Target | Asset name |
|---|---|
darwin-x64 |
biu-darwin-x64.zip |
darwin-aarch64 |
biu-darwin-aarch64.zip |
linux-x64 |
biu-linux-x64.zip |
linux-aarch64 |
biu-linux-aarch64.zip |
linux-x64-musl |
biu-linux-x64-musl.zip |
linux-aarch64-musl |
biu-linux-aarch64-musl.zip |
windows-x64 |
biu-windows-x64.zip |
Each zip ships the
biubinary plusUSAGE.md. Linux musl variants are auto-detected (Alpine, etc.). Windows ARM64 falls back towindows-x64under emulation since Bun does not yet provide a native ARM64 build.
Override defaults via env vars: BIU_INSTALL (install root) or GITHUB
(GitHub origin, useful for proxy mirrors).
Releases are produced by the Release GitHub Actions workflow
on every v* tag — push a tag to trigger it:
git tag v1.1.9
git push origin v1.1.9Since biu is built with Bun, you can compile it into a single executable
binary for portability.
- Ensure you have Bun installed.
- Clone the repository and navigate to the directory.
- Prepare dependencies:
bun i - Compile with the built-in
--buildcommand:
# Self-compile to ./bin/biu (default)
bun run biu.ts --build
# Specify output path
bun run biu.ts --build ./bin/biu
bun run biu.ts --build /usr/local/bin/Bun's auto-install will fetch any missing dependencies automatically. The resulting binary is fully standalone.
Run the biu binary or use bun run directly.
If a
./backenddirectory exists, biu treats it as a Next.js-style file-system router (see Backend (API Routes)).
biu [src-dir] [out-dir] [--watch] [--static dir] [--serve port] \
[--depends mode] [--post-build file] [--build outfile] [--force] \
[--backend-dir dir] [--backend-style nextjs] \
[--cdn-cache [dir]] [--offline]src-dir: The source directory (default:./src).out-dir: The output directory (default:./dist).--watch: Enable watch mode — rebuild on file changes.--static dir: Specify a static assets directory to copy as-is into the output (default:./static). If the directory exists, its contents are copied before each build. In watch mode the static directory is also monitored.--depends [mode]: Control how missing npm dependencies are handled (default:auto). See Dependency Management for details.--post-build <file>: Module.ts/.jsor shell script to run after each build. Receives the output directory as the first argument ($1).--serve [port]: Start a static file server for the output directory on the given port (default:3000when no port is specified). Implies--watch. Also enables the backend router (if./backendexists) and the CDN proxy (if--cdn-cacheis set).--build [outfile]: Self-compilebiu.tsinto a standalone binary at the given path (default:./biu). Usesbun build --compile --minifyunder the hood.--force: Force rewrite output files even if their content hash matches an existing file.--backend-dir <dir>: Override the API routes directory (default:./backend). Only consulted when--serveis active.--backend-style <style>: File-system router style passed toBun.FileSystemRouter(default:nextjs).--cdn-cache [dir]: Cache all referenced CDN URLs into a local directory (default:.biu-cache/cdn) and rewrite outputs to load them from<outDir>/cdn/. See CDN Caching.--offline: Never hit the network — only serve from cache; warn on misses. Implies--cdn-cache.-v, --version: Show version info.-h, --help: Show help / usage.
Options can appear in any order.
Basic build:
cd demo-project
biu # Equivalent to default ./src and ./dist directories
# biu ./src ./distWatch mode:
biu --watchDev watch and server on port 8080:
# biu --serve # default using 3000
biu --serve 8080Custom static directory + dev server:
biu --static ./public --serve 4000Post-build script:
biu --post-build ./scripts/deploy.shBackend API + static dev server:
# Drop a route in ./backend/hello.ts, then:
biu --serve 3000
# → GET http://localhost:3000/helloCache CDN deps for offline use:
biu --cdn-cache # fetch + cache, rewrite outputs to ./dist/cdn/
biu --cdn-cache --offline # build with zero network egress
biu --serve --cdn-cache # dev server with lazy /_cdn/<host>/<path> proxyCompile to binary and run:
# From the biu repo
bun run biu.ts --build bin/biu
export PATH=$PATH:`pwd`/bin
# From a project directory
cd demo-project
biu --serve 3000Install globally:
bun run biu.ts --build /usr/local/bin/biu
cd demo-project
biu --serve 3000biu supports smart module splitting based on basename visibility in HTML:
- Independent module: If a
.ts/.jsfile's basename (e.g.main.ts) appears anywhere in any HTML file's content, it is built as an independent module with its own hashed output file. - Auto inline: If the basename never appears in any HTML, and the file is
imported by another
.ts/.js, it is automatically inlined into its importer — no separate output file is generated. - Force inline (
??): Use the??suffix to force a module to be bundled inline regardless of whether its basename appears in HTML:import { myUtil } from "./utils.ts??";
This means you typically don't need to think about bundling strategy — files mentioned in HTML get their own output, and pure helper/utility modules are automatically bundled into the files that use them.
biu automatically scans your source files for import/require statements
and detects referenced npm packages. The --depends option controls what
happens next:
| Mode | Flag | Behavior |
|---|---|---|
| auto (default) | --depends=auto or omitted |
Detect missing packages and install them via bun add |
| confirm | --depends=confirm |
List missing packages, prompt y/n; y installs, n records to package.json |
| json | --depends=json |
Record all detected deps to package.json (no install) |
| custom json | --depends=my-deps.json |
Record to a custom .json file in package.json format |
| txt | --depends=deps.txt |
Record to a .txt file, one package,version per line |
# Auto-install (default)
biu src dist
# Interactive confirm before installing
biu src dist --depends=confirm
# Record to package.json only
biu src dist --depends=json
# Record to a custom JSON file
biu src dist --depends=my-deps.json
# Record to a text file
biu src dist --depends=deps.txtIn json and txt modes, existing files are merged — new entries are added without overwriting previously recorded dependencies.
In confirm mode the prompt looks like:
📦 Missing dependencies detected:
• dayjs
• lodash-es
install dir: /path/to/project
Install these dependencies? (y/n):
Files under the static directory (default ./static) are copied verbatim into
the output directory before the build runs. This is useful for assets that
should not be processed or hashed — e.g. robots.txt, manifest.json,
third-party scripts, etc.
# Use the default ./static directory
biu
# Specify a different directory
biu --static ./public--serve starts a lightweight HTTP server powered by Bun.serve that serves
the output directory:
/maps to/index.html.- Requests without a file extension fall back to
/index.html(SPA-friendly). - Proper MIME types are automatically detected by Bun.
--serveautomatically enables watch mode, so changes trigger a rebuild.- API routes from
./backend(or--backend-dir) are mounted in front of static files (see Backend (API Routes)). - When
--cdn-cacheis active, a lazy proxy at/_cdn/<host>/<path>populates the cache on demand (see CDN Caching).
If a ./backend directory exists (or --backend-dir <dir> is provided), biu
enables an integrated API layer powered by Bun.FileSystemRouter. It only
runs when --serve is active — pure static builds are unaffected.
File layout:
backend/
├── index.ts → GET /
├── hello.ts → GET /hello
├── users/
│ ├── index.ts → GET /users
│ └── [id].ts → GET /users/:id (params.id)
└── api/
└── search.ts → GET /api/search (?q=...)
Handler shape: each route file may export named methods (GET, POST,
PUT, PATCH, DELETE, ...) and/or a default function. Named methods
take precedence; default is the catch-all fallback. Anything else returns
405 Method Not Allowed.
// backend/users/[id].ts
import type { RouteContext } from "biu/server";
export async function GET(req: Request, ctx: RouteContext) {
return { id: ctx.params.id, name: "Ada" };
}
export async function DELETE(_req: Request, ctx: RouteContext) {
return new Response(null, { status: 204 });
}Return value coercion: handlers may return any of:
| Returned value | Response |
|---|---|
Response |
passed through |
string |
text/plain; charset=utf-8 |
null / undefined |
204 No Content |
ArrayBuffer / Uint8Array / Blob / ReadableStream |
binary stream |
| anything else (object/array/number/...) | JSON (application/json; charset=utf-8) |
Errors thrown inside a handler are logged and surface as 500.
RouteContext:
interface RouteContext {
params: Record<string, string>; // dynamic segments, e.g. /users/[id]
query: Record<string, string>; // parsed query string
pathname: string; // matched route pathname
}Hot reload: the backend directory is watched recursively. Edits invalidate
the route module cache (via a ?v=<n> import suffix) and the next request
re-imports the latest version — no server restart, no debounce flicker.
# Defaults: ./backend, nextjs style
biu --serve 3000
# Custom location
biu --serve 3000 --backend-dir ./api--cdn-cache lets you ship a build that works fully offline by mirroring
every external CDN dependency into a local directory. It is opt-in — without
the flag biu ignores remote URLs.
What is discovered:
- HTML attributes:
<script src>,<link href>,<img src>,<iframe src>,<video src>,<source src>,data-src, etc. <script type="importmap">JSON entries (recursively).- CSS:
url(https://...)and@import "https://...". - JS/TS:
import ... from "https://...", bareimport "...", dynamicimport("..."),new URL("https://..."), plus conservative literal-URL detection for runtime loaders (coverss.src = "https://cdn/foo.js"style code). - Recursive walk: each fetched JS/CSS body is scanned for further CDN URLs (and relative paths inside CDN packages) until the closure is complete.
What happens at build time:
- URLs are downloaded into
<cacheDir>/<host>/<path>(defaultcacheDir:.biu-cache/cdn, persistent across builds). - A manifest (
manifest.json) is written next to the cache so subsequent builds reuse it even if the network is unavailable. - JS/CSS bodies inside the cache are rewritten to refer to their cache siblings via relative paths — the cache directory is fully self-contained.
- The cache is mirrored into
<outDir>/cdn/. - All HTML / CSS / JS in
<outDir>are rewritten so the original CDN URLs point at./cdn/<host>/<path>. - A tiny runtime shim (
<script data-biu-cdn>) is injected into every HTML that intercepts dynamicdocument.createElement('script'|'link'),fetch(), andXMLHttpRequestcalls and redirects known CDN URLs to the local copy. Unknown URLs fall back to the network — or, in--servemode, to the dev-server proxy at/_cdn/<host>/<path>.
Dev-server proxy: while biu --serve is running with --cdn-cache, any
request to /_cdn/<host>/<path> is served from cache; on a miss it fetches
upstream, writes to cache, and serves the body. With --offline the proxy
returns 504 instead of touching the network.
Examples:
# Default cache dir (.biu-cache/cdn) — fetch on first build, cache on subsequent
biu --cdn-cache
# Custom cache dir
biu --cdn-cache ./vendor-cdn
# Build for production with no network at all (CI without egress, air-gapped, etc.)
biu --cdn-cache --offline
# Dev server with on-demand proxy filling
biu --serve 3000 --cdn-cacheAfter the build, <outDir>/cdn/ is fully self-contained and can be deployed
along with the rest of the site — your users never hit the upstream CDN.
Add extra static asset extensions beyond the built-in set (images, fonts, audio, video, etc.). Extensions can be specified with or without a leading dot, separated by commas, spaces, or semicolons.
BIU_ASSETS_EXTS=".glb .gltf .hdr" biu
BIU_ASSETS_EXTS="glb,gltf,hdr" biuA regex pattern to exclude files from processing. Case-insensitive.
BIU_EXCLUDED="test|spec" biuTests are written with Bun's built-in test runner. Each module in src/ has a
corresponding *_test.ts file.
# Run all tests
bun test src/
# Run a specific test file
bun test src/html_test.tsMIT