jtcsv 1.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/LICENSE +21 -0
- package/README.md +393 -0
- package/bin/jtcsv.js +394 -0
- package/cli-tui.js +0 -0
- package/csv-to-json.js +492 -0
- package/errors.js +188 -0
- package/index.d.ts +348 -0
- package/index.js +44 -0
- package/json-save.js +248 -0
- package/json-to-csv.js +430 -0
- package/package.json +87 -0
- package/stream-csv-to-json.js +613 -0
- package/stream-json-to-csv.js +532 -0
package/json-to-csv.js
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON to CSV Converter - Node.js Module
|
|
3
|
+
*
|
|
4
|
+
* A lightweight, efficient module for converting JSON data to CSV format
|
|
5
|
+
* with proper escaping and formatting for Excel compatibility.
|
|
6
|
+
*
|
|
7
|
+
* @module json-to-csv
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
ValidationError,
|
|
12
|
+
SecurityError,
|
|
13
|
+
FileSystemError,
|
|
14
|
+
LimitError,
|
|
15
|
+
ConfigurationError,
|
|
16
|
+
safeExecute
|
|
17
|
+
} = require('./errors');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validates input data and options
|
|
21
|
+
* @private
|
|
22
|
+
*/
|
|
23
|
+
function validateInput(data, options) {
|
|
24
|
+
// Validate data
|
|
25
|
+
if (!Array.isArray(data)) {
|
|
26
|
+
throw new ValidationError('Input data must be an array');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Validate options
|
|
30
|
+
if (options && typeof options !== 'object') {
|
|
31
|
+
throw new ConfigurationError('Options must be an object');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Validate delimiter
|
|
35
|
+
if (options?.delimiter && typeof options.delimiter !== 'string') {
|
|
36
|
+
throw new ConfigurationError('Delimiter must be a string');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (options?.delimiter && options.delimiter.length !== 1) {
|
|
40
|
+
throw new ConfigurationError('Delimiter must be a single character');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Validate renameMap
|
|
44
|
+
if (options?.renameMap && typeof options.renameMap !== 'object') {
|
|
45
|
+
throw new ConfigurationError('renameMap must be an object');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Validate maxRecords
|
|
49
|
+
if (options && options.maxRecords !== undefined) {
|
|
50
|
+
if (typeof options.maxRecords !== 'number' || options.maxRecords <= 0) {
|
|
51
|
+
throw new ConfigurationError('maxRecords must be a positive number');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Validate preventCsvInjection
|
|
56
|
+
if (options?.preventCsvInjection !== undefined && typeof options.preventCsvInjection !== 'boolean') {
|
|
57
|
+
throw new ConfigurationError('preventCsvInjection must be a boolean');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Validate rfc4180Compliant
|
|
61
|
+
if (options?.rfc4180Compliant !== undefined && typeof options.rfc4180Compliant !== 'boolean') {
|
|
62
|
+
throw new ConfigurationError('rfc4180Compliant must be a boolean');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Converts JSON data to CSV format
|
|
70
|
+
*
|
|
71
|
+
* @param {Array<Object>} data - Array of objects to convert to CSV
|
|
72
|
+
* @param {Object} [options] - Configuration options
|
|
73
|
+
* @param {string} [options.delimiter=';'] - CSV delimiter character
|
|
74
|
+
* @param {boolean} [options.includeHeaders=true] - Whether to include headers row
|
|
75
|
+
* @param {Object} [options.renameMap={}] - Map for renaming column headers (oldKey: newKey)
|
|
76
|
+
* @param {Object} [options.template={}] - Template object to ensure consistent column order
|
|
77
|
+
* @param {number} [options.maxRecords=1000000] - Maximum number of records to process
|
|
78
|
+
* @param {boolean} [options.preventCsvInjection=true] - Prevent CSV injection attacks by escaping formulas
|
|
79
|
+
* @param {boolean} [options.rfc4180Compliant=true] - Ensure RFC 4180 compliance (proper quoting, line endings)
|
|
80
|
+
* @returns {string} CSV formatted string
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* const jsonToCsv = require('./json-to-csv');
|
|
84
|
+
*
|
|
85
|
+
* const data = [
|
|
86
|
+
* { id: 1, name: 'John', email: 'john@example.com' },
|
|
87
|
+
* { id: 2, name: 'Jane', email: 'jane@example.com' }
|
|
88
|
+
* ];
|
|
89
|
+
*
|
|
90
|
+
* const csv = jsonToCsv(data, {
|
|
91
|
+
* delimiter: ',',
|
|
92
|
+
* renameMap: { id: 'ID', name: 'Full Name' },
|
|
93
|
+
* preventCsvInjection: true,
|
|
94
|
+
* rfc4180Compliant: true
|
|
95
|
+
* });
|
|
96
|
+
*/
|
|
97
|
+
function jsonToCsv(data, options = {}) {
|
|
98
|
+
return safeExecute(() => {
|
|
99
|
+
// Validate input
|
|
100
|
+
validateInput(data, options);
|
|
101
|
+
|
|
102
|
+
const opts = options && typeof options === 'object' ? options : {};
|
|
103
|
+
|
|
104
|
+
const {
|
|
105
|
+
delimiter = ';',
|
|
106
|
+
includeHeaders = true,
|
|
107
|
+
renameMap = {},
|
|
108
|
+
template = {},
|
|
109
|
+
maxRecords = 1000000,
|
|
110
|
+
preventCsvInjection = true,
|
|
111
|
+
rfc4180Compliant = true
|
|
112
|
+
} = opts;
|
|
113
|
+
|
|
114
|
+
// Handle empty data
|
|
115
|
+
if (data.length === 0) {
|
|
116
|
+
return '';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Limit data size to prevent OOM
|
|
120
|
+
if (data.length > maxRecords) {
|
|
121
|
+
throw new LimitError(
|
|
122
|
+
`Data size exceeds maximum limit of ${maxRecords} records`,
|
|
123
|
+
maxRecords,
|
|
124
|
+
data.length
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Get all unique keys from all objects
|
|
129
|
+
const allKeys = new Set();
|
|
130
|
+
data.forEach((item) => {
|
|
131
|
+
if (!item || typeof item !== 'object') {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
Object.keys(item).forEach(key => allKeys.add(key));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Convert Set to Array
|
|
138
|
+
const originalKeys = Array.from(allKeys);
|
|
139
|
+
|
|
140
|
+
// Apply rename map to create header names
|
|
141
|
+
const headers = originalKeys.map(key => renameMap[key] || key);
|
|
142
|
+
|
|
143
|
+
// Create a reverse mapping from new header to original key
|
|
144
|
+
const reverseRenameMap = {};
|
|
145
|
+
originalKeys.forEach((key, index) => {
|
|
146
|
+
reverseRenameMap[headers[index]] = key;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Apply template ordering if provided
|
|
150
|
+
let finalHeaders = headers;
|
|
151
|
+
if (Object.keys(template).length > 0) {
|
|
152
|
+
// Create template headers with renaming applied
|
|
153
|
+
const templateHeaders = Object.keys(template).map(key => renameMap[key] || key);
|
|
154
|
+
const extraHeaders = headers.filter(h => !templateHeaders.includes(h));
|
|
155
|
+
finalHeaders = [...templateHeaders, ...extraHeaders];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Escapes a value for CSV format with CSV injection protection
|
|
160
|
+
*
|
|
161
|
+
* @private
|
|
162
|
+
* @param {*} value - The value to escape
|
|
163
|
+
* @returns {string} Escaped CSV value
|
|
164
|
+
*/
|
|
165
|
+
const escapeValue = (value) => {
|
|
166
|
+
if (value === null || value === undefined || value === '') {
|
|
167
|
+
return '';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const stringValue = String(value);
|
|
171
|
+
|
|
172
|
+
// CSV Injection protection - escape formulas if enabled
|
|
173
|
+
let escapedValue = stringValue;
|
|
174
|
+
if (preventCsvInjection && /^[=+\-@]/.test(stringValue)) {
|
|
175
|
+
// Prepend single quote to prevent formula execution in Excel
|
|
176
|
+
escapedValue = "'" + stringValue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// RFC 4180 compliance: fields containing line breaks, double quotes, or commas must be quoted
|
|
180
|
+
const needsQuoting = rfc4180Compliant
|
|
181
|
+
? (escapedValue.includes(delimiter) ||
|
|
182
|
+
escapedValue.includes('"') ||
|
|
183
|
+
escapedValue.includes('\n') ||
|
|
184
|
+
escapedValue.includes('\r'))
|
|
185
|
+
: (escapedValue.includes(delimiter) ||
|
|
186
|
+
escapedValue.includes('"') ||
|
|
187
|
+
escapedValue.includes('\n') ||
|
|
188
|
+
escapedValue.includes('\r'));
|
|
189
|
+
|
|
190
|
+
if (needsQuoting) {
|
|
191
|
+
// RFC 4180: If double-quotes are used to enclose fields, then a double-quote
|
|
192
|
+
// appearing inside a field must be escaped by preceding it with another double quote.
|
|
193
|
+
return `"${escapedValue.replace(/"/g, '""')}"`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return escapedValue;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// Build CSV rows
|
|
200
|
+
const rows = [];
|
|
201
|
+
|
|
202
|
+
// Add headers row if requested
|
|
203
|
+
if (includeHeaders && finalHeaders.length > 0) {
|
|
204
|
+
rows.push(finalHeaders.join(delimiter));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Add data rows
|
|
208
|
+
for (const item of data) {
|
|
209
|
+
if (!item || typeof item !== 'object') {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const row = finalHeaders.map(header => {
|
|
214
|
+
// Get the original key for this header
|
|
215
|
+
const originalKey = reverseRenameMap[header] || header;
|
|
216
|
+
const value = item[originalKey];
|
|
217
|
+
return escapeValue(value);
|
|
218
|
+
}).join(delimiter);
|
|
219
|
+
|
|
220
|
+
rows.push(row);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// RFC 4180: Each record is located on a separate line, delimited by a line break (CRLF)
|
|
224
|
+
const lineEnding = rfc4180Compliant ? '\r\n' : '\n';
|
|
225
|
+
return rows.join(lineEnding);
|
|
226
|
+
}, 'PARSE_FAILED', { function: 'jsonToCsv' });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Deeply unwraps nested objects and arrays to extract primitive values
|
|
231
|
+
*
|
|
232
|
+
* @param {*} value - Value to unwrap
|
|
233
|
+
* @param {number} [depth=0] - Current recursion depth
|
|
234
|
+
* @param {number} [maxDepth=5] - Maximum recursion depth
|
|
235
|
+
* @param {Set} [visited=new Set()] - Set of visited objects to detect circular references
|
|
236
|
+
* @returns {string} Unwrapped string value
|
|
237
|
+
*
|
|
238
|
+
* @private
|
|
239
|
+
*/
|
|
240
|
+
function deepUnwrap(value, depth = 0, maxDepth = 5, visited = new Set()) {
|
|
241
|
+
// Check depth before processing
|
|
242
|
+
if (depth >= maxDepth) {
|
|
243
|
+
return '[Too Deep]';
|
|
244
|
+
}
|
|
245
|
+
if (value === null || value === undefined) {
|
|
246
|
+
return '';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Handle circular references - return early for circular refs
|
|
250
|
+
if (typeof value === 'object') {
|
|
251
|
+
if (visited.has(value)) {
|
|
252
|
+
return '[Circular Reference]';
|
|
253
|
+
}
|
|
254
|
+
visited.add(value);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Handle arrays - join all elements
|
|
258
|
+
if (Array.isArray(value)) {
|
|
259
|
+
if (value.length === 0) {
|
|
260
|
+
return '';
|
|
261
|
+
}
|
|
262
|
+
const unwrappedItems = value.map(item =>
|
|
263
|
+
deepUnwrap(item, depth + 1, maxDepth, visited)
|
|
264
|
+
).filter(item => item !== '');
|
|
265
|
+
return unwrappedItems.join(', ');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Handle objects
|
|
269
|
+
if (typeof value === 'object') {
|
|
270
|
+
const keys = Object.keys(value);
|
|
271
|
+
if (keys.length === 0) {
|
|
272
|
+
return '';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// For maxDepth = 0 or 1, return [Too Deep] for objects
|
|
276
|
+
if (depth + 1 >= maxDepth) {
|
|
277
|
+
return '[Too Deep]';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Stringify complex objects
|
|
281
|
+
try {
|
|
282
|
+
return JSON.stringify(value);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
// Check if it's a circular reference
|
|
285
|
+
if (error.message.includes('circular') || error.message.includes('Converting circular')) {
|
|
286
|
+
return '[Circular Reference]';
|
|
287
|
+
}
|
|
288
|
+
return '[Unstringifiable Object]';
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Convert to string for primitive values
|
|
293
|
+
return String(value);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Preprocesses JSON data by deeply unwrapping nested structures
|
|
298
|
+
*
|
|
299
|
+
* @param {Array<Object>} data - Array of objects to preprocess
|
|
300
|
+
* @returns {Array<Object>} Preprocessed data with unwrapped values
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* const processed = preprocessData(complexJsonData);
|
|
304
|
+
* const csv = jsonToCsv(processed);
|
|
305
|
+
*/
|
|
306
|
+
function preprocessData(data) {
|
|
307
|
+
if (!Array.isArray(data)) {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return data.map(item => {
|
|
312
|
+
if (!item || typeof item !== 'object') {
|
|
313
|
+
return {};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const processed = {};
|
|
317
|
+
|
|
318
|
+
for (const key in item) {
|
|
319
|
+
if (Object.prototype.hasOwnProperty.call(item, key)) {
|
|
320
|
+
const value = item[key];
|
|
321
|
+
if (value && typeof value === 'object') {
|
|
322
|
+
processed[key] = deepUnwrap(value);
|
|
323
|
+
} else {
|
|
324
|
+
processed[key] = value;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return processed;
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Validates file path to prevent path traversal attacks
|
|
335
|
+
* @private
|
|
336
|
+
*/
|
|
337
|
+
function validateFilePath(filePath) {
|
|
338
|
+
const path = require('path');
|
|
339
|
+
|
|
340
|
+
// Basic validation
|
|
341
|
+
if (typeof filePath !== 'string' || filePath.trim() === '') {
|
|
342
|
+
throw new ValidationError('File path must be a non-empty string');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Ensure file has .csv extension
|
|
346
|
+
if (!filePath.toLowerCase().endsWith('.csv')) {
|
|
347
|
+
throw new ValidationError('File must have .csv extension');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Get absolute path and check for traversal
|
|
351
|
+
const absolutePath = path.resolve(filePath);
|
|
352
|
+
const normalizedPath = path.normalize(filePath);
|
|
353
|
+
|
|
354
|
+
// Prevent directory traversal attacks
|
|
355
|
+
// Check if normalized path contains parent directory references
|
|
356
|
+
if (normalizedPath.includes('..') ||
|
|
357
|
+
/\\\.\.\\|\/\.\.\//.test(filePath) ||
|
|
358
|
+
filePath.startsWith('..') ||
|
|
359
|
+
filePath.includes('/..')) {
|
|
360
|
+
throw new SecurityError('Directory traversal detected in file path');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return absolutePath;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Converts JSON to CSV and saves it to a file
|
|
368
|
+
*
|
|
369
|
+
* @param {Array<Object>} data - Array of objects to convert
|
|
370
|
+
* @param {string} filePath - Path to save the CSV file
|
|
371
|
+
* @param {Object} [options] - Configuration options (same as jsonToCsv)
|
|
372
|
+
* @returns {Promise<void>}
|
|
373
|
+
*
|
|
374
|
+
* @example
|
|
375
|
+
* const { saveAsCsv } = require('./json-to-csv');
|
|
376
|
+
*
|
|
377
|
+
* await saveAsCsv(data, './output.csv', {
|
|
378
|
+
* delimiter: ',',
|
|
379
|
+
* renameMap: { id: 'ID' }
|
|
380
|
+
* });
|
|
381
|
+
*/
|
|
382
|
+
async function saveAsCsv(data, filePath, options = {}) {
|
|
383
|
+
return safeExecute(async () => {
|
|
384
|
+
const fs = require('fs').promises;
|
|
385
|
+
|
|
386
|
+
// Validate file path
|
|
387
|
+
const safePath = validateFilePath(filePath);
|
|
388
|
+
|
|
389
|
+
// Convert data to CSV
|
|
390
|
+
const csvContent = jsonToCsv(data, options);
|
|
391
|
+
|
|
392
|
+
// Ensure directory exists
|
|
393
|
+
const dir = require('path').dirname(safePath);
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
await fs.mkdir(dir, { recursive: true });
|
|
397
|
+
|
|
398
|
+
// Write file
|
|
399
|
+
await fs.writeFile(safePath, csvContent, 'utf8');
|
|
400
|
+
|
|
401
|
+
return safePath;
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (error.code === 'ENOENT') {
|
|
404
|
+
throw new FileSystemError(`Directory does not exist: ${dir}`, error);
|
|
405
|
+
}
|
|
406
|
+
if (error.code === 'EACCES') {
|
|
407
|
+
throw new FileSystemError(`Permission denied: ${safePath}`, error);
|
|
408
|
+
}
|
|
409
|
+
if (error.code === 'ENOSPC') {
|
|
410
|
+
throw new FileSystemError(`No space left on device: ${safePath}`, error);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
throw new FileSystemError(`Failed to write CSV file: ${error.message}`, error);
|
|
414
|
+
}
|
|
415
|
+
}, 'FILE_SYSTEM_ERROR', { function: 'saveAsCsv' });
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Export the main functions
|
|
419
|
+
module.exports = {
|
|
420
|
+
jsonToCsv,
|
|
421
|
+
preprocessData,
|
|
422
|
+
saveAsCsv,
|
|
423
|
+
deepUnwrap,
|
|
424
|
+
validateFilePath
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// For ES6 module compatibility
|
|
428
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
429
|
+
module.exports.default = jsonToCsv;
|
|
430
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jtcsv",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Complete JSON↔CSV converter for Node.js with streaming, security, TUI, and TypeScript support - Zero dependencies",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"jtcsv": "./bin/jtcsv.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "jest",
|
|
12
|
+
"test:coverage": "jest --coverage",
|
|
13
|
+
"test:watch": "jest --watch",
|
|
14
|
+
"lint": "eslint index.js json-to-csv.js csv-to-json.js errors.js stream-json-to-csv.js stream-csv-to-json.js json-save.js",
|
|
15
|
+
"lint:all": "eslint .",
|
|
16
|
+
"security-check": "npm audit",
|
|
17
|
+
"prepublishOnly": "npm test && eslint index.js json-to-csv.js csv-to-json.js errors.js stream-json-to-csv.js stream-csv-to-json.js json-save.js",
|
|
18
|
+
"tui": "node cli-tui.js",
|
|
19
|
+
"cli": "node bin/jtcsv.js"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"json",
|
|
23
|
+
"csv",
|
|
24
|
+
"converter",
|
|
25
|
+
"json-to-csv",
|
|
26
|
+
"csv-to-json",
|
|
27
|
+
"json2csv",
|
|
28
|
+
"csv2json",
|
|
29
|
+
"export",
|
|
30
|
+
"import",
|
|
31
|
+
"excel",
|
|
32
|
+
"data",
|
|
33
|
+
"transform",
|
|
34
|
+
"nodejs",
|
|
35
|
+
"javascript",
|
|
36
|
+
"simple",
|
|
37
|
+
"lightweight",
|
|
38
|
+
"zero-dependencies",
|
|
39
|
+
"security",
|
|
40
|
+
"streaming",
|
|
41
|
+
"utf8",
|
|
42
|
+
"cyrillic",
|
|
43
|
+
"typescript",
|
|
44
|
+
"bidirectional",
|
|
45
|
+
"cli",
|
|
46
|
+
"tui",
|
|
47
|
+
"terminal",
|
|
48
|
+
"interface",
|
|
49
|
+
"gui",
|
|
50
|
+
"tool",
|
|
51
|
+
"utility"
|
|
52
|
+
],
|
|
53
|
+
"author": "Ruslan Fomenko",
|
|
54
|
+
"license": "MIT",
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "https://github.com/Linol-Hamelton/jtcsv.git"
|
|
58
|
+
},
|
|
59
|
+
"bugs": {
|
|
60
|
+
"url": "https://github.com/Linol-Hamelton/jtcsv/issues"
|
|
61
|
+
},
|
|
62
|
+
"homepage": "https://github.com/Linol-Hamelton/jtcsv#readme",
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=12.0.0"
|
|
65
|
+
},
|
|
66
|
+
"files": [
|
|
67
|
+
"index.js",
|
|
68
|
+
"json-to-csv.js",
|
|
69
|
+
"csv-to-json.js",
|
|
70
|
+
"errors.js",
|
|
71
|
+
"stream-json-to-csv.js",
|
|
72
|
+
"stream-csv-to-json.js",
|
|
73
|
+
"json-save.js",
|
|
74
|
+
"index.d.ts",
|
|
75
|
+
"bin/",
|
|
76
|
+
"cli-tui.js"
|
|
77
|
+
],
|
|
78
|
+
"devDependencies": {
|
|
79
|
+
"jest": "^29.0.0",
|
|
80
|
+
"eslint": "^8.0.0"
|
|
81
|
+
},
|
|
82
|
+
"optionalDependencies": {
|
|
83
|
+
"blessed": "^0.1.81",
|
|
84
|
+
"blessed-contrib": "^4.11.0"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|