Electron to electrobun
A collection of agent skills for Claude Code and 40+ other AI coding tools
npx -y skills add recrsn/agent-skills --skill electron-to-electrobunAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Port an Electron app to Electrobun. Runs a compatibility audit first, then migrates IPC, windows, preload, menus, dialogs, and build config to Electrobun equivalents.
SKILL.md
13.1 KB, as published. Nobody here has run it
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:
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