electron-to-electrobun

Port an Electron app to Electrobun. Runs a compatibility audit first, then migrates IPC, windows, preload, menus, dialogs, and build config to Electrobun equivalents.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "electron-to-electrobun" with this command: npx skills add recrsn/claude-plugins/recrsn-claude-plugins-electron-to-electrobun

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 Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Security

claude-security-settings

No summary provided by upstream source.

Repository SourceNeeds Review
Security

configure-security

No summary provided by upstream source.

Repository SourceNeeds Review
Security

meta-audit

No summary provided by upstream source.

Repository SourceNeeds Review