npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

electron-message-bridge

v0.4.5

Published

A small, typed, zero-boilerplate Electron IPC helper. Full end-to-end type inference across main, preload, and renderer.

Downloads

719

Readme

electron-message-bridge

CI CodeQL GitHub Release npm npm downloads license


Introduction

A small, typed, zero‑boilerplate Electron IPC library that gives you a clean, safe, and fully typed IPC layer without the usual wiring or runtime overhead. It provides end‑to‑end type inference across main, preload, and renderer—no any leakage, no manual channel plumbing, and no hidden abstractions.

Why this library?

  • Zero boilerplate — abstracts all IPC setup for main, preload, and renderer.
  • Fully typed — complete type inference across processes with friendly TS autocompletion.
  • Context‑isolation ready — works seamlessly with sandboxing and secure preload bridges.
  • Decoupled design — channel names and handler logic stay separate for maintainability and security.
  • Composable — define multiple APIs and event sets without conflicts.
  • Safe by default — guaranteed cleanup of handlers and listeners to prevent memory leaks.
  • Hot‑reload friendly — explicit dispose() methods and Vite HMR integration.
  • Lightweight — minimal runtime dependencies, minimal API surface, and near-zero runtime cost beyond Electron’s IPC.
  • Framework‑agnostic — works with React, Vue, Svelte, Solid, vanilla JS, or anything else.
  • Well‑tested — unit tests cover core behavior and edge cases, with CI to prevent regressions.

Modular by design

Use only what you need:

  • Core IPC helper — import from the root package.
  • Optional modules — import separately when needed:
    • integrations — helpers for Electron modules like dialog and shell.
    • menus — declarative menu definitions.
    • appkit — common app utilities.
    • lifecycle — child‑process lifecycle helpers.

Nothing outside the core is required.

The core package (electron-message-bridge) is published from the repository root. Optional adapters and plugins are maintained as separate workspace packages under packages/* and can be installed independently.

For optional adapters, install the adapter package directly. The library ships thin shims under electron-message-bridge/adapters/* that lazily import the adapter package and throw a helpful AdapterMissingError if the package is not installed.

Optional adapters

This project provides several optional transport adapters you can install when you need to expose handlers over different IPC mechanisms.

  • AssemblyScript / WebAssembly adapter — @electron-message-bridge/adapter-assemblyscript

    Install:

    pnpm add @electron-message-bridge/adapter-assemblyscript
    # or: npm install @electron-message-bridge/adapter-assemblyscript

    Usage (shim import):

    const { createAssemblyScriptAdapter } = await import('electron-message-bridge/adapters/assemblyscript');
    // or import directly from the adapter package:
    // import { createAssemblyScriptAdapter } from '@electron-message-bridge/adapter-assemblyscript';
  • gRPC adapter — @electron-message-bridge/adapter-grpc (requires @grpc/grpc-js)

    Install:

    pnpm add @electron-message-bridge/adapter-grpc @grpc/grpc-js

    Usage:

    const { createGrpcServerTransport } = await import('electron-message-bridge/adapters/grpc');
    // or import from the adapter package directly:
    // import { createGrpcServerTransport } from '@electron-message-bridge/adapter-grpc';
  • Named Pipe / Unix socket adapter — @electron-message-bridge/adapter-named-pipe

    Install:

    pnpm add @electron-message-bridge/adapter-named-pipe

    Usage:

    const { createNamedPipeServerTransport } = await import('electron-message-bridge/adapters/named-pipe');
    // or import directly from the adapter package:
    // import { createNamedPipeServerTransport } from '@electron-message-bridge/adapter-named-pipe';
  • stdio (stdin/stdout) adapter — @electron-message-bridge/adapter-stdio

    Install:

    pnpm add @electron-message-bridge/adapter-stdio

    Usage:

    const { createStdioServerTransport } = await import('electron-message-bridge/adapters/stdio');
    // or import directly from the adapter package:
    // import { createStdioServerTransport } from '@electron-message-bridge/adapter-stdio';

Notes:

  • The lightweight shims under electron-message-bridge/adapters/* keep bundlers and consumers happy by only dynamically importing the adapter package at runtime.
  • If you try to use a shim without installing the adapter package, you'll get an AdapterMissingError that tells you which package to install.

Perfect for…

  • New Electron apps that want a clean, maintainable IPC layer from day one.
  • Existing apps that want to gradually replace manual ipcMain/ipcRenderer code with a typed, safer alternative.
  • Teams that value type safety, predictable architecture, and minimal boilerplate.

What you get

  • A tiny, focused API that’s easy to learn.
  • Strong TypeScript guidance everywhere.
  • Cleaner main and preload scripts—focused on app logic, not IPC wiring.
  • Fewer IPC bugs, less maintenance overhead, and a more scalable architecture.

Features

| Feature | Description | |---|---| | defineIpcApi | Register typed request/response handlers in the main process | | exposeApiToRenderer | Bridge the API to the renderer via contextBridge | | defineIpcEvents | Define typed push events (main → renderer) | | exposeEventsToRenderer | Subscribe to push events with built-in cleanup | | exposeValues | Expose static read-only constants to the renderer | | menus subpath | Load declarative JSON/YAML menus and build Electron templates | | appkit subpath | Glue IPC + integrations + menus setup in one place | | lifecycle subpath | Supervise child process lifecycle with restart and readiness checks | | dispose() | Remove all registered handlers from ipcMain |


Installation

pnpm add electron-message-bridge

electron must be installed separately as a peer dependency.


Docker integration mock (optional)

This repo ships a lightweight backend mock image for integration testing.

pnpm run docker:mock:up

The mock service listens on http://localhost:4010.

Stop and clean up:

pnpm run docker:mock:down

Full details: docs/docker-integration.md.

Run the integration suite:

pnpm run test:integration

Quick start

1 — Define the API in the main process

// src-electron/api.ts
import { defineIpcApi } from 'electron-message-bridge';
import { db } from './db';

export const api = defineIpcApi({
  getUser:      async (id: string)       => db.users.findById(id),
  saveSettings: async (s: UserSettings)  => db.settings.save(s),
  ping:         async ()                 => 'pong' as const,
});

2 — Define push events in the main process

// src-electron/events.ts
import { defineIpcEvents } from 'electron-message-bridge';

export const events = defineIpcEvents({
  backendReady:   (_code: number)                            => {},
  folderSelected: (_path: string)                            => {},
  backendCrashed: (_code: number | null, _sig: string | null) => {},
});

3 — Bridge everything in the preload script

// preload.ts
import { exposeApiToRenderer, exposeEventsToRenderer, exposeValues } from 'electron-message-bridge/preload';
import { api }    from './api';
import { events } from './events';

// window.api  — typed request/response methods
exposeApiToRenderer(api);

// window.events — typed push-event subscriptions
exposeEventsToRenderer(events);

// window.meta — static constants (no Node.js leakage)
exposeValues({ platform: process.platform }, 'meta');

4 — Augment Window in the renderer

// renderer.d.ts
import type { api }    from '../src-electron/api';
import type { events } from '../src-electron/events';
import type {
  ExtractRendererApi,
  ExtractRendererEvents,
} from 'electron-message-bridge';

declare global {
  interface Window {
    api:    ExtractRendererApi<typeof api>;
    events: ExtractRendererEvents<typeof events>;
    meta:   { platform: NodeJS.Platform };
  }
}

5 — Call from the renderer

// Any renderer file — fully typed, no IPC boilerplate
const user = await window.api.getUser('42');
//    ^? { id: string; name: string }

const unsub = window.events.folderSelected((path) => {
  console.log('folder opened:', path);
});

// Clean up when the component unmounts
unsub();

API reference

defineIpcApi(handlers) — main process

Registers each key of handlers as an ipcMain.handle channel.

import { defineIpcApi } from 'electron-message-bridge';

const api = defineIpcApi({
  myMethod: async (arg: string) => `hello ${arg}`,
});

// Dispose (remove all handlers) when done
api.dispose();

| Parameter | Type | Description | |---|---|---| | handlers | Record<string, (...args) => Promise<any>> | Handler object — every value must be an async function |

Returns an IpcApi<T> handle. The _channels array is frozen; no new channels can be injected after creation.

Safety

  • The IpcMainInvokeEvent is never forwarded to your handlers.
  • Channel names are derived solely from object keys at call time.

api.dispose() — main process

Calls ipcMain.removeHandler for every channel registered by this IpcApi. Idempotent; safe to call multiple times.

// Useful in Vite hot-reload setups
if (import.meta.hot) {
  import.meta.hot.accept(() => api.dispose());
}

defineIpcEvents(schema) — main process

Declares a set of typed push events. Schema values are descriptor functions — they are never called; they exist only so TypeScript can infer parameter types.

import { defineIpcEvents } from 'electron-message-bridge';

const events = defineIpcEvents({
  backendReady:   (_code: number)   => {},
  folderSelected: (_path: string)   => {},
});

// Send a push event to a BrowserWindow
events.emit(browserWindow, 'backendReady', 0);

| Method | Description | |---|---| | events.emit(win, channel, ...args) | Sends webContents.send(channel, ...args) — fully type-checked |


exposeApiToRenderer(api[, key]) — preload

Exposes the typed request/response API to the renderer via contextBridge.exposeInMainWorld.

exposeApiToRenderer(api);          // → window.api
exposeApiToRenderer(api, 'myApp'); // → window.myApp

exposeEventsToRenderer(events[, key]) — preload

Exposes typed push-event subscription functions to the renderer. Each exposed function returns an unsubscribe callback.

exposeEventsToRenderer(events);           // → window.events
exposeEventsToRenderer(events, 'notify'); // → window.notify

Renderer usage:

const unsub = window.events.backendReady((code) => {
  console.log('ready, exit code:', code);
});

// Remove the listener (prevents memory leaks)
unsub();

The IpcRendererEvent injected by Electron is stripped before your callback receives its arguments.


exposeValues(values, key) — preload

Exposes a plain object of static serialisable values to the renderer without leaking any Node.js globals.

import { app } from 'electron';

exposeValues(
  { platform: process.platform, version: app.getVersion() },
  'meta',
);
// Renderer: window.meta.platform, window.meta.version

Composing multiple APIs

If your app has separate feature areas, define each with its own defineIpcApi call and expose them under different keys:

// preload.ts
exposeApiToRenderer(userApi,     'userApi');
exposeApiToRenderer(settingsApi, 'settingsApi');
exposeEventsToRenderer(appEvents, 'appEvents');
// renderer.d.ts
interface Window {
  userApi:     ExtractRendererApi<typeof userApi>;
  settingsApi: ExtractRendererApi<typeof settingsApi>;
  appEvents:   ExtractRendererEvents<typeof appEvents>;
}

Declarative Menus (JSON/YAML)

Use the optional menus module when you want to define Electron menus in config files and map menu actions to app callbacks.

Example config/menu.yaml:

items:
  - label: File
    submenu:
      - label: Open...
        accelerator: CmdOrCtrl+O
        actionId: file.open
      - type: separator
      - role: quit
  - label: Help
    submenu:
      - label: Documentation
        actionId: help.docs
import {
  applyApplicationMenuFromFile,
  buildMenuTemplate,
  loadMenuSpecFromFile,
} from 'electron-message-bridge/menus';

const spec = await loadMenuSpecFromFile('config/menu.yaml');

const commands: Record<string, () => void> = {
  'file.open': () => {
    // open file flow
  },
  'help.docs': () => {
    // open docs URL
  },
};

const template = buildMenuTemplate(spec.items, {
  commands,
  onAction: (actionId) => {
    // optional global hook (logging/analytics)
    console.log('menu action:', actionId);
  },
});

// Or do all steps at once:
await applyApplicationMenuFromFile('config/menu.yaml', {
  commands,
  onAction: (actionId) => {
    console.log('menu action:', actionId);
  },
});

AppKit (Optional Glue Layer)

Use electron-message-bridge/appkit when you want one setup flow that composes core IPC, optional integrations, and optional menus.

// main.ts
import { setupMainAppKit } from 'electron-message-bridge/appkit';

const appkit = await setupMainAppKit({
  apiHandlers: {
    ping: async () => 'pong' as const,
  },
  eventSchema: {
    ready: (_code: number) => {},
  },
  dialogs: true,
  shell: true,
  menu: {
    filePath: 'config/menu.yaml',
    commands: {
      'file.open': () => {
        console.log('open requested');
      },
    },
  },
});

// Later at shutdown/hot-reload
appkit.dispose();
// preload.ts
import { setupPreloadAppKit } from 'electron-message-bridge/appkit';

setupPreloadAppKit({
  api: appkit.api,
  events: appkit.events,
  values: { platform: process.platform },
  dialogs: true,
  shell: true,
});

Child Process Lifecycle

Use electron-message-bridge/lifecycle to supervise a backend process from the main process with optional readiness checks and bounded auto-restarts.

import { ChildProcessLifecycle } from 'electron-message-bridge/lifecycle';

const lifecycle = new ChildProcessLifecycle({
  command: 'dotnet',
  args: ['run', '--project', 'MyBackend'],
  readyCheck: async () => {
    // Replace with your own health check (pipe/socket/http probe).
  },
  maxRestarts: 3,
  restartDelayMs: 1_000,
});

lifecycle.on('ready', () => {
  console.log('backend ready');
});

lifecycle.on('crashed', (info) => {
  console.warn('backend crashed', info.code, info.signal);
});

lifecycle.on('failed', (reason) => {
  console.error('backend failed permanently', reason.message);
});

await lifecycle.start();

// Later during shutdown:
await lifecycle.stop();

Hot-reload / Vite integration

Call dispose() to remove handlers before the module is replaced:

// api.ts (main process, Vite HMR setup)
export const api = defineIpcApi({ /* ... */ });

if (import.meta.hot) {
  import.meta.hot.dispose(() => api.dispose());
}

Security

  • contextIsolation: true and sandbox: true are fully supported.
  • ipcRenderer is never exposed to the renderer.
  • Channel names are derived from object keys at registration time. Dynamic or injected channel strings are not possible.
  • IpcMainInvokeEvent and IpcRendererEvent are stripped before user code sees them.

Contributing

pnpm install       # install deps
pnpm run check     # lint + typecheck
pnpm run lint      # ESLint
pnpm run typecheck # tsc --noEmit
pnpm test          # vitest run
pnpm run build     # tsup

All four checks must pass on every pull request (enforced by CI).


GitHub workflows

| Workflow | File | Trigger | What it does | |---|---|---|---| | CI | .github/workflows/ci.yml | Push and pull request on main | Runs static checks (lint, typecheck), tests (Node 18/20/22), and build artefact verification | | CodeQL | .github/workflows/codeql.yml | Push, pull request on main, weekly schedule | Runs GitHub CodeQL with security-extended queries for JavaScript/TypeScript |

CI pipeline layout

  1. check job: pnpm run check (lint + typecheck + type tests).
  2. test job: pnpm test on Node 18, 20, and 22.
  3. build job: pnpm run build and verifies expected files in dist/.

The typecheck script uses tsconfig.typecheck.json and is scoped to library code (src/, tests/) so example app files do not block package publishing CI.

Dependabot

Dependabot configuration lives in .github/dependabot.yml.

  • Weekly updates for npm dependencies.
  • Weekly updates for GitHub Actions.
  • Groups development dependency updates to reduce PR noise.
  • Keeps electron pinned for manual review before upgrades.

Local checks

Run these before opening a pull request:

pnpm run check
pnpm test
pnpm run build

Note on impact

In a typical setup with 3 request/response IPC methods and 3 push events, electron-message-bridge usually removes around 35 lines of IPC boilerplate and reduces IPC maintenance surface by roughly 70%.