Get the FREE Ultimate OpenClaw Setup Guide →

electron-to-electrobun

Scanned
npx machina-cli add skill recrsn/agent-skills/electron-to-electrobun --openclaw
Files (1)
SKILL.md
13.1 KB

Electron → Electrobun Migration

Two phases: compatibility check, then migration. For mechanical transformations (import rewrites, IPC channel renames, API mappings), prefer JS/TS codemods using jscodeshift or ts-morph over manual edits.

Many Electrobun APIs are identical to Electron: BrowserWindow options (title, titleBarStyle, transparent), methods (setTitle, close, focus, minimize/isMinimized, maximize/unmaximize/isMaximized, setFullScreen/isFullScreen, setAlwaysOnTop/isAlwaysOnTop, setPosition, setSize), events (resize, focus), menu props (role, label, type, enabled, checked, submenu), GlobalShortcut, Screen, Session/Cookies. Tables below only list differences. Caveat: getPosition(){x,y} not [x,y]; getSize(){width,height} not [w,h].

Quick reference

ElectronElectrobun
Node.js (V8)Bun (JavaScriptCore)
Bundled ChromiumSystem WebView (WebKit/WebView2/WebKitGTK) or CEF
ipcMain.handle/ipcRenderer.invokeBrowserView.defineRPC<S>()/Electroview.defineRPC<S>()
webContents.send/ipcRenderer.onRPC messages (fire-and-forget)
preload + contextBridgeTyped RPC (preload supported, not for IPC bridging)
dialog.showOpenDialog()Utils.openFileDialog()
dialog.showMessageBox()Utils.showMessageBox()
Menu.buildFromTemplate()ApplicationMenu.setApplicationMenu([...])
Menu.popup()ContextMenu.showContextMenu([...])
clipboard.*Utils.clipboard*()
new Notification()Utils.showNotification({title,body,subtitle?,silent?})
app.getPath(name)Utils.paths.*
shell.openExternal/openPath/showItemInFolderUtils.openExternal/openPath/showItemInFolder
shell.trashItemUtils.moveToTrash
safeStorage.encrypt/decryptBun.secrets.get/set/delete (OS keychain, key-value)
app.quit()Utils.quit()
electron-builder/forgeelectrobun build (built-in, BSDIFF patches)
file:// / custom protocolviews://viewname/path
CSS -webkit-app-region: dragCSS class electrobun-webkit-app-region-drag

RPC pattern

Key difference: Electron IPC is unidirectional — ipcMain.handle only serves renderer→main, webContents.send only pushes main→renderer. Electrobun RPC is bidirectional in a single schema — both sides can define requests (async call/response) and messages (fire-and-forget) in one type. This eliminates the need for separate IPC channel registrations, event forwarders, and bridge layers. A single defineRPC<Schema>() call on each side replaces ipcMain.handle + webContents.send + contextBridge.exposeInMainWorld + ipcRenderer.invoke + ipcRenderer.on.

Shared schema type:

import type { RPCSchema } from "electrobun/bun";
type MySchema = {
  bun: RPCSchema<{
    requests: { myMethod: { params: { id: string }; response: Result } };
    messages: Record<string, never>;
  }>;
  webview: RPCSchema<{
    requests: Record<string, never>;
    messages: { myEvent: { data: string } };
  }>;
};

bun.requests = renderer→main (returns response). bun.messages = renderer→main (fire-and-forget). webview.requests/messages = main→renderer.

Bun side:

const rpc = BrowserView.defineRPC<MySchema>({ handlers: { requests: {...}, messages: {...} } });
const win = new BrowserWindow({ url: "views://main/index.html", rpc });
win.webview.rpc.myEvent({ data: "hello" });

Webview side:

const rpc = Electroview.defineRPC<MySchema>({ handlers: { requests: {...}, messages: {...} } });
const view = new Electroview({ rpc });
await view.rpc.request.myMethod({ id: "123" });

Phase 1: Compatibility Check

1.1 Scan Electron API usage

Main: import {...} from 'electron' (list all), BrowserWindow (options/methods/events/webContents), ipcMain.handle/.on (channels), webContents.send (channels), dialog.*, Menu.*, Tray, clipboard.*, globalShortcut.*, screen.*, session/cookies, shell.*, Notification, app.getPath(), app.on('ready')/whenReady()/'window-all-closed'/'before-quit', safeStorage, nativeTheme, protocol.registerFileProtocol, autoUpdater/electron-updater

Preload: contextBridge.exposeInMainWorld (remove), ipcRenderer.* (remove). Categorize each file: IPC bridge (remove) vs non-IPC (keep).

Renderer: window.electron/window.api/custom bridge names, remote module

Build: electron-builder/forge config, Vite/webpack electron plugins

Native modules: *.node, node-gyp, prebuild, ffi-napi, better-sqlite3, keytar, node-pty

1.2 Unsupported patterns

remote → RPC requests. webRequest → limited. No equivalent: desktopCapturer, powerMonitor, powerSaveBlocker, TouchBar, crashReporter, vibrancy/visualEffectState (NSVisualEffectView), trafficLightPosition. systemPreferences → limited. nodeIntegration: true → N/A. dialog.showSaveDialog → none yet. @electron/rebuild → Bun native modules.

1.3 Report template

## Compatibility Report
### Direct equivalents
- [ ] BrowserWindow (N instances)
- [ ] IPC: N handles, M sends → RPC
- [ ] Dialogs, Menu, Clipboard, Shortcuts, Screen, Session
### Requires refactoring
- [ ] Preload IPC bridge (N files) → strip
- [ ] Preload non-IPC (N files) → keep
### No equivalent
- [ ] ...
### Effort: N schemas, N preloads, N handlers, N events

STOP. Present report. Enter plan mode and draft a migration plan based on the report. Get user approval before proceeding.


Phase 2: Migration

Type-check after each step.

2.1 Install

bun add electrobun
bun remove electron electron-builder @electron-forge/* electron-devtools-installer @electron/rebuild

Remove electron vite/webpack plugins.

2.2 electrobun.config.ts

import type { ElectrobunConfig } from "electrobun";
export default {
  app: { name: "AppName", identifier: "com.x.app", version: "1.0.0" },
  build: {
    bun: { entrypoint: "src/main/index.ts" },
    views: { main: { entrypoint: "src/renderer/main.tsx" } },
    copy: { "src/renderer/index.html": "views/main/index.html" },
  },
} satisfies ElectrobunConfig;

With Vite: keep for dev, use copy for prod output.

2.3 RPC schemas

Per window type, create schema in src/common/rpc/.

ipcMain.handle(ch)bun.requests.ch. webContents.send(ch)webview.messages.ch. ipcRenderer.invoke(ch)rpc.request.ch(). ipcRenderer.on(ch) → message handler in defineRPC or rpc.addMessageListener.

Schema keys must be valid JS identifiers — rename :, ., / channels.

2.4 Preload

Strip: contextBridge.exposeInMainWorld(), all ipcRenderer, electron imports for these. Keep: polyfills, error handlers, globals, CSS injection, DOM prep. Delete file if only IPC bridge code. Wire kept preloads: preload: "views://main/preload.js". Remove global.d.ts/window.api type declarations.

2.5 Main process

BrowserWindow constructor:

ElectronElectrobun
width,height,x,yframe: {width,height,x,y}
webPreferences.preloadpreload (top-level, accepts views://, remote URL, inline JS)
webPreferences.nodeIntegrationN/A (never available)
webPreferences.contextIsolationN/A (always isolated)
webPreferences.partitionpartition (persist: prefix for persistence)
frame: falsestyleMask: {Borderless:true, Titled:false}
vibrancy / visualEffectStateNo equivalent (use CSS backdrop-filter + transparent: true)
trafficLightPositionNo equivalent (system default only)
N/Asandbox: true (disables RPC), html: "...", rpc

styleMask (macOS): Borderless, Titled, Closable, Miniaturizable, Resizable, UnifiedTitleAndToolbar, FullScreen, FullSizeContentView, UtilityWindow, DocModalWindow, NonactivatingPanel, HUDWindow. titleBarStyle auto-sets styleMask.

BrowserWindow methods (differences only):

ElectronElectrobun
restore()unminimize()
setBounds(rect)setFrame(x,y,w,h)
getBounds()getFrame(){x,y,width,height}
loadURL(url)webview.loadURL(url)
webContentswebview (all webContents.* moves here)
webContents.openDevTools({mode})webview.openDevTools() (no args)
N/Awebview.toggleDevTools()

BrowserWindow events (differences only):

ElectronElectrobunData
'closed''close'{id}
'move'/'moved''move'{id,x,y}

Events also on Electrobun.events.on(name, ...).

App lifecycle:

// Electron:
app.whenReady().then(createWindow);
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
// Electrobun — no 'ready', starts immediately:
createWindow();
// exitOnLastWindowClosed in config. before-quit:
Electrobun.events.on('before-quit', (e) => { e.response = { allow: false }; });

openFileDialog options:

ElectronElectrobun
properties: ['openFile']canChooseFiles: true
properties: ['openDirectory']canChooseDirectory: true
properties: ['multiSelections']allowsMultipleSelection: true
defaultPathstartingFolder
filters: [{extensions:[...]}]allowedFileTypes: "png,jpg" (comma-sep, "*" for all)

Returns string[] directly (not {filePaths, canceled}). showMessageBox options/return identical.

Menus:

// Electron: Menu.setApplicationMenu(Menu.buildFromTemplate(template));
// Electrobun — no buildFromTemplate:
ApplicationMenu.setApplicationMenu([
  { submenu: [{ label: 'Quit', role: 'quit' }] },
  { label: 'Edit', submenu: [{ role: 'undo' }, { role: 'redo' }, { type: 'separator' }, { role: 'cut' }, { role: 'copy' }, { role: 'paste' }, { role: 'selectAll' }] },
]);

Menu item differences:

ElectronElectrobun
click: () => {}action: "string" + Electrobun.events.on('application-menu-clicked', e => e.data.action)
accelerator: "CmdOrCtrl+S"accelerator: "s" (just key, modifier auto-applied)
visible: falsehidden: true
toolTiptooltip

Roles: quit, hide, hideOthers, undo, redo, cut, copy, paste, pasteAndMatchStyle, delete, selectAll, minimize, close, toggleFullScreen, zoom, bringAllToFront, cycleThroughWindows. Separators: {type:"separator"} or {type:"divider"}. Linux: menus unsupported.

Context menus: ContextMenu.showContextMenu([...]) + Electrobun.events.on('context-menu-clicked', ...).

Shell: shell.*Utils.*. trashItemmoveToTrash (no restore metadata on macOS). openExternal/openPath return boolean.

Clipboard: clipboard.*()Utils.clipboard*(). readImage()Uint8Array (PNG) or null. availableFormats()["text","image","files","html"].

Paths: app.getPath(name)Utils.paths.{name}. All sync. userData is app-scoped: {appData}/{identifier}/{channel}. Extra: Utils.paths.config, .cache, .userCache, .userLogs.

Credentials:

// Electron: safeStorage.encryptString(value); safeStorage.decryptString(buffer);
// Electrobun (OS keychain, key-value):
await Bun.secrets.set({ service: "my-app", name: "api-key", value: "secret" });
await Bun.secrets.get({ service: "my-app", name: "api-key" }); // string | null
await Bun.secrets.delete({ service: "my-app", name: "api-key" }); // boolean

Not raw encrypt/decrypt — refactor to key-value by service+name.

GlobalShortcut, Screen, Session/Cookies: Import from "electrobun/bun". Same APIs.

2.6 Renderer

Replace bridge calls: window.api.call(ch, args)rpc.request.ch(args). window.api.on(ch, cb)rpc.addMessageListener('ch', cb).

Per-entrypoint rpc.ts:

import { Electroview } from "electrobun/view";
import type { MySchema } from "../common/rpc/my-schema.js";
export const rpc = Electroview.defineRPC<MySchema>({ handlers: { requests: {}, messages: {} } });
const view = new Electroview({ rpc });

CSS: -webkit-app-region: drag/no-drag → classes electrobun-webkit-app-region-drag/-no-drag.

Remove window.api/window.electron type declarations, global.d.ts/preload.d.ts.

2.7 Build

With Vite: keep for dev, copy maps output in electrobun.config.ts. Without: build.views in config.

2.8 Verify

bun run typecheckelectrobun build → test windows/RPC/events/menus/dialogs → electrobun build --env=stable

Pitfalls

  1. Preload ≠ delete — strip only contextBridge/ipcRenderer; keep polyfills/globals
  2. Channel names — RPC keys must be valid JS identifiers, rename :./ consistently
  3. safeStorageBun.secrets — key-value, not encrypt/decrypt
  4. remote → must become RPC requests
  5. Native modules — may need Bun-compatible alternatives
  6. Multi-window — each window type needs its own RPC schema
  7. Draggable regions — CSS property → CSS class

Source

git clone https://github.com/recrsn/agent-skills/blob/main/skills/electron-to-electrobun/SKILL.mdView on GitHub

Overview

Port an Electron app to Electrobun by running a compatibility audit first, then migrating IPC, windows, preload, menus, dialogs, and build config to Electrobun equivalents. The process favors mechanical transformations with codemods over manual edits to minimize drift.

How This Skill Works

The workflow has two phases: first, a compatibility check to identify Electron-specific APIs that need adaptation; second, a migration that rewrites IPC, window handling, preload usage, menus, dialogs, and build config to Electrobun equivalents. For mechanical changes (import rewrites, IPC channel renames, API mappings), use JS/TS codemods with jscodeshift or ts-morph instead of hand edits.

When to Use It

  • Migrating a large Electron app with extensive IPC to Electrobun using the new RPC model.
  • Porting BrowserWindow options and window lifecycle logic to Electrobun equivalents.
  • Replacing dialog.showOpenDialog, dialog.showMessageBox, and menu patterns with Electrobun utilities.
  • Updating build/packaging config to leverage electrobun build tooling.
  • Refactoring preload and contextBridge usage to a typed RPC schema.

Quick Start

  1. Step 1: Run the compatibility audit to identify Electron-specific APIs that require adaptation.
  2. Step 2: Apply mechanical transformations with codemods (e.g., IPC renames, import rewrites) to migrate to Electrobun equivalents.
  3. Step 3: Update build/config to use Electrobun tooling and thoroughly test renderer and main processes.

Best Practices

  • Run the compatibility audit first to surface Electron-specific APIs that require adaptation.
  • Prefer codemods (jscodeshift/ts-morph) for mechanical rewrites over manual edits.
  • Migrate incrementally by feature/module rather than rewiring the entire app at once.
  • Maintain a single shared RPC schema across renderer and webviews to minimize drift.
  • Write regression tests for critical IPC paths and UI flows after migration.

Example Use Cases

  • Port a note-taking Electron app to Electrobun, converting IPC channels to a BrowserView.defineRPC schema.
  • Migrate a dashboard app that uses menus and dialogs to Electrobun equivalents like Utils.openFileDialog and Utils.showMessageBox.
  • Update a video editor's build configuration to use electrobun build tooling with built-in patches.
  • Convert an app that relies on preload scripts and contextBridge to a typed RPC model for both main and renderer.
  • Port a chat application with main/renderer IPC to Electrobun RPC without changing core UX.

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers