Skip to content

phtdacosta/bundlerbus

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

📦🚍 Bundlerbus

Native bindings bundler for Bun's --compile flag

npm version npm downloads (monthly) npm downloads (total) license GitHub stars GitHub forks GitHub issues GitHub last commit Repo size THYPRESS.ORG

Compile Bun projects with Sharp, Canvas, or other native modules into single-file executables. Drop-in replacement for bun build --compile.

The Problem → The Solution

# ❌ 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.exe

How: 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.


Quick Start

# 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.


How It Works

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.


Entry Point Detection

Bundlerbus finds your entry point automatically:

  1. Explicit argument: bundlerbus ./src/cli.js
  2. package.json"bin" field (if single entry)
  3. package.json"main" field
  4. 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.exe

What Gets Packed

Strategy: Pack everything for maximum reliability.

Included:

  • All project files (except node_modules/ and dist/ 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.


Examples

Basic

bundlerbus ./index.js --outfile ./dist/app.exe
bundlerbus ./src/main.js --minify --outfile ./build/app

Cross-Platform Build Script

// 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);
});

With Windows Metadata

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 Support

Platform Status Testing
Windows x64 ✅ Production Fully tested (Sharp, Canvas)
macOS Intel/ARM ⚠️ Beta Code ready, needs community testing
Linux x64 ⚠️ Beta Code ready, needs community testing

Troubleshooting

Native Module Not Found

Error: Cannot find module 'sharp'

Fix: Ensure module is in dependencies (not devDependencies), run npm install before building.


Permission Denied

[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.


Concurrent Starts (First Run)

[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.


Files In Use (Development Only)

[BOOTSTRAP ERROR] Cannot delete cache - files are in use!

Cause: Old instance still running (common when rebuilding without closing).

Fix:

  1. Close all running instances
  2. Wait a few seconds
  3. Try again

Note: Only happens in dev with same version number. Production uses different versions → different cache dirs → no conflict.


Wrong Entry Point

[FAILURE] Could not determine entry point

Fix: Specify explicitly or add to package.json:

bundlerbus ./src/index.js [flags...]
{
  "bin": "./src/cli.js"
}

Comparison to Bun's --compile

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%

Technical Deep Dive

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: __dirnameC:\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 path

Without 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 .hash file
  • 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.


Debug Logging

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.


Manual Cache Cleanup

# Windows
del /s /q %LOCALAPPDATA%\your-app

# macOS
rm -rf ~/Library/Application\ Support/your-app

# Linux
rm -rf ~/.cache/your-app

Old versions accumulate but don't interfere. Safe to delete manually.


Limitations

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.

Roadmap

  • SHA-256 cache validation
  • Cross-platform cache dirs
  • Race condition protection
  • Fallback cache locations
  • Smart filtering (opt-in "files" field support)
  • bundlerbus clean command
  • Delta extraction (only changed files)
  • Lazy loading (extract on require())

Related Projects

Built for THYPRESS — zero-config static site generator with Sharp image processing.

Using Bundlerbus? Add your project here! 🚍


License

MIT


Credits

Created by @phteocos. Inspired by Electron ASAR, designed for Bun's native binding challenges.

About

One-liner, drop-in, universal native bindings bundler for Bun's --compile flag

Resources

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors