5
Plugins
meleki edited this page 2026-09-23 13:59:23 +02:00

A plugin extends the editor. It is one JavaScript file — an ES module — in data/plugins/ on the server, and the editor loads the ones the open project asks for. v1 is deliberately small: it offers what the demo plugin needs (read the assets, add a tab, show nodes on a board) and nothing it does not.

Where plugins come from. A file is put into data/plugins/ by hand; there is no upload yet (#116). A file put there is off in every project until someone switches it on in the list of plugins (#114, #119), which Menu → Plugins opens. The plugins that ship with the repository live in plugins/ and are copied to data/plugins/ by npm run build and by tools/seed-e2e.mjs; a file of the same name is replaced, anything else in the folder is left alone. data/plugins/ is not in the repository. Which plugins run is decided by the project (#119): the folder says what is available, the open project says what is switched on. Plugins run in the editor only, never in a running game or an export.

The format

export const meta = { id: 'demo', name: 'Demo plugin', version: '1.0.0', api: 1 };

export function activate(api) {
  // register what the plugin adds
}
Export Meaning
meta.id Required, unique among the active plugins. A second plugin with the same id is not activated.
meta.name, meta.version Shown in reports; the name falls back to the id.
meta.api The plugin API version the plugin was written for. Anything but 1 is not activated.
activate(api) Required. Called once at start-up; may be async, and the next plugin waits for it.
deactivate() Optional. Called when the plugin is switched off, before what it registered is taken away.

A plugin imports nothing. Everything comes in through api. The editor is one WebGL canvas with one Pixi instance: a plugin that brought its own Pixi would draw into a second renderer. For the same reason api.tabs.add accepts only widgets from api.ui.

Loading

The editor asks GET ?r=plugins for the list, fetches the project file, and imports each file the project names through GET ?r=plugins/{file} (see API) — in file-name order, one after the other. Only then is the project read into the model.

That order is the point of #119: what a plugin registers has to exist before the project is deserialized, because anything the editor does not know at that moment is dropped as unknown. So a plugin can bring node types and asset kinds along later on, and a project using them still opens.

Opening another project switches over: the plugins it does not name are deactivated first, then its own are activated. A plugin the project does not name is never fetched, so none of its code runs. A name that is not a file on the server is passed over in silence.

Nothing a plugin does wrong stops the editor or the other plugins. Each of these leaves the plugin inactive, with a line in the console and a notice in the toolbar:

  • the file does not load (a syntax error, a failed request),
  • it exports no meta or no activate, or meta.id is empty,
  • meta.api is not a version this editor offers,
  • a plugin with the same meta.id is active already,
  • activate throws (or its promise rejects).

What activate registered before it threw is taken away again, so a half-built plugin leaves no tab behind.

Switching a plugin on and off

Menu → Plugins opens a tab that lists every file in data/plugins/ with its name, version, file and state, each with a switch. It closes like a document. For an administrator it is the lower half of the User management tab, which Menu → User management opens as well (#179, see Accounts). The switch belongs to the open project: it is kept in the project file as "plugins": ["demo-plugin.js"], so it travels with the project and says nothing about any other one. A new project switches on nothing.

A switch takes effect at once, without reloading the editor:

  • On writes it into the project, then loads and activates the plugin — after every plugin that is running already, whatever the file names say.
  • Off writes it into the project, calls the plugin's deactivate(), and then takes away what it registered through the API: its tabs, their content and its change listeners. Whoever was looking at one of its tabs lands on Scene.
  • It is an undo step like any other change to the project, and undo switches the plugin back.
  • A plugin that is off has no name on its row until it has run once in this session: reading a name out of it would mean running it.
  • What the plugin did outside the API — a timer it started, something it wrote to the server — the editor cannot take back. That is the same right-to-everything as in Limits below.
  • When the project does not take the switch, nothing changes and the toolbar says why.

Since the switch is the project's, the server can no longer know whether a file is in use: it hands out every plugin it has, and only the editor decides what runs. That was decided in #117 and is of no consequence while nothing secret lives in a plugin; #115 and #116 have to take it into account.

API v1

Member What it does
api.version 1
api.project.assets() The assets of the open project in list order, as { id, name, type } — copies, so writing to them changes nothing.
api.project.on('change', fn) Calls fn whenever the asset list changes: an asset created, renamed or deleted, by hand, by undo or by loading another project. A change that leaves ids, names and types alone (moving an object, editing a property) does not call it. Returns a function that unsubscribes. A listener that throws is reported and stays subscribed. Any event but 'change' throws.
api.tabs.add({ id, label, create }) Adds a tab after the fixed ones (Scene, Game), ahead of the open documents; it stays for as long as the plugin is on. create() is called once and must return a widget from api.ui. Returns false, with a console warning, when the id is taken — by a built-in tab (scene, graph, …) or a plugin that came earlier; create is not called then. A missing id or label, or a create that is not a function, throws.
new api.ui.NodeBoard() A read-only node graph: setNodes([{ id, title, x, y }]) replaces what it shows, fit() frames it. It has a Fit button and a node count above the canvas; a drag with any button pans, the wheel zooms about the pointer. The nodes are drawn by the graph editor's own node view, empty but for their title — no ports, no editing, no selection. The first setNodes with anything in it frames the nodes once the tab has a size; after that the view stays where the user put it. A title that is not text is empty, a position that is not a number is 0, and something that is not a list throws.

The demo plugin

plugins/demo-plugin.js adds the tab Assets: one node per asset, titled with its name, four to a row in the order of the asset list. It subscribes to change and hands the board the new list, so creating, renaming or deleting an asset shows at once. e2e step 38 checks exactly that.

Limits

  • A plugin runs with every right the editor has — it is code on the page, and the api object is a convenience, not a boundary: nothing stops a plugin from calling the PHP API itself. Accepted for v1, open in #115.
  • No writing to the project, no node types of its own, no asset types, no inspector rows, no hooks into a running game. Those come when a plugin needs them.
  • No upload (#116): a file is put into data/plugins/ by hand.