@owncast/plugin-sdk 0.3.0 → 0.4.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.
@@ -25,6 +25,27 @@ function fail(e) {
25
25
  process.exit(1);
26
26
  }
27
27
 
28
+ // toolchainEnv extends the current environment with the variables
29
+ // the dynamic linker needs to find `libbinaryen` next to `wasm-merge`
30
+ // and `wasm-opt` (which extism-js shells out to during the wasm
31
+ // pipeline). Linux uses LD_LIBRARY_PATH; macOS uses DYLD_LIBRARY_PATH
32
+ // plus DYLD_FALLBACK_LIBRARY_PATH (Apple Silicon strips
33
+ // DYLD_LIBRARY_PATH in some sandboxed contexts, the FALLBACK
34
+ // variant survives). Setting all three is safe on both OSes; the
35
+ // inactive ones are ignored. This is the difference between "build
36
+ // succeeds" and `library not loaded: @rpath/libbinaryen.dylib` on
37
+ // macOS.
38
+ function toolchainEnv(cache) {
39
+ const libDir = path.join(cache, "lib");
40
+ return {
41
+ ...process.env,
42
+ PATH: `${cache}:${process.env.PATH}`,
43
+ LD_LIBRARY_PATH: `${libDir}:${process.env.LD_LIBRARY_PATH || ""}`,
44
+ DYLD_LIBRARY_PATH: `${libDir}:${process.env.DYLD_LIBRARY_PATH || ""}`,
45
+ DYLD_FALLBACK_LIBRARY_PATH: `${libDir}:${process.env.DYLD_FALLBACK_LIBRARY_PATH || "/usr/local/lib:/usr/lib"}`,
46
+ };
47
+ }
48
+
28
49
  // slugPattern matches a valid plugin slug: a lowercase letter
29
50
  // followed by lowercase letters/digits/hyphens, up to 64 chars total.
30
51
  // Same shape the host + SDK + registry all validate against.
@@ -106,10 +127,7 @@ function runBinary(name, args) {
106
127
  );
107
128
  process.exit(1);
108
129
  }
109
- const env = {
110
- ...process.env,
111
- LD_LIBRARY_PATH: `${path.join(cache, "lib")}:${process.env.LD_LIBRARY_PATH || ""}`,
112
- };
130
+ const env = toolchainEnv(cache);
113
131
  try {
114
132
  execFileSync(bin, args.length > 0 ? args : [process.cwd()], {
115
133
  stdio: "inherit",
@@ -212,11 +230,7 @@ module.exports = { register, on_event, on_filter, on_http_request };
212
230
  `extism-js not found at ${extismJs}, run \`npm install\` to fetch the toolchain`,
213
231
  );
214
232
  }
215
- const env = {
216
- ...process.env,
217
- PATH: `${cache}:${process.env.PATH}`,
218
- LD_LIBRARY_PATH: `${path.join(cache, "lib")}:${process.env.LD_LIBRARY_PATH || ""}`,
219
- };
233
+ const env = toolchainEnv(cache);
220
234
 
221
235
  const wasmOut = path.join(cwd, `${slug}.wasm`);
222
236
  execFileSync(extismJs, [bundledJS, "-i", dts, "-o", wasmOut], {
@@ -224,50 +238,18 @@ module.exports = { register, on_event, on_filter, on_http_request };
224
238
  env,
225
239
  });
226
240
 
227
- // If the project ships static assets in ./assets/, mirror them to the
228
- // canonical deployment layout (<name>-assets/) so plugin.Server finds them
229
- // without per-deployment renames. We use a symlink so edits to assets/
230
- // show up live during dev (no rebuild needed for HTML/CSS changes).
231
- const assetsSrc = path.join(cwd, "assets");
232
- if (fs.existsSync(assetsSrc) && fs.statSync(assetsSrc).isDirectory()) {
233
- const assetsDest = path.join(cwd, `${slug}-assets`);
234
- let needsLink = true;
235
- // Use lstatSync (not existsSync), existsSync follows symlinks and
236
- // returns false for a dangling link, but the link's inode is still
237
- // there and would make symlinkSync below fail with EEXIST. lstatSync
238
- // sees the link itself regardless of whether its target resolves.
239
- let st;
240
- try {
241
- st = fs.lstatSync(assetsDest);
242
- } catch {
243
- // path doesn't exist at all, fall through to create it.
244
- }
245
- if (st) {
246
- let target;
247
- if (st.isSymbolicLink()) {
248
- // realpathSync throws on dangling links; treat that as "doesn't
249
- // match, replace it" rather than letting it abort the build.
250
- try {
251
- target = fs.realpathSync(assetsDest);
252
- } catch {}
253
- }
254
- if (target && target === fs.realpathSync(assetsSrc)) {
255
- needsLink = false;
256
- } else {
257
- fs.rmSync(assetsDest, { recursive: true, force: true });
258
- }
259
- }
260
- if (needsLink) {
261
- fs.symlinkSync(path.resolve(assetsSrc), assetsDest, "dir");
262
- }
263
- }
241
+ // public/ and assets/ live at the source root; the host's
242
+ // loose-files loader picks them up as siblings of the built
243
+ // <slug>.wasm without any rename, so the build CLI doesn't need to
244
+ // create or mirror anything for them.
264
245
 
265
246
  console.log(`built ${path.relative(cwd, wasmOut)}`);
266
247
  }
267
248
 
268
249
  // `owncast-plugin package`, bundle the project into a single .ocpkg file
269
- // (zip archive with plugin.manifest.json, plugin.wasm, and optional assets/).
270
- // Builds the wasm first if it doesn't exist.
250
+ // (zip archive with plugin.manifest.json, plugin.wasm, and optional
251
+ // public/ and assets/ directories). Builds the wasm first if it
252
+ // doesn't exist.
271
253
  async function packageMain() {
272
254
  const cwd = process.cwd();
273
255
  const manifestPath = path.join(cwd, "plugin.manifest.json");
@@ -282,6 +264,7 @@ async function packageMain() {
282
264
  await buildMain();
283
265
  }
284
266
 
267
+ const publicDir = path.join(cwd, "public");
285
268
  const assetsDir = path.join(cwd, "assets");
286
269
  const zip = new JSZip();
287
270
  zip.file("plugin.manifest.json", fs.readFileSync(manifestPath));
@@ -296,6 +279,26 @@ async function packageMain() {
296
279
  zip.file("icon.png", fs.readFileSync(iconPath));
297
280
  fileCount++;
298
281
  }
282
+ // Bundle a top-level INSTRUCTIONS.md if the plugin source root has one.
283
+ // The host serves it to the admin (which renders it as markdown in a
284
+ // details tab); like icon.png it needs no manifest field and no
285
+ // http.serve permission. The filename is fixed for simplicity.
286
+ const instructionsPath = path.join(cwd, "INSTRUCTIONS.md");
287
+ if (fs.existsSync(instructionsPath) && fs.statSync(instructionsPath).isFile()) {
288
+ zip.file("INSTRUCTIONS.md", fs.readFileSync(instructionsPath));
289
+ fileCount++;
290
+ }
291
+ // public/ → /plugins/<slug>/<path>, served by the host.
292
+ if (fs.existsSync(publicDir) && fs.statSync(publicDir).isDirectory()) {
293
+ for (const file of walkFiles(publicDir)) {
294
+ const rel = path.relative(publicDir, file).split(path.sep).join("/");
295
+ zip.file(`public/${rel}`, fs.readFileSync(file));
296
+ fileCount++;
297
+ }
298
+ }
299
+ // assets/ → host reads internally for manifest fields that inline
300
+ // file contents (styles, scripts, extraPageContent). Not served at
301
+ // a URL.
299
302
  if (fs.existsSync(assetsDir) && fs.statSync(assetsDir).isDirectory()) {
300
303
  for (const file of walkFiles(assetsDir)) {
301
304
  const rel = path.relative(assetsDir, file).split(path.sep).join("/");
@@ -315,12 +318,29 @@ async function packageMain() {
315
318
  console.log(
316
319
  `packaged ${path.relative(cwd, outPath)} (${sizeKb} KB, ${fileCount} files)`,
317
320
  );
321
+
322
+ // Drop the intermediate <slug>.wasm now that it's bundled inside the
323
+ // .ocpkg. The .ocpkg is the only artifact authors care about: leaving
324
+ // the loose .wasm next to it just confuses "what do I ship". Only
325
+ // runs on a successful package so a mid-pipeline failure leaves the
326
+ // last good build in place for debugging.
327
+ try {
328
+ fs.unlinkSync(wasmPath);
329
+ } catch (e) {
330
+ // Don't fail the package step over a cleanup miss. The .ocpkg is
331
+ // already written; surface the warning so the author notices the
332
+ // straggler but treat the run as successful.
333
+ if (e.code !== "ENOENT") {
334
+ console.warn(`warning: could not clean up ${path.relative(cwd, wasmPath)}: ${e.message}`);
335
+ }
336
+ }
318
337
  }
319
338
 
320
339
  function* walkFiles(dir) {
321
340
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
322
- // Resolve symlinks so the assets/ <name>-assets/ link the build CLI
323
- // makes doesn't cause us to skip files. statSync follows.
341
+ // statSync (not lstatSync) so a symlinked file or directory in
342
+ // the source tree resolves to its target and we read its contents
343
+ // rather than skipping it.
324
344
  const full = path.join(dir, entry.name);
325
345
  let info;
326
346
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owncast/plugin-sdk",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "SDK for authoring Owncast plugins in JavaScript",
5
5
  "license": "MIT",
6
6
  "author": "Owncast",
package/testing.js CHANGED
@@ -141,9 +141,17 @@ function runScenarios(scenarios, opts = {}) {
141
141
  JSON.stringify(scenarios, null, 2),
142
142
  );
143
143
 
144
+ // Match the build CLI: extism-js (and its wasm-merge/wasm-opt
145
+ // children) needs LD_LIBRARY_PATH on Linux and DYLD_LIBRARY_PATH +
146
+ // DYLD_FALLBACK_LIBRARY_PATH on macOS to find libbinaryen via
147
+ // @rpath. Setting all three is safe on both OSes; the inactive
148
+ // ones are ignored.
149
+ const libDir = path.join(cache, "lib");
144
150
  const env = {
145
151
  ...process.env,
146
- LD_LIBRARY_PATH: `${path.join(cache, "lib")}:${process.env.LD_LIBRARY_PATH || ""}`,
152
+ LD_LIBRARY_PATH: `${libDir}:${process.env.LD_LIBRARY_PATH || ""}`,
153
+ DYLD_LIBRARY_PATH: `${libDir}:${process.env.DYLD_LIBRARY_PATH || ""}`,
154
+ DYLD_FALLBACK_LIBRARY_PATH: `${libDir}:${process.env.DYLD_FALLBACK_LIBRARY_PATH || "/usr/local/lib:/usr/lib"}`,
147
155
  };
148
156
  try {
149
157
  execFileSync(bin, [tmp], { stdio: "inherit", env });