@owncast/plugin-sdk 0.6.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/bin/owncast-plugin.js +53 -36
- package/index.d.ts +331 -107
- package/index.js +327 -320
- package/package.json +2 -1
- package/scripts/postinstall.js +5 -5
- package/slug.js +26 -0
- package/testing.js +15 -48
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @owncast/plugin-sdk
|
|
2
2
|
|
|
3
|
-
SDK for authoring [Owncast](https://owncast.online) plugins in JavaScript or TypeScript. Plugins
|
|
3
|
+
SDK for authoring [Owncast](https://owncast.online) plugins in JavaScript or TypeScript. Plugins ship as source and run sandboxed inside the Owncast server, on a JavaScript engine the host embeds, so there's no wasm toolchain to install.
|
|
4
4
|
|
|
5
5
|
Most authors don't install this directly, instead, scaffold a new project with `npx create-owncast-plugin@latest <slug>` and the generated `package.json` already lists it as a dependency.
|
|
6
6
|
|
|
@@ -9,10 +9,10 @@ Most authors don't install this directly, instead, scaffold a new project with `
|
|
|
9
9
|
```sh
|
|
10
10
|
npx create-owncast-plugin@latest my-plugin
|
|
11
11
|
cd my-plugin
|
|
12
|
-
npm install # postinstall fetches the
|
|
13
|
-
npm run build #
|
|
14
|
-
npm
|
|
15
|
-
npm
|
|
12
|
+
npm install # postinstall fetches the prebuilt test/serve host binaries
|
|
13
|
+
npm run build # bundles src/plugin.{js,ts} into my-plugin.js
|
|
14
|
+
npm test # builds, then runs scenarios from __tests__/
|
|
15
|
+
npm run package # zips manifest + my-plugin.js + assets + icon.png into my-plugin.ocpkg
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
Then install `my-plugin.ocpkg` in Owncast. From the admin's **Plugins** page click **Upload plugin** and pick the file, or copy it directly to the server's `data/plugins/` directory. Toggle **Enabled** on the plugin's row to load it.
|
|
@@ -43,7 +43,7 @@ Declare the permissions your plugin uses (`chat.send` for the example above) in
|
|
|
43
43
|
- `index.d.ts`, TypeScript declarations for editor autocomplete on every event payload and host API.
|
|
44
44
|
- `testing.js`, JS test API (`runScenarios`) for writing `__tests__/*.test.js` with the full ergonomics of JavaScript instead of static JSON.
|
|
45
45
|
- `bin/owncast-plugin`, CLI: `build`, `test`, `serve`, `package` subcommands.
|
|
46
|
-
- `scripts/postinstall.js`, downloads the
|
|
46
|
+
- `scripts/postinstall.js`, downloads the Go test/serve host binaries on install. Plugins ship as source and run on the engine the host embeds, so no wasm toolchain (`extism-js`, binaryen) is fetched (that's a maintainer-only dependency of the engine build).
|
|
47
47
|
|
|
48
48
|
## License
|
|
49
49
|
|
package/bin/owncast-plugin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// `owncast-plugin build` , bundle src/plugin.{js,ts} into <slug>.
|
|
3
|
-
// `owncast-plugin test` , run scenarios in __tests__/ against the
|
|
2
|
+
// `owncast-plugin build` , bundle src/plugin.{js,ts} into <slug>.js
|
|
3
|
+
// `owncast-plugin test` , run scenarios in __tests__/ against the plugin
|
|
4
4
|
// `owncast-plugin serve` , run a localhost dev HTTP server
|
|
5
5
|
// `owncast-plugin package`, produce a single-file <slug>.ocpkg suitable
|
|
6
6
|
// for distribution / installation
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// "Slug" is the plugin's identifier: lowercase, hyphenated, used in
|
|
9
9
|
// filenames, URL segments, and as the registry's primary key. Plugin
|
|
10
10
|
// authors set the human-readable display name via `name` in their
|
|
11
|
-
// manifest
|
|
11
|
+
// manifest. If they don't set `slug`, the CLI auto-derives it from
|
|
12
12
|
// `name`.
|
|
13
13
|
|
|
14
14
|
const fs = require("fs");
|
|
@@ -16,6 +16,7 @@ const path = require("path");
|
|
|
16
16
|
const { execFileSync } = require("child_process");
|
|
17
17
|
const esbuild = require("esbuild");
|
|
18
18
|
const JSZip = require("jszip");
|
|
19
|
+
const { slugify } = require("../slug");
|
|
19
20
|
|
|
20
21
|
const cmd = process.argv[2] || "build";
|
|
21
22
|
const restArgs = process.argv.slice(3);
|
|
@@ -30,30 +31,13 @@ function fail(e) {
|
|
|
30
31
|
// Same shape the host + SDK + registry all validate against.
|
|
31
32
|
const slugPattern = /^[a-z][a-z0-9-]{0,63}$/;
|
|
32
33
|
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
let out = "";
|
|
41
|
-
let prevHyphen = false;
|
|
42
|
-
for (const ch of input) {
|
|
43
|
-
const code = ch.codePointAt(0);
|
|
44
|
-
let lower = ch;
|
|
45
|
-
if (code >= 65 && code <= 90) lower = String.fromCodePoint(code + 32);
|
|
46
|
-
const lc = lower.codePointAt(0);
|
|
47
|
-
if ((lc >= 97 && lc <= 122) || (lc >= 48 && lc <= 57)) {
|
|
48
|
-
out += lower;
|
|
49
|
-
prevHyphen = false;
|
|
50
|
-
} else if (!prevHyphen && out.length > 0) {
|
|
51
|
-
out += "-";
|
|
52
|
-
prevHyphen = true;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return out.replace(/-+$/, "");
|
|
56
|
-
}
|
|
34
|
+
// Canonical registry browse categories for the optional manifest
|
|
35
|
+
// `category` field. Shared taxonomy with the registry and the admin UI.
|
|
36
|
+
const categories = new Set([
|
|
37
|
+
"chat-bots", "chat-filters", "moderation", "authentication", "themes",
|
|
38
|
+
"overlays", "notifications", "integrations", "video", "analytics",
|
|
39
|
+
"games", "admin-utilities", "examples", "other",
|
|
40
|
+
]);
|
|
57
41
|
|
|
58
42
|
// readAndResolveManifest loads plugin.manifest.json, validates the
|
|
59
43
|
// required fields, and returns a manifest object with `slug` filled
|
|
@@ -83,6 +67,13 @@ function readAndResolveManifest(manifestPath) {
|
|
|
83
67
|
`manifest.slug ${JSON.stringify(slug)} must match ${slugPattern} (lowercase letters/digits/hyphens, starting with a letter, max 64 chars)`,
|
|
84
68
|
);
|
|
85
69
|
}
|
|
70
|
+
if (manifest.category !== undefined && !categories.has(manifest.category)) {
|
|
71
|
+
// Warn only: the host and registry tolerate unknown categories, they
|
|
72
|
+
// just won't match any browse filter.
|
|
73
|
+
console.warn(
|
|
74
|
+
`warning: manifest.category ${JSON.stringify(manifest.category)} is not a known category (${[...categories].join(", ")})`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
86
77
|
manifest.slug = slug;
|
|
87
78
|
return manifest;
|
|
88
79
|
}
|
|
@@ -115,6 +106,26 @@ function runBinary(name, args) {
|
|
|
115
106
|
}
|
|
116
107
|
}
|
|
117
108
|
|
|
109
|
+
// loadCheck runs `owncast-plugin-test --load-only <dir>` and aborts the
|
|
110
|
+
// current command when the plugin fails the install-time load check.
|
|
111
|
+
function loadCheck(dir) {
|
|
112
|
+
const cache = findCacheDir();
|
|
113
|
+
const bin = path.join(cache, "owncast-plugin-test");
|
|
114
|
+
if (!fs.existsSync(bin)) {
|
|
115
|
+
console.error(
|
|
116
|
+
`owncast-plugin-test not found at ${bin} — cannot verify the plugin ` +
|
|
117
|
+
`loads. Run npm install so the SDK postinstall fetches the host binaries.`,
|
|
118
|
+
);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
execFileSync(bin, ["--load-only", dir], { stdio: "inherit" });
|
|
123
|
+
} catch (e) {
|
|
124
|
+
console.error("package aborted: plugin failed the install-time load check");
|
|
125
|
+
process.exit(typeof e.status === "number" ? e.status : 1);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
118
129
|
async function buildMain() {
|
|
119
130
|
const cwd = process.cwd();
|
|
120
131
|
const manifestPath = path.join(cwd, "plugin.manifest.json");
|
|
@@ -145,7 +156,7 @@ async function buildMain() {
|
|
|
145
156
|
|
|
146
157
|
// Shared-engine model: bundle the author's plugin into a tiny CommonJS
|
|
147
158
|
// script with @owncast/plugin-sdk marked EXTERNAL. It ships in the .ocpkg as
|
|
148
|
-
// plugin.js
|
|
159
|
+
// plugin.js. The host infers the JavaScript runtime from that filename and
|
|
149
160
|
// runs it on the embedded JS engine, which provides
|
|
150
161
|
// require("@owncast/plugin-sdk"). No per-plugin wasm, no extism-js.
|
|
151
162
|
const buildDir = path.join(cwd, ".owncast-build");
|
|
@@ -162,13 +173,13 @@ async function buildMain() {
|
|
|
162
173
|
logLevel: "warning",
|
|
163
174
|
});
|
|
164
175
|
|
|
165
|
-
// public/ and assets/ live at the source root
|
|
176
|
+
// public/ and assets/ live at the source root, and the packager picks them up.
|
|
166
177
|
console.log(`built ${path.relative(cwd, scriptOut)}`);
|
|
167
178
|
}
|
|
168
179
|
|
|
169
180
|
// `owncast-plugin package`, bundle the project into a single .ocpkg file
|
|
170
|
-
// (zip archive with plugin.manifest.json, plugin.
|
|
171
|
-
// public/ and assets/ directories). Builds the
|
|
181
|
+
// (zip archive with plugin.manifest.json, plugin.js source, and optional
|
|
182
|
+
// public/ and assets/ directories). Builds the source first if it
|
|
172
183
|
// doesn't exist.
|
|
173
184
|
async function packageMain() {
|
|
174
185
|
const cwd = process.cwd();
|
|
@@ -184,9 +195,15 @@ async function packageMain() {
|
|
|
184
195
|
await buildMain();
|
|
185
196
|
}
|
|
186
197
|
|
|
198
|
+
// Refuse to package a plugin a real Owncast server would refuse to load.
|
|
199
|
+
// owncast-plugin-test --load-only runs the same install-time load path the
|
|
200
|
+
// host runs: register(), manifest/runtime agreement, and permission-gated
|
|
201
|
+
// subscriptions (e.g. a fediverse handler without "fediverse.inbound").
|
|
202
|
+
loadCheck(cwd);
|
|
203
|
+
|
|
187
204
|
// The code entry's name (plugin.js) is what tells the host this is a
|
|
188
|
-
// JavaScript plugin
|
|
189
|
-
// verbatim.
|
|
205
|
+
// JavaScript plugin, so there is no "type" field in the manifest. The
|
|
206
|
+
// manifest ships verbatim.
|
|
190
207
|
const publicDir = path.join(cwd, "public");
|
|
191
208
|
const assetsDir = path.join(cwd, "assets");
|
|
192
209
|
const zip = new JSZip();
|
|
@@ -204,7 +221,7 @@ async function packageMain() {
|
|
|
204
221
|
}
|
|
205
222
|
// Bundle a top-level INSTRUCTIONS.md if the plugin source root has one.
|
|
206
223
|
// The host serves it to the admin (which renders it as markdown in a
|
|
207
|
-
// details tab)
|
|
224
|
+
// details tab). Like icon.png it needs no manifest field and no
|
|
208
225
|
// http.serve permission. The filename is fixed for simplicity.
|
|
209
226
|
const instructionsPath = path.join(cwd, "INSTRUCTIONS.md");
|
|
210
227
|
if (fs.existsSync(instructionsPath) && fs.statSync(instructionsPath).isFile()) {
|
|
@@ -251,7 +268,7 @@ async function packageMain() {
|
|
|
251
268
|
fs.unlinkSync(scriptPath);
|
|
252
269
|
} catch (e) {
|
|
253
270
|
// Don't fail the package step over a cleanup miss. The .ocpkg is
|
|
254
|
-
// already written
|
|
271
|
+
// already written, so surface the warning so the author notices the
|
|
255
272
|
// straggler but treat the run as successful.
|
|
256
273
|
if (e.code !== "ENOENT") {
|
|
257
274
|
console.warn(`warning: could not clean up ${path.relative(cwd, scriptPath)}: ${e.message}`);
|
|
@@ -290,7 +307,7 @@ function findCacheDir() {
|
|
|
290
307
|
path.join(__dirname, "..", "..", "..", "tools"),
|
|
291
308
|
];
|
|
292
309
|
// Pick the first candidate that has the prebuilt host binaries (the only
|
|
293
|
-
// tooling the SDK ships now
|
|
310
|
+
// tooling the SDK ships now, since `build` is pure esbuild and needs nothing here).
|
|
294
311
|
for (const c of candidates) {
|
|
295
312
|
if (
|
|
296
313
|
fs.existsSync(path.join(c, "owncast-plugin-test")) ||
|