jtcsv 2.1.0 → 2.1.3
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 +63 -17
- package/bin/jtcsv.js +1013 -117
- package/csv-to-json.js +385 -311
- package/examples/simple-usage.js +2 -3
- package/index.d.ts +288 -5
- package/index.js +23 -0
- package/json-to-csv.js +130 -89
- package/package.json +47 -19
- package/plugins/README.md +146 -2
- package/plugins/hono/README.md +25 -0
- package/plugins/hono/index.d.ts +12 -0
- package/plugins/hono/index.js +36 -0
- package/plugins/hono/package.json +35 -0
- package/plugins/nestjs/README.md +33 -0
- package/plugins/nestjs/index.d.ts +25 -0
- package/plugins/nestjs/index.js +77 -0
- package/plugins/nestjs/package.json +37 -0
- package/plugins/nuxt/README.md +25 -0
- package/plugins/nuxt/index.js +21 -0
- package/plugins/nuxt/package.json +35 -0
- package/plugins/nuxt/runtime/composables/useJtcsv.js +6 -0
- package/plugins/nuxt/runtime/plugin.js +6 -0
- package/plugins/remix/README.md +26 -0
- package/plugins/remix/index.d.ts +16 -0
- package/plugins/remix/index.js +62 -0
- package/plugins/remix/package.json +35 -0
- package/plugins/sveltekit/README.md +28 -0
- package/plugins/sveltekit/index.d.ts +17 -0
- package/plugins/sveltekit/index.js +54 -0
- package/plugins/sveltekit/package.json +33 -0
- package/plugins/trpc/README.md +22 -0
- package/plugins/trpc/index.d.ts +7 -0
- package/plugins/trpc/index.js +32 -0
- package/plugins/trpc/package.json +34 -0
- package/src/core/delimiter-cache.js +186 -0
- package/src/core/transform-hooks.js +350 -0
- package/src/engines/fast-path-engine.js +829 -340
- package/src/formats/tsv-parser.js +336 -0
- package/src/index-with-plugins.js +36 -14
- package/cli-tui.js +0 -5
package/bin/jtcsv.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* jtcsv CLI - Command Line Interface
|
|
4
|
+
* jtcsv CLI - Complete Command Line Interface
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* with streaming
|
|
6
|
+
* Full-featured command-line interface for JSON↔CSV conversion
|
|
7
|
+
* with streaming, batch processing, and all security features.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const fs = require('fs');
|
|
@@ -40,14 +40,27 @@ ${color('The Complete JSON↔CSV Converter for Node.js', 'dim')}
|
|
|
40
40
|
${color('USAGE:', 'bright')}
|
|
41
41
|
jtcsv [command] [options] [file...]
|
|
42
42
|
|
|
43
|
-
${color('COMMANDS:', 'bright')}
|
|
43
|
+
${color('MAIN COMMANDS:', 'bright')}
|
|
44
44
|
${color('json-to-csv', 'green')} Convert JSON to CSV (alias: json2csv)
|
|
45
45
|
${color('csv-to-json', 'green')} Convert CSV to JSON (alias: csv2json)
|
|
46
|
-
${color('
|
|
47
|
-
${color('
|
|
48
|
-
${color('
|
|
49
|
-
${color('
|
|
50
|
-
${color('
|
|
46
|
+
${color('save-json', 'yellow')} Save data as JSON file
|
|
47
|
+
${color('stream', 'yellow')} Streaming conversion for large files
|
|
48
|
+
${color('batch', 'yellow')} Batch process multiple files
|
|
49
|
+
${color('preprocess', 'magenta')} Preprocess JSON with deep unwrapping
|
|
50
|
+
${color('tui', 'magenta')} Launch Terminal User Interface (@jtcsv/tui)
|
|
51
|
+
${color('help', 'blue')} Show this help message
|
|
52
|
+
${color('version', 'blue')} Show version information
|
|
53
|
+
|
|
54
|
+
${color('STREAMING SUBCOMMANDS:', 'bright')}
|
|
55
|
+
${color('stream json-to-csv', 'dim')} Stream JSON to CSV
|
|
56
|
+
${color('stream csv-to-json', 'dim')} Stream CSV to JSON
|
|
57
|
+
${color('stream file-to-csv', 'dim')} Stream file to CSV
|
|
58
|
+
${color('stream file-to-json', 'dim')} Stream file to JSON
|
|
59
|
+
|
|
60
|
+
${color('BATCH SUBCOMMANDS:', 'bright')}
|
|
61
|
+
${color('batch json-to-csv', 'dim')} Batch convert JSON files to CSV
|
|
62
|
+
${color('batch csv-to-json', 'dim')} Batch convert CSV files to JSON
|
|
63
|
+
${color('batch process', 'dim')} Process mixed file types
|
|
51
64
|
|
|
52
65
|
${color('EXAMPLES:', 'bright')}
|
|
53
66
|
${color('Convert JSON file to CSV:', 'dim')}
|
|
@@ -56,13 +69,25 @@ ${color('EXAMPLES:', 'bright')}
|
|
|
56
69
|
${color('Convert CSV file to JSON:', 'dim')}
|
|
57
70
|
jtcsv csv-to-json input.csv output.json --parse-numbers --auto-detect
|
|
58
71
|
|
|
72
|
+
${color('Save data as JSON file:', 'dim')}
|
|
73
|
+
jtcsv save-json data.json output.json --pretty
|
|
74
|
+
|
|
59
75
|
${color('Stream large JSON file to CSV:', 'dim')}
|
|
60
76
|
jtcsv stream json-to-csv large.json output.csv --max-records=1000000
|
|
61
77
|
|
|
78
|
+
${color('Stream CSV file to JSON:', 'dim')}
|
|
79
|
+
jtcsv stream csv-to-json large.csv output.json --max-rows=500000
|
|
80
|
+
|
|
81
|
+
${color('Preprocess complex JSON:', 'dim')}
|
|
82
|
+
jtcsv preprocess complex.json simplified.json --max-depth=3
|
|
83
|
+
|
|
84
|
+
${color('Batch convert JSON files:', 'dim')}
|
|
85
|
+
jtcsv batch json-to-csv "data/*.json" "output/" --delimiter=;
|
|
86
|
+
|
|
62
87
|
${color('Launch TUI interface:', 'dim')}
|
|
63
88
|
jtcsv tui
|
|
64
89
|
|
|
65
|
-
${color('OPTIONS:', 'bright')}
|
|
90
|
+
${color('CONVERSION OPTIONS:', 'bright')}
|
|
66
91
|
${color('--delimiter=', 'cyan')}CHAR CSV delimiter (default: ;)
|
|
67
92
|
${color('--auto-detect', 'cyan')} Auto-detect delimiter (default: true)
|
|
68
93
|
${color('--candidates=', 'cyan')}LIST Delimiter candidates (default: ;,\t|)
|
|
@@ -70,37 +95,71 @@ ${color('OPTIONS:', 'bright')}
|
|
|
70
95
|
${color('--parse-numbers', 'cyan')} Parse numeric values in CSV
|
|
71
96
|
${color('--parse-booleans', 'cyan')} Parse boolean values in CSV
|
|
72
97
|
${color('--no-trim', 'cyan')} Don't trim whitespace from CSV values
|
|
98
|
+
${color('--no-fast-path', 'cyan')} Disable fast-path parser (force quote-aware)
|
|
99
|
+
${color('--fast-path-mode=', 'cyan')}MODE Fast path output mode (objects|compact)
|
|
73
100
|
${color('--rename=', 'cyan')}JSON Rename columns (JSON map)
|
|
74
101
|
${color('--template=', 'cyan')}JSON Column order template (JSON object)
|
|
75
102
|
${color('--no-injection-protection', 'cyan')} Disable CSV injection protection
|
|
76
103
|
${color('--no-rfc4180', 'cyan')} Disable RFC 4180 compliance
|
|
77
|
-
${color('--max-records=', 'cyan')}N Maximum records to process
|
|
78
|
-
${color('--max-rows=', 'cyan')}N Maximum rows to process
|
|
104
|
+
${color('--max-records=', 'cyan')}N Maximum records to process
|
|
105
|
+
${color('--max-rows=', 'cyan')}N Maximum rows to process
|
|
79
106
|
${color('--pretty', 'cyan')} Pretty print JSON output
|
|
107
|
+
${color('--schema=', 'cyan')}JSON JSON schema for validation
|
|
108
|
+
${color('--transform=', 'cyan')}JS Custom transform function (JavaScript file)
|
|
109
|
+
|
|
110
|
+
${color('PREPROCESS OPTIONS:', 'bright')}
|
|
111
|
+
${color('--max-depth=', 'cyan')}N Maximum recursion depth (default: 5)
|
|
112
|
+
${color('--unwrap-arrays', 'cyan')} Unwrap arrays to strings
|
|
113
|
+
${color('--stringify-objects', 'cyan')} Stringify complex objects
|
|
114
|
+
|
|
115
|
+
${color('STREAMING OPTIONS:', 'bright')}
|
|
116
|
+
${color('--chunk-size=', 'cyan')}N Chunk size in bytes (default: 65536)
|
|
117
|
+
${color('--buffer-size=', 'cyan')}N Buffer size in records (default: 1000)
|
|
118
|
+
${color('--add-bom', 'cyan')} Add UTF-8 BOM for Excel compatibility
|
|
119
|
+
|
|
120
|
+
${color('BATCH OPTIONS:', 'bright')}
|
|
121
|
+
${color('--recursive', 'cyan')} Process directories recursively
|
|
122
|
+
${color('--pattern=', 'cyan')}GLOB File pattern to match
|
|
123
|
+
${color('--output-dir=', 'cyan')}DIR Output directory for batch processing
|
|
124
|
+
${color('--overwrite', 'cyan')} Overwrite existing files
|
|
125
|
+
${color('--parallel=', 'cyan')}N Parallel processing limit (default: 4)
|
|
126
|
+
|
|
127
|
+
${color('GENERAL OPTIONS:', 'bright')}
|
|
80
128
|
${color('--silent', 'cyan')} Suppress all output except errors
|
|
81
129
|
${color('--verbose', 'cyan')} Show detailed progress information
|
|
130
|
+
${color('--debug', 'cyan')} Show debug information
|
|
131
|
+
${color('--dry-run', 'cyan')} Show what would be done without actually doing it
|
|
82
132
|
|
|
83
133
|
${color('SECURITY FEATURES:', 'bright')}
|
|
84
134
|
• CSV injection protection (enabled by default)
|
|
85
135
|
• Path traversal protection
|
|
86
136
|
• Input validation and sanitization
|
|
87
137
|
• Size limits to prevent DoS attacks
|
|
138
|
+
• Schema validation support
|
|
88
139
|
|
|
89
|
-
${color('
|
|
90
|
-
•
|
|
91
|
-
•
|
|
92
|
-
•
|
|
140
|
+
${color('PERFORMANCE FEATURES:', 'bright')}
|
|
141
|
+
• Streaming for files >100MB
|
|
142
|
+
• Batch processing with parallel execution
|
|
143
|
+
• Memory-efficient preprocessing
|
|
144
|
+
• Configurable buffer sizes
|
|
93
145
|
|
|
94
146
|
${color('LEARN MORE:', 'dim')}
|
|
95
147
|
GitHub: https://github.com/Linol-Hamelton/jtcsv
|
|
96
148
|
Issues: https://github.com/Linol-Hamelton/jtcsv/issues
|
|
149
|
+
Documentation: https://github.com/Linol-Hamelton/jtcsv#readme
|
|
97
150
|
`);
|
|
98
151
|
}
|
|
99
152
|
|
|
100
153
|
function showVersion() {
|
|
101
154
|
console.log(`jtcsv v${VERSION}`);
|
|
155
|
+
console.log(`Node.js ${process.version}`);
|
|
156
|
+
console.log(`Platform: ${process.platform} ${process.arch}`);
|
|
102
157
|
}
|
|
103
158
|
|
|
159
|
+
// ============================================================================
|
|
160
|
+
// CONVERSION FUNCTIONS
|
|
161
|
+
// ============================================================================
|
|
162
|
+
|
|
104
163
|
async function convertJsonToCsv(inputFile, outputFile, options) {
|
|
105
164
|
const startTime = Date.now();
|
|
106
165
|
|
|
@@ -113,7 +172,9 @@ async function convertJsonToCsv(inputFile, outputFile, options) {
|
|
|
113
172
|
throw new Error('JSON data must be an array of objects');
|
|
114
173
|
}
|
|
115
174
|
|
|
116
|
-
|
|
175
|
+
if (!options.silent) {
|
|
176
|
+
console.log(color(`Converting ${jsonData.length.toLocaleString()} records...`, 'dim'));
|
|
177
|
+
}
|
|
117
178
|
|
|
118
179
|
// Prepare options for jtcsv
|
|
119
180
|
const jtcsvOptions = {
|
|
@@ -133,11 +194,18 @@ async function convertJsonToCsv(inputFile, outputFile, options) {
|
|
|
133
194
|
await fs.promises.writeFile(outputFile, csvData, 'utf8');
|
|
134
195
|
|
|
135
196
|
const elapsed = Date.now() - startTime;
|
|
136
|
-
|
|
137
|
-
|
|
197
|
+
if (!options.silent) {
|
|
198
|
+
console.log(color(`✓ Converted ${jsonData.length.toLocaleString()} records in ${elapsed}ms`, 'green'));
|
|
199
|
+
console.log(color(` Output: ${outputFile} (${csvData.length.toLocaleString()} bytes)`, 'dim'));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { records: jsonData.length, bytes: csvData.length, time: elapsed };
|
|
138
203
|
|
|
139
204
|
} catch (error) {
|
|
140
205
|
console.error(color(`✗ Error: ${error.message}`, 'red'));
|
|
206
|
+
if (options.debug) {
|
|
207
|
+
console.error(error.stack);
|
|
208
|
+
}
|
|
141
209
|
process.exit(1);
|
|
142
210
|
}
|
|
143
211
|
}
|
|
@@ -146,7 +214,9 @@ async function convertCsvToJson(inputFile, outputFile, options) {
|
|
|
146
214
|
const startTime = Date.now();
|
|
147
215
|
|
|
148
216
|
try {
|
|
149
|
-
|
|
217
|
+
if (!options.silent) {
|
|
218
|
+
console.log(color('Reading CSV file...', 'dim'));
|
|
219
|
+
}
|
|
150
220
|
|
|
151
221
|
// Prepare options for jtcsv
|
|
152
222
|
const jtcsvOptions = {
|
|
@@ -158,7 +228,9 @@ async function convertCsvToJson(inputFile, outputFile, options) {
|
|
|
158
228
|
trim: options.trim,
|
|
159
229
|
parseNumbers: options.parseNumbers,
|
|
160
230
|
parseBooleans: options.parseBooleans,
|
|
161
|
-
maxRows: options.maxRows
|
|
231
|
+
maxRows: options.maxRows,
|
|
232
|
+
useFastPath: options.useFastPath,
|
|
233
|
+
fastPathMode: options.fastPathMode
|
|
162
234
|
};
|
|
163
235
|
|
|
164
236
|
// Read and convert CSV
|
|
@@ -173,59 +245,224 @@ async function convertCsvToJson(inputFile, outputFile, options) {
|
|
|
173
245
|
await fs.promises.writeFile(outputFile, jsonOutput, 'utf8');
|
|
174
246
|
|
|
175
247
|
const elapsed = Date.now() - startTime;
|
|
176
|
-
|
|
177
|
-
|
|
248
|
+
if (!options.silent) {
|
|
249
|
+
console.log(color(`✓ Converted ${jsonData.length.toLocaleString()} rows in ${elapsed}ms`, 'green'));
|
|
250
|
+
console.log(color(` Output: ${outputFile} (${jsonOutput.length.toLocaleString()} bytes)`, 'dim'));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return { rows: jsonData.length, bytes: jsonOutput.length, time: elapsed };
|
|
254
|
+
|
|
255
|
+
} catch (error) {
|
|
256
|
+
console.error(color(`✗ Error: ${error.message}`, 'red'));
|
|
257
|
+
if (options.debug) {
|
|
258
|
+
console.error(error.stack);
|
|
259
|
+
}
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function saveAsJson(inputFile, outputFile, options) {
|
|
265
|
+
const startTime = Date.now();
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
// Read input file
|
|
269
|
+
const inputData = await fs.promises.readFile(inputFile, 'utf8');
|
|
270
|
+
const jsonData = JSON.parse(inputData);
|
|
271
|
+
|
|
272
|
+
if (!options.silent) {
|
|
273
|
+
console.log(color(`Saving ${Array.isArray(jsonData) ? jsonData.length.toLocaleString() + ' records' : 'object'}...`, 'dim'));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Prepare options for jtcsv
|
|
277
|
+
const jtcsvOptions = {
|
|
278
|
+
prettyPrint: options.pretty
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// Save as JSON
|
|
282
|
+
await jtcsv.saveAsJson(jsonData, outputFile, jtcsvOptions);
|
|
283
|
+
|
|
284
|
+
const elapsed = Date.now() - startTime;
|
|
285
|
+
if (!options.silent) {
|
|
286
|
+
console.log(color(`✓ Saved JSON in ${elapsed}ms`, 'green'));
|
|
287
|
+
console.log(color(` Output: ${outputFile}`, 'dim'));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return { time: elapsed };
|
|
291
|
+
|
|
292
|
+
} catch (error) {
|
|
293
|
+
console.error(color(`✗ Error: ${error.message}`, 'red'));
|
|
294
|
+
if (options.debug) {
|
|
295
|
+
console.error(error.stack);
|
|
296
|
+
}
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function preprocessJson(inputFile, outputFile, options) {
|
|
302
|
+
const startTime = Date.now();
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
// Read input file
|
|
306
|
+
const inputData = await fs.promises.readFile(inputFile, 'utf8');
|
|
307
|
+
const jsonData = JSON.parse(inputData);
|
|
308
|
+
|
|
309
|
+
if (!Array.isArray(jsonData)) {
|
|
310
|
+
throw new Error('JSON data must be an array of objects for preprocessing');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (!options.silent) {
|
|
314
|
+
console.log(color(`Preprocessing ${jsonData.length.toLocaleString()} records...`, 'dim'));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Preprocess data
|
|
318
|
+
const processedData = jtcsv.preprocessData(jsonData);
|
|
319
|
+
|
|
320
|
+
// Apply deep unwrap if needed
|
|
321
|
+
if (options.unwrapArrays || options.stringifyObjects) {
|
|
322
|
+
const maxDepth = options.maxDepth || 5;
|
|
323
|
+
processedData.forEach(item => {
|
|
324
|
+
for (const key in item) {
|
|
325
|
+
if (item[key] && typeof item[key] === 'object') {
|
|
326
|
+
item[key] = jtcsv.deepUnwrap(item[key], 0, maxDepth);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Format JSON
|
|
333
|
+
const jsonOutput = options.pretty
|
|
334
|
+
? JSON.stringify(processedData, null, 2)
|
|
335
|
+
: JSON.stringify(processedData);
|
|
336
|
+
|
|
337
|
+
// Write output file
|
|
338
|
+
await fs.promises.writeFile(outputFile, jsonOutput, 'utf8');
|
|
339
|
+
|
|
340
|
+
const elapsed = Date.now() - startTime;
|
|
341
|
+
if (!options.silent) {
|
|
342
|
+
console.log(color(`✓ Preprocessed ${jsonData.length.toLocaleString()} records in ${elapsed}ms`, 'green'));
|
|
343
|
+
console.log(color(` Output: ${outputFile} (${jsonOutput.length.toLocaleString()} bytes)`, 'dim'));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return { records: jsonData.length, bytes: jsonOutput.length, time: elapsed };
|
|
178
347
|
|
|
179
348
|
} catch (error) {
|
|
180
349
|
console.error(color(`✗ Error: ${error.message}`, 'red'));
|
|
350
|
+
if (options.debug) {
|
|
351
|
+
console.error(error.stack);
|
|
352
|
+
}
|
|
181
353
|
process.exit(1);
|
|
182
354
|
}
|
|
183
355
|
}
|
|
184
356
|
|
|
357
|
+
// ============================================================================
|
|
358
|
+
// STREAMING FUNCTIONS
|
|
359
|
+
// ============================================================================
|
|
360
|
+
|
|
185
361
|
async function streamJsonToCsv(inputFile, outputFile, options) {
|
|
186
362
|
const startTime = Date.now();
|
|
363
|
+
let recordCount = 0;
|
|
187
364
|
|
|
188
365
|
try {
|
|
189
|
-
|
|
366
|
+
if (!options.silent) {
|
|
367
|
+
console.log(color('Streaming JSON to CSV...', 'dim'));
|
|
368
|
+
}
|
|
190
369
|
|
|
191
370
|
// Create streams
|
|
192
371
|
const readStream = fs.createReadStream(inputFile, 'utf8');
|
|
193
372
|
const writeStream = fs.createWriteStream(outputFile, 'utf8');
|
|
194
373
|
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
374
|
+
// Add UTF-8 BOM if requested
|
|
375
|
+
if (options.addBOM) {
|
|
376
|
+
writeStream.write('\uFEFF');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Parse JSON stream
|
|
198
380
|
let buffer = '';
|
|
381
|
+
let isFirstChunk = true;
|
|
382
|
+
let headersWritten = false;
|
|
199
383
|
|
|
200
384
|
readStream.on('data', (chunk) => {
|
|
201
385
|
buffer += chunk;
|
|
202
386
|
|
|
203
|
-
//
|
|
387
|
+
// Try to parse complete JSON objects
|
|
204
388
|
const lines = buffer.split('\n');
|
|
205
389
|
buffer = lines.pop() || '';
|
|
206
390
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
391
|
+
for (const line of lines) {
|
|
392
|
+
if (line.trim()) {
|
|
393
|
+
try {
|
|
394
|
+
const obj = JSON.parse(line);
|
|
395
|
+
recordCount++;
|
|
396
|
+
|
|
397
|
+
// Write headers on first object
|
|
398
|
+
if (!headersWritten && options.includeHeaders !== false) {
|
|
399
|
+
const headers = Object.keys(obj);
|
|
400
|
+
writeStream.write(headers.join(options.delimiter || ';') + '\n');
|
|
401
|
+
headersWritten = true;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Write CSV row
|
|
405
|
+
const row = Object.values(obj).map(value => {
|
|
406
|
+
const str = String(value);
|
|
407
|
+
if (str.includes(options.delimiter || ';') || str.includes('"') || str.includes('\n')) {
|
|
408
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
409
|
+
}
|
|
410
|
+
return str;
|
|
411
|
+
}).join(options.delimiter || ';') + '\n';
|
|
412
|
+
|
|
413
|
+
writeStream.write(row);
|
|
414
|
+
|
|
415
|
+
// Show progress
|
|
416
|
+
if (options.verbose && recordCount % 10000 === 0) {
|
|
417
|
+
process.stdout.write(color(` Processed ${recordCount.toLocaleString()} records\r`, 'dim'));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
} catch (error) {
|
|
421
|
+
// Skip invalid JSON lines
|
|
422
|
+
if (options.debug) {
|
|
423
|
+
console.warn(color(` Warning: Skipping invalid JSON line: ${error.message}`, 'yellow'));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
210
427
|
}
|
|
211
428
|
});
|
|
212
429
|
|
|
213
430
|
readStream.on('end', async () => {
|
|
214
431
|
// Process remaining buffer
|
|
215
432
|
if (buffer.trim()) {
|
|
216
|
-
|
|
433
|
+
try {
|
|
434
|
+
const obj = JSON.parse(buffer);
|
|
435
|
+
recordCount++;
|
|
436
|
+
|
|
437
|
+
if (!headersWritten && options.includeHeaders !== false) {
|
|
438
|
+
const headers = Object.keys(obj);
|
|
439
|
+
writeStream.write(headers.join(options.delimiter || ';') + '\n');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const row = Object.values(obj).map(value => {
|
|
443
|
+
const str = String(value);
|
|
444
|
+
if (str.includes(options.delimiter || ';') || str.includes('"') || str.includes('\n')) {
|
|
445
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
446
|
+
}
|
|
447
|
+
return str;
|
|
448
|
+
}).join(options.delimiter || ';') + '\n';
|
|
449
|
+
|
|
450
|
+
writeStream.write(row);
|
|
451
|
+
} catch (error) {
|
|
452
|
+
// Skip invalid JSON
|
|
453
|
+
}
|
|
217
454
|
}
|
|
218
455
|
|
|
219
|
-
|
|
220
|
-
const inputData = await fs.promises.readFile(inputFile, 'utf8');
|
|
221
|
-
const jsonData = JSON.parse(inputData);
|
|
222
|
-
const csvData = jtcsv.jsonToCsv(jsonData, options);
|
|
456
|
+
writeStream.end();
|
|
223
457
|
|
|
224
|
-
|
|
458
|
+
// Wait for write stream to finish
|
|
459
|
+
await new Promise(resolve => writeStream.on('finish', resolve));
|
|
225
460
|
|
|
226
461
|
const elapsed = Date.now() - startTime;
|
|
227
|
-
|
|
228
|
-
|
|
462
|
+
if (!options.silent) {
|
|
463
|
+
console.log(color(`\n✓ Streamed ${recordCount.toLocaleString()} records in ${elapsed}ms`, 'green'));
|
|
464
|
+
console.log(color(` Output: ${outputFile}`, 'dim'));
|
|
465
|
+
}
|
|
229
466
|
});
|
|
230
467
|
|
|
231
468
|
readStream.on('error', (error) => {
|
|
@@ -233,35 +470,387 @@ async function streamJsonToCsv(inputFile, outputFile, options) {
|
|
|
233
470
|
process.exit(1);
|
|
234
471
|
});
|
|
235
472
|
|
|
473
|
+
writeStream.on('error', (error) => {
|
|
474
|
+
console.error(color(`✗ Write error: ${error.message}`, 'red'));
|
|
475
|
+
process.exit(1);
|
|
476
|
+
});
|
|
477
|
+
|
|
236
478
|
} catch (error) {
|
|
237
479
|
console.error(color(`✗ Error: ${error.message}`, 'red'));
|
|
480
|
+
if (options.debug) {
|
|
481
|
+
console.error(error.stack);
|
|
482
|
+
}
|
|
238
483
|
process.exit(1);
|
|
239
484
|
}
|
|
240
485
|
}
|
|
241
486
|
|
|
242
|
-
async function
|
|
487
|
+
async function streamCsvToJson(inputFile, outputFile, options) {
|
|
488
|
+
const startTime = Date.now();
|
|
489
|
+
let rowCount = 0;
|
|
490
|
+
|
|
243
491
|
try {
|
|
244
|
-
|
|
245
|
-
|
|
492
|
+
if (!options.silent) {
|
|
493
|
+
console.log(color('Streaming CSV to JSON...', 'dim'));
|
|
494
|
+
}
|
|
246
495
|
|
|
247
|
-
|
|
248
|
-
|
|
496
|
+
// Create streams
|
|
497
|
+
const readStream = fs.createReadStream(inputFile, 'utf8');
|
|
498
|
+
const writeStream = fs.createWriteStream(outputFile, 'utf8');
|
|
249
499
|
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
500
|
+
// Write JSON array opening bracket
|
|
501
|
+
writeStream.write('[\n');
|
|
502
|
+
|
|
503
|
+
let buffer = '';
|
|
504
|
+
let isFirstRow = true;
|
|
505
|
+
let headers = [];
|
|
506
|
+
|
|
507
|
+
readStream.on('data', (chunk) => {
|
|
508
|
+
buffer += chunk;
|
|
509
|
+
|
|
510
|
+
// Process complete lines
|
|
511
|
+
const lines = buffer.split('\n');
|
|
512
|
+
buffer = lines.pop() || '';
|
|
513
|
+
|
|
514
|
+
for (let i = 0; i < lines.length; i++) {
|
|
515
|
+
const line = lines[i].trim();
|
|
516
|
+
if (!line) continue;
|
|
517
|
+
|
|
518
|
+
rowCount++;
|
|
519
|
+
|
|
520
|
+
// Parse CSV line
|
|
521
|
+
const fields = parseCsvLineSimple(line, options.delimiter || ';');
|
|
522
|
+
|
|
523
|
+
// First row might be headers
|
|
524
|
+
if (rowCount === 1 && options.hasHeaders !== false) {
|
|
525
|
+
headers = fields;
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Create JSON object
|
|
530
|
+
const obj = {};
|
|
531
|
+
const fieldCount = Math.min(fields.length, headers.length);
|
|
532
|
+
|
|
533
|
+
for (let j = 0; j < fieldCount; j++) {
|
|
534
|
+
const header = headers[j] || `column${j + 1}`;
|
|
535
|
+
let value = fields[j];
|
|
536
|
+
|
|
537
|
+
// Parse numbers if enabled
|
|
538
|
+
if (options.parseNumbers && /^-?\d+(\.\d+)?$/.test(value)) {
|
|
539
|
+
const num = parseFloat(value);
|
|
540
|
+
if (!isNaN(num)) {
|
|
541
|
+
value = num;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Parse booleans if enabled
|
|
546
|
+
if (options.parseBooleans) {
|
|
547
|
+
const lowerValue = value.toLowerCase();
|
|
548
|
+
if (lowerValue === 'true') value = true;
|
|
549
|
+
if (lowerValue === 'false') value = false;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
obj[header] = value;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Write JSON object
|
|
556
|
+
const jsonStr = JSON.stringify(obj);
|
|
557
|
+
if (!isFirstRow) {
|
|
558
|
+
writeStream.write(',\n');
|
|
559
|
+
}
|
|
560
|
+
writeStream.write(' ' + jsonStr);
|
|
561
|
+
isFirstRow = false;
|
|
562
|
+
|
|
563
|
+
// Show progress
|
|
564
|
+
if (options.verbose && rowCount % 10000 === 0) {
|
|
565
|
+
process.stdout.write(color(` Processed ${rowCount.toLocaleString()} rows\r`, 'dim'));
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
readStream.on('end', async () => {
|
|
571
|
+
// Process remaining buffer
|
|
572
|
+
if (buffer.trim()) {
|
|
573
|
+
const fields = parseCsvLineSimple(buffer.trim(), options.delimiter || ';');
|
|
574
|
+
|
|
575
|
+
if (fields.length > 0) {
|
|
576
|
+
rowCount++;
|
|
577
|
+
|
|
578
|
+
// Skip if it's headers
|
|
579
|
+
if (!(rowCount === 1 && options.hasHeaders !== false)) {
|
|
580
|
+
const obj = {};
|
|
581
|
+
const fieldCount = Math.min(fields.length, headers.length);
|
|
582
|
+
|
|
583
|
+
for (let j = 0; j < fieldCount; j++) {
|
|
584
|
+
const header = headers[j] || `column${j + 1}`;
|
|
585
|
+
obj[header] = fields[j];
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const jsonStr = JSON.stringify(obj);
|
|
589
|
+
if (!isFirstRow) {
|
|
590
|
+
writeStream.write(',\n');
|
|
591
|
+
}
|
|
592
|
+
writeStream.write(' ' + jsonStr);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Write JSON array closing bracket
|
|
598
|
+
writeStream.write('\n]');
|
|
599
|
+
writeStream.end();
|
|
600
|
+
|
|
601
|
+
// Wait for write stream to finish
|
|
602
|
+
await new Promise(resolve => writeStream.on('finish', resolve));
|
|
603
|
+
|
|
604
|
+
const elapsed = Date.now() - startTime;
|
|
605
|
+
if (!options.silent) {
|
|
606
|
+
console.log(color(`\n✓ Streamed ${(rowCount - (options.hasHeaders !== false ? 1 : 0)).toLocaleString()} rows in ${elapsed}ms`, 'green'));
|
|
607
|
+
console.log(color(` Output: ${outputFile}`, 'dim'));
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
readStream.on('error', (error) => {
|
|
612
|
+
console.error(color(`✗ Stream error: ${error.message}`, 'red'));
|
|
613
|
+
process.exit(1);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
writeStream.on('error', (error) => {
|
|
617
|
+
console.error(color(`✗ Write error: ${error.message}`, 'red'));
|
|
618
|
+
process.exit(1);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
} catch (error) {
|
|
622
|
+
console.error(color(`✗ Error: ${error.message}`, 'red'));
|
|
623
|
+
if (options.debug) {
|
|
624
|
+
console.error(error.stack);
|
|
625
|
+
}
|
|
626
|
+
process.exit(1);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Simple CSV line parser for streaming
|
|
631
|
+
function parseCsvLineSimple(line, delimiter) {
|
|
632
|
+
const fields = [];
|
|
633
|
+
let currentField = '';
|
|
634
|
+
let inQuotes = false;
|
|
635
|
+
|
|
636
|
+
for (let i = 0; i < line.length; i++) {
|
|
637
|
+
const char = line[i];
|
|
638
|
+
|
|
639
|
+
if (char === '"') {
|
|
640
|
+
if (inQuotes && i + 1 < line.length && line[i + 1] === '"') {
|
|
641
|
+
// Escaped quote
|
|
642
|
+
currentField += '"';
|
|
643
|
+
i++;
|
|
644
|
+
} else {
|
|
645
|
+
// Toggle quotes
|
|
646
|
+
inQuotes = !inQuotes;
|
|
647
|
+
}
|
|
648
|
+
} else if (char === delimiter && !inQuotes) {
|
|
649
|
+
fields.push(currentField);
|
|
650
|
+
currentField = '';
|
|
651
|
+
} else {
|
|
652
|
+
currentField += char;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
fields.push(currentField);
|
|
657
|
+
return fields;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// ============================================================================
|
|
661
|
+
// BATCH PROCESSING FUNCTIONS
|
|
662
|
+
// ============================================================================
|
|
663
|
+
|
|
664
|
+
async function batchJsonToCsv(inputPattern, outputDir, options) {
|
|
665
|
+
const startTime = Date.now();
|
|
666
|
+
|
|
667
|
+
try {
|
|
668
|
+
const glob = require('glob');
|
|
669
|
+
const files = glob.sync(inputPattern, {
|
|
670
|
+
absolute: true,
|
|
671
|
+
nodir: true
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
if (files.length === 0) {
|
|
675
|
+
console.error(color(`✗ No files found matching pattern: ${inputPattern}`, 'red'));
|
|
676
|
+
process.exit(1);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (!options.silent) {
|
|
680
|
+
console.log(color(`Found ${files.length} files to process...`, 'dim'));
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Create output directory if it doesn't exist
|
|
684
|
+
await fs.promises.mkdir(outputDir, { recursive: true });
|
|
685
|
+
|
|
686
|
+
const results = [];
|
|
687
|
+
const parallelLimit = options.parallel || 4;
|
|
688
|
+
|
|
689
|
+
// Process files in parallel batches
|
|
690
|
+
for (let i = 0; i < files.length; i += parallelLimit) {
|
|
691
|
+
const batch = files.slice(i, i + parallelLimit);
|
|
692
|
+
const promises = batch.map(async (file) => {
|
|
693
|
+
const fileName = path.basename(file, '.json');
|
|
694
|
+
const outputFile = path.join(outputDir, `${fileName}.csv`);
|
|
695
|
+
|
|
696
|
+
if (!options.silent && options.verbose) {
|
|
697
|
+
console.log(color(` Processing: ${file}`, 'dim'));
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
try {
|
|
701
|
+
const result = await convertJsonToCsv(file, outputFile, {
|
|
702
|
+
...options,
|
|
703
|
+
silent: true // Suppress individual file output
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
if (!options.silent) {
|
|
707
|
+
console.log(color(` ✓ ${fileName}.json → ${fileName}.csv (${result.records} records)`, 'green'));
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return { file, success: true, ...result };
|
|
711
|
+
} catch (error) {
|
|
712
|
+
if (!options.silent) {
|
|
713
|
+
console.log(color(` ✗ ${fileName}.json: ${error.message}`, 'red'));
|
|
714
|
+
}
|
|
715
|
+
return { file, success: false, error: error.message };
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
const batchResults = await Promise.all(promises);
|
|
720
|
+
results.push(...batchResults);
|
|
721
|
+
|
|
722
|
+
if (!options.silent) {
|
|
723
|
+
const processed = i + batch.length;
|
|
724
|
+
const percent = Math.round((processed / files.length) * 100);
|
|
725
|
+
console.log(color(` Progress: ${processed}/${files.length} (${percent}%)`, 'dim'));
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const elapsed = Date.now() - startTime;
|
|
730
|
+
const successful = results.filter(r => r.success).length;
|
|
731
|
+
const totalRecords = results.filter(r => r.success).reduce((sum, r) => sum + (r.records || 0), 0);
|
|
732
|
+
|
|
733
|
+
if (!options.silent) {
|
|
734
|
+
console.log(color(`\n✓ Batch processing completed in ${elapsed}ms`, 'green'));
|
|
735
|
+
console.log(color(` Successful: ${successful}/${files.length} files`, 'dim'));
|
|
736
|
+
console.log(color(` Total records: ${totalRecords.toLocaleString()}`, 'dim'));
|
|
737
|
+
console.log(color(` Output directory: ${outputDir}`, 'dim'));
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return {
|
|
741
|
+
totalFiles: files.length,
|
|
742
|
+
successful,
|
|
743
|
+
totalRecords,
|
|
744
|
+
time: elapsed,
|
|
745
|
+
results
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
} catch (error) {
|
|
749
|
+
console.error(color(`✗ Batch processing error: ${error.message}`, 'red'));
|
|
750
|
+
if (options.debug) {
|
|
751
|
+
console.error(error.stack);
|
|
752
|
+
}
|
|
753
|
+
process.exit(1);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
async function batchCsvToJson(inputPattern, outputDir, options) {
|
|
758
|
+
const startTime = Date.now();
|
|
759
|
+
|
|
760
|
+
try {
|
|
761
|
+
const glob = require('glob');
|
|
762
|
+
const files = glob.sync(inputPattern, {
|
|
763
|
+
absolute: true,
|
|
764
|
+
nodir: true
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
if (files.length === 0) {
|
|
768
|
+
console.error(color(`✗ No files found matching pattern: ${inputPattern}`, 'red'));
|
|
769
|
+
process.exit(1);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (!options.silent) {
|
|
773
|
+
console.log(color(`Found ${files.length} files to process...`, 'dim'));
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Create output directory if it doesn't exist
|
|
777
|
+
await fs.promises.mkdir(outputDir, { recursive: true });
|
|
778
|
+
|
|
779
|
+
const results = [];
|
|
780
|
+
const parallelLimit = options.parallel || 4;
|
|
781
|
+
|
|
782
|
+
// Process files in parallel batches
|
|
783
|
+
for (let i = 0; i < files.length; i += parallelLimit) {
|
|
784
|
+
const batch = files.slice(i, i + parallelLimit);
|
|
785
|
+
const promises = batch.map(async (file) => {
|
|
786
|
+
const fileName = path.basename(file, '.csv');
|
|
787
|
+
const outputFile = path.join(outputDir, `${fileName}.json`);
|
|
788
|
+
|
|
789
|
+
if (!options.silent && options.verbose) {
|
|
790
|
+
console.log(color(` Processing: ${file}`, 'dim'));
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
try {
|
|
794
|
+
const result = await convertCsvToJson(file, outputFile, {
|
|
795
|
+
...options,
|
|
796
|
+
silent: true // Suppress individual file output
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
if (!options.silent) {
|
|
800
|
+
console.log(color(` ✓ ${fileName}.csv → ${fileName}.json (${result.rows} rows)`, 'green'));
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return { file, success: true, ...result };
|
|
804
|
+
} catch (error) {
|
|
805
|
+
if (!options.silent) {
|
|
806
|
+
console.log(color(` ✗ ${fileName}.csv: ${error.message}`, 'red'));
|
|
807
|
+
}
|
|
808
|
+
return { file, success: false, error: error.message };
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
const batchResults = await Promise.all(promises);
|
|
813
|
+
results.push(...batchResults);
|
|
814
|
+
|
|
815
|
+
if (!options.silent) {
|
|
816
|
+
const processed = i + batch.length;
|
|
817
|
+
const percent = Math.round((processed / files.length) * 100);
|
|
818
|
+
console.log(color(` Progress: ${processed}/${files.length} (${percent}%)`, 'dim'));
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const elapsed = Date.now() - startTime;
|
|
823
|
+
const successful = results.filter(r => r.success).length;
|
|
824
|
+
const totalRows = results.filter(r => r.success).reduce((sum, r) => sum + (r.rows || 0), 0);
|
|
825
|
+
|
|
826
|
+
if (!options.silent) {
|
|
827
|
+
console.log(color(`\n✓ Batch processing completed in ${elapsed}ms`, 'green'));
|
|
828
|
+
console.log(color(` Successful: ${successful}/${files.length} files`, 'dim'));
|
|
829
|
+
console.log(color(` Total rows: ${totalRows.toLocaleString()}`, 'dim'));
|
|
830
|
+
console.log(color(` Output directory: ${outputDir}`, 'dim'));
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
return {
|
|
834
|
+
totalFiles: files.length,
|
|
835
|
+
successful,
|
|
836
|
+
totalRows,
|
|
837
|
+
time: elapsed,
|
|
838
|
+
results
|
|
839
|
+
};
|
|
254
840
|
|
|
255
841
|
} catch (error) {
|
|
256
|
-
console.error(color(
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
console.log(color(' jtcsv help', 'cyan'));
|
|
842
|
+
console.error(color(`✗ Batch processing error: ${error.message}`, 'red'));
|
|
843
|
+
if (options.debug) {
|
|
844
|
+
console.error(error.stack);
|
|
845
|
+
}
|
|
261
846
|
process.exit(1);
|
|
262
847
|
}
|
|
263
848
|
}
|
|
264
849
|
|
|
850
|
+
// ============================================================================
|
|
851
|
+
// OPTIONS PARSING
|
|
852
|
+
// ============================================================================
|
|
853
|
+
|
|
265
854
|
function parseOptions(args) {
|
|
266
855
|
const options = {
|
|
267
856
|
delimiter: ';',
|
|
@@ -274,13 +863,30 @@ function parseOptions(args) {
|
|
|
274
863
|
trim: true,
|
|
275
864
|
parseNumbers: false,
|
|
276
865
|
parseBooleans: false,
|
|
866
|
+
useFastPath: true,
|
|
867
|
+
fastPathMode: 'objects',
|
|
277
868
|
preventCsvInjection: true,
|
|
278
869
|
rfc4180Compliant: true,
|
|
279
870
|
maxRecords: undefined,
|
|
280
871
|
maxRows: undefined,
|
|
872
|
+
maxDepth: 5,
|
|
281
873
|
pretty: false,
|
|
282
874
|
silent: false,
|
|
283
|
-
verbose: false
|
|
875
|
+
verbose: false,
|
|
876
|
+
debug: false,
|
|
877
|
+
dryRun: false,
|
|
878
|
+
addBOM: false,
|
|
879
|
+
unwrapArrays: false,
|
|
880
|
+
stringifyObjects: false,
|
|
881
|
+
recursive: false,
|
|
882
|
+
pattern: '**/*',
|
|
883
|
+
outputDir: './output',
|
|
884
|
+
overwrite: false,
|
|
885
|
+
parallel: 4,
|
|
886
|
+
chunkSize: 65536,
|
|
887
|
+
bufferSize: 1000,
|
|
888
|
+
schema: undefined,
|
|
889
|
+
transform: undefined
|
|
284
890
|
};
|
|
285
891
|
|
|
286
892
|
const files = [];
|
|
@@ -294,7 +900,7 @@ function parseOptions(args) {
|
|
|
294
900
|
switch (key) {
|
|
295
901
|
case 'delimiter':
|
|
296
902
|
options.delimiter = value || ',';
|
|
297
|
-
options.autoDetect = false;
|
|
903
|
+
options.autoDetect = false;
|
|
298
904
|
break;
|
|
299
905
|
case 'auto-detect':
|
|
300
906
|
options.autoDetect = value !== 'false';
|
|
@@ -315,26 +921,34 @@ function parseOptions(args) {
|
|
|
315
921
|
case 'no-trim':
|
|
316
922
|
options.trim = false;
|
|
317
923
|
break;
|
|
924
|
+
case 'no-fast-path':
|
|
925
|
+
options.useFastPath = false;
|
|
926
|
+
break;
|
|
927
|
+
case 'fast-path':
|
|
928
|
+
options.useFastPath = value !== 'false';
|
|
929
|
+
break;
|
|
930
|
+
case 'fast-path-mode':
|
|
931
|
+
options.fastPathMode = value || 'objects';
|
|
932
|
+
if (options.fastPathMode !== 'objects' && options.fastPathMode !== 'compact') {
|
|
933
|
+
throw new Error('Invalid --fast-path-mode value (objects|compact)');
|
|
934
|
+
}
|
|
935
|
+
break;
|
|
318
936
|
case 'rename':
|
|
319
937
|
try {
|
|
320
|
-
// Handle both quoted and unquoted JSON
|
|
321
938
|
const jsonStr = value || '{}';
|
|
322
|
-
// Remove surrounding single quotes if present
|
|
323
939
|
const cleanStr = jsonStr.replace(/^'|'$/g, '').replace(/^"|"$/g, '');
|
|
324
940
|
options.renameMap = JSON.parse(cleanStr);
|
|
325
941
|
} catch (e) {
|
|
326
|
-
throw new Error(`Invalid JSON in --rename option: ${e.message}
|
|
942
|
+
throw new Error(`Invalid JSON in --rename option: ${e.message}`);
|
|
327
943
|
}
|
|
328
944
|
break;
|
|
329
945
|
case 'template':
|
|
330
946
|
try {
|
|
331
|
-
// Handle both quoted and unquoted JSON
|
|
332
947
|
const jsonStr = value || '{}';
|
|
333
|
-
// Remove surrounding single quotes if present
|
|
334
948
|
const cleanStr = jsonStr.replace(/^'|'$/g, '').replace(/^"|"$/g, '');
|
|
335
949
|
options.template = JSON.parse(cleanStr);
|
|
336
950
|
} catch (e) {
|
|
337
|
-
throw new Error(`Invalid JSON in --template option: ${e.message}
|
|
951
|
+
throw new Error(`Invalid JSON in --template option: ${e.message}`);
|
|
338
952
|
}
|
|
339
953
|
break;
|
|
340
954
|
case 'no-injection-protection':
|
|
@@ -349,6 +963,9 @@ function parseOptions(args) {
|
|
|
349
963
|
case 'max-rows':
|
|
350
964
|
options.maxRows = parseInt(value, 10);
|
|
351
965
|
break;
|
|
966
|
+
case 'max-depth':
|
|
967
|
+
options.maxDepth = parseInt(value, 10) || 5;
|
|
968
|
+
break;
|
|
352
969
|
case 'pretty':
|
|
353
970
|
options.pretty = true;
|
|
354
971
|
break;
|
|
@@ -358,6 +975,54 @@ function parseOptions(args) {
|
|
|
358
975
|
case 'verbose':
|
|
359
976
|
options.verbose = true;
|
|
360
977
|
break;
|
|
978
|
+
case 'debug':
|
|
979
|
+
options.debug = true;
|
|
980
|
+
break;
|
|
981
|
+
case 'dry-run':
|
|
982
|
+
options.dryRun = true;
|
|
983
|
+
break;
|
|
984
|
+
case 'add-bom':
|
|
985
|
+
options.addBOM = true;
|
|
986
|
+
break;
|
|
987
|
+
case 'unwrap-arrays':
|
|
988
|
+
options.unwrapArrays = true;
|
|
989
|
+
break;
|
|
990
|
+
case 'stringify-objects':
|
|
991
|
+
options.stringifyObjects = true;
|
|
992
|
+
break;
|
|
993
|
+
case 'recursive':
|
|
994
|
+
options.recursive = true;
|
|
995
|
+
break;
|
|
996
|
+
case 'pattern':
|
|
997
|
+
options.pattern = value || '**/*';
|
|
998
|
+
break;
|
|
999
|
+
case 'output-dir':
|
|
1000
|
+
options.outputDir = value || './output';
|
|
1001
|
+
break;
|
|
1002
|
+
case 'overwrite':
|
|
1003
|
+
options.overwrite = true;
|
|
1004
|
+
break;
|
|
1005
|
+
case 'parallel':
|
|
1006
|
+
options.parallel = parseInt(value, 10) || 4;
|
|
1007
|
+
break;
|
|
1008
|
+
case 'chunk-size':
|
|
1009
|
+
options.chunkSize = parseInt(value, 10) || 65536;
|
|
1010
|
+
break;
|
|
1011
|
+
case 'buffer-size':
|
|
1012
|
+
options.bufferSize = parseInt(value, 10) || 1000;
|
|
1013
|
+
break;
|
|
1014
|
+
case 'schema':
|
|
1015
|
+
try {
|
|
1016
|
+
const jsonStr = value || '{}';
|
|
1017
|
+
const cleanStr = jsonStr.replace(/^'|'$/g, '').replace(/^"|"$/g, '');
|
|
1018
|
+
options.schema = JSON.parse(cleanStr);
|
|
1019
|
+
} catch (e) {
|
|
1020
|
+
throw new Error(`Invalid JSON in --schema option: ${e.message}`);
|
|
1021
|
+
}
|
|
1022
|
+
break;
|
|
1023
|
+
case 'transform':
|
|
1024
|
+
options.transform = value;
|
|
1025
|
+
break;
|
|
361
1026
|
}
|
|
362
1027
|
} else if (!arg.startsWith('-')) {
|
|
363
1028
|
files.push(arg);
|
|
@@ -367,6 +1032,151 @@ function parseOptions(args) {
|
|
|
367
1032
|
return { options, files };
|
|
368
1033
|
}
|
|
369
1034
|
|
|
1035
|
+
// ============================================================================
|
|
1036
|
+
// TUI LAUNCHER
|
|
1037
|
+
// ============================================================================
|
|
1038
|
+
|
|
1039
|
+
async function launchTUI() {
|
|
1040
|
+
try {
|
|
1041
|
+
console.log(color('Launching Terminal User Interface...', 'cyan'));
|
|
1042
|
+
console.log(color('Press Ctrl+Q to exit', 'dim'));
|
|
1043
|
+
|
|
1044
|
+
const JtcsvTUI = require('@jtcsv/tui');
|
|
1045
|
+
const tui = new JtcsvTUI();
|
|
1046
|
+
tui.start();
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
if (error.code === 'MODULE_NOT_FOUND') {
|
|
1049
|
+
console.error(color('Error: @jtcsv/tui is not installed', 'red'));
|
|
1050
|
+
console.log(color('Install it with:', 'dim'));
|
|
1051
|
+
console.log(color(' npm install @jtcsv/tui', 'cyan'));
|
|
1052
|
+
console.log(color('\nOr use the CLI interface instead:', 'dim'));
|
|
1053
|
+
console.log(color(' jtcsv help', 'cyan'));
|
|
1054
|
+
} else {
|
|
1055
|
+
console.error(color(`Error: ${error.message}`, 'red'));
|
|
1056
|
+
}
|
|
1057
|
+
process.exit(1);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
async function startBasicTUI() {
|
|
1062
|
+
const readline = require('readline');
|
|
1063
|
+
|
|
1064
|
+
const rl = readline.createInterface({
|
|
1065
|
+
input: process.stdin,
|
|
1066
|
+
output: process.stdout
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
console.clear();
|
|
1070
|
+
console.log(color('╔══════════════════════════════════════╗', 'cyan'));
|
|
1071
|
+
console.log(color('║ JTCSV Terminal Interface ║', 'cyan'));
|
|
1072
|
+
console.log(color('╚══════════════════════════════════════╝', 'cyan'));
|
|
1073
|
+
console.log();
|
|
1074
|
+
console.log(color('Select operation:', 'bright'));
|
|
1075
|
+
console.log(' 1. JSON → CSV');
|
|
1076
|
+
console.log(' 2. CSV → JSON');
|
|
1077
|
+
console.log(' 3. Preprocess JSON');
|
|
1078
|
+
console.log(' 4. Batch Processing');
|
|
1079
|
+
console.log(' 5. Exit');
|
|
1080
|
+
console.log();
|
|
1081
|
+
|
|
1082
|
+
rl.question(color('Enter choice (1-5): ', 'cyan'), async (choice) => {
|
|
1083
|
+
switch (choice) {
|
|
1084
|
+
case '1':
|
|
1085
|
+
await runJsonToCsvTUI(rl);
|
|
1086
|
+
break;
|
|
1087
|
+
case '2':
|
|
1088
|
+
await runCsvToJsonTUI(rl);
|
|
1089
|
+
break;
|
|
1090
|
+
case '3':
|
|
1091
|
+
console.log(color('Preprocess feature coming soon...', 'yellow'));
|
|
1092
|
+
rl.close();
|
|
1093
|
+
break;
|
|
1094
|
+
case '4':
|
|
1095
|
+
console.log(color('Batch processing coming soon...', 'yellow'));
|
|
1096
|
+
rl.close();
|
|
1097
|
+
break;
|
|
1098
|
+
case '5':
|
|
1099
|
+
console.log(color('Goodbye!', 'green'));
|
|
1100
|
+
rl.close();
|
|
1101
|
+
process.exit(0);
|
|
1102
|
+
break;
|
|
1103
|
+
default:
|
|
1104
|
+
console.log(color('Invalid choice', 'red'));
|
|
1105
|
+
rl.close();
|
|
1106
|
+
process.exit(1);
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
async function runJsonToCsvTUI(rl) {
|
|
1112
|
+
console.clear();
|
|
1113
|
+
console.log(color('JSON → CSV Conversion', 'cyan'));
|
|
1114
|
+
console.log();
|
|
1115
|
+
|
|
1116
|
+
rl.question('Input JSON file: ', (inputFile) => {
|
|
1117
|
+
rl.question('Output CSV file: ', async (outputFile) => {
|
|
1118
|
+
rl.question('Delimiter (default: ;): ', async (delimiter) => {
|
|
1119
|
+
try {
|
|
1120
|
+
console.log(color('\nConverting...', 'dim'));
|
|
1121
|
+
|
|
1122
|
+
const result = await convertJsonToCsv(inputFile, outputFile, {
|
|
1123
|
+
delimiter: delimiter || ';',
|
|
1124
|
+
silent: false
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
console.log(color('\n✓ Conversion complete!', 'green'));
|
|
1128
|
+
rl.question('\nPress Enter to continue...', () => {
|
|
1129
|
+
rl.close();
|
|
1130
|
+
startBasicTUI();
|
|
1131
|
+
});
|
|
1132
|
+
} catch (error) {
|
|
1133
|
+
console.error(color(`✗ Error: ${error.message}`, 'red'));
|
|
1134
|
+
rl.close();
|
|
1135
|
+
process.exit(1);
|
|
1136
|
+
}
|
|
1137
|
+
});
|
|
1138
|
+
});
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
async function runCsvToJsonTUI(rl) {
|
|
1143
|
+
console.clear();
|
|
1144
|
+
console.log(color('CSV → JSON Conversion', 'cyan'));
|
|
1145
|
+
console.log();
|
|
1146
|
+
|
|
1147
|
+
rl.question('Input CSV file: ', (inputFile) => {
|
|
1148
|
+
rl.question('Output JSON file: ', async (outputFile) => {
|
|
1149
|
+
rl.question('Delimiter (default: ;): ', async (delimiter) => {
|
|
1150
|
+
rl.question('Pretty print? (y/n): ', async (pretty) => {
|
|
1151
|
+
try {
|
|
1152
|
+
console.log(color('\nConverting...', 'dim'));
|
|
1153
|
+
|
|
1154
|
+
const result = await convertCsvToJson(inputFile, outputFile, {
|
|
1155
|
+
delimiter: delimiter || ';',
|
|
1156
|
+
pretty: pretty.toLowerCase() === 'y',
|
|
1157
|
+
silent: false
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
console.log(color('\n✓ Conversion complete!', 'green'));
|
|
1161
|
+
rl.question('\nPress Enter to continue...', () => {
|
|
1162
|
+
rl.close();
|
|
1163
|
+
startBasicTUI();
|
|
1164
|
+
});
|
|
1165
|
+
} catch (error) {
|
|
1166
|
+
console.error(color(`✗ Error: ${error.message}`, 'red'));
|
|
1167
|
+
rl.close();
|
|
1168
|
+
process.exit(1);
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
});
|
|
1172
|
+
});
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// ============================================================================
|
|
1177
|
+
// MAIN FUNCTION
|
|
1178
|
+
// ============================================================================
|
|
1179
|
+
|
|
370
1180
|
async function main() {
|
|
371
1181
|
const args = process.argv.slice(2);
|
|
372
1182
|
|
|
@@ -378,6 +1188,15 @@ async function main() {
|
|
|
378
1188
|
const command = args[0].toLowerCase();
|
|
379
1189
|
const { options, files } = parseOptions(args.slice(1));
|
|
380
1190
|
|
|
1191
|
+
// Handle dry run
|
|
1192
|
+
if (options.dryRun) {
|
|
1193
|
+
console.log(color('DRY RUN - No files will be modified', 'yellow'));
|
|
1194
|
+
console.log(`Command: ${command}`);
|
|
1195
|
+
console.log(`Files: ${files.join(', ')}`);
|
|
1196
|
+
console.log(`Options:`, options);
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
381
1200
|
// Suppress output if silent mode
|
|
382
1201
|
if (options.silent) {
|
|
383
1202
|
console.log = () => {};
|
|
@@ -385,63 +1204,147 @@ async function main() {
|
|
|
385
1204
|
}
|
|
386
1205
|
|
|
387
1206
|
switch (command) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
1207
|
+
// Main conversion commands
|
|
1208
|
+
case 'json-to-csv':
|
|
1209
|
+
case 'json2csv':
|
|
1210
|
+
if (files.length < 2) {
|
|
1211
|
+
console.error(color('Error: Input and output files required', 'red'));
|
|
1212
|
+
console.log(color('Usage: jtcsv json-to-csv input.json output.csv', 'cyan'));
|
|
1213
|
+
process.exit(1);
|
|
1214
|
+
}
|
|
1215
|
+
await convertJsonToCsv(files[0], files[1], options);
|
|
1216
|
+
break;
|
|
397
1217
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
1218
|
+
case 'csv-to-json':
|
|
1219
|
+
case 'csv2json':
|
|
1220
|
+
if (files.length < 2) {
|
|
1221
|
+
console.error(color('Error: Input and output files required', 'red'));
|
|
1222
|
+
console.log(color('Usage: jtcsv csv-to-json input.csv output.json', 'cyan'));
|
|
1223
|
+
process.exit(1);
|
|
1224
|
+
}
|
|
1225
|
+
await convertCsvToJson(files[0], files[1], options);
|
|
1226
|
+
break;
|
|
407
1227
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
await streamJsonToCsv(files[0], files[1], options);
|
|
417
|
-
} else {
|
|
418
|
-
console.error(color('Error: Invalid streaming command', 'red'));
|
|
419
|
-
process.exit(1);
|
|
420
|
-
}
|
|
421
|
-
break;
|
|
1228
|
+
case 'save-json':
|
|
1229
|
+
if (files.length < 2) {
|
|
1230
|
+
console.error(color('Error: Input and output files required', 'red'));
|
|
1231
|
+
console.log(color('Usage: jtcsv save-json input.json output.json', 'cyan'));
|
|
1232
|
+
process.exit(1);
|
|
1233
|
+
}
|
|
1234
|
+
await saveAsJson(files[0], files[1], options);
|
|
1235
|
+
break;
|
|
422
1236
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
1237
|
+
case 'preprocess':
|
|
1238
|
+
if (files.length < 2) {
|
|
1239
|
+
console.error(color('Error: Input and output files required', 'red'));
|
|
1240
|
+
console.log(color('Usage: jtcsv preprocess input.json output.json', 'cyan'));
|
|
1241
|
+
process.exit(1);
|
|
1242
|
+
}
|
|
1243
|
+
await preprocessJson(files[0], files[1], options);
|
|
1244
|
+
break;
|
|
426
1245
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
1246
|
+
// Streaming commands
|
|
1247
|
+
case 'stream':
|
|
1248
|
+
if (args.length < 2) {
|
|
1249
|
+
console.error(color('Error: Streaming mode requires subcommand', 'red'));
|
|
1250
|
+
console.log(color('Usage: jtcsv stream [json-to-csv|csv-to-json|file-to-csv|file-to-json]', 'cyan'));
|
|
1251
|
+
process.exit(1);
|
|
1252
|
+
}
|
|
430
1253
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
1254
|
+
const streamCommand = args[1].toLowerCase();
|
|
1255
|
+
if (streamCommand === 'json-to-csv' && files.length >= 2) {
|
|
1256
|
+
await streamJsonToCsv(files[0], files[1], options);
|
|
1257
|
+
} else if (streamCommand === 'csv-to-json' && files.length >= 2) {
|
|
1258
|
+
await streamCsvToJson(files[0], files[1], options);
|
|
1259
|
+
} else if (streamCommand === 'file-to-csv' && files.length >= 2) {
|
|
1260
|
+
// Use jtcsv streaming API if available
|
|
1261
|
+
try {
|
|
1262
|
+
const readStream = fs.createReadStream(files[0], 'utf8');
|
|
1263
|
+
const writeStream = fs.createWriteStream(files[1], 'utf8');
|
|
1264
|
+
|
|
1265
|
+
if (options.addBOM) {
|
|
1266
|
+
writeStream.write('\uFEFF');
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
const transformStream = jtcsv.createJsonToCsvStream(options);
|
|
1270
|
+
await pipeline(readStream, transformStream, writeStream);
|
|
1271
|
+
|
|
1272
|
+
console.log(color('✓ File streamed successfully', 'green'));
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
console.error(color(`✗ Streaming error: ${error.message}`, 'red'));
|
|
1275
|
+
process.exit(1);
|
|
1276
|
+
}
|
|
1277
|
+
} else if (streamCommand === 'file-to-json' && files.length >= 2) {
|
|
1278
|
+
// Use jtcsv streaming API if available
|
|
1279
|
+
try {
|
|
1280
|
+
const readStream = fs.createReadStream(files[0], 'utf8');
|
|
1281
|
+
const writeStream = fs.createWriteStream(files[1], 'utf8');
|
|
1282
|
+
|
|
1283
|
+
const transformStream = jtcsv.createCsvToJsonStream(options);
|
|
1284
|
+
await pipeline(readStream, transformStream, writeStream);
|
|
1285
|
+
|
|
1286
|
+
console.log(color('✓ File streamed successfully', 'green'));
|
|
1287
|
+
} catch (error) {
|
|
1288
|
+
console.error(color(`✗ Streaming error: ${error.message}`, 'red'));
|
|
1289
|
+
process.exit(1);
|
|
1290
|
+
}
|
|
1291
|
+
} else {
|
|
1292
|
+
console.error(color('Error: Invalid streaming command or missing files', 'red'));
|
|
1293
|
+
process.exit(1);
|
|
1294
|
+
}
|
|
1295
|
+
break;
|
|
436
1296
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
1297
|
+
// Batch processing commands
|
|
1298
|
+
case 'batch':
|
|
1299
|
+
if (args.length < 2) {
|
|
1300
|
+
console.error(color('Error: Batch mode requires subcommand', 'red'));
|
|
1301
|
+
console.log(color('Usage: jtcsv batch [json-to-csv|csv-to-json|process]', 'cyan'));
|
|
1302
|
+
process.exit(1);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
const batchCommand = args[1].toLowerCase();
|
|
1306
|
+
if (batchCommand === 'json-to-csv' && files.length >= 2) {
|
|
1307
|
+
await batchJsonToCsv(files[0], files[1], options);
|
|
1308
|
+
} else if (batchCommand === 'csv-to-json' && files.length >= 2) {
|
|
1309
|
+
await batchCsvToJson(files[0], files[1], options);
|
|
1310
|
+
} else if (batchCommand === 'process' && files.length >= 2) {
|
|
1311
|
+
console.log(color('Mixed batch processing coming soon...', 'yellow'));
|
|
1312
|
+
// TODO: Implement mixed batch processing
|
|
1313
|
+
} else {
|
|
1314
|
+
console.error(color('Error: Invalid batch command or missing files', 'red'));
|
|
1315
|
+
process.exit(1);
|
|
1316
|
+
}
|
|
1317
|
+
break;
|
|
1318
|
+
|
|
1319
|
+
// TUI command
|
|
1320
|
+
case 'tui':
|
|
1321
|
+
await launchTUI();
|
|
1322
|
+
break;
|
|
1323
|
+
|
|
1324
|
+
// Help and version
|
|
1325
|
+
case 'help':
|
|
1326
|
+
case '--help':
|
|
1327
|
+
case '-h':
|
|
1328
|
+
showHelp();
|
|
1329
|
+
break;
|
|
1330
|
+
|
|
1331
|
+
case 'version':
|
|
1332
|
+
case '-v':
|
|
1333
|
+
case '--version':
|
|
1334
|
+
showVersion();
|
|
1335
|
+
break;
|
|
1336
|
+
|
|
1337
|
+
default:
|
|
1338
|
+
console.error(color(`Error: Unknown command '${command}'`, 'red'));
|
|
1339
|
+
console.log(color('Use jtcsv help for available commands', 'cyan'));
|
|
1340
|
+
process.exit(1);
|
|
441
1341
|
}
|
|
442
1342
|
}
|
|
443
1343
|
|
|
444
|
-
//
|
|
1344
|
+
// ============================================================================
|
|
1345
|
+
// ERROR HANDLING
|
|
1346
|
+
// ============================================================================
|
|
1347
|
+
|
|
445
1348
|
process.on('uncaughtException', (error) => {
|
|
446
1349
|
console.error(color(`\n✗ Uncaught error: ${error.message}`, 'red'));
|
|
447
1350
|
if (process.env.DEBUG) {
|
|
@@ -466,11 +1369,4 @@ if (require.main === module) {
|
|
|
466
1369
|
});
|
|
467
1370
|
}
|
|
468
1371
|
|
|
469
|
-
module.exports = { main };
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
1372
|
+
module.exports = { main };
|