@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
package/dist/server/index.d.ts
CHANGED
|
@@ -6,6 +6,57 @@ import { serve } from '@hono/node-server';
|
|
|
6
6
|
* CORS configuration options - inferred from hono/cors
|
|
7
7
|
*/
|
|
8
8
|
type CorsConfig = Parameters<typeof cors>[0];
|
|
9
|
+
/**
|
|
10
|
+
* SPFN Plugin Interface
|
|
11
|
+
*
|
|
12
|
+
* Allows packages to automatically hook into server lifecycle
|
|
13
|
+
* Plugins are auto-discovered from @spfn/* packages in node_modules
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* // packages/auth/src/plugin.ts
|
|
18
|
+
* export const spfnPlugin: ServerPlugin = {
|
|
19
|
+
* name: '@spfn/auth',
|
|
20
|
+
* afterInfrastructure: async () => {
|
|
21
|
+
* await initializeAuth();
|
|
22
|
+
* },
|
|
23
|
+
* beforeRoutes: async (app) => {
|
|
24
|
+
* app.route('/_auth', authRoutes);
|
|
25
|
+
* }
|
|
26
|
+
* };
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
interface ServerPlugin {
|
|
30
|
+
/**
|
|
31
|
+
* Plugin name (should match package name)
|
|
32
|
+
*/
|
|
33
|
+
name: string;
|
|
34
|
+
/**
|
|
35
|
+
* Hook: Run after infrastructure (DB/Redis) initialization
|
|
36
|
+
* Use for: migrations, seeding, RBAC setup
|
|
37
|
+
*/
|
|
38
|
+
afterInfrastructure?: () => Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Hook: Run before routes are loaded
|
|
41
|
+
* Use for: mounting plugin routes, adding middleware
|
|
42
|
+
*/
|
|
43
|
+
beforeRoutes?: (app: Hono) => Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Hook: Run after all routes are loaded
|
|
46
|
+
* Use for: final setup, fallback handlers
|
|
47
|
+
*/
|
|
48
|
+
afterRoutes?: (app: Hono) => Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Hook: Run after server starts successfully
|
|
51
|
+
* Use for: notifications, health checks
|
|
52
|
+
*/
|
|
53
|
+
afterStart?: (instance: ServerInstance) => Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Hook: Run before graceful shutdown
|
|
56
|
+
* Use for: cleanup plugin resources
|
|
57
|
+
*/
|
|
58
|
+
beforeShutdown?: () => Promise<void>;
|
|
59
|
+
}
|
|
9
60
|
/**
|
|
10
61
|
* Server Configuration Options
|
|
11
62
|
*
|
|
@@ -405,7 +456,7 @@ declare module 'hono' {
|
|
|
405
456
|
* 2. server.config.ts -> Partial customization
|
|
406
457
|
* 3. app.ts -> Full control (no auto config)
|
|
407
458
|
*/
|
|
408
|
-
declare function createServer(config?: ServerConfig): Promise<Hono>;
|
|
459
|
+
declare function createServer(config?: ServerConfig, plugins?: ServerPlugin[]): Promise<Hono>;
|
|
409
460
|
|
|
410
461
|
/**
|
|
411
462
|
* Start SPFN Server
|
|
@@ -424,4 +475,4 @@ declare function createServer(config?: ServerConfig): Promise<Hono>;
|
|
|
424
475
|
*/
|
|
425
476
|
declare function startServer(config?: ServerConfig): Promise<ServerInstance>;
|
|
426
477
|
|
|
427
|
-
export { type AppFactory, type ServerConfig, type ServerInstance, createServer, startServer };
|
|
478
|
+
export { type AppFactory, type ServerConfig, type ServerInstance, type ServerPlugin, createServer, startServer };
|
package/dist/server/index.js
CHANGED
|
@@ -3111,29 +3111,133 @@ function buildStartupConfig(config, timeouts) {
|
|
|
3111
3111
|
};
|
|
3112
3112
|
}
|
|
3113
3113
|
|
|
3114
|
+
// src/server/plugin-discovery.ts
|
|
3115
|
+
init_logger2();
|
|
3116
|
+
var pluginLogger = logger.child("plugin");
|
|
3117
|
+
async function discoverPlugins(cwd = process.cwd()) {
|
|
3118
|
+
const plugins = [];
|
|
3119
|
+
const nodeModulesPath = join(cwd, "node_modules");
|
|
3120
|
+
try {
|
|
3121
|
+
const projectPkgPath = join(cwd, "package.json");
|
|
3122
|
+
if (!existsSync(projectPkgPath)) {
|
|
3123
|
+
pluginLogger.debug("No package.json found, skipping plugin discovery");
|
|
3124
|
+
return plugins;
|
|
3125
|
+
}
|
|
3126
|
+
const projectPkg = JSON.parse(readFileSync(projectPkgPath, "utf-8"));
|
|
3127
|
+
const dependencies = {
|
|
3128
|
+
...projectPkg.dependencies,
|
|
3129
|
+
...projectPkg.devDependencies
|
|
3130
|
+
};
|
|
3131
|
+
for (const [packageName] of Object.entries(dependencies)) {
|
|
3132
|
+
if (!packageName.startsWith("@spfn/")) {
|
|
3133
|
+
continue;
|
|
3134
|
+
}
|
|
3135
|
+
try {
|
|
3136
|
+
const plugin = await loadPluginFromPackage(packageName, nodeModulesPath);
|
|
3137
|
+
if (plugin) {
|
|
3138
|
+
plugins.push(plugin);
|
|
3139
|
+
pluginLogger.info("Plugin discovered", {
|
|
3140
|
+
name: plugin.name,
|
|
3141
|
+
hooks: getPluginHookNames(plugin)
|
|
3142
|
+
});
|
|
3143
|
+
}
|
|
3144
|
+
} catch (error) {
|
|
3145
|
+
pluginLogger.debug("Failed to load plugin", {
|
|
3146
|
+
package: packageName,
|
|
3147
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
3148
|
+
});
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
} catch (error) {
|
|
3152
|
+
pluginLogger.warn("Plugin discovery failed", {
|
|
3153
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
return plugins;
|
|
3157
|
+
}
|
|
3158
|
+
async function loadPluginFromPackage(packageName, nodeModulesPath) {
|
|
3159
|
+
const pkgPath = join(nodeModulesPath, ...packageName.split("/"), "package.json");
|
|
3160
|
+
if (!existsSync(pkgPath)) {
|
|
3161
|
+
return null;
|
|
3162
|
+
}
|
|
3163
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
3164
|
+
const packageDir = dirname(pkgPath);
|
|
3165
|
+
const mainEntry = pkg.main || "dist/index.js";
|
|
3166
|
+
const mainPath = join(packageDir, mainEntry);
|
|
3167
|
+
if (!existsSync(mainPath)) {
|
|
3168
|
+
return null;
|
|
3169
|
+
}
|
|
3170
|
+
try {
|
|
3171
|
+
const module = await import(mainPath);
|
|
3172
|
+
if (module.spfnPlugin && isValidPlugin(module.spfnPlugin)) {
|
|
3173
|
+
return module.spfnPlugin;
|
|
3174
|
+
}
|
|
3175
|
+
return null;
|
|
3176
|
+
} catch (error) {
|
|
3177
|
+
return null;
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
function isValidPlugin(plugin) {
|
|
3181
|
+
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");
|
|
3182
|
+
}
|
|
3183
|
+
function getPluginHookNames(plugin) {
|
|
3184
|
+
const hooks = [];
|
|
3185
|
+
if (plugin.afterInfrastructure) hooks.push("afterInfrastructure");
|
|
3186
|
+
if (plugin.beforeRoutes) hooks.push("beforeRoutes");
|
|
3187
|
+
if (plugin.afterRoutes) hooks.push("afterRoutes");
|
|
3188
|
+
if (plugin.afterStart) hooks.push("afterStart");
|
|
3189
|
+
if (plugin.beforeShutdown) hooks.push("beforeShutdown");
|
|
3190
|
+
return hooks;
|
|
3191
|
+
}
|
|
3192
|
+
async function executePluginHooks(plugins, hookName, ...args) {
|
|
3193
|
+
for (const plugin of plugins) {
|
|
3194
|
+
const hook = plugin[hookName];
|
|
3195
|
+
if (typeof hook === "function") {
|
|
3196
|
+
try {
|
|
3197
|
+
pluginLogger.debug("Executing plugin hook", {
|
|
3198
|
+
plugin: plugin.name,
|
|
3199
|
+
hook: hookName
|
|
3200
|
+
});
|
|
3201
|
+
await hook(...args);
|
|
3202
|
+
} catch (error) {
|
|
3203
|
+
pluginLogger.error("Plugin hook failed", {
|
|
3204
|
+
plugin: plugin.name,
|
|
3205
|
+
hook: hookName,
|
|
3206
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
3207
|
+
});
|
|
3208
|
+
throw new Error(
|
|
3209
|
+
`Plugin ${plugin.name} failed in ${hookName} hook: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
3210
|
+
);
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3114
3216
|
// src/server/create-server.ts
|
|
3115
3217
|
var serverLogger = logger.child("server");
|
|
3116
|
-
async function createServer(config) {
|
|
3218
|
+
async function createServer(config, plugins = []) {
|
|
3117
3219
|
const cwd = process.cwd();
|
|
3118
3220
|
const appPath = join(cwd, "src", "server", "app.ts");
|
|
3119
3221
|
const appJsPath = join(cwd, "src", "server", "app.js");
|
|
3120
3222
|
if (existsSync(appPath) || existsSync(appJsPath)) {
|
|
3121
|
-
return await loadCustomApp(appPath, appJsPath, config);
|
|
3223
|
+
return await loadCustomApp(appPath, appJsPath, config, plugins);
|
|
3122
3224
|
}
|
|
3123
|
-
return await createAutoConfiguredApp(config);
|
|
3225
|
+
return await createAutoConfiguredApp(config, plugins);
|
|
3124
3226
|
}
|
|
3125
|
-
async function loadCustomApp(appPath, appJsPath, config) {
|
|
3227
|
+
async function loadCustomApp(appPath, appJsPath, config, plugins = []) {
|
|
3126
3228
|
const appModule = await (existsSync(appPath) ? import(appPath) : import(appJsPath));
|
|
3127
3229
|
const appFactory = appModule.default;
|
|
3128
3230
|
if (!appFactory) {
|
|
3129
3231
|
throw new Error("app.ts must export a default function that returns a Hono app");
|
|
3130
3232
|
}
|
|
3131
3233
|
const app = await appFactory();
|
|
3234
|
+
await executePluginHooks(plugins, "beforeRoutes", app);
|
|
3132
3235
|
const debug = config?.debug ?? process.env.NODE_ENV === "development";
|
|
3133
3236
|
await loadRoutes(app, { routesDir: config?.routesPath, debug });
|
|
3237
|
+
await executePluginHooks(plugins, "afterRoutes", app);
|
|
3134
3238
|
return app;
|
|
3135
3239
|
}
|
|
3136
|
-
async function createAutoConfiguredApp(config) {
|
|
3240
|
+
async function createAutoConfiguredApp(config, plugins = []) {
|
|
3137
3241
|
const app = new Hono();
|
|
3138
3242
|
const middlewareConfig = config?.middleware ?? {};
|
|
3139
3243
|
const enableLogger = middlewareConfig.logger !== false;
|
|
@@ -3149,8 +3253,10 @@ async function createAutoConfiguredApp(config) {
|
|
|
3149
3253
|
config?.use?.forEach((mw) => app.use("*", mw));
|
|
3150
3254
|
registerHealthCheckEndpoint(app, config);
|
|
3151
3255
|
await executeBeforeRoutesHook(app, config);
|
|
3256
|
+
await executePluginHooks(plugins, "beforeRoutes", app);
|
|
3152
3257
|
await loadAppRoutes(app, config);
|
|
3153
3258
|
await executeAfterRoutesHook(app, config);
|
|
3259
|
+
await executePluginHooks(plugins, "afterRoutes", app);
|
|
3154
3260
|
if (enableErrorHandler) {
|
|
3155
3261
|
app.onError(ErrorHandler());
|
|
3156
3262
|
}
|
|
@@ -3295,9 +3401,17 @@ async function startServer(config) {
|
|
|
3295
3401
|
if (debug) {
|
|
3296
3402
|
logMiddlewareOrder(finalConfig);
|
|
3297
3403
|
}
|
|
3404
|
+
serverLogger2.debug("Discovering plugins...");
|
|
3405
|
+
const plugins = await discoverPlugins();
|
|
3406
|
+
if (plugins.length > 0) {
|
|
3407
|
+
serverLogger2.info("Plugins discovered", {
|
|
3408
|
+
count: plugins.length,
|
|
3409
|
+
plugins: plugins.map((p) => p.name)
|
|
3410
|
+
});
|
|
3411
|
+
}
|
|
3298
3412
|
try {
|
|
3299
|
-
await initializeInfrastructure(finalConfig);
|
|
3300
|
-
const app = await createServer(finalConfig);
|
|
3413
|
+
await initializeInfrastructure(finalConfig, plugins);
|
|
3414
|
+
const app = await createServer(finalConfig, plugins);
|
|
3301
3415
|
const server = startHttpServer(app, host, port);
|
|
3302
3416
|
const timeouts = getTimeoutConfig(finalConfig.timeout);
|
|
3303
3417
|
applyServerTimeouts(server, timeouts);
|
|
@@ -3308,7 +3422,7 @@ async function startServer(config) {
|
|
|
3308
3422
|
port
|
|
3309
3423
|
});
|
|
3310
3424
|
logServerStarted(debug, host, port, finalConfig, timeouts);
|
|
3311
|
-
const shutdownServer = createShutdownHandler(server, finalConfig);
|
|
3425
|
+
const shutdownServer = createShutdownHandler(server, finalConfig, plugins);
|
|
3312
3426
|
const shutdown = createGracefulShutdown(shutdownServer, finalConfig);
|
|
3313
3427
|
registerShutdownHandlers(shutdown);
|
|
3314
3428
|
const serverInstance = {
|
|
@@ -3328,6 +3442,7 @@ async function startServer(config) {
|
|
|
3328
3442
|
serverLogger2.error("afterStart hook failed", error);
|
|
3329
3443
|
}
|
|
3330
3444
|
}
|
|
3445
|
+
await executePluginHooks(plugins, "afterStart", serverInstance);
|
|
3331
3446
|
return serverInstance;
|
|
3332
3447
|
} catch (error) {
|
|
3333
3448
|
const err = error;
|
|
@@ -3369,7 +3484,7 @@ function logMiddlewareOrder(config) {
|
|
|
3369
3484
|
order: middlewareOrder
|
|
3370
3485
|
});
|
|
3371
3486
|
}
|
|
3372
|
-
async function initializeInfrastructure(config) {
|
|
3487
|
+
async function initializeInfrastructure(config, plugins) {
|
|
3373
3488
|
if (config.lifecycle?.beforeInfrastructure) {
|
|
3374
3489
|
serverLogger2.debug("Executing beforeInfrastructure hook...");
|
|
3375
3490
|
try {
|
|
@@ -3402,14 +3517,16 @@ async function initializeInfrastructure(config) {
|
|
|
3402
3517
|
throw new Error("Server initialization failed in afterInfrastructure hook");
|
|
3403
3518
|
}
|
|
3404
3519
|
}
|
|
3520
|
+
await executePluginHooks(plugins, "afterInfrastructure");
|
|
3405
3521
|
}
|
|
3406
3522
|
function startHttpServer(app, host, port) {
|
|
3407
3523
|
serverLogger2.debug(`Starting server on ${host}:${port}...`);
|
|
3408
|
-
|
|
3524
|
+
const server = serve({
|
|
3409
3525
|
fetch: app.fetch,
|
|
3410
3526
|
port,
|
|
3411
3527
|
hostname: host
|
|
3412
3528
|
});
|
|
3529
|
+
return server;
|
|
3413
3530
|
}
|
|
3414
3531
|
function logServerTimeouts(timeouts) {
|
|
3415
3532
|
serverLogger2.info("Server timeouts configured", {
|
|
@@ -3427,7 +3544,7 @@ function logServerStarted(debug, host, port, config, timeouts) {
|
|
|
3427
3544
|
config: startupConfig
|
|
3428
3545
|
});
|
|
3429
3546
|
}
|
|
3430
|
-
function createShutdownHandler(server, config) {
|
|
3547
|
+
function createShutdownHandler(server, config, plugins) {
|
|
3431
3548
|
return async () => {
|
|
3432
3549
|
serverLogger2.debug("Closing HTTP server...");
|
|
3433
3550
|
await new Promise((resolve) => {
|
|
@@ -3444,6 +3561,11 @@ function createShutdownHandler(server, config) {
|
|
|
3444
3561
|
serverLogger2.error("beforeShutdown hook failed", error);
|
|
3445
3562
|
}
|
|
3446
3563
|
}
|
|
3564
|
+
try {
|
|
3565
|
+
await executePluginHooks(plugins, "beforeShutdown");
|
|
3566
|
+
} catch (error) {
|
|
3567
|
+
serverLogger2.error("Plugin beforeShutdown hooks failed", error);
|
|
3568
|
+
}
|
|
3447
3569
|
const shouldCloseDatabase = config.infrastructure?.database !== false;
|
|
3448
3570
|
const shouldCloseRedis = config.infrastructure?.redis !== false;
|
|
3449
3571
|
if (shouldCloseDatabase) {
|
|
@@ -3489,15 +3611,28 @@ function registerShutdownHandlers(shutdown) {
|
|
|
3489
3611
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
3490
3612
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
3491
3613
|
process.on("uncaughtException", (error) => {
|
|
3492
|
-
|
|
3493
|
-
|
|
3614
|
+
if (error.message?.includes("EADDRINUSE")) {
|
|
3615
|
+
serverLogger2.error("Port conflict detected - detailed trace:", {
|
|
3616
|
+
error: error.message,
|
|
3617
|
+
stack: error.stack,
|
|
3618
|
+
code: error.code,
|
|
3619
|
+
port: error.port,
|
|
3620
|
+
address: error.address,
|
|
3621
|
+
syscall: error.syscall
|
|
3622
|
+
});
|
|
3623
|
+
} else {
|
|
3624
|
+
serverLogger2.error("Uncaught exception", error);
|
|
3625
|
+
}
|
|
3626
|
+
serverLogger2.info("Exiting immediately for clean restart");
|
|
3627
|
+
process.exit(1);
|
|
3494
3628
|
});
|
|
3495
3629
|
process.on("unhandledRejection", (reason, promise) => {
|
|
3496
3630
|
serverLogger2.error("Unhandled promise rejection", {
|
|
3497
3631
|
reason,
|
|
3498
3632
|
promise
|
|
3499
3633
|
});
|
|
3500
|
-
|
|
3634
|
+
serverLogger2.info("Exiting immediately for clean restart");
|
|
3635
|
+
process.exit(1);
|
|
3501
3636
|
});
|
|
3502
3637
|
}
|
|
3503
3638
|
async function cleanupOnFailure(config) {
|