The current version is 0.1.0-alpha.8 (npm i bunmaska installs the latest published alpha). Newest first; still a curated snapshot rather than a per-commit log.
0.1.0-alpha.8
A documentation-versus-reality pass, three real bugs it flushed out, and Bun 1.4. Every claim in the docs was checked against the code; where they disagreed, one of them was fixed. The React starter and the engine catalogue move to alpha.9.
Fixes
-
A
will-quit(orbefore-quit) veto keeps the app alive for real: the native run loop used to be stopped onwill-quitbefore the veto was honoured, so an app that cancelled its own quit stopped receiving native callbacks. The run loop now stops onquit, which only fires once no listener vetoed. -
bunmaska init my-app .works -inittakes[name] [dir], so a named project can scaffold into the current directory. -
A dev restart no longer steals focus from your editor: the respawned app comes up behind whatever is frontmost (macOS, Linux, Windows).
-
bunmaska buildhonoursname,idandiconfrombunmaska.config.ts(flag > config > entry file name). It used to read only the flags. -
Linux builds target the host architecture:
<Name>-linux-arm64.tar.gzand anarm64.debon arm64 hosts, and the.debcarries the app’s ownpackage.jsonversion instead of the framework’s. -
engine.embedon a Linux build is refused with a clear error (it used to silently drop the WebKitGTK dependency without shipping an engine), and--embed-engineis rejected for non-Windows targets. -
bunmaska <command> --helpprints the usage instead of “unknown flag”. -
doctorandengine whichsay “run bun install” when the config cannot be imported, instead of dumping the resolver stack. -
BrowserWindowemitsmaximize/unmaximizeon macOS (derived fromisZoomedacross resizes; AppKit has no zoom notification). -
Menu.setApplicationMenu(null)on Linux tears down the bars of windows that already have one. -
Notificationsilent: trueis honoured on Linux via the freedesktopsuppress-soundhint. -
A
NativeImagemarked as a template renders as a template in the macOS menu barTray. -
MenuPopupOptions,WebPreferences,LoadFileOptions,MouseInputEvent,KeyboardInputEvent,NativeInputEventandDEFAULT_MIME_TYPEare exported frombunmaskaas the docs claimed. -
The renderer build defines
NODE_ENVper command:devbuilds the React development runtime,buildthe production one. Bun 1.4 picks the JSX runtime from that define, so packaged apps stop shipping development React. -
Bunmaska is developed and tested on Bun 1.4.2 (CI on all three platforms); Bun 1.3 still works.
Docs
- The API reference, CLI, building, shipping, engine and parity pages now match the code, per platform; a new errors page documents the
BunmaskaErrorfamily.
Housekeeping
- One spawn helper for the build tools, one accelerator parser, one backend selector for the platform-split modules.
0.1.0-alpha.7
The dev loop, rebuilt - and updates you can actually ship. Plus the API cells the parity page had been admitting to: session.cookies, Linux capturePage, and real macOS window geometry.
Highlights
- Bunmaska owns the renderer build. A
rendererblock inbunmaska.config.ts(entry/outDir/copy) makesbunmaska devrebuild and live-reload on a renderer change instead of restarting the app, andbunmaska buildships the output asrenderer/beside the executable on all three platforms. The output is deliberately one recipe - a classic IIFE bundle (file://blocks ES modules) withNODE_ENV=developmentdefined (Bun emitsjsxDEVregardless of tsconfig; the production React runtime stubs it out). See Building & Distribution. - A dev loop that doesn’t waste saves. Changes are content-hashed, so a no-op save or metadata touch no longer restarts anything; atomic editor saves are never lost (directory rescan with a seeded content baseline); restarts await the old process’s exit (no double windows, no lost single-instance lock); preload edits restart instead of re-injecting the stale script;
dist/is watched so your own bundler’s output live-reloads; a reload after the app quit says “app is not running” instead of pretending.devandrunnow respect the engine pin too. - Window position survives dev restarts. The first window’s bounds persist to
.bunmaska-dev-state.json(the scaffold’s.gitignorecovers it) and are restored on the next start. Strictly dev-only - a packaged app never touches it. - Updates you can actually ship.
bunmaska keygenmints an Ed25519 key pair;build --update --update-keywrites a detached.sigbeside the artifact (skipping the key warns loudly - the runtime refuses unsigned updates);setFeedURLtakes{ url, publicKey, channel }, requires https, and rejects cross-channel or cross-platform manifests; andquitAndInstallperforms a real swap - a detached helper waits for exit, extracts to a temp sibling, rename-swaps, and relaunches. Zip-bomb size caps guard the decompression.AutoUpdaterImplis exported (the docs said so; now it’s true), and the installer stays injectable. The full flow. session.cookieson macOS and Linux.get/set/removewith Electron’s filter semantics. Windows rejects honestly (the WinCairo C API has no cookie read/write entry points), and macOS cannot persisthttpOnly(no public NSHTTPCookie key). See session.capturePageon Linux viawebkit_web_view_get_snapshot- the visible viewport as a PNG-backedNativeImage.- Real macOS window geometry.
getBoundsnow reads the actual on-screen frame from the window server, in global top-left coordinates (Electron’s contract - beating thebun:ffino-struct-return wall viaCGWindowListCopyWindowInfo);setBounds/setPosition/setSizeset the frame in the same space, and amoveevent fires on drags. --notarizeis real. WithAPPLE_ID,TEAM_ID, andBUNMASKA_NOTARIZE_PASSWORDset it zips the signed.app, submits viaxcrun notarytool --wait, and staples the ticket; missing credentials skip with guidance instead of failing the build.
Fixes
import { app } from 'bunmaska/electron'works - the shim now has named exports, as the migration guide always claimed.webContents.sendbefore the first load finishes is queued and delivered ondid-finish-loadon macOS, matching Linux.Menu.setApplicationMenu(null)actually clears the native menu bar (it used to only null the stored reference).Super/Metain accelerators maps to Cmd on macOS (it used to register the bare key).- X11 global shortcuts match their exact modifiers and survive NumLock/CapsLock (all four lock-bit grab variants are registered).
- Windows
getClientSizereads the RECT via the pointer, not the stale JS view of the buffer. - Registering a duplicate ObjC runtime class name throws instead of silently corrupting (nil
objc_allocateClassPair+class_addMethod).
Housekeeping
tools/andscripts/are under the same lint + type-check gates assrc/.- A comment-accuracy and test-suite pass: stale claims corrected, change-detector tests pruned, and the per-OS test budgets ratcheted to each CI leg’s measured numbers.
0.1.0-alpha.6
Foundation: the distribution pipeline, trusted input, and the crash-class + security fixes that have to land before the testing framework does. This is not that testing framework - no coverage gates, skip budgets, or shared harness shipped here.
Highlights
- The signed engine feed is LIVE.
bunmaska engine install <id>takes a bare engine-id, resolves it to the official feed atengines.bunmaska.org(or a configuredengine.feed.urlmirror), verifies the Ed25519 signature against the baked release key and the content hash, and installs it. The first WinCairo engine (webkit-2-2.53.3-bunmaska1-windows-x64) is hosted, and the full loop is proven end to end - download, verify, install, render.bunmaska engine availablelists what the feed publishes (read from a liveindex.json), and the new Engine Repository page covers which engines exist per platform and how to install one. A local engine directory still installs before any feed routing, and an already-installed id is a no-op. See Pinned WebKit Engine. webContents.sendInputEvent(event)- trusted input on Windows. Synthesize a real mouse or keyboard event that the page sees asisTrusted === true, which a script-dispatched event can never fake. Windows only (WinCairo, via Win32 messages); macOS and Linux throwUnsupportedPlatformError. It validates at the boundary - aTypeErroron an unknowntype, on non-finite mousex/y(a typo can’t fire a trusted click at 0,0), or on an emptykeyCode- instead of a silent no-op. Coordinates are client pixels from the view’s top-left, correct at 100% display scale. Honest limits, all follow-ups: no keyboard modifiers yet, synthesized drags don’t carry button state, andKeyboardEvent.code/ scan codes / F1-F24 aren’t wired. See webContents.loadFilegets Electron’s options.webContents.loadFile(filePath, options?)andBrowserWindow.loadFile(...)now accept{ hash, query, search }. The file path is percent-encoded, so names with spaces /#/?load correctly - which means a route fragment goes inoptions.hash, not inside the path. Hash-routed SPAs finally have a supported path. See webContents.- One shared WebKit context on Windows. All windows and views share a single WebKit context, so a login in one window is a login in the next (session / SSO parity), instead of each view starting from an empty jar.
- Smaller Windows binaries. The Windows build minifies whitespace and syntax. It deliberately does not mangle identifiers - that would break
Function.nameand stack traces - so error messages stay readable. - Event-driven macOS run loop. The cooperative pump no longer polls AppKit at a fixed 60 Hz. It sleeps in
CFRunLoopRunInModeuntil a native event arrives - input wakes it instantly - and backs off adaptively when idle. On an idle window that is roughly 10x less CPU (~2.5% > ~0.2%) with no added input latency. One honest trade-off: while the UI is idle, main-process JS timers run at up to ~125 ms granularity (rendererrequestAnimationFrameand IPC are unaffected - they ride the native event path). A true libuv-style integration like Electron’s isn’t possible from purebun:ffitoday - Bun’s loop is uSockets, not libuv, and its tick/wakeup primitives aren’t exported (oven-sh/bun#18546) - so this is the best event-driven behavior achievable while staying single-threaded.
Fixes
- Linux multi-window crash. Closing a window from its title bar ran
disconnectAll(), which freed a GTK signal callback that GTK was mid-return into - a SIGSEGV in any multi-window app. Disconnect is now synchronous and the callback close is deferred a tick. - macOS window use-after-free (and the leak that fix could have caused).
setReleasedWhenClosed:NOstops AppKit from deallocating the window out from under us on close; a deferred, guarded release of theNSWindow+WKWebViewinwillClosekeeps that from turning into a leak. loadFilepercent-encoding. A rawfile://${path}produced a nilNSURLand silently loaded nothing for any path with a space,#, or?; paths are now percent-encoded viapathToFileURL.- Two Windows engine-install bugs. A UTF-8 BOM on
engine.jsonbrokeJSON.parse, and the tar extractor mangled a backslash-Cpath - both fixed, and the previously-skipped Windows remote-install test is unskipped. dom-readyfired twice. It emitted twice per navigation on Windows; now once.- Windows synthesized keystrokes.
charon a named key (Enter, etc.) used to type its first letter (“E”); it now emits the control code or nothing. Keystrokes are also sent past the message queue so the pump’sTranslateMessageno longer synthesizes a duplicateWM_CHAR.
Security
- Engine downgrade / substitution closed. The signature covers the artifact bytes, not the id binding - so a stale or compromised mirror could serve a genuinely release-signed older engine under a newer pinned id, and it verified and ran. The store now binds the install directory to the id inside the signed
engine.jsonand rejects a mismatch. - Reserved store ids rejected. A hostile manifest id like
.linksor__dirlockcould have clobbered the store’s internals;assertSafeEngineIdnow refuses reserved and dotfile ids. - The store lock now guards install-vs-gc. The previously-dead
withLocknow wraps the swap-into-place on install and all ofgc, so a concurrent install can’t race a prune. The slow extract stays outside the lock.
0.1.0-alpha.5
Frameless windows, a real preload, and a dev loop that doesn’t blink.
Highlights
- Custom frameless title bars.
frame: falsewindows get an app-region drag handle and built-in window controls, shared across platforms - native drag on macOS, real controls on Windows. See Frameless Windows. - Preloads can import. The preload is now bundled before injection, so
imports in your preload work instead of silently breakingwindow.api. BrowserWindow.setPosition/setBounds- full on Windows, best-effort on macOS and Linux.- Live reload in dev.
bunmaska devreloads the renderer when assets change instead of restarting the whole app.
Fixes
bunmaska buildskips dotfiles and never copies the build output into itself when collecting runtime assets.- Windows web view is sized to the client area, so content is no longer clipped by the window frame.
- The frameless title-bar script no longer leaks
__bunmaskainto the page world on macOS and Linux.
0.1.0-alpha.4
macOS packaged apps now actually work. Building a real app surfaced four bugs that each broke a double-clickable .app; all four are fixed, so bunmaska build produces a window that opens and responds to clicks and keys.
Fixes
- Windows now appear. A bundled app is brought to the foreground when its first window is shown, not only once at startup before any window exists.
- Apps respond to input. The macOS run loop now dispatches AppKit mouse and keyboard events (
nextEventMatchingMask:/sendEvent:) each tick - the window used to render but ignore clicks. - Built apps no longer crash on launch.
bunmaska buildcopies your runtime assets (the page, the preload, CSS, images) beside the executable, and the scaffold resolves them by the executable’s path when compiled - a compiled binary can’t read files fromimport.meta.dir. - Signed apps no longer trap. Code-signing grants the JIT entitlements Bun needs (
allow-jit,allow-unsigned-executable-memory,disable-library-validation); without them a hardened-runtime app died on its first FFI call.
Known limit
- The macOS run loop is a cooperative ~60 Hz poll - complete and correct, but not yet event-driven (so up to ~16 ms input latency). An event-driven
CFRunLoopintegration is the next focused change.
0.1.0-alpha.3
The first npm release - and Bunmaska became cross-platform on three OSes with a full Windows backend landed on main.
Highlights
-
Published to npm -
npm i bunmaska. -
Win32 + WinCairo WebKit runtime in pure
bun:ffi- native windows + a cooperative message pump, the WinCairo WebKit view, renderer↔main IPC with context isolation, an application menu bar, and the secondary modules: clipboard (text/HTML/images), dialogs, menus, tray, notifications,safeStorage(DPAPI), screen, shell, global shortcuts, power monitor/blocker, native theme, andsession.clearStorageData. Green on awindows-latestCI runner next to macOS and Linux. -
From-source WinCairo engine - we compile WebKit’s WinCairo port from source (a clang-cl build), relocate it into the engine store, and proved a real
BrowserWindowloads + runs JS from the store with no system WebKit (STORE_ENGINE_OK). A reproducible build script + CI workflow ship with it. -
Honest per-platform parity matrix published - every API cell marked full / partial / engine-blocked across macOS, Linux, and Windows.
Caveats (documented, not hidden)
- Engine-blocked on WinCairo: custom
protocol://schemes,printToPDF, andcapturePage- the WinCairo WebKit2 C API exposes no entry point for them. - x64 only (upstream WinCairo is x64-only; ARM64 is on the roadmap). A hosted prebuilt engine is still pending - for now you build + embed the engine (proven) rather than fetch it.
0.1.0-alpha.2
The pinned-WebKit engine store - the opt-in path to “tested == shipped.” Most apps still use the system WebKit by default; this adds the machinery to ship the exact build you tested. See Pinned WebKit Engine.
Highlights
- Side-by-side engine store at
~/.bunmaska/webkit/- content-addressed, many versions coexist, each app resolves its own pin (no global switch). Install marker, content-hash integrity, refcount, and garbage collection. - Launch resolver - env > baked
engine.id> marker check > loud fallback to the system WebKit if a pin is missing (the app still launches). bunmaska engineCLI (list/which/install/use/prune/verify) andbunmaska doctor.- Signed feed install -
engine install <url>verifies an Ed25519 signature + content hash before extracting. - Apps register on launch so
pruneonly collects engines nothing needs. - On Linux, a pinned app loads its WebKit from the store rather than the system soname.
- Fixed: the generated
.debnow declares its WebKitGTK + GTKDepends.
Still in progress
- A self-contained, relocatable WebKit that builds and loads from the store - its full dependency closure travels with it (
$ORIGIN). Next: serving the prebuilt engines from a signed feed + the final render pass. - macOS pinned engine (designed, feasible); engine delivery to end users (embed / auto-fetch). (Windows via WinCairo has since landed - see alpha.3 above.)
0.1.0-alpha.0
The first public alpha. It genuinely works on macOS and Linux (x64 and ARM), with no bundled Chromium and zero compiled native code.
Platforms
- macOS - AppKit +
WKWebViewviaobjc_msgSend. - Linux - GTK 4 + WebKitGTK 6 via
dlopen. - x64 and ARM64 (incl. Raspberry Pi where WebKitGTK 6 is available).
Modules (21)
app · BrowserWindow · webContents · ipcMain / ipcRenderer · contextBridge ·
Menu / MenuItem · dialog · clipboard (incl. images) · Tray · Notification ·
nativeImage · nativeTheme · globalShortcut · shell · protocol · screen ·
powerMonitor · powerSaveBlocker · safeStorage · session · autoUpdater.
See the full API Reference for what each one actually implements, and the parity matrix for the honest gaps.
Highlights
- Real context isolation in a dedicated isolated world on both platforms.
- The CLI:
bunmaska init/dev/run/build. - Packaging to real distributables -
.app/.dmg(macOS), AppDir/.deb(Linux) - plus a pure-BunautoUpdater(check > download > verify > stage). webContents.capturePage+printToPDFandsession.clearStorageData(macOS).
Known limits
- Alpha - the API surface will move between releases. Pin your version.
- Windows is beta (x64, from-source WinCairo - see the roadmap); ARM64 waits on upstream.
- ~70-80% Electron parity;
session.cookiesand somewebContentsevents are still in progress. autoUpdater.quitAndInstall’s final atomic swap-and-relaunch is experimental.
The roadmap is the forward-looking companion to this page: every stop between here and beta, with exit criteria.