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