BunBunBundle
A self-contained asset bundler for Ruby powered by Bun. No development dependencies, no complex configuration. Fast builds with CSS hot-reloading, fingerprinting, live reload, and a flexible plugin system. Works with Rails, Hanami, or any Rack app.
Why use BunBunBundle?
Lightning fast bundling
BunBunBundle leverages Bun's native bundler which is orders of magnitude faster than traditional Node.js-based tools. Your assets are built in milliseconds, not seconds.
CSS hot-reloading
CSS changes are hot-reloaded in the browser without a full page refresh. Your state stays intact, your scroll position is preserved, and you see changes instantly.
Asset fingerprinting
Every asset is fingerprinted with a content-based hash in production, so browsers always fetch the right version.
No surprises in production
Development and production builds go through the exact same pipeline. The only differences are fingerprinting and minification being enabled in production, but nothing is holding you back form them in development as well.
Extensible plugin system
Comes with built-in plugins for CSS glob imports, root aliases, and JS glob imports. Plugins are simple, plain JS files, so you can create your own JS/CSS transformers, and raw Bun plugins are supported as well.
Just one dependency: Bun
The bundler ships with the gem. Bun is the only external requirement, so there are zero dev dependencies.
Installation
- Add the gem to your
Gemfile:
gem 'bun_bun_bundle'
Run
bundle installMake sure Bun is installed:
curl -fsSL https://bun.sh/install | bash
Usage with Rails
The gem auto-configures itself through a Railtie. All helpers are available in your views immediately:
<!DOCTYPE html>
<html>
<head>
<%= bun_css_tag('css/app.css') %>
</head>
<body>
<%= bun_img_tag('images/logo.png', alt: 'My App') %>
<%= bun_js_tag('js/app.js', defer: true) %>
<%= bun_reload_tag %>
</body>
</html>
The DevCacheMiddleware is automatically inserted in development to prevent
stale asset caching.
Usage with Hanami
- Require the Hanami integration:
# config/app.rb
require 'bun_bun_bundle/hanami'
- Optionally add the dev cache middleware:
# config/app.rb
module MyApp
class App < Hanami::App
config.middleware.use BunBunBundle::DevCacheMiddleware if Hanami.env?(:development)
end
end
- Include the helpers in your views:
# app/views/helpers.rb
module MyApp
module Views
module Helpers
include BunBunBundle::Helpers
include BunBunBundle::ReloadTag
end
end
end
- Use them in your templates:
<%= bun_css_tag('css/app.css') %>
<%= bun_js_tag('js/app.js') %>
<%= bun_reload_tag %>
Usage with any Rack app
require 'bun_bun_bundle'
# Configure manually
BunBunBundle.config = BunBunBundle::Config.load(root: __dir__)
BunBunBundle.manifest = BunBunBundle::Manifest.load(root: __dir__)
# Optionally set a CDN host
BunBunBundle.asset_host = 'https://cdn.example.com'
Helpers
All helpers are prefixed with bun_ to avoid conflicts with framework helpers:
| Helper | Description |
|---|---|
bun_asset('images/logo.png') |
Returns the fingerprinted asset path |
bun_js_tag('js/app.js') |
Generates a <script> tag |
bun_css_tag('css/app.css') |
Generates a <link> tag |
bun_img_tag('images/logo.png') |
Generates an <img> tag |
bun_reload_tag |
Live reload script (only renders in development) |
All tag helpers accept additional HTML attributes:
<%= bun_js_tag('js/app.js', defer: true, async: true) %>
<%= bun_css_tag('css/app.css', media: 'print') %>
<%= bun_img_tag('images/logo.png', alt: 'My App', class: 'logo') %>
CLI
Build your assets using the bundled CLI:
# Development: builds, watches, and starts the live reload server
bun_bun_bundle dev
# Production: builds with fingerprinting and minification
bun_bun_bundle build
# Development with a production build (fingerprinting + minification)
bun_bun_bundle dev --prod
Configuration
Place a config/bun.json in your project root:
{
"entryPoints": {
"js": ["app/assets/js/app.js"],
"css": ["app/assets/css/app.css"]
},
"outDir": "public/assets",
"publicPath": "/assets",
"manifestPath": "public/bun-manifest.json",
"staticDirs": ["app/assets/images", "app/assets/fonts"],
"devServer": {
"host": "127.0.0.1",
"port": 3002,
"secure": false
},
"plugins": {
"css": ["cssAliases", "cssGlobs"],
"js": ["jsGlobs"]
}
}
All values shown above are defaults, you only need to specify what you want to override.
Plugins
Three plugins are included out of the box:
| Plugin | Description |
|---|---|
cssAliases |
Resolves $/ root aliases in CSS url() references |
cssGlobs |
Expands glob patterns in @import statements |
jsGlobs |
Compiles import x from 'glob:./path/*.js' into object mappings |
Custom plugins
Create a JS file that exports a factory function:
// config/bun/banner.js
export default function banner({ prod }) {
return (content) => {
const stamp = prod ? "" : ` (dev build ${new Date().toISOString()})`;
return `/* My App${stamp} */\n${content}`;
};
}
Then reference it in your config:
{
"plugins": {
"css": ["cssAliases", "cssGlobs", "config/bun/banner.js"]
}
}
Project structure
your-app/
├── app/
│ └── assets/
│ ├── css/
│ │ └── app.css # CSS entry point
│ ├── js/
│ │ └── app.js # JS entry point
│ ├── images/ # Static images (copied + fingerprinted)
│ └── fonts/ # Static fonts (copied + fingerprinted)
├── config/
│ └── bun.json # Optional bundler configuration
└── public/
├── assets/ # Built assets (generated)
└── bun-manifest.json # Asset manifest (generated)
Origins
BunBunBundle was originally built for Fluck, a self-hostable website builder using Lucky Framework. I wanted to have a fast, comprehensive asset bundler that would not require too much maintenance in the long term.
Bun was the natural choice because it does almost everything:
- JS bundling, tree-shaking, and minification
- CSS processing and minification (through the built-in LightningCSS library)
- WebSocket server for hot and live reloading
- Content hashing for asset fingerprints
- Extendability with simple plugins
It's also fast and reliable. We use this setup heavily in two Lucky apps and it is rock solid, and it has since been adopted by Lucky as the default builder.
I wanted to have the same setup in my Ruby apps as well, that's when this Gem was born. I hope you enjoy it too!
Contributing
We use conventional commits.
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'feat: new feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
Contributors
- Wout - creator and maintainer