@owncast/plugin-sdk 0.1.0 → 0.3.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.
@@ -2,13 +2,13 @@
2
2
  // Downloads per-platform tooling into <sdk>/bin/.cache so the build CLI
3
3
  // finds it 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
- // - owncast-plugin-test/serve scenario runner + dev server (this repo's releases)
5
+ // - extism-js , JS → wasm compiler (extism/js-pdk releases)
6
+ // - wasm-merge, wasm-opt, lib , binaryen post-processing (WebAssembly/binaryen releases)
7
+ // - owncast-plugin-test/serve , scenario runner + dev server (this repo's releases)
8
8
  //
9
9
  // PoC scope: linux-x86_64 + darwin-arm64 + darwin-x86_64 covered.
10
10
  // owncast-plugin-test/serve downloads gracefully skip if the matching
11
- // release asset isn't published yet dev builds can substitute their own
11
+ // release asset isn't published yet, dev builds can substitute their own
12
12
  // via tools/bootstrap.sh.
13
13
 
14
14
  const fs = require("fs");
@@ -41,7 +41,7 @@ function extismJsURL() {
41
41
  "linux-x86_64": `extism-js-x86_64-linux-${EXTISM_JS_VERSION}.gz`,
42
42
  "linux-aarch64": `extism-js-aarch64-linux-${EXTISM_JS_VERSION}.gz`,
43
43
  "darwin-x86_64": `extism-js-x86_64-macos-${EXTISM_JS_VERSION}.gz`,
44
- "darwin-arm64": `extism-js-aarch64-macos-${EXTISM_JS_VERSION}.gz`
44
+ "darwin-arm64": `extism-js-aarch64-macos-${EXTISM_JS_VERSION}.gz`,
45
45
  };
46
46
  const file = map[platformKey()];
47
47
  return `https://github.com/extism/js-pdk/releases/download/${EXTISM_JS_VERSION}/${file}`;
@@ -52,7 +52,7 @@ function binaryenURL() {
52
52
  "linux-x86_64": `binaryen-${BINARYEN_VERSION}-x86_64-linux.tar.gz`,
53
53
  "linux-aarch64": `binaryen-${BINARYEN_VERSION}-aarch64-linux.tar.gz`,
54
54
  "darwin-x86_64": `binaryen-${BINARYEN_VERSION}-x86_64-macos.tar.gz`,
55
- "darwin-arm64": `binaryen-${BINARYEN_VERSION}-arm64-macos.tar.gz`
55
+ "darwin-arm64": `binaryen-${BINARYEN_VERSION}-arm64-macos.tar.gz`,
56
56
  };
57
57
  const file = map[platformKey()];
58
58
  return `https://github.com/WebAssembly/binaryen/releases/download/${BINARYEN_VERSION}/${file}`;
@@ -65,7 +65,7 @@ function hostBinaryURL(name) {
65
65
  "linux-x86_64": "linux-amd64",
66
66
  "linux-aarch64": "linux-arm64",
67
67
  "darwin-x86_64": "darwin-amd64",
68
- "darwin-arm64": "darwin-arm64"
68
+ "darwin-arm64": "darwin-arm64",
69
69
  };
70
70
  const suffix = map[platformKey()];
71
71
  return `https://github.com/${HOST_BINARIES_REPO}/releases/download/v${HOST_BINARIES_VERSION}/${name}-${suffix}`;
@@ -75,8 +75,10 @@ function download(url, dest) {
75
75
  return new Promise((resolve, reject) => {
76
76
  const req = (u) =>
77
77
  https.get(u, (res) => {
78
- if (res.statusCode === 302 || res.statusCode === 301) return req(res.headers.location);
79
- if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode} for ${u}`));
78
+ if (res.statusCode === 302 || res.statusCode === 301)
79
+ return req(res.headers.location);
80
+ if (res.statusCode !== 200)
81
+ return reject(new Error(`HTTP ${res.statusCode} for ${u}`));
80
82
  const out = fs.createWriteStream(dest);
81
83
  res.pipe(out);
82
84
  out.on("finish", () => out.close(resolve));
@@ -113,7 +115,7 @@ async function main() {
113
115
  fs.copyFileSync(path.join(extracted, "bin", "wasm-opt"), wasmOptDest);
114
116
  fs.chmodSync(wasmMergeDest, 0o755);
115
117
  fs.chmodSync(wasmOptDest, 0o755);
116
- // copy lib too wasm-opt links against libbinaryen.so on linux
118
+ // copy lib too, wasm-opt links against libbinaryen.so on linux
117
119
  const libSrc = path.join(extracted, "lib");
118
120
  if (fs.existsSync(libSrc)) {
119
121
  fs.cpSync(libSrc, path.join(cacheDir, "lib"), { recursive: true });
@@ -122,7 +124,7 @@ async function main() {
122
124
  fs.unlinkSync(tar);
123
125
  }
124
126
 
125
- // owncast-plugin-test + owncast-plugin-serve built from this repo's
127
+ // owncast-plugin-test + owncast-plugin-serve, built from this repo's
126
128
  // host-runtime/ Go sources, published as gzipped release assets on
127
129
  // github.com/owncast/plugin-sdk (roughly halves the download). Skip silently
128
130
  // if the release doesn't exist yet (dev environments running against a
@@ -133,7 +135,9 @@ async function main() {
133
135
  if (fs.existsSync(dest)) continue;
134
136
  const gz = dest + ".gz";
135
137
  try {
136
- console.log(`[plugin-sdk] downloading ${binary} ${HOST_BINARIES_VERSION}...`);
138
+ console.log(
139
+ `[plugin-sdk] downloading ${binary} ${HOST_BINARIES_VERSION}...`,
140
+ );
137
141
  await download(hostBinaryURL(binary) + ".gz", gz);
138
142
  fs.writeFileSync(dest, zlib.gunzipSync(fs.readFileSync(gz)));
139
143
  fs.chmodSync(dest, 0o755);
@@ -143,7 +147,7 @@ async function main() {
143
147
  // warning so the user sees them but the install still succeeds.
144
148
  console.warn(
145
149
  `[plugin-sdk] could not fetch ${binary}: ${e.message}\n` +
146
- ` Build locally via tools/bootstrap.sh, or use the latest GitHub release.`
150
+ ` Build locally via tools/bootstrap.sh, or use the latest GitHub release.`,
147
151
  );
148
152
  // Make sure no partial files are left behind.
149
153
  for (const p of [gz, dest]) if (fs.existsSync(p)) fs.unlinkSync(p);
package/testing.js ADDED
@@ -0,0 +1,158 @@
1
+ // JavaScript test API for Owncast plugins.
2
+ //
3
+ // Lets authors write their __tests__/*.test.js with the full ergonomics of JS
4
+ //, loops, helpers, fixtures, computed payloads, shared setup, instead of
5
+ // hand-authoring static JSON. Each call to `runScenarios([...])` invokes the
6
+ // same `owncast-plugin-test` host binary the JSON scenarios use, so this is
7
+ // purely a more pleasant authoring layer over the same execution.
8
+ //
9
+ // Quick start:
10
+ //
11
+ // const { runScenarios } = require("@owncast/plugin-sdk/testing");
12
+ //
13
+ // const chat = (user, body) => ({
14
+ // event: "chat.message.received",
15
+ // payload: { id: "1", user, body, timestamp: "2024-01-01T00:00:00Z" },
16
+ // });
17
+ //
18
+ // runScenarios([
19
+ // { name: "greets users", events: [chat("alice", "hi")], expect: { chatSends: ["hello, alice!"] } },
20
+ // { name: "ignores others", events: [chat("bob", "morning")], expect: { chatSends: [] } },
21
+ // ]);
22
+ //
23
+ // Each scenario object has the same shape as a JSON scenario file:
24
+ // { name, given?, events: [...], expect?: {...} }
25
+ // See the Plugin Author Guide for every assertion field.
26
+
27
+ const fs = require("fs");
28
+ const os = require("os");
29
+ const path = require("path");
30
+ const { execFileSync } = require("child_process");
31
+
32
+ // Find the directory holding the owncast-plugin-test binary. Check for that
33
+ // binary specifically (not just any toolchain file) so we correctly fall
34
+ // through to the dev tools/ dir when postinstall has only fetched part of the
35
+ // 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
+ function findCacheDir() {
61
+ const candidates = [
62
+ path.join(__dirname, "bin", ".cache"), // installed under node_modules
63
+ path.join(__dirname, "..", "..", "tools"), // dev fallback (repo root tools/)
64
+ ];
65
+ for (const c of candidates) {
66
+ if (fs.existsSync(path.join(c, "owncast-plugin-test"))) return c;
67
+ }
68
+ return candidates[0];
69
+ }
70
+
71
+ /**
72
+ * Run an array of scenarios against the loaded plugin via the
73
+ * `owncast-plugin-test` host binary.
74
+ *
75
+ * The binary takes a project directory and auto-discovers
76
+ * `__tests__/*.test.json`. To avoid colliding with any JSON scenarios you
77
+ * might also have in the project, this function sets up a temporary project
78
+ * dir that links to your manifest + wasm and contains only the generated
79
+ * scenarios it's running.
80
+ *
81
+ * Exits the process with status 0 if every scenario passed, non-zero otherwise.
82
+ *
83
+ * @param {Array<object>} scenarios, scenario objects: { name, given?, events, expect? }
84
+ * @param {object} [opts]
85
+ * @param {string} [opts.cwd], plugin project directory (default: process.cwd())
86
+ */
87
+ function runScenarios(scenarios, opts = {}) {
88
+ if (!Array.isArray(scenarios) || scenarios.length === 0) {
89
+ console.error("runScenarios: no scenarios provided");
90
+ process.exit(2);
91
+ }
92
+
93
+ const cwd = opts.cwd ? path.resolve(opts.cwd) : process.cwd();
94
+ const manifestPath = path.join(cwd, "plugin.manifest.json");
95
+ if (!fs.existsSync(manifestPath)) {
96
+ console.error(`plugin.manifest.json not found in ${cwd}`);
97
+ process.exit(2);
98
+ }
99
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
100
+ if (!manifest.name) {
101
+ console.error("manifest.name is required");
102
+ process.exit(2);
103
+ }
104
+ // wasm + symlink filenames key off slug (the identifier), not the
105
+ // display name. Derive the slug here the same way the build CLI
106
+ // does so this entrypoint works on manifests that omit `slug`.
107
+ const slug = manifest.slug || slugifyForTest(manifest.name);
108
+ if (!slug) {
109
+ console.error(
110
+ `could not derive slug from manifest.name ${JSON.stringify(manifest.name)}; set manifest.slug explicitly`,
111
+ );
112
+ process.exit(2);
113
+ }
114
+ const wasmPath = path.join(cwd, `${slug}.wasm`);
115
+ if (!fs.existsSync(wasmPath)) {
116
+ console.error(
117
+ `${slug}.wasm not found at ${wasmPath}, run \`owncast-plugin package\` first`,
118
+ );
119
+ process.exit(2);
120
+ }
121
+
122
+ const cache = findCacheDir();
123
+ const bin = path.join(cache, "owncast-plugin-test");
124
+ if (!fs.existsSync(bin)) {
125
+ console.error(
126
+ `owncast-plugin-test not found at ${bin}\n` +
127
+ `Reinstall @owncast/plugin-sdk to fetch the host toolchain (postinstall handles it).`,
128
+ );
129
+ process.exit(2);
130
+ }
131
+
132
+ // Build a temp project dir that links to the wasm + manifest and contains
133
+ // only the scenarios we're running. The binary will auto-discover them.
134
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "owncast-plugin-test-"));
135
+ try {
136
+ fs.symlinkSync(manifestPath, path.join(tmp, "plugin.manifest.json"));
137
+ fs.symlinkSync(wasmPath, path.join(tmp, `${slug}.wasm`));
138
+ fs.mkdirSync(path.join(tmp, "__tests__"));
139
+ fs.writeFileSync(
140
+ path.join(tmp, "__tests__", "scenarios.test.json"),
141
+ JSON.stringify(scenarios, null, 2),
142
+ );
143
+
144
+ const env = {
145
+ ...process.env,
146
+ LD_LIBRARY_PATH: `${path.join(cache, "lib")}:${process.env.LD_LIBRARY_PATH || ""}`,
147
+ };
148
+ try {
149
+ execFileSync(bin, [tmp], { stdio: "inherit", env });
150
+ } catch (e) {
151
+ process.exit(typeof e.status === "number" ? e.status : 1);
152
+ }
153
+ } finally {
154
+ fs.rmSync(tmp, { recursive: true, force: true });
155
+ }
156
+ }
157
+
158
+ module.exports = { runScenarios };