@spfn/core 0.1.0-alpha.64 → 0.1.0-alpha.68

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 (37) hide show
  1. package/README.md +5 -5
  2. package/dist/{auto-loader-CdsxOceW.d.ts → auto-loader-JFaZ9gON.d.ts} +3 -2
  3. package/dist/cache/index.d.ts +211 -0
  4. package/dist/cache/index.js +992 -0
  5. package/dist/cache/index.js.map +1 -0
  6. package/dist/client/index.d.ts +2 -2
  7. package/dist/codegen/generators/index.d.ts +7 -6
  8. package/dist/codegen/generators/index.js +208 -27
  9. package/dist/codegen/generators/index.js.map +1 -1
  10. package/dist/codegen/index.d.ts +67 -118
  11. package/dist/codegen/index.js +1419 -1295
  12. package/dist/codegen/index.js.map +1 -1
  13. package/dist/database-errors-CoPrcOpq.d.ts +86 -0
  14. package/dist/db/index.d.ts +316 -9
  15. package/dist/db/index.js +6 -6
  16. package/dist/db/index.js.map +1 -1
  17. package/dist/error-handler-wjLL3v-a.d.ts +44 -0
  18. package/dist/errors/index.d.ts +119 -0
  19. package/dist/errors/index.js +160 -0
  20. package/dist/errors/index.js.map +1 -0
  21. package/dist/index-DHiAqhKv.d.ts +101 -0
  22. package/dist/index.d.ts +2 -228
  23. package/dist/index.js +274 -292
  24. package/dist/index.js.map +1 -1
  25. package/dist/middleware/index.d.ts +33 -0
  26. package/dist/middleware/index.js +890 -0
  27. package/dist/middleware/index.js.map +1 -0
  28. package/dist/route/index.d.ts +172 -7
  29. package/dist/route/index.js +209 -70
  30. package/dist/route/index.js.map +1 -1
  31. package/dist/server/index.js +267 -176
  32. package/dist/server/index.js.map +1 -1
  33. package/dist/{types-Bd8YsFSU.d.ts → types-CAON3Mmg.d.ts} +1 -1
  34. package/package.json +19 -2
  35. package/dist/bind-CSzshBtm.d.ts +0 -17
  36. package/dist/contract-generator-CqKsfsNE.d.ts +0 -52
  37. package/dist/postgres-errors-lw1aRUFe.d.ts +0 -397
@@ -1,1503 +1,1558 @@
1
- import { existsSync, mkdirSync, accessSync, constants, writeFileSync, unlinkSync, readFileSync, createWriteStream, statSync, readdirSync, renameSync } from 'fs';
2
- import { readdir, stat, mkdir, writeFile } from 'fs/promises';
3
- import { join, relative } from 'path';
4
- import * as ts from 'typescript';
5
1
  import { watch } from 'chokidar';
6
- import pino from 'pino';
2
+ import { join, relative } from 'path';
7
3
  import mm from 'micromatch';
4
+ import pino from 'pino';
5
+ import { existsSync, mkdirSync, accessSync, constants, writeFileSync, unlinkSync, createWriteStream, statSync, readdirSync, renameSync, readFileSync } from 'fs';
6
+ import { readdir, stat, mkdir, writeFile } from 'fs/promises';
7
+ import * as ts from 'typescript';
8
8
  import { createJiti } from 'jiti';
9
9
 
10
- // src/codegen/contract-scanner.ts
11
- async function scanContracts(contractsDir) {
12
- const contractFiles = await scanContractFiles(contractsDir);
13
- const mappings = [];
14
- for (let i = 0; i < contractFiles.length; i++) {
15
- const filePath = contractFiles[i];
16
- const exports = extractContractExports(filePath);
17
- for (let j = 0; j < exports.length; j++) {
18
- const contractExport = exports[j];
19
- if (!contractExport.path.startsWith("/")) {
20
- throw new Error(
21
- `Contract '${contractExport.name}' in ${filePath} must use absolute path. Found: '${contractExport.path}'. Use '/your-path' instead.`
22
- );
23
- }
24
- mappings.push({
25
- method: contractExport.method,
26
- path: contractExport.path,
27
- contractName: contractExport.name,
28
- contractImportPath: getImportPath(filePath),
29
- routeFile: "",
30
- contractFile: filePath,
31
- hasQuery: contractExport.hasQuery,
32
- hasBody: contractExport.hasBody,
33
- hasParams: contractExport.hasParams
34
- });
35
- }
10
+ // src/codegen/core/orchestrator.ts
11
+ var PinoAdapter = class _PinoAdapter {
12
+ logger;
13
+ constructor(config) {
14
+ this.logger = pino({
15
+ level: config.level,
16
+ // 기본 필드
17
+ base: config.module ? { module: config.module } : void 0
18
+ });
36
19
  }
37
- return mappings;
38
- }
39
- async function scanContractFiles(dir, files = []) {
40
- try {
41
- const entries = await readdir(dir);
42
- for (let i = 0; i < entries.length; i++) {
43
- const entry = entries[i];
44
- const fullPath = join(dir, entry);
45
- const fileStat = await stat(fullPath);
46
- if (fileStat.isDirectory()) {
47
- await scanContractFiles(fullPath, files);
48
- } else {
49
- if ((entry.endsWith(".ts") || entry.endsWith(".js") || entry.endsWith(".mjs")) && !entry.endsWith(".d.ts") && !entry.endsWith(".test.ts") && !entry.endsWith(".test.js") && !entry.endsWith(".test.mjs")) {
50
- files.push(fullPath);
51
- }
52
- }
20
+ child(module) {
21
+ const childLogger = new _PinoAdapter({ level: this.logger.level, module });
22
+ childLogger.logger = this.logger.child({ module });
23
+ return childLogger;
24
+ }
25
+ debug(message, context) {
26
+ this.logger.debug(context || {}, message);
27
+ }
28
+ info(message, context) {
29
+ this.logger.info(context || {}, message);
30
+ }
31
+ warn(message, errorOrContext, context) {
32
+ if (errorOrContext instanceof Error) {
33
+ this.logger.warn({ err: errorOrContext, ...context }, message);
34
+ } else {
35
+ this.logger.warn(errorOrContext || {}, message);
53
36
  }
54
- } catch (error) {
55
37
  }
56
- return files;
57
- }
58
- function extractContractExports(filePath) {
59
- const sourceCode = readFileSync(filePath, "utf-8");
60
- const sourceFile = ts.createSourceFile(
61
- filePath,
62
- sourceCode,
63
- ts.ScriptTarget.Latest,
64
- true
65
- );
66
- const exports = [];
67
- function visit(node) {
68
- if (ts.isVariableStatement(node)) {
69
- const hasExport = node.modifiers?.some(
70
- (m) => m.kind === ts.SyntaxKind.ExportKeyword
71
- );
72
- if (hasExport && node.declarationList.declarations.length > 0) {
73
- const declaration = node.declarationList.declarations[0];
74
- if (ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name) && declaration.initializer) {
75
- const name = declaration.name.text;
76
- const hasSatisfiesRouteContract = checkSatisfiesRouteContract(declaration.initializer);
77
- if (hasSatisfiesRouteContract) {
78
- const objectLiteral = extractObjectLiteral(declaration.initializer);
79
- if (objectLiteral) {
80
- const contractData = extractContractData(objectLiteral);
81
- if (contractData.method && contractData.path) {
82
- exports.push({
83
- name,
84
- method: contractData.method,
85
- path: contractData.path,
86
- hasQuery: contractData.hasQuery,
87
- hasBody: contractData.hasBody,
88
- hasParams: contractData.hasParams
89
- });
90
- }
91
- }
92
- return;
93
- }
94
- if (isContractName(name)) {
95
- const objectLiteral = extractObjectLiteral(declaration.initializer);
96
- if (objectLiteral) {
97
- const contractData = extractContractData(objectLiteral);
98
- if (contractData.method && contractData.path) {
99
- exports.push({
100
- name,
101
- method: contractData.method,
102
- path: contractData.path,
103
- hasQuery: contractData.hasQuery,
104
- hasBody: contractData.hasBody,
105
- hasParams: contractData.hasParams
106
- });
107
- }
108
- }
109
- }
110
- }
111
- }
38
+ error(message, errorOrContext, context) {
39
+ if (errorOrContext instanceof Error) {
40
+ this.logger.error({ err: errorOrContext, ...context }, message);
41
+ } else {
42
+ this.logger.error(errorOrContext || {}, message);
112
43
  }
113
- ts.forEachChild(node, visit);
114
44
  }
115
- visit(sourceFile);
116
- return exports;
117
- }
118
- function checkSatisfiesRouteContract(initializer) {
119
- if (!ts.isSatisfiesExpression(initializer)) {
120
- return false;
45
+ fatal(message, errorOrContext, context) {
46
+ if (errorOrContext instanceof Error) {
47
+ this.logger.fatal({ err: errorOrContext, ...context }, message);
48
+ } else {
49
+ this.logger.fatal(errorOrContext || {}, message);
50
+ }
121
51
  }
122
- const typeNode = initializer.type;
123
- if (ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
124
- return typeNode.typeName.text === "RouteContract";
52
+ async close() {
125
53
  }
126
- return false;
54
+ };
55
+
56
+ // src/logger/types.ts
57
+ var LOG_LEVEL_PRIORITY = {
58
+ debug: 0,
59
+ info: 1,
60
+ warn: 2,
61
+ error: 3,
62
+ fatal: 4
63
+ };
64
+
65
+ // src/logger/formatters.ts
66
+ var SENSITIVE_KEYS = [
67
+ "password",
68
+ "passwd",
69
+ "pwd",
70
+ "secret",
71
+ "token",
72
+ "apikey",
73
+ "api_key",
74
+ "accesstoken",
75
+ "access_token",
76
+ "refreshtoken",
77
+ "refresh_token",
78
+ "authorization",
79
+ "auth",
80
+ "cookie",
81
+ "session",
82
+ "sessionid",
83
+ "session_id",
84
+ "privatekey",
85
+ "private_key",
86
+ "creditcard",
87
+ "credit_card",
88
+ "cardnumber",
89
+ "card_number",
90
+ "cvv",
91
+ "ssn",
92
+ "pin"
93
+ ];
94
+ var MASKED_VALUE = "***MASKED***";
95
+ function isSensitiveKey(key) {
96
+ const lowerKey = key.toLowerCase();
97
+ return SENSITIVE_KEYS.some((sensitive) => lowerKey.includes(sensitive));
127
98
  }
128
- function extractObjectLiteral(initializer) {
129
- if (ts.isObjectLiteralExpression(initializer)) {
130
- return initializer;
131
- }
132
- if (ts.isSatisfiesExpression(initializer)) {
133
- return extractObjectLiteral(initializer.expression);
99
+ function maskSensitiveData(data) {
100
+ if (data === null || data === void 0) {
101
+ return data;
134
102
  }
135
- if (ts.isAsExpression(initializer)) {
136
- return extractObjectLiteral(initializer.expression);
103
+ if (Array.isArray(data)) {
104
+ return data.map((item) => maskSensitiveData(item));
137
105
  }
138
- return void 0;
139
- }
140
- function extractContractData(objectLiteral) {
141
- const result = {};
142
- for (let i = 0; i < objectLiteral.properties.length; i++) {
143
- const prop = objectLiteral.properties[i];
144
- if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
145
- const propName = prop.name.text;
146
- if (propName === "method") {
147
- let value;
148
- if (ts.isStringLiteral(prop.initializer)) {
149
- value = prop.initializer.text;
150
- } else if (ts.isAsExpression(prop.initializer) && ts.isStringLiteral(prop.initializer.expression)) {
151
- value = prop.initializer.expression.text;
152
- }
153
- if (value) result.method = value;
154
- } else if (propName === "path") {
155
- let value;
156
- if (ts.isStringLiteral(prop.initializer)) {
157
- value = prop.initializer.text;
158
- } else if (ts.isAsExpression(prop.initializer) && ts.isStringLiteral(prop.initializer.expression)) {
159
- value = prop.initializer.expression.text;
160
- }
161
- if (value) result.path = value;
162
- } else if (propName === "query") {
163
- result.hasQuery = true;
164
- } else if (propName === "body") {
165
- result.hasBody = true;
166
- } else if (propName === "params") {
167
- result.hasParams = true;
106
+ if (typeof data === "object") {
107
+ const masked = {};
108
+ for (const [key, value] of Object.entries(data)) {
109
+ if (isSensitiveKey(key)) {
110
+ masked[key] = MASKED_VALUE;
111
+ } else if (typeof value === "object" && value !== null) {
112
+ masked[key] = maskSensitiveData(value);
113
+ } else {
114
+ masked[key] = value;
168
115
  }
169
116
  }
117
+ return masked;
170
118
  }
171
- return result;
172
- }
173
- function isContractName(name) {
174
- return name.indexOf("Contract") !== -1 || name.indexOf("contract") !== -1 || name.endsWith("Schema") || name.endsWith("schema");
119
+ return data;
175
120
  }
176
- function getImportPath(filePath) {
177
- const srcIndex = filePath.indexOf("/src/");
178
- if (srcIndex === -1) {
179
- throw new Error(`Cannot determine import path for ${filePath}: /src/ directory not found`);
180
- }
181
- let cleanPath = filePath.substring(srcIndex + 5);
182
- if (cleanPath.endsWith(".ts")) {
183
- cleanPath = cleanPath.slice(0, -3);
184
- } else if (cleanPath.endsWith(".js")) {
185
- cleanPath = cleanPath.slice(0, -3);
186
- } else if (cleanPath.endsWith(".mjs")) {
187
- cleanPath = cleanPath.slice(0, -4);
188
- }
189
- return "@/" + cleanPath;
121
+ var COLORS = {
122
+ reset: "\x1B[0m",
123
+ bright: "\x1B[1m",
124
+ dim: "\x1B[2m",
125
+ // 로그 레벨 컬러
126
+ debug: "\x1B[36m",
127
+ // cyan
128
+ info: "\x1B[32m",
129
+ // green
130
+ warn: "\x1B[33m",
131
+ // yellow
132
+ error: "\x1B[31m",
133
+ // red
134
+ fatal: "\x1B[35m",
135
+ // magenta
136
+ // 추가 컬러
137
+ gray: "\x1B[90m"
138
+ };
139
+ function formatTimestamp(date) {
140
+ return date.toISOString();
190
141
  }
191
-
192
- // src/codegen/route-scanner.ts
193
- function groupByResource(mappings) {
194
- const grouped = {};
195
- for (let i = 0; i < mappings.length; i++) {
196
- const mapping = mappings[i];
197
- const resource = extractResourceName(mapping.path);
198
- if (!grouped[resource]) {
199
- grouped[resource] = [];
200
- }
201
- grouped[resource].push(mapping);
142
+ function formatTimestampHuman(date) {
143
+ const year = date.getFullYear();
144
+ const month = String(date.getMonth() + 1).padStart(2, "0");
145
+ const day = String(date.getDate()).padStart(2, "0");
146
+ const hours = String(date.getHours()).padStart(2, "0");
147
+ const minutes = String(date.getMinutes()).padStart(2, "0");
148
+ const seconds = String(date.getSeconds()).padStart(2, "0");
149
+ const ms = String(date.getMilliseconds()).padStart(3, "0");
150
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
151
+ }
152
+ function formatError(error) {
153
+ const lines = [];
154
+ lines.push(`${error.name}: ${error.message}`);
155
+ if (error.stack) {
156
+ const stackLines = error.stack.split("\n").slice(1);
157
+ lines.push(...stackLines);
202
158
  }
203
- return grouped;
159
+ return lines.join("\n");
204
160
  }
205
- function extractResourceName(path) {
206
- const segments = path.slice(1).split("/").filter((s) => s && s !== "*");
207
- const staticSegments = [];
208
- for (let i = 0; i < segments.length; i++) {
209
- const seg = segments[i];
210
- if (!seg.startsWith(":")) {
211
- staticSegments.push(seg);
212
- }
161
+ function formatConsole(metadata, colorize = true) {
162
+ const parts = [];
163
+ const timestamp = formatTimestampHuman(metadata.timestamp);
164
+ if (colorize) {
165
+ parts.push(`${COLORS.gray}[${timestamp}]${COLORS.reset}`);
166
+ } else {
167
+ parts.push(`[${timestamp}]`);
213
168
  }
214
- if (staticSegments.length === 0) {
215
- return "root";
169
+ if (metadata.module) {
170
+ if (colorize) {
171
+ parts.push(`${COLORS.dim}[module=${metadata.module}]${COLORS.reset}`);
172
+ } else {
173
+ parts.push(`[module=${metadata.module}]`);
174
+ }
216
175
  }
217
- const first = toCamelCase(staticSegments[0], false);
218
- if (staticSegments.length === 1) {
219
- return first;
176
+ if (metadata.context && Object.keys(metadata.context).length > 0) {
177
+ Object.entries(metadata.context).forEach(([key, value]) => {
178
+ const valueStr = typeof value === "string" ? value : String(value);
179
+ if (colorize) {
180
+ parts.push(`${COLORS.dim}[${key}=${valueStr}]${COLORS.reset}`);
181
+ } else {
182
+ parts.push(`[${key}=${valueStr}]`);
183
+ }
184
+ });
220
185
  }
221
- const result = [first];
222
- for (let i = 1; i < staticSegments.length; i++) {
223
- const seg = staticSegments[i];
224
- result.push(toCamelCase(seg, true));
186
+ const levelStr = metadata.level.toUpperCase();
187
+ if (colorize) {
188
+ const color = COLORS[metadata.level];
189
+ parts.push(`${color}(${levelStr})${COLORS.reset}:`);
190
+ } else {
191
+ parts.push(`(${levelStr}):`);
225
192
  }
226
- return result.join("");
227
- }
228
- function toCamelCase(str, capitalize2) {
229
- const parts = str.split(/[-_]/);
230
- if (parts.length === 1) {
231
- return capitalize2 ? str.charAt(0).toUpperCase() + str.slice(1) : str;
193
+ if (colorize) {
194
+ parts.push(`${COLORS.bright}${metadata.message}${COLORS.reset}`);
195
+ } else {
196
+ parts.push(metadata.message);
232
197
  }
233
- const result = [];
234
- for (let i = 0; i < parts.length; i++) {
235
- const part = parts[i];
236
- if (i === 0 && !capitalize2) {
237
- result.push(part);
238
- } else {
239
- result.push(part.charAt(0).toUpperCase() + part.slice(1));
240
- }
198
+ let output = parts.join(" ");
199
+ if (metadata.error) {
200
+ output += "\n" + formatError(metadata.error);
241
201
  }
242
- return result.join("");
202
+ return output;
243
203
  }
244
- async function generateClient(mappings, options) {
245
- const startTime = Date.now();
246
- const grouped = groupByResource(mappings);
247
- const resourceNames = Object.keys(grouped);
248
- await generateSplitClient(mappings, grouped, options);
249
- return {
250
- routesScanned: mappings.length,
251
- contractsFound: mappings.length,
252
- contractFiles: countUniqueContractFiles(mappings),
253
- resourcesGenerated: resourceNames.length,
254
- methodsGenerated: mappings.length,
255
- duration: Date.now() - startTime
204
+ function formatJSON(metadata) {
205
+ const obj = {
206
+ timestamp: formatTimestamp(metadata.timestamp),
207
+ level: metadata.level,
208
+ message: metadata.message
256
209
  };
257
- }
258
- function generateHeader() {
259
- return `/**
260
- * Auto-generated API Client
261
- *
262
- * Generated by @spfn/core codegen
263
- * DO NOT EDIT MANUALLY
264
- *
265
- * @generated ${(/* @__PURE__ */ new Date()).toISOString()}
266
- */
267
-
268
- `;
269
- }
270
- function groupContractsByImportPath(mappings) {
271
- const groups = {};
272
- for (let i = 0; i < mappings.length; i++) {
273
- const mapping = mappings[i];
274
- const path = mapping.contractImportPath;
275
- if (!groups[path]) {
276
- groups[path] = /* @__PURE__ */ new Set();
277
- }
278
- groups[path].add(mapping.contractName);
279
- }
280
- const result = {};
281
- const keys = Object.keys(groups);
282
- for (let i = 0; i < keys.length; i++) {
283
- const key = keys[i];
284
- result[key] = Array.from(groups[key]);
210
+ if (metadata.module) {
211
+ obj.module = metadata.module;
285
212
  }
286
- return result;
287
- }
288
- function generateTypeName(mapping) {
289
- let name = mapping.contractName;
290
- if (name.endsWith("Contract")) {
291
- name = name.slice(0, -8);
213
+ if (metadata.context) {
214
+ obj.context = metadata.context;
292
215
  }
293
- if (name.length > 0) {
294
- name = name.charAt(0).toUpperCase() + name.slice(1);
216
+ if (metadata.error) {
217
+ obj.error = {
218
+ name: metadata.error.name,
219
+ message: metadata.error.message,
220
+ stack: metadata.error.stack
221
+ };
295
222
  }
296
- return name;
223
+ return JSON.stringify(obj);
297
224
  }
298
- function generateMethodCode(mapping, options) {
299
- const methodName = generateMethodName(mapping);
300
- const hasParams = mapping.hasParams || mapping.path.includes(":");
301
- const hasQuery = mapping.hasQuery || false;
302
- const hasBody = mapping.hasBody || false;
303
- let code = "";
304
- if (options.includeJsDoc !== false) {
305
- code += ` /**
306
- `;
307
- code += ` * ${mapping.method} ${mapping.path}
308
- `;
309
- code += ` */
310
- `;
311
- }
312
- code += ` ${methodName}: (`;
313
- const params = [];
314
- const typeName = generateTypeName(mapping);
315
- if (hasParams) {
316
- params.push(`params: ${typeName}Params`);
225
+
226
+ // src/logger/logger.ts
227
+ var Logger = class _Logger {
228
+ config;
229
+ module;
230
+ constructor(config) {
231
+ this.config = config;
232
+ this.module = config.module;
317
233
  }
318
- if (hasQuery) {
319
- params.push(`query?: ${typeName}Query`);
234
+ /**
235
+ * Get current log level
236
+ */
237
+ get level() {
238
+ return this.config.level;
320
239
  }
321
- if (hasBody) {
322
- params.push(`body: ${typeName}Body`);
240
+ /**
241
+ * Create child logger (per module)
242
+ */
243
+ child(module) {
244
+ return new _Logger({
245
+ ...this.config,
246
+ module
247
+ });
323
248
  }
324
- if (params.length > 0) {
325
- code += `options: { ${params.join(", ")} }`;
249
+ /**
250
+ * Debug log
251
+ */
252
+ debug(message, context) {
253
+ this.log("debug", message, void 0, context);
326
254
  }
327
- code += `) => `;
328
- code += `client.call(${mapping.contractName}`;
329
- if (params.length > 0) {
330
- code += `, options`;
255
+ /**
256
+ * Info log
257
+ */
258
+ info(message, context) {
259
+ this.log("info", message, void 0, context);
331
260
  }
332
- code += `),
333
- `;
334
- return code;
335
- }
336
- function generateMethodName(mapping) {
337
- const method = mapping.method.toLowerCase();
338
- if (mapping.path === "/" || mapping.path.match(/^\/[\w-]+$/)) {
339
- if (method === "get") {
340
- return "list";
341
- }
342
- if (method === "post") {
343
- return "create";
261
+ warn(message, errorOrContext, context) {
262
+ if (errorOrContext instanceof Error) {
263
+ this.log("warn", message, errorOrContext, context);
264
+ } else {
265
+ this.log("warn", message, void 0, errorOrContext);
344
266
  }
345
267
  }
346
- if (mapping.path.includes(":")) {
347
- if (method === "get") {
348
- return "getById";
349
- }
350
- if (method === "put" || method === "patch") {
351
- return "update";
352
- }
353
- if (method === "delete") {
354
- return "delete";
268
+ error(message, errorOrContext, context) {
269
+ if (errorOrContext instanceof Error) {
270
+ this.log("error", message, errorOrContext, context);
271
+ } else {
272
+ this.log("error", message, void 0, errorOrContext);
355
273
  }
356
274
  }
357
- return method;
358
- }
359
- function countUniqueContractFiles(mappings) {
360
- const files = /* @__PURE__ */ new Set();
361
- for (let i = 0; i < mappings.length; i++) {
362
- if (mappings[i].contractFile) {
363
- files.add(mappings[i].contractFile);
275
+ fatal(message, errorOrContext, context) {
276
+ if (errorOrContext instanceof Error) {
277
+ this.log("fatal", message, errorOrContext, context);
278
+ } else {
279
+ this.log("fatal", message, void 0, errorOrContext);
364
280
  }
365
281
  }
366
- return files.size;
367
- }
368
- async function generateSplitClient(_mappings, grouped, options) {
369
- const outputPath = options.outputPath;
370
- const outputDir = outputPath.endsWith(".ts") || outputPath.endsWith(".js") ? outputPath.replace(/\.[jt]s$/, "") : outputPath;
371
- await mkdir(outputDir, { recursive: true });
372
- const resourceNames = Object.keys(grouped);
373
- for (let i = 0; i < resourceNames.length; i++) {
374
- const resourceName = resourceNames[i];
375
- const routes = grouped[resourceName];
376
- const code = generateResourceFile(resourceName, routes, options);
377
- const filePath = `${outputDir}/${resourceName}.ts`;
378
- await writeFile(filePath, code, "utf-8");
379
- }
380
- const indexCode = generateIndexFile(resourceNames);
381
- const indexPath = `${outputDir}/index.ts`;
382
- await writeFile(indexPath, indexCode, "utf-8");
383
- }
384
- function generateResourceFile(resourceName, routes, options) {
385
- let code = "";
386
- code += generateHeader();
387
- code += `import { client } from '@spfn/core/client';
388
- `;
389
- if (options.includeTypes !== false) {
390
- code += `import type { InferContract } from '@spfn/core';
391
- `;
282
+ /**
283
+ * Log processing (internal)
284
+ */
285
+ log(level, message, error, context) {
286
+ if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[this.config.level]) {
287
+ return;
288
+ }
289
+ const metadata = {
290
+ timestamp: /* @__PURE__ */ new Date(),
291
+ level,
292
+ message,
293
+ module: this.module,
294
+ error,
295
+ // Mask sensitive information in context to prevent credential leaks
296
+ context: context ? maskSensitiveData(context) : void 0
297
+ };
298
+ this.processTransports(metadata);
392
299
  }
393
- code += `
394
- `;
395
- const importGroups = groupContractsByImportPath(routes);
396
- const importPaths = Object.keys(importGroups);
397
- for (let i = 0; i < importPaths.length; i++) {
398
- const importPath = importPaths[i];
399
- const contracts = importGroups[importPath];
400
- code += `import { ${contracts.join(", ")} } from '${importPath}';
401
- `;
300
+ /**
301
+ * Process Transports
302
+ */
303
+ processTransports(metadata) {
304
+ const promises = this.config.transports.filter((transport) => transport.enabled).map((transport) => this.safeTransportLog(transport, metadata));
305
+ Promise.all(promises).catch((error) => {
306
+ const errorMessage = error instanceof Error ? error.message : String(error);
307
+ process.stderr.write(`[Logger] Transport error: ${errorMessage}
308
+ `);
309
+ });
402
310
  }
403
- code += `
404
- `;
405
- if (options.includeTypes !== false) {
406
- code += `// ============================================
407
- `;
408
- code += `// Types
409
- `;
410
- code += `// ============================================
411
-
412
- `;
413
- for (let i = 0; i < routes.length; i++) {
414
- const route = routes[i];
415
- const typeName = generateTypeName(route);
416
- const contractType = `typeof ${route.contractName}`;
417
- code += `export type ${typeName}Response = InferContract<${contractType}>['response'];
418
- `;
419
- if (route.hasQuery) {
420
- code += `export type ${typeName}Query = InferContract<${contractType}>['query'];
421
- `;
422
- }
423
- if (route.hasParams || route.path.includes(":")) {
424
- code += `export type ${typeName}Params = InferContract<${contractType}>['params'];
425
- `;
426
- }
427
- if (route.hasBody) {
428
- code += `export type ${typeName}Body = InferContract<${contractType}>['body'];
429
- `;
430
- }
431
- code += `
432
- `;
311
+ /**
312
+ * Transport log (error-safe)
313
+ */
314
+ async safeTransportLog(transport, metadata) {
315
+ try {
316
+ await transport.log(metadata);
317
+ } catch (error) {
318
+ const errorMessage = error instanceof Error ? error.message : String(error);
319
+ process.stderr.write(`[Logger] Transport "${transport.name}" failed: ${errorMessage}
320
+ `);
433
321
  }
434
322
  }
435
- code += `/**
436
- `;
437
- code += ` * ${resourceName} API
438
- `;
439
- code += ` */
440
- `;
441
- code += `export const ${resourceName} = {
442
- `;
443
- for (let i = 0; i < routes.length; i++) {
444
- const route = routes[i];
445
- code += generateMethodCode(route, options);
323
+ /**
324
+ * Close all Transports
325
+ */
326
+ async close() {
327
+ const closePromises = this.config.transports.filter((transport) => transport.close).map((transport) => transport.close());
328
+ await Promise.all(closePromises);
446
329
  }
447
- code += `} as const;
448
- `;
449
- return code;
450
- }
451
- function generateIndexFile(resourceNames, _options) {
452
- let code = "";
453
- code += generateHeader();
454
- code += `export { client } from '@spfn/core/client';
330
+ };
455
331
 
456
- `;
457
- for (let i = 0; i < resourceNames.length; i++) {
458
- const resourceName = resourceNames[i];
459
- code += `export * from './${resourceName}.js';
460
- `;
461
- }
462
- code += `
463
- `;
464
- for (let i = 0; i < resourceNames.length; i++) {
465
- const resourceName = resourceNames[i];
466
- code += `import { ${resourceName} } from './${resourceName}.js';
467
- `;
468
- }
469
- code += `
470
- `;
471
- code += `/**
472
- `;
473
- code += ` * Type-safe API client
474
- `;
475
- code += ` */
476
- `;
477
- code += `export const api = {
478
- `;
479
- for (let i = 0; i < resourceNames.length; i++) {
480
- const resourceName = resourceNames[i];
481
- code += ` ${resourceName}`;
482
- if (i < resourceNames.length - 1) {
483
- code += `,`;
484
- }
485
- code += `
486
- `;
487
- }
488
- code += `} as const;
489
- `;
490
- return code;
491
- }
492
- var PinoAdapter = class _PinoAdapter {
493
- logger;
332
+ // src/logger/transports/console.ts
333
+ var ConsoleTransport = class {
334
+ name = "console";
335
+ level;
336
+ enabled;
337
+ colorize;
494
338
  constructor(config) {
495
- this.logger = pino({
496
- level: config.level,
497
- // 기본 필드
498
- base: config.module ? { module: config.module } : void 0
499
- });
500
- }
501
- child(module) {
502
- const childLogger = new _PinoAdapter({ level: this.logger.level, module });
503
- childLogger.logger = this.logger.child({ module });
504
- return childLogger;
505
- }
506
- debug(message, context) {
507
- this.logger.debug(context || {}, message);
508
- }
509
- info(message, context) {
510
- this.logger.info(context || {}, message);
339
+ this.level = config.level;
340
+ this.enabled = config.enabled;
341
+ this.colorize = config.colorize ?? true;
511
342
  }
512
- warn(message, errorOrContext, context) {
513
- if (errorOrContext instanceof Error) {
514
- this.logger.warn({ err: errorOrContext, ...context }, message);
515
- } else {
516
- this.logger.warn(errorOrContext || {}, message);
343
+ async log(metadata) {
344
+ if (!this.enabled) {
345
+ return;
517
346
  }
518
- }
519
- error(message, errorOrContext, context) {
520
- if (errorOrContext instanceof Error) {
521
- this.logger.error({ err: errorOrContext, ...context }, message);
522
- } else {
523
- this.logger.error(errorOrContext || {}, message);
347
+ if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
348
+ return;
524
349
  }
525
- }
526
- fatal(message, errorOrContext, context) {
527
- if (errorOrContext instanceof Error) {
528
- this.logger.fatal({ err: errorOrContext, ...context }, message);
350
+ const message = formatConsole(metadata, this.colorize);
351
+ if (metadata.level === "warn" || metadata.level === "error" || metadata.level === "fatal") {
352
+ console.error(message);
529
353
  } else {
530
- this.logger.fatal(errorOrContext || {}, message);
354
+ console.log(message);
531
355
  }
532
356
  }
533
- async close() {
534
- }
535
357
  };
536
-
537
- // src/logger/types.ts
538
- var LOG_LEVEL_PRIORITY = {
539
- debug: 0,
540
- info: 1,
541
- warn: 2,
542
- error: 3,
543
- fatal: 4
544
- };
545
-
546
- // src/logger/formatters.ts
547
- var SENSITIVE_KEYS = [
548
- "password",
549
- "passwd",
550
- "pwd",
551
- "secret",
552
- "token",
553
- "apikey",
554
- "api_key",
555
- "accesstoken",
556
- "access_token",
557
- "refreshtoken",
558
- "refresh_token",
559
- "authorization",
560
- "auth",
561
- "cookie",
562
- "session",
563
- "sessionid",
564
- "session_id",
565
- "privatekey",
566
- "private_key",
567
- "creditcard",
568
- "credit_card",
569
- "cardnumber",
570
- "card_number",
571
- "cvv",
572
- "ssn",
573
- "pin"
574
- ];
575
- var MASKED_VALUE = "***MASKED***";
576
- function isSensitiveKey(key) {
577
- const lowerKey = key.toLowerCase();
578
- return SENSITIVE_KEYS.some((sensitive) => lowerKey.includes(sensitive));
579
- }
580
- function maskSensitiveData(data) {
581
- if (data === null || data === void 0) {
582
- return data;
358
+ var FileTransport = class {
359
+ name = "file";
360
+ level;
361
+ enabled;
362
+ logDir;
363
+ maxFileSize;
364
+ maxFiles;
365
+ currentStream = null;
366
+ currentFilename = null;
367
+ constructor(config) {
368
+ this.level = config.level;
369
+ this.enabled = config.enabled;
370
+ this.logDir = config.logDir;
371
+ this.maxFileSize = config.maxFileSize ?? 10 * 1024 * 1024;
372
+ this.maxFiles = config.maxFiles ?? 10;
373
+ if (!existsSync(this.logDir)) {
374
+ mkdirSync(this.logDir, { recursive: true });
375
+ }
583
376
  }
584
- if (Array.isArray(data)) {
585
- return data.map((item) => maskSensitiveData(item));
377
+ async log(metadata) {
378
+ if (!this.enabled) {
379
+ return;
380
+ }
381
+ if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
382
+ return;
383
+ }
384
+ const message = formatJSON(metadata);
385
+ const filename = this.getLogFilename(metadata.timestamp);
386
+ if (this.currentFilename !== filename) {
387
+ await this.rotateStream(filename);
388
+ await this.cleanOldFiles();
389
+ } else if (this.currentFilename) {
390
+ await this.checkAndRotateBySize();
391
+ }
392
+ if (this.currentStream) {
393
+ return new Promise((resolve, reject) => {
394
+ this.currentStream.write(message + "\n", "utf-8", (error) => {
395
+ if (error) {
396
+ process.stderr.write(`[FileTransport] Failed to write log: ${error.message}
397
+ `);
398
+ reject(error);
399
+ } else {
400
+ resolve();
401
+ }
402
+ });
403
+ });
404
+ }
586
405
  }
587
- if (typeof data === "object") {
588
- const masked = {};
589
- for (const [key, value] of Object.entries(data)) {
590
- if (isSensitiveKey(key)) {
591
- masked[key] = MASKED_VALUE;
592
- } else if (typeof value === "object" && value !== null) {
593
- masked[key] = maskSensitiveData(value);
594
- } else {
595
- masked[key] = value;
406
+ /**
407
+ * 스트림 교체 (날짜 변경 시)
408
+ */
409
+ async rotateStream(filename) {
410
+ if (this.currentStream) {
411
+ await this.closeStream();
412
+ }
413
+ const filepath = join(this.logDir, filename);
414
+ this.currentStream = createWriteStream(filepath, {
415
+ flags: "a",
416
+ // append mode
417
+ encoding: "utf-8"
418
+ });
419
+ this.currentFilename = filename;
420
+ this.currentStream.on("error", (error) => {
421
+ process.stderr.write(`[FileTransport] Stream error: ${error.message}
422
+ `);
423
+ this.currentStream = null;
424
+ this.currentFilename = null;
425
+ });
426
+ }
427
+ /**
428
+ * 현재 스트림 닫기
429
+ */
430
+ async closeStream() {
431
+ if (!this.currentStream) {
432
+ return;
433
+ }
434
+ return new Promise((resolve, reject) => {
435
+ this.currentStream.end((error) => {
436
+ if (error) {
437
+ reject(error);
438
+ } else {
439
+ this.currentStream = null;
440
+ this.currentFilename = null;
441
+ resolve();
442
+ }
443
+ });
444
+ });
445
+ }
446
+ /**
447
+ * 파일 크기 체크 및 크기 기반 로테이션
448
+ */
449
+ async checkAndRotateBySize() {
450
+ if (!this.currentFilename) {
451
+ return;
452
+ }
453
+ const filepath = join(this.logDir, this.currentFilename);
454
+ if (!existsSync(filepath)) {
455
+ return;
456
+ }
457
+ try {
458
+ const stats = statSync(filepath);
459
+ if (stats.size >= this.maxFileSize) {
460
+ await this.rotateBySize();
596
461
  }
462
+ } catch (error) {
463
+ const errorMessage = error instanceof Error ? error.message : String(error);
464
+ process.stderr.write(`[FileTransport] Failed to check file size: ${errorMessage}
465
+ `);
597
466
  }
598
- return masked;
599
467
  }
600
- return data;
601
- }
602
- var COLORS = {
603
- reset: "\x1B[0m",
604
- bright: "\x1B[1m",
605
- dim: "\x1B[2m",
606
- // 로그 레벨 컬러
607
- debug: "\x1B[36m",
608
- // cyan
609
- info: "\x1B[32m",
610
- // green
611
- warn: "\x1B[33m",
612
- // yellow
613
- error: "\x1B[31m",
614
- // red
615
- fatal: "\x1B[35m",
616
- // magenta
617
- // 추가 컬러
618
- gray: "\x1B[90m"
468
+ /**
469
+ * 크기 기반 로테이션 수행
470
+ * 예: 2025-01-01.log -> 2025-01-01.1.log, 2025-01-01.1.log -> 2025-01-01.2.log
471
+ */
472
+ async rotateBySize() {
473
+ if (!this.currentFilename) {
474
+ return;
475
+ }
476
+ await this.closeStream();
477
+ const baseName = this.currentFilename.replace(/\.log$/, "");
478
+ const files = readdirSync(this.logDir);
479
+ const relatedFiles = files.filter((file) => file.startsWith(baseName) && file.endsWith(".log")).sort().reverse();
480
+ for (const file of relatedFiles) {
481
+ const match = file.match(/\.(\d+)\.log$/);
482
+ if (match) {
483
+ const oldNum = parseInt(match[1], 10);
484
+ const newNum = oldNum + 1;
485
+ const oldPath = join(this.logDir, file);
486
+ const newPath2 = join(this.logDir, `${baseName}.${newNum}.log`);
487
+ try {
488
+ renameSync(oldPath, newPath2);
489
+ } catch (error) {
490
+ const errorMessage = error instanceof Error ? error.message : String(error);
491
+ process.stderr.write(`[FileTransport] Failed to rotate file: ${errorMessage}
492
+ `);
493
+ }
494
+ }
495
+ }
496
+ const currentPath = join(this.logDir, this.currentFilename);
497
+ const newPath = join(this.logDir, `${baseName}.1.log`);
498
+ try {
499
+ if (existsSync(currentPath)) {
500
+ renameSync(currentPath, newPath);
501
+ }
502
+ } catch (error) {
503
+ const errorMessage = error instanceof Error ? error.message : String(error);
504
+ process.stderr.write(`[FileTransport] Failed to rotate current file: ${errorMessage}
505
+ `);
506
+ }
507
+ await this.rotateStream(this.currentFilename);
508
+ }
509
+ /**
510
+ * 오래된 로그 파일 정리
511
+ * maxFiles 개수를 초과하는 로그 파일 삭제
512
+ */
513
+ async cleanOldFiles() {
514
+ try {
515
+ if (!existsSync(this.logDir)) {
516
+ return;
517
+ }
518
+ const files = readdirSync(this.logDir);
519
+ const logFiles = files.filter((file) => file.endsWith(".log")).map((file) => {
520
+ const filepath = join(this.logDir, file);
521
+ const stats = statSync(filepath);
522
+ return { file, mtime: stats.mtime };
523
+ }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
524
+ if (logFiles.length > this.maxFiles) {
525
+ const filesToDelete = logFiles.slice(this.maxFiles);
526
+ for (const { file } of filesToDelete) {
527
+ const filepath = join(this.logDir, file);
528
+ try {
529
+ unlinkSync(filepath);
530
+ } catch (error) {
531
+ const errorMessage = error instanceof Error ? error.message : String(error);
532
+ process.stderr.write(`[FileTransport] Failed to delete old file "${file}": ${errorMessage}
533
+ `);
534
+ }
535
+ }
536
+ }
537
+ } catch (error) {
538
+ const errorMessage = error instanceof Error ? error.message : String(error);
539
+ process.stderr.write(`[FileTransport] Failed to clean old files: ${errorMessage}
540
+ `);
541
+ }
542
+ }
543
+ /**
544
+ * 날짜별 로그 파일명 생성
545
+ */
546
+ getLogFilename(date) {
547
+ const year = date.getFullYear();
548
+ const month = String(date.getMonth() + 1).padStart(2, "0");
549
+ const day = String(date.getDate()).padStart(2, "0");
550
+ return `${year}-${month}-${day}.log`;
551
+ }
552
+ async close() {
553
+ await this.closeStream();
554
+ }
619
555
  };
620
- function formatTimestamp(date) {
621
- return date.toISOString();
556
+ function isFileLoggingEnabled() {
557
+ return process.env.LOGGER_FILE_ENABLED === "true";
622
558
  }
623
- function formatTimestampHuman(date) {
624
- const year = date.getFullYear();
625
- const month = String(date.getMonth() + 1).padStart(2, "0");
626
- const day = String(date.getDate()).padStart(2, "0");
627
- const hours = String(date.getHours()).padStart(2, "0");
628
- const minutes = String(date.getMinutes()).padStart(2, "0");
629
- const seconds = String(date.getSeconds()).padStart(2, "0");
630
- const ms = String(date.getMilliseconds()).padStart(3, "0");
631
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
559
+ function getDefaultLogLevel() {
560
+ const isProduction = process.env.NODE_ENV === "production";
561
+ const isDevelopment = process.env.NODE_ENV === "development";
562
+ if (isDevelopment) {
563
+ return "debug";
564
+ }
565
+ if (isProduction) {
566
+ return "info";
567
+ }
568
+ return "warn";
632
569
  }
633
- function formatError(error) {
634
- const lines = [];
635
- lines.push(`${error.name}: ${error.message}`);
636
- if (error.stack) {
637
- const stackLines = error.stack.split("\n").slice(1);
638
- lines.push(...stackLines);
570
+ function getConsoleConfig() {
571
+ const isProduction = process.env.NODE_ENV === "production";
572
+ return {
573
+ level: "debug",
574
+ enabled: true,
575
+ colorize: !isProduction
576
+ // Dev: colored output, Production: plain text
577
+ };
578
+ }
579
+ function getFileConfig() {
580
+ const isProduction = process.env.NODE_ENV === "production";
581
+ return {
582
+ level: "info",
583
+ enabled: isProduction,
584
+ // File logging in production only
585
+ logDir: process.env.LOG_DIR || "./logs",
586
+ maxFileSize: 10 * 1024 * 1024,
587
+ // 10MB
588
+ maxFiles: 10
589
+ };
590
+ }
591
+ function validateDirectoryWritable(dirPath) {
592
+ if (!existsSync(dirPath)) {
593
+ try {
594
+ mkdirSync(dirPath, { recursive: true });
595
+ } catch (error) {
596
+ const errorMessage = error instanceof Error ? error.message : String(error);
597
+ throw new Error(`Failed to create log directory "${dirPath}": ${errorMessage}`);
598
+ }
599
+ }
600
+ try {
601
+ accessSync(dirPath, constants.W_OK);
602
+ } catch {
603
+ throw new Error(`Log directory "${dirPath}" is not writable. Please check permissions.`);
604
+ }
605
+ const testFile = join(dirPath, ".logger-write-test");
606
+ try {
607
+ writeFileSync(testFile, "test", "utf-8");
608
+ unlinkSync(testFile);
609
+ } catch (error) {
610
+ const errorMessage = error instanceof Error ? error.message : String(error);
611
+ throw new Error(`Cannot write to log directory "${dirPath}": ${errorMessage}`);
639
612
  }
640
- return lines.join("\n");
641
613
  }
642
- function formatConsole(metadata, colorize = true) {
643
- const parts = [];
644
- const timestamp = formatTimestampHuman(metadata.timestamp);
645
- if (colorize) {
646
- parts.push(`${COLORS.gray}[${timestamp}]${COLORS.reset}`);
647
- } else {
648
- parts.push(`[${timestamp}]`);
614
+ function validateFileConfig() {
615
+ if (!isFileLoggingEnabled()) {
616
+ return;
649
617
  }
650
- if (metadata.module) {
651
- if (colorize) {
652
- parts.push(`${COLORS.dim}[module=${metadata.module}]${COLORS.reset}`);
653
- } else {
654
- parts.push(`[module=${metadata.module}]`);
655
- }
618
+ const logDir = process.env.LOG_DIR;
619
+ if (!logDir) {
620
+ throw new Error(
621
+ "LOG_DIR environment variable is required when LOGGER_FILE_ENABLED=true. Example: LOG_DIR=/var/log/myapp"
622
+ );
656
623
  }
657
- if (metadata.context && Object.keys(metadata.context).length > 0) {
658
- Object.entries(metadata.context).forEach(([key, value]) => {
659
- const valueStr = typeof value === "string" ? value : String(value);
660
- if (colorize) {
661
- parts.push(`${COLORS.dim}[${key}=${valueStr}]${COLORS.reset}`);
662
- } else {
663
- parts.push(`[${key}=${valueStr}]`);
664
- }
665
- });
624
+ validateDirectoryWritable(logDir);
625
+ }
626
+ function validateSlackConfig() {
627
+ const webhookUrl = process.env.SLACK_WEBHOOK_URL;
628
+ if (!webhookUrl) {
629
+ return;
666
630
  }
667
- const levelStr = metadata.level.toUpperCase();
668
- if (colorize) {
669
- const color = COLORS[metadata.level];
670
- parts.push(`${color}(${levelStr})${COLORS.reset}:`);
671
- } else {
672
- parts.push(`(${levelStr}):`);
631
+ if (!webhookUrl.startsWith("https://hooks.slack.com/")) {
632
+ throw new Error(
633
+ `Invalid SLACK_WEBHOOK_URL: "${webhookUrl}". Slack webhook URLs must start with "https://hooks.slack.com/"`
634
+ );
635
+ }
636
+ }
637
+ function validateEmailConfig() {
638
+ const smtpHost = process.env.SMTP_HOST;
639
+ const smtpPort = process.env.SMTP_PORT;
640
+ const emailFrom = process.env.EMAIL_FROM;
641
+ const emailTo = process.env.EMAIL_TO;
642
+ const hasAnyEmailConfig = smtpHost || smtpPort || emailFrom || emailTo;
643
+ if (!hasAnyEmailConfig) {
644
+ return;
645
+ }
646
+ const missingFields = [];
647
+ if (!smtpHost) missingFields.push("SMTP_HOST");
648
+ if (!smtpPort) missingFields.push("SMTP_PORT");
649
+ if (!emailFrom) missingFields.push("EMAIL_FROM");
650
+ if (!emailTo) missingFields.push("EMAIL_TO");
651
+ if (missingFields.length > 0) {
652
+ throw new Error(
653
+ `Email transport configuration incomplete. Missing: ${missingFields.join(", ")}. Either set all required fields or remove all email configuration.`
654
+ );
655
+ }
656
+ const port = parseInt(smtpPort, 10);
657
+ if (isNaN(port) || port < 1 || port > 65535) {
658
+ throw new Error(
659
+ `Invalid SMTP_PORT: "${smtpPort}". Must be a number between 1 and 65535.`
660
+ );
673
661
  }
674
- if (colorize) {
675
- parts.push(`${COLORS.bright}${metadata.message}${COLORS.reset}`);
676
- } else {
677
- parts.push(metadata.message);
662
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
663
+ if (!emailRegex.test(emailFrom)) {
664
+ throw new Error(`Invalid EMAIL_FROM format: "${emailFrom}"`);
678
665
  }
679
- let output = parts.join(" ");
680
- if (metadata.error) {
681
- output += "\n" + formatError(metadata.error);
666
+ const recipients = emailTo.split(",").map((e) => e.trim());
667
+ for (const email of recipients) {
668
+ if (!emailRegex.test(email)) {
669
+ throw new Error(`Invalid email address in EMAIL_TO: "${email}"`);
670
+ }
682
671
  }
683
- return output;
684
672
  }
685
- function formatJSON(metadata) {
686
- const obj = {
687
- timestamp: formatTimestamp(metadata.timestamp),
688
- level: metadata.level,
689
- message: metadata.message
690
- };
691
- if (metadata.module) {
692
- obj.module = metadata.module;
693
- }
694
- if (metadata.context) {
695
- obj.context = metadata.context;
673
+ function validateEnvironment() {
674
+ const nodeEnv = process.env.NODE_ENV;
675
+ if (!nodeEnv) {
676
+ process.stderr.write(
677
+ "[Logger] Warning: NODE_ENV is not set. Defaulting to test environment.\n"
678
+ );
696
679
  }
697
- if (metadata.error) {
698
- obj.error = {
699
- name: metadata.error.name,
700
- message: metadata.error.message,
701
- stack: metadata.error.stack
702
- };
680
+ }
681
+ function validateConfig() {
682
+ try {
683
+ validateEnvironment();
684
+ validateFileConfig();
685
+ validateSlackConfig();
686
+ validateEmailConfig();
687
+ } catch (error) {
688
+ if (error instanceof Error) {
689
+ throw new Error(`[Logger] Configuration validation failed: ${error.message}`);
690
+ }
691
+ throw error;
703
692
  }
704
- return JSON.stringify(obj);
705
693
  }
706
694
 
707
- // src/logger/logger.ts
708
- var Logger = class _Logger {
709
- config;
710
- module;
711
- constructor(config) {
712
- this.config = config;
713
- this.module = config.module;
695
+ // src/logger/adapters/custom.ts
696
+ function initializeTransports() {
697
+ const transports = [];
698
+ const consoleConfig = getConsoleConfig();
699
+ transports.push(new ConsoleTransport(consoleConfig));
700
+ const fileConfig = getFileConfig();
701
+ if (fileConfig.enabled) {
702
+ transports.push(new FileTransport(fileConfig));
714
703
  }
715
- /**
716
- * Get current log level
717
- */
718
- get level() {
719
- return this.config.level;
704
+ return transports;
705
+ }
706
+ var CustomAdapter = class _CustomAdapter {
707
+ logger;
708
+ constructor(config) {
709
+ this.logger = new Logger({
710
+ level: config.level,
711
+ module: config.module,
712
+ transports: initializeTransports()
713
+ });
720
714
  }
721
- /**
722
- * Create child logger (per module)
723
- */
724
715
  child(module) {
725
- return new _Logger({
726
- ...this.config,
727
- module
728
- });
716
+ const adapter = new _CustomAdapter({ level: this.logger.level, module });
717
+ adapter.logger = this.logger.child(module);
718
+ return adapter;
729
719
  }
730
- /**
731
- * Debug log
732
- */
733
720
  debug(message, context) {
734
- this.log("debug", message, void 0, context);
721
+ this.logger.debug(message, context);
735
722
  }
736
- /**
737
- * Info log
738
- */
739
723
  info(message, context) {
740
- this.log("info", message, void 0, context);
724
+ this.logger.info(message, context);
741
725
  }
742
726
  warn(message, errorOrContext, context) {
743
727
  if (errorOrContext instanceof Error) {
744
- this.log("warn", message, errorOrContext, context);
728
+ this.logger.warn(message, errorOrContext, context);
745
729
  } else {
746
- this.log("warn", message, void 0, errorOrContext);
730
+ this.logger.warn(message, errorOrContext);
747
731
  }
748
732
  }
749
733
  error(message, errorOrContext, context) {
750
734
  if (errorOrContext instanceof Error) {
751
- this.log("error", message, errorOrContext, context);
735
+ this.logger.error(message, errorOrContext, context);
752
736
  } else {
753
- this.log("error", message, void 0, errorOrContext);
737
+ this.logger.error(message, errorOrContext);
754
738
  }
755
739
  }
756
740
  fatal(message, errorOrContext, context) {
757
741
  if (errorOrContext instanceof Error) {
758
- this.log("fatal", message, errorOrContext, context);
742
+ this.logger.fatal(message, errorOrContext, context);
759
743
  } else {
760
- this.log("fatal", message, void 0, errorOrContext);
761
- }
762
- }
763
- /**
764
- * Log processing (internal)
765
- */
766
- log(level, message, error, context) {
767
- if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[this.config.level]) {
768
- return;
769
- }
770
- const metadata = {
771
- timestamp: /* @__PURE__ */ new Date(),
772
- level,
773
- message,
774
- module: this.module,
775
- error,
776
- // Mask sensitive information in context to prevent credential leaks
777
- context: context ? maskSensitiveData(context) : void 0
778
- };
779
- this.processTransports(metadata);
780
- }
781
- /**
782
- * Process Transports
783
- */
784
- processTransports(metadata) {
785
- const promises = this.config.transports.filter((transport) => transport.enabled).map((transport) => this.safeTransportLog(transport, metadata));
786
- Promise.all(promises).catch((error) => {
787
- const errorMessage = error instanceof Error ? error.message : String(error);
788
- process.stderr.write(`[Logger] Transport error: ${errorMessage}
789
- `);
790
- });
791
- }
792
- /**
793
- * Transport log (error-safe)
794
- */
795
- async safeTransportLog(transport, metadata) {
796
- try {
797
- await transport.log(metadata);
798
- } catch (error) {
799
- const errorMessage = error instanceof Error ? error.message : String(error);
800
- process.stderr.write(`[Logger] Transport "${transport.name}" failed: ${errorMessage}
801
- `);
744
+ this.logger.fatal(message, errorOrContext);
802
745
  }
803
746
  }
804
- /**
805
- * Close all Transports
806
- */
807
747
  async close() {
808
- const closePromises = this.config.transports.filter((transport) => transport.close).map((transport) => transport.close());
809
- await Promise.all(closePromises);
748
+ await this.logger.close();
810
749
  }
811
750
  };
812
751
 
813
- // src/logger/transports/console.ts
814
- var ConsoleTransport = class {
815
- name = "console";
816
- level;
817
- enabled;
818
- colorize;
819
- constructor(config) {
820
- this.level = config.level;
821
- this.enabled = config.enabled;
822
- this.colorize = config.colorize ?? true;
823
- }
824
- async log(metadata) {
825
- if (!this.enabled) {
826
- return;
827
- }
828
- if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
829
- return;
830
- }
831
- const message = formatConsole(metadata, this.colorize);
832
- if (metadata.level === "warn" || metadata.level === "error" || metadata.level === "fatal") {
833
- console.error(message);
834
- } else {
835
- console.log(message);
836
- }
752
+ // src/logger/adapter-factory.ts
753
+ function createAdapter(type) {
754
+ const level = getDefaultLogLevel();
755
+ switch (type) {
756
+ case "pino":
757
+ return new PinoAdapter({ level });
758
+ case "custom":
759
+ return new CustomAdapter({ level });
760
+ default:
761
+ return new PinoAdapter({ level });
837
762
  }
838
- };
839
- var FileTransport = class {
840
- name = "file";
841
- level;
842
- enabled;
843
- logDir;
844
- maxFileSize;
845
- maxFiles;
846
- currentStream = null;
847
- currentFilename = null;
848
- constructor(config) {
849
- this.level = config.level;
850
- this.enabled = config.enabled;
851
- this.logDir = config.logDir;
852
- this.maxFileSize = config.maxFileSize ?? 10 * 1024 * 1024;
853
- this.maxFiles = config.maxFiles ?? 10;
854
- if (!existsSync(this.logDir)) {
855
- mkdirSync(this.logDir, { recursive: true });
856
- }
763
+ }
764
+ function getAdapterType() {
765
+ const adapterEnv = process.env.LOGGER_ADAPTER;
766
+ if (adapterEnv === "custom" || adapterEnv === "pino") {
767
+ return adapterEnv;
857
768
  }
858
- async log(metadata) {
859
- if (!this.enabled) {
860
- return;
861
- }
862
- if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
863
- return;
864
- }
865
- const message = formatJSON(metadata);
866
- const filename = this.getLogFilename(metadata.timestamp);
867
- if (this.currentFilename !== filename) {
868
- await this.rotateStream(filename);
869
- await this.cleanOldFiles();
870
- } else if (this.currentFilename) {
871
- await this.checkAndRotateBySize();
872
- }
873
- if (this.currentStream) {
874
- return new Promise((resolve, reject) => {
875
- this.currentStream.write(message + "\n", "utf-8", (error) => {
876
- if (error) {
877
- process.stderr.write(`[FileTransport] Failed to write log: ${error.message}
878
- `);
879
- reject(error);
880
- } else {
881
- resolve();
882
- }
883
- });
884
- });
885
- }
769
+ return "pino";
770
+ }
771
+ function initializeLogger() {
772
+ validateConfig();
773
+ return createAdapter(getAdapterType());
774
+ }
775
+ var logger = initializeLogger();
776
+
777
+ // src/codegen/core/orchestrator.ts
778
+ var orchestratorLogger = logger.child("orchestrator");
779
+ var CodegenOrchestrator = class {
780
+ generators;
781
+ cwd;
782
+ debug;
783
+ isGenerating = false;
784
+ pendingRegenerations = /* @__PURE__ */ new Set();
785
+ constructor(options) {
786
+ this.generators = options.generators;
787
+ this.cwd = options.cwd ?? process.cwd();
788
+ this.debug = options.debug ?? false;
886
789
  }
887
790
  /**
888
- * 스트림 교체 (날짜 변경 시)
791
+ * Check if generator should run for given trigger
889
792
  */
890
- async rotateStream(filename) {
891
- if (this.currentStream) {
892
- await this.closeStream();
893
- }
894
- const filepath = join(this.logDir, filename);
895
- this.currentStream = createWriteStream(filepath, {
896
- flags: "a",
897
- // append mode
898
- encoding: "utf-8"
899
- });
900
- this.currentFilename = filename;
901
- this.currentStream.on("error", (error) => {
902
- process.stderr.write(`[FileTransport] Stream error: ${error.message}
903
- `);
904
- this.currentStream = null;
905
- this.currentFilename = null;
906
- });
793
+ shouldRun(generator, trigger) {
794
+ const runOn = generator.runOn ?? ["watch", "manual", "build"];
795
+ return runOn.includes(trigger);
907
796
  }
908
797
  /**
909
- * 현재 스트림 닫기
798
+ * Run all generators once
799
+ *
800
+ * @param trigger - How the generators are being triggered
910
801
  */
911
- async closeStream() {
912
- if (!this.currentStream) {
913
- return;
802
+ async generateAll(trigger = "manual") {
803
+ if (this.debug) {
804
+ orchestratorLogger.info("Running all generators", {
805
+ count: this.generators.length,
806
+ names: this.generators.map((g) => g.name),
807
+ trigger
808
+ });
914
809
  }
915
- return new Promise((resolve, reject) => {
916
- this.currentStream.end((error) => {
917
- if (error) {
918
- reject(error);
919
- } else {
920
- this.currentStream = null;
921
- this.currentFilename = null;
922
- resolve();
810
+ for (const generator of this.generators) {
811
+ if (!this.shouldRun(generator, trigger)) {
812
+ if (this.debug) {
813
+ orchestratorLogger.info(`[${generator.name}] Skipped (runOn: ${generator.runOn?.join(", ") ?? "default"})`);
923
814
  }
924
- });
925
- });
815
+ continue;
816
+ }
817
+ try {
818
+ const genOptions = {
819
+ cwd: this.cwd,
820
+ debug: this.debug,
821
+ trigger: {
822
+ type: trigger
823
+ }
824
+ };
825
+ await generator.generate(genOptions);
826
+ if (this.debug) {
827
+ orchestratorLogger.info(`[${generator.name}] Generated successfully`);
828
+ }
829
+ } catch (error) {
830
+ const err = error instanceof Error ? error : new Error(String(error));
831
+ orchestratorLogger.error(`[${generator.name}] Generation failed`, err);
832
+ }
833
+ }
926
834
  }
927
835
  /**
928
- * 파일 크기 체크 및 크기 기반 로테이션
836
+ * Start watch mode
929
837
  */
930
- async checkAndRotateBySize() {
931
- if (!this.currentFilename) {
838
+ async watch() {
839
+ await this.generateAll("watch");
840
+ const allPatterns = this.generators.flatMap((g) => g.watchPatterns);
841
+ if (allPatterns.length === 0) {
842
+ orchestratorLogger.warn("No watch patterns defined, exiting watch mode");
932
843
  return;
933
844
  }
934
- const filepath = join(this.logDir, this.currentFilename);
935
- if (!existsSync(filepath)) {
936
- return;
845
+ const dirsToWatch = /* @__PURE__ */ new Set();
846
+ for (const pattern of allPatterns) {
847
+ const baseDir = pattern.split("**")[0].replace(/\/$/, "") || ".";
848
+ dirsToWatch.add(join(this.cwd, baseDir));
937
849
  }
938
- try {
939
- const stats = statSync(filepath);
940
- if (stats.size >= this.maxFileSize) {
941
- await this.rotateBySize();
850
+ const watchDirs = Array.from(dirsToWatch);
851
+ if (this.debug) {
852
+ orchestratorLogger.info("Starting watch mode", {
853
+ patterns: allPatterns,
854
+ watchDirs,
855
+ cwd: this.cwd
856
+ });
857
+ }
858
+ const watcher = watch(watchDirs, {
859
+ ignored: /(^|[\/\\])\../,
860
+ // ignore dotfiles
861
+ persistent: true,
862
+ ignoreInitial: true,
863
+ awaitWriteFinish: {
864
+ stabilityThreshold: 100,
865
+ pollInterval: 50
866
+ }
867
+ });
868
+ const handleChange = async (absolutePath, event) => {
869
+ const filePath = relative(this.cwd, absolutePath);
870
+ if (this.isGenerating) {
871
+ this.pendingRegenerations.add(absolutePath);
872
+ return;
873
+ }
874
+ this.isGenerating = true;
875
+ this.pendingRegenerations.clear();
876
+ if (this.debug) {
877
+ orchestratorLogger.info(`File ${event}`, { file: filePath });
878
+ }
879
+ for (const generator of this.generators) {
880
+ if (!this.shouldRun(generator, "watch")) {
881
+ continue;
882
+ }
883
+ const matches = generator.watchPatterns.some(
884
+ (pattern) => mm.isMatch(filePath, pattern)
885
+ );
886
+ if (matches) {
887
+ try {
888
+ const genOptions = {
889
+ cwd: this.cwd,
890
+ debug: this.debug,
891
+ trigger: {
892
+ type: "watch",
893
+ changedFile: {
894
+ path: filePath,
895
+ event
896
+ }
897
+ }
898
+ };
899
+ await generator.generate(genOptions);
900
+ if (this.debug) {
901
+ orchestratorLogger.info(`[${generator.name}] Regenerated`);
902
+ }
903
+ } catch (error) {
904
+ const err = error instanceof Error ? error : new Error(String(error));
905
+ orchestratorLogger.error(`[${generator.name}] Regeneration failed`, err);
906
+ }
907
+ }
908
+ }
909
+ this.isGenerating = false;
910
+ if (this.pendingRegenerations.size > 0) {
911
+ const next = Array.from(this.pendingRegenerations)[0];
912
+ await handleChange(next, "change");
913
+ }
914
+ };
915
+ watcher.on("add", (path) => handleChange(path, "add")).on("change", (path) => handleChange(path, "change")).on("unlink", (path) => handleChange(path, "unlink"));
916
+ process.on("SIGINT", () => {
917
+ if (this.debug) {
918
+ orchestratorLogger.info("Shutting down watch mode");
919
+ }
920
+ watcher.close();
921
+ process.exit(0);
922
+ });
923
+ await new Promise(() => {
924
+ });
925
+ }
926
+ };
927
+ async function scanContracts(contractsDir, packagePrefix) {
928
+ const contractFiles = await scanContractFiles(contractsDir);
929
+ const mappings = [];
930
+ for (let i = 0; i < contractFiles.length; i++) {
931
+ const filePath = contractFiles[i];
932
+ const exports = extractContractExports(filePath);
933
+ for (let j = 0; j < exports.length; j++) {
934
+ const contractExport = exports[j];
935
+ if (!contractExport.path.startsWith("/")) {
936
+ throw new Error(
937
+ `Contract '${contractExport.name}' in ${filePath} must use absolute path. Found: '${contractExport.path}'. Use '/your-path' instead.`
938
+ );
939
+ }
940
+ if (packagePrefix && !contractExport.path.startsWith(packagePrefix)) {
941
+ throw new Error(
942
+ `Contract '${contractExport.name}' in ${filePath} must include package prefix. Expected path to start with '${packagePrefix}', but found: '${contractExport.path}'. Example: path: '${packagePrefix}/${contractExport.path}'`
943
+ );
944
+ }
945
+ mappings.push({
946
+ method: contractExport.method,
947
+ path: contractExport.path,
948
+ contractName: contractExport.name,
949
+ contractImportPath: getImportPath(filePath),
950
+ routeFile: "",
951
+ contractFile: filePath,
952
+ hasQuery: contractExport.hasQuery,
953
+ hasBody: contractExport.hasBody,
954
+ hasParams: contractExport.hasParams
955
+ });
956
+ }
957
+ }
958
+ return mappings;
959
+ }
960
+ async function scanContractFiles(dir, files = []) {
961
+ try {
962
+ const entries = await readdir(dir);
963
+ for (let i = 0; i < entries.length; i++) {
964
+ const entry = entries[i];
965
+ const fullPath = join(dir, entry);
966
+ const fileStat = await stat(fullPath);
967
+ if (fileStat.isDirectory()) {
968
+ await scanContractFiles(fullPath, files);
969
+ } else {
970
+ if ((entry.endsWith(".ts") || entry.endsWith(".js") || entry.endsWith(".mjs")) && !entry.endsWith(".d.ts") && !entry.endsWith(".test.ts") && !entry.endsWith(".test.js") && !entry.endsWith(".test.mjs")) {
971
+ files.push(fullPath);
972
+ }
973
+ }
974
+ }
975
+ } catch (error) {
976
+ }
977
+ return files;
978
+ }
979
+ function extractContractExports(filePath) {
980
+ const sourceCode = readFileSync(filePath, "utf-8");
981
+ const sourceFile = ts.createSourceFile(
982
+ filePath,
983
+ sourceCode,
984
+ ts.ScriptTarget.Latest,
985
+ true
986
+ );
987
+ const exports = [];
988
+ function visit(node) {
989
+ if (ts.isVariableStatement(node)) {
990
+ const hasExport = node.modifiers?.some(
991
+ (m) => m.kind === ts.SyntaxKind.ExportKeyword
992
+ );
993
+ if (hasExport && node.declarationList.declarations.length > 0) {
994
+ const declaration = node.declarationList.declarations[0];
995
+ if (ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name) && declaration.initializer) {
996
+ const name = declaration.name.text;
997
+ const hasSatisfiesRouteContract = checkSatisfiesRouteContract(declaration.initializer);
998
+ if (hasSatisfiesRouteContract) {
999
+ const objectLiteral = extractObjectLiteral(declaration.initializer);
1000
+ if (objectLiteral) {
1001
+ const contractData = extractContractData(objectLiteral);
1002
+ if (contractData.method && contractData.path) {
1003
+ exports.push({
1004
+ name,
1005
+ method: contractData.method,
1006
+ path: contractData.path,
1007
+ hasQuery: contractData.hasQuery,
1008
+ hasBody: contractData.hasBody,
1009
+ hasParams: contractData.hasParams
1010
+ });
1011
+ }
1012
+ }
1013
+ return;
1014
+ }
1015
+ if (isContractName(name)) {
1016
+ const objectLiteral = extractObjectLiteral(declaration.initializer);
1017
+ if (objectLiteral) {
1018
+ const contractData = extractContractData(objectLiteral);
1019
+ if (contractData.method && contractData.path) {
1020
+ exports.push({
1021
+ name,
1022
+ method: contractData.method,
1023
+ path: contractData.path,
1024
+ hasQuery: contractData.hasQuery,
1025
+ hasBody: contractData.hasBody,
1026
+ hasParams: contractData.hasParams
1027
+ });
1028
+ }
1029
+ }
1030
+ }
1031
+ }
942
1032
  }
943
- } catch (error) {
944
- const errorMessage = error instanceof Error ? error.message : String(error);
945
- process.stderr.write(`[FileTransport] Failed to check file size: ${errorMessage}
946
- `);
947
1033
  }
1034
+ ts.forEachChild(node, visit);
948
1035
  }
949
- /**
950
- * 크기 기반 로테이션 수행
951
- * 예: 2025-01-01.log -> 2025-01-01.1.log, 2025-01-01.1.log -> 2025-01-01.2.log
952
- */
953
- async rotateBySize() {
954
- if (!this.currentFilename) {
955
- return;
956
- }
957
- await this.closeStream();
958
- const baseName = this.currentFilename.replace(/\.log$/, "");
959
- const files = readdirSync(this.logDir);
960
- const relatedFiles = files.filter((file) => file.startsWith(baseName) && file.endsWith(".log")).sort().reverse();
961
- for (const file of relatedFiles) {
962
- const match = file.match(/\.(\d+)\.log$/);
963
- if (match) {
964
- const oldNum = parseInt(match[1], 10);
965
- const newNum = oldNum + 1;
966
- const oldPath = join(this.logDir, file);
967
- const newPath2 = join(this.logDir, `${baseName}.${newNum}.log`);
968
- try {
969
- renameSync(oldPath, newPath2);
970
- } catch (error) {
971
- const errorMessage = error instanceof Error ? error.message : String(error);
972
- process.stderr.write(`[FileTransport] Failed to rotate file: ${errorMessage}
973
- `);
1036
+ visit(sourceFile);
1037
+ return exports;
1038
+ }
1039
+ function checkSatisfiesRouteContract(initializer) {
1040
+ if (!ts.isSatisfiesExpression(initializer)) {
1041
+ return false;
1042
+ }
1043
+ const typeNode = initializer.type;
1044
+ if (ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
1045
+ return typeNode.typeName.text === "RouteContract";
1046
+ }
1047
+ return false;
1048
+ }
1049
+ function extractObjectLiteral(initializer) {
1050
+ if (ts.isObjectLiteralExpression(initializer)) {
1051
+ return initializer;
1052
+ }
1053
+ if (ts.isSatisfiesExpression(initializer)) {
1054
+ return extractObjectLiteral(initializer.expression);
1055
+ }
1056
+ if (ts.isAsExpression(initializer)) {
1057
+ return extractObjectLiteral(initializer.expression);
1058
+ }
1059
+ return void 0;
1060
+ }
1061
+ function extractContractData(objectLiteral) {
1062
+ const result = {};
1063
+ for (let i = 0; i < objectLiteral.properties.length; i++) {
1064
+ const prop = objectLiteral.properties[i];
1065
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
1066
+ const propName = prop.name.text;
1067
+ if (propName === "method") {
1068
+ let value;
1069
+ if (ts.isStringLiteral(prop.initializer)) {
1070
+ value = prop.initializer.text;
1071
+ } else if (ts.isAsExpression(prop.initializer) && ts.isStringLiteral(prop.initializer.expression)) {
1072
+ value = prop.initializer.expression.text;
974
1073
  }
1074
+ if (value) result.method = value;
1075
+ } else if (propName === "path") {
1076
+ let value;
1077
+ if (ts.isStringLiteral(prop.initializer)) {
1078
+ value = prop.initializer.text;
1079
+ } else if (ts.isAsExpression(prop.initializer) && ts.isStringLiteral(prop.initializer.expression)) {
1080
+ value = prop.initializer.expression.text;
1081
+ }
1082
+ if (value) result.path = value;
1083
+ } else if (propName === "query") {
1084
+ result.hasQuery = true;
1085
+ } else if (propName === "body") {
1086
+ result.hasBody = true;
1087
+ } else if (propName === "params") {
1088
+ result.hasParams = true;
975
1089
  }
976
1090
  }
977
- const currentPath = join(this.logDir, this.currentFilename);
978
- const newPath = join(this.logDir, `${baseName}.1.log`);
979
- try {
980
- if (existsSync(currentPath)) {
981
- renameSync(currentPath, newPath);
982
- }
983
- } catch (error) {
984
- const errorMessage = error instanceof Error ? error.message : String(error);
985
- process.stderr.write(`[FileTransport] Failed to rotate current file: ${errorMessage}
986
- `);
1091
+ }
1092
+ return result;
1093
+ }
1094
+ function isContractName(name) {
1095
+ return name.indexOf("Contract") !== -1 || name.indexOf("contract") !== -1 || name.endsWith("Schema") || name.endsWith("schema");
1096
+ }
1097
+ function getImportPath(filePath) {
1098
+ const srcIndex = filePath.indexOf("/src/");
1099
+ if (srcIndex === -1) {
1100
+ throw new Error(`Cannot determine import path for ${filePath}: /src/ directory not found`);
1101
+ }
1102
+ let cleanPath = filePath.substring(srcIndex + 5);
1103
+ if (cleanPath.endsWith(".ts")) {
1104
+ cleanPath = cleanPath.slice(0, -3);
1105
+ } else if (cleanPath.endsWith(".js")) {
1106
+ cleanPath = cleanPath.slice(0, -3);
1107
+ } else if (cleanPath.endsWith(".mjs")) {
1108
+ cleanPath = cleanPath.slice(0, -4);
1109
+ }
1110
+ return "@/" + cleanPath;
1111
+ }
1112
+
1113
+ // src/codegen/built-in/contract/helpers.ts
1114
+ function groupByResource(mappings) {
1115
+ const grouped = {};
1116
+ for (let i = 0; i < mappings.length; i++) {
1117
+ const mapping = mappings[i];
1118
+ const resource = extractResourceName(mapping.path);
1119
+ if (!grouped[resource]) {
1120
+ grouped[resource] = [];
987
1121
  }
988
- await this.rotateStream(this.currentFilename);
1122
+ grouped[resource].push(mapping);
989
1123
  }
990
- /**
991
- * 오래된 로그 파일 정리
992
- * maxFiles 개수를 초과하는 로그 파일 삭제
993
- */
994
- async cleanOldFiles() {
995
- try {
996
- if (!existsSync(this.logDir)) {
997
- return;
998
- }
999
- const files = readdirSync(this.logDir);
1000
- const logFiles = files.filter((file) => file.endsWith(".log")).map((file) => {
1001
- const filepath = join(this.logDir, file);
1002
- const stats = statSync(filepath);
1003
- return { file, mtime: stats.mtime };
1004
- }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
1005
- if (logFiles.length > this.maxFiles) {
1006
- const filesToDelete = logFiles.slice(this.maxFiles);
1007
- for (const { file } of filesToDelete) {
1008
- const filepath = join(this.logDir, file);
1009
- try {
1010
- unlinkSync(filepath);
1011
- } catch (error) {
1012
- const errorMessage = error instanceof Error ? error.message : String(error);
1013
- process.stderr.write(`[FileTransport] Failed to delete old file "${file}": ${errorMessage}
1014
- `);
1015
- }
1016
- }
1017
- }
1018
- } catch (error) {
1019
- const errorMessage = error instanceof Error ? error.message : String(error);
1020
- process.stderr.write(`[FileTransport] Failed to clean old files: ${errorMessage}
1021
- `);
1124
+ return grouped;
1125
+ }
1126
+ function extractResourceName(path) {
1127
+ let processedPath = path;
1128
+ if (!processedPath.startsWith("/")) {
1129
+ processedPath = "/" + processedPath;
1130
+ }
1131
+ const segments = processedPath.slice(1).split("/").filter((s) => s && s !== "*");
1132
+ const staticSegments = [];
1133
+ for (let i = 0; i < segments.length; i++) {
1134
+ const seg = segments[i];
1135
+ if (!seg.startsWith(":")) {
1136
+ staticSegments.push(seg);
1022
1137
  }
1023
1138
  }
1024
- /**
1025
- * 날짜별 로그 파일명 생성
1026
- */
1027
- getLogFilename(date) {
1028
- const year = date.getFullYear();
1029
- const month = String(date.getMonth() + 1).padStart(2, "0");
1030
- const day = String(date.getDate()).padStart(2, "0");
1031
- return `${year}-${month}-${day}.log`;
1139
+ if (staticSegments.length === 0) {
1140
+ return "root";
1032
1141
  }
1033
- async close() {
1034
- await this.closeStream();
1142
+ const first = toCamelCase(staticSegments[0], false);
1143
+ if (staticSegments.length === 1) {
1144
+ return first;
1035
1145
  }
1036
- };
1037
- function isFileLoggingEnabled() {
1038
- return process.env.LOGGER_FILE_ENABLED === "true";
1146
+ const result = [first];
1147
+ for (let i = 1; i < staticSegments.length; i++) {
1148
+ const seg = staticSegments[i];
1149
+ result.push(toCamelCase(seg, true));
1150
+ }
1151
+ return result.join("");
1039
1152
  }
1040
- function getDefaultLogLevel() {
1041
- const isProduction = process.env.NODE_ENV === "production";
1042
- const isDevelopment = process.env.NODE_ENV === "development";
1043
- if (isDevelopment) {
1044
- return "debug";
1153
+ function toCamelCase(str, capitalize2) {
1154
+ const parts = str.split(/[-_]/);
1155
+ if (parts.length === 1) {
1156
+ return capitalize2 ? str.charAt(0).toUpperCase() + str.slice(1) : str;
1045
1157
  }
1046
- if (isProduction) {
1047
- return "info";
1158
+ const result = [];
1159
+ for (let i = 0; i < parts.length; i++) {
1160
+ const part = parts[i];
1161
+ if (i === 0 && !capitalize2) {
1162
+ result.push(part);
1163
+ } else {
1164
+ result.push(part.charAt(0).toUpperCase() + part.slice(1));
1165
+ }
1048
1166
  }
1049
- return "warn";
1167
+ return result.join("");
1050
1168
  }
1051
- function getConsoleConfig() {
1052
- const isProduction = process.env.NODE_ENV === "production";
1169
+
1170
+ // src/codegen/built-in/contract/emitter.ts
1171
+ async function generateClient(mappings, options) {
1172
+ const startTime = Date.now();
1173
+ const grouped = groupByResource(mappings);
1174
+ const resourceNames = Object.keys(grouped);
1175
+ await generateSplitClient(mappings, grouped, options);
1053
1176
  return {
1054
- level: "debug",
1055
- enabled: true,
1056
- colorize: !isProduction
1057
- // Dev: colored output, Production: plain text
1177
+ routesScanned: mappings.length,
1178
+ contractsFound: mappings.length,
1179
+ contractFiles: countUniqueContractFiles(mappings),
1180
+ resourcesGenerated: resourceNames.length,
1181
+ methodsGenerated: mappings.length,
1182
+ duration: Date.now() - startTime
1058
1183
  };
1059
1184
  }
1060
- function getFileConfig() {
1061
- const isProduction = process.env.NODE_ENV === "production";
1062
- return {
1063
- level: "info",
1064
- enabled: isProduction,
1065
- // File logging in production only
1066
- logDir: process.env.LOG_DIR || "./logs",
1067
- maxFileSize: 10 * 1024 * 1024,
1068
- // 10MB
1069
- maxFiles: 10
1070
- };
1185
+ function generateHeader() {
1186
+ return `/**
1187
+ * Auto-generated API Client
1188
+ *
1189
+ * Generated by @spfn/core codegen
1190
+ * DO NOT EDIT MANUALLY
1191
+ *
1192
+ * @generated ${(/* @__PURE__ */ new Date()).toISOString()}
1193
+ */
1194
+
1195
+ `;
1071
1196
  }
1072
- function validateDirectoryWritable(dirPath) {
1073
- if (!existsSync(dirPath)) {
1074
- try {
1075
- mkdirSync(dirPath, { recursive: true });
1076
- } catch (error) {
1077
- const errorMessage = error instanceof Error ? error.message : String(error);
1078
- throw new Error(`Failed to create log directory "${dirPath}": ${errorMessage}`);
1197
+ function groupContractsByImportPath(mappings) {
1198
+ const groups = {};
1199
+ for (let i = 0; i < mappings.length; i++) {
1200
+ const mapping = mappings[i];
1201
+ const path = mapping.contractImportPath;
1202
+ if (!groups[path]) {
1203
+ groups[path] = /* @__PURE__ */ new Set();
1079
1204
  }
1205
+ groups[path].add(mapping.contractName);
1080
1206
  }
1081
- try {
1082
- accessSync(dirPath, constants.W_OK);
1083
- } catch {
1084
- throw new Error(`Log directory "${dirPath}" is not writable. Please check permissions.`);
1085
- }
1086
- const testFile = join(dirPath, ".logger-write-test");
1087
- try {
1088
- writeFileSync(testFile, "test", "utf-8");
1089
- unlinkSync(testFile);
1090
- } catch (error) {
1091
- const errorMessage = error instanceof Error ? error.message : String(error);
1092
- throw new Error(`Cannot write to log directory "${dirPath}": ${errorMessage}`);
1207
+ const result = {};
1208
+ const keys = Object.keys(groups);
1209
+ for (let i = 0; i < keys.length; i++) {
1210
+ const key = keys[i];
1211
+ result[key] = Array.from(groups[key]);
1093
1212
  }
1213
+ return result;
1094
1214
  }
1095
- function validateFileConfig() {
1096
- if (!isFileLoggingEnabled()) {
1097
- return;
1215
+ function generateTypeName(mapping) {
1216
+ let name = mapping.contractName;
1217
+ if (name.endsWith("Contract")) {
1218
+ name = name.slice(0, -8);
1098
1219
  }
1099
- const logDir = process.env.LOG_DIR;
1100
- if (!logDir) {
1101
- throw new Error(
1102
- "LOG_DIR environment variable is required when LOGGER_FILE_ENABLED=true. Example: LOG_DIR=/var/log/myapp"
1103
- );
1220
+ if (name.length > 0) {
1221
+ name = name.charAt(0).toUpperCase() + name.slice(1);
1104
1222
  }
1105
- validateDirectoryWritable(logDir);
1223
+ return name;
1106
1224
  }
1107
- function validateSlackConfig() {
1108
- const webhookUrl = process.env.SLACK_WEBHOOK_URL;
1109
- if (!webhookUrl) {
1110
- return;
1225
+ function generateMethodCode(mapping, options) {
1226
+ const methodName = generateMethodName(mapping);
1227
+ const hasParams = mapping.hasParams || mapping.path.includes(":");
1228
+ const hasQuery = mapping.hasQuery || false;
1229
+ const hasBody = mapping.hasBody || false;
1230
+ let code = "";
1231
+ if (options.includeJsDoc !== false) {
1232
+ code += ` /**
1233
+ `;
1234
+ code += ` * ${mapping.method} ${mapping.path}
1235
+ `;
1236
+ code += ` */
1237
+ `;
1111
1238
  }
1112
- if (!webhookUrl.startsWith("https://hooks.slack.com/")) {
1113
- throw new Error(
1114
- `Invalid SLACK_WEBHOOK_URL: "${webhookUrl}". Slack webhook URLs must start with "https://hooks.slack.com/"`
1115
- );
1239
+ code += ` ${methodName}: (`;
1240
+ const params = [];
1241
+ const typeName = generateTypeName(mapping);
1242
+ if (hasParams) {
1243
+ params.push(`params: ${typeName}Params`);
1116
1244
  }
1117
- }
1118
- function validateEmailConfig() {
1119
- const smtpHost = process.env.SMTP_HOST;
1120
- const smtpPort = process.env.SMTP_PORT;
1121
- const emailFrom = process.env.EMAIL_FROM;
1122
- const emailTo = process.env.EMAIL_TO;
1123
- const hasAnyEmailConfig = smtpHost || smtpPort || emailFrom || emailTo;
1124
- if (!hasAnyEmailConfig) {
1125
- return;
1245
+ if (hasQuery) {
1246
+ params.push(`query?: ${typeName}Query`);
1126
1247
  }
1127
- const missingFields = [];
1128
- if (!smtpHost) missingFields.push("SMTP_HOST");
1129
- if (!smtpPort) missingFields.push("SMTP_PORT");
1130
- if (!emailFrom) missingFields.push("EMAIL_FROM");
1131
- if (!emailTo) missingFields.push("EMAIL_TO");
1132
- if (missingFields.length > 0) {
1133
- throw new Error(
1134
- `Email transport configuration incomplete. Missing: ${missingFields.join(", ")}. Either set all required fields or remove all email configuration.`
1135
- );
1248
+ if (hasBody) {
1249
+ params.push(`body: ${typeName}Body`);
1136
1250
  }
1137
- const port = parseInt(smtpPort, 10);
1138
- if (isNaN(port) || port < 1 || port > 65535) {
1139
- throw new Error(
1140
- `Invalid SMTP_PORT: "${smtpPort}". Must be a number between 1 and 65535.`
1141
- );
1251
+ if (params.length > 0) {
1252
+ code += `options: { ${params.join(", ")} }`;
1142
1253
  }
1143
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1144
- if (!emailRegex.test(emailFrom)) {
1145
- throw new Error(`Invalid EMAIL_FROM format: "${emailFrom}"`);
1254
+ code += `) => `;
1255
+ code += `client.call(${mapping.contractName}`;
1256
+ if (params.length > 0) {
1257
+ code += `, options`;
1146
1258
  }
1147
- const recipients = emailTo.split(",").map((e) => e.trim());
1148
- for (const email of recipients) {
1149
- if (!emailRegex.test(email)) {
1150
- throw new Error(`Invalid email address in EMAIL_TO: "${email}"`);
1259
+ code += `),
1260
+ `;
1261
+ return code;
1262
+ }
1263
+ function generateMethodName(mapping) {
1264
+ const method = mapping.method.toLowerCase();
1265
+ if (mapping.path === "/" || mapping.path.match(/^\/[\w-]+$/)) {
1266
+ if (method === "get") {
1267
+ return "list";
1268
+ }
1269
+ if (method === "post") {
1270
+ return "create";
1151
1271
  }
1152
1272
  }
1153
- }
1154
- function validateEnvironment() {
1155
- const nodeEnv = process.env.NODE_ENV;
1156
- if (!nodeEnv) {
1157
- process.stderr.write(
1158
- "[Logger] Warning: NODE_ENV is not set. Defaulting to test environment.\n"
1159
- );
1273
+ if (mapping.path.includes(":")) {
1274
+ if (method === "get") {
1275
+ return "getById";
1276
+ }
1277
+ if (method === "put" || method === "patch") {
1278
+ return "update";
1279
+ }
1280
+ if (method === "delete") {
1281
+ return "delete";
1282
+ }
1160
1283
  }
1284
+ return method;
1161
1285
  }
1162
- function validateConfig() {
1163
- try {
1164
- validateEnvironment();
1165
- validateFileConfig();
1166
- validateSlackConfig();
1167
- validateEmailConfig();
1168
- } catch (error) {
1169
- if (error instanceof Error) {
1170
- throw new Error(`[Logger] Configuration validation failed: ${error.message}`);
1286
+ function countUniqueContractFiles(mappings) {
1287
+ const files = /* @__PURE__ */ new Set();
1288
+ for (let i = 0; i < mappings.length; i++) {
1289
+ if (mappings[i].contractFile) {
1290
+ files.add(mappings[i].contractFile);
1171
1291
  }
1172
- throw error;
1173
1292
  }
1293
+ return files.size;
1174
1294
  }
1175
-
1176
- // src/logger/adapters/custom.ts
1177
- function initializeTransports() {
1178
- const transports = [];
1179
- const consoleConfig = getConsoleConfig();
1180
- transports.push(new ConsoleTransport(consoleConfig));
1181
- const fileConfig = getFileConfig();
1182
- if (fileConfig.enabled) {
1183
- transports.push(new FileTransport(fileConfig));
1295
+ function toKebabCase(str) {
1296
+ if (str.length === 0) {
1297
+ return str;
1184
1298
  }
1185
- return transports;
1299
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
1186
1300
  }
1187
- var CustomAdapter = class _CustomAdapter {
1188
- logger;
1189
- constructor(config) {
1190
- this.logger = new Logger({
1191
- level: config.level,
1192
- module: config.module,
1193
- transports: initializeTransports()
1194
- });
1195
- }
1196
- child(module) {
1197
- const adapter = new _CustomAdapter({ level: this.logger.level, module });
1198
- adapter.logger = this.logger.child(module);
1199
- return adapter;
1200
- }
1201
- debug(message, context) {
1202
- this.logger.debug(message, context);
1203
- }
1204
- info(message, context) {
1205
- this.logger.info(message, context);
1301
+ async function generateSplitClient(_mappings, grouped, options) {
1302
+ const outputPath = options.outputPath;
1303
+ const outputDir = outputPath.endsWith(".ts") || outputPath.endsWith(".js") ? outputPath.replace(/\.[jt]s$/, "") : outputPath;
1304
+ await mkdir(outputDir, { recursive: true });
1305
+ const resourceNames = Object.keys(grouped);
1306
+ for (let i = 0; i < resourceNames.length; i++) {
1307
+ const resourceName = resourceNames[i];
1308
+ const routes = grouped[resourceName];
1309
+ const code = generateResourceFile(resourceName, routes, options);
1310
+ const kebabName = toKebabCase(resourceName);
1311
+ const filePath = `${outputDir}/${kebabName}.ts`;
1312
+ await writeFile(filePath, code, "utf-8");
1206
1313
  }
1207
- warn(message, errorOrContext, context) {
1208
- if (errorOrContext instanceof Error) {
1209
- this.logger.warn(message, errorOrContext, context);
1210
- } else {
1211
- this.logger.warn(message, errorOrContext);
1212
- }
1314
+ const indexCode = generateIndexFile(resourceNames, options);
1315
+ const indexPath = `${outputDir}/index.ts`;
1316
+ await writeFile(indexPath, indexCode, "utf-8");
1317
+ }
1318
+ function generateResourceFile(resourceName, routes, options) {
1319
+ let code = "";
1320
+ code += generateHeader();
1321
+ code += `import { client } from '@spfn/core/client';
1322
+ `;
1323
+ if (options.includeTypes !== false) {
1324
+ code += `import type { InferContract } from '@spfn/core';
1325
+ `;
1213
1326
  }
1214
- error(message, errorOrContext, context) {
1215
- if (errorOrContext instanceof Error) {
1216
- this.logger.error(message, errorOrContext, context);
1217
- } else {
1218
- this.logger.error(message, errorOrContext);
1219
- }
1327
+ code += `
1328
+ `;
1329
+ const importGroups = groupContractsByImportPath(routes);
1330
+ const importPaths = Object.keys(importGroups);
1331
+ for (let i = 0; i < importPaths.length; i++) {
1332
+ const importPath = importPaths[i];
1333
+ const contracts = importGroups[importPath];
1334
+ code += `import { ${contracts.join(", ")} } from '${importPath}';
1335
+ `;
1220
1336
  }
1221
- fatal(message, errorOrContext, context) {
1222
- if (errorOrContext instanceof Error) {
1223
- this.logger.fatal(message, errorOrContext, context);
1224
- } else {
1225
- this.logger.fatal(message, errorOrContext);
1337
+ code += `
1338
+ `;
1339
+ if (options.includeTypes !== false) {
1340
+ code += `// ============================================
1341
+ `;
1342
+ code += `// Types
1343
+ `;
1344
+ code += `// ============================================
1345
+
1346
+ `;
1347
+ for (let i = 0; i < routes.length; i++) {
1348
+ const route = routes[i];
1349
+ const typeName = generateTypeName(route);
1350
+ const contractType = `typeof ${route.contractName}`;
1351
+ code += `export type ${typeName}Response = InferContract<${contractType}>['response'];
1352
+ `;
1353
+ if (route.hasQuery) {
1354
+ code += `export type ${typeName}Query = InferContract<${contractType}>['query'];
1355
+ `;
1356
+ }
1357
+ if (route.hasParams || route.path.includes(":")) {
1358
+ code += `export type ${typeName}Params = InferContract<${contractType}>['params'];
1359
+ `;
1360
+ }
1361
+ if (route.hasBody) {
1362
+ code += `export type ${typeName}Body = InferContract<${contractType}>['body'];
1363
+ `;
1364
+ }
1365
+ code += `
1366
+ `;
1226
1367
  }
1227
1368
  }
1228
- async close() {
1229
- await this.logger.close();
1230
- }
1231
- };
1232
-
1233
- // src/logger/adapter-factory.ts
1234
- function createAdapter(type) {
1235
- const level = getDefaultLogLevel();
1236
- switch (type) {
1237
- case "pino":
1238
- return new PinoAdapter({ level });
1239
- case "custom":
1240
- return new CustomAdapter({ level });
1241
- default:
1242
- return new PinoAdapter({ level });
1369
+ code += `/**
1370
+ `;
1371
+ code += ` * ${resourceName} API
1372
+ `;
1373
+ code += ` */
1374
+ `;
1375
+ code += `export const ${resourceName} = {
1376
+ `;
1377
+ for (let i = 0; i < routes.length; i++) {
1378
+ const route = routes[i];
1379
+ code += generateMethodCode(route, options);
1243
1380
  }
1381
+ code += `} as const;
1382
+ `;
1383
+ return code;
1244
1384
  }
1245
- function getAdapterType() {
1246
- const adapterEnv = process.env.LOGGER_ADAPTER;
1247
- if (adapterEnv === "custom" || adapterEnv === "pino") {
1248
- return adapterEnv;
1385
+ function toCamelCase2(str) {
1386
+ if (str.length === 0) {
1387
+ return str;
1249
1388
  }
1250
- return "pino";
1251
- }
1252
- function initializeLogger() {
1253
- validateConfig();
1254
- return createAdapter(getAdapterType());
1389
+ return str.charAt(0).toLowerCase() + str.slice(1);
1255
1390
  }
1256
- var logger = initializeLogger();
1391
+ function generateIndexFile(resourceNames, options) {
1392
+ let code = "";
1393
+ const apiName = options.apiName || "api";
1394
+ code += generateHeader();
1395
+ code += `export { client } from '@spfn/core/client';
1257
1396
 
1258
- // src/codegen/watch-generate.ts
1259
- var codegenLogger = logger.child("codegen");
1260
- async function generateOnce(options) {
1261
- const cwd = process.cwd();
1262
- const routesDir = options.routesDir ?? join(cwd, "src", "server", "routes");
1263
- const outputPath = options.outputPath ?? join(cwd, "src", "lib", "api.ts");
1264
- try {
1265
- const contracts = await scanContracts(routesDir);
1266
- if (contracts.length === 0) {
1267
- if (options.debug) {
1268
- codegenLogger.warn("No contracts found");
1269
- }
1270
- return null;
1271
- }
1272
- const stats = await generateClient(contracts, {
1273
- outputPath,
1274
- includeTypes: true,
1275
- includeJsDoc: true
1276
- });
1277
- if (options.debug) {
1278
- codegenLogger.info("Client generated", {
1279
- endpoints: stats.methodsGenerated,
1280
- resources: stats.resourcesGenerated,
1281
- duration: stats.duration
1282
- });
1397
+ `;
1398
+ for (let i = 0; i < resourceNames.length; i++) {
1399
+ const resourceName = resourceNames[i];
1400
+ const kebabName = toKebabCase(resourceName);
1401
+ code += `export * from './${kebabName}.js';
1402
+ `;
1403
+ }
1404
+ code += `
1405
+ `;
1406
+ for (let i = 0; i < resourceNames.length; i++) {
1407
+ const resourceName = resourceNames[i];
1408
+ const kebabName = toKebabCase(resourceName);
1409
+ code += `import { ${resourceName} } from './${kebabName}.js';
1410
+ `;
1411
+ }
1412
+ code += `
1413
+ `;
1414
+ const resourceKeys = resourceNames.map((name) => toCamelCase2(name));
1415
+ code += `/**
1416
+ `;
1417
+ code += ` * Type-safe API client
1418
+ `;
1419
+ code += ` */
1420
+ `;
1421
+ code += `export const ${apiName} = {
1422
+ `;
1423
+ for (let i = 0; i < resourceNames.length; i++) {
1424
+ const resourceName = resourceNames[i];
1425
+ const resourceKey = resourceKeys[i];
1426
+ code += ` ${resourceKey}: ${resourceName}`;
1427
+ if (i < resourceNames.length - 1) {
1428
+ code += `,`;
1283
1429
  }
1284
- return stats;
1285
- } catch (error) {
1286
- codegenLogger.error(
1287
- "Generation failed",
1288
- error instanceof Error ? error : new Error(String(error))
1289
- );
1290
- return null;
1430
+ code += `
1431
+ `;
1291
1432
  }
1433
+ code += `} as const;
1434
+ `;
1435
+ return code;
1292
1436
  }
1293
- async function watchAndGenerate(options = {}) {
1294
- const cwd = process.cwd();
1295
- const routesDir = options.routesDir ?? join(cwd, "src", "server", "routes");
1296
- const outputPath = options.outputPath ?? join(cwd, "src", "lib", "api.ts");
1297
- const watchMode = options.watch !== false;
1298
- if (options.debug) {
1299
- codegenLogger.info("Contract Watcher Started", { routesDir, outputPath, watch: watchMode });
1300
- }
1301
- await generateOnce(options);
1302
- if (watchMode) {
1303
- let isGenerating = false;
1304
- let pendingRegeneration = false;
1305
- const watcher = watch(routesDir, {
1306
- ignored: /(^|[\/\\])\../,
1307
- // ignore dotfiles
1308
- persistent: true,
1309
- ignoreInitial: true,
1310
- awaitWriteFinish: {
1311
- stabilityThreshold: 100,
1312
- pollInterval: 50
1313
- }
1314
- });
1315
- const regenerate = async () => {
1316
- if (isGenerating) {
1317
- pendingRegeneration = true;
1318
- return;
1319
- }
1320
- isGenerating = true;
1321
- pendingRegeneration = false;
1322
- if (options.debug) {
1323
- codegenLogger.info("Contracts changed, regenerating...");
1324
- }
1325
- await generateOnce(options);
1326
- isGenerating = false;
1327
- if (pendingRegeneration) {
1328
- await regenerate();
1329
- }
1330
- };
1331
- watcher.on("add", regenerate).on("change", regenerate).on("unlink", regenerate);
1332
- process.on("SIGINT", () => {
1333
- watcher.close();
1334
- process.exit(0);
1335
- });
1336
- await new Promise(() => {
1337
- });
1437
+
1438
+ // src/codegen/built-in/contract/index.ts
1439
+ var contractLogger = logger.child("contract-gen");
1440
+ var DEFAULT_CONTRACTS_DIR = "src/lib/contracts";
1441
+ var DEFAULT_OUTPUT_PATH = "src/lib/api";
1442
+ var contractCache = null;
1443
+ function needsFullRegeneration(event) {
1444
+ if (event === "add" || event === "unlink") {
1445
+ return true;
1446
+ }
1447
+ if (!contractCache) {
1448
+ return true;
1338
1449
  }
1450
+ return false;
1339
1451
  }
1340
- if (import.meta.url === `file://${process.argv[1]}`) {
1341
- watchAndGenerate({ debug: true });
1452
+ function createClientOptions(contractsDir, outputPath, baseUrl, apiName) {
1453
+ return {
1454
+ routesDir: contractsDir,
1455
+ outputPath,
1456
+ baseUrl,
1457
+ apiName,
1458
+ includeTypes: true,
1459
+ includeJsDoc: true,
1460
+ splitByResource: true
1461
+ };
1342
1462
  }
1343
- var orchestratorLogger = logger.child("orchestrator");
1344
- var CodegenOrchestrator = class {
1345
- generators;
1346
- cwd;
1347
- debug;
1348
- isGenerating = false;
1349
- pendingRegenerations = /* @__PURE__ */ new Set();
1350
- constructor(options) {
1351
- this.generators = options.generators;
1352
- this.cwd = options.cwd ?? process.cwd();
1353
- this.debug = options.debug ?? false;
1463
+ function generateApiName(prefix) {
1464
+ if (!prefix || prefix === "/api") {
1465
+ return "api";
1354
1466
  }
1355
- /**
1356
- * Run all generators once
1357
- */
1358
- async generateAll() {
1359
- if (this.debug) {
1360
- orchestratorLogger.info("Running all generators", {
1361
- count: this.generators.length,
1362
- names: this.generators.map((g) => g.name)
1363
- });
1364
- }
1365
- for (const generator of this.generators) {
1366
- try {
1367
- const genOptions = {
1368
- cwd: this.cwd,
1369
- debug: this.debug
1370
- };
1371
- await generator.generate(genOptions);
1372
- if (this.debug) {
1373
- orchestratorLogger.info(`[${generator.name}] Generated successfully`);
1374
- }
1375
- } catch (error) {
1376
- const err = error instanceof Error ? error : new Error(String(error));
1377
- orchestratorLogger.error(`[${generator.name}] Generation failed`, err);
1378
- }
1379
- }
1467
+ const name = prefix.replace(/^\/[_-]?/, "").replace(/[_-]/g, "");
1468
+ if (!name) {
1469
+ return "api";
1380
1470
  }
1381
- /**
1382
- * Start watch mode
1383
- */
1384
- async watch() {
1385
- await this.generateAll();
1386
- const allPatterns = this.generators.flatMap((g) => g.watchPatterns);
1387
- if (allPatterns.length === 0) {
1388
- orchestratorLogger.warn("No watch patterns defined, exiting watch mode");
1389
- return;
1390
- }
1391
- const dirsToWatch = /* @__PURE__ */ new Set();
1392
- for (const pattern of allPatterns) {
1393
- const baseDir = pattern.split("**")[0].replace(/\/$/, "") || ".";
1394
- dirsToWatch.add(join(this.cwd, baseDir));
1395
- }
1396
- const watchDirs = Array.from(dirsToWatch);
1397
- if (this.debug) {
1398
- orchestratorLogger.info("Starting watch mode", {
1399
- patterns: allPatterns,
1400
- watchDirs,
1401
- cwd: this.cwd
1402
- });
1471
+ return `${name}Api`;
1472
+ }
1473
+ function readPrefixFromPackageJson(cwd) {
1474
+ try {
1475
+ const packageJsonPath = join(cwd, "package.json");
1476
+ if (!existsSync(packageJsonPath)) {
1477
+ return void 0;
1403
1478
  }
1404
- const watcher = watch(watchDirs, {
1405
- ignored: /(^|[\/\\])\../,
1406
- // ignore dotfiles
1407
- persistent: true,
1408
- ignoreInitial: true,
1409
- awaitWriteFinish: {
1410
- stabilityThreshold: 100,
1411
- pollInterval: 50
1412
- }
1413
- });
1414
- const handleChange = async (absolutePath, event) => {
1415
- const filePath = relative(this.cwd, absolutePath);
1416
- if (this.isGenerating) {
1417
- this.pendingRegenerations.add(absolutePath);
1418
- return;
1419
- }
1420
- this.isGenerating = true;
1421
- this.pendingRegenerations.clear();
1422
- if (this.debug) {
1423
- orchestratorLogger.info(`File ${event}`, { file: filePath });
1424
- }
1425
- for (const generator of this.generators) {
1426
- const matches = generator.watchPatterns.some(
1427
- (pattern) => mm.isMatch(filePath, pattern)
1428
- );
1429
- if (matches) {
1430
- try {
1431
- if (generator.onFileChange) {
1432
- await generator.onFileChange(filePath, event);
1433
- } else {
1434
- const genOptions = {
1435
- cwd: this.cwd,
1436
- debug: this.debug
1437
- };
1438
- await generator.generate(genOptions);
1439
- }
1440
- if (this.debug) {
1441
- orchestratorLogger.info(`[${generator.name}] Regenerated`);
1442
- }
1443
- } catch (error) {
1444
- const err = error instanceof Error ? error : new Error(String(error));
1445
- orchestratorLogger.error(`[${generator.name}] Regeneration failed`, err);
1446
- }
1447
- }
1448
- }
1449
- this.isGenerating = false;
1450
- if (this.pendingRegenerations.size > 0) {
1451
- const next = Array.from(this.pendingRegenerations)[0];
1452
- await handleChange(next, "change");
1453
- }
1454
- };
1455
- watcher.on("add", (path) => handleChange(path, "add")).on("change", (path) => handleChange(path, "change")).on("unlink", (path) => handleChange(path, "unlink"));
1456
- process.on("SIGINT", () => {
1457
- if (this.debug) {
1458
- orchestratorLogger.info("Shutting down watch mode");
1459
- }
1460
- watcher.close();
1461
- process.exit(0);
1462
- });
1463
- await new Promise(() => {
1464
- });
1479
+ const content = readFileSync(packageJsonPath, "utf-8");
1480
+ const packageJson = JSON.parse(content);
1481
+ return packageJson.spfn?.prefix;
1482
+ } catch (error) {
1483
+ return void 0;
1465
1484
  }
1466
- };
1467
- var contractLogger = logger.child("contract-gen");
1485
+ }
1468
1486
  function createContractGenerator(config = {}) {
1487
+ const contractsDir = config.contractsDir ?? DEFAULT_CONTRACTS_DIR;
1488
+ const outputPath = config.outputPath ?? DEFAULT_OUTPUT_PATH;
1489
+ const runOn = config.runOn ?? ["watch", "manual", "build"];
1469
1490
  return {
1470
1491
  name: "contract",
1471
1492
  watchPatterns: [
1472
- "src/lib/contracts/**/*.ts"
1493
+ `${contractsDir}/**/*.ts`
1473
1494
  ],
1495
+ runOn,
1474
1496
  async generate(options) {
1475
1497
  const cwd = options.cwd;
1476
- const libContractsDir = join(cwd, "src", "lib", "contracts");
1477
- const outputPath = config.outputPath ?? join(cwd, "src", "lib", "api.ts");
1498
+ const fullContractsDir = join(cwd, contractsDir);
1499
+ const fullOutputPath = join(cwd, outputPath);
1500
+ const prefix = readPrefixFromPackageJson(cwd);
1501
+ const apiName = generateApiName(prefix);
1478
1502
  try {
1479
- if (!existsSync(libContractsDir)) {
1503
+ if (!existsSync(fullContractsDir)) {
1480
1504
  if (options.debug) {
1481
- contractLogger.warn("No contracts directory found at src/lib/contracts");
1505
+ contractLogger.warn(`No contracts directory found at ${contractsDir}`);
1482
1506
  }
1483
1507
  return;
1484
1508
  }
1485
- const allContracts = await scanContracts(libContractsDir);
1509
+ const changedFile = options.trigger?.changedFile;
1510
+ if (changedFile && !needsFullRegeneration(changedFile.event)) {
1511
+ if (options.debug) {
1512
+ contractLogger.info("Attempting incremental update", {
1513
+ file: changedFile.path,
1514
+ event: changedFile.event
1515
+ });
1516
+ }
1517
+ const success = await attemptIncrementalUpdate({
1518
+ cwd,
1519
+ contractsDir: fullContractsDir,
1520
+ outputPath: fullOutputPath,
1521
+ changedFilePath: changedFile.path,
1522
+ baseUrl: config.baseUrl,
1523
+ apiName,
1524
+ debug: options.debug
1525
+ });
1526
+ if (success) {
1527
+ if (options.debug) {
1528
+ contractLogger.info("Incremental update successful");
1529
+ }
1530
+ return;
1531
+ }
1532
+ if (options.debug) {
1533
+ contractLogger.info("Incremental update failed, doing full regen");
1534
+ }
1535
+ }
1536
+ const allContracts = await scanContracts(fullContractsDir, prefix);
1486
1537
  if (allContracts.length === 0) {
1487
1538
  if (options.debug) {
1488
1539
  contractLogger.warn("No contracts found");
1489
1540
  }
1541
+ contractCache = null;
1490
1542
  return;
1491
1543
  }
1492
- const stats = await generateClient(allContracts, {
1493
- outputPath,
1494
- includeTypes: true,
1495
- includeJsDoc: true});
1544
+ const clientOptions = createClientOptions(fullContractsDir, fullOutputPath, config.baseUrl, apiName);
1545
+ const stats = await generateClient(allContracts, clientOptions);
1546
+ contractCache = {
1547
+ contracts: allContracts,
1548
+ lastScan: Date.now()
1549
+ };
1496
1550
  if (options.debug) {
1497
1551
  contractLogger.info("Client generated", {
1498
1552
  endpoints: stats.methodsGenerated,
1499
1553
  resources: stats.resourcesGenerated,
1500
- duration: stats.duration
1554
+ duration: stats.duration,
1555
+ mode: changedFile ? "incremental-fallback" : "full"
1501
1556
  });
1502
1557
  }
1503
1558
  } catch (error) {
@@ -1508,6 +1563,75 @@ function createContractGenerator(config = {}) {
1508
1563
  }
1509
1564
  };
1510
1565
  }
1566
+ async function attemptIncrementalUpdate(options) {
1567
+ const { cwd, contractsDir, outputPath, changedFilePath, baseUrl, apiName, debug } = options;
1568
+ if (!contractCache) {
1569
+ return false;
1570
+ }
1571
+ try {
1572
+ const fullPath = join(cwd, changedFilePath);
1573
+ if (!existsSync(fullPath)) {
1574
+ return false;
1575
+ }
1576
+ const updatedContracts = await scanContracts(contractsDir);
1577
+ if (updatedContracts.length === 0) {
1578
+ contractCache = null;
1579
+ return false;
1580
+ }
1581
+ const changedContracts = findChangedContracts(
1582
+ contractCache.contracts,
1583
+ updatedContracts,
1584
+ changedFilePath
1585
+ );
1586
+ if (changedContracts.size === 0) {
1587
+ if (debug) {
1588
+ contractLogger.info("No contract changes detected, skipping regeneration");
1589
+ }
1590
+ return true;
1591
+ }
1592
+ const clientOptions = createClientOptions(contractsDir, outputPath, baseUrl, apiName);
1593
+ const stats = await generateClient(updatedContracts, clientOptions);
1594
+ contractCache = {
1595
+ contracts: updatedContracts,
1596
+ lastScan: Date.now()
1597
+ };
1598
+ if (debug) {
1599
+ contractLogger.info("Incremental update successful", {
1600
+ changedContracts: changedContracts.size,
1601
+ endpoints: stats.methodsGenerated,
1602
+ resources: stats.resourcesGenerated,
1603
+ duration: stats.duration
1604
+ });
1605
+ }
1606
+ return true;
1607
+ } catch (error) {
1608
+ if (debug) {
1609
+ const err = error instanceof Error ? error : new Error(String(error));
1610
+ contractLogger.warn("Incremental update failed", err);
1611
+ }
1612
+ return false;
1613
+ }
1614
+ }
1615
+ function findChangedContracts(oldContracts, newContracts, changedFilePath) {
1616
+ const changed = /* @__PURE__ */ new Set();
1617
+ const oldInFile = oldContracts.filter((c) => c.contractFile?.includes(changedFilePath));
1618
+ const newInFile = newContracts.filter((c) => c.contractFile?.includes(changedFilePath));
1619
+ if (oldInFile.length !== newInFile.length) {
1620
+ newInFile.forEach((c) => changed.add(c.contractName));
1621
+ return changed;
1622
+ }
1623
+ for (const newContract of newInFile) {
1624
+ const oldContract = oldInFile.find((c) => c.contractName === newContract.contractName);
1625
+ if (!oldContract) {
1626
+ changed.add(newContract.contractName);
1627
+ continue;
1628
+ }
1629
+ if (oldContract.method !== newContract.method || oldContract.path !== newContract.path || oldContract.hasQuery !== newContract.hasQuery || oldContract.hasBody !== newContract.hasBody || oldContract.hasParams !== newContract.hasParams) {
1630
+ changed.add(newContract.contractName);
1631
+ }
1632
+ }
1633
+ return changed;
1634
+ }
1511
1635
  var configLogger = logger.child("config");
1512
1636
  function loadCodegenConfig(cwd) {
1513
1637
  const rcPath = join(cwd, ".spfnrc.json");
@@ -1627,6 +1751,6 @@ async function createGeneratorsFromConfig(config, cwd) {
1627
1751
  return generators;
1628
1752
  }
1629
1753
 
1630
- export { CodegenOrchestrator, createContractGenerator, createGeneratorsFromConfig, generateClient, groupByResource, loadCodegenConfig, scanContracts, watchAndGenerate };
1754
+ export { CodegenOrchestrator, createContractGenerator, createGeneratorsFromConfig, loadCodegenConfig };
1631
1755
  //# sourceMappingURL=index.js.map
1632
1756
  //# sourceMappingURL=index.js.map