@enclosurejs/platform-electron
v1.1.0
Published
> [!IMPORTANT] > This package bridges `@enclosurejs/core` to Electron. It provides `ElectronBackend` (the `BackendAdapter`), a preload helper, a main-process IPC setup function, and concrete implementations for all 26 capability contracts. Your applicatio
Readme
@enclosurejs/platform-electron — Electron Backend + 26 Capability Implementations
[!IMPORTANT] This package bridges
@enclosurejs/coreto Electron. It providesElectronBackend(theBackendAdapter), a preload helper, a main-process IPC setup function, and concrete implementations for all 26 capability contracts. Your application code never imports this package directly — it consumes capability tokens from@enclosurejs/core.
The Problem
Electron's API surface is split across three processes (main, renderer, preload), connected by ipcMain.handle / ipcRenderer.invoke. Wiring 26 capabilities across this boundary by hand means hundreds of IPC channels, manual serialization, and duplicated boilerplate. If you later want to switch to Tauri, every callsite that touches ipcRenderer must be rewritten.
@enclosurejs/platform-electron solves this by mapping every @enclosurejs/core capability token to an Electron implementation behind a single registration call. The renderer process talks to abstract interfaces via DI; the main process registers IPC handlers via one function call. Switching to Tauri means swapping the import — zero changes to business logic.
Architecture
┌────────────────── Renderer Process ──────────────────┐
│ │
│ App code → context.use(FileSystemToken) │
│ │ │
│ ElectronFileSystem.readTextFile(path) │
│ │ │
│ window.__enclosure.invoke('enclosure:fs:readTextFile')│
│ │ (contextBridge) │
└───────────────┼───────────────────────────────────────┘
│ ipcRenderer.invoke
┌───────────────┼──── Main Process ─────────────────────┐
│ │ │
│ setupElectronMain() │
│ ipcMain.handle('enclosure:fs:readTextFile', ...) │
│ → node:fs/promises.readFile(path, 'utf-8') │
│ │
└───────────────────────────────────────────────────────┘Three layers, each with a single responsibility:
| Layer | File | Runs in | Responsibility |
| ---------------------- | -------------------------------------- | ---------------- | -------------------------------------------------------------- |
| Main setup | @enclosurejs/platform-electron/main | Main process | Register all ipcMain.handle handlers |
| Preload | @enclosurejs/platform-electron/preload | Preload script | Expose window.__enclosure bridge via contextBridge |
| Backend + Services | @enclosurejs/platform-electron | Renderer process | ElectronBackend + 26 service classes that call invoke/on |
How It Works
Main process calls
setupElectronMain(win)— registers ~90ipcMain.handlechannels that map to native Electron APIs, Node.js builtins, and optional native modules.Preload script calls
createEnclosurePreload()— exposeswindow.__enclosurewith three methods:invoke(request/response),on(event subscription),once(one-shot event).Renderer process creates an
ElectronBackendand callsregisterElectronCapabilities(ctx)— all 26 service classes are bound to DI tokens. Each service callswindow.__enclosure.invoke(channel, args)which crosses the process boundary viaipcRenderer.invoke.Application code never touches Electron directly — it calls
context.use(FileSystemToken)and gets aFileSysteminterface. ReplaceElectronBackendwithTauriBackendand every service swaps automatically.
Quick Start
1. Main process (main.ts)
import { app, BrowserWindow } from 'electron';
import { setupElectronMain } from '@enclosurejs/platform-electron/main';
app.whenReady().then(() => {
const win = new BrowserWindow({
webPreferences: {
contextIsolation: true,
preload: path.join(__dirname, 'preload.js'),
},
});
setupElectronMain(win, { preloadPath: path.join(__dirname, 'preload.js') });
win.loadFile('index.html');
});2. Preload script (preload.ts)
import { createEnclosurePreload } from '@enclosurejs/platform-electron/preload';
createEnclosurePreload();3. Renderer process (app.ts)
import { createApp, FileSystemToken } from '@enclosurejs/core';
import { ElectronBackend, registerElectronCapabilities } from '@enclosurejs/platform-electron';
const app = createApp({
backend: new ElectronBackend(),
frontend: {
mount({ container, context }) {
const fs = context.use(FileSystemToken);
fs.readTextFile('/etc/hostname').then((text) => {
container.textContent = text;
});
},
},
container: document.getElementById('app')!,
modules: [
{
id: 'electron-caps',
install(ctx) {
registerElectronCapabilities(ctx.context);
},
},
],
});
await app.start();API
Entrypoints
| Import path | Export | Purpose |
| --------------------------------------- | ----------------------------------- | --------------------------------------------------------------------- |
| @enclosurejs/platform-electron | ElectronBackend | BackendAdapter for renderer process |
| | registerElectronCapabilities(ctx) | Registers all 26 capability tokens in one call |
| | EnclosureElectronBridge | TypeScript interface for window.__enclosure |
| | Electron* (26 classes) | Individual service implementations |
| @enclosurejs/platform-electron/preload | createEnclosurePreload() | Sets up window.__enclosure bridge |
| @enclosurejs/platform-electron/main | setupElectronMain(win?, opts?) | Registers ipcMain.handle handlers; returns boolean (false = quit) |
| | updateMainWindow(win) | Update window reference after re-creation |
| @enclosurejs/platform-electron/register | registerElectronCapabilities(ctx) | Same as main export (direct import) |
ElectronBackend
Implements BackendAdapter from @enclosurejs/core:
| Member | Type | Description |
| ---------- | ----------------------------------------------------------- | --------------------------------------------- |
| platform | 'electron' | Platform identifier |
| invoke | (cmd: string, args?: Record<string, unknown>) => Promise | IPC request/response via ipcRenderer.invoke |
| on | (event: string, handler: (payload) => void) => Disposable | IPC event subscription, idempotent dispose |
| once | (event: string) => Promise | One-shot event, resolves on first message |
registerElectronCapabilities(ctx)
Binds all 26 service classes to their corresponding @enclosurejs/core tokens in a single call. Each token maps to exactly one Electron* class. Throws CoreError if called twice on the same context (token-already-provided).
setupElectronMain(win?, opts?)
Registers ~90 ipcMain.handle channels and acquires a single-instance lock. Returns false when a second instance is detected (caller should quit). On subsequent calls, updates the window reference without re-registering handlers. Accepts an optional preloadPath for child window creation.
IPC Protocol
All IPC channels follow the naming convention enclosure:{capability}:{method}:
enclosure:fs:readTextFile → ipcMain.handle → node:fs/promises
enclosure:dialog:confirm → ipcMain.handle → electron dialog
enclosure:shell:spawn → ipcMain.handle → node:child_process
enclosure:serial:data:{connId} → main → renderer (event stream)
enclosure:window:{id}:resize → main → renderer (event stream)Request flow: renderer → window.__enclosure.invoke(channel, args) → ipcRenderer.invoke → ipcMain.handle → native API → response.
Event flow: main → webContents.send(channel, payload) → ipcRenderer.on → window.__enclosure.on(channel, handler) → service event handler.
Capability Implementations (26)
| Service class | Token | Main-process API |
| ----------------------- | ------------------- | ----------------------------------------- |
| ElectronDialogs | DialogToken | electron.dialog |
| ElectronWindows | WindowToken | BrowserWindow |
| ElectronIpc | IpcToken | ipcMain / webContents.send |
| ElectronFileSystem | FileSystemToken | node:fs/promises |
| ElectronShell | ShellToken | node:child_process |
| ElectronClipboard | ClipboardToken | navigator.clipboard (renderer-side) |
| ElectronNotifications | NotificationToken | new Notification() (renderer-side) |
| ElectronStorage | StorageToken | localStorage (renderer-side, prefixed) |
| ElectronHttp | HttpClientToken | net.fetch (main-process, bypasses CORS) |
| ElectronUpdater | UpdaterToken | electron-updater (dynamic import) |
| ElectronPath | PathToken | Pure JS path operations |
| ElectronSystemInfo | SystemInfoToken | node:os |
| ElectronTray | TrayToken | electron.Tray + Menu |
| ElectronShortcuts | ShortcutToken | electron.globalShortcut |
| ElectronDisplay | DisplayToken | electron.screen |
| ElectronPrint | PrintToken | webContents.print |
| ElectronSerial | SerialToken | serialport (optional dep) |
| ElectronFileWatcher | FileWatcherToken | @parcel/watcher |
| ElectronUsb | UsbToken | usb (optional dep) |
| ElectronDatabase | DatabaseToken | better-sqlite3 (optional dep) |
| ElectronDeepLink | DeepLinkToken | app.setAsDefaultProtocolClient |
| ElectronAppLifecycle | AppLifecycleToken | electron.app + powerMonitor |
| ElectronTheme | ThemeToken | electron.nativeTheme |
| ElectronAutoLaunch | AutoLaunchToken | app.setLoginItemSettings |
| ElectronPowerMonitor | PowerMonitorToken | electron.powerMonitor + Battery API |
| ElectronKeychain | KeychainToken | electron.safeStorage + encrypted JSON |
Configuration
Zero configuration files. The package adapts to Electron's environment automatically:
- Main process:
setupElectronMain(win)— only requires aBrowserWindowreference. OptionalpreloadPathfor child windows. - Renderer process:
registerElectronCapabilities(ctx)— one call, no options. - Services: each service resolves its main-process counterpart by channel name. No env vars, no config objects.
Optional Dependencies
Four capabilities require native npm packages declared as optionalDependencies:
| Package | Capability | Install | Loaded via |
| ------------------ | --------------- | --------------------------- | ------------------ |
| better-sqlite3 | DatabaseService | pnpm add better-sqlite3 | createRequire() |
| serialport | SerialService | pnpm add serialport | dynamic import() |
| usb | UsbService | pnpm add usb | dynamic import() |
| electron-updater | UpdaterService | pnpm add electron-updater | dynamic import() |
These are loaded at runtime via dynamic import() / createRequire(). If not installed, the service throws a descriptive error on first use — the rest of the application works fine.
Required dependency
| Package | Purpose |
| ----------------- | ------------------------------------------------------------- |
| @parcel/watcher | FileWatcherService — native file system watcher (no chokidar) |
Types Exported
Types other packages and application code depend on:
| Type | Used by |
| ------------------------------ | ---------------------------------------------- |
| EnclosureElectronBridge | Preload scripts, custom bridge implementations |
| ElectronBackend | App entry point (renderer) |
| All 26 Electron* classes | Direct use when bypassing DI (not recommended) |
| registerElectronCapabilities | Module install() for DI registration |
| setupElectronMain | Main process entry point |
| updateMainWindow | macOS activate handler |
| createEnclosurePreload | Preload script |
Entrypoint separation keeps process-specific types isolated:
| Import path | Runs in | Imports from Electron |
| --------------------------------------- | ---------------- | --------------------- |
| @enclosurejs/platform-electron | Renderer process | No (bridge only) |
| @enclosurejs/platform-electron/preload | Preload script | Yes (contextBridge) |
| @enclosurejs/platform-electron/main | Main process | Yes (full Electron) |
| @enclosurejs/platform-electron/register | Renderer process | No (bridge only) |
Safety
Process Isolation
- Renderer never imports
electron— all access goes throughwindow.__enclosurewhich is exposed viacontextBridgewithcontextIsolation: true. setupElectronMain()acquires a single-instance lock on first call and returnsfalseif a second instance is detected. Subsequent calls update the window reference without re-registering handlers.updateMainWindow()safely replaces the window reference for macOSactivatere-creation.
Lifecycle Safety
ElectronBackend.on()returnsDisposablewith idempotentdispose()— double-dispose is safe.- All event-based services (IPC, shortcuts, tray, theme, power monitor, file watcher) properly unsubscribe on dispose.
registerElectronCapabilities()throwsCoreErroron double-call — prevents silent token shadowing.
Error Safety
- All services throw
CoreErrorwith codeBRIDGE_UNAVAILABLEwhenwindow.__enclosureis missing, pointing to preload misconfiguration. - Optional dependencies (
better-sqlite3,serialport,usb) are loaded dynamically withtry/catch— missing packages produce clear error messages, not cryptic crashes. ElectronHttpwraps allnet.fetchfailures inCoreErrorwithHTTP_REQUEST_FAILEDcode — including non-Error thrown values.ElectronClipboard.writeImagewraps failures inCoreErrorwithCLIPBOARD_WRITE_IMAGEcode.ElectronStorage.get()returnsundefinedfor corrupt JSON — no throws on data corruption.
Data Safety
ElectronKeychainusessafeStorage.encryptString/decryptString— OS-level encryption (DPAPI on Windows, Keychain on macOS, libsecret on Linux).ElectronDatabaseenables WAL mode by default and runs migrations in a single transaction — partial migration is rolled back.ElectronStorageprefixes all keys with__enclosure_store_— no collision with otherlocalStorageusers.ElectronShell.spawn()shallow-copiesargsandenv— prevents mutation after call.
Deep Link Safety
app.requestSingleInstanceLock()enforces single instance — second instance sends URL viasecond-instanceevent and quits.- URL parsing uses
new URL()with fallback for malformed URLs — no throws on invalid deep link input.
Platform Specifics
Renderer vs IPC Split
Not all services cross the IPC boundary. Three services run entirely in the renderer:
| Service | Renderer API | Why no IPC |
| ----------------------- | --------------------- | -------------------------------------------- |
| ElectronClipboard | navigator.clipboard | Web Clipboard API works in renderer directly |
| ElectronNotifications | new Notification() | Web Notification API, Chromium handles it |
| ElectronStorage | localStorage | Renderer-local, prefixed keys |
Two services are hybrid (renderer + IPC):
| Service | Renderer part | IPC part |
| ---------------------- | -------------------------------------- | ----------------------------------------------------- |
| ElectronAppLifecycle | DOM visibilitychange, focus/blur | quit, relaunch, hide, show, vetoQuit |
| ElectronPowerMonitor | Battery API (navigator.getBattery()) | isOnBatteryPower, getSystemIdleTime, power events |
All other 21 services are pure IPC — every method call crosses ipcRenderer.invoke. ElectronHttp routes through IPC to net.fetch in the main process, bypassing renderer CORS restrictions.
Path — Pure JS, Not Node's path
ElectronPath is a custom JS implementation — it does NOT delegate to Node.js path module via IPC. Platform detection uses navigator.userAgent.includes('Windows') for sep and delimiter. The resolve() method uses a walk-from-the-end algorithm that may diverge from Node's path.resolve for edge cases (UNC paths, extended-length paths).
Storage — localStorage with Prefix
| Detail | Value |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Backend | window.localStorage |
| Key prefix | __enclosure_store_ |
| Persistence | Per-origin, survives restart, NOT shared across windows with different origins |
| size() | Approximate UTF-16 estimate (key.length*2 + value.length*2), not actual on-disk size |
| get() on corrupt JSON | Returns undefined (silent JSON.parse catch) |
| clear() | Removes all prefixed keys, then fires undefined to ALL registered onChange watchers (broadcast-style, not per-key) |
Database — better-sqlite3 via createRequire
| Detail | Value |
| -------------- | -------------------------------------------------------------------------------- |
| Location | {app.getPath('userData')}/databases/{name}.sqlite |
| In-memory | :memory: path |
| WAL mode | Enabled by default (PRAGMA journal_mode = WAL) |
| Migrations | Sorted by version, run in one transaction, PRAGMA user_version tracks progress |
| Loading | createRequire(import.meta.url)('better-sqlite3') — loaded once, cached |
Keychain — Encrypted JSON File
| Detail | Value |
| ----------------- | ---------------------------------------------------------------------------- |
| Location | {app.getPath('userData')}/enclosure-keychain.json |
| Encryption | safeStorage.encryptString / decryptString (DPAPI / Keychain / libsecret) |
| Format | { [service]: { [account]: password } } — vault structure, one file |
| Corrupt vault | Returns {} (silent catch in readVault) |
Deep Link — Single Instance Lock
| Detail | Value |
| ------------------- | --------------------------------------------------------- |
| Single instance | app.requestSingleInstanceLock() — second instance quits |
| Windows/Linux | second-instance event extracts URLs from argv |
| macOS | open-url event handled separately |
| URL parsing | new URL() with fallback for malformed URLs |
File Watcher — @parcel/watcher
| Detail | Value |
| -------------------- | ----------------------------------------------------------------- |
| Backend | @parcel/watcher (native, not chokidar/fs.watch) |
| Rename detection | Correlates close delete+create pairs with matching base filenames |
| Glob filtering | Regex approximation of glob patterns (not full glob semantics) |
| Ignore patterns | Pushed to native watcher layer |
Shell — Raw Byte Streams
ElectronShell.spawn() sends stdout/stderr as Buffer → Uint8Array chunks over per-pid IPC channels (enclosure:shell:stdout:{pid}). This provides raw byte streams, unlike Tauri's line-buffered approach. kill() uses void invoke — rejections are not surfaced.
Shortcuts — Client-Side Registration Map
ElectronShortcuts.isRegistered() checks a renderer-side Map, not the main process or OS. If shortcuts are registered outside this service, isRegistered will return incorrect results.
Theme — Silent Degradation
If window.__enclosure is missing during construction, ElectronTheme initializes with 'light' / 'system' defaults and never subscribes to IPC updates. No error is thrown — the service silently operates with stale data.
AppLifecycle — Dual State Sources
State is driven by both DOM events (visibilitychange, focus, blur) and main-process IPC (enclosure:lifecycle:state for suspend/resume/lock). These can briefly disagree. blur sets state to 'inactive' which may not match OS "background" semantics.
IPC — Fire-and-Forget Sends
ElectronIpc.send() and broadcast() use void invoke — errors from the main process are not returned to the caller. getLastMessage() returns from a renderer-side cache only.
Notifications — Chromium Permissions
ElectronNotifications uses the Web Notification API in the renderer, not Electron's main-process Notification. Behavior depends on Chromium's notification permission model and OS notification settings.
Clipboard — PNG Only for Images
ElectronClipboard.readImage() reads only image/png from the clipboard. Other image formats are not supported. Any failure (permission denied, no image) returns null silently. writeImage() uses ClipboardItem — wraps failures in CoreError.
Deep Link — Silent No-Op Without Bridge
ElectronDeepLink.ensureListener() checks for window.__enclosure — if missing, it returns without subscribing. Handlers registered via onOpen() will never fire. No warning is logged.
Silent .catch(() => {}) Patterns
Several services silently swallow errors from event subscriptions or initial setup. If the bridge is missing or event registration fails, the service continues without the subscription:
| Service | Silent catch location |
| ---------------------- | ---------------------------------------------------------- |
| ElectronTheme | Initial get()/getSource() IPC calls in constructor |
| ElectronDeepLink | ensureListener() — no subscription if no bridge |
| ElectronPowerMonitor | getBattery() catch, battery change setup |
| ElectronIpc | send() / broadcast() — void invoke (fire-and-forget) |
| ElectronShortcuts | register() / unregister() — void invoke |
| ElectronFileWatcher | unwatch() in dispose — void invoke |
Error Wrapping
All services throw CoreError consistently:
| Error code | Thrown by | Trigger |
| ----------------------- | ----------------------------------- | ------------------------------------------- |
| BRIDGE_UNAVAILABLE | All 26 services + ElectronBackend | window.__enclosure missing (preload skip) |
| HTTP_REQUEST_FAILED | ElectronHttp | net.fetch failure or non-Error throw |
| CLIPBOARD_WRITE_IMAGE | ElectronClipboard | ClipboardItem write failure |
| FILE_WATCHER_TIMEOUT | ElectronFileWatcher | waitFor predicate not matched in time |
| SHELL_STDIN_IGNORED | ElectronShell | write() called with stdin: 'ignore' |
Platform-Specific Main Process Behavior
The main-process setup (setupElectronMain) contains OS-specific branches:
| Service | OS branch |
| ------------ | ------------------------------------------------------------------------------------------------------------- |
| AutoLaunch | darwin: openAsHidden via app.setLoginItemSettings({ openAsHidden }); win32: --hidden flag in args |
| AppLifecycle | darwin: app.hide() / app.show(); others: win.minimize() / win.show() |
| Keychain | Encryption backend varies: DPAPI (Windows), Keychain (macOS), libsecret (Linux) |
Comparison with platform-tauri
Key implementation differences between the two platform packages:
| Aspect | Electron | Tauri |
| -------------------- | --------------------------------------- | ------------------------------------------------ |
| Architecture | 3 processes (main, preload, renderer) | 1 JS process (webview) + Rust core |
| Storage backend | localStorage (renderer, prefix-based) | LazyStore (file-based, auto-flushed) |
| Keychain | Encrypted JSON file (safeStorage) | OS native keyring (custom Rust plugin) |
| Database path | {userData}/databases/{name}.sqlite | sqlite:{name}.db (plugin-managed) |
| Shell streams | Raw byte Buffer chunks | Line-buffered text (line + '\n') |
| Path impl | Pure JS, userAgent for OS detection | Pure JS, @tauri-apps/api/path for sep/delim |
| Print | All methods via IPC to main | print() = window.print(); others via Rust |
| Updater feed URL | Configurable at runtime | Fixed in tauri.conf.json (setFeedUrl throws) |
| Deep link | requestSingleInstanceLock() + argv | Plugin-based (@tauri-apps/plugin-deep-link) |
| HTTP | Main-process net.fetch() via IPC | @tauri-apps/plugin-http (Rust-backed fetch) |
| Notifications | Web Notification API (renderer) | @tauri-apps/plugin-notification (native) |
Benchmarks
Not applicable. @enclosurejs/platform-electron is a platform bridge — its performance is dominated by IPC round-trip latency (ipcRenderer.invoke → main → response), which is ~0.1–0.5ms per call in a typical Electron app. The service layer adds negligible overhead (argument serialization, Uint8Array conversion). Real-world performance depends on:
- Electron's IPC serialization overhead (structured clone)
- Main-process handler execution time (native API calls)
- Optional native module performance (
better-sqlite3,@parcel/watcher,serialport,usb)
For production profiling, use Electron's built-in Performance API and Chrome DevTools.
Bundle Size
| Output | File | Size |
| ------------ | ----------------- | --------- |
| Runtime (JS) | index.js | 48.87 KB |
| | register.js | 47.70 KB |
| | main/setup.js | 64.63 KB |
| | preload.js | 0.74 KB |
| Types (DTS) | index.d.ts | 14.52 KB |
| | register.d.ts | 0.24 KB |
| | main/setup.d.ts | 0.70 KB |
| | preload.d.ts | 0.26 KB |
| Total JS | | 161.94 KB |
| Total | | 177.66 KB |
index.js and register.js share most code (26 service classes). main/setup.js contains all ipcMain.handle registrations — the largest file because it includes native API wiring for all 26 capabilities plus single-instance lock, deep link handling, and optional dependency loading. preload.js is minimal — just contextBridge.exposeInMainWorld. All native dependencies (electron, @parcel/watcher, better-sqlite3, serialport, usb, electron-updater) are externalized in the build.
Quality
| Metric | Value |
| --------------------- | ------------------------------------------------------------------------------------- |
| Unit tests | 369 (all pass) |
| Test files | 10 test + 1 mock helper = 11 |
| Source files | 26 services + backend + preload + main setup + register + barrel + ambient types = 32 |
| Dependencies | @enclosurejs/core, @parcel/watcher |
| Optional dependencies | better-sqlite3, electron-updater, serialport, usb |
| Dev dependencies | electron, tsup |
| Coverage thresholds | statements >= 90%, branches >= 85%, functions >= 95%, lines >= 90% |
Quality Layers
Layer 1: STATIC ANALYSIS (every commit)
tsc --noEmit strict mode, zero errors
eslint ESLint 9 flat config, zero warnings
prettier --check formatting
Layer 2: UNIT TESTS (every commit)
369 tests backend, register, 26 services, bridge mock
covers lifecycle, dispose, errors, edge cases
v8 coverage statements >= 90%, branches >= 85%, functions >= 95%, lines >= 90%
Layer 3: BENCHMARKS
N/A platform bridge — performance dominated by Electron IPC
Layer 4: PACKAGE HEALTH
2 deps @enclosurejs/core (workspace), @parcel/watcher (native)
4 optional deps native modules loaded dynamically, graceful failure
tsup build ESM + DTS output, 4 entrypointsFile Structure
packages/platform-electron/
├── src/
│ ├── index.ts ElectronBackend + EnclosureElectronBridge, re-exports
│ ├── optional-deps.d.ts Ambient types for optional native modules
│ ├── preload.ts createEnclosurePreload (contextBridge setup)
│ ├── register.ts registerElectronCapabilities (26 tokens → DI)
│ ├── main/
│ │ └── setup.ts setupElectronMain — all ipcMain.handle registrations
│ ├── services/
│ │ ├── index.ts Barrel for all 26 Electron* service classes
│ │ ├── app-lifecycle.ts ElectronAppLifecycle
│ │ ├── auto-launch.ts ElectronAutoLaunch
│ │ ├── clipboard.ts ElectronClipboard
│ │ ├── database.ts ElectronDatabase
│ │ ├── deep-link.ts ElectronDeepLink
│ │ ├── dialogs.ts ElectronDialogs
│ │ ├── display.ts ElectronDisplay
│ │ ├── file-watcher.ts ElectronFileWatcher
│ │ ├── fs.ts ElectronFileSystem
│ │ ├── http.ts ElectronHttp
│ │ ├── ipc.ts ElectronIpc
│ │ ├── keychain.ts ElectronKeychain
│ │ ├── notifications.ts ElectronNotifications
│ │ ├── path.ts ElectronPath
│ │ ├── power-monitor.ts ElectronPowerMonitor
│ │ ├── print.ts ElectronPrint
│ │ ├── serial.ts ElectronSerial
│ │ ├── shell.ts ElectronShell
│ │ ├── shortcuts.ts ElectronShortcuts
│ │ ├── storage.ts ElectronStorage
│ │ ├── system-info.ts ElectronSystemInfo
│ │ ├── theme.ts ElectronTheme
│ │ ├── tray.ts ElectronTray
│ │ ├── updater.ts ElectronUpdater
│ │ ├── usb.ts ElectronUsb
│ │ └── windows.ts ElectronWindows
│ └── __tests__/
│ ├── _bridge-mock.ts Shared MockBridge for window.__enclosure
│ ├── backend.test.ts 7 tests — ElectronBackend adapter
│ ├── clipboard.test.ts 8 tests — ClipboardService (navigator.clipboard)
│ ├── http.test.ts 24 tests — HttpClient (IPC to net.fetch, errors)
│ ├── notifications.test.ts 8 tests — NotificationService (Web Notification API)
│ ├── path.test.ts 35 tests — PathService (Windows + Unix)
│ ├── register.test.ts 3 tests — registerElectronCapabilities (26 tokens)
│ ├── services-complex.test.ts 130 tests — FS, Database, IPC, Updater, Shortcuts,
│ │ Tray, Serial, USB, Shell, FileWatcher
│ ├── services-ipc.test.ts 56 tests — AutoLaunch, Keychain, Dialogs, Display,
│ │ SystemInfo, Print, DeepLink
│ ├── services-stateful.test.ts 71 tests — Theme, AppLifecycle, Windows, PowerMonitor
│ └── storage.test.ts 27 tests — StorageService (localStorage, onChange)
├── package.json
├── tsconfig.json
├── tsup.config.ts
└── README.mdLicense
MIT
