@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.
- package/dist/codegen/index.d.ts +5 -0
- package/dist/codegen/index.js +15 -11
- package/dist/codegen/index.js.map +1 -1
- package/dist/events/index.d.ts +183 -0
- package/dist/events/index.js +77 -0
- package/dist/events/index.js.map +1 -0
- package/dist/index.js +149 -14
- package/dist/index.js.map +1 -1
- package/dist/server/index.d.ts +53 -2
- package/dist/server/index.js +149 -14
- package/dist/server/index.js.map +1 -1
- package/package.json +6 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
3498
|
-
|
|
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
|
-
|
|
3639
|
+
serverLogger2.info("Exiting immediately for clean restart");
|
|
3640
|
+
process.exit(1);
|
|
3506
3641
|
});
|
|
3507
3642
|
}
|
|
3508
3643
|
async function cleanupOnFailure(config) {
|