@spfn/core 0.1.0-alpha.84 → 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.
@@ -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 };
@@ -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,6 +3517,7 @@ 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}...`);
@@ -3428,7 +3544,7 @@ function logServerStarted(debug, host, port, config, timeouts) {
3428
3544
  config: startupConfig
3429
3545
  });
3430
3546
  }
3431
- function createShutdownHandler(server, config) {
3547
+ function createShutdownHandler(server, config, plugins) {
3432
3548
  return async () => {
3433
3549
  serverLogger2.debug("Closing HTTP server...");
3434
3550
  await new Promise((resolve) => {
@@ -3445,6 +3561,11 @@ function createShutdownHandler(server, config) {
3445
3561
  serverLogger2.error("beforeShutdown hook failed", error);
3446
3562
  }
3447
3563
  }
3564
+ try {
3565
+ await executePluginHooks(plugins, "beforeShutdown");
3566
+ } catch (error) {
3567
+ serverLogger2.error("Plugin beforeShutdown hooks failed", error);
3568
+ }
3448
3569
  const shouldCloseDatabase = config.infrastructure?.database !== false;
3449
3570
  const shouldCloseRedis = config.infrastructure?.redis !== false;
3450
3571
  if (shouldCloseDatabase) {
@@ -3502,14 +3623,16 @@ function registerShutdownHandlers(shutdown) {
3502
3623
  } else {
3503
3624
  serverLogger2.error("Uncaught exception", error);
3504
3625
  }
3505
- shutdown("UNCAUGHT_EXCEPTION");
3626
+ serverLogger2.info("Exiting immediately for clean restart");
3627
+ process.exit(1);
3506
3628
  });
3507
3629
  process.on("unhandledRejection", (reason, promise) => {
3508
3630
  serverLogger2.error("Unhandled promise rejection", {
3509
3631
  reason,
3510
3632
  promise
3511
3633
  });
3512
- shutdown("UNHANDLED_REJECTION");
3634
+ serverLogger2.info("Exiting immediately for clean restart");
3635
+ process.exit(1);
3513
3636
  });
3514
3637
  }
3515
3638
  async function cleanupOnFailure(config) {