@runmu.sh/create-extension
v0.7.0
Published
Scaffold a μClient extension: npm create @runmu.sh/extension my-ext. TypeScript, esbuild, the SDK types, a Vue or plain-DOM panel, the hot-reload dev server, and optionally a Rust WASM helper.
Maintainers
Readme
@runmu.sh/create-extension
Scaffold a μClient extension (R-EXT-DEV): TypeScript, esbuild, the
@muclient/sdk types, a Vue or plain-DOM panel example, and optionally a Rust WASM helper crate.
No dependencies; Node ≥ 18.
npm create @runmu.sh/extension my-ext # asks: id, package name, display name, vue/dom, wasm
npm create @runmu.sh/extension my-ext -- --ui vue --wasm -y # no prompts
node clients/create-extension/index.mjs my-ext --sdk local -y # from this checkout, against the local SDKThe generated project depends on two packages from npm: @muclient/sdk as an alias of
@runmu.sh/sdk (types; the host supplies the module at runtime) and
@runmu.sh/dev (the dev server). The GitHub template
runmu-sh/extension_template is the same starting point
with three worked examples, for people who prefer "Use this template".
| Option | |
|---|---|
| [dir] | Target folder (default: the package name without its scope) |
| --name <pkg> | npm name (default ext-<id>) |
| --id <id> | Extension id, [a-z0-9][a-z0-9._-]{0,63} (default: the name without scope and ext-, as the host derives it) |
| --display-name, --description | Shown in μClient |
| --ui vue\|dom | The panel example (default dom) |
| --wasm / --no-wasm | Add the Rust crate (default no) |
| --sdk <spec> | The @muclient/sdk devDependency: a spec (default npm:@runmu.sh/sdk@^1.7.1), file:<path>, or local for clients/extensions/sdk next to this package. Env MUCLIENT_SDK does the same |
| --install | Run npm install afterwards |
| --force | Write into a non-empty folder |
| -y, --yes | No prompts (also when stdin is not a terminal) |
What it writes
package.jsonwith amuclientmanifest (id,api: "^1.7",source, contributions: a panel, a command, GMCPRoom 1; with--wasmalsomuclient.wasm). It is checked withlib/manifest.mjsbefore anything is written: the rules ofmanifestFromPackageJsoninclients/web/src/features/extensions/host.tsandcrates/backend/src/extensions/manifest.rs.src/index.ts:defineExtensionwithmu.settings.define,mu.gmcp.supports+mu.gmcp.on('Room.Info'),mu.commands.register,mu.panels.registerwithsnapshot()/restore().src/panel.ts: plain DOM withmu.ui.cssclasses, or a Vue component with render functions (no SFC compiler) mounted withmu.panels.vue.vuestays external, so the host's Vue is used.scripts/build.mjs: esbuild, ESM, es2022, externalsvue,@muclient/sdk,@muclient/ui(the same asclients/web/build/muExtensions.tsand the dev server), then the manifest check.scripts/dev.mjs(npm run dev): runs the dev server@runmu.sh/dev(a devDependency) on the folder;$MUCLIENT_DEVoverrides it.CHANGELOG.md,.github/workflows/ci.yml(build + typecheck).--wasm:wasm/(acdylibcrate with plain#[no_mangle] extern "C"exports:add,alloc,dealloc,word_count; no wasm-bindgen, somu.wasm.load(path)needs no imports or JS glue),src/wasm.ts, andscripts/build-wasm.mjs(cargo build --target wasm32-unknown-unknown --release, copy todist/<crate>.wasm, write its sha256 intomuclient.wasm).CARGOpicks another cargo.
The API
@muclient/sdk (clients/extensions/sdk/index.ts, TSDoc with @since per member). Some recipes:
- Bridge a game's own out-of-band protocol to the host's state, which the first-party Scene, Channels and Media
extensions show:
mu.gmcp.on('Room.Info', …)→mu.scene.set(...),mu.channels.push(...),mu.media.play(...),mu.widgets.show(...). - Show the host's channels, feeds or media your own way (SDK 1.7):
mu.channels.get/watch/select/send/configure,mu.feeds.get/watch/viewing/clear,mu.media.get/watch/clearImages/setOutput; everywatchis disposable. - Open a web page the game asked for (SDK 1.6, R-WEB-OPEN):
mu.panels.openWeb({ url, id, title }, sid)opens it in a Web tab peridor in a new window, per the player's Access → "Open web pages", and returns'panel' | 'window' | 'blocked'. The first-partyext-webpages(clients/extensions/ext-webpages, thirty lines) does exactly this for GMCPClient.Web.Open {url,id,title}(docs/api_protocol.md, "Client.Web"); read it as the example.
Test
node --test test/*.test.mjs # scaffold both variants, npm install, build, typecheck, dev server
CME_SKIP_INSTALL=1 node --test test/*.test.mjs # scaffold + manifest only, no network
CME_CARGO=/path/to/cargo node --test test/*.test.mjs # also compile the Rust crate and instantiate the .wasmThe install uses --sdk local, so it needs this checkout's clients/extensions/sdk and the npm registry
(or cache) for esbuild, typescript and vue.
Publish this package
Bump the versions on main, then run the Publish extension SDK packages workflow
(.github/workflows/publish-sdk.yml): it publishes each of the three whose version is not on npm yet. By hand:
cd clients/extensions/sdk && npm publish # @runmu.sh/sdk: bump with SDK_VERSION in index.ts
cd clients/extensions/dev && npm publish # @runmu.sh/dev
cd clients/create-extension && npm publish # @runmu.sh/create-extensionThe npm token comes from the person, never from a file in the repo.
