domma-cms 1.10.21 → 1.10.23

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.10.23] - 2026-10-25
4
+
5
+ - **One plugin's broken event listener no longer stops the others.** Every `hooks.on(...)` listener now runs on its own: one that throws, or an async one that rejects, is logged, and the listeners after it still hear the event. Before, the first throw stopped the rest and reached the code that emitted it.
6
+
7
+ ## [1.10.22] - 2026-10-25
8
+
9
+ - Survey definition exports and imports preserve public or invitation-only access, supporting keyless public surveys in Surveys 1.8.0. Imported surveys still start as drafts.
10
+
3
11
  ## [1.10.21] - 2026-10-25
4
12
 
5
13
  - **A form in a footer block keeps cached pages up to date.** What a footer block draws (a `[form]`, its file cap) now tags every cached page that shows it, so saving the form or changing the cap reaches those pages at once instead of after the cache expires. Producers can add tags to a plain cached value with `{_cacheValue, _cacheTags}`.
@@ -375,7 +375,8 @@ export default async function myPlugin(fastify, options) {
375
375
  ### 1. Event Bus
376
376
 
377
377
  Subscribe to fire-and-forget lifecycle events emitted by the CMS. Handlers receive a payload object and must not return
378
- a value - errors thrown inside handlers are caught and logged.
378
+ a value - an error thrown inside a handler (or a rejected promise from an async one) is logged, and the other
379
+ handlers still run.
379
380
 
380
381
  ```js
381
382
  options.hooks.on('content:pageCreated', ({ page, urlPath }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.10.21",
3
+ "version": "1.10.23",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -409,19 +409,6 @@ export async function listMedia() {
409
409
  return stats.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
410
410
  }
411
411
 
412
- /**
413
- * Emit a media event without letting a listener undo the work: the hooks are a plain
414
- * EventEmitter, so a plugin's listener that throws would throw out of saveMedia after the
415
- * file was written - the caller would report a failure and never clean the file up.
416
- */
417
- function tellListeners(event, data) {
418
- try {
419
- hooks.emit(event, data);
420
- } catch (err) {
421
- console.warn(`[media] A ${event} listener failed: ${err?.message || err}`);
422
- }
423
- }
424
-
425
412
  /**
426
413
  * Save an uploaded file to the media directory.
427
414
  *
@@ -444,7 +431,7 @@ export async function saveMedia(filename, buffer, {exclusive = false} = {}) {
444
431
  } else {
445
432
  await fs.writeFile(filePath, buffer);
446
433
  }
447
- tellListeners('content:mediaUploaded', {filename});
434
+ hooks.emit('content:mediaUploaded', {filename});
448
435
  return { name: filename, url: `/media/${filename}` };
449
436
  }
450
437
 
@@ -457,7 +444,7 @@ export async function saveMedia(filename, buffer, {exclusive = false} = {}) {
457
444
  export async function deleteMedia(filename) {
458
445
  const filePath = path.join(MEDIA_DIR, filename);
459
446
  await fs.unlink(filePath);
460
- tellListeners('content:mediaDeleted', {filename});
447
+ hooks.emit('content:mediaDeleted', {filename});
461
448
  }
462
449
 
463
450
  /**
@@ -1,7 +1,7 @@
1
1
  /** Versioned, definition-only transfers. Runtime entries and audience IDs stay on their site. */
2
2
  const KEYS = {
3
3
  form: ['slug', 'title', 'description', 'fields', 'settings', 'actions'],
4
- survey: ['slug', 'title', 'description', 'formSlug', 'mode', 'reminderAfterDays', 'intro', 'footer', 'thankYou', 'inviteEmail'],
4
+ survey: ['slug', 'title', 'description', 'formSlug', 'access', 'mode', 'reminderAfterDays', 'intro', 'footer', 'thankYou', 'inviteEmail'],
5
5
  campaign: ['slug', 'title', 'description', 'message', 'linkUrl', 'endsAt', 'offer']
6
6
  };
7
7
  export function definition(kind, value) {
@@ -12,7 +12,30 @@ import {EventEmitter} from 'node:events';
12
12
  // 1. Event bus
13
13
  // ---------------------------------------------------------------------------
14
14
 
15
- const hooks = new EventEmitter();
15
+ /**
16
+ * The events are fire-and-forget, so one listener must not stop the others or reach the code
17
+ * that emitted: a plain EventEmitter stops at the first listener that throws and throws out of
18
+ * emit(). Each listener runs on its own here; a throw or a rejected promise is logged.
19
+ */
20
+ class HookBus extends EventEmitter {
21
+ emit(event, ...args) {
22
+ if (event === 'error') return super.emit(event, ...args);
23
+ const listeners = this.rawListeners(event);
24
+ if (!listeners.length) return false;
25
+ const failed = (err) => console.warn(`[hooks] A ${String(event)} listener failed: ${err?.message || err}`);
26
+ for (const listener of listeners) {
27
+ try {
28
+ const result = listener.apply(this, args);
29
+ if (typeof result?.then === 'function') result.then(null, failed);
30
+ } catch (err) {
31
+ failed(err);
32
+ }
33
+ }
34
+ return true;
35
+ }
36
+ }
37
+
38
+ const hooks = new HookBus();
16
39
  hooks.on('error', (err) => console.error('[hooks]', err));
17
40
 
18
41
  export {hooks};