@spfn/core 0.1.0-alpha.83 → 0.1.0-alpha.85

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.
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Event handler function type
3
+ */
4
+ type EventHandler<T = any> = (data: T) => Promise<void> | void;
5
+ /**
6
+ * EventEmitter interface
7
+ *
8
+ * All event emitter adapters must implement this interface
9
+ */
10
+ interface EventEmitter {
11
+ /**
12
+ * Subscribe to an event
13
+ *
14
+ * @param event - Event name
15
+ * @param handler - Event handler function
16
+ */
17
+ on(event: string, handler: EventHandler): void;
18
+ /**
19
+ * Emit an event
20
+ *
21
+ * @param event - Event name
22
+ * @param data - Event data
23
+ */
24
+ emit(event: string, data?: any): Promise<void>;
25
+ /**
26
+ * Unsubscribe from an event
27
+ *
28
+ * @param event - Event name
29
+ */
30
+ off(event: string): void;
31
+ /**
32
+ * Clear all event subscriptions
33
+ */
34
+ clear(): void;
35
+ }
36
+
37
+ /**
38
+ * Event Emitter
39
+ *
40
+ * Adapter-based event emitter for decoupled communication between packages.
41
+ *
42
+ * Default adapter: InMemoryEventEmitter (single-instance)
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * // Subscribe to events
47
+ * import { on } from '@spfn/core/events';
48
+ *
49
+ * on('user:created', async (data) => {
50
+ * console.log('User created:', data.email);
51
+ * });
52
+ *
53
+ * // Emit events
54
+ * import { emit } from '@spfn/core/events';
55
+ *
56
+ * await emit('user:created', {
57
+ * userId: '123',
58
+ * email: 'user@example.com'
59
+ * });
60
+ *
61
+ * // Switch to Redis adapter (multi-instance)
62
+ * import { setEventEmitter } from '@spfn/core/events';
63
+ * import { RedisEventEmitter } from '@spfn/core/events/adapters';
64
+ *
65
+ * setEventEmitter(new RedisEventEmitter({
66
+ * host: 'localhost',
67
+ * port: 6379
68
+ * }));
69
+ * ```
70
+ */
71
+
72
+ /**
73
+ * Set the event emitter adapter
74
+ *
75
+ * @param adapter - EventEmitter adapter implementation
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * import { setEventEmitter } from '@spfn/core/events';
80
+ * import { InMemoryEventEmitter } from '@spfn/core/events/adapters';
81
+ *
82
+ * setEventEmitter(new InMemoryEventEmitter());
83
+ * ```
84
+ */
85
+ declare function setEventEmitter(adapter: EventEmitter): void;
86
+ /**
87
+ * Get the current event emitter adapter
88
+ *
89
+ * @returns Current EventEmitter instance
90
+ */
91
+ declare function getEventEmitter(): EventEmitter;
92
+ /**
93
+ * Subscribe to an event
94
+ *
95
+ * @param event - Event name
96
+ * @param handler - Event handler function
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * on('user:created', async (data) => {
101
+ * console.log('User created:', data.email);
102
+ * });
103
+ * ```
104
+ */
105
+ declare function on(event: string, handler: EventHandler): void;
106
+ /**
107
+ * Emit an event
108
+ *
109
+ * @param event - Event name
110
+ * @param data - Event data
111
+ *
112
+ * @example
113
+ * ```typescript
114
+ * await emit('user:created', {
115
+ * userId: '123',
116
+ * email: 'user@example.com'
117
+ * });
118
+ * ```
119
+ */
120
+ declare function emit(event: string, data?: any): Promise<void>;
121
+ /**
122
+ * Unsubscribe from an event
123
+ *
124
+ * @param event - Event name
125
+ *
126
+ * @example
127
+ * ```typescript
128
+ * off('user:created');
129
+ * ```
130
+ */
131
+ declare function off(event: string): void;
132
+ /**
133
+ * Clear all event subscriptions
134
+ *
135
+ * Useful for testing or cleanup
136
+ *
137
+ * @example
138
+ * ```typescript
139
+ * // In tests
140
+ * beforeEach(() => {
141
+ * clear();
142
+ * });
143
+ * ```
144
+ */
145
+ declare function clear(): void;
146
+
147
+ /**
148
+ * In-Memory Event Emitter
149
+ *
150
+ * Simple in-process event emitter for single-instance deployments.
151
+ * Events are not shared across multiple server instances.
152
+ *
153
+ * Use this adapter for:
154
+ * - Development
155
+ * - Single-instance production deployments
156
+ * - When you don't need distributed events
157
+ *
158
+ * @example
159
+ * ```typescript
160
+ * import { setEventEmitter } from '@spfn/core/events';
161
+ * import { InMemoryEventEmitter } from '@spfn/core/events/adapters';
162
+ *
163
+ * setEventEmitter(new InMemoryEventEmitter());
164
+ * ```
165
+ */
166
+
167
+ declare class InMemoryEventEmitter implements EventEmitter {
168
+ private listeners;
169
+ on(event: string, handler: EventHandler): void;
170
+ emit(event: string, data?: any): Promise<void>;
171
+ off(event: string): void;
172
+ clear(): void;
173
+ /**
174
+ * Get list of registered events (for debugging)
175
+ */
176
+ getEvents(): string[];
177
+ /**
178
+ * Get number of handlers for an event (for debugging)
179
+ */
180
+ getHandlerCount(event: string): number;
181
+ }
182
+
183
+ export { type EventEmitter, type EventHandler, InMemoryEventEmitter, clear, emit, getEventEmitter, off, on, setEventEmitter };
@@ -0,0 +1,77 @@
1
+ // src/events/adapters/memory.ts
2
+ var InMemoryEventEmitter = class {
3
+ listeners = /* @__PURE__ */ new Map();
4
+ on(event, handler) {
5
+ if (!this.listeners.has(event)) {
6
+ this.listeners.set(event, []);
7
+ }
8
+ this.listeners.get(event).push(handler);
9
+ }
10
+ async emit(event, data) {
11
+ const handlers = this.listeners.get(event) || [];
12
+ if (handlers.length === 0) {
13
+ return;
14
+ }
15
+ const results = await Promise.allSettled(
16
+ handlers.map(async (handler) => {
17
+ try {
18
+ return await handler(data);
19
+ } catch (error) {
20
+ throw error;
21
+ }
22
+ })
23
+ );
24
+ const failed = results.filter((r) => r.status === "rejected");
25
+ if (failed.length > 0) {
26
+ console.error(
27
+ `[Events] ${failed.length}/${handlers.length} handlers failed for event "${event}"`,
28
+ {
29
+ errors: failed.map((r) => r.reason)
30
+ }
31
+ );
32
+ }
33
+ }
34
+ off(event) {
35
+ this.listeners.delete(event);
36
+ }
37
+ clear() {
38
+ this.listeners.clear();
39
+ }
40
+ /**
41
+ * Get list of registered events (for debugging)
42
+ */
43
+ getEvents() {
44
+ return Array.from(this.listeners.keys());
45
+ }
46
+ /**
47
+ * Get number of handlers for an event (for debugging)
48
+ */
49
+ getHandlerCount(event) {
50
+ return this.listeners.get(event)?.length || 0;
51
+ }
52
+ };
53
+
54
+ // src/events/emitter.ts
55
+ var emitter = new InMemoryEventEmitter();
56
+ function setEventEmitter(adapter) {
57
+ emitter = adapter;
58
+ }
59
+ function getEventEmitter() {
60
+ return emitter;
61
+ }
62
+ function on(event, handler) {
63
+ emitter.on(event, handler);
64
+ }
65
+ async function emit(event, data) {
66
+ await emitter.emit(event, data);
67
+ }
68
+ function off(event) {
69
+ emitter.off(event);
70
+ }
71
+ function clear() {
72
+ emitter.clear();
73
+ }
74
+
75
+ export { InMemoryEventEmitter, clear, emit, getEventEmitter, off, on, setEventEmitter };
76
+ //# sourceMappingURL=index.js.map
77
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/events/adapters/memory.ts","../../src/events/emitter.ts"],"names":[],"mappings":";AAsBO,IAAM,uBAAN,MACP;AAAA,EACY,SAAA,uBAAgB,GAAA,EAA4B;AAAA,EAEpD,EAAA,CAAG,OAAe,OAAA,EAClB;AACI,IAAA,IAAI,CAAC,IAAA,CAAK,SAAA,CAAU,GAAA,CAAI,KAAK,CAAA,EAC7B;AACI,MAAA,IAAA,CAAK,SAAA,CAAU,GAAA,CAAI,KAAA,EAAO,EAAE,CAAA;AAAA,IAChC;AACA,IAAA,IAAA,CAAK,SAAA,CAAU,GAAA,CAAI,KAAK,CAAA,CAAG,KAAK,OAAO,CAAA;AAAA,EAC3C;AAAA,EAEA,MAAM,IAAA,CAAK,KAAA,EAAe,IAAA,EAC1B;AACI,IAAA,MAAM,WAAW,IAAA,CAAK,SAAA,CAAU,GAAA,CAAI,KAAK,KAAK,EAAC;AAE/C,IAAA,IAAI,QAAA,CAAS,WAAW,CAAA,EACxB;AACI,MAAA;AAAA,IACJ;AAIA,IAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,UAAA;AAAA,MAC1B,QAAA,CAAS,GAAA,CAAI,OAAO,OAAA,KACpB;AACI,QAAA,IACA;AACI,UAAA,OAAO,MAAM,QAAQ,IAAI,CAAA;AAAA,QAC7B,SACO,KAAA,EACP;AAEI,UAAA,MAAM,KAAA;AAAA,QACV;AAAA,MACJ,CAAC;AAAA,KACL;AAGA,IAAA,MAAM,SAAS,OAAA,CAAQ,MAAA,CAAO,CAAA,CAAA,KAAK,CAAA,CAAE,WAAW,UAAU,CAAA;AAC1D,IAAA,IAAI,MAAA,CAAO,SAAS,CAAA,EACpB;AACI,MAAA,OAAA,CAAQ,KAAA;AAAA,QACJ,YAAY,MAAA,CAAO,MAAM,IAAI,QAAA,CAAS,MAAM,+BAA+B,KAAK,CAAA,CAAA,CAAA;AAAA,QAChF;AAAA,UACI,MAAA,EAAQ,MAAA,CAAO,GAAA,CAAI,CAAA,CAAA,KAAM,EAA4B,MAAM;AAAA;AAC/D,OACJ;AAAA,IACJ;AAAA,EACJ;AAAA,EAEA,IAAI,KAAA,EACJ;AACI,IAAA,IAAA,CAAK,SAAA,CAAU,OAAO,KAAK,CAAA;AAAA,EAC/B;AAAA,EAEA,KAAA,GACA;AACI,IAAA,IAAA,CAAK,UAAU,KAAA,EAAM;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA,EAKA,SAAA,GACA;AACI,IAAA,OAAO,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,SAAA,CAAU,MAAM,CAAA;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAgB,KAAA,EAChB;AACI,IAAA,OAAO,IAAA,CAAK,SAAA,CAAU,GAAA,CAAI,KAAK,GAAG,MAAA,IAAU,CAAA;AAAA,EAChD;AACJ;;;AC7DA,IAAI,OAAA,GAAwB,IAAI,oBAAA,EAAqB;AAe9C,SAAS,gBAAgB,OAAA,EAChC;AACI,EAAA,OAAA,GAAU,OAAA;AACd;AAOO,SAAS,eAAA,GAChB;AACI,EAAA,OAAO,OAAA;AACX;AAeO,SAAS,EAAA,CAAG,OAAe,OAAA,EAClC;AACI,EAAA,OAAA,CAAQ,EAAA,CAAG,OAAO,OAAO,CAAA;AAC7B;AAgBA,eAAsB,IAAA,CAAK,OAAe,IAAA,EAC1C;AACI,EAAA,MAAM,OAAA,CAAQ,IAAA,CAAK,KAAA,EAAO,IAAI,CAAA;AAClC;AAYO,SAAS,IAAI,KAAA,EACpB;AACI,EAAA,OAAA,CAAQ,IAAI,KAAK,CAAA;AACrB;AAeO,SAAS,KAAA,GAChB;AACI,EAAA,OAAA,CAAQ,KAAA,EAAM;AAClB","file":"index.js","sourcesContent":["/**\n * In-Memory Event Emitter\n *\n * Simple in-process event emitter for single-instance deployments.\n * Events are not shared across multiple server instances.\n *\n * Use this adapter for:\n * - Development\n * - Single-instance production deployments\n * - When you don't need distributed events\n *\n * @example\n * ```typescript\n * import { setEventEmitter } from '@spfn/core/events';\n * import { InMemoryEventEmitter } from '@spfn/core/events/adapters';\n *\n * setEventEmitter(new InMemoryEventEmitter());\n * ```\n */\n\nimport type { EventEmitter, EventHandler } from '../types';\n\nexport class InMemoryEventEmitter implements EventEmitter\n{\n private listeners = new Map<string, EventHandler[]>();\n\n on(event: string, handler: EventHandler): void\n {\n if (!this.listeners.has(event))\n {\n this.listeners.set(event, []);\n }\n this.listeners.get(event)!.push(handler);\n }\n\n async emit(event: string, data?: any): Promise<void>\n {\n const handlers = this.listeners.get(event) || [];\n\n if (handlers.length === 0)\n {\n return;\n }\n\n // Execute all handlers in parallel\n // Failures in individual handlers don't affect others\n const results = await Promise.allSettled(\n handlers.map(async (handler) =>\n {\n try\n {\n return await handler(data);\n }\n catch (error)\n {\n // Catch and re-throw for Promise.allSettled\n throw error;\n }\n })\n );\n\n // Log failed handlers for debugging\n const failed = results.filter(r => r.status === 'rejected');\n if (failed.length > 0)\n {\n console.error(\n `[Events] ${failed.length}/${handlers.length} handlers failed for event \"${event}\"`,\n {\n errors: failed.map(r => (r as PromiseRejectedResult).reason),\n }\n );\n }\n }\n\n off(event: string): void\n {\n this.listeners.delete(event);\n }\n\n clear(): void\n {\n this.listeners.clear();\n }\n\n /**\n * Get list of registered events (for debugging)\n */\n getEvents(): string[]\n {\n return Array.from(this.listeners.keys());\n }\n\n /**\n * Get number of handlers for an event (for debugging)\n */\n getHandlerCount(event: string): number\n {\n return this.listeners.get(event)?.length || 0;\n }\n}","/**\n * Event Emitter\n *\n * Adapter-based event emitter for decoupled communication between packages.\n *\n * Default adapter: InMemoryEventEmitter (single-instance)\n *\n * @example\n * ```typescript\n * // Subscribe to events\n * import { on } from '@spfn/core/events';\n *\n * on('user:created', async (data) => {\n * console.log('User created:', data.email);\n * });\n *\n * // Emit events\n * import { emit } from '@spfn/core/events';\n *\n * await emit('user:created', {\n * userId: '123',\n * email: 'user@example.com'\n * });\n *\n * // Switch to Redis adapter (multi-instance)\n * import { setEventEmitter } from '@spfn/core/events';\n * import { RedisEventEmitter } from '@spfn/core/events/adapters';\n *\n * setEventEmitter(new RedisEventEmitter({\n * host: 'localhost',\n * port: 6379\n * }));\n * ```\n */\n\nimport type { EventEmitter, EventHandler } from './types';\nimport { InMemoryEventEmitter } from './adapters/memory';\n\nlet emitter: EventEmitter = new InMemoryEventEmitter();\n\n/**\n * Set the event emitter adapter\n *\n * @param adapter - EventEmitter adapter implementation\n *\n * @example\n * ```typescript\n * import { setEventEmitter } from '@spfn/core/events';\n * import { InMemoryEventEmitter } from '@spfn/core/events/adapters';\n *\n * setEventEmitter(new InMemoryEventEmitter());\n * ```\n */\nexport function setEventEmitter(adapter: EventEmitter): void\n{\n emitter = adapter;\n}\n\n/**\n * Get the current event emitter adapter\n *\n * @returns Current EventEmitter instance\n */\nexport function getEventEmitter(): EventEmitter\n{\n return emitter;\n}\n\n/**\n * Subscribe to an event\n *\n * @param event - Event name\n * @param handler - Event handler function\n *\n * @example\n * ```typescript\n * on('user:created', async (data) => {\n * console.log('User created:', data.email);\n * });\n * ```\n */\nexport function on(event: string, handler: EventHandler): void\n{\n emitter.on(event, handler);\n}\n\n/**\n * Emit an event\n *\n * @param event - Event name\n * @param data - Event data\n *\n * @example\n * ```typescript\n * await emit('user:created', {\n * userId: '123',\n * email: 'user@example.com'\n * });\n * ```\n */\nexport async function emit(event: string, data?: any): Promise<void>\n{\n await emitter.emit(event, data);\n}\n\n/**\n * Unsubscribe from an event\n *\n * @param event - Event name\n *\n * @example\n * ```typescript\n * off('user:created');\n * ```\n */\nexport function off(event: string): void\n{\n emitter.off(event);\n}\n\n/**\n * Clear all event subscriptions\n *\n * Useful for testing or cleanup\n *\n * @example\n * ```typescript\n * // In tests\n * beforeEach(() => {\n * clear();\n * });\n * ```\n */\nexport function clear(): void\n{\n emitter.clear();\n}"]}
package/dist/index.js CHANGED
@@ -3116,29 +3116,133 @@ function buildStartupConfig(config, timeouts) {
3116
3116
  };
3117
3117
  }
3118
3118
 
3119
+ // src/server/plugin-discovery.ts
3120
+ init_logger2();
3121
+ var pluginLogger = logger.child("plugin");
3122
+ async function discoverPlugins(cwd = process.cwd()) {
3123
+ const plugins = [];
3124
+ const nodeModulesPath = join(cwd, "node_modules");
3125
+ try {
3126
+ const projectPkgPath = join(cwd, "package.json");
3127
+ if (!existsSync(projectPkgPath)) {
3128
+ pluginLogger.debug("No package.json found, skipping plugin discovery");
3129
+ return plugins;
3130
+ }
3131
+ const projectPkg = JSON.parse(readFileSync(projectPkgPath, "utf-8"));
3132
+ const dependencies = {
3133
+ ...projectPkg.dependencies,
3134
+ ...projectPkg.devDependencies
3135
+ };
3136
+ for (const [packageName] of Object.entries(dependencies)) {
3137
+ if (!packageName.startsWith("@spfn/")) {
3138
+ continue;
3139
+ }
3140
+ try {
3141
+ const plugin = await loadPluginFromPackage(packageName, nodeModulesPath);
3142
+ if (plugin) {
3143
+ plugins.push(plugin);
3144
+ pluginLogger.info("Plugin discovered", {
3145
+ name: plugin.name,
3146
+ hooks: getPluginHookNames(plugin)
3147
+ });
3148
+ }
3149
+ } catch (error) {
3150
+ pluginLogger.debug("Failed to load plugin", {
3151
+ package: packageName,
3152
+ error: error instanceof Error ? error.message : "Unknown error"
3153
+ });
3154
+ }
3155
+ }
3156
+ } catch (error) {
3157
+ pluginLogger.warn("Plugin discovery failed", {
3158
+ error: error instanceof Error ? error.message : "Unknown error"
3159
+ });
3160
+ }
3161
+ return plugins;
3162
+ }
3163
+ async function loadPluginFromPackage(packageName, nodeModulesPath) {
3164
+ const pkgPath = join(nodeModulesPath, ...packageName.split("/"), "package.json");
3165
+ if (!existsSync(pkgPath)) {
3166
+ return null;
3167
+ }
3168
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
3169
+ const packageDir = dirname(pkgPath);
3170
+ const mainEntry = pkg.main || "dist/index.js";
3171
+ const mainPath = join(packageDir, mainEntry);
3172
+ if (!existsSync(mainPath)) {
3173
+ return null;
3174
+ }
3175
+ try {
3176
+ const module = await import(mainPath);
3177
+ if (module.spfnPlugin && isValidPlugin(module.spfnPlugin)) {
3178
+ return module.spfnPlugin;
3179
+ }
3180
+ return null;
3181
+ } catch (error) {
3182
+ return null;
3183
+ }
3184
+ }
3185
+ function isValidPlugin(plugin) {
3186
+ return plugin && typeof plugin === "object" && typeof plugin.name === "string" && (typeof plugin.afterInfrastructure === "function" || typeof plugin.beforeRoutes === "function" || typeof plugin.afterRoutes === "function" || typeof plugin.afterStart === "function" || typeof plugin.beforeShutdown === "function");
3187
+ }
3188
+ function getPluginHookNames(plugin) {
3189
+ const hooks = [];
3190
+ if (plugin.afterInfrastructure) hooks.push("afterInfrastructure");
3191
+ if (plugin.beforeRoutes) hooks.push("beforeRoutes");
3192
+ if (plugin.afterRoutes) hooks.push("afterRoutes");
3193
+ if (plugin.afterStart) hooks.push("afterStart");
3194
+ if (plugin.beforeShutdown) hooks.push("beforeShutdown");
3195
+ return hooks;
3196
+ }
3197
+ async function executePluginHooks(plugins, hookName, ...args) {
3198
+ for (const plugin of plugins) {
3199
+ const hook = plugin[hookName];
3200
+ if (typeof hook === "function") {
3201
+ try {
3202
+ pluginLogger.debug("Executing plugin hook", {
3203
+ plugin: plugin.name,
3204
+ hook: hookName
3205
+ });
3206
+ await hook(...args);
3207
+ } catch (error) {
3208
+ pluginLogger.error("Plugin hook failed", {
3209
+ plugin: plugin.name,
3210
+ hook: hookName,
3211
+ error: error instanceof Error ? error.message : "Unknown error"
3212
+ });
3213
+ throw new Error(
3214
+ `Plugin ${plugin.name} failed in ${hookName} hook: ${error instanceof Error ? error.message : "Unknown error"}`
3215
+ );
3216
+ }
3217
+ }
3218
+ }
3219
+ }
3220
+
3119
3221
  // src/server/create-server.ts
3120
3222
  var serverLogger = logger.child("server");
3121
- async function createServer(config) {
3223
+ async function createServer(config, plugins = []) {
3122
3224
  const cwd = process.cwd();
3123
3225
  const appPath = join(cwd, "src", "server", "app.ts");
3124
3226
  const appJsPath = join(cwd, "src", "server", "app.js");
3125
3227
  if (existsSync(appPath) || existsSync(appJsPath)) {
3126
- return await loadCustomApp(appPath, appJsPath, config);
3228
+ return await loadCustomApp(appPath, appJsPath, config, plugins);
3127
3229
  }
3128
- return await createAutoConfiguredApp(config);
3230
+ return await createAutoConfiguredApp(config, plugins);
3129
3231
  }
3130
- async function loadCustomApp(appPath, appJsPath, config) {
3232
+ async function loadCustomApp(appPath, appJsPath, config, plugins = []) {
3131
3233
  const appModule = await (existsSync(appPath) ? import(appPath) : import(appJsPath));
3132
3234
  const appFactory = appModule.default;
3133
3235
  if (!appFactory) {
3134
3236
  throw new Error("app.ts must export a default function that returns a Hono app");
3135
3237
  }
3136
3238
  const app = await appFactory();
3239
+ await executePluginHooks(plugins, "beforeRoutes", app);
3137
3240
  const debug = config?.debug ?? process.env.NODE_ENV === "development";
3138
3241
  await loadRoutes(app, { routesDir: config?.routesPath, debug });
3242
+ await executePluginHooks(plugins, "afterRoutes", app);
3139
3243
  return app;
3140
3244
  }
3141
- async function createAutoConfiguredApp(config) {
3245
+ async function createAutoConfiguredApp(config, plugins = []) {
3142
3246
  const app = new Hono();
3143
3247
  const middlewareConfig = config?.middleware ?? {};
3144
3248
  const enableLogger = middlewareConfig.logger !== false;
@@ -3154,8 +3258,10 @@ async function createAutoConfiguredApp(config) {
3154
3258
  config?.use?.forEach((mw) => app.use("*", mw));
3155
3259
  registerHealthCheckEndpoint(app, config);
3156
3260
  await executeBeforeRoutesHook(app, config);
3261
+ await executePluginHooks(plugins, "beforeRoutes", app);
3157
3262
  await loadAppRoutes(app, config);
3158
3263
  await executeAfterRoutesHook(app, config);
3264
+ await executePluginHooks(plugins, "afterRoutes", app);
3159
3265
  if (enableErrorHandler) {
3160
3266
  app.onError(ErrorHandler());
3161
3267
  }
@@ -3300,9 +3406,17 @@ async function startServer(config) {
3300
3406
  if (debug) {
3301
3407
  logMiddlewareOrder(finalConfig);
3302
3408
  }
3409
+ serverLogger2.debug("Discovering plugins...");
3410
+ const plugins = await discoverPlugins();
3411
+ if (plugins.length > 0) {
3412
+ serverLogger2.info("Plugins discovered", {
3413
+ count: plugins.length,
3414
+ plugins: plugins.map((p) => p.name)
3415
+ });
3416
+ }
3303
3417
  try {
3304
- await initializeInfrastructure(finalConfig);
3305
- const app = await createServer(finalConfig);
3418
+ await initializeInfrastructure(finalConfig, plugins);
3419
+ const app = await createServer(finalConfig, plugins);
3306
3420
  const server = startHttpServer(app, host, port);
3307
3421
  const timeouts = getTimeoutConfig(finalConfig.timeout);
3308
3422
  applyServerTimeouts(server, timeouts);
@@ -3313,7 +3427,7 @@ async function startServer(config) {
3313
3427
  port
3314
3428
  });
3315
3429
  logServerStarted(debug, host, port, finalConfig, timeouts);
3316
- const shutdownServer = createShutdownHandler(server, finalConfig);
3430
+ const shutdownServer = createShutdownHandler(server, finalConfig, plugins);
3317
3431
  const shutdown = createGracefulShutdown(shutdownServer, finalConfig);
3318
3432
  registerShutdownHandlers(shutdown);
3319
3433
  const serverInstance = {
@@ -3333,6 +3447,7 @@ async function startServer(config) {
3333
3447
  serverLogger2.error("afterStart hook failed", error);
3334
3448
  }
3335
3449
  }
3450
+ await executePluginHooks(plugins, "afterStart", serverInstance);
3336
3451
  return serverInstance;
3337
3452
  } catch (error) {
3338
3453
  const err = error;
@@ -3374,7 +3489,7 @@ function logMiddlewareOrder(config) {
3374
3489
  order: middlewareOrder
3375
3490
  });
3376
3491
  }
3377
- async function initializeInfrastructure(config) {
3492
+ async function initializeInfrastructure(config, plugins) {
3378
3493
  if (config.lifecycle?.beforeInfrastructure) {
3379
3494
  serverLogger2.debug("Executing beforeInfrastructure hook...");
3380
3495
  try {
@@ -3407,14 +3522,16 @@ async function initializeInfrastructure(config) {
3407
3522
  throw new Error("Server initialization failed in afterInfrastructure hook");
3408
3523
  }
3409
3524
  }
3525
+ await executePluginHooks(plugins, "afterInfrastructure");
3410
3526
  }
3411
3527
  function startHttpServer(app, host, port) {
3412
3528
  serverLogger2.debug(`Starting server on ${host}:${port}...`);
3413
- return serve({
3529
+ const server = serve({
3414
3530
  fetch: app.fetch,
3415
3531
  port,
3416
3532
  hostname: host
3417
3533
  });
3534
+ return server;
3418
3535
  }
3419
3536
  function logServerTimeouts(timeouts) {
3420
3537
  serverLogger2.info("Server timeouts configured", {
@@ -3432,7 +3549,7 @@ function logServerStarted(debug, host, port, config, timeouts) {
3432
3549
  config: startupConfig
3433
3550
  });
3434
3551
  }
3435
- function createShutdownHandler(server, config) {
3552
+ function createShutdownHandler(server, config, plugins) {
3436
3553
  return async () => {
3437
3554
  serverLogger2.debug("Closing HTTP server...");
3438
3555
  await new Promise((resolve) => {
@@ -3449,6 +3566,11 @@ function createShutdownHandler(server, config) {
3449
3566
  serverLogger2.error("beforeShutdown hook failed", error);
3450
3567
  }
3451
3568
  }
3569
+ try {
3570
+ await executePluginHooks(plugins, "beforeShutdown");
3571
+ } catch (error) {
3572
+ serverLogger2.error("Plugin beforeShutdown hooks failed", error);
3573
+ }
3452
3574
  const shouldCloseDatabase = config.infrastructure?.database !== false;
3453
3575
  const shouldCloseRedis = config.infrastructure?.redis !== false;
3454
3576
  if (shouldCloseDatabase) {
@@ -3494,15 +3616,28 @@ function registerShutdownHandlers(shutdown) {
3494
3616
  process.on("SIGTERM", () => shutdown("SIGTERM"));
3495
3617
  process.on("SIGINT", () => shutdown("SIGINT"));
3496
3618
  process.on("uncaughtException", (error) => {
3497
- serverLogger2.error("Uncaught exception", error);
3498
- shutdown("UNCAUGHT_EXCEPTION");
3619
+ if (error.message?.includes("EADDRINUSE")) {
3620
+ serverLogger2.error("Port conflict detected - detailed trace:", {
3621
+ error: error.message,
3622
+ stack: error.stack,
3623
+ code: error.code,
3624
+ port: error.port,
3625
+ address: error.address,
3626
+ syscall: error.syscall
3627
+ });
3628
+ } else {
3629
+ serverLogger2.error("Uncaught exception", error);
3630
+ }
3631
+ serverLogger2.info("Exiting immediately for clean restart");
3632
+ process.exit(1);
3499
3633
  });
3500
3634
  process.on("unhandledRejection", (reason, promise) => {
3501
3635
  serverLogger2.error("Unhandled promise rejection", {
3502
3636
  reason,
3503
3637
  promise
3504
3638
  });
3505
- shutdown("UNHANDLED_REJECTION");
3639
+ serverLogger2.info("Exiting immediately for clean restart");
3640
+ process.exit(1);
3506
3641
  });
3507
3642
  }
3508
3643
  async function cleanupOnFailure(config) {