electron-to-electrobun
Scannednpx machina-cli add skill recrsn/agent-skills/electron-to-electrobun --openclawElectron → 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:
BrowserWindowoptions (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
| Electron | Electrobun |
|---|---|
| Node.js (V8) | Bun (JavaScriptCore) |
| Bundled Chromium | System WebView (WebKit/WebView2/WebKitGTK) or CEF |
ipcMain.handle/ipcRenderer.invoke | BrowserView.defineRPC<S>()/Electroview.defineRPC<S>() |
webContents.send/ipcRenderer.on | RPC messages (fire-and-forget) |
| preload + contextBridge | Typed 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/showItemInFolder | Utils.openExternal/openPath/showItemInFolder |
shell.trashItem | Utils.moveToTrash |
safeStorage.encrypt/decrypt | Bun.secrets.get/set/delete (OS keychain, key-value) |
app.quit() | Utils.quit() |
| electron-builder/forge | electrobun build (built-in, BSDIFF patches) |
file:// / custom protocol | views://viewname/path |
CSS -webkit-app-region: drag | CSS 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:
| Electron | Electrobun |
|---|---|
width,height,x,y | frame: {width,height,x,y} |
webPreferences.preload | preload (top-level, accepts views://, remote URL, inline JS) |
webPreferences.nodeIntegration | N/A (never available) |
webPreferences.contextIsolation | N/A (always isolated) |
webPreferences.partition | partition (persist: prefix for persistence) |
frame: false | styleMask: {Borderless:true, Titled:false} |
vibrancy / visualEffectState | No equivalent (use CSS backdrop-filter + transparent: true) |
trafficLightPosition | No equivalent (system default only) |
| N/A | sandbox: 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):
| Electron | Electrobun |
|---|---|
restore() | unminimize() |
setBounds(rect) | setFrame(x,y,w,h) |
getBounds() | getFrame() → {x,y,width,height} |
loadURL(url) | webview.loadURL(url) |
webContents | webview (all webContents.* moves here) |
webContents.openDevTools({mode}) | webview.openDevTools() (no args) |
| N/A | webview.toggleDevTools() |
BrowserWindow events (differences only):
| Electron | Electrobun | Data |
|---|---|---|
'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:
| Electron | Electrobun |
|---|---|
properties: ['openFile'] | canChooseFiles: true |
properties: ['openDirectory'] | canChooseDirectory: true |
properties: ['multiSelections'] | allowsMultipleSelection: true |
defaultPath | startingFolder |
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:
| Electron | Electrobun |
|---|---|
click: () => {} | action: "string" + Electrobun.events.on('application-menu-clicked', e => e.data.action) |
accelerator: "CmdOrCtrl+S" | accelerator: "s" (just key, modifier auto-applied) |
visible: false | hidden: true |
toolTip | tooltip |
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.*. trashItem → moveToTrash (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 typecheck → electrobun build → test windows/RPC/events/menus/dialogs → electrobun build --env=stable
Pitfalls
- Preload ≠ delete — strip only
contextBridge/ipcRenderer; keep polyfills/globals - Channel names — RPC keys must be valid JS identifiers, rename
:./consistently safeStorage→Bun.secrets— key-value, not encrypt/decryptremote→ must become RPC requests- Native modules — may need Bun-compatible alternatives
- Multi-window — each window type needs its own RPC schema
- 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
- Step 1: Run the compatibility audit to identify Electron-specific APIs that require adaptation.
- Step 2: Apply mechanical transformations with codemods (e.g., IPC renames, import rewrites) to migrate to Electrobun equivalents.
- 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.