@owncast/plugin-sdk 0.6.0 → 0.10.1

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 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 compile to WebAssembly and run sandboxed inside the Owncast server.
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 per-platform wasm toolchain
13
- npm run build # compiles src/plugin.js into an intermediate build artifact
14
- npm run package # zips manifest + wasm + assets + icon.png into my-plugin.ocpkg
15
- npm test # runs scenarios from __tests__/
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 per-platform wasm toolchain (`extism-js`, `wasm-merge`, `wasm-opt`) and the Go test/serve runner on install.
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
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- // `owncast-plugin build` , bundle src/plugin.{js,ts} into <slug>.wasm
3
- // `owncast-plugin test` , run scenarios in __tests__/ against the wasm
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; if they don't set `slug`, the CLI auto-derives it from
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,31 +31,6 @@ 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
- // slugify mirrors the host's Go slugify: ASCII letters and digits
34
- // pass through lowercased; everything else collapses to a single
35
- // hyphen; leading and trailing hyphens are trimmed.
36
- // Non-ASCII names (e.g. "Café") degrade noisily (-> "caf"); plugins
37
- // with accented or non-Latin display names should pin `slug` in the
38
- // manifest instead of relying on auto-derivation.
39
- function slugify(input) {
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
- }
57
-
58
34
  // readAndResolveManifest loads plugin.manifest.json, validates the
59
35
  // required fields, and returns a manifest object with `slug` filled
60
36
  // in: either the author's explicit `slug`, or one auto-derived from
@@ -115,6 +91,26 @@ function runBinary(name, args) {
115
91
  }
116
92
  }
117
93
 
94
+ // loadCheck runs `owncast-plugin-test --load-only <dir>` and aborts the
95
+ // current command when the plugin fails the install-time load check.
96
+ function loadCheck(dir) {
97
+ const cache = findCacheDir();
98
+ const bin = path.join(cache, "owncast-plugin-test");
99
+ if (!fs.existsSync(bin)) {
100
+ console.error(
101
+ `owncast-plugin-test not found at ${bin} — cannot verify the plugin ` +
102
+ `loads. Run npm install so the SDK postinstall fetches the host binaries.`,
103
+ );
104
+ process.exit(1);
105
+ }
106
+ try {
107
+ execFileSync(bin, ["--load-only", dir], { stdio: "inherit" });
108
+ } catch (e) {
109
+ console.error("package aborted: plugin failed the install-time load check");
110
+ process.exit(typeof e.status === "number" ? e.status : 1);
111
+ }
112
+ }
113
+
118
114
  async function buildMain() {
119
115
  const cwd = process.cwd();
120
116
  const manifestPath = path.join(cwd, "plugin.manifest.json");
@@ -145,7 +141,7 @@ async function buildMain() {
145
141
 
146
142
  // Shared-engine model: bundle the author's plugin into a tiny CommonJS
147
143
  // script with @owncast/plugin-sdk marked EXTERNAL. It ships in the .ocpkg as
148
- // plugin.js; the host infers the JavaScript runtime from that filename and
144
+ // plugin.js. The host infers the JavaScript runtime from that filename and
149
145
  // runs it on the embedded JS engine, which provides
150
146
  // require("@owncast/plugin-sdk"). No per-plugin wasm, no extism-js.
151
147
  const buildDir = path.join(cwd, ".owncast-build");
@@ -162,13 +158,13 @@ async function buildMain() {
162
158
  logLevel: "warning",
163
159
  });
164
160
 
165
- // public/ and assets/ live at the source root; the packager picks them up.
161
+ // public/ and assets/ live at the source root, and the packager picks them up.
166
162
  console.log(`built ${path.relative(cwd, scriptOut)}`);
167
163
  }
168
164
 
169
165
  // `owncast-plugin package`, bundle the project into a single .ocpkg file
170
- // (zip archive with plugin.manifest.json, plugin.wasm, and optional
171
- // public/ and assets/ directories). Builds the wasm first if it
166
+ // (zip archive with plugin.manifest.json, plugin.js source, and optional
167
+ // public/ and assets/ directories). Builds the source first if it
172
168
  // doesn't exist.
173
169
  async function packageMain() {
174
170
  const cwd = process.cwd();
@@ -184,9 +180,15 @@ async function packageMain() {
184
180
  await buildMain();
185
181
  }
186
182
 
183
+ // Refuse to package a plugin a real Owncast server would refuse to load.
184
+ // owncast-plugin-test --load-only runs the same install-time load path the
185
+ // host runs: register(), manifest/runtime agreement, and permission-gated
186
+ // subscriptions (e.g. a fediverse handler without "fediverse.inbound").
187
+ loadCheck(cwd);
188
+
187
189
  // The code entry's name (plugin.js) is what tells the host this is a
188
- // JavaScript plugin no "type" field in the manifest. The manifest ships
189
- // verbatim.
190
+ // JavaScript plugin, so there is no "type" field in the manifest. The
191
+ // manifest ships verbatim.
190
192
  const publicDir = path.join(cwd, "public");
191
193
  const assetsDir = path.join(cwd, "assets");
192
194
  const zip = new JSZip();
@@ -204,7 +206,7 @@ async function packageMain() {
204
206
  }
205
207
  // Bundle a top-level INSTRUCTIONS.md if the plugin source root has one.
206
208
  // The host serves it to the admin (which renders it as markdown in a
207
- // details tab); like icon.png it needs no manifest field and no
209
+ // details tab). Like icon.png it needs no manifest field and no
208
210
  // http.serve permission. The filename is fixed for simplicity.
209
211
  const instructionsPath = path.join(cwd, "INSTRUCTIONS.md");
210
212
  if (fs.existsSync(instructionsPath) && fs.statSync(instructionsPath).isFile()) {
@@ -251,7 +253,7 @@ async function packageMain() {
251
253
  fs.unlinkSync(scriptPath);
252
254
  } catch (e) {
253
255
  // Don't fail the package step over a cleanup miss. The .ocpkg is
254
- // already written; surface the warning so the author notices the
256
+ // already written, so surface the warning so the author notices the
255
257
  // straggler but treat the run as successful.
256
258
  if (e.code !== "ENOENT") {
257
259
  console.warn(`warning: could not clean up ${path.relative(cwd, scriptPath)}: ${e.message}`);
@@ -290,7 +292,7 @@ function findCacheDir() {
290
292
  path.join(__dirname, "..", "..", "..", "tools"),
291
293
  ];
292
294
  // Pick the first candidate that has the prebuilt host binaries (the only
293
- // tooling the SDK ships now `build` is pure esbuild and needs nothing here).
295
+ // tooling the SDK ships now, since `build` is pure esbuild and needs nothing here).
294
296
  for (const c of candidates) {
295
297
  if (
296
298
  fs.existsSync(path.join(c, "owncast-plugin-test")) ||