npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bunlet/core

v0.3.1

Published

Build cross-platform desktop apps with Bun and WebView — a lightweight, familiar alternative to Electron

Readme

@bunlet/core

Build cross-platform desktop apps with Bun and WebView — a lightweight, familiar alternative to Electron.

npm version License: MIT TypeScript Bun

Bunlet combines Bun's speed with native system WebViews. You get an Electron-compatible API, type-safe IPC via Zod, and a Rust-backed native layer — without bundling Chromium.


Quick Start

bun add @bunlet/core

main.ts

import { app, BrowserWindow, z } from '@bunlet/core';
import path from 'path';

// Register type-safe IPC handlers before the app is ready
app.handle('greet', z.object({ name: z.string() }), async ({ name }) => {
  return { message: `Hello, ${name}!` };
});

// Wait for the native runtime to initialise
await app.whenReady();

const win = new BrowserWindow({
  width: 800,
  height: 600,
  title: 'My Bunlet App',
  webPreferences: {
    preload: './preload.ts',
  },
});

// Load a local HTML file or a remote URL
win.loadFile(path.join(import.meta.dir, 'index.html'));

// Start the event loop (blocking — call this last)
app.run();

preload.ts

import { contextBridge, ipcRenderer } from '@bunlet/core';

contextBridge.exposeInMainWorld('api', {
  greet: (name: string) => ipcRenderer.invoke('greet', { name }),
});

index.html

<!DOCTYPE html>
<html>
<body>
  <h1>Bunlet App</h1>
  <button onclick="greet()">Greet</button>
  <script>
    async function greet() {
      const result = await window.api.greet('World');
      alert(result.message);
    }
  </script>
</body>
</html>
bun run main.ts

Why Bunlet?

| | Bunlet | Electron | Tauri | |---|---|---|---| | Runtime | Bun (JavaScriptCore) | Node.js (V8) | Rust | | WebView | System WebView | Chromium (bundled) | System WebView | | App size | ~20–40 MB | 80–150 MB | 2–10 MB | | Language | TypeScript | JavaScript / HTML | Rust + JS | | IPC | Zod-validated, Electron-compatible | contextBridge / ipcMain | Command-based | | Learning curve | Low (Electron-like API) | Low | Moderate | | Dev experience | Bun's bundler, HMR | webpack / vite | Rust toolchain |

  • Familiar API — if you've used Electron, you already know Bunlet. app, BrowserWindow, ipcMain/ipcRenderer, Menu, Tray, dialog, Notification all work the same way.
  • TypeScript-first — full type safety with Zod schema validation on every IPC channel. Invalid payloads are rejected before your handler runs.
  • Small footprint — uses the OS WebView (WebKit on macOS, WebView2 on Windows, WebKitGTK on Linux). No bundled browser engine.
  • Rust performance — native module written in Rust with NAPI bindings. Window management, IPC dispatch, and platform APIs run outside the JS thread.

Platform Support

| Platform | WebView Engine | Status | |---|---|---| | macOS 11+ (arm64 / x64) | WKWebView | Stable | | Windows 10+ (x64) | WebView2 | Stable | | Linux | WebKitGTK 4.1 | Stable |

Linux dependencies

# Ubuntu / Debian
sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev

# Fedora
sudo dnf install webkit2gtk4.1-devel gtk3-devel

# Arch
sudo pacman -S webkit2gtk-4.1 gtk3

API Overview

Application lifecycle

import { app } from '@bunlet/core';

app.handle('channel', schema, handler);   // register IPC handler
app.removeHandler('channel');             // remove IPC handler
await app.whenReady();                    // wait for native runtime
app.run();                                // start event loop (blocking)
app.quit();                               // gracefully quit
app.exit(code);                           // force exit
app.relaunch();                           // restart the app
app.getPath('userData');                  // platform-aware paths
app.getName();                            // app name
app.getVersion();                         // app version

app.on('ready', () => {});
app.on('window-all-closed', () => {});
app.on('before-quit', (event) => { event.preventDefault(); });
app.on('will-quit', () => {});
app.on('quit', () => {});

BrowserWindow

import { BrowserWindow } from '@bunlet/core';

const win = new BrowserWindow({
  width: 800,
  height: 600,
  title: 'Bunlet',
  resizable: true,
  frame: true,
  show: true,
  transparent: false,
  alwaysOnTop: false,
  minWidth: 400,
  minHeight: 300,
  webPreferences: {
    preload: './preload.ts',
    devTools: true,
    partition: 'persist:my-session',
  },
  parent: parentWindow,   // child window
  modal: true,            // modal dialog
});

// Content
win.loadFile('index.html');
win.loadURL('https://example.com');
win.webContents.reload();

// Visibility
win.show();
win.hide();
win.focus();
win.blur();

// State
win.maximize();
win.minimize();
win.restore();
win.setFullScreen(true);
win.isMaximized();

// Geometry
const bounds = win.getBounds();
win.setBounds({ x: 100, y: 100, width: 800, height: 600 });
win.setSize(1024, 768);
win.setPosition(0, 0);
win.center();

// Properties
win.setTitle('New Title');
win.setAlwaysOnTop(true);

// Lifecycle
win.close();
win.destroy();
win.isDestroyed();

// Events
win.on('close', (event) => { event.preventDefault(); });
win.on('closed', () => {});
win.on('focus', () => {});
win.on('blur', () => {});
win.on('maximize', () => {});
win.on('minimize', () => {});
win.on('resize', (bounds) => {});
win.on('move', (bounds) => {});
win.on('enter-full-screen', () => {});
win.on('leave-full-screen', () => {});
win.on('file-drop', (files) => {});
win.on('ready-to-show', () => {});

// Static methods
BrowserWindow.getAllWindows();
BrowserWindow.getFocusedWindow();
BrowserWindow.fromId(id);

IPC (Inter-Process Communication)

// Main process
app.handle('do-thing', z.object({ id: z.string() }), async (params, ctx) => {
  // ctx.window → the BrowserWindow that sent the request
  // ctx.windowId → numeric window id
  return { ok: true };
});

// Send a message to the renderer from main
win.webContents.send('event-name', { data: 123 });

// Preload script
import { contextBridge, ipcRenderer } from '@bunlet/core';

contextBridge.exposeInMainWorld('api', {
  doThing: (id: string) => ipcRenderer.invoke('do-thing', { id }),
  onEvent: (cb) => ipcRenderer.on('event-name', cb),
});

// Renderer (browser)
const result = await window.api.doThing('abc');
window.api.onEvent((event, data) => console.log(data));

Native APIs

import { dialog, Menu, Tray, Notification, clipboard, shell, globalShortcut, powerMonitor, screen, fileWatcher } from '@bunlet/core';

// File dialogs
const { filePath, canceled } = await dialog.showOpenDialog({
  title: 'Open File',
  filters: [{ name: 'Images', extensions: ['png', 'jpg'] }],
});
await dialog.showSaveDialog({ title: 'Save As' });
const { response } = await dialog.showMessageBox({ type: 'info', message: 'Done!' });

// Menus
const menu = Menu.buildFromTemplate([
  { label: 'File', submenu: [{ label: 'Quit', role: 'quit' }] },
  { label: 'Edit', submenu: [{ label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' }] },
]);
Menu.setApplicationMenu(menu);

// Context menus
win.webContents.on('context-menu', () => {
  const ctxMenu = Menu.buildFromTemplate([
    { label: 'Inspect', click: () => win.webContents.openDevTools() },
  ]);
  ctxMenu.popup();
});

// System tray
const tray = new Tray('/path/to/icon.png');
tray.setToolTip('My App');
tray.setContextMenu(menu);
tray.on('click', () => win.show());

// Notifications
new Notification({ title: 'Hello', body: 'World!' }).show();

// Clipboard
clipboard.writeText('copied!');
const text = clipboard.readText();

// Shell
shell.openExternal('https://bun.sh');
shell.showItemInFolder('/path/to/file');

// Global shortcuts
globalShortcut.register('CmdOrCtrl+Shift+K', () => console.log('pressed!'));

// Power monitor
const info = powerMonitor.getBatteryInfo();
powerMonitor.on('suspend', () => {});
powerMonitor.on('resume', () => {});

// Screen
const displays = screen.getAllDisplays();
const primary = screen.getPrimaryDisplay();

// File watching
const watcher = fileWatcher.watch('/path/to/dir', { recursive: true });
watcher.on('change', (event) => console.log(event));

Auto-Updater

import { autoUpdater } from '@bunlet/core';

autoUpdater.setFeedURL({
  provider: 'github',
  github: { owner: 'user', repo: 'my-app' },
});

autoUpdater.on('update-available', (info) => console.log('New version:', info.version));
autoUpdater.on('download-progress', (p) => console.log(`${p.percent}%`));
autoUpdater.on('update-downloaded', () => autoUpdater.quitAndInstall());

autoUpdater.checkForUpdates();

Session & Cookies

import { session } from '@bunlet/core';

const sess = session.fromPartition('persist:my-partition');
const cookies = await sess.cookies.get({ url: 'https://example.com' });
await sess.cookies.set({ url: 'https://example.com', name: 'token', value: 'abc' });
await sess.clearStorageData({ storages: ['cookies', 'localstorage'] });

Configuration

Bunlet apps can declare a bunlet.config.ts (or bunlet.config.js) at the project root:

import { defineConfig } from 'bunlet/config';

export default defineConfig({
  main: 'src/main.ts',
  preload: 'src/preload.ts',
  renderer: {
    root: 'renderer',
    index: 'index.html',
  },
  webview: {
    engine: 'system',   // or 'cef' for the CEF backend
  },
  build: {
    outDir: 'dist',
    minify: true,
    sourcemap: 'external',
  },
  package: {
    name: 'my-app',
    version: '1.0.0',
    bundleId: 'com.example.myapp',
    icon: './icon.png',
    mac: { category: 'public.app-category.developer-tools' },
  },
  publish: {
    provider: 'github',
    owner: 'user',
    repo: 'my-app',
  },
});

Requirements


Known Limitations

  • Linux Screen API: display enumeration can conflict with GTK's event loop. Window centering may use fallback values.
  • System WebView cookies: the system WebView backend cannot enumerate cookies; cookies.get() returns []. Use the CEF backend for full cookie support.
  • executeJavaScript is fire-and-forget on the system WebView backend. Use IPC to return values from JS.
  • CEF backend is optional and requires building the @bunlet/cef package. Parity is in progress.

See the repository docs for architecture details and the full roadmap.


FAQ

How do I pass data between main and renderer?

Use the IPC system. In the main process, register a handler with Zod validation:

// Main process
app.handle('get-data', z.object({ id: z.string() }), async ({ id }) => {
  return { name: 'Item ' + id, count: 42 };
});

In the preload script, expose it via the context bridge:

// Preload
contextBridge.exposeInMainWorld('api', {
  getData: (id: string) => ipcRenderer.invoke('get-data', { id }),
});

In the renderer, call it directly:

const result = await window.api.getData('abc123');

How do I add a system tray icon?

import { Tray, Menu } from '@bunlet/core';

const tray = new Tray('/path/to/icon.png');
tray.setToolTip('My App');
tray.setContextMenu(Menu.buildFromTemplate([
  { label: 'Show', click: () => win.show() },
  { label: 'Quit', role: 'quit' },
]));

Can I use React, Vue, or Svelte with Bunlet?

Yes — Bunlet renders standard HTML/CSS/JS, so any frontend framework works. Bundle your renderer with Vite, esbuild, or Bun's bundler and load the output. The bunlet create command scaffolds a framework-agnostic project; add your framework of choice.

Why is my window blank on Linux?

You likely don't have the WebKitGTK libraries installed:

sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev

How do I update my app automatically?

import { autoUpdater } from '@bunlet/core';

autoUpdater.setFeedURL({
  provider: 'github',
  github: { owner: 'user', repo: 'my-app' },
});

autoUpdater.on('update-available', (info) => {
  console.log('New version:', info.version);
});
autoUpdater.on('update-downloaded', () => autoUpdater.quitAndInstall());
autoUpdater.checkForUpdates();

Should I use the system WebView or CEF backend?

Use system WebView (default) for most apps — it adds 0MB to your bundle and uses the OS's native browser engine. Use the CEF backend (@bunlet/cef) when you need Chromium-specific features: full DevTools, executeJavaScript return values, cookie read/write, or pixel-perfect cross-platform rendering. CEF adds approximately 100MB to your app size.

Can I distribute my app outside app stores?

Yes. The @bunlet/cli package command produces standalone installers (.dmg for macOS, .msi/.exe for Windows, .AppImage/.deb for Linux) that users download and install directly — no app store review process.


Migrating from Electron

Bunlet's API is designed to feel familiar to Electron developers. Here are the key differences:

| Pattern | Electron | Bunlet | |---|---|---| | Install | npm install electron | bun add @bunlet/core | | CLI | npx create-electron-app | bunx @bunlet/cli create | | Import | import { app } from 'electron' | import { app } from '@bunlet/core' | | IPC validation | Manual / none | Zod schema required | | Runtime | Node.js | Bun | | WebView | Bundled Chromium | System WebView (or optional CEF) | | App size | 80–150MB | 20–40MB (120–150MB with CEF) | | Start event loop | app.whenReady() + implicit | await app.whenReady(); app.run() (explicit) |

Quick code translation — Electron to Bunlet:

// Electron
import { app, BrowserWindow } from 'electron';

app.whenReady().then(() => {
  const win = new BrowserWindow({ width: 800, height: 600 });
  win.loadFile('index.html');
});

// Bunlet
import { app, BrowserWindow } from '@bunlet/core';

await app.whenReady();
const win = new BrowserWindow({ width: 800, height: 600 });
win.loadFile('index.html');
app.run();

Key differences:

  • IPC handlers require Zod schemas in Bunlet (type-safe by default)
  • app.run() must be called explicitly to start the event loop
  • Preload scripts use import { contextBridge, ipcRenderer } from '@bunlet/core' (not 'electron')
  • win.webContents.send() and ipcRenderer.on() work the same way

Migrating from Tauri

| | Bunlet | Tauri | |---|---|---| | Backend language | TypeScript (Bun) | Rust | | Frontend | Any (HTML/CSS/JS) | Any (HTML/CSS/JS) | | IPC | Zod-validated, Electron-compatible | Command-based | | App size | 20–40MB | 2–10MB | | CEF/Chromium option | Yes (optional) | No | | Learning curve | Low (TypeScript) | Moderate (Rust + JS) |

Bunlet lets you write your entire backend in TypeScript instead of splitting logic between Rust and JS. If you're already comfortable with TypeScript and Bun, you get the same native WebView behavior as Tauri without learning Rust for your backend logic.


Related packages


License

MIT — see LICENSE.