locai 2.1.0
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 +928 -0
- package/dist/cli.js +718 -0
- package/dist/commands/review.js +357 -0
- package/dist/commands/translator.js +29 -0
- package/dist/config/index.js +52 -0
- package/dist/core/adapters/base-adapter.js +1 -0
- package/dist/core/adapters/factory.js +44 -0
- package/dist/core/adapters/json-adapter.js +20 -0
- package/dist/core/adapters/po-adapter.js +50 -0
- package/dist/core/adapters/properties-adapter.js +31 -0
- package/dist/core/adapters/yaml-adapter.js +24 -0
- package/dist/core/context-processor.js +247 -0
- package/dist/core/detection/framework-detector.js +56 -0
- package/dist/core/detection/structure-detector.js +30 -0
- package/dist/core/fallback-provider.js +442 -0
- package/dist/core/orchestrator.js +438 -0
- package/dist/core/pipeline/Pipeline.js +48 -0
- package/dist/core/pipeline/PipelineStep.js +13 -0
- package/dist/core/pipeline/context.js +21 -0
- package/dist/core/pipeline/steps/CacheReadStep.js +80 -0
- package/dist/core/pipeline/steps/CacheWriteStep.js +32 -0
- package/dist/core/pipeline/steps/ConfidenceCheckStep.js +67 -0
- package/dist/core/pipeline/steps/GlossaryPostStep.js +17 -0
- package/dist/core/pipeline/steps/GlossaryPreStep.js +20 -0
- package/dist/core/pipeline/steps/InputValidationStep.js +26 -0
- package/dist/core/pipeline/steps/QualityCheckStep.js +48 -0
- package/dist/core/pipeline/steps/ToneCheckStep.js +58 -0
- package/dist/core/pipeline/steps/TranslationStep.js +65 -0
- package/dist/core/pipeline/steps/VectorCacheReadStep.js +84 -0
- package/dist/core/pipeline/steps/VectorCacheWriteStep.js +42 -0
- package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.js +100 -0
- package/dist/core/provider-factory.js +139 -0
- package/dist/providers/base-provider.js +230 -0
- package/dist/providers/dashscope.js +128 -0
- package/dist/providers/deepseek.js +111 -0
- package/dist/providers/gemini.js +135 -0
- package/dist/providers/openai.js +203 -0
- package/dist/providers/xai.js +111 -0
- package/dist/services/embedding-provider.js +50 -0
- package/dist/services/translation-service.js +609 -0
- package/dist/services/vector-store.js +79 -0
- package/dist/utils/ai-context-analyzer.js +460 -0
- package/dist/utils/confidence-scorer.js +277 -0
- package/dist/utils/error-helper.js +386 -0
- package/dist/utils/file-manager.js +357 -0
- package/dist/utils/glossary-manager.js +194 -0
- package/dist/utils/graceful-shutdown.js +118 -0
- package/dist/utils/input-validator.js +663 -0
- package/dist/utils/logger.js +374 -0
- package/dist/utils/object-transformer.js +177 -0
- package/dist/utils/progress-tracker.js +277 -0
- package/dist/utils/prompt-templates.js +415 -0
- package/dist/utils/quality/__tests__/tone-verifier.test.js +91 -0
- package/dist/utils/quality/base-checker.js +73 -0
- package/dist/utils/quality/code-block-checker.js +214 -0
- package/dist/utils/quality/html-tag-checker.js +64 -0
- package/dist/utils/quality/index.js +220 -0
- package/dist/utils/quality/length-checker.js +140 -0
- package/dist/utils/quality/markdown-checker.js +220 -0
- package/dist/utils/quality/placeholder-checker.js +142 -0
- package/dist/utils/quality/punctuation-checker.js +50 -0
- package/dist/utils/quality/quote-balance-checker.js +143 -0
- package/dist/utils/quality/special-characters-checker.js +229 -0
- package/dist/utils/quality/style-guide-checker.js +192 -0
- package/dist/utils/quality/text-sanitizer.js +120 -0
- package/dist/utils/quality/tone-verifier.js +91 -0
- package/dist/utils/rate-limiter.js +398 -0
- package/dist/utils/retry-helper.js +166 -0
- package/dist/utils/state-manager.js +162 -0
- package/dist/utils/statistics-manager.js +51 -0
- package/dist/utils/ui-manager.js +98 -0
- package/package.json +80 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced Logging System.
|
|
3
|
+
* Implements saveErrorLogs, logDirectory, logRotation, and other logging features.
|
|
4
|
+
*/
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { promises as fsPromises } from "fs";
|
|
7
|
+
class Logger {
|
|
8
|
+
config;
|
|
9
|
+
logFiles;
|
|
10
|
+
currentLogSizes;
|
|
11
|
+
initialized;
|
|
12
|
+
/**
|
|
13
|
+
* Create a new Logger instance.
|
|
14
|
+
* @param config - Logger configuration.
|
|
15
|
+
*/
|
|
16
|
+
constructor(config = {}) {
|
|
17
|
+
this.config = {
|
|
18
|
+
verbose: config.verbose || false,
|
|
19
|
+
diagnosticsLevel: config.diagnosticsLevel || "minimal",
|
|
20
|
+
outputFormat: config.outputFormat || "pretty",
|
|
21
|
+
saveErrorLogs: config.saveErrorLogs !== false,
|
|
22
|
+
logDirectory: config.logDirectory || "./logs",
|
|
23
|
+
includeTimestamps: config.includeTimestamps !== false,
|
|
24
|
+
logRotation: {
|
|
25
|
+
enabled: config.logRotation?.enabled !== false,
|
|
26
|
+
maxFiles: config.logRotation?.maxFiles || 5,
|
|
27
|
+
maxSize: this.parseSize(config.logRotation?.maxSize || "10MB"),
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
this.logFiles = {
|
|
31
|
+
error: path.join(this.config.logDirectory, "errors.log"),
|
|
32
|
+
warning: path.join(this.config.logDirectory, "warnings.log"),
|
|
33
|
+
info: path.join(this.config.logDirectory, "info.log"),
|
|
34
|
+
debug: path.join(this.config.logDirectory, "debug.log"),
|
|
35
|
+
};
|
|
36
|
+
this.currentLogSizes = {};
|
|
37
|
+
this.initialized = false;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Initialize logger (create directories, check rotation).
|
|
41
|
+
*/
|
|
42
|
+
async initialize() {
|
|
43
|
+
if (this.initialized)
|
|
44
|
+
return;
|
|
45
|
+
try {
|
|
46
|
+
// Create log directory if it doesn't exist
|
|
47
|
+
if (this.config.saveErrorLogs) {
|
|
48
|
+
await fsPromises.mkdir(this.config.logDirectory, { recursive: true });
|
|
49
|
+
// Check log rotation for existing files
|
|
50
|
+
if (this.config.logRotation.enabled) {
|
|
51
|
+
await this.checkAndRotateLogs();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
this.initialized = true;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.warn(`Logger initialization warning: ${error.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Parse size string (e.g., "10MB") to bytes.
|
|
62
|
+
* @param sizeStr - Size string to parse.
|
|
63
|
+
*/
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
65
|
+
parseSize(sizeStr) {
|
|
66
|
+
if (typeof sizeStr === "number")
|
|
67
|
+
return sizeStr;
|
|
68
|
+
if (typeof sizeStr !== "string")
|
|
69
|
+
return 10 * 1024 * 1024;
|
|
70
|
+
const units = {
|
|
71
|
+
B: 1,
|
|
72
|
+
KB: 1024,
|
|
73
|
+
MB: 1024 * 1024,
|
|
74
|
+
GB: 1024 * 1024 * 1024,
|
|
75
|
+
};
|
|
76
|
+
const match = sizeStr.match(/^(\d+(?:\.\d+)?)\s*([A-Z]+)$/i);
|
|
77
|
+
if (!match)
|
|
78
|
+
return 10 * 1024 * 1024; // Default 10MB
|
|
79
|
+
const [, size, unit] = match;
|
|
80
|
+
return parseFloat(size) * (units[unit.toUpperCase()] || 1);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Check and rotate logs if needed.
|
|
84
|
+
*/
|
|
85
|
+
async checkAndRotateLogs() {
|
|
86
|
+
for (const [level, logPath] of Object.entries(this.logFiles)) {
|
|
87
|
+
try {
|
|
88
|
+
const stats = await fsPromises.stat(logPath);
|
|
89
|
+
this.currentLogSizes[level] = stats.size;
|
|
90
|
+
if (stats.size >= this.config.logRotation.maxSize) {
|
|
91
|
+
await this.rotateLog(logPath);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
console.warn(`Log rotation failed for ${logPath}: ${error.message}`);
|
|
96
|
+
this.currentLogSizes[level] = 0;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Rotate a log file.
|
|
102
|
+
* @param logPath - Path to the log file.
|
|
103
|
+
*/
|
|
104
|
+
async rotateLog(logPath) {
|
|
105
|
+
try {
|
|
106
|
+
const dir = path.dirname(logPath);
|
|
107
|
+
const basename = path.basename(logPath, ".log");
|
|
108
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
109
|
+
// Rename current log
|
|
110
|
+
const archivedPath = path.join(dir, `${basename}.${timestamp}.log`);
|
|
111
|
+
await fsPromises.rename(logPath, archivedPath);
|
|
112
|
+
// Clean up old logs if exceeding maxFiles
|
|
113
|
+
await this.cleanupOldLogs(dir, basename);
|
|
114
|
+
// Reset size tracker
|
|
115
|
+
const level = Object.keys(this.logFiles).find((key) => this.logFiles[key] === logPath);
|
|
116
|
+
if (level) {
|
|
117
|
+
this.currentLogSizes[level] = 0;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
console.warn(`Log rotation failed for ${logPath}: ${error.message}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Clean up old log files.
|
|
126
|
+
* @param dir - Directory containing log files.
|
|
127
|
+
* @param basename - Basename of the log file.
|
|
128
|
+
*/
|
|
129
|
+
async cleanupOldLogs(dir, basename) {
|
|
130
|
+
try {
|
|
131
|
+
const files = await fsPromises.readdir(dir);
|
|
132
|
+
const logFiles = files
|
|
133
|
+
.filter((file) => file.startsWith(basename) && file.endsWith(".log"))
|
|
134
|
+
.map((file) => ({
|
|
135
|
+
name: file,
|
|
136
|
+
path: path.join(dir, file),
|
|
137
|
+
}));
|
|
138
|
+
// Sort by modification time
|
|
139
|
+
const fileStats = await Promise.all(logFiles.map(async (file) => ({
|
|
140
|
+
...file,
|
|
141
|
+
stats: await fsPromises.stat(file.path),
|
|
142
|
+
})));
|
|
143
|
+
fileStats.sort((a, b) => b.stats.mtime.getTime() - a.stats.mtime.getTime());
|
|
144
|
+
// Delete oldest files if exceeding maxFiles
|
|
145
|
+
if (fileStats.length > this.config.logRotation.maxFiles) {
|
|
146
|
+
const filesToDelete = fileStats.slice(this.config.logRotation.maxFiles);
|
|
147
|
+
for (const file of filesToDelete) {
|
|
148
|
+
await fsPromises.unlink(file.path);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
console.warn(`Cleanup old logs failed: ${error.message}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Format log message.
|
|
158
|
+
* @param level - Log level.
|
|
159
|
+
* @param message - Log message.
|
|
160
|
+
* @param data - Additional data to log.
|
|
161
|
+
*/
|
|
162
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
163
|
+
formatMessage(level, message, data = null) {
|
|
164
|
+
const timestamp = this.config.includeTimestamps ? new Date().toISOString() : null;
|
|
165
|
+
switch (this.config.outputFormat) {
|
|
166
|
+
case "json":
|
|
167
|
+
return JSON.stringify({
|
|
168
|
+
timestamp,
|
|
169
|
+
level,
|
|
170
|
+
message,
|
|
171
|
+
data,
|
|
172
|
+
});
|
|
173
|
+
case "simple":
|
|
174
|
+
return `[${level.toUpperCase()}] ${message}`;
|
|
175
|
+
case "pretty":
|
|
176
|
+
default: {
|
|
177
|
+
const timeStr = timestamp ? `[${timestamp}] ` : "";
|
|
178
|
+
const levelStr = `[${level.toUpperCase()}]`;
|
|
179
|
+
const dataStr = data ? `\n${JSON.stringify(data, null, 2)}` : "";
|
|
180
|
+
return `${timeStr}${levelStr} ${message}${dataStr}`;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Write to log file.
|
|
186
|
+
* @param level - Log level.
|
|
187
|
+
* @param message - Log message.
|
|
188
|
+
* @param data - Additional data to log.
|
|
189
|
+
*/
|
|
190
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
191
|
+
async writeToFile(level, message, data = null) {
|
|
192
|
+
if (!this.config.saveErrorLogs)
|
|
193
|
+
return;
|
|
194
|
+
await this.initialize();
|
|
195
|
+
const logPath = this.logFiles[level];
|
|
196
|
+
if (!logPath)
|
|
197
|
+
return;
|
|
198
|
+
try {
|
|
199
|
+
const formattedMessage = this.formatMessage(level, message, data);
|
|
200
|
+
const logEntry = `${formattedMessage}\n`;
|
|
201
|
+
await fsPromises.appendFile(logPath, logEntry, "utf8");
|
|
202
|
+
// Update size tracker
|
|
203
|
+
this.currentLogSizes[level] = (this.currentLogSizes[level] || 0) + logEntry.length;
|
|
204
|
+
// Check if rotation is needed
|
|
205
|
+
if (this.config.logRotation.enabled &&
|
|
206
|
+
this.currentLogSizes[level] >= this.config.logRotation.maxSize) {
|
|
207
|
+
await this.rotateLog(logPath);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
console.warn(`Failed to write to log file: ${error.message}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Simple console log with debug awareness.
|
|
216
|
+
* @param message - Message to log
|
|
217
|
+
* @param debugOnly - Only log if debug mode is enabled
|
|
218
|
+
*/
|
|
219
|
+
async log(message, debugOnly = false) {
|
|
220
|
+
if (debugOnly && !this.config.verbose && !process.env.DEBUG) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
console.log(message);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Log error.
|
|
227
|
+
* @param message - Error message.
|
|
228
|
+
* @param data - Additional data.
|
|
229
|
+
*/
|
|
230
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
231
|
+
async error(message, data = null) {
|
|
232
|
+
console.error(`ERROR: ${message}`);
|
|
233
|
+
if (data && this.config.verbose) {
|
|
234
|
+
console.error(data);
|
|
235
|
+
}
|
|
236
|
+
await this.writeToFile("error", message, data);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Log warning.
|
|
240
|
+
* @param message - Warning message.
|
|
241
|
+
* @param data - Additional data.
|
|
242
|
+
*/
|
|
243
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
244
|
+
async warn(message, data = null) {
|
|
245
|
+
console.warn(`WARNING: ${message}`);
|
|
246
|
+
if (data && this.config.verbose) {
|
|
247
|
+
console.warn(data);
|
|
248
|
+
}
|
|
249
|
+
await this.writeToFile("warning", message, data);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Log info.
|
|
253
|
+
* @param message - Info message.
|
|
254
|
+
* @param data - Additional data.
|
|
255
|
+
*/
|
|
256
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
257
|
+
async info(message, data = null) {
|
|
258
|
+
if (this.config.diagnosticsLevel !== "minimal" || this.config.verbose) {
|
|
259
|
+
console.log(`INFO: ${message}`);
|
|
260
|
+
if (data && this.config.verbose) {
|
|
261
|
+
console.log(data);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
await this.writeToFile("info", message, data);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Log debug.
|
|
268
|
+
* @param message - Debug message.
|
|
269
|
+
* @param data - Additional data.
|
|
270
|
+
*/
|
|
271
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
272
|
+
async debug(message, data = null) {
|
|
273
|
+
if (this.config.verbose || this.config.diagnosticsLevel === "detailed") {
|
|
274
|
+
console.log(`DEBUG: ${message}`);
|
|
275
|
+
if (data) {
|
|
276
|
+
console.log(data);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
await this.writeToFile("debug", message, data);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Log based on diagnostics level.
|
|
283
|
+
* @param level - Log level.
|
|
284
|
+
* @param message - Log message.
|
|
285
|
+
* @param data - Additional data.
|
|
286
|
+
*/
|
|
287
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
288
|
+
async diagnostics(level, message, data = null) {
|
|
289
|
+
const levels = {
|
|
290
|
+
minimal: 1,
|
|
291
|
+
normal: 2,
|
|
292
|
+
detailed: 3,
|
|
293
|
+
};
|
|
294
|
+
const currentLevel = levels[this.config.diagnosticsLevel] || 1;
|
|
295
|
+
const messageLevel = levels[level] || 2;
|
|
296
|
+
if (messageLevel <= currentLevel) {
|
|
297
|
+
await this.info(message, data);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Get log statistics.
|
|
302
|
+
* @returns Object containing log stats.
|
|
303
|
+
*/
|
|
304
|
+
async getLogStats() {
|
|
305
|
+
const stats = {};
|
|
306
|
+
for (const [level, logPath] of Object.entries(this.logFiles)) {
|
|
307
|
+
try {
|
|
308
|
+
const fileStats = await fsPromises.stat(logPath);
|
|
309
|
+
stats[level] = {
|
|
310
|
+
size: fileStats.size,
|
|
311
|
+
sizeFormatted: this.formatSize(fileStats.size),
|
|
312
|
+
modified: fileStats.mtime,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
console.warn(`Failed to get stats for ${logPath}: ${error.message}`);
|
|
317
|
+
stats[level] = {
|
|
318
|
+
size: 0,
|
|
319
|
+
sizeFormatted: "0 B",
|
|
320
|
+
modified: null,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return stats;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Format size to human-readable.
|
|
328
|
+
* @param bytes - Size in bytes.
|
|
329
|
+
*/
|
|
330
|
+
formatSize(bytes) {
|
|
331
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
332
|
+
let size = bytes;
|
|
333
|
+
let unitIndex = 0;
|
|
334
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
335
|
+
size /= 1024;
|
|
336
|
+
unitIndex++;
|
|
337
|
+
}
|
|
338
|
+
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Clear all logs.
|
|
342
|
+
*/
|
|
343
|
+
async clearLogs() {
|
|
344
|
+
for (const logPath of Object.values(this.logFiles)) {
|
|
345
|
+
try {
|
|
346
|
+
await fsPromises.unlink(logPath);
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
console.warn(`Failed to clear log ${logPath}: ${error.message}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
this.currentLogSizes = {};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// Create singleton instance
|
|
356
|
+
let loggerInstance = null;
|
|
357
|
+
export function getLogger(config = null) {
|
|
358
|
+
if (!loggerInstance || config) {
|
|
359
|
+
loggerInstance = new Logger(config || {});
|
|
360
|
+
}
|
|
361
|
+
return loggerInstance;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Quick debug-aware logging helper.
|
|
365
|
+
* @param message - Message to log.
|
|
366
|
+
* @param debugOnly - Only log in debug/verbose mode.
|
|
367
|
+
*/
|
|
368
|
+
export function log(message, debugOnly = false) {
|
|
369
|
+
if (debugOnly && !process.env.DEBUG && !process.env.VERBOSE) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
console.log(message);
|
|
373
|
+
}
|
|
374
|
+
export default Logger;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility class for object manipulation.
|
|
3
|
+
*/
|
|
4
|
+
class ObjectTransformer {
|
|
5
|
+
/**
|
|
6
|
+
* Flatten a nested object into a single-level object with dot notation keys.
|
|
7
|
+
* @param obj - The object to flatten.
|
|
8
|
+
* @param prefix - Prefix for keys (used internally).
|
|
9
|
+
* @param maxDepth - Maximum recursion depth.
|
|
10
|
+
* @returns Flattened object.
|
|
11
|
+
*/
|
|
12
|
+
static flatten(obj, prefix = "", maxDepth = 20) {
|
|
13
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15
|
+
return prefix ? { [prefix]: obj } : {};
|
|
16
|
+
}
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
+
const result = {};
|
|
19
|
+
const stack = [{ obj, prefix, depth: 0 }];
|
|
20
|
+
while (stack.length > 0) {
|
|
21
|
+
const stackItem = stack.pop();
|
|
22
|
+
if (!stackItem)
|
|
23
|
+
continue;
|
|
24
|
+
const { obj: currentObj, prefix: currentPrefix, depth } = stackItem;
|
|
25
|
+
if (depth > maxDepth) {
|
|
26
|
+
result[currentPrefix] = "[Object too deep]";
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const keys = Object.keys(currentObj);
|
|
30
|
+
const keysLength = keys.length;
|
|
31
|
+
for (let i = 0; i < keysLength; i++) {
|
|
32
|
+
const key = keys[i];
|
|
33
|
+
const value = currentObj[key];
|
|
34
|
+
const newKey = currentPrefix ? `${currentPrefix}.${key}` : key;
|
|
35
|
+
if (value !== null &&
|
|
36
|
+
typeof value === "object" &&
|
|
37
|
+
!Array.isArray(value) &&
|
|
38
|
+
value.constructor === Object) {
|
|
39
|
+
stack.push({ obj: value, prefix: newKey, depth: depth + 1 });
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
result[newKey] = value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Unflatten an object with dot notation keys into a nested object.
|
|
50
|
+
* @param obj - The flattened object.
|
|
51
|
+
* @returns Nested object.
|
|
52
|
+
*/
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
+
static unflatten(obj) {
|
|
55
|
+
if (!obj || typeof obj !== "object") {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
const result = {};
|
|
59
|
+
const keys = Object.keys(obj);
|
|
60
|
+
const keysLength = keys.length;
|
|
61
|
+
keys.sort();
|
|
62
|
+
for (let keyIndex = 0; keyIndex < keysLength; keyIndex++) {
|
|
63
|
+
const key = keys[keyIndex];
|
|
64
|
+
const value = obj[key];
|
|
65
|
+
const keyParts = key.split(".");
|
|
66
|
+
const partsLength = keyParts.length;
|
|
67
|
+
if (partsLength === 0)
|
|
68
|
+
continue;
|
|
69
|
+
let hasEmptyPart = false;
|
|
70
|
+
for (let i = 0; i < partsLength; i++) {
|
|
71
|
+
if (keyParts[i] === "") {
|
|
72
|
+
hasEmptyPart = true;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (hasEmptyPart)
|
|
77
|
+
continue;
|
|
78
|
+
let current = result;
|
|
79
|
+
const lastIndex = partsLength - 1;
|
|
80
|
+
for (let i = 0; i < lastIndex; i++) {
|
|
81
|
+
const part = keyParts[i];
|
|
82
|
+
if (current[part] == null ||
|
|
83
|
+
typeof current[part] !== "object" ||
|
|
84
|
+
Array.isArray(current[part])) {
|
|
85
|
+
current[part] = {};
|
|
86
|
+
}
|
|
87
|
+
current = current[part];
|
|
88
|
+
}
|
|
89
|
+
current[keyParts[lastIndex]] = value;
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Create a deep copy of an object.
|
|
95
|
+
* @param obj - The object to clone.
|
|
96
|
+
* @param seen - Internal set to track circular references.
|
|
97
|
+
* @returns Deep copy of the object.
|
|
98
|
+
*/
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
100
|
+
static deepClone(obj, seen = new WeakSet()) {
|
|
101
|
+
if (obj === null || typeof obj !== "object") {
|
|
102
|
+
return obj;
|
|
103
|
+
}
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
105
|
+
if (seen.has(obj)) {
|
|
106
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
110
|
+
seen.add(obj);
|
|
111
|
+
if (Array.isArray(obj)) {
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
113
|
+
const result = obj.map((item) => this.deepClone(item, seen));
|
|
114
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
115
|
+
seen.delete(obj);
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
if (obj instanceof Date) {
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
120
|
+
seen.delete(obj);
|
|
121
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
122
|
+
return new Date(obj.getTime());
|
|
123
|
+
}
|
|
124
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
125
|
+
if (obj.constructor !== Object) {
|
|
126
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
127
|
+
seen.delete(obj);
|
|
128
|
+
return obj;
|
|
129
|
+
}
|
|
130
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
131
|
+
const result = {};
|
|
132
|
+
for (const key of Object.keys(obj)) {
|
|
133
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
134
|
+
result[key] = this.deepClone(obj[key], seen);
|
|
135
|
+
}
|
|
136
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
137
|
+
seen.delete(obj);
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* deeply merge two objects.
|
|
142
|
+
* @param target - The target object.
|
|
143
|
+
* @param source - The source object.
|
|
144
|
+
* @param overwriteArrays - Whether to overwrite arrays or merge them.
|
|
145
|
+
* @returns Merged object.
|
|
146
|
+
*/
|
|
147
|
+
static mergeObjects(target, source, overwriteArrays = false) {
|
|
148
|
+
if (!target || !source || typeof target !== "object" || typeof source !== "object") {
|
|
149
|
+
return target;
|
|
150
|
+
}
|
|
151
|
+
const result = this.deepClone(target);
|
|
152
|
+
for (const key of Object.keys(source)) {
|
|
153
|
+
const sourceValue = source[key];
|
|
154
|
+
const targetValue = result[key];
|
|
155
|
+
if (sourceValue === undefined) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (Array.isArray(sourceValue)) {
|
|
159
|
+
result[key] = overwriteArrays
|
|
160
|
+
? [...sourceValue]
|
|
161
|
+
: Array.isArray(targetValue)
|
|
162
|
+
? [...targetValue, ...sourceValue]
|
|
163
|
+
: [...sourceValue];
|
|
164
|
+
}
|
|
165
|
+
else if (sourceValue &&
|
|
166
|
+
typeof sourceValue === "object" &&
|
|
167
|
+
sourceValue.constructor === Object) {
|
|
168
|
+
result[key] = this.mergeObjects(targetValue || {}, sourceValue, overwriteArrays);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
result[key] = sourceValue;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
export default ObjectTransformer;
|