tronbun
v0.0.12
Published
A webview library for Bun
Downloads
231
Maintainers
Readme
Tronbun (ALPHA)
A powerful desktop application framework that combines Bun's performance with native webviews. Build desktop apps using TypeScript for both backend (Bun) and frontend (webview) with seamless IPC communication.
Features
- 🚀 Bun-powered backend: Leverage Bun's speed and modern JavaScript APIs
- 🌐 Native webviews: Use web technologies for UI (WebKit on macOS, WebView2 on Windows)
- 🔄 Seamless IPC: Easy communication between Bun backend and web frontend
- 🖱️ System tray support: Cross-platform tray icons with custom menus
- 🛠️ Built-in CLI: Comprehensive tooling for development and building
- ⚡ Fast builds: Powered by Bun's built-in bundler
- 🔧 TypeScript support: Full TypeScript support for both backend and frontend
- 🔒 Secure asset embedding: Web assets embedded in executable with obfuscation
- 💻 Cross-platform: Full support for macOS and Windows
Quick Start
Create a new project
npx tronbun init my-appDevelopment workflow
cd my-app
bun install
# Development mode with hot reload
bun run dev
# Or build and run
bun run build
bun run start
# Or compile
bun run compile
# Compile for Windows
bun run compile --platform windowsCLI Commands
The Tronbun CLI provides comprehensive tooling for desktop app development:
init [name]
Create a new Tronbun project with the specified name.
npx tronbun init my-appbuild
Build both backend (Bun) and frontend (web) parts of your application.
npx tronbun build
# Development build (no minification)
npx tronbun build --dev
# Build with file watching
npx tronbun build --watchdev
Start development mode with file watching and hot reload.
npx tronbun devstart
Run the built application.
npx tronbun startcompile
Create an executable for your application.
# Compile for current platform (auto-detected)
npx tronbun compile
# Compile for specific platform
npx tronbun compile --platform windows
npx tronbun compile --platform macos
# Compile with custom output name
npx tronbun compile -o my-app
npx tronbun compile --platform windows -o my-appProject Structure
A typical Tronbun project has the following structure:
my-app/
├── src/
│ ├── main.ts # Backend code (runs in Bun)
│ └── web/
│ └── index.ts # Frontend code (runs in webview)
├── public/ # Static assets
├── dist/ # Built files
├── tronbun.config.json # Configuration
├── package.json
└── tsconfig.jsonConfiguration
The tronbun.config.json file controls build settings:
{
"name": "my-app",
"version": "1.0.0",
"main": "src/main.ts",
"web": {
"entry": "src/web/index.ts",
"outDir": "dist/web",
"publicDir": "public"
},
"backend": {
"entry": "src/main.ts",
"outDir": "dist"
},
"build": {
"target": "bun",
"minify": false,
"sourcemap": true
}
}API Usage
Backend (Bun)
Tronbun provides two ways to create windows: the basic Window class approach and the decorator-based WindowIPC approach for type-safe IPC handlers.
Benefits of Decorator-Based Approach
The decorator-based WindowIPC approach offers several advantages:
- Type Safety: Full TypeScript support with typed method parameters and return values
- Better Organization: IPC handlers are methods on your window class, making code more structured
- Automatic Registration: Handlers are automatically registered using decorators
- IntelliSense Support: Better IDE support for auto-completion and refactoring
- Easier Testing: Window classes can be easily unit tested
- Clear Intent:
@windowNameand@mainHandlerdecorators make the code self-documenting
Approach 1: Basic Window Class
import { Window } from "tronbun";
async function main() {
const window = new Window({
title: "My App",
width: 800,
height: 600
});
// Register IPC handlers
window.registerIPCHandler('getData', async (params) => {
// Your backend logic here
return { message: "Hello from Bun!", timestamp: Date.now() };
});
// Load your web interface
await window.setHtml(`
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
<script type="module" src="./index.js"></script>
</head>
<body>
<div id="app">Loading...</div>
</body>
</html>
`);
}
main().catch(console.error);Approach 2: Decorator-Based WindowIPC (Recommended)
For better organization and type safety, you can extend the WindowIPC class and use decorators:
import { WindowIPC, windowName, mainHandler, findWebAssetPath } from "tronbun";
@windowName('MyApp')
export class MainWindow extends WindowIPC {
constructor() {
super({
title: "My App",
width: 800,
height: 600
});
// Load your web interface
this.navigate(`file://${findWebAssetPath('index.html')}`);
}
@mainHandler('getData')
async handleGetData(params: any): Promise<{message: string, timestamp: number}> {
// Your backend logic here with full TypeScript support
return { message: "Hello from Bun!", timestamp: Date.now() };
}
@mainHandler('greet')
async handleGreet(name: string): Promise<string> {
console.log(`Hello ${name}`);
return `Hello, ${name} from the decorated window!`;
}
@mainHandler('calculate')
async handleCalculate(data: { a: number; b: number; operation: string }): Promise<number> {
const { a, b, operation } = data;
switch (operation) {
case 'add': return a + b;
case 'subtract': return a - b;
case 'multiply': return a * b;
case 'divide': return b !== 0 ? a / b : 0;
default: throw new Error(`Unknown operation: ${operation}`);
}
}
}
// Create and use your window
const mainWindow = new MainWindow();Frontend (Web)
With Basic Window Class
// Declare the tronbun API
declare global {
interface Window {
tronbun: {
invoke: (channel: string, data?: any) => Promise<any>;
};
}
}
// Use IPC to communicate with backend
async function loadData() {
try {
const result = await window.tronbun.invoke('getData', { userId: 123 });
document.getElementById('app')!.innerHTML = `
<h1>${result.message}</h1>
<p>Timestamp: ${result.timestamp}</p>
`;
} catch (error) {
console.error('Failed to load data:', error);
}
}
// Initialize your app
loadData();With Decorator-Based WindowIPC
When using WindowIPC, the class automatically creates a global object with the window name, providing direct access to your handlers:
// For a @windowName('MyApp') window, declare the auto-generated API
declare global {
interface Window {
MyApp: {
getData: (params?: any) => Promise<{message: string, timestamp: number}>;
greet: (name: string) => Promise<string>;
calculate: (data: { a: number; b: number; operation: string }) => Promise<number>;
};
// The base tronbun API is still available
tronbun: {
invoke: (channel: string, data?: any) => Promise<any>;
};
}
}
// Use the type-safe window-specific API
async function loadData() {
try {
// Direct method calls with full type safety
const result = await window.MyApp.getData({ userId: 123 });
const greeting = await window.MyApp.greet('World');
const calculation = await window.MyApp.calculate({ a: 5, b: 3, operation: 'add' });
document.getElementById('app')!.innerHTML = `
<h1>${result.message}</h1>
<p>Timestamp: ${result.timestamp}</p>
<p>${greeting}</p>
<p>5 + 3 = ${calculation}</p>
`;
} catch (error) {
console.error('Failed to load data:', error);
}
}
// Initialize your app
loadData();Advanced Features
Multiple Windows
With Basic Window Class
import { Window } from "tronbun";
const mainWindow = new Window({ title: "Main Window" });
const settingsWindow = new Window({ title: "Settings", width: 400, height: 300 });
// Each window has its own IPC handlers
mainWindow.registerIPCHandler('openSettings', () => {
settingsWindow.setHtml('<h1>Settings</h1>');
});With Decorator-Based Approach
import { WindowIPC, windowName, mainHandler } from "tronbun";
@windowName('MainWindow')
class MainWindow extends WindowIPC {
private settingsWindow: SettingsWindow;
constructor() {
super({ title: "Main Window" });
this.settingsWindow = new SettingsWindow();
}
@mainHandler('openSettings')
async handleOpenSettings(): Promise<void> {
await this.settingsWindow.setHtml('<h1>Settings</h1>');
}
}
@windowName('SettingsWindow')
class SettingsWindow extends WindowIPC {
constructor() {
super({ title: "Settings", width: 400, height: 300 });
}
@mainHandler('saveSettings')
async handleSaveSettings(settings: any): Promise<boolean> {
// Save settings logic
return true;
}
}System Tray Icons
Tronbun provides comprehensive system tray support with custom menus, and event handling across all platforms (Windows, macOS, Linux).
Basic Tray Usage
import { Tray } from "tronbun";
const tray = new Tray({
icon: "path/to/icon.png",
tooltip: "My Application",
menu: [
{
id: 'show',
label: 'Show Window',
type: 'normal',
enabled: true,
callback: () => {
console.log("Show window clicked!");
// Handle show window logic here
}
},
{
id: 'separator1',
label: '',
type: 'separator'
},
{
id: 'quit',
label: 'Quit',
type: 'normal',
accelerator: 'Cmd+Q',
callback: async () => {
await tray.destroy();
process.exit(0);
}
}
]
});
// Handle tray icon clicks (optional)
tray.onClick(() => {
console.log("Tray icon clicked!");
});Alternative: External Menu Handlers
You can still use the traditional approach with external handlers if preferred:
const tray = new Tray({
icon: "path/to/icon.png",
tooltip: "My Application",
menu: [
{ id: 'show', label: 'Show Window', type: 'normal' },
{ id: 'quit', label: 'Quit', type: 'normal', accelerator: 'Cmd+Q' }
]
});
// External handlers (will override inline callbacks if both are defined)
tray.onMenuClick('show', (menuId) => {
console.log(`Menu item ${menuId} clicked`);
});
tray.onMenuClick('quit', async () => {
await tray.destroy();
process.exit(0);
});Menu Item Types
Tray menus support different item types with optional inline callbacks:
const menuItems = [
// Normal menu item with callback
{
id: 'action',
label: 'Perform Action',
type: 'normal',
enabled: true,
callback: () => {
console.log('Action performed!');
}
},
// Checkbox item with state management
{
id: 'toggle',
label: 'Toggle Feature',
type: 'checkbox',
checked: false,
callback: () => {
// Toggle logic here
console.log('Feature toggled!');
}
},
// Separator (no callback needed)
{
id: 'sep1',
label: '',
type: 'separator'
},
// Item with keyboard shortcut and async callback
{
id: 'shortcut',
label: 'With Shortcut',
type: 'normal',
accelerator: 'Ctrl+N',
callback: async () => {
console.log('Shortcut activated!');
// Perform async operations
await performAsyncAction();
}
},
// Submenu with nested callbacks
{
id: 'submenu',
label: 'Options',
type: 'submenu',
submenu: [
{
id: 'option1',
label: 'Option 1',
type: 'normal',
callback: () => console.log('Option 1 selected')
},
{
id: 'option2',
label: 'Option 2',
type: 'normal',
callback: () => console.log('Option 2 selected')
}
]
}
];Dynamic Menu Updates
Update the tray menu dynamically with callbacks:
// Update menu based on application state
let isConnected = false;
const createDynamicMenu = () => [
{
id: 'status',
label: isConnected ? 'Connected ✓' : 'Disconnected ✗',
type: 'normal',
enabled: false
},
{
id: 'connect',
label: isConnected ? 'Disconnect' : 'Connect',
type: 'normal',
callback: async () => {
if (isConnected) {
// Disconnect logic
await disconnect();
isConnected = false;
} else {
// Connect logic
await connect();
isConnected = true;
}
// Update menu to reflect new state
await tray.setMenu(createDynamicMenu());
}
},
{
id: 'separator',
label: '',
type: 'separator'
},
{
id: 'refresh',
label: 'Refresh Status',
type: 'normal',
callback: async () => {
// Check connection status
isConnected = await checkConnectionStatus();
await tray.setMenu(createDynamicMenu());
}
}
];
// Set initial menu
await tray.setMenu(createDynamicMenu());Tray with Window Control
Combine tray icons with window management using inline callbacks:
import { Tray, Window } from "tronbun";
const window = new Window({
title: "My App",
hidden: true // Start hidden
});
const tray = new Tray({
icon: "icon.png",
tooltip: "My App",
menu: [
{
id: 'show',
label: 'Show Window',
type: 'normal',
callback: () => {
window.showWindow();
}
},
{
id: 'hide',
label: 'Hide Window',
type: 'normal',
callback: () => {
window.hideWindow();
}
},
{
id: 'separator',
label: '',
type: 'separator'
},
{
id: 'quit',
label: 'Quit',
type: 'normal',
callback: async () => {
await tray.destroy();
await window.close();
process.exit(0);
}
}
]
});
// Toggle window visibility on tray click
tray.onClick(() => {
// Toggle window visibility logic here
// Note: You may need to track window state manually
window.showWindow(); // or implement toggle logic
});Platform Support
- macOS: Uses NSStatusItem with native menu support
- Windows: Uses Shell_NotifyIcon with popup menus and balloon tooltips
- Linux: Uses GTK StatusIcon
Check platform support:
if (Tray.isSupported()) {
const tray = new Tray({ /* options */ });
} else {
console.log("Tray icons not supported on this platform");
}Complete Tray Example
See examples/tray-example/ for a complete working example that demonstrates:
- Tray icon creation and management
- Custom menus with different item types
- Click and menu event handling
- Window control from tray
- Proper cleanup and resource management
File System Access
Since your backend runs in Bun, you have full access to the file system:
With Basic Window Class
import { readFileSync, writeFileSync } from 'fs';
window.registerIPCHandler('saveFile', async (data) => {
writeFileSync('user-data.json', JSON.stringify(data));
return { success: true };
});
window.registerIPCHandler('loadFile', async () => {
const data = readFileSync('user-data.json', 'utf-8');
return JSON.parse(data);
});With Decorator-Based Approach
import { WindowIPC, windowName, mainHandler } from "tronbun";
import { readFileSync, writeFileSync } from 'fs';
@windowName('FileManager')
export class FileManagerWindow extends WindowIPC {
@mainHandler('saveFile')
async handleSaveFile(data: any): Promise<{ success: boolean }> {
try {
writeFileSync('user-data.json', JSON.stringify(data));
return { success: true };
} catch (error) {
console.error('Failed to save file:', error);
return { success: false };
}
}
@mainHandler('loadFile')
async handleLoadFile(): Promise<any> {
try {
const data = readFileSync('user-data.json', 'utf-8');
return JSON.parse(data);
} catch (error) {
console.error('Failed to load file:', error);
return null;
}
}
}External APIs
Use Bun's built-in fetch and other APIs:
With Basic Window Class
window.registerIPCHandler('fetchWeather', async (city) => {
const response = await fetch(`https://api.weather.com/v1/current?q=${city}`);
return await response.json();
});With Decorator-Based Approach
import { WindowIPC, windowName, mainHandler } from "tronbun";
@windowName('WeatherApp')
export class WeatherWindow extends WindowIPC {
@mainHandler('fetchWeather')
async handleFetchWeather(city: string): Promise<any> {
try {
const response = await fetch(`https://api.weather.com/v1/current?q=${city}`);
if (!response.ok) {
throw new Error(`Weather API error: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Failed to fetch weather:', error);
throw error;
}
}
@mainHandler('getSystemInfo')
async handleGetSystemInfo(): Promise<{ platform: string; version: string }> {
return {
platform: process.platform,
version: process.version
};
}
}Platform Support
Tronbun is designed to work seamlessly on both macOS and Windows:
| Platform | WebView Engine | Tray Support | Status | |----------|---------------|--------------|--------| | macOS | WebKit (WKWebView) | NSStatusItem | Full Support | | Windows | WebView2 (Edge Chromium) | Shell_NotifyIcon | Full Support | | Linux | WebKitGTK | GTK StatusIcon | Experimental |
Native Components
Each platform uses its native webview implementation:
- macOS: Uses
WKURLSchemeHandlerfor the customtronbun://protocol - Windows: Uses
WebResourceRequestedevent for the customtronbun://protocol
Platform-Specific Compilation
Tronbun supports compilation for different platforms with embedded web assets for security and distribution simplicity.
How Compilation Works
When you run npx tronbun compile, Tronbun performs the following steps:
Build without sourcemaps: Compiles your backend and frontend code without
.mapfiles (production mode)Create production bundle: Inlines all CSS and JavaScript into a single HTML string:
- CSS
<link>tags are replaced with inline<style>blocks - JavaScript
<script src="...">tags are replaced with inline<script>blocks - Sourcemap comments are stripped
- CSS
Obfuscate JavaScript: Applies heavy obfuscation to frontend code:
- Control flow flattening
- Dead code injection
- String array encoding (base64)
- Identifier renaming to hexadecimal
Compress and embed: Assets are gzip compressed and base64 encoded before embedding
Compile executable: Uses Bun's compiler to create a single binary
Copy native components: Copies the webview executable and assets folder to the output
Security Benefits
- No external HTML/JS files: Web content is embedded in the binary, preventing tampering
- No source maps in production: Debug information is excluded from compiled apps
- JavaScript obfuscation: Frontend code is heavily obfuscated, making reverse engineering difficult
- Compressed embedding: Assets are gzip compressed, further obscuring content
- Custom URL protocol: Uses
tronbun://protocol to serve embedded content securely - Single executable: Easier to distribute and verify integrity
Automatic Mode Detection
Your code works seamlessly in both development and production:
import { findWebAssetPath, resolveAssetPath } from "tronbun";
// Works in both dev and compiled mode
const htmlPath = findWebAssetPath("index.html", __dirname);
await window.navigate(`file://${htmlPath}`);
// For assets like tray icons
const iconPath = resolveAssetPath("icon.ico");- Development mode: Loads files from disk (
dist/web/), enables hot reload - Compiled mode: Uses embedded HTML content, resolves assets from app bundle
Windows Compilation
Creates a standalone .exe executable with embedded web content.
npx tronbun compile --platform windowsOutput structure:
build/
├── my-app.exe # Main executable (web content embedded)
├── webview_main.exe # Webview component
├── tray_main.exe # Tray component
└── assets/ # Asset files (icons, etc.)
└── icon.icomacOS Compilation
Creates a .app bundle with proper macOS app structure.
npx tronbun compile --platform macosOutput structure:
my-app.app/
├── Contents/
│ ├── MacOS/
│ │ └── my-app # Main executable (web content embedded)
│ ├── Resources/
│ │ ├── webview/ # Webview component
│ │ │ └── build/
│ │ │ ├── webview_main
│ │ │ └── tray_main
│ │ ├── assets/ # Asset files (icons, etc.)
│ │ │ └── icon.ico
│ │ └── icon.icns # App icon
│ └── Info.plist # App metadataNote: There is no dist/ folder in compiled apps - all web content is embedded in the executable.
Cross-Platform Development
- Use
--platform auto(default) to compile for the current platform - Web assets are automatically embedded (no external HTML/JS files)
- Asset files (icons, images) are copied to the appropriate location
- Use
resolveAssetPath()for runtime asset path resolution - All features work identically on macOS and Windows
- The
tronbun://custom protocol handles embedded assets on both platforms
Development Tips
- Hot Reload: Use
bun run devfor automatic rebuilding during development - Debugging: Backend logs appear in terminal, frontend logs in webview dev tools
- Performance: The backend runs in Bun (fast), webview renders HTML/CSS/JS natively
- Distribution: Built apps are portable - just ship the compiled files
- Platform Detection: The CLI automatically detects your platform for compilation
Library development
Build webview
The native webview component needs to be built separately:
cd webview
make allContributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
GPLv3 - see LICENSE file for details.
