jtcsv 1.1.0 → 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.
Files changed (49) hide show
  1. package/README.md +275 -317
  2. package/bin/jtcsv.js +171 -89
  3. package/cli-tui.js +0 -0
  4. package/csv-to-json.js +135 -22
  5. package/dist/jtcsv.cjs.js +1619 -0
  6. package/dist/jtcsv.cjs.js.map +1 -0
  7. package/dist/jtcsv.esm.js +1599 -0
  8. package/dist/jtcsv.esm.js.map +1 -0
  9. package/dist/jtcsv.umd.js +1625 -0
  10. package/dist/jtcsv.umd.js.map +1 -0
  11. package/examples/cli-tool.js +186 -0
  12. package/examples/express-api.js +167 -0
  13. package/examples/large-dataset-example.js +185 -0
  14. package/examples/plugin-excel-exporter.js +407 -0
  15. package/examples/simple-usage.js +280 -0
  16. package/examples/streaming-example.js +419 -0
  17. package/index.d.ts +22 -3
  18. package/index.js +1 -0
  19. package/json-save.js +1 -1
  20. package/json-to-csv.js +16 -6
  21. package/package.json +130 -16
  22. package/plugins/README.md +373 -0
  23. package/plugins/express-middleware/README.md +306 -0
  24. package/plugins/express-middleware/example.js +136 -0
  25. package/plugins/express-middleware/index.d.ts +114 -0
  26. package/plugins/express-middleware/index.js +360 -0
  27. package/plugins/express-middleware/package.json +52 -0
  28. package/plugins/fastify-plugin/index.js +406 -0
  29. package/plugins/fastify-plugin/package.json +55 -0
  30. package/plugins/nextjs-api/README.md +452 -0
  31. package/plugins/nextjs-api/examples/ConverterComponent.jsx +386 -0
  32. package/plugins/nextjs-api/examples/api-convert.js +69 -0
  33. package/plugins/nextjs-api/index.js +388 -0
  34. package/plugins/nextjs-api/package.json +63 -0
  35. package/plugins/nextjs-api/route.js +372 -0
  36. package/src/browser/browser-functions.js +189 -0
  37. package/src/browser/csv-to-json-browser.js +442 -0
  38. package/src/browser/errors-browser.js +194 -0
  39. package/src/browser/index.js +79 -0
  40. package/src/browser/json-to-csv-browser.js +309 -0
  41. package/src/browser/workers/csv-parser.worker.js +359 -0
  42. package/src/browser/workers/worker-pool.js +467 -0
  43. package/src/core/plugin-system.js +472 -0
  44. package/src/engines/fast-path-engine-new.js +338 -0
  45. package/src/engines/fast-path-engine.js +347 -0
  46. package/src/formats/ndjson-parser.js +419 -0
  47. package/src/index-with-plugins.js +349 -0
  48. package/stream-csv-to-json.js +1 -1
  49. package/stream-json-to-csv.js +1 -1
package/bin/jtcsv.js CHANGED
@@ -41,8 +41,8 @@ ${color('USAGE:', 'bright')}
41
41
  jtcsv [command] [options] [file...]
42
42
 
43
43
  ${color('COMMANDS:', 'bright')}
44
- ${color('json2csv', 'green')} Convert JSON to CSV
45
- ${color('csv2json', 'green')} Convert CSV to JSON
44
+ ${color('json-to-csv', 'green')} Convert JSON to CSV (alias: json2csv)
45
+ ${color('csv-to-json', 'green')} Convert CSV to JSON (alias: csv2json)
46
46
  ${color('stream', 'yellow')} Streaming conversion for large files
47
47
  ${color('batch', 'yellow')} Batch process multiple files
48
48
  ${color('tui', 'magenta')} Launch Terminal User Interface (requires blessed)
@@ -51,25 +51,31 @@ ${color('COMMANDS:', 'bright')}
51
51
 
52
52
  ${color('EXAMPLES:', 'bright')}
53
53
  ${color('Convert JSON file to CSV:', 'dim')}
54
- jtcsv json2csv input.json output.csv --delimiter=,
54
+ jtcsv json-to-csv input.json output.csv --delimiter=,
55
55
 
56
56
  ${color('Convert CSV file to JSON:', 'dim')}
57
- jtcsv csv2json input.csv output.json --parse-numbers
57
+ jtcsv csv-to-json input.csv output.json --parse-numbers --auto-detect
58
58
 
59
59
  ${color('Stream large JSON file to CSV:', 'dim')}
60
- jtcsv stream json2csv large.json output.csv --max-records=1000000
60
+ jtcsv stream json-to-csv large.json output.csv --max-records=1000000
61
61
 
62
62
  ${color('Launch TUI interface:', 'dim')}
63
63
  jtcsv tui
64
64
 
65
65
  ${color('OPTIONS:', 'bright')}
66
66
  ${color('--delimiter=', 'cyan')}CHAR CSV delimiter (default: ;)
67
+ ${color('--auto-detect', 'cyan')} Auto-detect delimiter (default: true)
68
+ ${color('--candidates=', 'cyan')}LIST Delimiter candidates (default: ;,\t|)
67
69
  ${color('--no-headers', 'cyan')} Exclude headers from CSV output
68
70
  ${color('--parse-numbers', 'cyan')} Parse numeric values in CSV
69
71
  ${color('--parse-booleans', 'cyan')} Parse boolean values in CSV
72
+ ${color('--no-trim', 'cyan')} Don't trim whitespace from CSV values
73
+ ${color('--rename=', 'cyan')}JSON Rename columns (JSON map)
74
+ ${color('--template=', 'cyan')}JSON Column order template (JSON object)
70
75
  ${color('--no-injection-protection', 'cyan')} Disable CSV injection protection
71
- ${color('--max-records=', 'cyan')}N Maximum records to process (default: 1000000)
72
- ${color('--max-rows=', 'cyan')}N Maximum rows to process (default: 1000000)
76
+ ${color('--no-rfc4180', 'cyan')} Disable RFC 4180 compliance
77
+ ${color('--max-records=', 'cyan')}N Maximum records to process (optional, no limit by default)
78
+ ${color('--max-rows=', 'cyan')}N Maximum rows to process (optional, no limit by default)
73
79
  ${color('--pretty', 'cyan')} Pretty print JSON output
74
80
  ${color('--silent', 'cyan')} Suppress all output except errors
75
81
  ${color('--verbose', 'cyan')} Show detailed progress information
@@ -109,8 +115,19 @@ async function convertJsonToCsv(inputFile, outputFile, options) {
109
115
 
110
116
  console.log(color(`Converting ${jsonData.length} records...`, 'dim'));
111
117
 
118
+ // Prepare options for jtcsv
119
+ const jtcsvOptions = {
120
+ delimiter: options.delimiter,
121
+ includeHeaders: options.includeHeaders,
122
+ renameMap: options.renameMap,
123
+ template: options.template,
124
+ maxRecords: options.maxRecords,
125
+ preventCsvInjection: options.preventCsvInjection,
126
+ rfc4180Compliant: options.rfc4180Compliant
127
+ };
128
+
112
129
  // Convert to CSV
113
- const csvData = jtcsv.jsonToCsv(jsonData, options);
130
+ const csvData = jtcsv.jsonToCsv(jsonData, jtcsvOptions);
114
131
 
115
132
  // Write output file
116
133
  await fs.promises.writeFile(outputFile, csvData, 'utf8');
@@ -129,10 +146,23 @@ async function convertCsvToJson(inputFile, outputFile, options) {
129
146
  const startTime = Date.now();
130
147
 
131
148
  try {
132
- console.log(color(`Reading CSV file...`, 'dim'));
149
+ console.log(color('Reading CSV file...', 'dim'));
150
+
151
+ // Prepare options for jtcsv
152
+ const jtcsvOptions = {
153
+ delimiter: options.delimiter,
154
+ autoDetect: options.autoDetect,
155
+ candidates: options.candidates,
156
+ hasHeaders: options.hasHeaders,
157
+ renameMap: options.renameMap,
158
+ trim: options.trim,
159
+ parseNumbers: options.parseNumbers,
160
+ parseBooleans: options.parseBooleans,
161
+ maxRows: options.maxRows
162
+ };
133
163
 
134
164
  // Read and convert CSV
135
- const jsonData = await jtcsv.readCsvAsJson(inputFile, options);
165
+ const jsonData = await jtcsv.readCsvAsJson(inputFile, jtcsvOptions);
136
166
 
137
167
  // Format JSON
138
168
  const jsonOutput = options.pretty
@@ -156,7 +186,7 @@ async function streamJsonToCsv(inputFile, outputFile, options) {
156
186
  const startTime = Date.now();
157
187
 
158
188
  try {
159
- console.log(color(`Streaming conversion started...`, 'dim'));
189
+ console.log(color('Streaming conversion started...', 'dim'));
160
190
 
161
191
  // Create streams
162
192
  const readStream = fs.createReadStream(inputFile, 'utf8');
@@ -235,12 +265,19 @@ async function launchTUI() {
235
265
  function parseOptions(args) {
236
266
  const options = {
237
267
  delimiter: ';',
268
+ autoDetect: true,
269
+ candidates: [';', ',', '\t', '|'],
270
+ hasHeaders: true,
238
271
  includeHeaders: true,
272
+ renameMap: undefined,
273
+ template: undefined,
274
+ trim: true,
239
275
  parseNumbers: false,
240
276
  parseBooleans: false,
241
277
  preventCsvInjection: true,
242
- maxRecords: 1000000,
243
- maxRows: 1000000,
278
+ rfc4180Compliant: true,
279
+ maxRecords: undefined,
280
+ maxRows: undefined,
244
281
  pretty: false,
245
282
  silent: false,
246
283
  verbose: false
@@ -255,36 +292,72 @@ function parseOptions(args) {
255
292
  const [key, value] = arg.slice(2).split('=');
256
293
 
257
294
  switch (key) {
258
- case 'delimiter':
259
- options.delimiter = value || ',';
260
- break;
261
- case 'no-headers':
262
- options.includeHeaders = false;
263
- break;
264
- case 'parse-numbers':
265
- options.parseNumbers = true;
266
- break;
267
- case 'parse-booleans':
268
- options.parseBooleans = true;
269
- break;
270
- case 'no-injection-protection':
271
- options.preventCsvInjection = false;
272
- break;
273
- case 'max-records':
274
- options.maxRecords = parseInt(value, 10);
275
- break;
276
- case 'max-rows':
277
- options.maxRows = parseInt(value, 10);
278
- break;
279
- case 'pretty':
280
- options.pretty = true;
281
- break;
282
- case 'silent':
283
- options.silent = true;
284
- break;
285
- case 'verbose':
286
- options.verbose = true;
287
- break;
295
+ case 'delimiter':
296
+ options.delimiter = value || ',';
297
+ options.autoDetect = false; // Disable auto-detect if delimiter is specified
298
+ break;
299
+ case 'auto-detect':
300
+ options.autoDetect = value !== 'false';
301
+ break;
302
+ case 'candidates':
303
+ options.candidates = value ? value.split(',') : [';', ',', '\t', '|'];
304
+ break;
305
+ case 'no-headers':
306
+ options.includeHeaders = false;
307
+ options.hasHeaders = false;
308
+ break;
309
+ case 'parse-numbers':
310
+ options.parseNumbers = true;
311
+ break;
312
+ case 'parse-booleans':
313
+ options.parseBooleans = true;
314
+ break;
315
+ case 'no-trim':
316
+ options.trim = false;
317
+ break;
318
+ case 'rename':
319
+ try {
320
+ // Handle both quoted and unquoted JSON
321
+ const jsonStr = value || '{}';
322
+ // Remove surrounding single quotes if present
323
+ const cleanStr = jsonStr.replace(/^'|'$/g, '').replace(/^"|"$/g, '');
324
+ options.renameMap = JSON.parse(cleanStr);
325
+ } catch (e) {
326
+ throw new Error(`Invalid JSON in --rename option: ${e.message}. Value: ${value}`);
327
+ }
328
+ break;
329
+ case 'template':
330
+ try {
331
+ // Handle both quoted and unquoted JSON
332
+ const jsonStr = value || '{}';
333
+ // Remove surrounding single quotes if present
334
+ const cleanStr = jsonStr.replace(/^'|'$/g, '').replace(/^"|"$/g, '');
335
+ options.template = JSON.parse(cleanStr);
336
+ } catch (e) {
337
+ throw new Error(`Invalid JSON in --template option: ${e.message}. Value: ${value}`);
338
+ }
339
+ break;
340
+ case 'no-injection-protection':
341
+ options.preventCsvInjection = false;
342
+ break;
343
+ case 'no-rfc4180':
344
+ options.rfc4180Compliant = false;
345
+ break;
346
+ case 'max-records':
347
+ options.maxRecords = parseInt(value, 10);
348
+ break;
349
+ case 'max-rows':
350
+ options.maxRows = parseInt(value, 10);
351
+ break;
352
+ case 'pretty':
353
+ options.pretty = true;
354
+ break;
355
+ case 'silent':
356
+ options.silent = true;
357
+ break;
358
+ case 'verbose':
359
+ options.verbose = true;
360
+ break;
288
361
  }
289
362
  } else if (!arg.startsWith('-')) {
290
363
  files.push(arg);
@@ -312,57 +385,59 @@ async function main() {
312
385
  }
313
386
 
314
387
  switch (command) {
315
- case 'json2csv':
316
- if (files.length < 2) {
317
- console.error(color('Error: Input and output files required', 'red'));
318
- console.log(color('Usage: jtcsv json2csv input.json output.csv', 'cyan'));
319
- process.exit(1);
320
- }
321
- await convertJsonToCsv(files[0], files[1], options);
322
- break;
388
+ case 'json-to-csv':
389
+ case 'json2csv': // Backward compatibility
390
+ if (files.length < 2) {
391
+ console.error(color('Error: Input and output files required', 'red'));
392
+ console.log(color('Usage: jtcsv json-to-csv input.json output.csv', 'cyan'));
393
+ process.exit(1);
394
+ }
395
+ await convertJsonToCsv(files[0], files[1], options);
396
+ break;
323
397
 
324
- case 'csv2json':
325
- if (files.length < 2) {
326
- console.error(color('Error: Input and output files required', 'red'));
327
- console.log(color('Usage: jtcsv csv2json input.csv output.json', 'cyan'));
328
- process.exit(1);
329
- }
330
- await convertCsvToJson(files[0], files[1], options);
331
- break;
398
+ case 'csv-to-json':
399
+ case 'csv2json': // Backward compatibility
400
+ if (files.length < 2) {
401
+ console.error(color('Error: Input and output files required', 'red'));
402
+ console.log(color('Usage: jtcsv csv-to-json input.csv output.json', 'cyan'));
403
+ process.exit(1);
404
+ }
405
+ await convertCsvToJson(files[0], files[1], options);
406
+ break;
332
407
 
333
- case 'stream':
334
- if (args.length < 2) {
335
- console.error(color('Error: Streaming mode requires subcommand', 'red'));
336
- console.log(color('Usage: jtcsv stream [json2csv|csv2json] input output', 'cyan'));
337
- process.exit(1);
338
- }
339
- const streamCommand = args[1].toLowerCase();
340
- if (streamCommand === 'json2csv' && files.length >= 2) {
341
- await streamJsonToCsv(files[0], files[1], options);
342
- } else {
343
- console.error(color('Error: Invalid streaming command', 'red'));
344
- process.exit(1);
345
- }
346
- break;
408
+ case 'stream':
409
+ if (args.length < 2) {
410
+ console.error(color('Error: Streaming mode requires subcommand', 'red'));
411
+ console.log(color('Usage: jtcsv stream [json2csv|csv2json] input output', 'cyan'));
412
+ process.exit(1);
413
+ }
414
+ const streamCommand = args[1].toLowerCase();
415
+ if (streamCommand === 'json2csv' && files.length >= 2) {
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;
347
422
 
348
- case 'tui':
349
- await launchTUI();
350
- break;
423
+ case 'tui':
424
+ await launchTUI();
425
+ break;
351
426
 
352
- case 'help':
353
- showHelp();
354
- break;
427
+ case 'help':
428
+ showHelp();
429
+ break;
355
430
 
356
- case 'version':
357
- case '-v':
358
- case '--version':
359
- showVersion();
360
- break;
431
+ case 'version':
432
+ case '-v':
433
+ case '--version':
434
+ showVersion();
435
+ break;
361
436
 
362
- default:
363
- console.error(color(`Error: Unknown command '${command}'`, 'red'));
364
- console.log(color('Use jtcsv help for available commands', 'cyan'));
365
- process.exit(1);
437
+ default:
438
+ console.error(color(`Error: Unknown command '${command}'`, 'red'));
439
+ console.log(color('Use jtcsv help for available commands', 'cyan'));
440
+ process.exit(1);
366
441
  }
367
442
  }
368
443
 
@@ -391,4 +466,11 @@ if (require.main === module) {
391
466
  });
392
467
  }
393
468
 
394
- module.exports = { main };
469
+ module.exports = { main };
470
+
471
+
472
+
473
+
474
+
475
+
476
+
package/cli-tui.js CHANGED
Binary file
package/csv-to-json.js CHANGED
@@ -41,8 +41,18 @@ function validateCsvInput(csv, options) {
41
41
  throw new ConfigurationError('Delimiter must be a single character');
42
42
  }
43
43
 
44
+ // Validate autoDetect
45
+ if (options?.autoDetect !== undefined && typeof options.autoDetect !== 'boolean') {
46
+ throw new ConfigurationError('autoDetect must be a boolean');
47
+ }
48
+
49
+ // Validate candidates
50
+ if (options?.candidates && !Array.isArray(options.candidates)) {
51
+ throw new ConfigurationError('candidates must be an array');
52
+ }
53
+
44
54
  // Validate maxRows
45
- if (options?.maxRows && (typeof options.maxRows !== 'number' || options.maxRows <= 0)) {
55
+ if (options?.maxRows !== undefined && (typeof options.maxRows !== 'number' || options.maxRows <= 0)) {
46
56
  throw new ConfigurationError('maxRows must be a positive number');
47
57
  }
48
58
 
@@ -50,7 +60,7 @@ function validateCsvInput(csv, options) {
50
60
  }
51
61
 
52
62
  /**
53
- * Parses a single CSV line with proper escaping
63
+ но * Parses a single CSV line with proper escaping
54
64
  * @private
55
65
  */
56
66
  function parseCsvLine(line, lineNumber, delimiter) {
@@ -69,7 +79,17 @@ function parseCsvLine(line, lineNumber, delimiter) {
69
79
  }
70
80
 
71
81
  if (char === '\\') {
72
- escapeNext = true;
82
+ if (i + 1 === line.length) {
83
+ // Backslash at end of line - treat as literal
84
+ currentField += char;
85
+ } else if (line[i + 1] === '\\') {
86
+ // Double backslash - add one backslash to field and skip next
87
+ currentField += char;
88
+ i++; // Skip next backslash
89
+ } else {
90
+ // Escape next character
91
+ escapeNext = true;
92
+ }
73
93
  continue;
74
94
  }
75
95
 
@@ -87,6 +107,22 @@ function parseCsvLine(line, lineNumber, delimiter) {
87
107
  // Escaped quote inside quotes ("" -> ")
88
108
  currentField += '"';
89
109
  i++; // Skip next quote
110
+ // Check if this is the end of the quoted field
111
+ // Look ahead to see if next char is delimiter or end of line
112
+ let isEndOfField = false;
113
+ let j = i + 1;
114
+ // Skip whitespace
115
+ while (j < line.length && (line[j] === ' ' || line[j] === '\t')) {
116
+ j++;
117
+ }
118
+ if (j === line.length || line[j] === delimiter) {
119
+ isEndOfField = true;
120
+ }
121
+
122
+ if (isEndOfField) {
123
+ // This is the closing quote
124
+ insideQuotes = false;
125
+ }
90
126
  }
91
127
  } else {
92
128
  // Check if this is really the end of the quoted field
@@ -126,6 +162,13 @@ function parseCsvLine(line, lineNumber, delimiter) {
126
162
  currentField += char;
127
163
  }
128
164
 
165
+ // Handle case where escapeNext is still true at end of line
166
+ if (escapeNext) {
167
+ // This happens when line ends with backslash
168
+ // Add the backslash as literal character
169
+ currentField += '\\';
170
+ }
171
+
129
172
  // Add last field
130
173
  fields.push(currentField);
131
174
 
@@ -171,11 +214,11 @@ function parseCsvValue(value, options) {
171
214
  // Parse booleans
172
215
  if (parseBooleans) {
173
216
  const lowerValue = result.toLowerCase();
174
- if (lowerValue === 'true') {
175
- return true;
217
+ if (lowerValue === 'true') {
218
+ return true;
176
219
  }
177
- if (lowerValue === 'false') {
178
- return false;
220
+ if (lowerValue === 'false') {
221
+ return false;
179
222
  }
180
223
  }
181
224
 
@@ -187,18 +230,66 @@ function parseCsvValue(value, options) {
187
230
  return result;
188
231
  }
189
232
 
233
+ /**
234
+ * Auto-detect CSV delimiter from content
235
+ * @private
236
+ */
237
+ function autoDetectDelimiter(csv, candidates = [';', ',', '\t', '|']) {
238
+ if (!csv || typeof csv !== 'string') {
239
+ return ';'; // default
240
+ }
241
+
242
+ const lines = csv.split('\n').filter(line => line.trim().length > 0);
243
+
244
+ if (lines.length === 0) {
245
+ return ';'; // default
246
+ }
247
+
248
+ // Use first non-empty line for detection
249
+ const firstLine = lines[0];
250
+
251
+ const counts = {};
252
+ candidates.forEach(delim => {
253
+ // Escape special regex characters
254
+ const escapedDelim = delim.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
255
+ const regex = new RegExp(escapedDelim, 'g');
256
+ const matches = firstLine.match(regex);
257
+ counts[delim] = matches ? matches.length : 0;
258
+ });
259
+
260
+ // Find delimiter with maximum count
261
+ let maxCount = -1;
262
+ let detectedDelimiter = ';'; // default
263
+
264
+ for (const [delim, count] of Object.entries(counts)) {
265
+ if (count > maxCount) {
266
+ maxCount = count;
267
+ detectedDelimiter = delim;
268
+ }
269
+ }
270
+
271
+ // If no delimiter found or tie, return default
272
+ if (maxCount === 0) {
273
+ return ';'; // default
274
+ }
275
+
276
+ return detectedDelimiter;
277
+ }
278
+
190
279
  /**
191
280
  * Converts CSV string to JSON array
192
281
  *
193
- * @param {string} csv - CSV string to convert
282
+ * @param {string} csv - CSV string to convert
194
283
  * @param {Object} [options] - Configuration options
195
- * @param {string} [options.delimiter=';'] - CSV delimiter character
284
+ * @param {string} [options.delimiter] - CSV delimiter character (default: auto-detected)
285
+ * @param {boolean} [options.autoDetect=true] - Auto-detect delimiter if not specified
286
+ * @param {Array} [options.candidates=[';', ',', '\t', '|']] - Candidate delimiters for auto-detection
196
287
  * @param {boolean} [options.hasHeaders=true] - Whether CSV has headers row
197
288
  * @param {Object} [options.renameMap={}] - Map for renaming column headers (newKey: oldKey)
198
289
  * @param {boolean} [options.trim=true] - Trim whitespace from values
199
290
  * @param {boolean} [options.parseNumbers=false] - Parse numeric values
200
291
  * @param {boolean} [options.parseBooleans=false] - Parse boolean values
201
- * @param {number} [options.maxRows=1000000] - Maximum number of rows to process
292
+ * @param {number} [options.maxRows] - Maximum number of rows to process (optional, no limit by default)
202
293
  * @returns {Array<Object>} JSON array
203
294
  *
204
295
  * @example
@@ -218,15 +309,24 @@ function csvToJson(csv, options = {}) {
218
309
  const opts = options && typeof options === 'object' ? options : {};
219
310
 
220
311
  const {
221
- delimiter = ';',
312
+ delimiter,
313
+ autoDetect = true,
314
+ candidates = [';', ',', '\t', '|'],
222
315
  hasHeaders = true,
223
316
  renameMap = {},
224
317
  trim = true,
225
318
  parseNumbers = false,
226
319
  parseBooleans = false,
227
- maxRows = 1000000
320
+ maxRows
228
321
  } = opts;
229
322
 
323
+ // Determine delimiter
324
+ let finalDelimiter = delimiter;
325
+ if (!finalDelimiter && autoDetect) {
326
+ finalDelimiter = autoDetectDelimiter(csv, candidates);
327
+ }
328
+ finalDelimiter = finalDelimiter || ';'; // fallback
329
+
230
330
  // Handle empty CSV
231
331
  if (csv.trim() === '') {
232
332
  return [];
@@ -274,16 +374,28 @@ function csvToJson(csv, options = {}) {
274
374
  }
275
375
 
276
376
  // Check for unclosed quotes
277
- if (insideQuotes) {
278
- throw new ParsingError('Unclosed quotes in CSV', lines.length);
279
- }
377
+ // Note: This check is moved to parseCsvLine which has better context
378
+ // for handling escaped quotes like ""
379
+ // if (insideQuotes) {
380
+ // throw new ParsingError('Unclosed quotes in CSV', lines.length);
381
+ // }
280
382
 
281
383
  if (lines.length === 0) {
282
384
  return [];
283
385
  }
284
386
 
285
- // Limit rows to prevent OOM
286
- if (lines.length > maxRows) {
387
+ // Show warning for large datasets (optional limit)
388
+ if (lines.length > 1000000 && !maxRows && process.env.NODE_ENV !== 'test') {
389
+ console.warn(
390
+ '⚠️ Warning: Processing >1M records in memory may be slow.\n' +
391
+ '💡 Consider using createCsvToJsonStream() for better performance with large files.\n' +
392
+ '📊 Current size: ' + lines.length.toLocaleString() + ' rows\n' +
393
+ '🔧 Tip: Use { maxRows: N } option to set a custom limit if needed.'
394
+ );
395
+ }
396
+
397
+ // Apply optional row limit if specified
398
+ if (maxRows && lines.length > maxRows) {
287
399
  throw new LimitError(
288
400
  `CSV size exceeds maximum limit of ${maxRows} rows`,
289
401
  maxRows,
@@ -297,7 +409,7 @@ function csvToJson(csv, options = {}) {
297
409
  // Parse headers if present
298
410
  if (hasHeaders && lines.length > 0) {
299
411
  try {
300
- headers = parseCsvLine(lines[0], 1, delimiter).map(header => {
412
+ headers = parseCsvLine(lines[0], 1, finalDelimiter).map(header => {
301
413
  const trimmed = trim ? header.trim() : header;
302
414
  // Apply rename map
303
415
  return renameMap[trimmed] || trimmed;
@@ -312,7 +424,7 @@ function csvToJson(csv, options = {}) {
312
424
  } else {
313
425
  // Generate numeric headers from first line
314
426
  try {
315
- const firstLineFields = parseCsvLine(lines[0], 1, delimiter);
427
+ const firstLineFields = parseCsvLine(lines[0], 1, finalDelimiter);
316
428
  headers = firstLineFields.map((_, index) => `column${index + 1}`);
317
429
  } catch (error) {
318
430
  if (error instanceof ParsingError) {
@@ -334,7 +446,7 @@ function csvToJson(csv, options = {}) {
334
446
  }
335
447
 
336
448
  try {
337
- const fields = parseCsvLine(line, i + 1, delimiter);
449
+ const fields = parseCsvLine(line, i + 1, finalDelimiter);
338
450
 
339
451
  // Handle mismatched field count
340
452
  const row = {};
@@ -399,7 +511,7 @@ function validateCsvFilePath(filePath) {
399
511
  * @returns {Promise<Array<Object>>} Promise that resolves to JSON array
400
512
  *
401
513
  * @example
402
- * const { readCsvAsJson } = require('./csv-to-json');
514
+ * const { readCsvAsJson } = require('./csv-to-json');
403
515
  *
404
516
  * const json = await readCsvAsJson('./data.csv', {
405
517
  * delimiter: ',',
@@ -483,7 +595,8 @@ function readCsvAsJsonSync(filePath, options = {}) {
483
595
  module.exports = {
484
596
  csvToJson,
485
597
  readCsvAsJson,
486
- readCsvAsJsonSync
598
+ readCsvAsJsonSync,
599
+ autoDetectDelimiter
487
600
  };
488
601
 
489
602
  // For ES6 module compatibility