Plugins
Package custom kinds as a plugin the document names, so they render in the browser, workers, server export and MCP.
Coming in the next release
Self-describing plugins are built and tested in @miraiclip/core, @miraiclip/renderer, @miraiclip/server-export and @miraiclip/mcp, and ship in the next SDK release.
Custom effects, transitions and clip kinds registered in your app's code render only where that code runs. A plugin packages them as one standalone ES module, and the document lists it in doc.plugins. Every runtime that opens the document can then load the same code: your editor, the export worker, server export, the player and the MCP server.
Write a plugin
A plugin is a standalone ES module with no bare imports. Its default export (or a named plugin export) has a register(api) function. The runtime passes in everything the plugin needs:
// countdown-plugin.js
export default {
name: "@acme/countdown",
version: "1.0.0",
register({ z, core, renderer }) {
const props = z.object({ from: z.number().int().min(1).default(3), color: z.string().default("#fff") });
core.registerClipKind("countdown", { propsSchema: props, trackKinds: ["video"] });
// Absent in Node (the MCP server only validates; it doesn't draw)
renderer?.registerClipRenderer("countdown", {
create(container, clip, context) {
const text = new renderer.pixi.Text({ text: "", style: { fontSize: 160, fill: clip.props.color } });
text.anchor.set(0.5);
container.addChild(text);
return {
update(next) { text.style.fill = next.props.color; },
tick(next, timeUs) {
const left = next.props.from - Math.floor((timeUs - next.startUs) / 1_000_000);
text.text = String(Math.max(left, 1));
},
destroy() { text.destroy(); },
};
},
});
},
};api member | What it is |
|---|---|
environment | "browser", "worker", "server" or "node" |
z | Core's zod. Build schemas with it, not your own copy |
core | registerClipKind, registerEffectKind, registerTransitionKind |
renderer | Present where something draws. registerClipRenderer, registerEffectRenderer, registerTransitionRenderer, and pixi: the renderer's own Pixi classes (Container, Sprite, Text, Graphics, Texture, Filter, GlProgram, BlurFilter, ColorMatrixFilter) |
A clip renderer's create(container, clip, context) gets a Pixi container centred on the clip. The clip's transform, keyframes, effects, effect regions and transitions apply to it like any built-in clip. context has compositionSize(), the document's assets, and invalidate() for content that changes outside update and tick.
definePlugin(plugin) from @miraiclip/core types a plugin written in TypeScript. Bundle it to a single file before publishing.
Add it to a document
project.dispatch({
type: "plugin/add",
payload: {
name: "@acme/countdown",
version: "1.0.0",
url: "https://cdn.example.com/plugins/countdown-1.0.0.js",
integrity: "sha384-…",
},
});| Command | Payload |
|---|---|
plugin/add | { name, version, url, integrity?, replace? }. integrity is an SRI hash (sha256-, sha384- or sha512-). replace: true upgrades an entry with the same name; without it a duplicate name rejects with duplicate-plugin |
plugin/remove | { name }. Rejects with plugin-not-found |
documentPlugins(doc) returns the list.
Load plugins
| Where | How |
|---|---|
| Your editor (main thread) | await loadDocumentPlugins(doc) from @miraiclip/renderer before creating the player |
| Worker export | Automatic |
| Server export | Opt in with plugins: { allow }. See Server export |
| MCP server | Opt in with --allow-plugins. See MCP server |
<miraiclip-player> | Add the plugins attribute. See Player |
import { loadDocumentPlugins } from "@miraiclip/renderer";
const loaded = await loadDocumentPlugins(project.getState().doc, {
allow: (ref) => ref.url.startsWith("https://cdn.example.com/plugins/"),
requireIntegrity: true,
});
// Later, to forget every kind they registered (for example before opening another project):
loaded.unload();| Option | Default | Meaning |
|---|---|---|
allow(ref) | allow all | Return false to refuse a plugin. The load then fails |
requireIntegrity | false | Refuse plugins without an integrity hash |
baseUrl | the page or worker URL | Base for relative plugin URLs |
fetchSource(url) | fetch | Read a plugin's source yourself |
environment | detected | Passed to register |
The loader checks the integrity hash before running anything. Loading the same URL and hash twice registers once.
Safety
A plugin is code. Server export and the MCP server never run one unless you allow it, and a document that lists a plugin you haven't allowed fails with a clear error. On a public service, allow only URL prefixes you control and require integrity hashes.
Replacing and unloading kinds
Registering the same kind again with the same definition does nothing, so hot reload and shared render workers are safe. To redefine a kind, pass { replace: true } to registerClipKind, registerEffectKind or registerTransitionKind. unregisterClipKind, unregisterEffectKind and unregisterTransitionKind remove one. Built-in kinds can't be replaced or removed.