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.
Project setup
Section titled “Project setup”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.
Writing a plugin
Section titled “Writing a plugin”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.
Calling PhoenixCord
Section titled “Calling PhoenixCord”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".
Type mapping
Section titled “Type mapping”| 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).
UI helpers
Section titled “UI helpers”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, }),);Limits
Section titled “Limits”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.