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