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