@owncast/plugin-sdk 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owncast/plugin-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.10.1",
4
4
  "description": "SDK for authoring Owncast plugins in JavaScript",
5
5
  "license": "MIT",
6
6
  "author": "Owncast",
@@ -28,6 +28,7 @@
28
28
  "files": [
29
29
  "index.js",
30
30
  "index.d.ts",
31
+ "slug.js",
31
32
  "testing.js",
32
33
  "bin/owncast-plugin.js",
33
34
  "scripts/postinstall.js"
@@ -1,11 +1,14 @@
1
1
  #!/usr/bin/env node
2
- // Downloads per-platform tooling into <sdk>/bin/.cache so the build CLI
3
- // finds it without polluting the user's system:
2
+ // Downloads the prebuilt host binaries into <sdk>/bin/.cache so `owncast-plugin
3
+ // test` / `serve` work without polluting the user's system:
4
4
  //
5
- // - extism-js , JS → wasm compiler (extism/js-pdk releases)
6
- // - wasm-merge, wasm-opt, lib , binaryen post-processing (WebAssembly/binaryen releases)
7
5
  // - owncast-plugin-test/serve , scenario runner + dev server (this repo's releases)
8
6
  //
7
+ // That's all an author needs: plugins ship source and run on the interpreter
8
+ // engine the host already embeds, so the wasm compiler toolchain (extism-js,
9
+ // binaryen) is NOT downloaded here. It's a maintainer-only dependency of the
10
+ // engine build (see engines/install-toolchain.mjs).
11
+ //
9
12
  // PoC scope: linux-x86_64 + darwin-arm64 + darwin-x86_64 covered.
10
13
  // owncast-plugin-test/serve downloads gracefully skip if the matching
11
14
  // release asset isn't published yet, dev builds can substitute their own
@@ -15,23 +18,20 @@ const fs = require("fs");
15
18
  const path = require("path");
16
19
  const https = require("https");
17
20
  const zlib = require("zlib");
18
- const { execFileSync } = require("child_process");
19
21
 
20
- const EXTISM_JS_VERSION = "v1.6.0";
21
- const BINARYEN_VERSION = "version_119";
22
22
  const HOST_BINARIES_REPO = "owncast/plugin-sdk";
23
23
 
24
24
  // The host binaries (owncast-plugin-test/serve) implement the host-function
25
25
  // contract that the bundled JS runtime imports. That contract is additive
26
- // within a major version host functions are only ever added, never removed or
27
- // renamed (a removal is a breaking change that requires a major bump) so the
26
+ // within a major version. Host functions are only ever added, never removed or
27
+ // renamed (a removal is a breaking change that requires a major bump), so the
28
28
  // NEWEST published binary is compatible with every plugin runtime. We therefore
29
29
  // fetch the latest release tag rather than deriving one from the npm version.
30
30
  //
31
31
  // This keeps the binary in lockstep with `@owncast/plugin-sdk@^x` (which npm
32
32
  // already floats to the newest compatible runtime) and fixes the old "zero the
33
33
  // patch" guess: that fetched v<major>.<minor>.0, which 404'd on JS-only patches
34
- // and when a host change shipped in a patch (e.g. timer support in 0.4.2)
34
+ // and, when a host change shipped in a patch (e.g. timer support in 0.4.2),
35
35
  // fetched a binary too old to satisfy the runtime's imports, breaking
36
36
  // `npm test`.
37
37
  //
@@ -98,29 +98,6 @@ function platformKey() {
98
98
  throw new Error(`unsupported platform: ${platform}/${arch}`);
99
99
  }
100
100
 
101
- function extismJsURL() {
102
- // extism-js release naming uses different conventions per OS.
103
- const map = {
104
- "linux-x86_64": `extism-js-x86_64-linux-${EXTISM_JS_VERSION}.gz`,
105
- "linux-aarch64": `extism-js-aarch64-linux-${EXTISM_JS_VERSION}.gz`,
106
- "darwin-x86_64": `extism-js-x86_64-macos-${EXTISM_JS_VERSION}.gz`,
107
- "darwin-arm64": `extism-js-aarch64-macos-${EXTISM_JS_VERSION}.gz`,
108
- };
109
- const file = map[platformKey()];
110
- return `https://github.com/extism/js-pdk/releases/download/${EXTISM_JS_VERSION}/${file}`;
111
- }
112
-
113
- function binaryenURL() {
114
- const map = {
115
- "linux-x86_64": `binaryen-${BINARYEN_VERSION}-x86_64-linux.tar.gz`,
116
- "linux-aarch64": `binaryen-${BINARYEN_VERSION}-aarch64-linux.tar.gz`,
117
- "darwin-x86_64": `binaryen-${BINARYEN_VERSION}-x86_64-macos.tar.gz`,
118
- "darwin-arm64": `binaryen-${BINARYEN_VERSION}-arm64-macos.tar.gz`,
119
- };
120
- const file = map[platformKey()];
121
- return `https://github.com/WebAssembly/binaryen/releases/download/${BINARYEN_VERSION}/${file}`;
122
- }
123
-
124
101
  function hostBinaryURL(name, version) {
125
102
  // Per-platform asset naming matches Go's GOOS-GOARCH convention so the
126
103
  // release CI can `go build` once per matrix entry without renaming.
@@ -155,38 +132,6 @@ async function main() {
155
132
  const cacheDir = path.join(__dirname, "..", "bin", ".cache");
156
133
  fs.mkdirSync(cacheDir, { recursive: true });
157
134
 
158
- const extismDest = path.join(cacheDir, "extism-js");
159
- if (!fs.existsSync(extismDest)) {
160
- const gz = path.join(cacheDir, "extism-js.gz");
161
- console.log(`[plugin-sdk] downloading extism-js ${EXTISM_JS_VERSION}...`);
162
- await download(extismJsURL(), gz);
163
- const buf = zlib.gunzipSync(fs.readFileSync(gz));
164
- fs.writeFileSync(extismDest, buf);
165
- fs.chmodSync(extismDest, 0o755);
166
- fs.unlinkSync(gz);
167
- }
168
-
169
- const wasmMergeDest = path.join(cacheDir, "wasm-merge");
170
- const wasmOptDest = path.join(cacheDir, "wasm-opt");
171
- if (!fs.existsSync(wasmMergeDest) || !fs.existsSync(wasmOptDest)) {
172
- const tar = path.join(cacheDir, "binaryen.tar.gz");
173
- console.log(`[plugin-sdk] downloading binaryen ${BINARYEN_VERSION}...`);
174
- await download(binaryenURL(), tar);
175
- execFileSync("tar", ["xzf", tar, "-C", cacheDir]);
176
- const extracted = path.join(cacheDir, `binaryen-${BINARYEN_VERSION}`);
177
- fs.copyFileSync(path.join(extracted, "bin", "wasm-merge"), wasmMergeDest);
178
- fs.copyFileSync(path.join(extracted, "bin", "wasm-opt"), wasmOptDest);
179
- fs.chmodSync(wasmMergeDest, 0o755);
180
- fs.chmodSync(wasmOptDest, 0o755);
181
- // copy lib too, wasm-opt links against libbinaryen.so on linux
182
- const libSrc = path.join(extracted, "lib");
183
- if (fs.existsSync(libSrc)) {
184
- fs.cpSync(libSrc, path.join(cacheDir, "lib"), { recursive: true });
185
- }
186
- fs.rmSync(extracted, { recursive: true });
187
- fs.unlinkSync(tar);
188
- }
189
-
190
135
  // owncast-plugin-test + owncast-plugin-serve, built from this repo's
191
136
  // host-runtime/ Go sources, published as gzipped release assets on
192
137
  // github.com/owncast/plugin-sdk (roughly halves the download). Skip silently
@@ -211,7 +156,7 @@ async function main() {
211
156
  fs.chmodSync(dest, 0o755);
212
157
  fs.unlinkSync(gz);
213
158
  } catch (e) {
214
- // 404 is expected before the first release; other errors get a soft
159
+ // 404 is expected before the first release. Other errors get a soft
215
160
  // warning so the user sees them but the install still succeeds.
216
161
  console.warn(
217
162
  `[plugin-sdk] could not fetch ${binary}: ${e.message}\n` +
package/slug.js ADDED
@@ -0,0 +1,26 @@
1
+ // slugify mirrors the host's Go slugify and the Python SDK's: ASCII letters and
2
+ // digits pass through lowercased, everything else collapses to a single hyphen,
3
+ // and trailing hyphens are trimmed. Shared by the build CLI and the JS test API
4
+ // so a manifest that omits `slug` resolves to the same `<slug>.js` artifact name
5
+ // everywhere. Non-ASCII names (e.g. "Café") degrade noisily (-> "caf"), so pin
6
+ // `slug` in the manifest for accented or non-Latin display names.
7
+ function slugify(input) {
8
+ let out = "";
9
+ let prevHyphen = false;
10
+ for (const ch of input) {
11
+ const code = ch.codePointAt(0);
12
+ let lower = ch;
13
+ if (code >= 65 && code <= 90) lower = String.fromCodePoint(code + 32);
14
+ const lc = lower.codePointAt(0);
15
+ if ((lc >= 97 && lc <= 122) || (lc >= 48 && lc <= 57)) {
16
+ out += lower;
17
+ prevHyphen = false;
18
+ } else if (!prevHyphen && out.length > 0) {
19
+ out += "-";
20
+ prevHyphen = true;
21
+ }
22
+ }
23
+ return out.replace(/-+$/, "");
24
+ }
25
+
26
+ module.exports = { slugify };
package/testing.js CHANGED
@@ -10,9 +10,9 @@
10
10
  //
11
11
  // const { runScenarios } = require("@owncast/plugin-sdk/testing");
12
12
  //
13
- // const chat = (user, body) => ({
13
+ // const chat = (name, body) => ({
14
14
  // event: "chat.message.received",
15
- // payload: { id: "1", user, body, timestamp: "2024-01-01T00:00:00Z" },
15
+ // payload: { id: "1", user: { id: name, displayName: name }, body, timestamp: "2024-01-01T00:00:00Z" },
16
16
  // });
17
17
  //
18
18
  // runScenarios([
@@ -28,35 +28,12 @@ const fs = require("fs");
28
28
  const os = require("os");
29
29
  const path = require("path");
30
30
  const { execFileSync } = require("child_process");
31
+ const { slugify } = require("./slug");
31
32
 
32
33
  // Find the directory holding the owncast-plugin-test binary. Check for that
33
34
  // binary specifically (not just any toolchain file) so we correctly fall
34
35
  // through to the dev tools/ dir when postinstall has only fetched part of the
35
36
  // toolchain (e.g., on a not-yet-released SDK version).
36
- // slugifyForTest mirrors the slugify in the build CLI + host SDKs so
37
- // this entrypoint can locate the .wasm file when a manifest omits
38
- // `slug`. ASCII letters and digits pass through lowercased;
39
- // everything else collapses to a single hyphen; trailing hyphens are
40
- // trimmed.
41
- function slugifyForTest(input) {
42
- let out = "";
43
- let prevHyphen = false;
44
- for (const ch of input) {
45
- const code = ch.codePointAt(0);
46
- let lower = ch;
47
- if (code >= 65 && code <= 90) lower = String.fromCodePoint(code + 32);
48
- const lc = lower.codePointAt(0);
49
- if ((lc >= 97 && lc <= 122) || (lc >= 48 && lc <= 57)) {
50
- out += lower;
51
- prevHyphen = false;
52
- } else if (!prevHyphen && out.length > 0) {
53
- out += "-";
54
- prevHyphen = true;
55
- }
56
- }
57
- return out.replace(/-+$/, "");
58
- }
59
-
60
37
  function findCacheDir() {
61
38
  const candidates = [
62
39
  path.join(__dirname, "bin", ".cache"), // installed under node_modules
@@ -81,7 +58,7 @@ function findCacheDir() {
81
58
  * Sets `process.exitCode` to non-zero if any scenario failed (never resets a
82
59
  * previously-failed code), and returns true on success / false on failure
83
60
  * WITHOUT exiting the process. That lets one node process run several test
84
- * files in a row (see runScenarioFiles); the process ends with the right code
61
+ * files in a row (see runScenarioFiles). The process ends with the right code
85
62
  * once the event loop drains.
86
63
  *
87
64
  * @param {Array<object>} scenarios, scenario objects: { name, given?, events, expect? }
@@ -113,20 +90,22 @@ function runScenarios(scenarios, opts = {}) {
113
90
  console.error("manifest.name is required");
114
91
  return fail(2);
115
92
  }
116
- // wasm + symlink filenames key off slug (the identifier), not the
117
- // display name. Derive the slug here the same way the build CLI
118
- // does so this entrypoint works on manifests that omit `slug`.
119
- const slug = manifest.slug || slugifyForTest(manifest.name);
93
+ // The built artifact + symlink filenames key off slug (the identifier), not
94
+ // the display name. Derive the slug here the same way the build CLI does so
95
+ // this entrypoint works on manifests that omit `slug`.
96
+ const slug = manifest.slug || slugify(manifest.name);
120
97
  if (!slug) {
121
98
  console.error(
122
99
  `could not derive slug from manifest.name ${JSON.stringify(manifest.name)}; set manifest.slug explicitly`,
123
100
  );
124
101
  return fail(2);
125
102
  }
126
- const wasmPath = path.join(cwd, `${slug}.wasm`);
127
- if (!fs.existsSync(wasmPath)) {
103
+ // Plugins ship as source: `owncast-plugin build` bundles src/plugin.{js,ts}
104
+ // into <slug>.js, which is what the host loads. (`npm test` builds first.)
105
+ const scriptPath = path.join(cwd, `${slug}.js`);
106
+ if (!fs.existsSync(scriptPath)) {
128
107
  console.error(
129
- `${slug}.wasm not found at ${wasmPath}, run \`owncast-plugin package\` first`,
108
+ `${slug}.js not found at ${scriptPath}, run \`owncast-plugin build\` first`,
130
109
  );
131
110
  return fail(2);
132
111
  }
@@ -141,32 +120,20 @@ function runScenarios(scenarios, opts = {}) {
141
120
  return fail(2);
142
121
  }
143
122
 
144
- // Build a temp project dir that links to the wasm + manifest and contains
145
- // only the scenarios we're running. The binary will auto-discover them.
123
+ // Build a temp project dir that links to the built script + manifest and
124
+ // contains only the scenarios we're running. The binary auto-discovers them.
146
125
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "owncast-plugin-test-"));
147
126
  try {
148
127
  fs.symlinkSync(manifestPath, path.join(tmp, "plugin.manifest.json"));
149
- fs.symlinkSync(wasmPath, path.join(tmp, `${slug}.wasm`));
128
+ fs.symlinkSync(scriptPath, path.join(tmp, `${slug}.js`));
150
129
  fs.mkdirSync(path.join(tmp, "__tests__"));
151
130
  fs.writeFileSync(
152
131
  path.join(tmp, "__tests__", "scenarios.test.json"),
153
132
  JSON.stringify(scenarios, null, 2),
154
133
  );
155
134
 
156
- // Match the build CLI: extism-js (and its wasm-merge/wasm-opt
157
- // children) needs LD_LIBRARY_PATH on Linux and DYLD_LIBRARY_PATH +
158
- // DYLD_FALLBACK_LIBRARY_PATH on macOS to find libbinaryen via
159
- // @rpath. Setting all three is safe on both OSes; the inactive
160
- // ones are ignored.
161
- const libDir = path.join(cache, "lib");
162
- const env = {
163
- ...process.env,
164
- LD_LIBRARY_PATH: `${libDir}:${process.env.LD_LIBRARY_PATH || ""}`,
165
- DYLD_LIBRARY_PATH: `${libDir}:${process.env.DYLD_LIBRARY_PATH || ""}`,
166
- DYLD_FALLBACK_LIBRARY_PATH: `${libDir}:${process.env.DYLD_FALLBACK_LIBRARY_PATH || "/usr/local/lib:/usr/lib"}`,
167
- };
168
135
  try {
169
- execFileSync(bin, [tmp], { stdio: "inherit", env });
136
+ execFileSync(bin, [tmp], { stdio: "inherit" });
170
137
  } catch (e) {
171
138
  return fail(typeof e.status === "number" ? e.status : 1);
172
139
  }