Docs

Plugins

Capabilities that don't belong in the base API (filesystem access, a hardware integration, anything app- or domain-specific) are added as plugins instead of growing the core package. A plugin is a package (or a local project file) with a main-process half and a preload-side manifest, wired in exactly the same explicit way every built-in feature is — no runtime plugin loader, no magic.

Writing a plugin

Main process (pkg/main.ts) — default-exports a function that registers the plugin's methods with registerPlugin():

import { registerPlugin } from '@devioarts/electron/main';
import type { ElectronConfig } from '@devioarts/electron';

class Filesystem {
  async readFile(root: string, path: string) {
    /* ... */
  }
  async writeFile(root: string, path: string, data: string) {
    /* ... */
  }
}

export default function registerFilesystemPlugin(config: ElectronConfig): void {
  registerPlugin('Filesystem', new Filesystem(), ['readFile', 'writeFile']);
}

registerPlugin(name, instance, methods, events?) registers each method as a plugin:<name>:<method> channel through the same trustedIpcHandleSafe() every core channel uses — callers get the same IpcResult envelope (check result.ok, no try/catch) and sender-trust check for free. Method arguments are forwarded positionally, not as a single options object, matching this package's own bridge style (clipboard.writeText(text), not clipboard.writeText({ text })).

For push-style events, pass an events map and use the emit() function registerPlugin() returns — hooks fire only on the first-subscriber / last-unsubscriber transition, so e.g. a file watcher only runs while someone's actually listening:

const emit = registerPlugin('Watcher', instance, ['start', 'stop'], {
  changed: {
    onAdd: () => watcher.start(),
    onRemove: () => watcher.stop(),
  },
});
watcher.on('change', (path) => emit('changed', { path }));

Preload (pkg/preload.ts) — default-exports a plain manifest. No Node or Electron imports here, so it's always safe to bundle into the renderer even though the main half touches the filesystem:

import type { PluginManifest } from '@devioarts/electron/preload';

export default {
  name: 'Filesystem',
  methods: ['readFile', 'writeFile'],
} satisfies PluginManifest;

Add events: ['changed'] to the manifest to also get a window.Electron.Filesystem.on(eventType, callback) subscription method for free.

Config — a plugin that needs its own config declares it once via TypeScript declaration merging:

// pkg/types.ts
declare module '@devioarts/electron' {
  interface PluginConfigMap {
    filesystem: { roots?: Record<string, string> };
  }
}

The app triggers the merge by importing that module once from its own electron-env.d.ts (the same step used to extend ElectronBridge, see below). config.plugins?.filesystem is then fully typed inside registerFilesystemPlugin(config) — no cast needed.

Types — to make window.Electron.Filesystem typed in the app, the plugin extends ElectronBridge:

declare module '@devioarts/electron' {
  interface ElectronBridge {
    Filesystem: {
      readFile(root: string, path: string): Promise<IpcResult<string>>;
      writeFile(root: string, path: string, data: string): Promise<IpcResult<void>>;
    };
  }
}

Installing a plugin

Two ways a plugin ends up wired into electron/main.ts / electron/preload.ts:

  • Auto-detected — add "dae": { "plugin": true } to the plugin package's package.json, npm install it, then run npx dae sync. It scans declared dependencies for the marker and regenerates electron/plugins/generated/{main,preload}.ts — don't hand-edit these; re-run dae sync after installing or removing a plugin.
  • Hand-registered — anything without the marker (a third-party package that doesn't carry it, or a plugin file local to your project). Add the same registerPlugin() call and manifest by hand to electron/plugins/user/{main,preload}.ts (scaffolded by dae init, never touched by dae sync).

Both are merged and passed into createElectronApp / createElectronBridge by the scaffolded electron/main.ts / electron/preload.ts — nothing else to wire up.

Last updated on July 16, 2026