scmJS docs

Writing a plugin

plugin.json:

{
  "name": "Hello",
  "version": "1.0.0",
  "description": "Says hello from the Tools menu.",
  "entry": "plugin.ts",
  "build": "dist/plugin.js",
  "icon": "icon.svg",
  "api": 1
}

name is the only required field; id (a slug for storage keys and log prefixes) is derived from the name when absent, and entry defaults to plugin.ts, then plugin.js.

build names a JavaScript bundle to load in place of entry — see Building below. Leave it out and the editor fetches your source and transpiles it, which is the shortest way to start.

The icon#

icon is the plugin's face in Manage Plugins and in the title bar of every dialog api.ui.dialog opens. Four forms are understood:

icon What it means
"icon.svg", "art/mark.png" An image file beside the manifest (.png .svg .jpg .gif .webp .avif .ico).
"https://…/mark.png" An image anywhere; it is fetched by the browser when the dialog shows.
"data:image/svg+xml,…" An image inline in the manifest — nothing extra to fetch.
"🗺️" Up to four characters, drawn as text: an emoji is the cheapest icon there is.

Anything else — another URL scheme, a longer string — is ignored, and the plugin shows the editor's default plugin mark, as it does when it declares no icon at all or the image fails to load. Draw for a 30 px square (it is also shown at 14 px in a dialog title), on nothing: the editor draws no frame or plate behind it, and an icon that is itself a bordered square reads as a second control next to the row's tick box. Terrain from Image's icon.svg is the worked example.

plugin.ts:

import type { PluginApi } from "@scm-js/plugin-api";

export default function activate(api: PluginApi) {
  api.menu.add("Tools", {
    label: "Say Hello",
    enabled: () => api.document.isOpen(),
    run: () => api.ui.status(`Hello, ${api.document.info()?.name}!`),
  });
}

The import type line is erased before the file runs, so the package only matters for editing and checking:

npm i -D @scm-js/plugin-api

npm outdated and npm update say what you would expect (the versioning rule is under The contract), and the same files are committed and tagged at scm-js/plugin-api if you would rather read them there or depend on a git ref.

Everything add/on returns is a Disposable; keep the ones you need to drop early and forget the rest — deactivation disposes them all. Returning a function from activate runs it at deactivation too, for anything outside the API (timers, sockets).

To develop: serve the folder (npx serve --cors .), add http://localhost:3000/ in Plugins ▸ Manage Plugins…, and press Reload after each change.

Building#

A plugin can ship a built bundle and name it in the manifest's build. It is worth doing for anything bigger than a single file: the editor fetches one JavaScript file and imports it, instead of fetching your source, starting the TypeScript compiler in a worker and walking your imports one file at a time — and only a built plugin can use an npm dependency, since the source path has no resolver behind its fetch.

The organisation's plugins all do it the same way, with one esbuild call in a build script:

"build": "esbuild plugin.ts --bundle --format=esm --target=es2022 --platform=browser --outfile=dist/plugin.js",
"dev": "npm run build -- --watch"

and dist/plugin.js is committed, because the editor loads it straight from the repository at whatever ref the spec names. The shared workflow in scm-js/.github does the rest — a plugin repository calls it in six lines:

name: CI
on:
  push: { branches: [main], tags: ["v*"] }
  pull_request:
  schedule: [{ cron: "0 6 * * 1" }]
permissions: { contents: write }
jobs:
  ci:
    uses: scm-js/.github/.github/workflows/plugin-ci.yml@main

It type-checks, tests, rebuilds the bundle and commits it on a push to main; at a v* tag it rebuilds and checks instead, so the bundle a pinned plugin runs is provably what its source builds to (esbuild's output is deterministic, and the bundle carries no commit hash or date for that reason). The scheduled run type-checks against the newest @scm-js/plugin-api, so a contract that moved under the plugin turns a check red rather than going unnoticed.

The bundle is not minified. What the confirmation dialog offers to show a user is the repository, and a plugin they cannot read is a plugin they cannot judge.

scmJS 0.1.0 · Generated from the repository. StarCraft and Brood War are trademarks of Blizzard Entertainment; this project ships none of their data.