Native bindings bundler for Bun's --compile flag
Compile Bun projects with Sharp, Canvas, or other native modules into single-file executables. Drop-in replacement for bun build --compile.
# ❌ This fails with native modules
bun build --compile ./app.js --outfile ./app.exe
# Error: Cannot find module '/path/to/$bunfs/node_modules/sharp/...'
# ✅ This works
bundlerbus ./app.js --outfile ./app.exeHow: Extracts your app + node_modules to a real filesystem at runtime. Native bindings see real paths instead of Bun's virtual $bunfs.
Trade-off: Larger bundles (~100MB+), ~2-3s first-run delay. Subsequent runs: < 50ms. Zero configuration.
# Install
npm install -g bundlerbus
# Use (same flags as Bun)
bundlerbus ./src/cli.js --target bun-windows-x64 --outfile ./dist/app.exe
# That's it. Your app with native modules now compiles.Forwards all Bun flags: --target, --minify, --define, --windows-icon, etc.
Build Time (your machine):
[Your Code] + [node_modules] → payload.tar.gz → compiled.exe
First Run (user's machine):
compiled.exe → Extract to cache (~/.cache/app/1.0.0/) → Launch app
└─ Native modules see real paths ✅
Subsequent Runs:
compiled.exe → Check hash → Cache hit → Launch (< 50ms)
Cache locations (automatic fallback):
- Windows:
%LOCALAPPDATA%\your-app\1.0.0\ - macOS:
~/Library/Application Support/your-app/1.0.0/ - Linux:
~/.cache/your-app/1.0.0/
Falls back to system temp or current directory if standard location is locked.
Bundlerbus finds your entry point automatically:
- Explicit argument:
bundlerbus ./src/cli.js package.json→"bin"field (if single entry)package.json→"main"field- Convention:
./index.js,./src/index.js, or./main.js
Multiple binaries? Specify explicitly:
bundlerbus ./src/cli.js --outfile ./dist/app.exe
bundlerbus ./src/server.js --outfile ./dist/server.exeStrategy: Pack everything for maximum reliability.
✅ Included:
- All project files (except
node_modules/anddist/during root scan) - Complete
node_modules/directory with zero filtering
❌ Excluded:
dist/directory (build output)
Why pack everything?
- Zero configuration required
- No missing dependencies at runtime
- Works with any project structure
- No "forgot to include X" errors
Trade-off: Larger bundles, but 100% reliability. Native modules have complex dependency chains—missing a single .dll causes runtime failures.
bundlerbus ./index.js --outfile ./dist/app.exe
bundlerbus ./src/main.js --minify --outfile ./build/app// build.js
import { spawnSync } from 'child_process';
import pkg from './package.json' assert { type: 'json' };
const platforms = {
win32: ['--target', 'bun-windows-x64', '--outfile', './dist/app-win.exe'],
darwin: ['--target', 'bun-darwin-arm64', '--outfile', './dist/app-mac'],
linux: ['--target', 'bun-linux-x64', '--outfile', './dist/app-linux']
};
Object.entries(platforms).forEach(([name, flags]) => {
console.log(`Building for ${name}...`);
const result = spawnSync('bundlerbus', ['./src/cli.js', ...flags], {
stdio: 'inherit'
});
if (result.status !== 0) process.exit(1);
});const args = [
'./src/cli.js',
'--windows-icon=./icon.ico',
'--windows-publisher="Your Company"',
`--windows-version="${pkg.version}"`,
'--target=bun-windows-x64',
'--outfile=./dist/app.exe'
];
spawnSync('bundlerbus', args, { stdio: 'inherit' });| Platform | Status | Testing |
|---|---|---|
| Windows x64 | ✅ Production | Fully tested (Sharp, Canvas) |
| macOS Intel/ARM | Code ready, needs community testing | |
| Linux x64 | Code ready, needs community testing |
Error: Cannot find module 'sharp'
Fix: Ensure module is in dependencies (not devDependencies), run npm install before building.
[BOOTSTRAP ERROR] Cannot find writable cache directory!
Fix: Bundlerbus tries 3 locations automatically. Check logs to see which were tested. User needs write access to at least one.
[BOOTSTRAP LOCK] Waiting for other instance to finish extraction...
Not an error. If multiple instances start during first extraction:
- First instance extracts (~2-3s)
- Others wait safely
- All start normally after extraction completes
This is race condition protection working correctly.
[BOOTSTRAP ERROR] Cannot delete cache - files are in use!
Cause: Old instance still running (common when rebuilding without closing).
Fix:
- Close all running instances
- Wait a few seconds
- Try again
Note: Only happens in dev with same version number. Production uses different versions → different cache dirs → no conflict.
[FAILURE] Could not determine entry point
Fix: Specify explicitly or add to package.json:
bundlerbus ./src/index.js [flags...]{
"bin": "./src/cli.js"
}| Feature | Bun --compile | Bundlerbus |
|---|---|---|
| Pure JS projects | ✅ | ✅ |
| Native bindings | ❌ | ✅ |
| Startup time | Instant | ~50ms cached, ~2s first |
| Bundle size | Smaller | Larger (~100MB+) |
| Configuration | None | None |
| Reliability with natives | 0% | 100% |
Why Extraction Works
Native modules use __dirname to locate binaries:
// sharp/lib/constructor.js
const libvips = require(path.join(__dirname, '../build/Release/sharp.node'));In Bun's $bunfs: __dirname → $bunfs/node_modules/sharp/lib (virtual, unreadable by native code)
After extraction: __dirname → C:\Users\...\AppData\Local\app\1.0.0\node_modules\sharp\lib (real path ✅)
Native Module Resolution
Bundlerbus sets NODE_PATH to extracted node_modules:
process.env.NODE_PATH = '/cache/path/node_modules'Modern native modules use relative loading:
- Linux:
RPATH=$ORIGIN - macOS:
@loader_path - Windows: Same-directory search
No LD_LIBRARY_PATH manipulation needed. Works with macOS SIP.
Conditional Working Directory
Smart cwd management:
With path arguments (drag-and-drop, CLI):
app.exe ./content # cwd unchanged, app gets correct pathWithout paths (double-click):
app.exe --verbose # cwd = exe folder (portable behavior)Allows apps to work as both CLI tools and portable executables.
Cache Invalidation
SHA-256 hash-based:
- Payload hash stored in
.hashfile - Checked on every run (< 1ms)
- Mismatch triggers re-extraction
- Version number doesn't matter—any change detected
File-based locking prevents corruption if multiple instances start during extraction.
Comprehensive logging for troubleshooting:
[BOOTSTRAP DEBUG] Platform: win32
[BOOTSTRAP CACHE] ✓ Selected writable cache directory: C:\...\AppData\Local\app\1.0.0
[BOOTSTRAP LOCK] ✓ Acquired lock (PID: 12345)
[BOOTSTRAP] Extracting archive...
[BOOTSTRAP] Extracted 500 files...
[BOOTSTRAP] ✓ Extraction complete: 3564 files, 0 directories
[BOOTSTRAP TIMING] Bootstrap completed in 2895ms
All errors include actionable solutions.
# Windows
del /s /q %LOCALAPPDATA%\your-app
# macOS
rm -rf ~/Library/Application\ Support/your-app
# Linux
rm -rf ~/.cache/your-appOld versions accumulate but don't interfere. Safe to delete manually.
| Limitation | Impact | Justification |
|---|---|---|
| Large bundles (~100MB+) | Storage | Native modules + dependencies. Reliability > size. |
| First-run delay (~2-3s) | UX | Extraction cost. Cached runs: < 50ms. |
| Disk space (2x bundle) | Storage | Uncompressed cache. Users can clean old versions. |
- SHA-256 cache validation
- Cross-platform cache dirs
- Race condition protection
- Fallback cache locations
- Smart filtering (opt-in
"files"field support) -
bundlerbus cleancommand - Delta extraction (only changed files)
- Lazy loading (extract on
require())
Built for THYPRESS — zero-config static site generator with Sharp image processing.
- THYPRESS Launcher — https://github.com/thypress/launcher
Using Bundlerbus? Add your project here! 🚍
MIT
Created by @phteocos. Inspired by Electron ASAR, designed for Bun's native binding challenges.