@momentumcms/plugins-core 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## 0.1.0 (2026-02-16)
2
+
3
+ This was a version bump only for plugins-core to align it with other projects, there were no code changes.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-present Momentum CMS Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/index.cjs ADDED
@@ -0,0 +1,530 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // libs/plugins/core/src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ MomentumEventBus: () => MomentumEventBus,
24
+ PluginFatalError: () => PluginFatalError,
25
+ PluginRunner: () => PluginRunner,
26
+ eventBusPlugin: () => eventBusPlugin,
27
+ injectCollectionEventHooks: () => injectCollectionEventHooks
28
+ });
29
+ module.exports = __toCommonJS(src_exports);
30
+
31
+ // libs/logger/src/lib/log-level.ts
32
+ var LOG_LEVEL_VALUES = {
33
+ debug: 0,
34
+ info: 1,
35
+ warn: 2,
36
+ error: 3,
37
+ fatal: 4,
38
+ silent: 5
39
+ };
40
+ function shouldLog(messageLevel, configuredLevel) {
41
+ return LOG_LEVEL_VALUES[messageLevel] >= LOG_LEVEL_VALUES[configuredLevel];
42
+ }
43
+
44
+ // libs/logger/src/lib/ansi-colors.ts
45
+ var ANSI = {
46
+ reset: "\x1B[0m",
47
+ bold: "\x1B[1m",
48
+ dim: "\x1B[2m",
49
+ // Foreground colors
50
+ red: "\x1B[31m",
51
+ green: "\x1B[32m",
52
+ yellow: "\x1B[33m",
53
+ blue: "\x1B[34m",
54
+ magenta: "\x1B[35m",
55
+ cyan: "\x1B[36m",
56
+ white: "\x1B[37m",
57
+ gray: "\x1B[90m",
58
+ // Background colors
59
+ bgRed: "\x1B[41m",
60
+ bgYellow: "\x1B[43m"
61
+ };
62
+ function colorize(text, ...codes) {
63
+ if (codes.length === 0)
64
+ return text;
65
+ return `${codes.join("")}${text}${ANSI.reset}`;
66
+ }
67
+ function supportsColor() {
68
+ if (process.env["FORCE_COLOR"] === "1")
69
+ return true;
70
+ if (process.env["NO_COLOR"] !== void 0)
71
+ return false;
72
+ if (process.env["TERM"] === "dumb")
73
+ return false;
74
+ return process.stdout.isTTY === true;
75
+ }
76
+
77
+ // libs/logger/src/lib/formatters.ts
78
+ var LEVEL_COLORS = {
79
+ debug: [ANSI.dim, ANSI.gray],
80
+ info: [ANSI.cyan],
81
+ warn: [ANSI.yellow],
82
+ error: [ANSI.red],
83
+ fatal: [ANSI.bold, ANSI.white, ANSI.bgRed]
84
+ };
85
+ function padLevel(level) {
86
+ return level.toUpperCase().padEnd(5);
87
+ }
88
+ function formatTimestamp(date) {
89
+ const y = date.getFullYear();
90
+ const mo = String(date.getMonth() + 1).padStart(2, "0");
91
+ const d = String(date.getDate()).padStart(2, "0");
92
+ const h = String(date.getHours()).padStart(2, "0");
93
+ const mi = String(date.getMinutes()).padStart(2, "0");
94
+ const s = String(date.getSeconds()).padStart(2, "0");
95
+ const ms = String(date.getMilliseconds()).padStart(3, "0");
96
+ return `${y}-${mo}-${d} ${h}:${mi}:${s}.${ms}`;
97
+ }
98
+ function formatData(data) {
99
+ const entries = Object.entries(data);
100
+ if (entries.length === 0)
101
+ return "";
102
+ return " " + entries.map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`).join(" ");
103
+ }
104
+ function prettyFormatter(entry) {
105
+ const useColor = supportsColor();
106
+ const level = entry.level;
107
+ const ts = formatTimestamp(entry.timestamp);
108
+ const levelStr = padLevel(entry.level);
109
+ const ctx = `[${entry.context}]`;
110
+ const msg = entry.message;
111
+ const enrichmentStr = entry.enrichments ? formatData(entry.enrichments) : "";
112
+ const dataStr = entry.data ? formatData(entry.data) : "";
113
+ const extra = `${enrichmentStr}${dataStr}`;
114
+ if (useColor) {
115
+ const colors = LEVEL_COLORS[level];
116
+ const coloredLevel = colorize(levelStr, ...colors);
117
+ const coloredCtx = colorize(ctx, ANSI.magenta);
118
+ const coloredTs = colorize(ts, ANSI.gray);
119
+ return `${coloredTs} ${coloredLevel} ${coloredCtx} ${msg}${extra}
120
+ `;
121
+ }
122
+ return `${ts} ${levelStr} ${ctx} ${msg}${extra}
123
+ `;
124
+ }
125
+ function jsonFormatter(entry) {
126
+ const output = {
127
+ timestamp: entry.timestamp.toISOString(),
128
+ level: entry.level,
129
+ context: entry.context,
130
+ message: entry.message
131
+ };
132
+ if (entry.enrichments && Object.keys(entry.enrichments).length > 0) {
133
+ Object.assign(output, entry.enrichments);
134
+ }
135
+ if (entry.data && Object.keys(entry.data).length > 0) {
136
+ output["data"] = entry.data;
137
+ }
138
+ return JSON.stringify(output) + "\n";
139
+ }
140
+
141
+ // libs/logger/src/lib/logger-config.types.ts
142
+ function resolveLoggingConfig(config) {
143
+ return {
144
+ level: config?.level ?? "info",
145
+ format: config?.format ?? "pretty",
146
+ timestamps: config?.timestamps ?? true,
147
+ output: config?.output ?? ((msg) => {
148
+ process.stdout.write(msg);
149
+ }),
150
+ errorOutput: config?.errorOutput ?? ((msg) => {
151
+ process.stderr.write(msg);
152
+ })
153
+ };
154
+ }
155
+
156
+ // libs/logger/src/lib/logger.ts
157
+ var ERROR_LEVELS = /* @__PURE__ */ new Set(["warn", "error", "fatal"]);
158
+ var MomentumLogger = class _MomentumLogger {
159
+ static {
160
+ this.enrichers = [];
161
+ }
162
+ constructor(context, config) {
163
+ this.context = context;
164
+ this.config = isResolvedConfig(config) ? config : resolveLoggingConfig(config);
165
+ this.formatter = this.config.format === "json" ? jsonFormatter : prettyFormatter;
166
+ }
167
+ debug(message, data) {
168
+ this.log("debug", message, data);
169
+ }
170
+ info(message, data) {
171
+ this.log("info", message, data);
172
+ }
173
+ warn(message, data) {
174
+ this.log("warn", message, data);
175
+ }
176
+ error(message, data) {
177
+ this.log("error", message, data);
178
+ }
179
+ fatal(message, data) {
180
+ this.log("fatal", message, data);
181
+ }
182
+ /**
183
+ * Creates a child logger with a sub-context.
184
+ * e.g., `Momentum:DB` → `Momentum:DB:Migrate`
185
+ */
186
+ child(subContext) {
187
+ return new _MomentumLogger(`${this.context}:${subContext}`, this.config);
188
+ }
189
+ /**
190
+ * Registers a global enricher that adds extra fields to all log entries.
191
+ */
192
+ static registerEnricher(enricher) {
193
+ _MomentumLogger.enrichers.push(enricher);
194
+ }
195
+ /**
196
+ * Removes a previously registered enricher.
197
+ */
198
+ static removeEnricher(enricher) {
199
+ const index = _MomentumLogger.enrichers.indexOf(enricher);
200
+ if (index >= 0) {
201
+ _MomentumLogger.enrichers.splice(index, 1);
202
+ }
203
+ }
204
+ /**
205
+ * Clears all registered enrichers. Primarily for testing.
206
+ */
207
+ static clearEnrichers() {
208
+ _MomentumLogger.enrichers.length = 0;
209
+ }
210
+ log(level, message, data) {
211
+ if (!shouldLog(level, this.config.level))
212
+ return;
213
+ const enrichments = this.collectEnrichments();
214
+ const entry = {
215
+ timestamp: /* @__PURE__ */ new Date(),
216
+ level,
217
+ context: this.context,
218
+ message,
219
+ data,
220
+ enrichments: Object.keys(enrichments).length > 0 ? enrichments : void 0
221
+ };
222
+ const formatted = this.formatter(entry);
223
+ if (ERROR_LEVELS.has(level)) {
224
+ this.config.errorOutput(formatted);
225
+ } else {
226
+ this.config.output(formatted);
227
+ }
228
+ }
229
+ collectEnrichments() {
230
+ const result = {};
231
+ for (const enricher of _MomentumLogger.enrichers) {
232
+ Object.assign(result, enricher.enrich());
233
+ }
234
+ return result;
235
+ }
236
+ };
237
+ function isResolvedConfig(config) {
238
+ if (!config)
239
+ return false;
240
+ return typeof config.level === "string" && typeof config.format === "string" && typeof config.timestamps === "boolean" && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- type guard narrows union
241
+ typeof config.output === "function" && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- type guard narrows union
242
+ typeof config.errorOutput === "function";
243
+ }
244
+
245
+ // libs/logger/src/lib/logger-singleton.ts
246
+ var loggerInstance = null;
247
+ var ROOT_CONTEXT = "Momentum";
248
+ function getMomentumLogger() {
249
+ if (!loggerInstance) {
250
+ loggerInstance = new MomentumLogger(ROOT_CONTEXT);
251
+ }
252
+ return loggerInstance;
253
+ }
254
+ function createLogger(context) {
255
+ return getMomentumLogger().child(context);
256
+ }
257
+
258
+ // libs/plugins/core/src/lib/plugin-fatal-error.ts
259
+ var PluginFatalError = class extends Error {
260
+ constructor(pluginName, message) {
261
+ super(`[Plugin:${pluginName}] Fatal: ${message}`);
262
+ this.name = "PluginFatalError";
263
+ this.pluginName = pluginName;
264
+ }
265
+ };
266
+
267
+ // libs/plugins/core/src/lib/plugin-runner.ts
268
+ var PluginRunner = class {
269
+ constructor(options) {
270
+ this.middlewareDescriptors = [];
271
+ this.providerDescriptors = [];
272
+ this.initialized = false;
273
+ this.ready = false;
274
+ this.plugins = options.plugins;
275
+ this.config = options.config;
276
+ this.collections = options.collections;
277
+ this.logger = createLogger("Plugins");
278
+ }
279
+ /**
280
+ * Create a plugin-scoped context.
281
+ */
282
+ createContext(plugin) {
283
+ return {
284
+ config: this.config,
285
+ collections: this.collections,
286
+ logger: this.logger.child(plugin.name),
287
+ registerMiddleware: (descriptor) => {
288
+ this.middlewareDescriptors.push(descriptor);
289
+ },
290
+ registerProvider: (descriptor) => {
291
+ this.providerDescriptors.push(descriptor);
292
+ }
293
+ };
294
+ }
295
+ /**
296
+ * Run onInit for all plugins (in order).
297
+ * Call this before API initialization.
298
+ */
299
+ async runInit() {
300
+ if (this.initialized) {
301
+ this.logger.warn("Plugins already initialized");
302
+ return;
303
+ }
304
+ for (const plugin of this.plugins) {
305
+ try {
306
+ if (plugin.onInit) {
307
+ this.logger.debug(`Initializing plugin: ${plugin.name}`);
308
+ await plugin.onInit(this.createContext(plugin));
309
+ }
310
+ } catch (error) {
311
+ if (error instanceof PluginFatalError) {
312
+ throw error;
313
+ }
314
+ const message = error instanceof Error ? error.message : String(error);
315
+ this.logger.error(`Plugin "${plugin.name}" failed during onInit: ${message}`);
316
+ }
317
+ }
318
+ this.initialized = true;
319
+ }
320
+ /**
321
+ * Run onReady for all plugins (in order).
322
+ * Call this after API + seeding complete.
323
+ */
324
+ async runReady(api) {
325
+ if (this.ready) {
326
+ this.logger.warn("Plugins already marked ready");
327
+ return;
328
+ }
329
+ for (const plugin of this.plugins) {
330
+ try {
331
+ if (plugin.onReady) {
332
+ this.logger.debug(`Plugin ready: ${plugin.name}`);
333
+ const context = {
334
+ ...this.createContext(plugin),
335
+ api
336
+ };
337
+ await plugin.onReady(context);
338
+ }
339
+ } catch (error) {
340
+ if (error instanceof PluginFatalError) {
341
+ throw error;
342
+ }
343
+ const message = error instanceof Error ? error.message : String(error);
344
+ this.logger.error(`Plugin "${plugin.name}" failed during onReady: ${message}`);
345
+ }
346
+ }
347
+ this.ready = true;
348
+ }
349
+ /**
350
+ * Run onShutdown for all plugins (in reverse order).
351
+ * Call this during graceful shutdown.
352
+ */
353
+ async runShutdown() {
354
+ const reversed = [...this.plugins].reverse();
355
+ for (const plugin of reversed) {
356
+ try {
357
+ if (plugin.onShutdown) {
358
+ this.logger.debug(`Shutting down plugin: ${plugin.name}`);
359
+ await plugin.onShutdown(this.createContext(plugin));
360
+ }
361
+ } catch (error) {
362
+ const message = error instanceof Error ? error.message : String(error);
363
+ this.logger.error(`Plugin "${plugin.name}" failed during onShutdown: ${message}`);
364
+ }
365
+ }
366
+ }
367
+ /**
368
+ * Get the list of registered plugin names.
369
+ */
370
+ getPluginNames() {
371
+ return this.plugins.map((p) => p.name);
372
+ }
373
+ /**
374
+ * Get middleware descriptors registered by plugins during onInit.
375
+ * Returns descriptors in the order they were registered.
376
+ */
377
+ getMiddleware() {
378
+ return [...this.middlewareDescriptors];
379
+ }
380
+ /**
381
+ * Get provider descriptors registered by plugins during onInit.
382
+ * Returns descriptors in the order they were registered.
383
+ */
384
+ getProviders() {
385
+ return [...this.providerDescriptors];
386
+ }
387
+ };
388
+
389
+ // libs/plugins/core/src/lib/hook-injector.ts
390
+ function injectCollectionEventHooks(collections, listener) {
391
+ for (const collection of collections) {
392
+ collection.hooks = collection.hooks ?? {};
393
+ const afterChangeHook = (args) => {
394
+ const operation = args.operation ?? "create";
395
+ const eventType = operation === "create" ? "afterChange" : "afterChange";
396
+ const event = {
397
+ collection: collection.slug,
398
+ event: eventType,
399
+ operation,
400
+ doc: args.doc ?? args.data,
401
+ previousDoc: args.originalDoc,
402
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
403
+ };
404
+ listener(event);
405
+ };
406
+ const existingAfterChange = collection.hooks.afterChange ?? [];
407
+ collection.hooks.afterChange = [...existingAfterChange, afterChangeHook];
408
+ const afterDeleteHook = (args) => {
409
+ const event = {
410
+ collection: collection.slug,
411
+ event: "afterDelete",
412
+ operation: "delete",
413
+ doc: args.doc,
414
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
415
+ };
416
+ listener(event);
417
+ };
418
+ const existingAfterDelete = collection.hooks.afterDelete ?? [];
419
+ collection.hooks.afterDelete = [...existingAfterDelete, afterDeleteHook];
420
+ }
421
+ }
422
+
423
+ // libs/plugins/core/src/lib/event-bus/event-bus.ts
424
+ var MomentumEventBus = class {
425
+ constructor() {
426
+ this.subscriptions = [];
427
+ }
428
+ /**
429
+ * Subscribe to events matching a pattern.
430
+ *
431
+ * @param pattern - Pattern string: "collection:event", "*:event", "collection:*", or "*"
432
+ * @param handler - Callback invoked when a matching event occurs
433
+ * @returns Unsubscribe function
434
+ *
435
+ * @example
436
+ * ```typescript
437
+ * bus.on('posts:afterChange', (event) => { ... });
438
+ * bus.on('*:afterDelete', (event) => { ... });
439
+ * const unsub = bus.on('*', (event) => { ... });
440
+ * unsub(); // remove subscription
441
+ * ```
442
+ */
443
+ on(pattern, handler) {
444
+ const subscription = { pattern, handler };
445
+ this.subscriptions.push(subscription);
446
+ return () => {
447
+ const index = this.subscriptions.indexOf(subscription);
448
+ if (index !== -1) {
449
+ this.subscriptions.splice(index, 1);
450
+ }
451
+ };
452
+ }
453
+ /**
454
+ * Subscribe to all events for a specific collection.
455
+ * Shorthand for `on("collection:*", handler)`.
456
+ */
457
+ onCollection(collection, handler) {
458
+ return this.on(`${collection}:*`, handler);
459
+ }
460
+ /**
461
+ * Subscribe to a specific event type across all collections.
462
+ * Shorthand for `on("*:event", handler)`.
463
+ */
464
+ onEvent(event, handler) {
465
+ return this.on(`*:${event}`, handler);
466
+ }
467
+ /**
468
+ * Emit a collection event to all matching subscribers.
469
+ */
470
+ emit(event) {
471
+ for (const sub of this.subscriptions) {
472
+ if (matchesPattern(sub.pattern, event.collection, event.event)) {
473
+ sub.handler(event);
474
+ }
475
+ }
476
+ }
477
+ /**
478
+ * Remove all subscriptions.
479
+ */
480
+ clear() {
481
+ this.subscriptions = [];
482
+ }
483
+ /**
484
+ * Get the current number of subscriptions.
485
+ */
486
+ get size() {
487
+ return this.subscriptions.length;
488
+ }
489
+ };
490
+ function matchesPattern(pattern, collection, event) {
491
+ if (pattern === "*") {
492
+ return true;
493
+ }
494
+ const parts = pattern.split(":");
495
+ if (parts.length !== 2) {
496
+ return false;
497
+ }
498
+ const [patternCollection, patternEvent] = parts;
499
+ const collectionMatch = patternCollection === "*" || patternCollection === collection;
500
+ const eventMatch = patternEvent === "*" || patternEvent === event;
501
+ return collectionMatch && eventMatch;
502
+ }
503
+
504
+ // libs/plugins/core/src/lib/event-bus/event-bus-plugin.ts
505
+ function eventBusPlugin() {
506
+ const bus = new MomentumEventBus();
507
+ return {
508
+ name: "event-bus",
509
+ bus,
510
+ onInit({ collections, logger }) {
511
+ logger.info("Injecting event hooks into collections");
512
+ injectCollectionEventHooks(collections, (event) => {
513
+ bus.emit(event);
514
+ });
515
+ logger.info(`Event hooks injected into ${collections.length} collections`);
516
+ },
517
+ onShutdown({ logger }) {
518
+ logger.info("Clearing event bus subscriptions");
519
+ bus.clear();
520
+ }
521
+ };
522
+ }
523
+ // Annotate the CommonJS export names for ESM import in node:
524
+ 0 && (module.exports = {
525
+ MomentumEventBus,
526
+ PluginFatalError,
527
+ PluginRunner,
528
+ eventBusPlugin,
529
+ injectCollectionEventHooks
530
+ });
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@momentumcms/plugins-core",
3
+ "version": "0.1.0",
4
+ "description": "Plugin system core and event bus for Momentum CMS",
5
+ "license": "MIT",
6
+ "author": "Momentum CMS Contributors",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/momentum-cms/momentum-cms.git",
10
+ "directory": "libs/plugins/core"
11
+ },
12
+ "homepage": "https://github.com/momentum-cms/momentum-cms#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/momentum-cms/momentum-cms/issues"
15
+ },
16
+ "keywords": [
17
+ "cms",
18
+ "momentum-cms",
19
+ "plugins",
20
+ "event-bus"
21
+ ],
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "type": "commonjs",
26
+ "main": "./index.cjs",
27
+ "types": "./src/index.d.ts",
28
+ "dependencies": {
29
+ "@momentumcms/core": "0.1.0",
30
+ "@momentumcms/logger": "0.1.0"
31
+ }
32
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export type { MomentumPlugin, PluginContext, PluginReadyContext, MomentumAPI, CollectionEvent, CollectionEventType, PluginMiddlewareDescriptor, PluginProviderDescriptor, PluginAdminRouteDescriptor, } from './lib/plugin.types';
2
+ export { PluginRunner, type PluginRunnerOptions } from './lib/plugin-runner';
3
+ export { PluginFatalError } from './lib/plugin-fatal-error';
4
+ export { injectCollectionEventHooks, type CollectionEventListener } from './lib/hook-injector';
5
+ export { MomentumEventBus, type EventHandler, type EventPattern } from './lib/event-bus/event-bus';
6
+ export { eventBusPlugin, type EventBusPlugin } from './lib/event-bus/event-bus-plugin';
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Event Bus Plugin
3
+ *
4
+ * A Momentum plugin that wires the event bus to collection hooks.
5
+ * Events are emitted for all collection CRUD operations.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { eventBusPlugin } from '@momentumcms/plugins/core';
10
+ *
11
+ * const events = eventBusPlugin();
12
+ *
13
+ * // Subscribe to events
14
+ * events.bus.on('posts:afterChange', (event) => {
15
+ * // cache invalidation, notifications, etc.
16
+ * });
17
+ *
18
+ * // Register in config
19
+ * export default defineMomentumConfig({
20
+ * plugins: [events],
21
+ * // ...
22
+ * });
23
+ * ```
24
+ */
25
+ import type { MomentumPlugin } from '../plugin.types';
26
+ import { MomentumEventBus } from './event-bus';
27
+ /**
28
+ * Event bus plugin with an accessible bus instance.
29
+ */
30
+ export interface EventBusPlugin extends MomentumPlugin {
31
+ /** The event bus instance — subscribe to events here */
32
+ bus: MomentumEventBus;
33
+ }
34
+ /**
35
+ * Creates an event bus plugin.
36
+ *
37
+ * The returned object has a `.bus` property for subscribing to events,
38
+ * and implements MomentumPlugin for lifecycle integration.
39
+ *
40
+ * @returns Plugin instance with event bus
41
+ */
42
+ export declare function eventBusPlugin(): EventBusPlugin;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Momentum Event Bus
3
+ *
4
+ * Lightweight pub/sub with pattern matching for collection events.
5
+ *
6
+ * Patterns:
7
+ * - "posts:afterChange" — specific collection + event
8
+ * - "*:afterDelete" — any collection, specific event
9
+ * - "posts:*" — specific collection, any event
10
+ * - "*" — all events
11
+ */
12
+ import type { CollectionEvent, CollectionEventType } from '../plugin.types';
13
+ /**
14
+ * Event handler callback.
15
+ */
16
+ export type EventHandler = (event: CollectionEvent) => void;
17
+ /**
18
+ * Subscription pattern string.
19
+ * Format: "collection:event" where either part can be "*" for wildcard.
20
+ */
21
+ export type EventPattern = string;
22
+ /**
23
+ * Lightweight pub/sub event bus for collection events.
24
+ */
25
+ export declare class MomentumEventBus {
26
+ private subscriptions;
27
+ /**
28
+ * Subscribe to events matching a pattern.
29
+ *
30
+ * @param pattern - Pattern string: "collection:event", "*:event", "collection:*", or "*"
31
+ * @param handler - Callback invoked when a matching event occurs
32
+ * @returns Unsubscribe function
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * bus.on('posts:afterChange', (event) => { ... });
37
+ * bus.on('*:afterDelete', (event) => { ... });
38
+ * const unsub = bus.on('*', (event) => { ... });
39
+ * unsub(); // remove subscription
40
+ * ```
41
+ */
42
+ on(pattern: EventPattern, handler: EventHandler): () => void;
43
+ /**
44
+ * Subscribe to all events for a specific collection.
45
+ * Shorthand for `on("collection:*", handler)`.
46
+ */
47
+ onCollection(collection: string, handler: EventHandler): () => void;
48
+ /**
49
+ * Subscribe to a specific event type across all collections.
50
+ * Shorthand for `on("*:event", handler)`.
51
+ */
52
+ onEvent(event: CollectionEventType, handler: EventHandler): () => void;
53
+ /**
54
+ * Emit a collection event to all matching subscribers.
55
+ */
56
+ emit(event: CollectionEvent): void;
57
+ /**
58
+ * Remove all subscriptions.
59
+ */
60
+ clear(): void;
61
+ /**
62
+ * Get the current number of subscriptions.
63
+ */
64
+ get size(): number;
65
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Hook Injector
3
+ *
4
+ * Utility to inject collection event hooks that emit CollectionEvents.
5
+ * Mirrors the pattern from registerWebhookHooks() in webhooks.ts.
6
+ */
7
+ import type { CollectionConfig } from '@momentumcms/core';
8
+ import type { CollectionEvent } from './plugin.types';
9
+ /**
10
+ * Callback type for collection event listeners.
11
+ */
12
+ export type CollectionEventListener = (event: CollectionEvent) => void;
13
+ /**
14
+ * Injects afterChange and afterDelete hooks into collections that emit events.
15
+ *
16
+ * @param collections - Mutable collections array
17
+ * @param listener - Callback invoked for each collection event
18
+ */
19
+ export declare function injectCollectionEventHooks(collections: CollectionConfig[], listener: CollectionEventListener): void;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Plugin Fatal Error
3
+ *
4
+ * Thrown by plugins that must halt server startup.
5
+ * Regular plugin errors are logged and skipped (fail-open),
6
+ * but PluginFatalError causes the entire init to fail.
7
+ */
8
+ export declare class PluginFatalError extends Error {
9
+ readonly pluginName: string;
10
+ constructor(pluginName: string, message: string);
11
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Plugin Runner
3
+ *
4
+ * Orchestrates plugin lifecycle: init → ready → shutdown.
5
+ * Plugins execute in array order; shutdown runs in reverse.
6
+ * Errors are logged and skipped unless PluginFatalError is thrown.
7
+ */
8
+ import type { CollectionConfig, MomentumConfig } from '@momentumcms/core';
9
+ import type { MomentumAPI, MomentumPlugin, PluginMiddlewareDescriptor, PluginProviderDescriptor } from './plugin.types';
10
+ /**
11
+ * Options for creating a PluginRunner.
12
+ */
13
+ export interface PluginRunnerOptions {
14
+ /** The Momentum config */
15
+ config: MomentumConfig;
16
+ /** Mutable collections array */
17
+ collections: CollectionConfig[];
18
+ /** Plugins to run */
19
+ plugins: MomentumPlugin[];
20
+ }
21
+ /**
22
+ * Manages plugin lifecycle execution.
23
+ */
24
+ export declare class PluginRunner {
25
+ private readonly plugins;
26
+ private readonly config;
27
+ private readonly collections;
28
+ private readonly logger;
29
+ private readonly middlewareDescriptors;
30
+ private readonly providerDescriptors;
31
+ private initialized;
32
+ private ready;
33
+ constructor(options: PluginRunnerOptions);
34
+ /**
35
+ * Create a plugin-scoped context.
36
+ */
37
+ private createContext;
38
+ /**
39
+ * Run onInit for all plugins (in order).
40
+ * Call this before API initialization.
41
+ */
42
+ runInit(): Promise<void>;
43
+ /**
44
+ * Run onReady for all plugins (in order).
45
+ * Call this after API + seeding complete.
46
+ */
47
+ runReady(api: MomentumAPI): Promise<void>;
48
+ /**
49
+ * Run onShutdown for all plugins (in reverse order).
50
+ * Call this during graceful shutdown.
51
+ */
52
+ runShutdown(): Promise<void>;
53
+ /**
54
+ * Get the list of registered plugin names.
55
+ */
56
+ getPluginNames(): string[];
57
+ /**
58
+ * Get middleware descriptors registered by plugins during onInit.
59
+ * Returns descriptors in the order they were registered.
60
+ */
61
+ getMiddleware(): PluginMiddlewareDescriptor[];
62
+ /**
63
+ * Get provider descriptors registered by plugins during onInit.
64
+ * Returns descriptors in the order they were registered.
65
+ */
66
+ getProviders(): PluginProviderDescriptor[];
67
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Plugin System Types — Re-exported from @momentumcms/core
3
+ *
4
+ * Canonical definitions now live in @momentumcms/core.
5
+ * This file re-exports them for backward compatibility.
6
+ */
7
+ export type { MomentumPlugin, PluginContext, PluginReadyContext, MomentumAPI, CollectionEvent, CollectionEventType, PluginMiddlewareDescriptor, PluginProviderDescriptor, PluginAdminRouteDescriptor, PluginLogger, } from '@momentumcms/core';