git clone https://github.com/ComeOnOliver/skillshub
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/recrsn/agent-skills/electron-to-electrobun" ~/.claude/skills/comeonoliver-skillshub-electron-to-electrobun && rm -rf "$T"
skills/recrsn/agent-skills/electron-to-electrobun/SKILL.mdElectron → 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:
options (BrowserWindow,title,titleBarStyle), methods (transparent,setTitle,close,focus/minimize,isMinimized/maximize/unmaximize,isMaximized/setFullScreen,isFullScreen/setAlwaysOnTop,isAlwaysOnTop,setPosition), events (setSize,resize), menu props (focus,role,label,type,enabled,checked),submenu,GlobalShortcut,Screen/Session. Tables below only list differences. Caveat:Cookies→getPosition()not{x,y};[x,y]→getSize()not{width,height}.[w,h]
Quick reference
| Electron | Electrobun |
|---|---|
| Node.js (V8) | Bun (JavaScriptCore) |
| Bundled Chromium | System WebView (WebKit/WebView2/WebKitGTK) or CEF |
/ | / |
/ | RPC messages (fire-and-forget) |
| preload + contextBridge | Typed RPC (preload supported, not for IPC bridging) |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| (OS keychain, key-value) |
| |
| electron-builder/forge | (built-in, BSDIFF patches) |
/ custom protocol | |
CSS | CSS class |
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
electrobun.config.tsimport 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 |
|---|---|
,,, | |
| (top-level, accepts , remote URL, inline JS) |
| N/A (never available) |
| N/A (always isolated) |
| ( prefix for persistence) |
| |
/ | No equivalent (use CSS + ) |
| No equivalent (system default only) |
| N/A | (disables 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 |
|---|---|
| |
| |
| → |
| |
| (all moves here) |
| (no args) |
| N/A | |
BrowserWindow events (differences only):
| Electron | Electrobun | Data |
|---|---|---|
| | |
/ | | |
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 |
|---|---|
| |
| |
| |
| |
| (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 |
|---|---|
| + |
| (just key, modifier auto-applied) |
| |
| |
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
; keep polyfills/globalsipcRenderer - Channel names — RPC keys must be valid JS identifiers, rename
consistently:./
→safeStorage
— key-value, not encrypt/decryptBun.secrets
→ must become RPC requestsremote- Native modules — may need Bun-compatible alternatives
- Multi-window — each window type needs its own RPC schema
- Draggable regions — CSS property → CSS class