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

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