@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.
- package/README.md +12 -10
- package/bin/owncast-plugin.js +179 -55
- package/index.d.ts +44 -24
- package/index.js +250 -110
- package/package.json +3 -2
- package/scripts/postinstall.js +17 -13
- package/testing.js +158 -0
package/scripts/postinstall.js
CHANGED
|
@@ -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
|
|
6
|
-
// - wasm-merge, wasm-opt, lib
|
|
7
|
-
// - owncast-plugin-test/serve
|
|
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
|
|
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)
|
|
79
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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 };
|