@spfn/core 0.1.0-alpha.21 → 0.1.0-alpha.23

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.
@@ -1,971 +1,1143 @@
1
1
  import { drizzle } from 'drizzle-orm/postgres-js';
2
2
  import { migrate } from 'drizzle-orm/postgres-js/migrator';
3
3
  import postgres from 'postgres';
4
- import { config } from 'dotenv';
5
4
  import { fileURLToPath } from 'url';
6
5
  import { dirname, join, resolve } from 'path';
6
+ import { config } from 'dotenv';
7
+ import { existsSync, mkdirSync, createWriteStream, readFileSync } from 'fs';
8
+ import pino from 'pino';
7
9
  import { mkdir, writeFile, readdir, stat } from 'fs/promises';
8
10
  import * as ts from 'typescript';
9
- import { existsSync, mkdirSync, createWriteStream, readFileSync } from 'fs';
10
11
  import { watch } from 'chokidar';
11
- import pino from 'pino';
12
12
  import chalk from 'chalk';
13
13
 
14
14
  // src/scripts/migrate.ts
15
- config({ path: ".env.local" });
16
- var __filename = fileURLToPath(import.meta.url);
17
- var __dirname = dirname(__filename);
18
- var projectRoot = join(__dirname, "../../..");
19
- var DATABASE_URL = process.env.DATABASE_URL;
20
- if (!DATABASE_URL) {
21
- console.error("\u274C DATABASE_URL environment variable is required");
22
- process.exit(1);
23
- }
24
- async function runMigrations() {
25
- console.log("\u{1F504} Starting database migration...");
26
- console.log(`\u{1F4C2} Migrations folder: ${join(projectRoot, "drizzle")}`);
27
- const migrationConnection = postgres(DATABASE_URL, { max: 1 });
28
- const db = drizzle(migrationConnection);
29
- try {
30
- console.log("\u23F3 Applying migrations...");
31
- await migrate(db, {
32
- migrationsFolder: join(projectRoot, "drizzle")
33
- });
34
- console.log("\u2705 Migration completed successfully");
35
- } catch (error) {
36
- console.error("\u274C Migration failed:", error);
37
- throw error;
38
- } finally {
39
- await migrationConnection.end();
40
- console.log("\u{1F50C} Database connection closed");
41
- }
42
- }
43
- runMigrations().then(() => {
44
- console.log("\u{1F389} All migrations applied");
45
- process.exit(0);
46
- }).catch((error) => {
47
- console.error("\u{1F4A5} Migration process failed:", error);
48
- process.exit(1);
49
- });
50
- async function scanContracts(routesDir) {
51
- const contractFiles = await scanContractFiles(routesDir);
52
- const mappings = [];
53
- for (let i = 0; i < contractFiles.length; i++) {
54
- const filePath = contractFiles[i];
55
- const exports = extractContractExports(filePath);
56
- const basePath = getBasePathFromFile(filePath, routesDir);
57
- for (let j = 0; j < exports.length; j++) {
58
- const contractExport = exports[j];
59
- const fullPath = combinePaths(basePath, contractExport.path);
60
- mappings.push({
61
- method: contractExport.method,
62
- path: fullPath,
63
- contractName: contractExport.name,
64
- contractImportPath: getImportPathFromRoutes(filePath, routesDir),
65
- routeFile: "",
66
- // Not needed anymore
67
- contractFile: filePath
15
+ var PinoAdapter = class _PinoAdapter {
16
+ logger;
17
+ constructor(config) {
18
+ const isProduction = process.env.NODE_ENV === "production";
19
+ const isDevelopment = process.env.NODE_ENV === "development";
20
+ const fileLoggingEnabled = process.env.LOGGER_FILE_ENABLED === "true";
21
+ const targets = [];
22
+ if (!isProduction && isDevelopment) {
23
+ targets.push({
24
+ target: "pino-pretty",
25
+ level: "debug",
26
+ options: {
27
+ colorize: true,
28
+ translateTime: "SYS:yyyy-mm-dd HH:MM:ss.l",
29
+ ignore: "pid,hostname"
30
+ }
68
31
  });
69
32
  }
70
- }
71
- return mappings;
72
- }
73
- async function scanContractFiles(dir, files = []) {
74
- try {
75
- const entries = await readdir(dir);
76
- for (let i = 0; i < entries.length; i++) {
77
- const entry = entries[i];
78
- const fullPath = join(dir, entry);
79
- const fileStat = await stat(fullPath);
80
- if (fileStat.isDirectory()) {
81
- await scanContractFiles(fullPath, files);
82
- } else if (entry === "contract.ts") {
83
- files.push(fullPath);
84
- }
85
- }
86
- } catch (error) {
87
- }
88
- return files;
89
- }
90
- function extractContractExports(filePath) {
91
- const sourceCode = readFileSync(filePath, "utf-8");
92
- const sourceFile = ts.createSourceFile(
93
- filePath,
94
- sourceCode,
95
- ts.ScriptTarget.Latest,
96
- true
97
- );
98
- const exports = [];
99
- function visit(node) {
100
- if (ts.isVariableStatement(node)) {
101
- const hasExport = node.modifiers?.some(
102
- (m) => m.kind === ts.SyntaxKind.ExportKeyword
103
- );
104
- if (hasExport && node.declarationList.declarations.length > 0) {
105
- const declaration = node.declarationList.declarations[0];
106
- if (ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name) && declaration.initializer && ts.isObjectLiteralExpression(declaration.initializer)) {
107
- const name = declaration.name.text;
108
- if (isContractName(name)) {
109
- const contractData = extractContractData(declaration.initializer);
110
- if (contractData.method && contractData.path) {
111
- exports.push({
112
- name,
113
- method: contractData.method,
114
- path: contractData.path
115
- });
116
- }
117
- }
33
+ if (fileLoggingEnabled && isProduction) {
34
+ const logDir = process.env.LOG_DIR || "./logs";
35
+ const maxFileSize = process.env.LOG_MAX_FILE_SIZE || "10M";
36
+ const maxFiles = parseInt(process.env.LOG_MAX_FILES || "10", 10);
37
+ targets.push({
38
+ target: "pino-roll",
39
+ level: "info",
40
+ options: {
41
+ file: `${logDir}/app.log`,
42
+ frequency: "daily",
43
+ size: maxFileSize,
44
+ limit: { count: maxFiles },
45
+ mkdir: true
118
46
  }
119
- }
47
+ });
120
48
  }
121
- ts.forEachChild(node, visit);
49
+ this.logger = pino({
50
+ level: config.level,
51
+ // Transport 설정 (targets가 있으면 사용, 없으면 기본 stdout)
52
+ transport: targets.length > 0 ? { targets } : void 0,
53
+ // 기본 필드
54
+ base: config.module ? { module: config.module } : void 0
55
+ });
122
56
  }
123
- visit(sourceFile);
124
- return exports;
125
- }
126
- function extractContractData(objectLiteral) {
127
- const result = {};
128
- for (let i = 0; i < objectLiteral.properties.length; i++) {
129
- const prop = objectLiteral.properties[i];
130
- if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
131
- const propName = prop.name.text;
132
- if (propName === "method") {
133
- let value;
134
- if (ts.isStringLiteral(prop.initializer)) {
135
- value = prop.initializer.text;
136
- } else if (ts.isAsExpression(prop.initializer) && ts.isStringLiteral(prop.initializer.expression)) {
137
- value = prop.initializer.expression.text;
138
- }
139
- if (value) result.method = value;
140
- } else if (propName === "path") {
141
- let value;
142
- if (ts.isStringLiteral(prop.initializer)) {
143
- value = prop.initializer.text;
144
- } else if (ts.isAsExpression(prop.initializer) && ts.isStringLiteral(prop.initializer.expression)) {
145
- value = prop.initializer.expression.text;
146
- }
147
- if (value) result.path = value;
148
- }
149
- }
57
+ child(module) {
58
+ const childLogger = new _PinoAdapter({ level: this.logger.level, module });
59
+ childLogger.logger = this.logger.child({ module });
60
+ return childLogger;
150
61
  }
151
- return result;
152
- }
153
- function isContractName(name) {
154
- return name.indexOf("Contract") !== -1 || name.indexOf("contract") !== -1 || name.endsWith("Schema") || name.endsWith("schema");
155
- }
156
- function getBasePathFromFile(filePath, routesDir) {
157
- let relativePath = filePath.replace(routesDir, "");
158
- if (relativePath.startsWith("/")) {
159
- relativePath = relativePath.slice(1);
62
+ debug(message, context) {
63
+ this.logger.debug(context || {}, message);
160
64
  }
161
- relativePath = relativePath.replace("/contract.ts", "");
162
- if (relativePath === "index" || relativePath === "") {
163
- return "/";
65
+ info(message, context) {
66
+ this.logger.info(context || {}, message);
164
67
  }
165
- const segments = relativePath.split("/");
166
- const transformed = [];
167
- for (let i = 0; i < segments.length; i++) {
168
- const seg = segments[i];
169
- if (seg === "index") {
170
- continue;
171
- }
172
- if (seg.startsWith("[") && seg.endsWith("]")) {
173
- transformed.push(":" + seg.slice(1, -1));
68
+ warn(message, errorOrContext, context) {
69
+ if (errorOrContext instanceof Error) {
70
+ this.logger.warn({ err: errorOrContext, ...context }, message);
174
71
  } else {
175
- transformed.push(seg);
72
+ this.logger.warn(errorOrContext || {}, message);
176
73
  }
177
74
  }
178
- if (transformed.length === 0) {
179
- return "/";
180
- }
181
- return "/" + transformed.join("/");
182
- }
183
- function combinePaths(basePath, contractPath) {
184
- basePath = basePath || "/";
185
- contractPath = contractPath || "/";
186
- if (basePath.endsWith("/") && basePath !== "/") {
187
- basePath = basePath.slice(0, -1);
75
+ error(message, errorOrContext, context) {
76
+ if (errorOrContext instanceof Error) {
77
+ this.logger.error({ err: errorOrContext, ...context }, message);
78
+ } else {
79
+ this.logger.error(errorOrContext || {}, message);
80
+ }
188
81
  }
189
- if (contractPath.startsWith("/") && contractPath !== "/") {
190
- if (basePath === "/") {
191
- return contractPath;
82
+ fatal(message, errorOrContext, context) {
83
+ if (errorOrContext instanceof Error) {
84
+ this.logger.fatal({ err: errorOrContext, ...context }, message);
85
+ } else {
86
+ this.logger.fatal(errorOrContext || {}, message);
192
87
  }
193
- return basePath + contractPath;
194
88
  }
195
- if (contractPath === "/") {
196
- return basePath;
89
+ async close() {
197
90
  }
198
- return basePath + "/" + contractPath;
199
- }
200
- function getImportPathFromRoutes(filePath, routesDir) {
201
- let relativePath = filePath.replace(routesDir, "");
202
- if (relativePath.startsWith("/")) {
203
- relativePath = relativePath.slice(1);
91
+ };
92
+
93
+ // src/logger/logger.ts
94
+ var Logger = class _Logger {
95
+ config;
96
+ module;
97
+ constructor(config) {
98
+ this.config = config;
99
+ this.module = config.module;
204
100
  }
205
- if (relativePath.endsWith(".ts")) {
206
- relativePath = relativePath.slice(0, -3);
101
+ /**
102
+ * Get current log level
103
+ */
104
+ get level() {
105
+ return this.config.level;
207
106
  }
208
- return "@/server/routes/" + relativePath;
209
- }
210
-
211
- // src/codegen/route-scanner.ts
212
- function groupByResource(mappings) {
213
- const grouped = {};
214
- for (let i = 0; i < mappings.length; i++) {
215
- const mapping = mappings[i];
216
- const resource = extractResourceName(mapping.path);
217
- if (!grouped[resource]) {
218
- grouped[resource] = [];
107
+ /**
108
+ * Create child logger (per module)
109
+ */
110
+ child(module) {
111
+ return new _Logger({
112
+ ...this.config,
113
+ module
114
+ });
115
+ }
116
+ /**
117
+ * Debug log
118
+ */
119
+ debug(message, context) {
120
+ this.log("debug", message, void 0, context);
121
+ }
122
+ /**
123
+ * Info log
124
+ */
125
+ info(message, context) {
126
+ this.log("info", message, void 0, context);
127
+ }
128
+ warn(message, errorOrContext, context) {
129
+ if (errorOrContext instanceof Error) {
130
+ this.log("warn", message, errorOrContext, context);
131
+ } else {
132
+ this.log("warn", message, void 0, errorOrContext);
219
133
  }
220
- grouped[resource].push(mapping);
221
134
  }
222
- return grouped;
223
- }
224
- function extractResourceName(path) {
225
- const segments = path.slice(1).split("/").filter((s) => s && s !== "*");
226
- const staticSegments = [];
227
- for (let i = 0; i < segments.length; i++) {
228
- const seg = segments[i];
229
- if (!seg.startsWith(":")) {
230
- staticSegments.push(seg);
135
+ error(message, errorOrContext, context) {
136
+ if (errorOrContext instanceof Error) {
137
+ this.log("error", message, errorOrContext, context);
138
+ } else {
139
+ this.log("error", message, void 0, errorOrContext);
231
140
  }
232
141
  }
233
- if (staticSegments.length === 0) {
234
- return "root";
142
+ fatal(message, errorOrContext, context) {
143
+ if (errorOrContext instanceof Error) {
144
+ this.log("fatal", message, errorOrContext, context);
145
+ } else {
146
+ this.log("fatal", message, void 0, errorOrContext);
147
+ }
235
148
  }
236
- if (staticSegments.length === 1) {
237
- return staticSegments[0];
149
+ /**
150
+ * Log processing (internal)
151
+ */
152
+ log(level, message, error, context) {
153
+ const metadata = {
154
+ timestamp: /* @__PURE__ */ new Date(),
155
+ level,
156
+ message,
157
+ module: this.module,
158
+ error,
159
+ context
160
+ };
161
+ this.processTransports(metadata);
238
162
  }
239
- const result = [staticSegments[0]];
240
- for (let i = 1; i < staticSegments.length; i++) {
241
- const seg = staticSegments[i];
242
- result.push(seg.charAt(0).toUpperCase() + seg.slice(1));
163
+ /**
164
+ * Process Transports
165
+ */
166
+ processTransports(metadata) {
167
+ const promises = this.config.transports.filter((transport) => transport.enabled).map((transport) => this.safeTransportLog(transport, metadata));
168
+ Promise.all(promises).catch((error) => {
169
+ const errorMessage = error instanceof Error ? error.message : String(error);
170
+ process.stderr.write(`[Logger] Transport error: ${errorMessage}
171
+ `);
172
+ });
243
173
  }
244
- return result.join("");
245
- }
246
- async function generateClient(mappings, options) {
247
- const startTime = Date.now();
248
- const grouped = groupByResource(mappings);
249
- const resourceNames = Object.keys(grouped);
250
- const code = generateClientCode(mappings, grouped, options);
251
- await mkdir(dirname(options.outputPath), { recursive: true });
252
- await writeFile(options.outputPath, code, "utf-8");
253
- const stats = {
254
- routesScanned: mappings.length,
255
- contractsFound: mappings.length,
256
- contractFiles: countUniqueContractFiles(mappings),
257
- resourcesGenerated: resourceNames.length,
258
- methodsGenerated: mappings.length,
259
- duration: Date.now() - startTime
260
- };
261
- return stats;
174
+ /**
175
+ * Transport log (error-safe)
176
+ */
177
+ async safeTransportLog(transport, metadata) {
178
+ try {
179
+ await transport.log(metadata);
180
+ } catch (error) {
181
+ const errorMessage = error instanceof Error ? error.message : String(error);
182
+ process.stderr.write(`[Logger] Transport "${transport.name}" failed: ${errorMessage}
183
+ `);
184
+ }
185
+ }
186
+ /**
187
+ * Close all Transports
188
+ */
189
+ async close() {
190
+ const closePromises = this.config.transports.filter((transport) => transport.close).map((transport) => transport.close());
191
+ await Promise.all(closePromises);
192
+ }
193
+ };
194
+
195
+ // src/logger/types.ts
196
+ var LOG_LEVEL_PRIORITY = {
197
+ debug: 0,
198
+ info: 1,
199
+ warn: 2,
200
+ error: 3,
201
+ fatal: 4
202
+ };
203
+
204
+ // src/logger/formatters.ts
205
+ var COLORS = {
206
+ reset: "\x1B[0m",
207
+ bright: "\x1B[1m",
208
+ dim: "\x1B[2m",
209
+ // 로그 레벨 컬러
210
+ debug: "\x1B[36m",
211
+ // cyan
212
+ info: "\x1B[32m",
213
+ // green
214
+ warn: "\x1B[33m",
215
+ // yellow
216
+ error: "\x1B[31m",
217
+ // red
218
+ fatal: "\x1B[35m",
219
+ // magenta
220
+ // 추가 컬러
221
+ gray: "\x1B[90m"
222
+ };
223
+ function colorizeLevel(level) {
224
+ const color = COLORS[level];
225
+ const levelStr = level.toUpperCase().padEnd(5);
226
+ return `${color}${levelStr}${COLORS.reset}`;
262
227
  }
263
- function generateClientCode(mappings, grouped, options) {
264
- let code = "";
265
- code += generateHeader();
266
- code += generateImports(mappings, options);
267
- code += generateApiObject(grouped, options);
268
- code += generateFooter();
269
- return code;
228
+ function formatTimestamp(date) {
229
+ return date.toISOString();
270
230
  }
271
- function generateHeader() {
272
- return `/**
273
- * Auto-generated API Client
274
- *
275
- * Generated by @spfn/core codegen
276
- * DO NOT EDIT MANUALLY
277
- *
278
- * @generated ${(/* @__PURE__ */ new Date()).toISOString()}
279
- */
280
-
281
- `;
231
+ function formatTimestampHuman(date) {
232
+ const year = date.getFullYear();
233
+ const month = String(date.getMonth() + 1).padStart(2, "0");
234
+ const day = String(date.getDate()).padStart(2, "0");
235
+ const hours = String(date.getHours()).padStart(2, "0");
236
+ const minutes = String(date.getMinutes()).padStart(2, "0");
237
+ const seconds = String(date.getSeconds()).padStart(2, "0");
238
+ const ms = String(date.getMilliseconds()).padStart(3, "0");
239
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
282
240
  }
283
- function generateImports(mappings, options) {
284
- let code = "";
285
- code += `import { client } from '@spfn/core/client';
286
- `;
287
- if (options.includeTypes !== false) {
288
- code += `import type { InferContract } from '@spfn/core';
289
- `;
241
+ function formatError(error) {
242
+ const lines = [];
243
+ lines.push(`${error.name}: ${error.message}`);
244
+ if (error.stack) {
245
+ const stackLines = error.stack.split("\n").slice(1);
246
+ lines.push(...stackLines);
290
247
  }
291
- code += `
292
- `;
293
- const importGroups = groupContractsByImportPath(mappings);
294
- const importPaths = Object.keys(importGroups);
295
- for (let i = 0; i < importPaths.length; i++) {
296
- const importPath = importPaths[i];
297
- const contracts = importGroups[importPath];
298
- code += `import { ${contracts.join(", ")} } from '${importPath}';
299
- `;
248
+ return lines.join("\n");
249
+ }
250
+ function formatContext(context) {
251
+ try {
252
+ return JSON.stringify(context, null, 2);
253
+ } catch (error) {
254
+ return "[Context serialization failed]";
300
255
  }
301
- code += `
302
- `;
303
- return code;
304
256
  }
305
- function groupContractsByImportPath(mappings) {
306
- const groups = {};
307
- for (let i = 0; i < mappings.length; i++) {
308
- const mapping = mappings[i];
309
- const path = mapping.contractImportPath;
310
- if (!groups[path]) {
311
- groups[path] = /* @__PURE__ */ new Set();
257
+ function formatConsole(metadata, colorize = true) {
258
+ const parts = [];
259
+ const timestamp = formatTimestampHuman(metadata.timestamp);
260
+ if (colorize) {
261
+ parts.push(`${COLORS.gray}${timestamp}${COLORS.reset}`);
262
+ } else {
263
+ parts.push(timestamp);
264
+ }
265
+ if (colorize) {
266
+ parts.push(colorizeLevel(metadata.level));
267
+ } else {
268
+ parts.push(metadata.level.toUpperCase().padEnd(5));
269
+ }
270
+ if (metadata.module) {
271
+ if (colorize) {
272
+ parts.push(`${COLORS.dim}[${metadata.module}]${COLORS.reset}`);
273
+ } else {
274
+ parts.push(`[${metadata.module}]`);
312
275
  }
313
- groups[path].add(mapping.contractName);
314
276
  }
315
- const result = {};
316
- const keys = Object.keys(groups);
317
- for (let i = 0; i < keys.length; i++) {
318
- const key = keys[i];
319
- result[key] = Array.from(groups[key]);
277
+ parts.push(metadata.message);
278
+ let output = parts.join(" ");
279
+ if (metadata.context && Object.keys(metadata.context).length > 0) {
280
+ output += "\n" + formatContext(metadata.context);
320
281
  }
321
- return result;
282
+ if (metadata.error) {
283
+ output += "\n" + formatError(metadata.error);
284
+ }
285
+ return output;
322
286
  }
323
- function generateApiObject(grouped, options) {
324
- let code = "";
325
- code += `/**
326
- * Type-safe API client
327
- */
328
- export const api = {
329
- `;
330
- const resourceNames = Object.keys(grouped);
331
- for (let i = 0; i < resourceNames.length; i++) {
332
- const resourceName = resourceNames[i];
333
- const routes = grouped[resourceName];
334
- code += ` ${resourceName}: {
335
- `;
336
- for (let j = 0; j < routes.length; j++) {
337
- const route = routes[j];
338
- code += generateMethodCode(route, options);
287
+ function formatJSON(metadata) {
288
+ const obj = {
289
+ timestamp: formatTimestamp(metadata.timestamp),
290
+ level: metadata.level,
291
+ message: metadata.message
292
+ };
293
+ if (metadata.module) {
294
+ obj.module = metadata.module;
295
+ }
296
+ if (metadata.context) {
297
+ obj.context = metadata.context;
298
+ }
299
+ if (metadata.error) {
300
+ obj.error = {
301
+ name: metadata.error.name,
302
+ message: metadata.error.message,
303
+ stack: metadata.error.stack
304
+ };
305
+ }
306
+ return JSON.stringify(obj);
307
+ }
308
+
309
+ // src/logger/transports/console.ts
310
+ var ConsoleTransport = class {
311
+ name = "console";
312
+ level;
313
+ enabled;
314
+ colorize;
315
+ constructor(config) {
316
+ this.level = config.level;
317
+ this.enabled = config.enabled;
318
+ this.colorize = config.colorize ?? true;
319
+ }
320
+ async log(metadata) {
321
+ if (!this.enabled) {
322
+ return;
339
323
  }
340
- code += ` }`;
341
- if (i < resourceNames.length - 1) {
342
- code += `,`;
324
+ if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
325
+ return;
326
+ }
327
+ const message = formatConsole(metadata, this.colorize);
328
+ if (metadata.level === "warn" || metadata.level === "error" || metadata.level === "fatal") {
329
+ console.error(message);
330
+ } else {
331
+ console.log(message);
343
332
  }
344
- code += `
345
- `;
346
333
  }
347
- code += `} as const;
348
-
349
- `;
350
- return code;
351
- }
352
- function generateMethodCode(mapping, options) {
353
- const methodName = generateMethodName(mapping);
354
- const contractType = `typeof ${mapping.contractName}`;
355
- const hasParams = mapping.path.includes(":");
356
- const hasBody = ["POST", "PUT", "PATCH"].indexOf(mapping.method) !== -1;
357
- let code = "";
358
- if (options.includeJsDoc !== false) {
359
- code += ` /**
360
- `;
361
- code += ` * ${mapping.method} ${mapping.path}
362
- `;
363
- code += ` */
364
- `;
334
+ };
335
+ var FileTransport = class {
336
+ name = "file";
337
+ level;
338
+ enabled;
339
+ logDir;
340
+ currentStream = null;
341
+ currentFilename = null;
342
+ constructor(config) {
343
+ this.level = config.level;
344
+ this.enabled = config.enabled;
345
+ this.logDir = config.logDir;
346
+ if (!existsSync(this.logDir)) {
347
+ mkdirSync(this.logDir, { recursive: true });
348
+ }
349
+ }
350
+ async log(metadata) {
351
+ if (!this.enabled) {
352
+ return;
353
+ }
354
+ if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
355
+ return;
356
+ }
357
+ const message = formatJSON(metadata);
358
+ const filename = this.getLogFilename(metadata.timestamp);
359
+ if (this.currentFilename !== filename) {
360
+ await this.rotateStream(filename);
361
+ }
362
+ if (this.currentStream) {
363
+ return new Promise((resolve2, reject) => {
364
+ this.currentStream.write(message + "\n", "utf-8", (error) => {
365
+ if (error) {
366
+ process.stderr.write(`[FileTransport] Failed to write log: ${error.message}
367
+ `);
368
+ reject(error);
369
+ } else {
370
+ resolve2();
371
+ }
372
+ });
373
+ });
374
+ }
365
375
  }
366
- code += ` ${methodName}: (`;
367
- const params = [];
368
- if (hasParams) {
369
- params.push(`params: InferContract<${contractType}>['params']`);
376
+ /**
377
+ * 스트림 교체 (날짜 변경 시)
378
+ */
379
+ async rotateStream(filename) {
380
+ if (this.currentStream) {
381
+ await this.closeStream();
382
+ }
383
+ const filepath = join(this.logDir, filename);
384
+ this.currentStream = createWriteStream(filepath, {
385
+ flags: "a",
386
+ // append mode
387
+ encoding: "utf-8"
388
+ });
389
+ this.currentFilename = filename;
390
+ this.currentStream.on("error", (error) => {
391
+ process.stderr.write(`[FileTransport] Stream error: ${error.message}
392
+ `);
393
+ this.currentStream = null;
394
+ this.currentFilename = null;
395
+ });
370
396
  }
371
- if (hasBody) {
372
- params.push(`body: InferContract<${contractType}>['body']`);
397
+ /**
398
+ * 현재 스트림 닫기
399
+ */
400
+ async closeStream() {
401
+ if (!this.currentStream) {
402
+ return;
403
+ }
404
+ return new Promise((resolve2, reject) => {
405
+ this.currentStream.end((error) => {
406
+ if (error) {
407
+ reject(error);
408
+ } else {
409
+ this.currentStream = null;
410
+ this.currentFilename = null;
411
+ resolve2();
412
+ }
413
+ });
414
+ });
373
415
  }
374
- if (params.length > 0) {
375
- code += `options: { ${params.join(", ")} }`;
416
+ /**
417
+ * 날짜별 로그 파일명 생성
418
+ */
419
+ getLogFilename(date) {
420
+ const year = date.getFullYear();
421
+ const month = String(date.getMonth() + 1).padStart(2, "0");
422
+ const day = String(date.getDate()).padStart(2, "0");
423
+ return `${year}-${month}-${day}.log`;
376
424
  }
377
- code += `) => `;
378
- code += `client.call('${mapping.path}', ${mapping.contractName}, `;
379
- if (params.length > 0) {
380
- code += `options`;
381
- } else {
382
- code += `{}`;
425
+ async close() {
426
+ await this.closeStream();
383
427
  }
384
- code += `),
385
- `;
386
- return code;
387
- }
388
- function generateMethodName(mapping) {
389
- const method = mapping.method.toLowerCase();
390
- if (mapping.path === "/" || mapping.path.match(/^\/[\w-]+$/)) {
391
- if (method === "get") {
392
- return "list";
393
- }
394
- if (method === "post") {
395
- return "create";
396
- }
428
+ };
429
+
430
+ // src/logger/config.ts
431
+ function getDefaultLogLevel() {
432
+ const isProduction = process.env.NODE_ENV === "production";
433
+ const isDevelopment = process.env.NODE_ENV === "development";
434
+ if (isDevelopment) {
435
+ return "debug";
397
436
  }
398
- if (mapping.path.includes(":")) {
399
- if (method === "get") {
400
- return "getById";
401
- }
402
- if (method === "put" || method === "patch") {
403
- return "update";
404
- }
405
- if (method === "delete") {
406
- return "delete";
407
- }
437
+ if (isProduction) {
438
+ return "info";
408
439
  }
409
- return method;
440
+ return "warn";
410
441
  }
411
- function generateFooter() {
412
- return `/**
413
- * Export client instance for advanced usage
414
- *
415
- * Use this to add interceptors or customize the client:
416
- *
417
- * @example
418
- * \`\`\`ts
419
- * import { client } from './api';
420
- * import { createAuthInterceptor } from '@spfn/auth/nextjs';
421
- * import { NextJSCookieProvider } from '@spfn/auth/nextjs';
422
- *
423
- * client.use(createAuthInterceptor({
424
- * cookieProvider: new NextJSCookieProvider(),
425
- * encryptionKey: process.env.ENCRYPTION_KEY!
426
- * }));
427
- * \`\`\`
428
- */
429
- export { client };
430
- `;
442
+ function getConsoleConfig() {
443
+ const isProduction = process.env.NODE_ENV === "production";
444
+ return {
445
+ level: "debug",
446
+ enabled: true,
447
+ colorize: !isProduction
448
+ // Dev: colored output, Production: plain text
449
+ };
431
450
  }
432
- function countUniqueContractFiles(mappings) {
433
- const files = /* @__PURE__ */ new Set();
434
- for (let i = 0; i < mappings.length; i++) {
435
- if (mappings[i].contractFile) {
436
- files.add(mappings[i].contractFile);
437
- }
451
+ function getFileConfig() {
452
+ const isProduction = process.env.NODE_ENV === "production";
453
+ return {
454
+ level: "info",
455
+ enabled: isProduction,
456
+ // File logging in production only
457
+ logDir: process.env.LOG_DIR || "./logs",
458
+ maxFileSize: 10 * 1024 * 1024,
459
+ // 10MB
460
+ maxFiles: 10
461
+ };
462
+ }
463
+
464
+ // src/logger/adapters/custom.ts
465
+ function initializeTransports() {
466
+ const transports = [];
467
+ const consoleConfig = getConsoleConfig();
468
+ transports.push(new ConsoleTransport(consoleConfig));
469
+ const fileConfig = getFileConfig();
470
+ if (fileConfig.enabled) {
471
+ transports.push(new FileTransport(fileConfig));
438
472
  }
439
- return files.size;
473
+ return transports;
440
474
  }
441
- var PinoAdapter = class _PinoAdapter {
475
+ var CustomAdapter = class _CustomAdapter {
442
476
  logger;
443
- constructor(config2) {
444
- const isProduction = process.env.NODE_ENV === "production";
445
- const isDevelopment = process.env.NODE_ENV === "development";
446
- const fileLoggingEnabled = process.env.LOGGER_FILE_ENABLED === "true";
447
- const targets = [];
448
- if (!isProduction && isDevelopment) {
449
- targets.push({
450
- target: "pino-pretty",
451
- level: "debug",
452
- options: {
453
- colorize: true,
454
- translateTime: "SYS:yyyy-mm-dd HH:MM:ss.l",
455
- ignore: "pid,hostname"
456
- }
457
- });
458
- }
459
- if (fileLoggingEnabled && isProduction) {
460
- const logDir = process.env.LOG_DIR || "./logs";
461
- const maxFileSize = process.env.LOG_MAX_FILE_SIZE || "10M";
462
- const maxFiles = parseInt(process.env.LOG_MAX_FILES || "10", 10);
463
- targets.push({
464
- target: "pino-roll",
465
- level: "info",
466
- options: {
467
- file: `${logDir}/app.log`,
468
- frequency: "daily",
469
- size: maxFileSize,
470
- limit: { count: maxFiles },
471
- mkdir: true
472
- }
473
- });
474
- }
475
- this.logger = pino({
476
- level: config2.level,
477
- // Transport 설정 (targets가 있으면 사용, 없으면 기본 stdout)
478
- transport: targets.length > 0 ? { targets } : void 0,
479
- // 기본 필드
480
- base: config2.module ? { module: config2.module } : void 0
477
+ constructor(config) {
478
+ this.logger = new Logger({
479
+ level: config.level,
480
+ module: config.module,
481
+ transports: initializeTransports()
481
482
  });
482
483
  }
483
484
  child(module) {
484
- const childLogger = new _PinoAdapter({ level: this.logger.level, module });
485
- childLogger.logger = this.logger.child({ module });
486
- return childLogger;
485
+ const adapter = new _CustomAdapter({ level: this.logger.level, module });
486
+ adapter.logger = this.logger.child(module);
487
+ return adapter;
487
488
  }
488
489
  debug(message, context) {
489
- this.logger.debug(context || {}, message);
490
+ this.logger.debug(message, context);
490
491
  }
491
492
  info(message, context) {
492
- this.logger.info(context || {}, message);
493
+ this.logger.info(message, context);
493
494
  }
494
495
  warn(message, errorOrContext, context) {
495
496
  if (errorOrContext instanceof Error) {
496
- this.logger.warn({ err: errorOrContext, ...context }, message);
497
+ this.logger.warn(message, errorOrContext, context);
497
498
  } else {
498
- this.logger.warn(errorOrContext || {}, message);
499
+ this.logger.warn(message, errorOrContext);
499
500
  }
500
501
  }
501
502
  error(message, errorOrContext, context) {
502
503
  if (errorOrContext instanceof Error) {
503
- this.logger.error({ err: errorOrContext, ...context }, message);
504
+ this.logger.error(message, errorOrContext, context);
504
505
  } else {
505
- this.logger.error(errorOrContext || {}, message);
506
+ this.logger.error(message, errorOrContext);
506
507
  }
507
508
  }
508
509
  fatal(message, errorOrContext, context) {
509
510
  if (errorOrContext instanceof Error) {
510
- this.logger.fatal({ err: errorOrContext, ...context }, message);
511
+ this.logger.fatal(message, errorOrContext, context);
511
512
  } else {
512
- this.logger.fatal(errorOrContext || {}, message);
513
+ this.logger.fatal(message, errorOrContext);
513
514
  }
514
515
  }
515
516
  async close() {
517
+ await this.logger.close();
516
518
  }
517
519
  };
518
520
 
519
- // src/logger/logger.ts
520
- var Logger = class _Logger {
521
- config;
522
- module;
523
- constructor(config2) {
524
- this.config = config2;
525
- this.module = config2.module;
521
+ // src/logger/adapter-factory.ts
522
+ function createAdapter(type) {
523
+ const level = getDefaultLogLevel();
524
+ switch (type) {
525
+ case "pino":
526
+ return new PinoAdapter({ level });
527
+ case "custom":
528
+ return new CustomAdapter({ level });
529
+ default:
530
+ return new PinoAdapter({ level });
526
531
  }
527
- /**
528
- * Get current log level
529
- */
530
- get level() {
531
- return this.config.level;
532
+ }
533
+ function getAdapterType() {
534
+ const adapterEnv = process.env.LOGGER_ADAPTER;
535
+ if (adapterEnv === "custom" || adapterEnv === "pino") {
536
+ return adapterEnv;
537
+ }
538
+ return "pino";
539
+ }
540
+ var logger = createAdapter(getAdapterType());
541
+
542
+ // src/env/config.ts
543
+ var ENV_FILE_PRIORITY = [
544
+ ".env",
545
+ // Base configuration (lowest priority)
546
+ ".env.{NODE_ENV}",
547
+ // Environment-specific
548
+ ".env.local",
549
+ // Local overrides
550
+ ".env.{NODE_ENV}.local"
551
+ // Local environment-specific (highest priority)
552
+ ];
553
+ var TEST_ONLY_FILES = [
554
+ ".env.test",
555
+ ".env.test.local"
556
+ ];
557
+
558
+ // src/env/loader.ts
559
+ var envLogger = logger.child("environment");
560
+ var environmentLoaded = false;
561
+ var cachedLoadResult;
562
+ function buildFileList(basePath, nodeEnv) {
563
+ const files = [];
564
+ for (const pattern of ENV_FILE_PRIORITY) {
565
+ const fileName = pattern.replace("{NODE_ENV}", nodeEnv);
566
+ if (nodeEnv !== "test" && TEST_ONLY_FILES.includes(fileName)) {
567
+ continue;
568
+ }
569
+ files.push(join(basePath, fileName));
570
+ }
571
+ return files;
572
+ }
573
+ function loadSingleFile(filePath, debug) {
574
+ if (!existsSync(filePath)) {
575
+ if (debug) {
576
+ envLogger.debug("Environment file not found (optional)", {
577
+ path: filePath
578
+ });
579
+ }
580
+ return { success: false, parsed: {}, error: "File not found" };
532
581
  }
533
- /**
534
- * Create child logger (per module)
535
- */
536
- child(module) {
537
- return new _Logger({
538
- ...this.config,
539
- module
582
+ try {
583
+ const result = config({ path: filePath });
584
+ if (result.error) {
585
+ envLogger.warn("Failed to parse environment file", {
586
+ path: filePath,
587
+ error: result.error.message
588
+ });
589
+ return {
590
+ success: false,
591
+ parsed: {},
592
+ error: result.error.message
593
+ };
594
+ }
595
+ const parsed = result.parsed || {};
596
+ if (debug) {
597
+ envLogger.debug("Environment file loaded successfully", {
598
+ path: filePath,
599
+ variables: Object.keys(parsed),
600
+ count: Object.keys(parsed).length
601
+ });
602
+ }
603
+ return { success: true, parsed };
604
+ } catch (error) {
605
+ const message = error instanceof Error ? error.message : "Unknown error";
606
+ envLogger.error("Error loading environment file", {
607
+ path: filePath,
608
+ error: message
540
609
  });
610
+ return { success: false, parsed: {}, error: message };
541
611
  }
542
- /**
543
- * Debug log
544
- */
545
- debug(message, context) {
546
- this.log("debug", message, void 0, context);
612
+ }
613
+ function validateRequiredVars(required, debug) {
614
+ const missing = [];
615
+ for (const varName of required) {
616
+ if (!process.env[varName]) {
617
+ missing.push(varName);
618
+ }
547
619
  }
548
- /**
549
- * Info log
550
- */
551
- info(message, context) {
552
- this.log("info", message, void 0, context);
620
+ if (missing.length > 0) {
621
+ const error = `Required environment variables missing: ${missing.join(", ")}`;
622
+ envLogger.error("Environment validation failed", {
623
+ missing,
624
+ required
625
+ });
626
+ throw new Error(error);
553
627
  }
554
- warn(message, errorOrContext, context) {
555
- if (errorOrContext instanceof Error) {
556
- this.log("warn", message, errorOrContext, context);
557
- } else {
558
- this.log("warn", message, void 0, errorOrContext);
559
- }
628
+ if (debug) {
629
+ envLogger.debug("Required environment variables validated", {
630
+ required,
631
+ allPresent: true
632
+ });
560
633
  }
561
- error(message, errorOrContext, context) {
562
- if (errorOrContext instanceof Error) {
563
- this.log("error", message, errorOrContext, context);
564
- } else {
565
- this.log("error", message, void 0, errorOrContext);
634
+ }
635
+ function loadEnvironment(options = {}) {
636
+ const {
637
+ basePath = process.cwd(),
638
+ customPaths = [],
639
+ debug = false,
640
+ nodeEnv = process.env.NODE_ENV || "development",
641
+ required = [],
642
+ useCache = true
643
+ } = options;
644
+ if (useCache && environmentLoaded && cachedLoadResult) {
645
+ if (debug) {
646
+ envLogger.debug("Returning cached environment", {
647
+ loaded: cachedLoadResult.loaded.length,
648
+ variables: Object.keys(cachedLoadResult.parsed).length
649
+ });
566
650
  }
651
+ return cachedLoadResult;
652
+ }
653
+ if (debug) {
654
+ envLogger.debug("Loading environment variables", {
655
+ basePath,
656
+ nodeEnv,
657
+ customPaths,
658
+ required
659
+ });
567
660
  }
568
- fatal(message, errorOrContext, context) {
569
- if (errorOrContext instanceof Error) {
570
- this.log("fatal", message, errorOrContext, context);
571
- } else {
572
- this.log("fatal", message, void 0, errorOrContext);
573
- }
661
+ const result = {
662
+ success: true,
663
+ loaded: [],
664
+ failed: [],
665
+ parsed: {},
666
+ warnings: []
667
+ };
668
+ const standardFiles = buildFileList(basePath, nodeEnv);
669
+ const allFiles = [...standardFiles, ...customPaths];
670
+ if (debug) {
671
+ envLogger.debug("Environment files to load", {
672
+ standardFiles,
673
+ customPaths,
674
+ total: allFiles.length
675
+ });
574
676
  }
575
- /**
576
- * Log processing (internal)
577
- */
578
- log(level, message, error, context) {
579
- const metadata = {
580
- timestamp: /* @__PURE__ */ new Date(),
581
- level,
582
- message,
583
- module: this.module,
584
- error,
585
- context
586
- };
587
- this.processTransports(metadata);
677
+ const reversedFiles = [...allFiles].reverse();
678
+ for (const filePath of reversedFiles) {
679
+ const fileResult = loadSingleFile(filePath, debug);
680
+ if (fileResult.success) {
681
+ result.loaded.push(filePath);
682
+ Object.assign(result.parsed, fileResult.parsed);
683
+ } else if (fileResult.error) {
684
+ result.failed.push({
685
+ path: filePath,
686
+ reason: fileResult.error
687
+ });
688
+ }
588
689
  }
589
- /**
590
- * Process Transports
591
- */
592
- processTransports(metadata) {
593
- const promises = this.config.transports.filter((transport) => transport.enabled).map((transport) => this.safeTransportLog(transport, metadata));
594
- Promise.all(promises).catch((error) => {
595
- const errorMessage = error instanceof Error ? error.message : String(error);
596
- process.stderr.write(`[Logger] Transport error: ${errorMessage}
597
- `);
690
+ if (debug || result.loaded.length > 0) {
691
+ envLogger.info("Environment loading complete", {
692
+ loaded: result.loaded.length,
693
+ failed: result.failed.length,
694
+ variables: Object.keys(result.parsed).length,
695
+ files: result.loaded
598
696
  });
599
697
  }
600
- /**
601
- * Transport log (error-safe)
602
- */
603
- async safeTransportLog(transport, metadata) {
698
+ if (required.length > 0) {
604
699
  try {
605
- await transport.log(metadata);
700
+ validateRequiredVars(required, debug);
606
701
  } catch (error) {
607
- const errorMessage = error instanceof Error ? error.message : String(error);
608
- process.stderr.write(`[Logger] Transport "${transport.name}" failed: ${errorMessage}
609
- `);
702
+ result.success = false;
703
+ result.errors = [
704
+ error instanceof Error ? error.message : "Validation failed"
705
+ ];
706
+ throw error;
610
707
  }
611
708
  }
612
- /**
613
- * Close all Transports
614
- */
615
- async close() {
616
- const closePromises = this.config.transports.filter((transport) => transport.close).map((transport) => transport.close());
617
- await Promise.all(closePromises);
618
- }
619
- };
620
-
621
- // src/logger/types.ts
622
- var LOG_LEVEL_PRIORITY = {
623
- debug: 0,
624
- info: 1,
625
- warn: 2,
626
- error: 3,
627
- fatal: 4
628
- };
709
+ environmentLoaded = true;
710
+ cachedLoadResult = result;
711
+ return result;
712
+ }
629
713
 
630
- // src/logger/formatters.ts
631
- var COLORS = {
632
- reset: "\x1B[0m",
633
- bright: "\x1B[1m",
634
- dim: "\x1B[2m",
635
- // 로그 레벨 컬러
636
- debug: "\x1B[36m",
637
- // cyan
638
- info: "\x1B[32m",
639
- // green
640
- warn: "\x1B[33m",
641
- // yellow
642
- error: "\x1B[31m",
643
- // red
644
- fatal: "\x1B[35m",
645
- // magenta
646
- // 추가 컬러
647
- gray: "\x1B[90m"
648
- };
649
- function colorizeLevel(level) {
650
- const color = COLORS[level];
651
- const levelStr = level.toUpperCase().padEnd(5);
652
- return `${color}${levelStr}${COLORS.reset}`;
714
+ // src/scripts/migrate.ts
715
+ loadEnvironment({ debug: true });
716
+ var __filename = fileURLToPath(import.meta.url);
717
+ var __dirname = dirname(__filename);
718
+ var projectRoot = join(__dirname, "../../..");
719
+ var DATABASE_URL = process.env.DATABASE_URL;
720
+ if (!DATABASE_URL) {
721
+ console.error("\u274C DATABASE_URL environment variable is required");
722
+ process.exit(1);
653
723
  }
654
- function formatTimestamp(date) {
655
- return date.toISOString();
724
+ async function runMigrations() {
725
+ console.log("\u{1F504} Starting database migration...");
726
+ console.log(`\u{1F4C2} Migrations folder: ${join(projectRoot, "drizzle")}`);
727
+ const migrationConnection = postgres(DATABASE_URL, { max: 1 });
728
+ const db = drizzle(migrationConnection);
729
+ try {
730
+ console.log("\u23F3 Applying migrations...");
731
+ await migrate(db, {
732
+ migrationsFolder: join(projectRoot, "drizzle")
733
+ });
734
+ console.log("\u2705 Migration completed successfully");
735
+ } catch (error) {
736
+ console.error("\u274C Migration failed:", error);
737
+ throw error;
738
+ } finally {
739
+ await migrationConnection.end();
740
+ console.log("\u{1F50C} Database connection closed");
741
+ }
656
742
  }
657
- function formatTimestampHuman(date) {
658
- const year = date.getFullYear();
659
- const month = String(date.getMonth() + 1).padStart(2, "0");
660
- const day = String(date.getDate()).padStart(2, "0");
661
- const hours = String(date.getHours()).padStart(2, "0");
662
- const minutes = String(date.getMinutes()).padStart(2, "0");
663
- const seconds = String(date.getSeconds()).padStart(2, "0");
664
- const ms = String(date.getMilliseconds()).padStart(3, "0");
665
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
743
+ runMigrations().then(() => {
744
+ console.log("\u{1F389} All migrations applied");
745
+ process.exit(0);
746
+ }).catch((error) => {
747
+ console.error("\u{1F4A5} Migration process failed:", error);
748
+ process.exit(1);
749
+ });
750
+ async function scanContracts(routesDir) {
751
+ const contractFiles = await scanContractFiles(routesDir);
752
+ const mappings = [];
753
+ for (let i = 0; i < contractFiles.length; i++) {
754
+ const filePath = contractFiles[i];
755
+ const exports = extractContractExports(filePath);
756
+ const basePath = getBasePathFromFile(filePath, routesDir);
757
+ for (let j = 0; j < exports.length; j++) {
758
+ const contractExport = exports[j];
759
+ const fullPath = combinePaths(basePath, contractExport.path);
760
+ mappings.push({
761
+ method: contractExport.method,
762
+ path: fullPath,
763
+ contractName: contractExport.name,
764
+ contractImportPath: getImportPathFromRoutes(filePath, routesDir),
765
+ routeFile: "",
766
+ // Not needed anymore
767
+ contractFile: filePath
768
+ });
769
+ }
770
+ }
771
+ return mappings;
666
772
  }
667
- function formatError(error) {
668
- const lines = [];
669
- lines.push(`${error.name}: ${error.message}`);
670
- if (error.stack) {
671
- const stackLines = error.stack.split("\n").slice(1);
672
- lines.push(...stackLines);
773
+ async function scanContractFiles(dir, files = []) {
774
+ try {
775
+ const entries = await readdir(dir);
776
+ for (let i = 0; i < entries.length; i++) {
777
+ const entry = entries[i];
778
+ const fullPath = join(dir, entry);
779
+ const fileStat = await stat(fullPath);
780
+ if (fileStat.isDirectory()) {
781
+ await scanContractFiles(fullPath, files);
782
+ } else if (entry === "contract.ts") {
783
+ files.push(fullPath);
784
+ }
785
+ }
786
+ } catch (error) {
787
+ }
788
+ return files;
789
+ }
790
+ function extractContractExports(filePath) {
791
+ const sourceCode = readFileSync(filePath, "utf-8");
792
+ const sourceFile = ts.createSourceFile(
793
+ filePath,
794
+ sourceCode,
795
+ ts.ScriptTarget.Latest,
796
+ true
797
+ );
798
+ const exports = [];
799
+ function visit(node) {
800
+ if (ts.isVariableStatement(node)) {
801
+ const hasExport = node.modifiers?.some(
802
+ (m) => m.kind === ts.SyntaxKind.ExportKeyword
803
+ );
804
+ if (hasExport && node.declarationList.declarations.length > 0) {
805
+ const declaration = node.declarationList.declarations[0];
806
+ if (ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name) && declaration.initializer && ts.isObjectLiteralExpression(declaration.initializer)) {
807
+ const name = declaration.name.text;
808
+ if (isContractName(name)) {
809
+ const contractData = extractContractData(declaration.initializer);
810
+ if (contractData.method && contractData.path) {
811
+ exports.push({
812
+ name,
813
+ method: contractData.method,
814
+ path: contractData.path
815
+ });
816
+ }
817
+ }
818
+ }
819
+ }
820
+ }
821
+ ts.forEachChild(node, visit);
822
+ }
823
+ visit(sourceFile);
824
+ return exports;
825
+ }
826
+ function extractContractData(objectLiteral) {
827
+ const result = {};
828
+ for (let i = 0; i < objectLiteral.properties.length; i++) {
829
+ const prop = objectLiteral.properties[i];
830
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
831
+ const propName = prop.name.text;
832
+ if (propName === "method") {
833
+ let value;
834
+ if (ts.isStringLiteral(prop.initializer)) {
835
+ value = prop.initializer.text;
836
+ } else if (ts.isAsExpression(prop.initializer) && ts.isStringLiteral(prop.initializer.expression)) {
837
+ value = prop.initializer.expression.text;
838
+ }
839
+ if (value) result.method = value;
840
+ } else if (propName === "path") {
841
+ let value;
842
+ if (ts.isStringLiteral(prop.initializer)) {
843
+ value = prop.initializer.text;
844
+ } else if (ts.isAsExpression(prop.initializer) && ts.isStringLiteral(prop.initializer.expression)) {
845
+ value = prop.initializer.expression.text;
846
+ }
847
+ if (value) result.path = value;
848
+ }
849
+ }
673
850
  }
674
- return lines.join("\n");
851
+ return result;
675
852
  }
676
- function formatContext(context) {
677
- try {
678
- return JSON.stringify(context, null, 2);
679
- } catch (error) {
680
- return "[Context serialization failed]";
681
- }
853
+ function isContractName(name) {
854
+ return name.indexOf("Contract") !== -1 || name.indexOf("contract") !== -1 || name.endsWith("Schema") || name.endsWith("schema");
682
855
  }
683
- function formatConsole(metadata, colorize = true) {
684
- const parts = [];
685
- const timestamp = formatTimestampHuman(metadata.timestamp);
686
- if (colorize) {
687
- parts.push(`${COLORS.gray}${timestamp}${COLORS.reset}`);
688
- } else {
689
- parts.push(timestamp);
856
+ function getBasePathFromFile(filePath, routesDir) {
857
+ let relativePath = filePath.replace(routesDir, "");
858
+ if (relativePath.startsWith("/")) {
859
+ relativePath = relativePath.slice(1);
690
860
  }
691
- if (colorize) {
692
- parts.push(colorizeLevel(metadata.level));
693
- } else {
694
- parts.push(metadata.level.toUpperCase().padEnd(5));
861
+ relativePath = relativePath.replace("/contract.ts", "");
862
+ if (relativePath === "index" || relativePath === "") {
863
+ return "/";
695
864
  }
696
- if (metadata.module) {
697
- if (colorize) {
698
- parts.push(`${COLORS.dim}[${metadata.module}]${COLORS.reset}`);
865
+ const segments = relativePath.split("/");
866
+ const transformed = [];
867
+ for (let i = 0; i < segments.length; i++) {
868
+ const seg = segments[i];
869
+ if (seg === "index") {
870
+ continue;
871
+ }
872
+ if (seg.startsWith("[") && seg.endsWith("]")) {
873
+ transformed.push(":" + seg.slice(1, -1));
699
874
  } else {
700
- parts.push(`[${metadata.module}]`);
875
+ transformed.push(seg);
701
876
  }
702
877
  }
703
- parts.push(metadata.message);
704
- let output = parts.join(" ");
705
- if (metadata.context && Object.keys(metadata.context).length > 0) {
706
- output += "\n" + formatContext(metadata.context);
707
- }
708
- if (metadata.error) {
709
- output += "\n" + formatError(metadata.error);
878
+ if (transformed.length === 0) {
879
+ return "/";
710
880
  }
711
- return output;
881
+ return "/" + transformed.join("/");
712
882
  }
713
- function formatJSON(metadata) {
714
- const obj = {
715
- timestamp: formatTimestamp(metadata.timestamp),
716
- level: metadata.level,
717
- message: metadata.message
718
- };
719
- if (metadata.module) {
720
- obj.module = metadata.module;
883
+ function combinePaths(basePath, contractPath) {
884
+ basePath = basePath || "/";
885
+ contractPath = contractPath || "/";
886
+ if (basePath.endsWith("/") && basePath !== "/") {
887
+ basePath = basePath.slice(0, -1);
721
888
  }
722
- if (metadata.context) {
723
- obj.context = metadata.context;
889
+ if (contractPath.startsWith("/") && contractPath !== "/") {
890
+ if (basePath === "/") {
891
+ return contractPath;
892
+ }
893
+ return basePath + contractPath;
724
894
  }
725
- if (metadata.error) {
726
- obj.error = {
727
- name: metadata.error.name,
728
- message: metadata.error.message,
729
- stack: metadata.error.stack
730
- };
895
+ if (contractPath === "/") {
896
+ return basePath;
731
897
  }
732
- return JSON.stringify(obj);
898
+ return basePath + "/" + contractPath;
733
899
  }
734
-
735
- // src/logger/transports/console.ts
736
- var ConsoleTransport = class {
737
- name = "console";
738
- level;
739
- enabled;
740
- colorize;
741
- constructor(config2) {
742
- this.level = config2.level;
743
- this.enabled = config2.enabled;
744
- this.colorize = config2.colorize ?? true;
900
+ function getImportPathFromRoutes(filePath, routesDir) {
901
+ let relativePath = filePath.replace(routesDir, "");
902
+ if (relativePath.startsWith("/")) {
903
+ relativePath = relativePath.slice(1);
745
904
  }
746
- async log(metadata) {
747
- if (!this.enabled) {
748
- return;
749
- }
750
- if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
751
- return;
752
- }
753
- const message = formatConsole(metadata, this.colorize);
754
- if (metadata.level === "warn" || metadata.level === "error" || metadata.level === "fatal") {
755
- console.error(message);
756
- } else {
757
- console.log(message);
758
- }
905
+ if (relativePath.endsWith(".ts")) {
906
+ relativePath = relativePath.slice(0, -3);
759
907
  }
760
- };
761
- var FileTransport = class {
762
- name = "file";
763
- level;
764
- enabled;
765
- logDir;
766
- currentStream = null;
767
- currentFilename = null;
768
- constructor(config2) {
769
- this.level = config2.level;
770
- this.enabled = config2.enabled;
771
- this.logDir = config2.logDir;
772
- if (!existsSync(this.logDir)) {
773
- mkdirSync(this.logDir, { recursive: true });
908
+ return "@/server/routes/" + relativePath;
909
+ }
910
+
911
+ // src/codegen/route-scanner.ts
912
+ function groupByResource(mappings) {
913
+ const grouped = {};
914
+ for (let i = 0; i < mappings.length; i++) {
915
+ const mapping = mappings[i];
916
+ const resource = extractResourceName(mapping.path);
917
+ if (!grouped[resource]) {
918
+ grouped[resource] = [];
774
919
  }
920
+ grouped[resource].push(mapping);
775
921
  }
776
- async log(metadata) {
777
- if (!this.enabled) {
778
- return;
779
- }
780
- if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
781
- return;
782
- }
783
- const message = formatJSON(metadata);
784
- const filename = this.getLogFilename(metadata.timestamp);
785
- if (this.currentFilename !== filename) {
786
- await this.rotateStream(filename);
787
- }
788
- if (this.currentStream) {
789
- return new Promise((resolve2, reject) => {
790
- this.currentStream.write(message + "\n", "utf-8", (error) => {
791
- if (error) {
792
- process.stderr.write(`[FileTransport] Failed to write log: ${error.message}
793
- `);
794
- reject(error);
795
- } else {
796
- resolve2();
797
- }
798
- });
799
- });
922
+ return grouped;
923
+ }
924
+ function extractResourceName(path) {
925
+ const segments = path.slice(1).split("/").filter((s) => s && s !== "*");
926
+ const staticSegments = [];
927
+ for (let i = 0; i < segments.length; i++) {
928
+ const seg = segments[i];
929
+ if (!seg.startsWith(":")) {
930
+ staticSegments.push(seg);
800
931
  }
801
932
  }
802
- /**
803
- * 스트림 교체 (날짜 변경 시)
804
- */
805
- async rotateStream(filename) {
806
- if (this.currentStream) {
807
- await this.closeStream();
808
- }
809
- const filepath = join(this.logDir, filename);
810
- this.currentStream = createWriteStream(filepath, {
811
- flags: "a",
812
- // append mode
813
- encoding: "utf-8"
814
- });
815
- this.currentFilename = filename;
816
- this.currentStream.on("error", (error) => {
817
- process.stderr.write(`[FileTransport] Stream error: ${error.message}
818
- `);
819
- this.currentStream = null;
820
- this.currentFilename = null;
821
- });
933
+ if (staticSegments.length === 0) {
934
+ return "root";
935
+ }
936
+ if (staticSegments.length === 1) {
937
+ return staticSegments[0];
938
+ }
939
+ const result = [staticSegments[0]];
940
+ for (let i = 1; i < staticSegments.length; i++) {
941
+ const seg = staticSegments[i];
942
+ result.push(seg.charAt(0).toUpperCase() + seg.slice(1));
943
+ }
944
+ return result.join("");
945
+ }
946
+ async function generateClient(mappings, options) {
947
+ const startTime = Date.now();
948
+ const grouped = groupByResource(mappings);
949
+ const resourceNames = Object.keys(grouped);
950
+ const code = generateClientCode(mappings, grouped, options);
951
+ await mkdir(dirname(options.outputPath), { recursive: true });
952
+ await writeFile(options.outputPath, code, "utf-8");
953
+ const stats = {
954
+ routesScanned: mappings.length,
955
+ contractsFound: mappings.length,
956
+ contractFiles: countUniqueContractFiles(mappings),
957
+ resourcesGenerated: resourceNames.length,
958
+ methodsGenerated: mappings.length,
959
+ duration: Date.now() - startTime
960
+ };
961
+ return stats;
962
+ }
963
+ function generateClientCode(mappings, grouped, options) {
964
+ let code = "";
965
+ code += generateHeader();
966
+ code += generateImports(mappings, options);
967
+ code += generateApiObject(grouped, options);
968
+ code += generateFooter();
969
+ return code;
970
+ }
971
+ function generateHeader() {
972
+ return `/**
973
+ * Auto-generated API Client
974
+ *
975
+ * Generated by @spfn/core codegen
976
+ * DO NOT EDIT MANUALLY
977
+ *
978
+ * @generated ${(/* @__PURE__ */ new Date()).toISOString()}
979
+ */
980
+
981
+ `;
982
+ }
983
+ function generateImports(mappings, options) {
984
+ let code = "";
985
+ code += `import { client } from '@spfn/core/client';
986
+ `;
987
+ if (options.includeTypes !== false) {
988
+ code += `import type { InferContract } from '@spfn/core';
989
+ `;
822
990
  }
823
- /**
824
- * 현재 스트림 닫기
825
- */
826
- async closeStream() {
827
- if (!this.currentStream) {
828
- return;
991
+ code += `
992
+ `;
993
+ const importGroups = groupContractsByImportPath(mappings);
994
+ const importPaths = Object.keys(importGroups);
995
+ for (let i = 0; i < importPaths.length; i++) {
996
+ const importPath = importPaths[i];
997
+ const contracts = importGroups[importPath];
998
+ code += `import { ${contracts.join(", ")} } from '${importPath}';
999
+ `;
1000
+ }
1001
+ code += `
1002
+ `;
1003
+ return code;
1004
+ }
1005
+ function groupContractsByImportPath(mappings) {
1006
+ const groups = {};
1007
+ for (let i = 0; i < mappings.length; i++) {
1008
+ const mapping = mappings[i];
1009
+ const path = mapping.contractImportPath;
1010
+ if (!groups[path]) {
1011
+ groups[path] = /* @__PURE__ */ new Set();
829
1012
  }
830
- return new Promise((resolve2, reject) => {
831
- this.currentStream.end((error) => {
832
- if (error) {
833
- reject(error);
834
- } else {
835
- this.currentStream = null;
836
- this.currentFilename = null;
837
- resolve2();
838
- }
839
- });
840
- });
1013
+ groups[path].add(mapping.contractName);
841
1014
  }
842
- /**
843
- * 날짜별 로그 파일명 생성
844
- */
845
- getLogFilename(date) {
846
- const year = date.getFullYear();
847
- const month = String(date.getMonth() + 1).padStart(2, "0");
848
- const day = String(date.getDate()).padStart(2, "0");
849
- return `${year}-${month}-${day}.log`;
1015
+ const result = {};
1016
+ const keys = Object.keys(groups);
1017
+ for (let i = 0; i < keys.length; i++) {
1018
+ const key = keys[i];
1019
+ result[key] = Array.from(groups[key]);
850
1020
  }
851
- async close() {
852
- await this.closeStream();
1021
+ return result;
1022
+ }
1023
+ function generateApiObject(grouped, options) {
1024
+ let code = "";
1025
+ code += `/**
1026
+ * Type-safe API client
1027
+ */
1028
+ export const api = {
1029
+ `;
1030
+ const resourceNames = Object.keys(grouped);
1031
+ for (let i = 0; i < resourceNames.length; i++) {
1032
+ const resourceName = resourceNames[i];
1033
+ const routes = grouped[resourceName];
1034
+ code += ` ${resourceName}: {
1035
+ `;
1036
+ for (let j = 0; j < routes.length; j++) {
1037
+ const route = routes[j];
1038
+ code += generateMethodCode(route, options);
1039
+ }
1040
+ code += ` }`;
1041
+ if (i < resourceNames.length - 1) {
1042
+ code += `,`;
1043
+ }
1044
+ code += `
1045
+ `;
853
1046
  }
854
- };
1047
+ code += `} as const;
855
1048
 
856
- // src/logger/config.ts
857
- function getDefaultLogLevel() {
858
- const isProduction = process.env.NODE_ENV === "production";
859
- const isDevelopment = process.env.NODE_ENV === "development";
860
- if (isDevelopment) {
861
- return "debug";
862
- }
863
- if (isProduction) {
864
- return "info";
865
- }
866
- return "warn";
867
- }
868
- function getConsoleConfig() {
869
- const isProduction = process.env.NODE_ENV === "production";
870
- return {
871
- level: "debug",
872
- enabled: true,
873
- colorize: !isProduction
874
- // Dev: colored output, Production: plain text
875
- };
876
- }
877
- function getFileConfig() {
878
- const isProduction = process.env.NODE_ENV === "production";
879
- return {
880
- level: "info",
881
- enabled: isProduction,
882
- // File logging in production only
883
- logDir: process.env.LOG_DIR || "./logs",
884
- maxFileSize: 10 * 1024 * 1024,
885
- // 10MB
886
- maxFiles: 10
887
- };
1049
+ `;
1050
+ return code;
888
1051
  }
889
-
890
- // src/logger/adapters/custom.ts
891
- function initializeTransports() {
892
- const transports = [];
893
- const consoleConfig = getConsoleConfig();
894
- transports.push(new ConsoleTransport(consoleConfig));
895
- const fileConfig = getFileConfig();
896
- if (fileConfig.enabled) {
897
- transports.push(new FileTransport(fileConfig));
1052
+ function generateMethodCode(mapping, options) {
1053
+ const methodName = generateMethodName(mapping);
1054
+ const contractType = `typeof ${mapping.contractName}`;
1055
+ const hasParams = mapping.path.includes(":");
1056
+ const hasBody = ["POST", "PUT", "PATCH"].indexOf(mapping.method) !== -1;
1057
+ let code = "";
1058
+ if (options.includeJsDoc !== false) {
1059
+ code += ` /**
1060
+ `;
1061
+ code += ` * ${mapping.method} ${mapping.path}
1062
+ `;
1063
+ code += ` */
1064
+ `;
898
1065
  }
899
- return transports;
900
- }
901
- var CustomAdapter = class _CustomAdapter {
902
- logger;
903
- constructor(config2) {
904
- this.logger = new Logger({
905
- level: config2.level,
906
- module: config2.module,
907
- transports: initializeTransports()
908
- });
1066
+ code += ` ${methodName}: (`;
1067
+ const params = [];
1068
+ if (hasParams) {
1069
+ params.push(`params: InferContract<${contractType}>['params']`);
909
1070
  }
910
- child(module) {
911
- const adapter = new _CustomAdapter({ level: this.logger.level, module });
912
- adapter.logger = this.logger.child(module);
913
- return adapter;
1071
+ if (hasBody) {
1072
+ params.push(`body: InferContract<${contractType}>['body']`);
914
1073
  }
915
- debug(message, context) {
916
- this.logger.debug(message, context);
1074
+ if (params.length > 0) {
1075
+ code += `options: { ${params.join(", ")} }`;
917
1076
  }
918
- info(message, context) {
919
- this.logger.info(message, context);
1077
+ code += `) => `;
1078
+ code += `client.call('${mapping.path}', ${mapping.contractName}, `;
1079
+ if (params.length > 0) {
1080
+ code += `options`;
1081
+ } else {
1082
+ code += `{}`;
920
1083
  }
921
- warn(message, errorOrContext, context) {
922
- if (errorOrContext instanceof Error) {
923
- this.logger.warn(message, errorOrContext, context);
924
- } else {
925
- this.logger.warn(message, errorOrContext);
1084
+ code += `),
1085
+ `;
1086
+ return code;
1087
+ }
1088
+ function generateMethodName(mapping) {
1089
+ const method = mapping.method.toLowerCase();
1090
+ if (mapping.path === "/" || mapping.path.match(/^\/[\w-]+$/)) {
1091
+ if (method === "get") {
1092
+ return "list";
926
1093
  }
927
- }
928
- error(message, errorOrContext, context) {
929
- if (errorOrContext instanceof Error) {
930
- this.logger.error(message, errorOrContext, context);
931
- } else {
932
- this.logger.error(message, errorOrContext);
1094
+ if (method === "post") {
1095
+ return "create";
933
1096
  }
934
1097
  }
935
- fatal(message, errorOrContext, context) {
936
- if (errorOrContext instanceof Error) {
937
- this.logger.fatal(message, errorOrContext, context);
938
- } else {
939
- this.logger.fatal(message, errorOrContext);
1098
+ if (mapping.path.includes(":")) {
1099
+ if (method === "get") {
1100
+ return "getById";
1101
+ }
1102
+ if (method === "put" || method === "patch") {
1103
+ return "update";
1104
+ }
1105
+ if (method === "delete") {
1106
+ return "delete";
940
1107
  }
941
1108
  }
942
- async close() {
943
- await this.logger.close();
944
- }
945
- };
946
-
947
- // src/logger/adapter-factory.ts
948
- function createAdapter(type) {
949
- const level = getDefaultLogLevel();
950
- switch (type) {
951
- case "pino":
952
- return new PinoAdapter({ level });
953
- case "custom":
954
- return new CustomAdapter({ level });
955
- default:
956
- return new PinoAdapter({ level });
957
- }
1109
+ return method;
958
1110
  }
959
- function getAdapterType() {
960
- const adapterEnv = process.env.LOGGER_ADAPTER;
961
- if (adapterEnv === "custom" || adapterEnv === "pino") {
962
- return adapterEnv;
1111
+ function generateFooter() {
1112
+ return `/**
1113
+ * Export client instance for advanced usage
1114
+ *
1115
+ * Use this to add interceptors or customize the client:
1116
+ *
1117
+ * @example
1118
+ * \`\`\`ts
1119
+ * import { client } from './api';
1120
+ * import { createAuthInterceptor } from '@spfn/auth/nextjs';
1121
+ * import { NextJSCookieProvider } from '@spfn/auth/nextjs';
1122
+ *
1123
+ * client.use(createAuthInterceptor({
1124
+ * cookieProvider: new NextJSCookieProvider(),
1125
+ * encryptionKey: process.env.ENCRYPTION_KEY!
1126
+ * }));
1127
+ * \`\`\`
1128
+ */
1129
+ export { client };
1130
+ `;
1131
+ }
1132
+ function countUniqueContractFiles(mappings) {
1133
+ const files = /* @__PURE__ */ new Set();
1134
+ for (let i = 0; i < mappings.length; i++) {
1135
+ if (mappings[i].contractFile) {
1136
+ files.add(mappings[i].contractFile);
1137
+ }
963
1138
  }
964
- return "pino";
1139
+ return files.size;
965
1140
  }
966
- var logger = createAdapter(getAdapterType());
967
-
968
- // src/codegen/watch-generate.ts
969
1141
  var codegenLogger = logger.child("codegen");
970
1142
  async function generateOnce(options) {
971
1143
  const cwd = process.cwd();