Plugin development Reference

Plugin anatomy & lifecycle

The manifest, entry points, the activate/deactivate lifecycle, and the hot-unload invariant that keeps live reload safe.

A plugin is a folder with a manifest and up to two entry points — one for the Electron main process and one for the renderer. This page documents the manifest fields, where plugins live on disk, how the two entries differ, and the lifecycle contract every plugin follows.

Folder layout

my-plugin/
  muxie-plugin.json     # manifest (required)
  main.cjs              # optional main-process entry (CommonJS)
  renderer.js           # optional renderer entry (plain ESM, no bundler)

Manifest

The manifest is muxie-plugin.json:

{
  "id": "my-plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "description": "…",
  "author": "…",
  "main": "main.cjs",
  "renderer": "renderer.js"
}
FieldNotes
idkebab-case; must equal the folder name
nameDisplay name
versionPlugin version
descriptionShort description
authorAuthor
mainPath to the main-process entry (optional)
rendererPath to the renderer entry (optional)

At least one of main / renderer is required. Entry paths must stay inside the plugin folder.

Install locations

  • Installed plugins live at <userData>/plugins/<id>/.
  • Per-plugin persisted storage is a JSON blob at <userData>/plugin-data/<id>.json (read and written through ctx.storage.get() / ctx.storage.set(v) in the main-process context).

The two entry points

Main entry (main.cjs) is CommonJS, loaded via require. Only the entry file itself is hot-reloaded, so bundle to a single file. It runs with full Node access and receives a main-process context.

Renderer entry (renderer.js) is plain ESM served over the privileged muxie-plugin:// scheme and import()ed by the host. It has no imports — use ctx.react for the app’s React instance (never bundle your own React; two Reacts break hooks). It receives a renderer context.

Tip: These are the files Muxie loads, but you don’t have to write them by hand. Writing plugins in TypeScript shows how to author in .ts with full type-checking and compile down to this exact main.cjs / renderer.js shape.

The activate / deactivate lifecycle

Both entries export an activate(ctx) function and may return an object with a deactivate() method for custom teardown:

export function activate(ctx) {
  // register hooks, slots, commands, surfaces, IPC handlers — all through ctx
  ctx.log("activated");

  return {
    deactivate() {
      // optional: tear down anything ctx didn't track for you
      ctx.log("deactivated");
    },
  };
}

(For a CommonJS main.cjs, export the same shape via module.exports = { activate }.)

The hot-unload invariant

Every registration made through ctx — hooks, slots, commands, IPC handlers, surfaces, the tab-close interceptor — is tracked and disposed automatically when the plugin deactivates. That is what makes live disable / uninstall / reload safe: nothing a plugin added survives it going away.

The rule: register only through ctx. If you create something ctx doesn’t know about (a timer, an event listener on a global, an external resource), tie it to the plugin lifetime yourself with:

ctx.track({ dispose: () => clearInterval(timer) });

Returning a deactivate() is for additional teardown beyond what ctx tracks — it’s not where you undo your ctx registrations, because those are already handled.