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"
}
| Field | Notes |
|---|---|
id | kebab-case; must equal the folder name |
name | Display name |
version | Plugin version |
description | Short description |
author | Author |
main | Path to the main-process entry (optional) |
renderer | Path 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 throughctx.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
.tswith full type-checking and compile down to this exactmain.cjs/renderer.jsshape.
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.