Field types
string, number, boolean, array, object. Drive the form input type and the value coercion the studio applies.
Xuda Developer Plugins
The category decides the naming convention, the expected exports, and the extension seam. Click a category below to pin the examples on this page.
The package name must follow @xuda.io/xuda-<category>-plugin-<name>. The category
segment is the dispatch key the runtime uses. The name segment is free-form kebab-case.
mkdir -p my-xuda-widget-plugin/src cd my-xuda-widget-plugin npm init -y # rename the package to follow the convention npm pkg set name="@xuda.io/xuda-widget-plugin-hello" npm pkg set version="1.0.0" npm pkg set license="Proprietary software licenses" npm pkg set author="xuda.io" npm install --save-dev vite
The runtime's infer_draft_plugin_manifest_entry helper looks for a specific set of files
inside the package. Anything else is ignored. Start with this layout.
my-xuda-widget-plugin/ ├── package.json # name, version, svg_icon, deps ├── vite.config.js # bundles src/runtime.mjs → dist/runtime.mjs ├── README.md # marketplace description ├── src/ │ ├── index.mjs # studio-time schema: capabilities, methods, fields │ └── runtime.mjs # browser entry: async function _default(fields, params) ├── dist/ # build output (published) │ ├── index.mjs │ ├── runtime.mjs │ └── runtime.css # optional, auto-loaded if present └── tests/ # optional vite dev harness
package.json with the marketplace icon.
The studio marketplace tile reads description, keywords, and the custom
svg_icon field (raw SVG markup). Keep runtime-only imports under
devDependencies; Vite inlines them.
package.json{
"name": "@xuda.io/xuda-widget-plugin-hello",
"version": "1.0.0",
"description": "Xuda Hello widget plugin",
"scripts": {
"pub": "npm version patch --force && npm publish --access public",
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"keywords": ["xuda", "plugin", "hello"],
"author": "xuda.io",
"license": "Proprietary software licenses",
"svg_icon": "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M12 2L2 22h20L12 2z' fill='currentColor'/></svg>",
"devDependencies": {
"vite": "^5.4.10",
"@xuda.io/plugin-sandbox": "^1.0.14"
}
}
src/index.mjs — the studio schema.
This file describes the plugin to the studio. No DOM, no imports from runtime.mjs,
no side effects. Just a declarative schema.
src/index.mjs — widget categoryexport const capabilities = {
browser: true,
headless: false,
};
export const methods = {
_default: {
name: "Default",
tooltip: "Render a hello message in the target container",
fields: {
greeting: {
label: "Greeting",
type: "string",
tooltip: "The text to display",
},
tone: {
label: "Tone",
type: "string",
render: "select",
options: [
{ label: "Friendly", value: "friendly" },
{ label: "Formal", value: "formal" },
{ label: "Enthusiastic",value: "enthusiastic"},
],
},
},
response: {},
},
};
string, number, boolean, array, object. Drive the form input type and the value coercion the studio applies.
select, segment, code, checkbox, eventId, field. Hints for the studio's form generator.
defaultValue can be a literal or a function returning a value. The sandbox evaluates it when generating the initial form state.
mandatory: true makes the field required; tooltip renders next to the label in the studio.
src/runtime.mjs — the browser entry.
One function per method. Each function is async (fields, params) => … for widget and
library plugins. UI plugins export a single fn(plugin_name, el, properties). Graphics
plugins export a tabs array.
src/runtime.mjs — widget categoryexport async function _default(fields, params) {
const container = params.container_node;
const emphasis = fields.tone === "enthusiastic" ? "!!!" :
fields.tone === "formal" ? "." : "!";
const bubble = document.createElement("div");
bubble.className = "hello-plugin-bubble";
bubble.textContent = `${fields.greeting || "Hello"}${emphasis}`;
container.replaceChildren(bubble);
}
params received by the methodparams = {
SESSION_ID, // current browser session id
method, // the invoked method name
_session, // resolved session metadata (app_id, domain, ...)
container_node, // a DOM node owned by the plugin
plugin_name, // full package name, for logging
plugin_setup, // per-app setup object, if any
api_utils: {
invoke_event(event_id), // trigger a Xuda event
debug_report(source, msg, t), // log to the runtime console
...
},
};
vite.config.js so the bundler produces dist/runtime.mjs.
Use the standard Xuda plugin vite config. It takes src/runtime.mjs as the entry,
produces a single ES module, copies non-runtime files into dist/, and names
CSS output runtime.css so the loader can pick it up.
vite.config.jsimport { resolve, basename } from "path";
import { cpSync } from "fs";
export default {
root: "./tests",
build: {
lib: {
entry: "src/runtime.mjs",
formats: ["es"],
fileName: (format, entryName) => `${entryName}.mjs`,
},
rollupOptions: {
input: "src/runtime.mjs",
output: [
{
dir: "dist",
format: "es",
exports: "named",
entryFileNames: "[name].mjs",
name: "MyLibrary",
inlineDynamicImports: true,
assetFileNames: (info) => {
if (info.name && info.name.endsWith(".css")) return "runtime[extname]";
return "[name]-[hash][extname]";
},
},
],
},
emptyOutDir: true,
},
plugins: [
{
name: "copy-index-file",
writeBundle() {
cpSync(resolve(__dirname, "src"), resolve("dist"), {
recursive: true,
filter: (p) => !p.endsWith("runtime.mjs"),
});
},
},
],
define: { "process.env": process.env },
};
The @xuda.io/plugin-sandbox package renders a split-pane harness: a live field form on the
left, your plugin running on the right. Wire it up inside tests/index.html and point
vite at it.
tests/main.js — widget sandboximport { initializeWidgetPlugin } from "@xuda.io/plugin-sandbox";
import * as plugin from "../src/runtime.mjs";
import { methods } from "../src/index.mjs";
initializeWidgetPlugin(
document.getElementById("app"),
plugin,
methods,
);
tests/main.js — UI plugin sandboximport { initializeUiPlugin } from "@xuda.io/plugin-sandbox";
import * as plugin from "../src/runtime.mjs";
import { properties } from "../src/index.mjs";
initializeUiPlugin(
document.getElementById("app"),
plugin,
properties,
`<span>1731600000000</span>`, // the element to decorate
);
npm run dev # opens the sandbox at http://localhost:5173 npm run build # produces dist/ ready to publish
The marketplace renders README.md on the plugin detail page. Lead with a short pitch,
then list features, installation, a properties table, and a minimal example. Match the style used by
the built-in plugins.
README.md# Hello Xuda Widget Plugin The **Hello Widget** renders a configurable greeting in any Xuda page. ## Features - Typed greeting with three tone presets - Zero dependencies, runs in the browser only ## Installation 1. Open the **Plugins** tab in your Xuda app. 2. Search for `xuda-widget-plugin-hello` and install it. ## Properties | Property | Type | Default | |----------|--------|---------------| | greeting | string | Hello | | tone | string | friendly | ## Example Drop a Widget node, pick **Hello**, set `greeting = Good morning`.
Every built-in plugin uses the same publish script: npm version patch --force && npm
publish --access public. The marketplace ingests the new version automatically; apps pick it
up via their plugin list.
npm run build npm run pub # equivalent to: npm version patch --force && npm publish --access public