@spfn/core 0.1.0-alpha.84 → 0.1.0-alpha.86
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/cache/index.js +12 -1
- package/dist/cache/index.js.map +1 -1
- package/dist/client/index.d.ts +192 -46
- package/dist/client/index.js +181 -9
- package/dist/client/index.js.map +1 -1
- package/dist/client/nextjs/index.d.ts +557 -0
- package/dist/client/nextjs/index.js +371 -0
- package/dist/client/nextjs/index.js.map +1 -0
- package/dist/codegen/generators/index.js +12 -1
- package/dist/codegen/generators/index.js.map +1 -1
- package/dist/codegen/index.js +12 -1
- package/dist/codegen/index.js.map +1 -1
- package/dist/db/index.js +12 -1
- package/dist/db/index.js.map +1 -1
- package/dist/env/index.js +12 -1
- package/dist/env/index.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +148 -13
- package/dist/index.js.map +1 -1
- package/dist/logger/index.js +12 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.js +12 -1
- package/dist/middleware/index.js.map +1 -1
- package/dist/route/index.js +15 -3
- package/dist/route/index.js.map +1 -1
- package/dist/server/index.d.ts +53 -2
- package/dist/server/index.js +148 -13
- package/dist/server/index.js.map +1 -1
- package/package.json +7 -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
|
@@ -104,7 +104,18 @@ function formatConsole(metadata, colorize = true) {
|
|
|
104
104
|
}
|
|
105
105
|
if (metadata.context && Object.keys(metadata.context).length > 0) {
|
|
106
106
|
Object.entries(metadata.context).forEach(([key, value]) => {
|
|
107
|
-
|
|
107
|
+
let valueStr;
|
|
108
|
+
if (typeof value === "string") {
|
|
109
|
+
valueStr = value;
|
|
110
|
+
} else if (typeof value === "object" && value !== null) {
|
|
111
|
+
try {
|
|
112
|
+
valueStr = JSON.stringify(value);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
valueStr = "[circular]";
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
valueStr = String(value);
|
|
118
|
+
}
|
|
108
119
|
if (colorize) {
|
|
109
120
|
parts.push(`${COLORS.dim}[${key}=${valueStr}]${COLORS.reset}`);
|
|
110
121
|
} else {
|
|
@@ -2851,6 +2862,7 @@ async function loadRoutes(app, options) {
|
|
|
2851
2862
|
|
|
2852
2863
|
// src/route/bind.ts
|
|
2853
2864
|
init_errors();
|
|
2865
|
+
init_logger2();
|
|
2854
2866
|
|
|
2855
2867
|
// src/middleware/error-handler.ts
|
|
2856
2868
|
init_logger2();
|
|
@@ -3111,29 +3123,133 @@ function buildStartupConfig(config, timeouts) {
|
|
|
3111
3123
|
};
|
|
3112
3124
|
}
|
|
3113
3125
|
|
|
3126
|
+
// src/server/plugin-discovery.ts
|
|
3127
|
+
init_logger2();
|
|
3128
|
+
var pluginLogger = logger.child("plugin");
|
|
3129
|
+
async function discoverPlugins(cwd = process.cwd()) {
|
|
3130
|
+
const plugins = [];
|
|
3131
|
+
const nodeModulesPath = join(cwd, "node_modules");
|
|
3132
|
+
try {
|
|
3133
|
+
const projectPkgPath = join(cwd, "package.json");
|
|
3134
|
+
if (!existsSync(projectPkgPath)) {
|
|
3135
|
+
pluginLogger.debug("No package.json found, skipping plugin discovery");
|
|
3136
|
+
return plugins;
|
|
3137
|
+
}
|
|
3138
|
+
const projectPkg = JSON.parse(readFileSync(projectPkgPath, "utf-8"));
|
|
3139
|
+
const dependencies = {
|
|
3140
|
+
...projectPkg.dependencies,
|
|
3141
|
+
...projectPkg.devDependencies
|
|
3142
|
+
};
|
|
3143
|
+
for (const [packageName] of Object.entries(dependencies)) {
|
|
3144
|
+
if (!packageName.startsWith("@spfn/")) {
|
|
3145
|
+
continue;
|
|
3146
|
+
}
|
|
3147
|
+
try {
|
|
3148
|
+
const plugin = await loadPluginFromPackage(packageName, nodeModulesPath);
|
|
3149
|
+
if (plugin) {
|
|
3150
|
+
plugins.push(plugin);
|
|
3151
|
+
pluginLogger.info("Plugin discovered", {
|
|
3152
|
+
name: plugin.name,
|
|
3153
|
+
hooks: getPluginHookNames(plugin)
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
} catch (error) {
|
|
3157
|
+
pluginLogger.debug("Failed to load plugin", {
|
|
3158
|
+
package: packageName,
|
|
3159
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
3160
|
+
});
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
} catch (error) {
|
|
3164
|
+
pluginLogger.warn("Plugin discovery failed", {
|
|
3165
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
3166
|
+
});
|
|
3167
|
+
}
|
|
3168
|
+
return plugins;
|
|
3169
|
+
}
|
|
3170
|
+
async function loadPluginFromPackage(packageName, nodeModulesPath) {
|
|
3171
|
+
const pkgPath = join(nodeModulesPath, ...packageName.split("/"), "package.json");
|
|
3172
|
+
if (!existsSync(pkgPath)) {
|
|
3173
|
+
return null;
|
|
3174
|
+
}
|
|
3175
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
3176
|
+
const packageDir = dirname(pkgPath);
|
|
3177
|
+
const mainEntry = pkg.main || "dist/index.js";
|
|
3178
|
+
const mainPath = join(packageDir, mainEntry);
|
|
3179
|
+
if (!existsSync(mainPath)) {
|
|
3180
|
+
return null;
|
|
3181
|
+
}
|
|
3182
|
+
try {
|
|
3183
|
+
const module = await import(mainPath);
|
|
3184
|
+
if (module.spfnPlugin && isValidPlugin(module.spfnPlugin)) {
|
|
3185
|
+
return module.spfnPlugin;
|
|
3186
|
+
}
|
|
3187
|
+
return null;
|
|
3188
|
+
} catch (error) {
|
|
3189
|
+
return null;
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
function isValidPlugin(plugin) {
|
|
3193
|
+
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");
|
|
3194
|
+
}
|
|
3195
|
+
function getPluginHookNames(plugin) {
|
|
3196
|
+
const hooks = [];
|
|
3197
|
+
if (plugin.afterInfrastructure) hooks.push("afterInfrastructure");
|
|
3198
|
+
if (plugin.beforeRoutes) hooks.push("beforeRoutes");
|
|
3199
|
+
if (plugin.afterRoutes) hooks.push("afterRoutes");
|
|
3200
|
+
if (plugin.afterStart) hooks.push("afterStart");
|
|
3201
|
+
if (plugin.beforeShutdown) hooks.push("beforeShutdown");
|
|
3202
|
+
return hooks;
|
|
3203
|
+
}
|
|
3204
|
+
async function executePluginHooks(plugins, hookName, ...args) {
|
|
3205
|
+
for (const plugin of plugins) {
|
|
3206
|
+
const hook = plugin[hookName];
|
|
3207
|
+
if (typeof hook === "function") {
|
|
3208
|
+
try {
|
|
3209
|
+
pluginLogger.debug("Executing plugin hook", {
|
|
3210
|
+
plugin: plugin.name,
|
|
3211
|
+
hook: hookName
|
|
3212
|
+
});
|
|
3213
|
+
await hook(...args);
|
|
3214
|
+
} catch (error) {
|
|
3215
|
+
pluginLogger.error("Plugin hook failed", {
|
|
3216
|
+
plugin: plugin.name,
|
|
3217
|
+
hook: hookName,
|
|
3218
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
3219
|
+
});
|
|
3220
|
+
throw new Error(
|
|
3221
|
+
`Plugin ${plugin.name} failed in ${hookName} hook: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
3222
|
+
);
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3114
3228
|
// src/server/create-server.ts
|
|
3115
3229
|
var serverLogger = logger.child("server");
|
|
3116
|
-
async function createServer(config) {
|
|
3230
|
+
async function createServer(config, plugins = []) {
|
|
3117
3231
|
const cwd = process.cwd();
|
|
3118
3232
|
const appPath = join(cwd, "src", "server", "app.ts");
|
|
3119
3233
|
const appJsPath = join(cwd, "src", "server", "app.js");
|
|
3120
3234
|
if (existsSync(appPath) || existsSync(appJsPath)) {
|
|
3121
|
-
return await loadCustomApp(appPath, appJsPath, config);
|
|
3235
|
+
return await loadCustomApp(appPath, appJsPath, config, plugins);
|
|
3122
3236
|
}
|
|
3123
|
-
return await createAutoConfiguredApp(config);
|
|
3237
|
+
return await createAutoConfiguredApp(config, plugins);
|
|
3124
3238
|
}
|
|
3125
|
-
async function loadCustomApp(appPath, appJsPath, config) {
|
|
3239
|
+
async function loadCustomApp(appPath, appJsPath, config, plugins = []) {
|
|
3126
3240
|
const appModule = await (existsSync(appPath) ? import(appPath) : import(appJsPath));
|
|
3127
3241
|
const appFactory = appModule.default;
|
|
3128
3242
|
if (!appFactory) {
|
|
3129
3243
|
throw new Error("app.ts must export a default function that returns a Hono app");
|
|
3130
3244
|
}
|
|
3131
3245
|
const app = await appFactory();
|
|
3246
|
+
await executePluginHooks(plugins, "beforeRoutes", app);
|
|
3132
3247
|
const debug = config?.debug ?? process.env.NODE_ENV === "development";
|
|
3133
3248
|
await loadRoutes(app, { routesDir: config?.routesPath, debug });
|
|
3249
|
+
await executePluginHooks(plugins, "afterRoutes", app);
|
|
3134
3250
|
return app;
|
|
3135
3251
|
}
|
|
3136
|
-
async function createAutoConfiguredApp(config) {
|
|
3252
|
+
async function createAutoConfiguredApp(config, plugins = []) {
|
|
3137
3253
|
const app = new Hono();
|
|
3138
3254
|
const middlewareConfig = config?.middleware ?? {};
|
|
3139
3255
|
const enableLogger = middlewareConfig.logger !== false;
|
|
@@ -3149,8 +3265,10 @@ async function createAutoConfiguredApp(config) {
|
|
|
3149
3265
|
config?.use?.forEach((mw) => app.use("*", mw));
|
|
3150
3266
|
registerHealthCheckEndpoint(app, config);
|
|
3151
3267
|
await executeBeforeRoutesHook(app, config);
|
|
3268
|
+
await executePluginHooks(plugins, "beforeRoutes", app);
|
|
3152
3269
|
await loadAppRoutes(app, config);
|
|
3153
3270
|
await executeAfterRoutesHook(app, config);
|
|
3271
|
+
await executePluginHooks(plugins, "afterRoutes", app);
|
|
3154
3272
|
if (enableErrorHandler) {
|
|
3155
3273
|
app.onError(ErrorHandler());
|
|
3156
3274
|
}
|
|
@@ -3295,9 +3413,17 @@ async function startServer(config) {
|
|
|
3295
3413
|
if (debug) {
|
|
3296
3414
|
logMiddlewareOrder(finalConfig);
|
|
3297
3415
|
}
|
|
3416
|
+
serverLogger2.debug("Discovering plugins...");
|
|
3417
|
+
const plugins = await discoverPlugins();
|
|
3418
|
+
if (plugins.length > 0) {
|
|
3419
|
+
serverLogger2.info("Plugins discovered", {
|
|
3420
|
+
count: plugins.length,
|
|
3421
|
+
plugins: plugins.map((p) => p.name)
|
|
3422
|
+
});
|
|
3423
|
+
}
|
|
3298
3424
|
try {
|
|
3299
|
-
await initializeInfrastructure(finalConfig);
|
|
3300
|
-
const app = await createServer(finalConfig);
|
|
3425
|
+
await initializeInfrastructure(finalConfig, plugins);
|
|
3426
|
+
const app = await createServer(finalConfig, plugins);
|
|
3301
3427
|
const server = startHttpServer(app, host, port);
|
|
3302
3428
|
const timeouts = getTimeoutConfig(finalConfig.timeout);
|
|
3303
3429
|
applyServerTimeouts(server, timeouts);
|
|
@@ -3308,7 +3434,7 @@ async function startServer(config) {
|
|
|
3308
3434
|
port
|
|
3309
3435
|
});
|
|
3310
3436
|
logServerStarted(debug, host, port, finalConfig, timeouts);
|
|
3311
|
-
const shutdownServer = createShutdownHandler(server, finalConfig);
|
|
3437
|
+
const shutdownServer = createShutdownHandler(server, finalConfig, plugins);
|
|
3312
3438
|
const shutdown = createGracefulShutdown(shutdownServer, finalConfig);
|
|
3313
3439
|
registerShutdownHandlers(shutdown);
|
|
3314
3440
|
const serverInstance = {
|
|
@@ -3328,6 +3454,7 @@ async function startServer(config) {
|
|
|
3328
3454
|
serverLogger2.error("afterStart hook failed", error);
|
|
3329
3455
|
}
|
|
3330
3456
|
}
|
|
3457
|
+
await executePluginHooks(plugins, "afterStart", serverInstance);
|
|
3331
3458
|
return serverInstance;
|
|
3332
3459
|
} catch (error) {
|
|
3333
3460
|
const err = error;
|
|
@@ -3369,7 +3496,7 @@ function logMiddlewareOrder(config) {
|
|
|
3369
3496
|
order: middlewareOrder
|
|
3370
3497
|
});
|
|
3371
3498
|
}
|
|
3372
|
-
async function initializeInfrastructure(config) {
|
|
3499
|
+
async function initializeInfrastructure(config, plugins) {
|
|
3373
3500
|
if (config.lifecycle?.beforeInfrastructure) {
|
|
3374
3501
|
serverLogger2.debug("Executing beforeInfrastructure hook...");
|
|
3375
3502
|
try {
|
|
@@ -3402,6 +3529,7 @@ async function initializeInfrastructure(config) {
|
|
|
3402
3529
|
throw new Error("Server initialization failed in afterInfrastructure hook");
|
|
3403
3530
|
}
|
|
3404
3531
|
}
|
|
3532
|
+
await executePluginHooks(plugins, "afterInfrastructure");
|
|
3405
3533
|
}
|
|
3406
3534
|
function startHttpServer(app, host, port) {
|
|
3407
3535
|
serverLogger2.debug(`Starting server on ${host}:${port}...`);
|
|
@@ -3428,7 +3556,7 @@ function logServerStarted(debug, host, port, config, timeouts) {
|
|
|
3428
3556
|
config: startupConfig
|
|
3429
3557
|
});
|
|
3430
3558
|
}
|
|
3431
|
-
function createShutdownHandler(server, config) {
|
|
3559
|
+
function createShutdownHandler(server, config, plugins) {
|
|
3432
3560
|
return async () => {
|
|
3433
3561
|
serverLogger2.debug("Closing HTTP server...");
|
|
3434
3562
|
await new Promise((resolve) => {
|
|
@@ -3445,6 +3573,11 @@ function createShutdownHandler(server, config) {
|
|
|
3445
3573
|
serverLogger2.error("beforeShutdown hook failed", error);
|
|
3446
3574
|
}
|
|
3447
3575
|
}
|
|
3576
|
+
try {
|
|
3577
|
+
await executePluginHooks(plugins, "beforeShutdown");
|
|
3578
|
+
} catch (error) {
|
|
3579
|
+
serverLogger2.error("Plugin beforeShutdown hooks failed", error);
|
|
3580
|
+
}
|
|
3448
3581
|
const shouldCloseDatabase = config.infrastructure?.database !== false;
|
|
3449
3582
|
const shouldCloseRedis = config.infrastructure?.redis !== false;
|
|
3450
3583
|
if (shouldCloseDatabase) {
|
|
@@ -3502,14 +3635,16 @@ function registerShutdownHandlers(shutdown) {
|
|
|
3502
3635
|
} else {
|
|
3503
3636
|
serverLogger2.error("Uncaught exception", error);
|
|
3504
3637
|
}
|
|
3505
|
-
|
|
3638
|
+
serverLogger2.info("Exiting immediately for clean restart");
|
|
3639
|
+
process.exit(1);
|
|
3506
3640
|
});
|
|
3507
3641
|
process.on("unhandledRejection", (reason, promise) => {
|
|
3508
3642
|
serverLogger2.error("Unhandled promise rejection", {
|
|
3509
3643
|
reason,
|
|
3510
3644
|
promise
|
|
3511
3645
|
});
|
|
3512
|
-
|
|
3646
|
+
serverLogger2.info("Exiting immediately for clean restart");
|
|
3647
|
+
process.exit(1);
|
|
3513
3648
|
});
|
|
3514
3649
|
}
|
|
3515
3650
|
async function cleanupOnFailure(config) {
|