@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/README.md +6 -6
- package/bin/owncast-plugin.js +57 -250
- package/index.d.ts +181 -77
- package/index.js +221 -242
- package/package.json +2 -1
- package/scripts/postinstall.js +11 -66
- package/slug.js +26 -0
- package/testing.js +17 -50
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owncast/plugin-sdk",
|
|
3
|
-
"version": "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"
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Downloads
|
|
3
|
-
//
|
|
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
|
|
27
|
-
// renamed (a removal is a breaking change that requires a major bump)
|
|
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
|
|
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
|
|
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 = (
|
|
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)
|
|
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
|
-
//
|
|
117
|
-
// display name. Derive the slug here the same way the build CLI
|
|
118
|
-
//
|
|
119
|
-
const slug = manifest.slug ||
|
|
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
|
-
|
|
127
|
-
|
|
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}.
|
|
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
|
|
145
|
-
// only the scenarios we're running. The binary
|
|
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(
|
|
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"
|
|
136
|
+
execFileSync(bin, [tmp], { stdio: "inherit" });
|
|
170
137
|
} catch (e) {
|
|
171
138
|
return fail(typeof e.status === "number" ? e.status : 1);
|
|
172
139
|
}
|