@mariodebono/di-electron
v3.4.0
Published
Electron DI app bootstrapper
Maintainers
Readme
@mariodebono/di-electron
Electron runtime support for applications built on @mariodebono/di.
This package bootstraps an Electron main process, manages the main window, registers controller-based IPC handlers, injects the preload bridge, and coordinates lifecycle hooks for startup and shutdown.
Use @mariodebono/di-electron for main-process code and @mariodebono/di-electron/renderer for the typed renderer bridge.
Install
npm install @mariodebono/di-electron @mariodebono/di electron reflect-metadataRequirements
- Node.js 24 or newer
- An ESM-capable Electron main-process build using Electron
^42,^43, or^44 - TypeScript legacy decorators enabled when using decorator APIs
reflect-metadataloaded before decorated classes are instantiated@mariodebono/diandelectronas peer dependencies
The package is developed against Electron 44.4.5, but applications can continue using any supported peer version. Electron 44 requires macOS 13 or newer and no longer supports 32-bit Windows or Linux armv7l.
This package publishes ESM .mjs files, .d.mts declarations, and a CommonJS preload bundle used internally by the window manager. CommonJS require() is not a supported public entry point.
TypeScript Setup
Use the same decorator setup required by @mariodebono/di:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Load reflect-metadata once in the Electron main-process entry before importing decorated application modules.
What It Provides
createElectronApplication()for DI-backed Electron bootstrapElectronModulewith Electron-specific servicesWindowManagerServicefor main-window tracking and additional windowsElectronAppServiceas a thin injectable facade over Electron app APIs- decorator-driven app-ready, app-launch, and window lifecycle hooks
- controller-based IPC registration through
BridgeController()and typed handle decorators IpcErrorhelpers for renderer-safe error payloadsElectronLoggerintegration through the built-in logger adapter
Quick Start
Main-process entry:
import "reflect-metadata";
import { Module } from "@mariodebono/di";
import {
BridgeController,
createElectronApplication,
createIpcHandleTyped,
ElectronAppService,
} from "@mariodebono/di-electron";
type AppBridge = {
getLocale(): Promise<string>;
};
const AppBridgeHandle = createIpcHandleTyped<AppBridge>();
@BridgeController({ namespace: "app" })
class AppBridgeController {
constructor(private readonly electronApp: ElectronAppService) {}
@AppBridgeHandle("getLocale")
getLocale(): string {
return this.electronApp.getLocale();
}
}
@Module({
providers: [AppBridgeController],
})
class AppModule {}
const result = await createElectronApplication(AppModule, {
appName: "My App",
desktopName: "com.example.MyApp.desktop",
mainWindowOptions: {
url: "http://localhost:5173",
width: 1280,
height: 800,
},
});
if (result.status === "started") {
console.log("Electron app started");
}Renderer code:
import { createRendererBridge } from "@mariodebono/di-electron/renderer";
type AppBridge = {
getLocale(): Promise<string>;
};
const api = createRendererBridge<{ app: AppBridge }>();
const locale = await api.app.getLocale();The renderer bridge uses the preload transport that @mariodebono/di-electron injects into windows created by WindowManagerService.
Bootstrap Model
createElectronApplication() wraps your entry module in ElectronModule, creates the DI application, and wires the result to Electron.
At runtime it:
- sets the configured internal Electron application name and Linux desktop identity
- creates the DI application
- registers all
BridgeController()handlers withipcMain.handle() - collects app-ready, app-launch, and window lifecycle handlers
- installs a minimal default macOS app menu
- waits for
app.whenReady() - runs
@AppReady({ order: AppReadyOrder.BeforeWindow }) - creates the main window through
WindowManagerService - runs
@AppReady({ order: AppReadyOrder.AfterWindow }) - dispatches the initial
@OnAppLaunch()callbacks - attaches main-window close, focus, blur, show, and app-quit listeners
If instanceMode: "single" is enabled and Electron cannot acquire the single-instance lock, bootstrap exits early and returns { status: "redirected" }. Otherwise it returns { status: "started", application }.
Application Options
createElectronApplication() accepts the normal @mariodebono/di application options plus Electron-specific settings.
appName?: stringsets Electron's internal application name before bootstrap begins; it does not configure OS-level application metadatadesktopName?: stringsets the Linux.desktopfilename before bootstrap so the XDG application ID andWM_CLASScan match the installed desktop entry; it is ignored on other platformsinstanceMode?: "multi" | "single"controls whether a second app instance is allowedhideOnClose?: booleanhides the main window instead of quitting when it is closedmainWindowOptions?: CreateWindowOptionsconfigures the mainBrowserWindowipcEventValidator?: (event, channel) => void | Promise<void>applies additional validation after the package sender checks passloggerOptions?: ElectronLoggerOptionsconfigures the built-inelectron-logadapter
Defaults:
instanceModedefaults to"multi"hideOnClosedefaults tofalse- the main window defaults to
url: "/"andstartMode: "normal"
Window Management
WindowManagerService is the main window abstraction exposed by the package.
It supports:
createWindow(options)for additional windowscreateMainWindow(options)for the singleton main windowgetMainWindow()to read the tracked main windowrevealMainWindow()to restore, show, and focus the main windowcloseAll()to close all tracked windows
CreateWindowOptions extends Electron's BrowserWindowConstructorOptions with:
url?: stringbasePath?: string | (() => string | Promise<string>)startMode?: "normal" | "minimized" | "hidden"
The package owns webPreferences.preload and injects its preload bridge automatically for every window.
URL resolution rules:
- absolute
http:,https:, andfile:URLs are loaded withloadURL() - relative URLs are resolved against
basePathwhen provided - otherwise the value is passed to
loadFile()
That makes the package suitable for both development servers and packaged files.
Lifecycle Decorators
Lifecycle decorators also mark the class as injectable when needed.
@AppReady()
Runs after app.whenReady().
order: AppReadyOrder.BeforeWindowruns before the main window is createdorder: AppReadyOrder.AfterWindowruns after the main window is createdprioritysorts handlers in ascending order
Handlers are awaited sequentially. Errors are logged and do not stop the remaining handlers from running.
@OnAppLaunch()
Runs when the application launches.
The handler receives:
type AppLaunchContext = {
kind: "initial" | "second-instance";
argv: string[];
workingDirectory?: string;
additionalData?: unknown;
};In instanceMode: "single", second-instance events are queued until initial startup has finished, then replayed in order.
Window and App Hooks
Available decorators:
@OnAppQuit()@OnMainWindowClose()@OnMainWindowFocus()@OnMainWindowBlur()@OnMainWindowShow()
Each accepts:
{
priority?: number;
order?: LifecycleHookOrder; // "before" | "after"
}These hooks are also awaited sequentially and sorted by ascending priority.
Close and Quit Guards
Use @BeforeAppQuit() to check permission before normal application quit, and
@BeforeMainWindowClose() to check permission before closing or hiding the managed
main window. Both accept { priority?: number } and return boolean or
Promise<boolean>. Return true to allow the action or false to cancel it.
import { Inject, Injectable } from "@mariodebono/di";
import {
BeforeAppQuit,
BeforeMainWindowClose,
} from "@mariodebono/di-electron";
@Injectable()
class OperationsService {
busy = false;
}
@Injectable()
class ExitPolicy {
constructor(@Inject(OperationsService) private readonly operations: OperationsService) {}
@BeforeAppQuit()
@BeforeMainWindowClose()
canExit(): boolean {
return !this.operations.busy;
}
}Register both providers in your module. Your application owns the busy state and
any message or confirmation dialog. Set the state before starting critical work,
and clear it in finally when the work finishes or fails. An async guard can await
an application-owned confirmation dialog before returning its decision.
Guards run sequentially in ascending priority and then discovery order. The first
false cancels the action. A thrown error, rejected promise or non-boolean result
is logged and also cancels it. Existing lifecycle hooks keep their notification
semantics; their return values do not cancel anything.
The package prevents the native action synchronously while guards are pending. Repeated requests share the pending check. Quit hooks run only after quit permission is granted; close hooks run only after close permission is granted. A cancelled attempt is not queued: the next request checks permission again. A guard that never settles keeps the action blocked, so guards should return promptly or wait only for a deliberate user decision.
An allowed window close retains hideOnClose behaviour. If it requests application
quit, quit guards must also approve. During an approved application quit, main-window
close guards are skipped to avoid a second permission cycle. Register both guards
when protecting work from both Close and Quit. A close guard alone does not veto Quit.
Normal menu Quit, keyboard Quit and ElectronAppService.quit() use the quit guards.
Force quit, crashes, system shutdown and app.exit() cannot be reliably blocked.
Updater installation needs a separate permission check before quitAndInstall():
Electron can close windows before emitting before-quit for that operation. Keep
recovery mechanisms for interrupted work. See the
Electron application lifecycle.
Applications without guards retain their existing close and quit behaviour.
IPC Controllers
IPC is controller-based. Classes marked with @BridgeController({ namespace }) are discovered from the DI container, then methods marked with @IpcHandleTyped() or a decorator created by createIpcHandleTyped() are registered through ipcMain.handle().
Before a controller method runs, the package verifies that the sender belongs to a window managed by WindowManagerService, that the request comes from its main frame, and that the frame remains on the configured web origin or local renderer file. Invalid senders receive a normalized electron.invalid_ipc_sender error. Use ipcEventValidator only for additional application-specific rules.
import { BridgeController, createIpcHandleTyped } from "@mariodebono/di-electron";
type ProjectsBridge = {
list(): Promise<string[]>;
};
const ProjectsHandle = createIpcHandleTyped<ProjectsBridge>();
@BridgeController({ namespace: "projects" })
class ProjectsBridgeController {
@ProjectsHandle("list")
async listProjects(): Promise<string[]> {
return ["project-a", "project-b"];
}
}Handler results are wrapped in a stable response shape and then unwrapped by the renderer bridge.
IPC Errors
Use IpcError when you want predictable, client-safe error payloads.
import { IpcError } from "@mariodebono/di-electron";
class ProjectNotFoundError extends IpcError {
constructor(projectId: string) {
super("projects.not_found", `Project ${projectId} was not found`);
}
}Helpers exported by the package:
serializeIpcError(error)toClientIpcError(error)isIpcError(value)isSerializedIpcError(value)
Plain thrown errors are normalized into a fallback { type, message } shape.
Renderer Bridge
Use @mariodebono/di-electron/renderer in renderer code.
import { createRendererBridge } from "@mariodebono/di-electron/renderer";
const api = createRendererBridge<{ app: { getLocale(): Promise<string> } }>();
const locale = await api.app.getLocale();The renderer bridge maps nested property access to dotted IPC channels and automatically unwraps successful responses.
The subpath also exports createRendererEvents() and getDefaultRendererTransport() for tests or specialized transports.
Use getPathForFile(file) when a drag-and-drop flow needs the native filesystem path for a browser File. The package-owned preload resolves the path through Electron's webUtils without exposing webUtils directly.
Electron Services
ElectronModule
ElectronModule provides and exports:
WindowManagerServiceElectronAppServiceCloseBehaviorService
You usually do not import ElectronModule manually when you use createElectronApplication(), because it is already composed into the platform root module.
ElectronAppService
ElectronAppService is a thin facade over frequently used Electron APIs. It keeps Electron-specific code injectable and easy to mock in tests.
Examples include:
getAppPath()getPreferredSystemLanguages()getLocale()getPath(name)quit()andexit(code?)setHideOnClose(value)andgetHideOnClose()setApplicationMenu(menu)andclearApplicationMenu()openPath(path),revealPath(path),openExternal(url)showMessageBox(options)andshowOpenDialog(options)hideDock(),showDock(),setDockIcon(icon)onActivate(listener)andoffActivate(listener)showMenu(window, menu, x, y)setName(name)setTheme(symbolColor, color)
Logging
Pass loggerOptions to createElectronApplication() to use the built-in electron-log adapter.
await createElectronApplication(AppModule, {
loggerOptions: {
appName: "my-app",
logFilePath: "/tmp/my-app.log",
fileLevel: "info",
consoleLevel: "debug",
},
});ElectronLoggerOptions supports:
appNamefileLevelconsoleLevellogFilePathmaxSizeconfigure(logger)loggerInstance
If you already have a logger implementation for @mariodebono/di, you can still pass logger directly through the base application options.
Public API
The package exports:
createElectronApplication()and the related bootstrap types- lifecycle decorators and enums
- IPC decorators, controller helpers, and error helpers
ElectronModuleElectronAppServiceWindowManagerServiceCloseBehaviorServiceElectronLoggerandcreateElectronLogger()IpcError,serializeIpcError(),toClientIpcError(),isIpcError(), andisSerializedIpcError()
Renderer Subpath
@mariodebono/di-electron/renderer exports:
createRendererBridge()createRendererEvents()getDefaultRendererTransport()RendererIpcListenerRendererIpcTransportRendererEventBridge
Changelog
See CHANGELOG.md.
License
This package is licensed under MPL-2.0.
