@spfn/core 0.1.0-alpha.65 → 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.
@@ -1,941 +1,431 @@
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, dirname, 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/scanners/contract-scanner.ts
11
- async function scanContracts(contractsDir) {
12
- const contractFiles = await scanContractFiles(contractsDir);
13
- const mappings = [];
14
- const packagePrefix = getPackagePrefix(contractsDir);
15
- for (let i = 0; i < contractFiles.length; i++) {
16
- const filePath = contractFiles[i];
17
- const exports = extractContractExports(filePath);
18
- for (let j = 0; j < exports.length; j++) {
19
- const contractExport = exports[j];
20
- if (!contractExport.path.startsWith("/")) {
21
- throw new Error(
22
- `Contract '${contractExport.name}' in ${filePath} must use absolute path. Found: '${contractExport.path}'. Use '/your-path' instead.`
23
- );
24
- }
25
- const finalPath = packagePrefix ? `${packagePrefix}${contractExport.path}` : contractExport.path;
26
- mappings.push({
27
- method: contractExport.method,
28
- path: finalPath,
29
- contractName: contractExport.name,
30
- contractImportPath: getImportPath(filePath),
31
- routeFile: "",
32
- contractFile: filePath,
33
- hasQuery: contractExport.hasQuery,
34
- hasBody: contractExport.hasBody,
35
- hasParams: contractExport.hasParams
36
- });
37
- }
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
+ });
38
19
  }
39
- return mappings;
40
- }
41
- async function scanContractFiles(dir, files = []) {
42
- try {
43
- const entries = await readdir(dir);
44
- for (let i = 0; i < entries.length; i++) {
45
- const entry = entries[i];
46
- const fullPath = join(dir, entry);
47
- const fileStat = await stat(fullPath);
48
- if (fileStat.isDirectory()) {
49
- await scanContractFiles(fullPath, files);
50
- } else {
51
- 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")) {
52
- files.push(fullPath);
53
- }
54
- }
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);
55
36
  }
56
- } catch (error) {
57
37
  }
58
- return files;
59
- }
60
- function extractContractExports(filePath) {
61
- const sourceCode = readFileSync(filePath, "utf-8");
62
- const sourceFile = ts.createSourceFile(
63
- filePath,
64
- sourceCode,
65
- ts.ScriptTarget.Latest,
66
- true
67
- );
68
- const exports = [];
69
- function visit(node) {
70
- if (ts.isVariableStatement(node)) {
71
- const hasExport = node.modifiers?.some(
72
- (m) => m.kind === ts.SyntaxKind.ExportKeyword
73
- );
74
- if (hasExport && node.declarationList.declarations.length > 0) {
75
- const declaration = node.declarationList.declarations[0];
76
- if (ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name) && declaration.initializer) {
77
- const name = declaration.name.text;
78
- const hasSatisfiesRouteContract = checkSatisfiesRouteContract(declaration.initializer);
79
- if (hasSatisfiesRouteContract) {
80
- const objectLiteral = extractObjectLiteral(declaration.initializer);
81
- if (objectLiteral) {
82
- const contractData = extractContractData(objectLiteral);
83
- if (contractData.method && contractData.path) {
84
- exports.push({
85
- name,
86
- method: contractData.method,
87
- path: contractData.path,
88
- hasQuery: contractData.hasQuery,
89
- hasBody: contractData.hasBody,
90
- hasParams: contractData.hasParams
91
- });
92
- }
93
- }
94
- return;
95
- }
96
- if (isContractName(name)) {
97
- const objectLiteral = extractObjectLiteral(declaration.initializer);
98
- if (objectLiteral) {
99
- const contractData = extractContractData(objectLiteral);
100
- if (contractData.method && contractData.path) {
101
- exports.push({
102
- name,
103
- method: contractData.method,
104
- path: contractData.path,
105
- hasQuery: contractData.hasQuery,
106
- hasBody: contractData.hasBody,
107
- hasParams: contractData.hasParams
108
- });
109
- }
110
- }
111
- }
112
- }
113
- }
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);
114
43
  }
115
- ts.forEachChild(node, visit);
116
44
  }
117
- visit(sourceFile);
118
- return exports;
119
- }
120
- function checkSatisfiesRouteContract(initializer) {
121
- if (!ts.isSatisfiesExpression(initializer)) {
122
- 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
+ }
123
51
  }
124
- const typeNode = initializer.type;
125
- if (ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
126
- return typeNode.typeName.text === "RouteContract";
52
+ async close() {
127
53
  }
128
- 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));
129
98
  }
130
- function extractObjectLiteral(initializer) {
131
- if (ts.isObjectLiteralExpression(initializer)) {
132
- return initializer;
133
- }
134
- if (ts.isSatisfiesExpression(initializer)) {
135
- return extractObjectLiteral(initializer.expression);
99
+ function maskSensitiveData(data) {
100
+ if (data === null || data === void 0) {
101
+ return data;
136
102
  }
137
- if (ts.isAsExpression(initializer)) {
138
- return extractObjectLiteral(initializer.expression);
103
+ if (Array.isArray(data)) {
104
+ return data.map((item) => maskSensitiveData(item));
139
105
  }
140
- return void 0;
141
- }
142
- function extractContractData(objectLiteral) {
143
- const result = {};
144
- for (let i = 0; i < objectLiteral.properties.length; i++) {
145
- const prop = objectLiteral.properties[i];
146
- if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
147
- const propName = prop.name.text;
148
- if (propName === "method") {
149
- let value;
150
- if (ts.isStringLiteral(prop.initializer)) {
151
- value = prop.initializer.text;
152
- } else if (ts.isAsExpression(prop.initializer) && ts.isStringLiteral(prop.initializer.expression)) {
153
- value = prop.initializer.expression.text;
154
- }
155
- if (value) result.method = value;
156
- } else if (propName === "path") {
157
- let value;
158
- if (ts.isStringLiteral(prop.initializer)) {
159
- value = prop.initializer.text;
160
- } else if (ts.isAsExpression(prop.initializer) && ts.isStringLiteral(prop.initializer.expression)) {
161
- value = prop.initializer.expression.text;
162
- }
163
- if (value) result.path = value;
164
- } else if (propName === "query") {
165
- result.hasQuery = true;
166
- } else if (propName === "body") {
167
- result.hasBody = true;
168
- } else if (propName === "params") {
169
- 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;
170
115
  }
171
116
  }
117
+ return masked;
172
118
  }
173
- return result;
174
- }
175
- function isContractName(name) {
176
- return name.indexOf("Contract") !== -1 || name.indexOf("contract") !== -1 || name.endsWith("Schema") || name.endsWith("schema");
119
+ return data;
177
120
  }
178
- function getPackagePrefix(contractsDir) {
179
- try {
180
- let currentDir = dirname(contractsDir);
181
- for (let i = 0; i < 5; i++) {
182
- const packageJsonPath = join(currentDir, "package.json");
183
- try {
184
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
185
- if (packageJson.spfn?.prefix) {
186
- return packageJson.spfn.prefix;
187
- }
188
- } catch {
189
- }
190
- currentDir = dirname(currentDir);
191
- }
192
- } catch {
193
- }
194
- return "";
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();
195
141
  }
196
- function getImportPath(filePath) {
197
- const srcIndex = filePath.indexOf("/src/");
198
- if (srcIndex === -1) {
199
- throw new Error(`Cannot determine import path for ${filePath}: /src/ directory not found`);
200
- }
201
- let cleanPath = filePath.substring(srcIndex + 5);
202
- if (cleanPath.endsWith(".ts")) {
203
- cleanPath = cleanPath.slice(0, -3);
204
- } else if (cleanPath.endsWith(".js")) {
205
- cleanPath = cleanPath.slice(0, -3);
206
- } else if (cleanPath.endsWith(".mjs")) {
207
- cleanPath = cleanPath.slice(0, -4);
208
- }
209
- return "@/" + cleanPath;
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}`;
210
151
  }
211
-
212
- // src/codegen/scanners/route-scanner.ts
213
- function groupByResource(mappings) {
214
- const grouped = {};
215
- for (let i = 0; i < mappings.length; i++) {
216
- const mapping = mappings[i];
217
- const resource = extractResourceName(mapping.path);
218
- if (!grouped[resource]) {
219
- grouped[resource] = [];
220
- }
221
- grouped[resource].push(mapping);
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);
222
158
  }
223
- return grouped;
159
+ return lines.join("\n");
224
160
  }
225
- function extractResourceName(path) {
226
- const segments = path.slice(1).split("/").filter((s) => s && s !== "*");
227
- const staticSegments = [];
228
- for (let i = 0; i < segments.length; i++) {
229
- const seg = segments[i];
230
- if (!seg.startsWith(":")) {
231
- staticSegments.push(seg);
232
- }
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}]`);
233
168
  }
234
- if (staticSegments.length === 0) {
235
- 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
+ }
236
175
  }
237
- const first = toCamelCase(staticSegments[0], false);
238
- if (staticSegments.length === 1) {
239
- 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
+ });
240
185
  }
241
- const result = [first];
242
- for (let i = 1; i < staticSegments.length; i++) {
243
- const seg = staticSegments[i];
244
- 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}):`);
245
192
  }
246
- return result.join("");
247
- }
248
- function toCamelCase(str, capitalize2) {
249
- const parts = str.split(/[-_]/);
250
- if (parts.length === 1) {
251
- 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);
252
197
  }
253
- const result = [];
254
- for (let i = 0; i < parts.length; i++) {
255
- const part = parts[i];
256
- if (i === 0 && !capitalize2) {
257
- result.push(part);
258
- } else {
259
- result.push(part.charAt(0).toUpperCase() + part.slice(1));
260
- }
198
+ let output = parts.join(" ");
199
+ if (metadata.error) {
200
+ output += "\n" + formatError(metadata.error);
261
201
  }
262
- return result.join("");
202
+ return output;
263
203
  }
264
- async function generateClient(mappings, options) {
265
- const startTime = Date.now();
266
- const grouped = groupByResource(mappings);
267
- const resourceNames = Object.keys(grouped);
268
- await generateSplitClient(mappings, grouped, options);
269
- return {
270
- routesScanned: mappings.length,
271
- contractsFound: mappings.length,
272
- contractFiles: countUniqueContractFiles(mappings),
273
- resourcesGenerated: resourceNames.length,
274
- methodsGenerated: mappings.length,
275
- duration: Date.now() - startTime
204
+ function formatJSON(metadata) {
205
+ const obj = {
206
+ timestamp: formatTimestamp(metadata.timestamp),
207
+ level: metadata.level,
208
+ message: metadata.message
276
209
  };
277
- }
278
- function generateHeader() {
279
- return `/**
280
- * Auto-generated API Client
281
- *
282
- * Generated by @spfn/core codegen
283
- * DO NOT EDIT MANUALLY
284
- *
285
- * @generated ${(/* @__PURE__ */ new Date()).toISOString()}
286
- */
287
-
288
- `;
289
- }
290
- function groupContractsByImportPath(mappings) {
291
- const groups = {};
292
- for (let i = 0; i < mappings.length; i++) {
293
- const mapping = mappings[i];
294
- const path = mapping.contractImportPath;
295
- if (!groups[path]) {
296
- groups[path] = /* @__PURE__ */ new Set();
297
- }
298
- groups[path].add(mapping.contractName);
299
- }
300
- const result = {};
301
- const keys = Object.keys(groups);
302
- for (let i = 0; i < keys.length; i++) {
303
- const key = keys[i];
304
- result[key] = Array.from(groups[key]);
210
+ if (metadata.module) {
211
+ obj.module = metadata.module;
305
212
  }
306
- return result;
307
- }
308
- function generateTypeName(mapping) {
309
- let name = mapping.contractName;
310
- if (name.endsWith("Contract")) {
311
- name = name.slice(0, -8);
213
+ if (metadata.context) {
214
+ obj.context = metadata.context;
312
215
  }
313
- if (name.length > 0) {
314
- 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
+ };
315
222
  }
316
- return name;
223
+ return JSON.stringify(obj);
317
224
  }
318
- function generateMethodCode(mapping, options) {
319
- const methodName = generateMethodName(mapping);
320
- const hasParams = mapping.hasParams || mapping.path.includes(":");
321
- const hasQuery = mapping.hasQuery || false;
322
- const hasBody = mapping.hasBody || false;
323
- let code = "";
324
- if (options.includeJsDoc !== false) {
325
- code += ` /**
326
- `;
327
- code += ` * ${mapping.method} ${mapping.path}
328
- `;
329
- code += ` */
330
- `;
331
- }
332
- code += ` ${methodName}: (`;
333
- const params = [];
334
- const typeName = generateTypeName(mapping);
335
- if (hasParams) {
336
- 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;
337
233
  }
338
- if (hasQuery) {
339
- params.push(`query?: ${typeName}Query`);
234
+ /**
235
+ * Get current log level
236
+ */
237
+ get level() {
238
+ return this.config.level;
340
239
  }
341
- if (hasBody) {
342
- 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
+ });
343
248
  }
344
- if (params.length > 0) {
345
- code += `options: { ${params.join(", ")} }`;
249
+ /**
250
+ * Debug log
251
+ */
252
+ debug(message, context) {
253
+ this.log("debug", message, void 0, context);
346
254
  }
347
- code += `) => `;
348
- code += `client.call(${mapping.contractName}`;
349
- if (params.length > 0) {
350
- code += `, options`;
351
- }
352
- code += `),
353
- `;
354
- return code;
355
- }
356
- function generateMethodName(mapping) {
357
- const method = mapping.method.toLowerCase();
358
- if (mapping.path === "/" || mapping.path.match(/^\/[\w-]+$/)) {
359
- if (method === "get") {
360
- return "list";
361
- }
362
- if (method === "post") {
363
- return "create";
364
- }
365
- }
366
- if (mapping.path.includes(":")) {
367
- if (method === "get") {
368
- return "getById";
369
- }
370
- if (method === "put" || method === "patch") {
371
- return "update";
372
- }
373
- if (method === "delete") {
374
- return "delete";
375
- }
376
- }
377
- return method;
378
- }
379
- function countUniqueContractFiles(mappings) {
380
- const files = /* @__PURE__ */ new Set();
381
- for (let i = 0; i < mappings.length; i++) {
382
- if (mappings[i].contractFile) {
383
- files.add(mappings[i].contractFile);
384
- }
385
- }
386
- return files.size;
387
- }
388
- async function generateSplitClient(_mappings, grouped, options) {
389
- const outputPath = options.outputPath;
390
- const outputDir = outputPath.endsWith(".ts") || outputPath.endsWith(".js") ? outputPath.replace(/\.[jt]s$/, "") : outputPath;
391
- await mkdir(outputDir, { recursive: true });
392
- const resourceNames = Object.keys(grouped);
393
- for (let i = 0; i < resourceNames.length; i++) {
394
- const resourceName = resourceNames[i];
395
- const routes = grouped[resourceName];
396
- const code = generateResourceFile(resourceName, routes, options);
397
- const filePath = `${outputDir}/${resourceName}.ts`;
398
- await writeFile(filePath, code, "utf-8");
399
- }
400
- const indexCode = generateIndexFile(resourceNames, options);
401
- const indexPath = `${outputDir}/index.ts`;
402
- await writeFile(indexPath, indexCode, "utf-8");
403
- }
404
- function generateResourceFile(resourceName, routes, options) {
405
- let code = "";
406
- code += generateHeader();
407
- code += `import { client } from '@spfn/core/client';
408
- `;
409
- if (options.includeTypes !== false) {
410
- code += `import type { InferContract } from '@spfn/core';
411
- `;
412
- }
413
- code += `
414
- `;
415
- const importGroups = groupContractsByImportPath(routes);
416
- const importPaths = Object.keys(importGroups);
417
- for (let i = 0; i < importPaths.length; i++) {
418
- const importPath = importPaths[i];
419
- const contracts = importGroups[importPath];
420
- code += `import { ${contracts.join(", ")} } from '${importPath}';
421
- `;
422
- }
423
- code += `
424
- `;
425
- if (options.includeTypes !== false) {
426
- code += `// ============================================
427
- `;
428
- code += `// Types
429
- `;
430
- code += `// ============================================
431
-
432
- `;
433
- for (let i = 0; i < routes.length; i++) {
434
- const route = routes[i];
435
- const typeName = generateTypeName(route);
436
- const contractType = `typeof ${route.contractName}`;
437
- code += `export type ${typeName}Response = InferContract<${contractType}>['response'];
438
- `;
439
- if (route.hasQuery) {
440
- code += `export type ${typeName}Query = InferContract<${contractType}>['query'];
441
- `;
442
- }
443
- if (route.hasParams || route.path.includes(":")) {
444
- code += `export type ${typeName}Params = InferContract<${contractType}>['params'];
445
- `;
446
- }
447
- if (route.hasBody) {
448
- code += `export type ${typeName}Body = InferContract<${contractType}>['body'];
449
- `;
450
- }
451
- code += `
452
- `;
453
- }
454
- }
455
- code += `/**
456
- `;
457
- code += ` * ${resourceName} API
458
- `;
459
- code += ` */
460
- `;
461
- code += `export const ${resourceName} = {
462
- `;
463
- for (let i = 0; i < routes.length; i++) {
464
- const route = routes[i];
465
- code += generateMethodCode(route, options);
466
- }
467
- code += `} as const;
468
- `;
469
- return code;
470
- }
471
- function toCamelCase2(str) {
472
- if (str.length === 0) {
473
- return str;
474
- }
475
- return str.charAt(0).toLowerCase() + str.slice(1);
476
- }
477
- function generateIndexFile(resourceNames, options) {
478
- let code = "";
479
- const apiName = options.apiName || "api";
480
- code += generateHeader();
481
- code += `export { client } from '@spfn/core/client';
482
-
483
- `;
484
- for (let i = 0; i < resourceNames.length; i++) {
485
- const resourceName = resourceNames[i];
486
- code += `export * from './${resourceName}.js';
487
- `;
488
- }
489
- code += `
490
- `;
491
- for (let i = 0; i < resourceNames.length; i++) {
492
- const resourceName = resourceNames[i];
493
- code += `import { ${resourceName} } from './${resourceName}.js';
494
- `;
495
- }
496
- code += `
497
- `;
498
- const resourceKeys = resourceNames.map((name) => toCamelCase2(name));
499
- code += `/**
500
- `;
501
- code += ` * Type-safe API client
502
- `;
503
- code += ` */
504
- `;
505
- code += `export const ${apiName} = {
506
- `;
507
- for (let i = 0; i < resourceNames.length; i++) {
508
- const resourceName = resourceNames[i];
509
- const resourceKey = resourceKeys[i];
510
- code += ` ${resourceKey}: ${resourceName}`;
511
- if (i < resourceNames.length - 1) {
512
- code += `,`;
513
- }
514
- code += `
515
- `;
516
- }
517
- code += `} as const;
518
- `;
519
- return code;
520
- }
521
- var PinoAdapter = class _PinoAdapter {
522
- logger;
523
- constructor(config) {
524
- this.logger = pino({
525
- level: config.level,
526
- // 기본 필드
527
- base: config.module ? { module: config.module } : void 0
528
- });
529
- }
530
- child(module) {
531
- const childLogger = new _PinoAdapter({ level: this.logger.level, module });
532
- childLogger.logger = this.logger.child({ module });
533
- return childLogger;
534
- }
535
- debug(message, context) {
536
- this.logger.debug(context || {}, message);
537
- }
538
- info(message, context) {
539
- this.logger.info(context || {}, message);
255
+ /**
256
+ * Info log
257
+ */
258
+ info(message, context) {
259
+ this.log("info", message, void 0, context);
540
260
  }
541
261
  warn(message, errorOrContext, context) {
542
262
  if (errorOrContext instanceof Error) {
543
- this.logger.warn({ err: errorOrContext, ...context }, message);
263
+ this.log("warn", message, errorOrContext, context);
544
264
  } else {
545
- this.logger.warn(errorOrContext || {}, message);
265
+ this.log("warn", message, void 0, errorOrContext);
546
266
  }
547
267
  }
548
268
  error(message, errorOrContext, context) {
549
269
  if (errorOrContext instanceof Error) {
550
- this.logger.error({ err: errorOrContext, ...context }, message);
270
+ this.log("error", message, errorOrContext, context);
551
271
  } else {
552
- this.logger.error(errorOrContext || {}, message);
272
+ this.log("error", message, void 0, errorOrContext);
553
273
  }
554
274
  }
555
275
  fatal(message, errorOrContext, context) {
556
276
  if (errorOrContext instanceof Error) {
557
- this.logger.fatal({ err: errorOrContext, ...context }, message);
277
+ this.log("fatal", message, errorOrContext, context);
558
278
  } else {
559
- this.logger.fatal(errorOrContext || {}, message);
279
+ this.log("fatal", message, void 0, errorOrContext);
280
+ }
281
+ }
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);
299
+ }
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
+ });
310
+ }
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
+ `);
560
321
  }
561
322
  }
323
+ /**
324
+ * Close all Transports
325
+ */
562
326
  async close() {
327
+ const closePromises = this.config.transports.filter((transport) => transport.close).map((transport) => transport.close());
328
+ await Promise.all(closePromises);
563
329
  }
564
330
  };
565
331
 
566
- // src/logger/types.ts
567
- var LOG_LEVEL_PRIORITY = {
568
- debug: 0,
569
- info: 1,
570
- warn: 2,
571
- error: 3,
572
- fatal: 4
573
- };
574
-
575
- // src/logger/formatters.ts
576
- var SENSITIVE_KEYS = [
577
- "password",
578
- "passwd",
579
- "pwd",
580
- "secret",
581
- "token",
582
- "apikey",
583
- "api_key",
584
- "accesstoken",
585
- "access_token",
586
- "refreshtoken",
587
- "refresh_token",
588
- "authorization",
589
- "auth",
590
- "cookie",
591
- "session",
592
- "sessionid",
593
- "session_id",
594
- "privatekey",
595
- "private_key",
596
- "creditcard",
597
- "credit_card",
598
- "cardnumber",
599
- "card_number",
600
- "cvv",
601
- "ssn",
602
- "pin"
603
- ];
604
- var MASKED_VALUE = "***MASKED***";
605
- function isSensitiveKey(key) {
606
- const lowerKey = key.toLowerCase();
607
- return SENSITIVE_KEYS.some((sensitive) => lowerKey.includes(sensitive));
608
- }
609
- function maskSensitiveData(data) {
610
- if (data === null || data === void 0) {
611
- return data;
612
- }
613
- if (Array.isArray(data)) {
614
- return data.map((item) => maskSensitiveData(item));
332
+ // src/logger/transports/console.ts
333
+ var ConsoleTransport = class {
334
+ name = "console";
335
+ level;
336
+ enabled;
337
+ colorize;
338
+ constructor(config) {
339
+ this.level = config.level;
340
+ this.enabled = config.enabled;
341
+ this.colorize = config.colorize ?? true;
615
342
  }
616
- if (typeof data === "object") {
617
- const masked = {};
618
- for (const [key, value] of Object.entries(data)) {
619
- if (isSensitiveKey(key)) {
620
- masked[key] = MASKED_VALUE;
621
- } else if (typeof value === "object" && value !== null) {
622
- masked[key] = maskSensitiveData(value);
623
- } else {
624
- masked[key] = value;
625
- }
343
+ async log(metadata) {
344
+ if (!this.enabled) {
345
+ return;
626
346
  }
627
- return masked;
628
- }
629
- return data;
630
- }
631
- var COLORS = {
632
- reset: "\x1B[0m",
633
- bright: "\x1B[1m",
634
- dim: "\x1B[2m",
635
- // 로그 레벨 컬러
636
- debug: "\x1B[36m",
637
- // cyan
638
- info: "\x1B[32m",
639
- // green
640
- warn: "\x1B[33m",
641
- // yellow
642
- error: "\x1B[31m",
643
- // red
644
- fatal: "\x1B[35m",
645
- // magenta
646
- // 추가 컬러
647
- gray: "\x1B[90m"
648
- };
649
- function formatTimestamp(date) {
650
- return date.toISOString();
651
- }
652
- function formatTimestampHuman(date) {
653
- const year = date.getFullYear();
654
- const month = String(date.getMonth() + 1).padStart(2, "0");
655
- const day = String(date.getDate()).padStart(2, "0");
656
- const hours = String(date.getHours()).padStart(2, "0");
657
- const minutes = String(date.getMinutes()).padStart(2, "0");
658
- const seconds = String(date.getSeconds()).padStart(2, "0");
659
- const ms = String(date.getMilliseconds()).padStart(3, "0");
660
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
661
- }
662
- function formatError(error) {
663
- const lines = [];
664
- lines.push(`${error.name}: ${error.message}`);
665
- if (error.stack) {
666
- const stackLines = error.stack.split("\n").slice(1);
667
- lines.push(...stackLines);
668
- }
669
- return lines.join("\n");
670
- }
671
- function formatConsole(metadata, colorize = true) {
672
- const parts = [];
673
- const timestamp = formatTimestampHuman(metadata.timestamp);
674
- if (colorize) {
675
- parts.push(`${COLORS.gray}[${timestamp}]${COLORS.reset}`);
676
- } else {
677
- parts.push(`[${timestamp}]`);
678
- }
679
- if (metadata.module) {
680
- if (colorize) {
681
- parts.push(`${COLORS.dim}[module=${metadata.module}]${COLORS.reset}`);
347
+ if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
348
+ return;
349
+ }
350
+ const message = formatConsole(metadata, this.colorize);
351
+ if (metadata.level === "warn" || metadata.level === "error" || metadata.level === "fatal") {
352
+ console.error(message);
682
353
  } else {
683
- parts.push(`[module=${metadata.module}]`);
354
+ console.log(message);
684
355
  }
685
356
  }
686
- if (metadata.context && Object.keys(metadata.context).length > 0) {
687
- Object.entries(metadata.context).forEach(([key, value]) => {
688
- const valueStr = typeof value === "string" ? value : String(value);
689
- if (colorize) {
690
- parts.push(`${COLORS.dim}[${key}=${valueStr}]${COLORS.reset}`);
691
- } else {
692
- parts.push(`[${key}=${valueStr}]`);
693
- }
694
- });
695
- }
696
- const levelStr = metadata.level.toUpperCase();
697
- if (colorize) {
698
- const color = COLORS[metadata.level];
699
- parts.push(`${color}(${levelStr})${COLORS.reset}:`);
700
- } else {
701
- parts.push(`(${levelStr}):`);
702
- }
703
- if (colorize) {
704
- parts.push(`${COLORS.bright}${metadata.message}${COLORS.reset}`);
705
- } else {
706
- parts.push(metadata.message);
707
- }
708
- let output = parts.join(" ");
709
- if (metadata.error) {
710
- output += "\n" + formatError(metadata.error);
711
- }
712
- return output;
713
- }
714
- function formatJSON(metadata) {
715
- const obj = {
716
- timestamp: formatTimestamp(metadata.timestamp),
717
- level: metadata.level,
718
- message: metadata.message
719
- };
720
- if (metadata.module) {
721
- obj.module = metadata.module;
722
- }
723
- if (metadata.context) {
724
- obj.context = metadata.context;
725
- }
726
- if (metadata.error) {
727
- obj.error = {
728
- name: metadata.error.name,
729
- message: metadata.error.message,
730
- stack: metadata.error.stack
731
- };
732
- }
733
- return JSON.stringify(obj);
734
- }
735
-
736
- // src/logger/logger.ts
737
- var Logger = class _Logger {
738
- config;
739
- module;
357
+ };
358
+ var FileTransport = class {
359
+ name = "file";
360
+ level;
361
+ enabled;
362
+ logDir;
363
+ maxFileSize;
364
+ maxFiles;
365
+ currentStream = null;
366
+ currentFilename = null;
740
367
  constructor(config) {
741
- this.config = config;
742
- this.module = config.module;
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
+ }
743
376
  }
744
- /**
745
- * Get current log level
746
- */
747
- get level() {
748
- return this.config.level;
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
+ }
749
405
  }
750
406
  /**
751
- * Create child logger (per module)
407
+ * 스트림 교체 (날짜 변경 시)
752
408
  */
753
- child(module) {
754
- return new _Logger({
755
- ...this.config,
756
- module
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;
757
425
  });
758
426
  }
759
427
  /**
760
- * Debug log
761
- */
762
- debug(message, context) {
763
- this.log("debug", message, void 0, context);
764
- }
765
- /**
766
- * Info log
767
- */
768
- info(message, context) {
769
- this.log("info", message, void 0, context);
770
- }
771
- warn(message, errorOrContext, context) {
772
- if (errorOrContext instanceof Error) {
773
- this.log("warn", message, errorOrContext, context);
774
- } else {
775
- this.log("warn", message, void 0, errorOrContext);
776
- }
777
- }
778
- error(message, errorOrContext, context) {
779
- if (errorOrContext instanceof Error) {
780
- this.log("error", message, errorOrContext, context);
781
- } else {
782
- this.log("error", message, void 0, errorOrContext);
783
- }
784
- }
785
- fatal(message, errorOrContext, context) {
786
- if (errorOrContext instanceof Error) {
787
- this.log("fatal", message, errorOrContext, context);
788
- } else {
789
- this.log("fatal", message, void 0, errorOrContext);
790
- }
791
- }
792
- /**
793
- * Log processing (internal)
794
- */
795
- log(level, message, error, context) {
796
- if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[this.config.level]) {
797
- return;
798
- }
799
- const metadata = {
800
- timestamp: /* @__PURE__ */ new Date(),
801
- level,
802
- message,
803
- module: this.module,
804
- error,
805
- // Mask sensitive information in context to prevent credential leaks
806
- context: context ? maskSensitiveData(context) : void 0
807
- };
808
- this.processTransports(metadata);
809
- }
810
- /**
811
- * Process Transports
812
- */
813
- processTransports(metadata) {
814
- const promises = this.config.transports.filter((transport) => transport.enabled).map((transport) => this.safeTransportLog(transport, metadata));
815
- Promise.all(promises).catch((error) => {
816
- const errorMessage = error instanceof Error ? error.message : String(error);
817
- process.stderr.write(`[Logger] Transport error: ${errorMessage}
818
- `);
819
- });
820
- }
821
- /**
822
- * Transport log (error-safe)
823
- */
824
- async safeTransportLog(transport, metadata) {
825
- try {
826
- await transport.log(metadata);
827
- } catch (error) {
828
- const errorMessage = error instanceof Error ? error.message : String(error);
829
- process.stderr.write(`[Logger] Transport "${transport.name}" failed: ${errorMessage}
830
- `);
831
- }
832
- }
833
- /**
834
- * Close all Transports
835
- */
836
- async close() {
837
- const closePromises = this.config.transports.filter((transport) => transport.close).map((transport) => transport.close());
838
- await Promise.all(closePromises);
839
- }
840
- };
841
-
842
- // src/logger/transports/console.ts
843
- var ConsoleTransport = class {
844
- name = "console";
845
- level;
846
- enabled;
847
- colorize;
848
- constructor(config) {
849
- this.level = config.level;
850
- this.enabled = config.enabled;
851
- this.colorize = config.colorize ?? true;
852
- }
853
- async log(metadata) {
854
- if (!this.enabled) {
855
- return;
856
- }
857
- if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
858
- return;
859
- }
860
- const message = formatConsole(metadata, this.colorize);
861
- if (metadata.level === "warn" || metadata.level === "error" || metadata.level === "fatal") {
862
- console.error(message);
863
- } else {
864
- console.log(message);
865
- }
866
- }
867
- };
868
- var FileTransport = class {
869
- name = "file";
870
- level;
871
- enabled;
872
- logDir;
873
- maxFileSize;
874
- maxFiles;
875
- currentStream = null;
876
- currentFilename = null;
877
- constructor(config) {
878
- this.level = config.level;
879
- this.enabled = config.enabled;
880
- this.logDir = config.logDir;
881
- this.maxFileSize = config.maxFileSize ?? 10 * 1024 * 1024;
882
- this.maxFiles = config.maxFiles ?? 10;
883
- if (!existsSync(this.logDir)) {
884
- mkdirSync(this.logDir, { recursive: true });
885
- }
886
- }
887
- async log(metadata) {
888
- if (!this.enabled) {
889
- return;
890
- }
891
- if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
892
- return;
893
- }
894
- const message = formatJSON(metadata);
895
- const filename = this.getLogFilename(metadata.timestamp);
896
- if (this.currentFilename !== filename) {
897
- await this.rotateStream(filename);
898
- await this.cleanOldFiles();
899
- } else if (this.currentFilename) {
900
- await this.checkAndRotateBySize();
901
- }
902
- if (this.currentStream) {
903
- return new Promise((resolve, reject) => {
904
- this.currentStream.write(message + "\n", "utf-8", (error) => {
905
- if (error) {
906
- process.stderr.write(`[FileTransport] Failed to write log: ${error.message}
907
- `);
908
- reject(error);
909
- } else {
910
- resolve();
911
- }
912
- });
913
- });
914
- }
915
- }
916
- /**
917
- * 스트림 교체 (날짜 변경 시)
918
- */
919
- async rotateStream(filename) {
920
- if (this.currentStream) {
921
- await this.closeStream();
922
- }
923
- const filepath = join(this.logDir, filename);
924
- this.currentStream = createWriteStream(filepath, {
925
- flags: "a",
926
- // append mode
927
- encoding: "utf-8"
928
- });
929
- this.currentFilename = filename;
930
- this.currentStream.on("error", (error) => {
931
- process.stderr.write(`[FileTransport] Stream error: ${error.message}
932
- `);
933
- this.currentStream = null;
934
- this.currentFilename = null;
935
- });
936
- }
937
- /**
938
- * 현재 스트림 닫기
428
+ * 현재 스트림 닫기
939
429
  */
940
430
  async closeStream() {
941
431
  if (!this.currentStream) {
@@ -1284,91 +774,7 @@ function initializeLogger() {
1284
774
  }
1285
775
  var logger = initializeLogger();
1286
776
 
1287
- // src/codegen/watch-generate.ts
1288
- var codegenLogger = logger.child("codegen");
1289
- async function generateOnce(options) {
1290
- const cwd = process.cwd();
1291
- const routesDir = options.routesDir ?? join(cwd, "src", "server", "routes");
1292
- const outputPath = options.outputPath ?? join(cwd, "src", "lib", "api.ts");
1293
- try {
1294
- const contracts = await scanContracts(routesDir);
1295
- if (contracts.length === 0) {
1296
- if (options.debug) {
1297
- codegenLogger.warn("No contracts found");
1298
- }
1299
- return null;
1300
- }
1301
- const stats = await generateClient(contracts, {
1302
- outputPath,
1303
- includeTypes: true,
1304
- includeJsDoc: true
1305
- });
1306
- if (options.debug) {
1307
- codegenLogger.info("Client generated", {
1308
- endpoints: stats.methodsGenerated,
1309
- resources: stats.resourcesGenerated,
1310
- duration: stats.duration
1311
- });
1312
- }
1313
- return stats;
1314
- } catch (error) {
1315
- codegenLogger.error(
1316
- "Generation failed",
1317
- error instanceof Error ? error : new Error(String(error))
1318
- );
1319
- return null;
1320
- }
1321
- }
1322
- async function watchAndGenerate(options = {}) {
1323
- const cwd = process.cwd();
1324
- const routesDir = options.routesDir ?? join(cwd, "src", "server", "routes");
1325
- const outputPath = options.outputPath ?? join(cwd, "src", "lib", "api.ts");
1326
- const watchMode = options.watch !== false;
1327
- if (options.debug) {
1328
- codegenLogger.info("Contract Watcher Started", { routesDir, outputPath, watch: watchMode });
1329
- }
1330
- await generateOnce(options);
1331
- if (watchMode) {
1332
- let isGenerating = false;
1333
- let pendingRegeneration = false;
1334
- const watcher = watch(routesDir, {
1335
- ignored: /(^|[\/\\])\../,
1336
- // ignore dotfiles
1337
- persistent: true,
1338
- ignoreInitial: true,
1339
- awaitWriteFinish: {
1340
- stabilityThreshold: 100,
1341
- pollInterval: 50
1342
- }
1343
- });
1344
- const regenerate = async () => {
1345
- if (isGenerating) {
1346
- pendingRegeneration = true;
1347
- return;
1348
- }
1349
- isGenerating = true;
1350
- pendingRegeneration = false;
1351
- if (options.debug) {
1352
- codegenLogger.info("Contracts changed, regenerating...");
1353
- }
1354
- await generateOnce(options);
1355
- isGenerating = false;
1356
- if (pendingRegeneration) {
1357
- await regenerate();
1358
- }
1359
- };
1360
- watcher.on("add", regenerate).on("change", regenerate).on("unlink", regenerate);
1361
- process.on("SIGINT", () => {
1362
- watcher.close();
1363
- process.exit(0);
1364
- });
1365
- await new Promise(() => {
1366
- });
1367
- }
1368
- }
1369
- if (import.meta.url === `file://${process.argv[1]}`) {
1370
- watchAndGenerate({ debug: true });
1371
- }
777
+ // src/codegen/core/orchestrator.ts
1372
778
  var orchestratorLogger = logger.child("orchestrator");
1373
779
  var CodegenOrchestrator = class {
1374
780
  generators;
@@ -1518,6 +924,518 @@ var CodegenOrchestrator = class {
1518
924
  });
1519
925
  }
1520
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
+ }
1032
+ }
1033
+ }
1034
+ ts.forEachChild(node, visit);
1035
+ }
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;
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;
1089
+ }
1090
+ }
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] = [];
1121
+ }
1122
+ grouped[resource].push(mapping);
1123
+ }
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);
1137
+ }
1138
+ }
1139
+ if (staticSegments.length === 0) {
1140
+ return "root";
1141
+ }
1142
+ const first = toCamelCase(staticSegments[0], false);
1143
+ if (staticSegments.length === 1) {
1144
+ return first;
1145
+ }
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("");
1152
+ }
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;
1157
+ }
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
+ }
1166
+ }
1167
+ return result.join("");
1168
+ }
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);
1176
+ return {
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
1183
+ };
1184
+ }
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
+ `;
1196
+ }
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();
1204
+ }
1205
+ groups[path].add(mapping.contractName);
1206
+ }
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]);
1212
+ }
1213
+ return result;
1214
+ }
1215
+ function generateTypeName(mapping) {
1216
+ let name = mapping.contractName;
1217
+ if (name.endsWith("Contract")) {
1218
+ name = name.slice(0, -8);
1219
+ }
1220
+ if (name.length > 0) {
1221
+ name = name.charAt(0).toUpperCase() + name.slice(1);
1222
+ }
1223
+ return name;
1224
+ }
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
+ `;
1238
+ }
1239
+ code += ` ${methodName}: (`;
1240
+ const params = [];
1241
+ const typeName = generateTypeName(mapping);
1242
+ if (hasParams) {
1243
+ params.push(`params: ${typeName}Params`);
1244
+ }
1245
+ if (hasQuery) {
1246
+ params.push(`query?: ${typeName}Query`);
1247
+ }
1248
+ if (hasBody) {
1249
+ params.push(`body: ${typeName}Body`);
1250
+ }
1251
+ if (params.length > 0) {
1252
+ code += `options: { ${params.join(", ")} }`;
1253
+ }
1254
+ code += `) => `;
1255
+ code += `client.call(${mapping.contractName}`;
1256
+ if (params.length > 0) {
1257
+ code += `, options`;
1258
+ }
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";
1271
+ }
1272
+ }
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
+ }
1283
+ }
1284
+ return method;
1285
+ }
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);
1291
+ }
1292
+ }
1293
+ return files.size;
1294
+ }
1295
+ function toKebabCase(str) {
1296
+ if (str.length === 0) {
1297
+ return str;
1298
+ }
1299
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
1300
+ }
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");
1313
+ }
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
+ `;
1326
+ }
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
+ `;
1336
+ }
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
+ `;
1367
+ }
1368
+ }
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);
1380
+ }
1381
+ code += `} as const;
1382
+ `;
1383
+ return code;
1384
+ }
1385
+ function toCamelCase2(str) {
1386
+ if (str.length === 0) {
1387
+ return str;
1388
+ }
1389
+ return str.charAt(0).toLowerCase() + str.slice(1);
1390
+ }
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';
1396
+
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 += `,`;
1429
+ }
1430
+ code += `
1431
+ `;
1432
+ }
1433
+ code += `} as const;
1434
+ `;
1435
+ return code;
1436
+ }
1437
+
1438
+ // src/codegen/built-in/contract/index.ts
1521
1439
  var contractLogger = logger.child("contract-gen");
1522
1440
  var DEFAULT_CONTRACTS_DIR = "src/lib/contracts";
1523
1441
  var DEFAULT_OUTPUT_PATH = "src/lib/api";
@@ -1579,6 +1497,8 @@ function createContractGenerator(config = {}) {
1579
1497
  const cwd = options.cwd;
1580
1498
  const fullContractsDir = join(cwd, contractsDir);
1581
1499
  const fullOutputPath = join(cwd, outputPath);
1500
+ const prefix = readPrefixFromPackageJson(cwd);
1501
+ const apiName = generateApiName(prefix);
1582
1502
  try {
1583
1503
  if (!existsSync(fullContractsDir)) {
1584
1504
  if (options.debug) {
@@ -1600,6 +1520,7 @@ function createContractGenerator(config = {}) {
1600
1520
  outputPath: fullOutputPath,
1601
1521
  changedFilePath: changedFile.path,
1602
1522
  baseUrl: config.baseUrl,
1523
+ apiName,
1603
1524
  debug: options.debug
1604
1525
  });
1605
1526
  if (success) {
@@ -1612,7 +1533,7 @@ function createContractGenerator(config = {}) {
1612
1533
  contractLogger.info("Incremental update failed, doing full regen");
1613
1534
  }
1614
1535
  }
1615
- const allContracts = await scanContracts(fullContractsDir);
1536
+ const allContracts = await scanContracts(fullContractsDir, prefix);
1616
1537
  if (allContracts.length === 0) {
1617
1538
  if (options.debug) {
1618
1539
  contractLogger.warn("No contracts found");
@@ -1620,8 +1541,6 @@ function createContractGenerator(config = {}) {
1620
1541
  contractCache = null;
1621
1542
  return;
1622
1543
  }
1623
- const prefix = readPrefixFromPackageJson(cwd);
1624
- const apiName = generateApiName(prefix);
1625
1544
  const clientOptions = createClientOptions(fullContractsDir, fullOutputPath, config.baseUrl, apiName);
1626
1545
  const stats = await generateClient(allContracts, clientOptions);
1627
1546
  contractCache = {
@@ -1645,7 +1564,7 @@ function createContractGenerator(config = {}) {
1645
1564
  };
1646
1565
  }
1647
1566
  async function attemptIncrementalUpdate(options) {
1648
- const { cwd, contractsDir, outputPath, changedFilePath, baseUrl, debug } = options;
1567
+ const { cwd, contractsDir, outputPath, changedFilePath, baseUrl, apiName, debug } = options;
1649
1568
  if (!contractCache) {
1650
1569
  return false;
1651
1570
  }
@@ -1670,7 +1589,7 @@ async function attemptIncrementalUpdate(options) {
1670
1589
  }
1671
1590
  return true;
1672
1591
  }
1673
- const clientOptions = createClientOptions(contractsDir, outputPath, baseUrl);
1592
+ const clientOptions = createClientOptions(contractsDir, outputPath, baseUrl, apiName);
1674
1593
  const stats = await generateClient(updatedContracts, clientOptions);
1675
1594
  contractCache = {
1676
1595
  contracts: updatedContracts,
@@ -1832,6 +1751,6 @@ async function createGeneratorsFromConfig(config, cwd) {
1832
1751
  return generators;
1833
1752
  }
1834
1753
 
1835
- export { CodegenOrchestrator, createContractGenerator, createGeneratorsFromConfig, generateClient, groupByResource, loadCodegenConfig, scanContracts, watchAndGenerate };
1754
+ export { CodegenOrchestrator, createContractGenerator, createGeneratorsFromConfig, loadCodegenConfig };
1836
1755
  //# sourceMappingURL=index.js.map
1837
1756
  //# sourceMappingURL=index.js.map