Skip to content

TypeScript

TypeScript plugins are compiled into WebAssembly components with jco, using an embedded QuickJS engine. The @phoenixcord/sdk package (bindings/typescript) provides typings generated from the WIT definition, with the same documentation as the API reference, plus a few helpers.

examples/typescript/shout is a complete plugin. Its package.json:

{
"name": "@phoenixcord/example-shout",
"private": true,
"type": "module",
"scripts": {
"build": "tsc && jco componentize src/index.ts --wit ../../../crates/sdk/wit --world-name phoenixcord --backend qjs --out shout.wasm"
},
"dependencies": {
"@phoenixcord/sdk": "workspace:*"
},
"devDependencies": {
"@bytecodealliance/componentize-js": "^0.23.0",
"@bytecodealliance/jco": "^1.41.0",
"typescript": "^7.0.2"
}
}

tsc type-checks the plugin against the generated typings, then jco componentize bundles it and produces shout.wasm. Generate the typings once with pnpm --filter @phoenixcord/sdk generate, or run cargo xtask examples, which does both and builds every example.

Export the plugin as plugin. definePlugin fills in every hook you leave out.

import { definePlugin, pluginInfo } from "@phoenixcord/sdk";
import { getSetting, requestRender } from "phoenixcord:sdk/host@0.1.0";
let enabled = false;
export const plugin = definePlugin({
info: () => ({
...pluginInfo("Shout", "Adds a chat bar button that sends your messages in capitals", ["you"]),
settings: [
{
key: "exclaim",
description: "End shouted messages with an exclamation mark",
kind: { tag: "boolean" },
default: { tag: "boolean", val: false },
restartNeeded: false,
},
],
hooks: ["chat-bar-buttons", "message-pre-send"],
}),
chatBarButtons: context =>
context.mainChat
? [{ tooltip: "Shout", icon: { tag: "named", val: "megaphone" }, action: "toggle", active: enabled }]
: [],
onEvent(event) {
if (event.tag === "action" && event.val.action === "toggle") {
enabled = !enabled;
requestRender();
}
},
preSend(message) {
if (!enabled) return { tag: "keep" };
const exclaim = getSetting("exclaim");
const suffix = exclaim?.tag === "boolean" && exclaim.val ? "!" : "";
return { tag: "replace", val: message.content.toUpperCase() + suffix };
},
});

Keep plugin state in module-level variables. The module is loaded once and lives as long as the plugin is loaded.

Host functions are imported from their WIT interface:

Interface Import from
host phoenixcord:sdk/host@0.1.0
client phoenixcord:sdk/client@0.1.0
actions phoenixcord:sdk/actions@0.1.0

Types are re-exported from @phoenixcord/sdk: import type { Event, Message } from "@phoenixcord/sdk".

WIT TypeScript
kebab-case names camelCase
u64, snowflake bigint
u32, f64 number
option<T> T | undefined
list<T> T[]
list<u8>, list<u32>, list<u64>, list<f64> Uint8Array, Uint32Array, BigUint64Array, Float64Array
enum string literal union, e.g. "message-pre-send"
variant { tag: "replace", val: string }
result<T, E> { tag: "ok", val: T } | { tag: "err", val: E }

Snowflakes are bigint, so compare them with === against other bigints and format them with String(id).

element, stack and text build elements without managing node indices by hand:

import { element, stack, text } from "@phoenixcord/sdk";
const badge = element(
stack([{ kind: { tag: "icon", val: { tag: "named", val: "clock" } } }, text("3s late")], {
direction: "row",
gap: 4,
}),
);

QuickJS supports modern JavaScript, but there is no Node.js or DOM: use host.fetch instead of fetch, timers instead of setTimeout, and storage instead of files. npm packages work when they are plain JavaScript without Node or browser APIs; jco bundles them into the component.