@spfn/core 0.1.0-alpha.8 → 0.1.0-alpha.81

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.
Files changed (59) hide show
  1. package/README.md +169 -195
  2. package/dist/auto-loader-JFaZ9gON.d.ts +80 -0
  3. package/dist/cache/index.d.ts +211 -0
  4. package/dist/cache/index.js +992 -0
  5. package/dist/cache/index.js.map +1 -0
  6. package/dist/client/index.d.ts +131 -92
  7. package/dist/client/index.js +93 -85
  8. package/dist/client/index.js.map +1 -1
  9. package/dist/codegen/generators/index.d.ts +19 -0
  10. package/dist/codegen/generators/index.js +1500 -0
  11. package/dist/codegen/generators/index.js.map +1 -0
  12. package/dist/codegen/index.d.ts +76 -60
  13. package/dist/codegen/index.js +1486 -736
  14. package/dist/codegen/index.js.map +1 -1
  15. package/dist/database-errors-BNNmLTJE.d.ts +86 -0
  16. package/dist/db/index.d.ts +844 -44
  17. package/dist/db/index.js +1262 -1309
  18. package/dist/db/index.js.map +1 -1
  19. package/dist/env/index.d.ts +508 -0
  20. package/dist/env/index.js +1106 -0
  21. package/dist/env/index.js.map +1 -0
  22. package/dist/error-handler-wjLL3v-a.d.ts +44 -0
  23. package/dist/errors/index.d.ts +136 -0
  24. package/dist/errors/index.js +172 -0
  25. package/dist/errors/index.js.map +1 -0
  26. package/dist/index-DHiAqhKv.d.ts +101 -0
  27. package/dist/index.d.ts +3 -374
  28. package/dist/index.js +2404 -2179
  29. package/dist/index.js.map +1 -1
  30. package/dist/logger/index.d.ts +94 -0
  31. package/dist/logger/index.js +774 -0
  32. package/dist/logger/index.js.map +1 -0
  33. package/dist/middleware/index.d.ts +33 -0
  34. package/dist/middleware/index.js +897 -0
  35. package/dist/middleware/index.js.map +1 -0
  36. package/dist/route/index.d.ts +21 -53
  37. package/dist/route/index.js +1238 -219
  38. package/dist/route/index.js.map +1 -1
  39. package/dist/server/index.d.ts +18 -0
  40. package/dist/server/index.js +2400 -2061
  41. package/dist/server/index.js.map +1 -1
  42. package/dist/types-DYueuoD6.d.ts +162 -0
  43. package/package.json +59 -15
  44. package/dist/auto-loader-C44TcLmM.d.ts +0 -125
  45. package/dist/bind-pssq1NRT.d.ts +0 -34
  46. package/dist/postgres-errors-CY_Es8EJ.d.ts +0 -1703
  47. package/dist/scripts/index.d.ts +0 -24
  48. package/dist/scripts/index.js +0 -1201
  49. package/dist/scripts/index.js.map +0 -1
  50. package/dist/scripts/templates/api-index.template.txt +0 -10
  51. package/dist/scripts/templates/api-tag.template.txt +0 -11
  52. package/dist/scripts/templates/contract.template.txt +0 -87
  53. package/dist/scripts/templates/entity-type.template.txt +0 -31
  54. package/dist/scripts/templates/entity.template.txt +0 -19
  55. package/dist/scripts/templates/index.template.txt +0 -10
  56. package/dist/scripts/templates/repository.template.txt +0 -37
  57. package/dist/scripts/templates/routes-id.template.txt +0 -59
  58. package/dist/scripts/templates/routes-index.template.txt +0 -44
  59. package/dist/types-SlzTr8ZO.d.ts +0 -143
@@ -1,9 +1,911 @@
1
+ import pino from 'pino';
2
+ import { readFileSync, existsSync, mkdirSync, accessSync, constants, writeFileSync, unlinkSync, createWriteStream, statSync, readdirSync, renameSync } from 'fs';
3
+ import { join, dirname, relative } from 'path';
1
4
  import { readdir, stat } from 'fs/promises';
2
- import { relative, join } from 'path';
3
5
  import { Value } from '@sinclair/typebox/value';
4
6
  import { Hono } from 'hono';
7
+ import { Type } from '@sinclair/typebox';
8
+
9
+ var __defProp = Object.defineProperty;
10
+ var __getOwnPropNames = Object.getOwnPropertyNames;
11
+ var __esm = (fn, res) => function __init() {
12
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
13
+ };
14
+ var __export = (target, all) => {
15
+ for (var name in all)
16
+ __defProp(target, name, { get: all[name], enumerable: true });
17
+ };
18
+ var PinoAdapter;
19
+ var init_pino = __esm({
20
+ "src/logger/adapters/pino.ts"() {
21
+ PinoAdapter = class _PinoAdapter {
22
+ logger;
23
+ constructor(config) {
24
+ this.logger = pino({
25
+ level: config.level,
26
+ // 기본 필드
27
+ base: config.module ? { module: config.module } : void 0
28
+ });
29
+ }
30
+ child(module) {
31
+ const childLogger = new _PinoAdapter({ level: this.logger.level, module });
32
+ childLogger.logger = this.logger.child({ module });
33
+ return childLogger;
34
+ }
35
+ debug(message, context) {
36
+ this.logger.debug(context || {}, message);
37
+ }
38
+ info(message, context) {
39
+ this.logger.info(context || {}, message);
40
+ }
41
+ warn(message, errorOrContext, context) {
42
+ if (errorOrContext instanceof Error) {
43
+ this.logger.warn({ err: errorOrContext, ...context }, message);
44
+ } else {
45
+ this.logger.warn(errorOrContext || {}, message);
46
+ }
47
+ }
48
+ error(message, errorOrContext, context) {
49
+ if (errorOrContext instanceof Error) {
50
+ this.logger.error({ err: errorOrContext, ...context }, message);
51
+ } else {
52
+ this.logger.error(errorOrContext || {}, message);
53
+ }
54
+ }
55
+ fatal(message, errorOrContext, context) {
56
+ if (errorOrContext instanceof Error) {
57
+ this.logger.fatal({ err: errorOrContext, ...context }, message);
58
+ } else {
59
+ this.logger.fatal(errorOrContext || {}, message);
60
+ }
61
+ }
62
+ async close() {
63
+ }
64
+ };
65
+ }
66
+ });
67
+
68
+ // src/logger/types.ts
69
+ var LOG_LEVEL_PRIORITY;
70
+ var init_types = __esm({
71
+ "src/logger/types.ts"() {
72
+ LOG_LEVEL_PRIORITY = {
73
+ debug: 0,
74
+ info: 1,
75
+ warn: 2,
76
+ error: 3,
77
+ fatal: 4
78
+ };
79
+ }
80
+ });
81
+
82
+ // src/logger/formatters.ts
83
+ function isSensitiveKey(key) {
84
+ const lowerKey = key.toLowerCase();
85
+ return SENSITIVE_KEYS.some((sensitive) => lowerKey.includes(sensitive));
86
+ }
87
+ function maskSensitiveData(data) {
88
+ if (data === null || data === void 0) {
89
+ return data;
90
+ }
91
+ if (Array.isArray(data)) {
92
+ return data.map((item) => maskSensitiveData(item));
93
+ }
94
+ if (typeof data === "object") {
95
+ const masked = {};
96
+ for (const [key, value] of Object.entries(data)) {
97
+ if (isSensitiveKey(key)) {
98
+ masked[key] = MASKED_VALUE;
99
+ } else if (typeof value === "object" && value !== null) {
100
+ masked[key] = maskSensitiveData(value);
101
+ } else {
102
+ masked[key] = value;
103
+ }
104
+ }
105
+ return masked;
106
+ }
107
+ return data;
108
+ }
109
+ function formatTimestamp(date) {
110
+ return date.toISOString();
111
+ }
112
+ function formatTimestampHuman(date) {
113
+ const year = date.getFullYear();
114
+ const month = String(date.getMonth() + 1).padStart(2, "0");
115
+ const day = String(date.getDate()).padStart(2, "0");
116
+ const hours = String(date.getHours()).padStart(2, "0");
117
+ const minutes = String(date.getMinutes()).padStart(2, "0");
118
+ const seconds = String(date.getSeconds()).padStart(2, "0");
119
+ const ms = String(date.getMilliseconds()).padStart(3, "0");
120
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
121
+ }
122
+ function formatError(error) {
123
+ const lines = [];
124
+ lines.push(`${error.name}: ${error.message}`);
125
+ if (error.stack) {
126
+ const stackLines = error.stack.split("\n").slice(1);
127
+ lines.push(...stackLines);
128
+ }
129
+ return lines.join("\n");
130
+ }
131
+ function formatConsole(metadata, colorize = true) {
132
+ const parts = [];
133
+ const timestamp = formatTimestampHuman(metadata.timestamp);
134
+ if (colorize) {
135
+ parts.push(`${COLORS.gray}[${timestamp}]${COLORS.reset}`);
136
+ } else {
137
+ parts.push(`[${timestamp}]`);
138
+ }
139
+ if (metadata.module) {
140
+ if (colorize) {
141
+ parts.push(`${COLORS.dim}[module=${metadata.module}]${COLORS.reset}`);
142
+ } else {
143
+ parts.push(`[module=${metadata.module}]`);
144
+ }
145
+ }
146
+ if (metadata.context && Object.keys(metadata.context).length > 0) {
147
+ Object.entries(metadata.context).forEach(([key, value]) => {
148
+ const valueStr = typeof value === "string" ? value : String(value);
149
+ if (colorize) {
150
+ parts.push(`${COLORS.dim}[${key}=${valueStr}]${COLORS.reset}`);
151
+ } else {
152
+ parts.push(`[${key}=${valueStr}]`);
153
+ }
154
+ });
155
+ }
156
+ const levelStr = metadata.level.toUpperCase();
157
+ if (colorize) {
158
+ const color = COLORS[metadata.level];
159
+ parts.push(`${color}(${levelStr})${COLORS.reset}:`);
160
+ } else {
161
+ parts.push(`(${levelStr}):`);
162
+ }
163
+ if (colorize) {
164
+ parts.push(`${COLORS.bright}${metadata.message}${COLORS.reset}`);
165
+ } else {
166
+ parts.push(metadata.message);
167
+ }
168
+ let output = parts.join(" ");
169
+ if (metadata.error) {
170
+ output += "\n" + formatError(metadata.error);
171
+ }
172
+ return output;
173
+ }
174
+ function formatJSON(metadata) {
175
+ const obj = {
176
+ timestamp: formatTimestamp(metadata.timestamp),
177
+ level: metadata.level,
178
+ message: metadata.message
179
+ };
180
+ if (metadata.module) {
181
+ obj.module = metadata.module;
182
+ }
183
+ if (metadata.context) {
184
+ obj.context = metadata.context;
185
+ }
186
+ if (metadata.error) {
187
+ obj.error = {
188
+ name: metadata.error.name,
189
+ message: metadata.error.message,
190
+ stack: metadata.error.stack
191
+ };
192
+ }
193
+ return JSON.stringify(obj);
194
+ }
195
+ var SENSITIVE_KEYS, MASKED_VALUE, COLORS;
196
+ var init_formatters = __esm({
197
+ "src/logger/formatters.ts"() {
198
+ SENSITIVE_KEYS = [
199
+ "password",
200
+ "passwd",
201
+ "pwd",
202
+ "secret",
203
+ "token",
204
+ "apikey",
205
+ "api_key",
206
+ "accesstoken",
207
+ "access_token",
208
+ "refreshtoken",
209
+ "refresh_token",
210
+ "authorization",
211
+ "auth",
212
+ "cookie",
213
+ "session",
214
+ "sessionid",
215
+ "session_id",
216
+ "privatekey",
217
+ "private_key",
218
+ "creditcard",
219
+ "credit_card",
220
+ "cardnumber",
221
+ "card_number",
222
+ "cvv",
223
+ "ssn",
224
+ "pin"
225
+ ];
226
+ MASKED_VALUE = "***MASKED***";
227
+ COLORS = {
228
+ reset: "\x1B[0m",
229
+ bright: "\x1B[1m",
230
+ dim: "\x1B[2m",
231
+ // 로그 레벨 컬러
232
+ debug: "\x1B[36m",
233
+ // cyan
234
+ info: "\x1B[32m",
235
+ // green
236
+ warn: "\x1B[33m",
237
+ // yellow
238
+ error: "\x1B[31m",
239
+ // red
240
+ fatal: "\x1B[35m",
241
+ // magenta
242
+ // 추가 컬러
243
+ gray: "\x1B[90m"
244
+ };
245
+ }
246
+ });
247
+
248
+ // src/logger/logger.ts
249
+ var Logger;
250
+ var init_logger = __esm({
251
+ "src/logger/logger.ts"() {
252
+ init_types();
253
+ init_formatters();
254
+ Logger = class _Logger {
255
+ config;
256
+ module;
257
+ constructor(config) {
258
+ this.config = config;
259
+ this.module = config.module;
260
+ }
261
+ /**
262
+ * Get current log level
263
+ */
264
+ get level() {
265
+ return this.config.level;
266
+ }
267
+ /**
268
+ * Create child logger (per module)
269
+ */
270
+ child(module) {
271
+ return new _Logger({
272
+ ...this.config,
273
+ module
274
+ });
275
+ }
276
+ /**
277
+ * Debug log
278
+ */
279
+ debug(message, context) {
280
+ this.log("debug", message, void 0, context);
281
+ }
282
+ /**
283
+ * Info log
284
+ */
285
+ info(message, context) {
286
+ this.log("info", message, void 0, context);
287
+ }
288
+ warn(message, errorOrContext, context) {
289
+ if (errorOrContext instanceof Error) {
290
+ this.log("warn", message, errorOrContext, context);
291
+ } else {
292
+ this.log("warn", message, void 0, errorOrContext);
293
+ }
294
+ }
295
+ error(message, errorOrContext, context) {
296
+ if (errorOrContext instanceof Error) {
297
+ this.log("error", message, errorOrContext, context);
298
+ } else {
299
+ this.log("error", message, void 0, errorOrContext);
300
+ }
301
+ }
302
+ fatal(message, errorOrContext, context) {
303
+ if (errorOrContext instanceof Error) {
304
+ this.log("fatal", message, errorOrContext, context);
305
+ } else {
306
+ this.log("fatal", message, void 0, errorOrContext);
307
+ }
308
+ }
309
+ /**
310
+ * Log processing (internal)
311
+ */
312
+ log(level, message, error, context) {
313
+ if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[this.config.level]) {
314
+ return;
315
+ }
316
+ const metadata = {
317
+ timestamp: /* @__PURE__ */ new Date(),
318
+ level,
319
+ message,
320
+ module: this.module,
321
+ error,
322
+ // Mask sensitive information in context to prevent credential leaks
323
+ context: context ? maskSensitiveData(context) : void 0
324
+ };
325
+ this.processTransports(metadata);
326
+ }
327
+ /**
328
+ * Process Transports
329
+ */
330
+ processTransports(metadata) {
331
+ const promises = this.config.transports.filter((transport) => transport.enabled).map((transport) => this.safeTransportLog(transport, metadata));
332
+ Promise.all(promises).catch((error) => {
333
+ const errorMessage = error instanceof Error ? error.message : String(error);
334
+ process.stderr.write(`[Logger] Transport error: ${errorMessage}
335
+ `);
336
+ });
337
+ }
338
+ /**
339
+ * Transport log (error-safe)
340
+ */
341
+ async safeTransportLog(transport, metadata) {
342
+ try {
343
+ await transport.log(metadata);
344
+ } catch (error) {
345
+ const errorMessage = error instanceof Error ? error.message : String(error);
346
+ process.stderr.write(`[Logger] Transport "${transport.name}" failed: ${errorMessage}
347
+ `);
348
+ }
349
+ }
350
+ /**
351
+ * Close all Transports
352
+ */
353
+ async close() {
354
+ const closePromises = this.config.transports.filter((transport) => transport.close).map((transport) => transport.close());
355
+ await Promise.all(closePromises);
356
+ }
357
+ };
358
+ }
359
+ });
360
+
361
+ // src/logger/transports/console.ts
362
+ var ConsoleTransport;
363
+ var init_console = __esm({
364
+ "src/logger/transports/console.ts"() {
365
+ init_types();
366
+ init_formatters();
367
+ ConsoleTransport = class {
368
+ name = "console";
369
+ level;
370
+ enabled;
371
+ colorize;
372
+ constructor(config) {
373
+ this.level = config.level;
374
+ this.enabled = config.enabled;
375
+ this.colorize = config.colorize ?? true;
376
+ }
377
+ async log(metadata) {
378
+ if (!this.enabled) {
379
+ return;
380
+ }
381
+ if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
382
+ return;
383
+ }
384
+ const message = formatConsole(metadata, this.colorize);
385
+ if (metadata.level === "warn" || metadata.level === "error" || metadata.level === "fatal") {
386
+ console.error(message);
387
+ } else {
388
+ console.log(message);
389
+ }
390
+ }
391
+ };
392
+ }
393
+ });
394
+ var FileTransport;
395
+ var init_file = __esm({
396
+ "src/logger/transports/file.ts"() {
397
+ init_types();
398
+ init_formatters();
399
+ FileTransport = class {
400
+ name = "file";
401
+ level;
402
+ enabled;
403
+ logDir;
404
+ maxFileSize;
405
+ maxFiles;
406
+ currentStream = null;
407
+ currentFilename = null;
408
+ constructor(config) {
409
+ this.level = config.level;
410
+ this.enabled = config.enabled;
411
+ this.logDir = config.logDir;
412
+ this.maxFileSize = config.maxFileSize ?? 10 * 1024 * 1024;
413
+ this.maxFiles = config.maxFiles ?? 10;
414
+ if (!existsSync(this.logDir)) {
415
+ mkdirSync(this.logDir, { recursive: true });
416
+ }
417
+ }
418
+ async log(metadata) {
419
+ if (!this.enabled) {
420
+ return;
421
+ }
422
+ if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
423
+ return;
424
+ }
425
+ const message = formatJSON(metadata);
426
+ const filename = this.getLogFilename(metadata.timestamp);
427
+ if (this.currentFilename !== filename) {
428
+ await this.rotateStream(filename);
429
+ await this.cleanOldFiles();
430
+ } else if (this.currentFilename) {
431
+ await this.checkAndRotateBySize();
432
+ }
433
+ if (this.currentStream) {
434
+ return new Promise((resolve, reject) => {
435
+ this.currentStream.write(message + "\n", "utf-8", (error) => {
436
+ if (error) {
437
+ process.stderr.write(`[FileTransport] Failed to write log: ${error.message}
438
+ `);
439
+ reject(error);
440
+ } else {
441
+ resolve();
442
+ }
443
+ });
444
+ });
445
+ }
446
+ }
447
+ /**
448
+ * 스트림 교체 (날짜 변경 시)
449
+ */
450
+ async rotateStream(filename) {
451
+ if (this.currentStream) {
452
+ await this.closeStream();
453
+ }
454
+ const filepath = join(this.logDir, filename);
455
+ this.currentStream = createWriteStream(filepath, {
456
+ flags: "a",
457
+ // append mode
458
+ encoding: "utf-8"
459
+ });
460
+ this.currentFilename = filename;
461
+ this.currentStream.on("error", (error) => {
462
+ process.stderr.write(`[FileTransport] Stream error: ${error.message}
463
+ `);
464
+ this.currentStream = null;
465
+ this.currentFilename = null;
466
+ });
467
+ }
468
+ /**
469
+ * 현재 스트림 닫기
470
+ */
471
+ async closeStream() {
472
+ if (!this.currentStream) {
473
+ return;
474
+ }
475
+ return new Promise((resolve, reject) => {
476
+ this.currentStream.end((error) => {
477
+ if (error) {
478
+ reject(error);
479
+ } else {
480
+ this.currentStream = null;
481
+ this.currentFilename = null;
482
+ resolve();
483
+ }
484
+ });
485
+ });
486
+ }
487
+ /**
488
+ * 파일 크기 체크 및 크기 기반 로테이션
489
+ */
490
+ async checkAndRotateBySize() {
491
+ if (!this.currentFilename) {
492
+ return;
493
+ }
494
+ const filepath = join(this.logDir, this.currentFilename);
495
+ if (!existsSync(filepath)) {
496
+ return;
497
+ }
498
+ try {
499
+ const stats = statSync(filepath);
500
+ if (stats.size >= this.maxFileSize) {
501
+ await this.rotateBySize();
502
+ }
503
+ } catch (error) {
504
+ const errorMessage = error instanceof Error ? error.message : String(error);
505
+ process.stderr.write(`[FileTransport] Failed to check file size: ${errorMessage}
506
+ `);
507
+ }
508
+ }
509
+ /**
510
+ * 크기 기반 로테이션 수행
511
+ * 예: 2025-01-01.log -> 2025-01-01.1.log, 2025-01-01.1.log -> 2025-01-01.2.log
512
+ */
513
+ async rotateBySize() {
514
+ if (!this.currentFilename) {
515
+ return;
516
+ }
517
+ await this.closeStream();
518
+ const baseName = this.currentFilename.replace(/\.log$/, "");
519
+ const files = readdirSync(this.logDir);
520
+ const relatedFiles = files.filter((file) => file.startsWith(baseName) && file.endsWith(".log")).sort().reverse();
521
+ for (const file of relatedFiles) {
522
+ const match = file.match(/\.(\d+)\.log$/);
523
+ if (match) {
524
+ const oldNum = parseInt(match[1], 10);
525
+ const newNum = oldNum + 1;
526
+ const oldPath = join(this.logDir, file);
527
+ const newPath2 = join(this.logDir, `${baseName}.${newNum}.log`);
528
+ try {
529
+ renameSync(oldPath, newPath2);
530
+ } catch (error) {
531
+ const errorMessage = error instanceof Error ? error.message : String(error);
532
+ process.stderr.write(`[FileTransport] Failed to rotate file: ${errorMessage}
533
+ `);
534
+ }
535
+ }
536
+ }
537
+ const currentPath = join(this.logDir, this.currentFilename);
538
+ const newPath = join(this.logDir, `${baseName}.1.log`);
539
+ try {
540
+ if (existsSync(currentPath)) {
541
+ renameSync(currentPath, newPath);
542
+ }
543
+ } catch (error) {
544
+ const errorMessage = error instanceof Error ? error.message : String(error);
545
+ process.stderr.write(`[FileTransport] Failed to rotate current file: ${errorMessage}
546
+ `);
547
+ }
548
+ await this.rotateStream(this.currentFilename);
549
+ }
550
+ /**
551
+ * 오래된 로그 파일 정리
552
+ * maxFiles 개수를 초과하는 로그 파일 삭제
553
+ */
554
+ async cleanOldFiles() {
555
+ try {
556
+ if (!existsSync(this.logDir)) {
557
+ return;
558
+ }
559
+ const files = readdirSync(this.logDir);
560
+ const logFiles = files.filter((file) => file.endsWith(".log")).map((file) => {
561
+ const filepath = join(this.logDir, file);
562
+ const stats = statSync(filepath);
563
+ return { file, mtime: stats.mtime };
564
+ }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
565
+ if (logFiles.length > this.maxFiles) {
566
+ const filesToDelete = logFiles.slice(this.maxFiles);
567
+ for (const { file } of filesToDelete) {
568
+ const filepath = join(this.logDir, file);
569
+ try {
570
+ unlinkSync(filepath);
571
+ } catch (error) {
572
+ const errorMessage = error instanceof Error ? error.message : String(error);
573
+ process.stderr.write(`[FileTransport] Failed to delete old file "${file}": ${errorMessage}
574
+ `);
575
+ }
576
+ }
577
+ }
578
+ } catch (error) {
579
+ const errorMessage = error instanceof Error ? error.message : String(error);
580
+ process.stderr.write(`[FileTransport] Failed to clean old files: ${errorMessage}
581
+ `);
582
+ }
583
+ }
584
+ /**
585
+ * 날짜별 로그 파일명 생성
586
+ */
587
+ getLogFilename(date) {
588
+ const year = date.getFullYear();
589
+ const month = String(date.getMonth() + 1).padStart(2, "0");
590
+ const day = String(date.getDate()).padStart(2, "0");
591
+ return `${year}-${month}-${day}.log`;
592
+ }
593
+ async close() {
594
+ await this.closeStream();
595
+ }
596
+ };
597
+ }
598
+ });
599
+ function isFileLoggingEnabled() {
600
+ return process.env.LOGGER_FILE_ENABLED === "true";
601
+ }
602
+ function getDefaultLogLevel() {
603
+ const isProduction = process.env.NODE_ENV === "production";
604
+ const isDevelopment = process.env.NODE_ENV === "development";
605
+ if (isDevelopment) {
606
+ return "debug";
607
+ }
608
+ if (isProduction) {
609
+ return "info";
610
+ }
611
+ return "warn";
612
+ }
613
+ function getConsoleConfig() {
614
+ const isProduction = process.env.NODE_ENV === "production";
615
+ return {
616
+ level: "debug",
617
+ enabled: true,
618
+ colorize: !isProduction
619
+ // Dev: colored output, Production: plain text
620
+ };
621
+ }
622
+ function getFileConfig() {
623
+ const isProduction = process.env.NODE_ENV === "production";
624
+ return {
625
+ level: "info",
626
+ enabled: isProduction,
627
+ // File logging in production only
628
+ logDir: process.env.LOG_DIR || "./logs",
629
+ maxFileSize: 10 * 1024 * 1024,
630
+ // 10MB
631
+ maxFiles: 10
632
+ };
633
+ }
634
+ function validateDirectoryWritable(dirPath) {
635
+ if (!existsSync(dirPath)) {
636
+ try {
637
+ mkdirSync(dirPath, { recursive: true });
638
+ } catch (error) {
639
+ const errorMessage = error instanceof Error ? error.message : String(error);
640
+ throw new Error(`Failed to create log directory "${dirPath}": ${errorMessage}`);
641
+ }
642
+ }
643
+ try {
644
+ accessSync(dirPath, constants.W_OK);
645
+ } catch {
646
+ throw new Error(`Log directory "${dirPath}" is not writable. Please check permissions.`);
647
+ }
648
+ const testFile = join(dirPath, ".logger-write-test");
649
+ try {
650
+ writeFileSync(testFile, "test", "utf-8");
651
+ unlinkSync(testFile);
652
+ } catch (error) {
653
+ const errorMessage = error instanceof Error ? error.message : String(error);
654
+ throw new Error(`Cannot write to log directory "${dirPath}": ${errorMessage}`);
655
+ }
656
+ }
657
+ function validateFileConfig() {
658
+ if (!isFileLoggingEnabled()) {
659
+ return;
660
+ }
661
+ const logDir = process.env.LOG_DIR;
662
+ if (!logDir) {
663
+ throw new Error(
664
+ "LOG_DIR environment variable is required when LOGGER_FILE_ENABLED=true. Example: LOG_DIR=/var/log/myapp"
665
+ );
666
+ }
667
+ validateDirectoryWritable(logDir);
668
+ }
669
+ function validateSlackConfig() {
670
+ const webhookUrl = process.env.SLACK_WEBHOOK_URL;
671
+ if (!webhookUrl) {
672
+ return;
673
+ }
674
+ if (!webhookUrl.startsWith("https://hooks.slack.com/")) {
675
+ throw new Error(
676
+ `Invalid SLACK_WEBHOOK_URL: "${webhookUrl}". Slack webhook URLs must start with "https://hooks.slack.com/"`
677
+ );
678
+ }
679
+ }
680
+ function validateEmailConfig() {
681
+ const smtpHost = process.env.SMTP_HOST;
682
+ const smtpPort = process.env.SMTP_PORT;
683
+ const emailFrom = process.env.EMAIL_FROM;
684
+ const emailTo = process.env.EMAIL_TO;
685
+ const hasAnyEmailConfig = smtpHost || smtpPort || emailFrom || emailTo;
686
+ if (!hasAnyEmailConfig) {
687
+ return;
688
+ }
689
+ const missingFields = [];
690
+ if (!smtpHost) missingFields.push("SMTP_HOST");
691
+ if (!smtpPort) missingFields.push("SMTP_PORT");
692
+ if (!emailFrom) missingFields.push("EMAIL_FROM");
693
+ if (!emailTo) missingFields.push("EMAIL_TO");
694
+ if (missingFields.length > 0) {
695
+ throw new Error(
696
+ `Email transport configuration incomplete. Missing: ${missingFields.join(", ")}. Either set all required fields or remove all email configuration.`
697
+ );
698
+ }
699
+ const port = parseInt(smtpPort, 10);
700
+ if (isNaN(port) || port < 1 || port > 65535) {
701
+ throw new Error(
702
+ `Invalid SMTP_PORT: "${smtpPort}". Must be a number between 1 and 65535.`
703
+ );
704
+ }
705
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
706
+ if (!emailRegex.test(emailFrom)) {
707
+ throw new Error(`Invalid EMAIL_FROM format: "${emailFrom}"`);
708
+ }
709
+ const recipients = emailTo.split(",").map((e) => e.trim());
710
+ for (const email of recipients) {
711
+ if (!emailRegex.test(email)) {
712
+ throw new Error(`Invalid email address in EMAIL_TO: "${email}"`);
713
+ }
714
+ }
715
+ }
716
+ function validateEnvironment() {
717
+ const nodeEnv = process.env.NODE_ENV;
718
+ if (!nodeEnv) {
719
+ process.stderr.write(
720
+ "[Logger] Warning: NODE_ENV is not set. Defaulting to test environment.\n"
721
+ );
722
+ }
723
+ }
724
+ function validateConfig() {
725
+ try {
726
+ validateEnvironment();
727
+ validateFileConfig();
728
+ validateSlackConfig();
729
+ validateEmailConfig();
730
+ } catch (error) {
731
+ if (error instanceof Error) {
732
+ throw new Error(`[Logger] Configuration validation failed: ${error.message}`);
733
+ }
734
+ throw error;
735
+ }
736
+ }
737
+ var init_config = __esm({
738
+ "src/logger/config.ts"() {
739
+ }
740
+ });
741
+
742
+ // src/logger/adapters/custom.ts
743
+ function initializeTransports() {
744
+ const transports = [];
745
+ const consoleConfig = getConsoleConfig();
746
+ transports.push(new ConsoleTransport(consoleConfig));
747
+ const fileConfig = getFileConfig();
748
+ if (fileConfig.enabled) {
749
+ transports.push(new FileTransport(fileConfig));
750
+ }
751
+ return transports;
752
+ }
753
+ var CustomAdapter;
754
+ var init_custom = __esm({
755
+ "src/logger/adapters/custom.ts"() {
756
+ init_logger();
757
+ init_console();
758
+ init_file();
759
+ init_config();
760
+ CustomAdapter = class _CustomAdapter {
761
+ logger;
762
+ constructor(config) {
763
+ this.logger = new Logger({
764
+ level: config.level,
765
+ module: config.module,
766
+ transports: initializeTransports()
767
+ });
768
+ }
769
+ child(module) {
770
+ const adapter = new _CustomAdapter({ level: this.logger.level, module });
771
+ adapter.logger = this.logger.child(module);
772
+ return adapter;
773
+ }
774
+ debug(message, context) {
775
+ this.logger.debug(message, context);
776
+ }
777
+ info(message, context) {
778
+ this.logger.info(message, context);
779
+ }
780
+ warn(message, errorOrContext, context) {
781
+ if (errorOrContext instanceof Error) {
782
+ this.logger.warn(message, errorOrContext, context);
783
+ } else {
784
+ this.logger.warn(message, errorOrContext);
785
+ }
786
+ }
787
+ error(message, errorOrContext, context) {
788
+ if (errorOrContext instanceof Error) {
789
+ this.logger.error(message, errorOrContext, context);
790
+ } else {
791
+ this.logger.error(message, errorOrContext);
792
+ }
793
+ }
794
+ fatal(message, errorOrContext, context) {
795
+ if (errorOrContext instanceof Error) {
796
+ this.logger.fatal(message, errorOrContext, context);
797
+ } else {
798
+ this.logger.fatal(message, errorOrContext);
799
+ }
800
+ }
801
+ async close() {
802
+ await this.logger.close();
803
+ }
804
+ };
805
+ }
806
+ });
807
+
808
+ // src/logger/adapter-factory.ts
809
+ function createAdapter(type) {
810
+ const level = getDefaultLogLevel();
811
+ switch (type) {
812
+ case "pino":
813
+ return new PinoAdapter({ level });
814
+ case "custom":
815
+ return new CustomAdapter({ level });
816
+ default:
817
+ return new PinoAdapter({ level });
818
+ }
819
+ }
820
+ function getAdapterType() {
821
+ const adapterEnv = process.env.LOGGER_ADAPTER;
822
+ if (adapterEnv === "custom" || adapterEnv === "pino") {
823
+ return adapterEnv;
824
+ }
825
+ return "pino";
826
+ }
827
+ function initializeLogger() {
828
+ validateConfig();
829
+ return createAdapter(getAdapterType());
830
+ }
831
+ var logger;
832
+ var init_adapter_factory = __esm({
833
+ "src/logger/adapter-factory.ts"() {
834
+ init_pino();
835
+ init_custom();
836
+ init_config();
837
+ logger = initializeLogger();
838
+ }
839
+ });
840
+
841
+ // src/logger/index.ts
842
+ var init_logger2 = __esm({
843
+ "src/logger/index.ts"() {
844
+ init_adapter_factory();
845
+ }
846
+ });
847
+
848
+ // src/route/function-routes.ts
849
+ var function_routes_exports = {};
850
+ __export(function_routes_exports, {
851
+ discoverFunctionRoutes: () => discoverFunctionRoutes
852
+ });
853
+ function discoverFunctionRoutes(cwd = process.cwd()) {
854
+ const functions = [];
855
+ const nodeModulesPath = join(cwd, "node_modules");
856
+ try {
857
+ const projectPkgPath = join(cwd, "package.json");
858
+ const projectPkg = JSON.parse(readFileSync(projectPkgPath, "utf-8"));
859
+ const dependencies = {
860
+ ...projectPkg.dependencies,
861
+ ...projectPkg.devDependencies
862
+ };
863
+ for (const [packageName] of Object.entries(dependencies)) {
864
+ if (!packageName.startsWith("@spfn/") && !packageName.startsWith("spfn-")) {
865
+ continue;
866
+ }
867
+ try {
868
+ const pkgPath = join(nodeModulesPath, ...packageName.split("/"), "package.json");
869
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
870
+ if (pkg.spfn?.routes?.dir) {
871
+ const { dir } = pkg.spfn.routes;
872
+ const prefix = pkg.spfn.prefix;
873
+ const packagePath = dirname(pkgPath);
874
+ const routesDir = join(packagePath, dir);
875
+ functions.push({
876
+ packageName,
877
+ routesDir,
878
+ packagePath,
879
+ prefix
880
+ // Include prefix in function info
881
+ });
882
+ routeLogger.debug("Discovered function routes", {
883
+ package: packageName,
884
+ dir,
885
+ prefix: prefix || "(none)"
886
+ });
887
+ }
888
+ } catch (error) {
889
+ }
890
+ }
891
+ } catch (error) {
892
+ routeLogger.warn("Failed to discover function routes", {
893
+ error: error instanceof Error ? error.message : "Unknown error"
894
+ });
895
+ }
896
+ return functions;
897
+ }
898
+ var routeLogger;
899
+ var init_function_routes = __esm({
900
+ "src/route/function-routes.ts"() {
901
+ init_logger2();
902
+ routeLogger = logger.child("function-routes");
903
+ }
904
+ });
5
905
 
6
906
  // src/route/auto-loader.ts
907
+ init_logger2();
908
+ var routeLogger2 = logger.child("route");
7
909
  var AutoRouteLoader = class {
8
910
  constructor(routesDir, debug = false, middlewares = []) {
9
911
  this.routesDir = routesDir;
@@ -11,36 +913,18 @@ var AutoRouteLoader = class {
11
913
  this.middlewares = middlewares;
12
914
  }
13
915
  routes = [];
14
- registeredRoutes = /* @__PURE__ */ new Map();
15
- // normalized path → file
16
916
  debug;
17
917
  middlewares;
18
- /**
19
- * Load all routes from directory
20
- */
21
918
  async load(app) {
22
919
  const startTime = Date.now();
23
920
  const files = await this.scanFiles(this.routesDir);
24
921
  if (files.length === 0) {
25
- console.warn("\u26A0\uFE0F No route files found");
922
+ routeLogger2.warn("No route files found");
26
923
  return this.getStats();
27
924
  }
28
- const filesWithPriority = files.map((file) => ({
29
- path: file,
30
- priority: this.calculatePriority(relative(this.routesDir, file))
31
- }));
32
- filesWithPriority.sort((a, b) => a.priority - b.priority);
33
- if (this.debug) {
34
- console.log(`
35
- \u{1F4CB} Route Registration Order:`);
36
- console.log(` Priority 1 (Static): ${filesWithPriority.filter((f) => f.priority === 1).length} routes`);
37
- console.log(` Priority 2 (Dynamic): ${filesWithPriority.filter((f) => f.priority === 2).length} routes`);
38
- console.log(` Priority 3 (Catch-all): ${filesWithPriority.filter((f) => f.priority === 3).length} routes
39
- `);
40
- }
41
925
  let failureCount = 0;
42
- for (const { path } of filesWithPriority) {
43
- const success = await this.loadRoute(app, path);
926
+ for (const file of files) {
927
+ const success = await this.loadRoute(app, file);
44
928
  if (success) ; else {
45
929
  failureCount++;
46
930
  }
@@ -51,13 +935,53 @@ var AutoRouteLoader = class {
51
935
  this.logStats(stats, elapsed);
52
936
  }
53
937
  if (failureCount > 0) {
54
- console.warn(`\u26A0\uFE0F ${failureCount} route(s) failed to load`);
938
+ routeLogger2.warn("Some routes failed to load", { failureCount });
55
939
  }
56
940
  return stats;
57
941
  }
58
942
  /**
59
- * Get route statistics
943
+ * Load routes from an external directory (e.g., from SPFN function packages)
944
+ * Reads package.json spfn.prefix and mounts routes under that prefix
945
+ *
946
+ * @param app - Hono app instance
947
+ * @param routesDir - Directory containing route handlers
948
+ * @param packageName - Name of the package (for logging)
949
+ * @param prefix - Optional prefix to mount routes under (from package.json spfn.prefix)
950
+ * @returns Route statistics
60
951
  */
952
+ async loadExternalRoutes(app, routesDir, packageName, prefix) {
953
+ const startTime = Date.now();
954
+ const tempRoutesDir = this.routesDir;
955
+ this.routesDir = routesDir;
956
+ const files = await this.scanFiles(routesDir);
957
+ if (files.length === 0) {
958
+ routeLogger2.warn("No route files found", { dir: routesDir, package: packageName });
959
+ this.routesDir = tempRoutesDir;
960
+ return this.getStats();
961
+ }
962
+ let successCount = 0;
963
+ let failureCount = 0;
964
+ for (const file of files) {
965
+ const success = await this.loadRoute(app, file, prefix);
966
+ if (success) {
967
+ successCount++;
968
+ } else {
969
+ failureCount++;
970
+ }
971
+ }
972
+ const elapsed = Date.now() - startTime;
973
+ if (this.debug) {
974
+ routeLogger2.info("External routes loaded", {
975
+ package: packageName,
976
+ prefix: prefix || "/",
977
+ total: successCount,
978
+ failed: failureCount,
979
+ elapsed: `${elapsed}ms`
980
+ });
981
+ }
982
+ this.routesDir = tempRoutesDir;
983
+ return this.getStats();
984
+ }
61
985
  getStats() {
62
986
  const stats = {
63
987
  total: this.routes.length,
@@ -77,12 +1001,6 @@ var AutoRouteLoader = class {
77
1001
  }
78
1002
  return stats;
79
1003
  }
80
- // ========================================================================
81
- // Private Methods
82
- // ========================================================================
83
- /**
84
- * Recursively scan directory for .ts files
85
- */
86
1004
  async scanFiles(dir, files = []) {
87
1005
  const entries = await readdir(dir);
88
1006
  for (const entry of entries) {
@@ -96,197 +1014,195 @@ var AutoRouteLoader = class {
96
1014
  }
97
1015
  return files;
98
1016
  }
99
- /**
100
- * Check if file is a valid route file
101
- */
102
1017
  isValidRouteFile(fileName) {
103
- return fileName.endsWith(".ts") && !fileName.endsWith(".test.ts") && !fileName.endsWith(".spec.ts") && !fileName.endsWith(".d.ts") && fileName !== "contract.ts";
1018
+ return fileName === "index.ts" || fileName === "index.js" || fileName === "index.mjs";
104
1019
  }
105
- /**
106
- * Load and register a single route
107
- * Returns true if successful, false if failed
108
- */
109
- async loadRoute(app, absolutePath) {
1020
+ async loadRoute(app, absolutePath, prefix) {
110
1021
  const relativePath = relative(this.routesDir, absolutePath);
111
1022
  try {
112
1023
  const module = await import(absolutePath);
113
- if (!module.default) {
114
- console.error(`\u274C ${relativePath}: Must export Hono instance as default`);
115
- return false;
116
- }
117
- if (typeof module.default.route !== "function") {
118
- console.error(`\u274C ${relativePath}: Default export is not a Hono instance`);
119
- return false;
120
- }
121
- const urlPath = this.fileToPath(relativePath);
122
- const priority = this.calculatePriority(relativePath);
123
- const normalizedPath = this.normalizePath(urlPath);
124
- const existingFile = this.registeredRoutes.get(normalizedPath);
125
- if (existingFile) {
126
- console.warn(`\u26A0\uFE0F Route conflict detected:`);
127
- console.warn(` Path: ${urlPath} (normalized: ${normalizedPath})`);
128
- console.warn(` Already registered by: ${existingFile}`);
129
- console.warn(` Attempted by: ${relativePath}`);
130
- console.warn(` \u2192 Skipping duplicate registration`);
1024
+ if (!this.validateModule(module, relativePath)) {
131
1025
  return false;
132
1026
  }
133
- this.registeredRoutes.set(normalizedPath, relativePath);
134
1027
  const hasContractMetas = module.default._contractMetas && module.default._contractMetas.size > 0;
135
- if (hasContractMetas) {
136
- const middlewarePath = urlPath === "/" ? "/*" : `${urlPath}/*`;
137
- app.use(middlewarePath, (c, next) => {
138
- const method = c.req.method;
139
- const requestPath = new URL(c.req.url).pathname;
140
- const relativePath2 = requestPath.startsWith(urlPath) ? requestPath.slice(urlPath.length) || "/" : requestPath;
141
- const key = `${method} ${relativePath2}`;
142
- const meta = module.default._contractMetas?.get(key);
143
- if (meta?.skipMiddlewares) {
144
- c.set("_skipMiddlewares", meta.skipMiddlewares);
145
- }
146
- return next();
1028
+ if (!hasContractMetas) {
1029
+ routeLogger2.error("Route must use contract-based routing", {
1030
+ file: relativePath,
1031
+ hint: "Export contracts using satisfies RouteContract and use app.bind()"
147
1032
  });
148
- for (const middleware of this.middlewares) {
149
- app.use(middlewarePath, async (c, next) => {
150
- const skipList = c.get("_skipMiddlewares") || [];
151
- if (skipList.includes(middleware.name)) {
152
- return next();
153
- }
154
- return middleware.handler(c, next);
1033
+ return false;
1034
+ }
1035
+ const contractPaths = this.extractContractPaths(module);
1036
+ if (prefix) {
1037
+ const invalidPaths = contractPaths.filter((path) => !path.startsWith(prefix));
1038
+ if (invalidPaths.length > 0) {
1039
+ routeLogger2.error("Contract paths must include the package prefix", {
1040
+ file: relativePath,
1041
+ prefix,
1042
+ invalidPaths,
1043
+ hint: `Contract paths should start with "${prefix}". Example: path: "${prefix}/labels"`
155
1044
  });
156
- }
157
- } else {
158
- const skipList = module.meta?.skipMiddlewares || [];
159
- const activeMiddlewares = this.middlewares.filter((m) => !skipList.includes(m.name));
160
- for (const middleware of activeMiddlewares) {
161
- app.use(urlPath, middleware.handler);
1045
+ return false;
162
1046
  }
163
1047
  }
164
- app.route(urlPath, module.default);
165
- this.routes.push({
166
- path: urlPath,
167
- file: relativePath,
168
- meta: module.meta,
169
- priority
1048
+ this.registerContractBasedMiddlewares(app, contractPaths, module);
1049
+ app.route("/", module.default);
1050
+ contractPaths.forEach((path) => {
1051
+ this.routes.push({
1052
+ path,
1053
+ // Use contract path as-is (already includes prefix)
1054
+ file: relativePath,
1055
+ meta: module.meta,
1056
+ priority: this.calculateContractPriority(path)
1057
+ });
1058
+ if (this.debug) {
1059
+ const icon = path.includes("*") ? "\u2B50" : path.includes(":") ? "\u{1F538}" : "\u{1F539}";
1060
+ routeLogger2.debug(`Registered route: ${path}`, { icon, file: relativePath });
1061
+ }
170
1062
  });
171
- if (this.debug) {
172
- const icon = priority === 1 ? "\u{1F539}" : priority === 2 ? "\u{1F538}" : "\u2B50";
173
- console.log(` ${icon} ${urlPath.padEnd(40)} \u2192 ${relativePath}`);
174
- }
175
1063
  return true;
176
1064
  } catch (error) {
177
- const err = error;
178
- if (err.message.includes("Cannot find module") || err.message.includes("MODULE_NOT_FOUND")) {
179
- console.error(`\u274C ${relativePath}: Missing dependency`);
180
- console.error(` ${err.message}`);
181
- console.error(` \u2192 Run: npm install`);
182
- } else if (err.message.includes("SyntaxError") || err.stack?.includes("SyntaxError")) {
183
- console.error(`\u274C ${relativePath}: Syntax error`);
184
- console.error(` ${err.message}`);
185
- if (this.debug && err.stack) {
186
- console.error(` Stack trace (first 5 lines):`);
187
- const stackLines = err.stack.split("\n").slice(0, 5);
188
- stackLines.forEach((line) => console.error(` ${line}`));
189
- }
190
- } else if (err.message.includes("Unexpected token")) {
191
- console.error(`\u274C ${relativePath}: Parse error`);
192
- console.error(` ${err.message}`);
193
- console.error(` \u2192 Check for syntax errors or invalid TypeScript`);
194
- } else {
195
- console.error(`\u274C ${relativePath}: ${err.message}`);
196
- if (this.debug && err.stack) {
197
- console.error(` Stack: ${err.stack}`);
1065
+ this.categorizeAndLogError(error, relativePath);
1066
+ return false;
1067
+ }
1068
+ }
1069
+ extractContractPaths(module) {
1070
+ const paths = /* @__PURE__ */ new Set();
1071
+ if (module.default._contractMetas) {
1072
+ for (const key of module.default._contractMetas.keys()) {
1073
+ const path = key.split(" ")[1];
1074
+ if (path) {
1075
+ paths.add(path);
198
1076
  }
199
1077
  }
200
- return false;
201
1078
  }
1079
+ return Array.from(paths);
202
1080
  }
203
- /**
204
- * Convert file path to URL path
205
- *
206
- * Examples:
207
- * - users/index.ts → /users
208
- * - users/[id].ts → /users/:id
209
- * - posts/[...slug].ts → /posts/*
210
- */
211
- fileToPath(filePath) {
212
- let path = filePath.replace(/\.ts$/, "");
213
- const segments = path.split("/");
214
- if (segments[segments.length - 1] === "index") {
215
- segments.pop();
1081
+ calculateContractPriority(path) {
1082
+ if (path.includes("*")) return 3;
1083
+ if (path.includes(":")) return 2;
1084
+ return 1;
1085
+ }
1086
+ validateModule(module, relativePath) {
1087
+ if (!module.default) {
1088
+ routeLogger2.error("Route must export Hono instance as default", { file: relativePath });
1089
+ return false;
216
1090
  }
217
- const transformed = segments.map((seg) => {
218
- if (/^\[\.\.\.[\w-]+]$/.test(seg)) {
219
- return "*";
220
- }
221
- if (/^\[[\w-]+]$/.test(seg)) {
222
- return ":" + seg.slice(1, -1);
1091
+ if (typeof module.default.route !== "function") {
1092
+ routeLogger2.error("Default export is not a Hono instance", { file: relativePath });
1093
+ return false;
1094
+ }
1095
+ return true;
1096
+ }
1097
+ registerContractBasedMiddlewares(app, contractPaths, module) {
1098
+ app.use("*", (c, next) => {
1099
+ const method = c.req.method;
1100
+ const requestPath = new URL(c.req.url).pathname;
1101
+ const key = `${method} ${requestPath}`;
1102
+ const meta = module.default._contractMetas?.get(key);
1103
+ if (meta?.skipMiddlewares) {
1104
+ c.set("_skipMiddlewares", meta.skipMiddlewares);
223
1105
  }
224
- if (seg === "index") {
225
- return null;
1106
+ return next();
1107
+ });
1108
+ for (const contractPath of contractPaths) {
1109
+ const middlewarePath = contractPath === "/" ? "/*" : `${contractPath}/*`;
1110
+ for (const middleware of this.middlewares) {
1111
+ app.use(middlewarePath, async (c, next) => {
1112
+ const skipList = c.get("_skipMiddlewares") || [];
1113
+ if (skipList.includes(middleware.name)) {
1114
+ return next();
1115
+ }
1116
+ return middleware.handler(c, next);
1117
+ });
226
1118
  }
227
- return seg;
228
- }).filter((seg) => seg !== null);
229
- const result = "/" + transformed.join("/");
230
- return result.replace(/\/+/g, "/").replace(/\/$/, "") || "/";
231
- }
232
- /**
233
- * Calculate route priority
234
- * 1 = static, 2 = dynamic, 3 = catch-all
235
- */
236
- calculatePriority(path) {
237
- if (/\[\.\.\.[\w-]+]/.test(path)) return 3;
238
- if (/\[[\w-]+]/.test(path)) return 2;
239
- return 1;
1119
+ }
240
1120
  }
241
- /**
242
- * Normalize path for conflict detection
243
- *
244
- * Converts dynamic parameter names to generic placeholders:
245
- * - /users/:id → /users/:param
246
- * - /users/:userId → /users/:param (conflict!)
247
- * - /posts/* → /posts/* (unchanged)
248
- *
249
- * This allows detection of routes with different param names
250
- * that would match the same URL patterns.
251
- */
252
- normalizePath(path) {
253
- return path.replace(/:\w+/g, ":param");
1121
+ categorizeAndLogError(error, relativePath) {
1122
+ const message = error.message;
1123
+ const stack = error.stack;
1124
+ if (message.includes("Cannot find module") || message.includes("MODULE_NOT_FOUND")) {
1125
+ routeLogger2.error("Missing dependency", {
1126
+ file: relativePath,
1127
+ error: message,
1128
+ hint: "Run: npm install"
1129
+ });
1130
+ } else if (message.includes("SyntaxError") || stack?.includes("SyntaxError")) {
1131
+ routeLogger2.error("Syntax error", {
1132
+ file: relativePath,
1133
+ error: message,
1134
+ ...this.debug && stack && {
1135
+ stack: stack.split("\n").slice(0, 5).join("\n")
1136
+ }
1137
+ });
1138
+ } else if (message.includes("Unexpected token")) {
1139
+ routeLogger2.error("Parse error", {
1140
+ file: relativePath,
1141
+ error: message,
1142
+ hint: "Check for syntax errors or invalid TypeScript"
1143
+ });
1144
+ } else {
1145
+ routeLogger2.error("Route loading failed", {
1146
+ file: relativePath,
1147
+ error: message,
1148
+ ...this.debug && stack && { stack }
1149
+ });
1150
+ }
254
1151
  }
255
- /**
256
- * Log statistics
257
- */
258
1152
  logStats(stats, elapsed) {
259
- console.log(`
260
- \u{1F4CA} Route Statistics:`);
261
- console.log(` Total: ${stats.total} routes`);
262
- console.log(
263
- ` Priority: ${stats.byPriority.static} static, ${stats.byPriority.dynamic} dynamic, ${stats.byPriority.catchAll} catch-all`
264
- );
265
- if (Object.keys(stats.byTag).length > 0) {
266
- const tagCounts = Object.entries(stats.byTag).map(([tag, count]) => `${tag}(${count})`).join(", ");
267
- console.log(` Tags: ${tagCounts}`);
268
- }
269
- console.log(`
270
- \u2705 Routes loaded in ${elapsed}ms
271
- `);
1153
+ const tagCounts = Object.entries(stats.byTag).map(([tag, count]) => `${tag}(${count})`).join(", ");
1154
+ routeLogger2.info("Routes loaded successfully", {
1155
+ total: stats.total,
1156
+ priority: {
1157
+ static: stats.byPriority.static,
1158
+ dynamic: stats.byPriority.dynamic,
1159
+ catchAll: stats.byPriority.catchAll
1160
+ },
1161
+ ...tagCounts && { tags: tagCounts },
1162
+ elapsed: `${elapsed}ms`
1163
+ });
272
1164
  }
273
1165
  };
274
1166
  async function loadRoutes(app, options) {
275
1167
  const routesDir = options?.routesDir ?? join(process.cwd(), "src", "server", "routes");
276
1168
  const debug = options?.debug ?? false;
277
1169
  const middlewares = options?.middlewares ?? [];
1170
+ const includeFunctionRoutes = options?.includeFunctionRoutes ?? true;
278
1171
  const loader = new AutoRouteLoader(routesDir, debug, middlewares);
279
- return loader.load(app);
1172
+ const stats = await loader.load(app);
1173
+ if (includeFunctionRoutes) {
1174
+ const { discoverFunctionRoutes: discoverFunctionRoutes2 } = await Promise.resolve().then(() => (init_function_routes(), function_routes_exports));
1175
+ const functionRoutes = discoverFunctionRoutes2();
1176
+ if (functionRoutes.length > 0) {
1177
+ routeLogger2.info("Loading function routes", { count: functionRoutes.length });
1178
+ for (const func of functionRoutes) {
1179
+ try {
1180
+ await loader.loadExternalRoutes(app, func.routesDir, func.packageName, func.prefix);
1181
+ routeLogger2.info("Function routes loaded", {
1182
+ package: func.packageName,
1183
+ routesDir: func.routesDir,
1184
+ prefix: func.prefix || "/"
1185
+ });
1186
+ } catch (error) {
1187
+ routeLogger2.error("Failed to load function routes", {
1188
+ package: func.packageName,
1189
+ error: error instanceof Error ? error.message : "Unknown error"
1190
+ });
1191
+ }
1192
+ }
1193
+ }
1194
+ }
1195
+ return stats;
280
1196
  }
281
1197
 
282
- // src/errors/database-errors.ts
283
- var DatabaseError = class extends Error {
1198
+ // src/errors/http-errors.ts
1199
+ var HttpError = class extends Error {
284
1200
  statusCode;
285
1201
  details;
286
1202
  timestamp;
287
- constructor(message, statusCode = 500, details) {
1203
+ constructor(message, statusCode, details) {
288
1204
  super(message);
289
- this.name = "DatabaseError";
1205
+ this.name = "HttpError";
290
1206
  this.statusCode = statusCode;
291
1207
  this.details = details;
292
1208
  this.timestamp = /* @__PURE__ */ new Date();
@@ -305,13 +1221,7 @@ var DatabaseError = class extends Error {
305
1221
  };
306
1222
  }
307
1223
  };
308
- var QueryError = class extends DatabaseError {
309
- constructor(message, statusCode = 500, details) {
310
- super(message, statusCode, details);
311
- this.name = "QueryError";
312
- }
313
- };
314
- var ValidationError = class extends QueryError {
1224
+ var ValidationError = class extends HttpError {
315
1225
  constructor(message, details) {
316
1226
  super(message, 400, details);
317
1227
  this.name = "ValidationError";
@@ -321,8 +1231,9 @@ var ValidationError = class extends QueryError {
321
1231
  // src/route/bind.ts
322
1232
  function bind(contract, handler) {
323
1233
  return async (rawContext) => {
324
- const params = rawContext.req.param();
1234
+ let params = rawContext.req.param();
325
1235
  if (contract.params) {
1236
+ params = Value.Convert(contract.params, params);
326
1237
  const errors = [...Value.Errors(contract.params, params)];
327
1238
  if (errors.length > 0) {
328
1239
  throw new ValidationError(
@@ -338,7 +1249,7 @@ function bind(contract, handler) {
338
1249
  }
339
1250
  }
340
1251
  const url = new URL(rawContext.req.url);
341
- const query = {};
1252
+ let query = {};
342
1253
  url.searchParams.forEach((v, k) => {
343
1254
  const existing = query[k];
344
1255
  if (existing) {
@@ -348,6 +1259,7 @@ function bind(contract, handler) {
348
1259
  }
349
1260
  });
350
1261
  if (contract.query) {
1262
+ query = Value.Convert(contract.query, query);
351
1263
  const errors = [...Value.Errors(contract.query, query)];
352
1264
  if (errors.length > 0) {
353
1265
  throw new ValidationError(
@@ -365,10 +1277,10 @@ function bind(contract, handler) {
365
1277
  const routeContext = {
366
1278
  params,
367
1279
  query,
368
- // data() - validates and returns body
369
1280
  data: async () => {
370
- const body = await rawContext.req.json();
1281
+ let body = await rawContext.req.json();
371
1282
  if (contract.body) {
1283
+ body = Value.Convert(contract.body, body);
372
1284
  const errors = [...Value.Errors(contract.body, body)];
373
1285
  if (errors.length > 0) {
374
1286
  throw new ValidationError(
@@ -385,58 +1297,165 @@ function bind(contract, handler) {
385
1297
  }
386
1298
  return body;
387
1299
  },
388
- // json() - returns typed response
389
1300
  json: (data, status, headers) => {
1301
+ const errorHandlerEnabled = rawContext.get("errorHandlerEnabled");
1302
+ if (errorHandlerEnabled && process.env.NODE_ENV !== "production") {
1303
+ const hasSuccessField = data && typeof data === "object" && "success" in data;
1304
+ if (!hasSuccessField) {
1305
+ console.warn(
1306
+ "[SPFN] Warning: ErrorHandler is enabled but c.json() is being used with non-standard response format.\nConsider using c.success() for consistent API responses, or disable ErrorHandler if you prefer custom formats."
1307
+ );
1308
+ }
1309
+ }
390
1310
  return rawContext.json(data, status, headers);
391
1311
  },
392
- // raw Hono context for advanced usage
1312
+ success: (data, meta, status = 200) => {
1313
+ const response = {
1314
+ success: true,
1315
+ data
1316
+ };
1317
+ if (meta) {
1318
+ response.meta = meta;
1319
+ }
1320
+ return rawContext.json(response, status);
1321
+ },
1322
+ paginated: (data, page, limit, total) => {
1323
+ const response = {
1324
+ success: true,
1325
+ data,
1326
+ meta: {
1327
+ pagination: {
1328
+ page,
1329
+ limit,
1330
+ total,
1331
+ totalPages: Math.ceil(total / limit)
1332
+ }
1333
+ }
1334
+ };
1335
+ return rawContext.json(response, 200);
1336
+ },
393
1337
  raw: rawContext
394
1338
  };
395
1339
  return handler(routeContext);
396
1340
  };
397
1341
  }
1342
+
1343
+ // src/middleware/error-handler.ts
1344
+ init_logger2();
1345
+ var errorLogger = logger.child("error-handler");
1346
+ function ErrorHandler(options = {}) {
1347
+ const {
1348
+ includeStack = process.env.NODE_ENV !== "production",
1349
+ enableLogging = true
1350
+ } = options;
1351
+ return (err, c) => {
1352
+ const errorWithCode = err;
1353
+ const statusCode = errorWithCode.statusCode || 500;
1354
+ const errorType = err.name || "Error";
1355
+ if (enableLogging) {
1356
+ const logLevel = statusCode >= 500 ? "error" : "warn";
1357
+ const logData = {
1358
+ type: errorType,
1359
+ message: err.message,
1360
+ statusCode,
1361
+ path: c.req.path,
1362
+ method: c.req.method
1363
+ };
1364
+ if (errorWithCode.details) {
1365
+ logData.details = errorWithCode.details;
1366
+ }
1367
+ if (statusCode >= 500 && includeStack) {
1368
+ logData.stack = err.stack;
1369
+ }
1370
+ errorLogger[logLevel]("Error occurred", logData);
1371
+ }
1372
+ const response = {
1373
+ success: false,
1374
+ error: {
1375
+ message: err.message || "Internal Server Error",
1376
+ type: errorType,
1377
+ statusCode
1378
+ }
1379
+ };
1380
+ if (errorWithCode.details) {
1381
+ response.error.details = errorWithCode.details;
1382
+ }
1383
+ if (includeStack) {
1384
+ response.error.stack = err.stack;
1385
+ }
1386
+ return c.json(response, statusCode);
1387
+ };
1388
+ }
1389
+
1390
+ // src/middleware/request-logger.ts
1391
+ init_logger2();
1392
+
1393
+ // src/route/create-app.ts
398
1394
  function createApp() {
399
1395
  const hono = new Hono();
400
1396
  const app = hono;
401
1397
  app._contractMetas = /* @__PURE__ */ new Map();
1398
+ app.onError(ErrorHandler());
1399
+ const methodMap = /* @__PURE__ */ new Map([
1400
+ ["get", (path, handlers) => hono.get(path, ...handlers)],
1401
+ ["post", (path, handlers) => hono.post(path, ...handlers)],
1402
+ ["put", (path, handlers) => hono.put(path, ...handlers)],
1403
+ ["patch", (path, handlers) => hono.patch(path, ...handlers)],
1404
+ ["delete", (path, handlers) => hono.delete(path, ...handlers)]
1405
+ ]);
402
1406
  app.bind = function(contract, ...args) {
403
1407
  const method = contract.method.toLowerCase();
404
1408
  const path = contract.path;
405
1409
  const [middlewares, handler] = args.length === 1 ? [[], args[0]] : [args[0], args[1]];
406
- if (contract.meta) {
407
- const key = `${contract.method} ${path}`;
408
- app._contractMetas.set(key, contract.meta);
409
- }
1410
+ const key = `${contract.method} ${path}`;
1411
+ app._contractMetas.set(key, contract.meta || {});
410
1412
  const boundHandler = bind(contract, handler);
411
1413
  const handlers = middlewares.length > 0 ? [...middlewares, boundHandler] : [boundHandler];
412
- switch (method) {
413
- case "get":
414
- hono.get(path, ...handlers);
415
- break;
416
- case "post":
417
- hono.post(path, ...handlers);
418
- break;
419
- case "put":
420
- hono.put(path, ...handlers);
421
- break;
422
- case "patch":
423
- hono.patch(path, ...handlers);
424
- break;
425
- case "delete":
426
- hono.delete(path, ...handlers);
427
- break;
428
- default:
429
- throw new Error(`Unsupported HTTP method: ${contract.method}`);
1414
+ const registerMethod = methodMap.get(method);
1415
+ if (!registerMethod) {
1416
+ throw new Error(`Unsupported HTTP method: ${contract.method}`);
430
1417
  }
1418
+ registerMethod(path, handlers);
431
1419
  };
432
1420
  return app;
433
1421
  }
1422
+ function ApiSuccessSchema(dataSchema) {
1423
+ return Type.Object({
1424
+ success: Type.Literal(true),
1425
+ data: dataSchema,
1426
+ meta: Type.Optional(Type.Object({
1427
+ timestamp: Type.Optional(Type.String()),
1428
+ requestId: Type.Optional(Type.String()),
1429
+ pagination: Type.Optional(Type.Object({
1430
+ page: Type.Number(),
1431
+ limit: Type.Number(),
1432
+ total: Type.Number(),
1433
+ totalPages: Type.Number()
1434
+ }))
1435
+ }))
1436
+ });
1437
+ }
1438
+ function ApiErrorSchema() {
1439
+ return Type.Object({
1440
+ success: Type.Literal(false),
1441
+ error: Type.Object({
1442
+ message: Type.String(),
1443
+ type: Type.String(),
1444
+ statusCode: Type.Number(),
1445
+ stack: Type.Optional(Type.String()),
1446
+ details: Type.Optional(Type.Any())
1447
+ })
1448
+ });
1449
+ }
1450
+ function ApiResponseSchema(dataSchema) {
1451
+ return ApiSuccessSchema(dataSchema);
1452
+ }
434
1453
 
435
1454
  // src/route/types.ts
436
1455
  function isHttpMethod(value) {
437
1456
  return typeof value === "string" && ["GET", "POST", "PUT", "PATCH", "DELETE"].includes(value);
438
1457
  }
439
1458
 
440
- export { AutoRouteLoader, bind, createApp, isHttpMethod, loadRoutes };
1459
+ export { ApiErrorSchema, ApiResponseSchema, ApiSuccessSchema, AutoRouteLoader, bind, createApp, isHttpMethod, loadRoutes };
441
1460
  //# sourceMappingURL=index.js.map
442
1461
  //# sourceMappingURL=index.js.map