jtcsv 2.1.3 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +60 -341
  3. package/bin/jtcsv.js +2462 -1372
  4. package/csv-to-json.js +35 -26
  5. package/dist/jtcsv.cjs.js +807 -133
  6. package/dist/jtcsv.cjs.js.map +1 -1
  7. package/dist/jtcsv.esm.js +800 -134
  8. package/dist/jtcsv.esm.js.map +1 -1
  9. package/dist/jtcsv.umd.js +807 -133
  10. package/dist/jtcsv.umd.js.map +1 -1
  11. package/errors.js +20 -0
  12. package/examples/browser-vanilla.html +37 -0
  13. package/examples/cli-batch-processing.js +38 -0
  14. package/examples/error-handling.js +324 -0
  15. package/examples/ndjson-processing.js +434 -0
  16. package/examples/react-integration.jsx +637 -0
  17. package/examples/schema-validation.js +640 -0
  18. package/examples/simple-usage.js +10 -7
  19. package/examples/typescript-example.ts +486 -0
  20. package/examples/web-workers-advanced.js +28 -0
  21. package/index.d.ts +2 -0
  22. package/json-save.js +2 -1
  23. package/json-to-csv.js +171 -131
  24. package/package.json +20 -4
  25. package/plugins/README.md +41 -467
  26. package/plugins/express-middleware/README.md +32 -274
  27. package/plugins/hono/README.md +16 -13
  28. package/plugins/nestjs/README.md +13 -11
  29. package/plugins/nextjs-api/README.md +28 -423
  30. package/plugins/nextjs-api/index.js +1 -2
  31. package/plugins/nextjs-api/route.js +1 -2
  32. package/plugins/nuxt/README.md +6 -7
  33. package/plugins/remix/README.md +9 -9
  34. package/plugins/sveltekit/README.md +8 -8
  35. package/plugins/trpc/README.md +8 -5
  36. package/src/browser/browser-functions.js +33 -3
  37. package/src/browser/csv-to-json-browser.js +269 -11
  38. package/src/browser/errors-browser.js +19 -1
  39. package/src/browser/index.js +39 -5
  40. package/src/browser/streams.js +393 -0
  41. package/src/browser/workers/csv-parser.worker.js +20 -2
  42. package/src/browser/workers/worker-pool.js +507 -447
  43. package/src/core/plugin-system.js +4 -0
  44. package/src/engines/fast-path-engine.js +31 -23
  45. package/src/errors.js +26 -0
  46. package/src/formats/ndjson-parser.js +54 -5
  47. package/src/formats/tsv-parser.js +4 -1
  48. package/src/utils/schema-validator.js +594 -0
  49. package/src/utils/transform-loader.js +205 -0
  50. package/src/web-server/index.js +683 -0
  51. package/stream-csv-to-json.js +16 -87
  52. package/stream-json-to-csv.js +18 -86
@@ -10,15 +10,10 @@ import {
10
10
  } from './errors-browser.js';
11
11
 
12
12
  /**
13
- * Валидация CSV ввода и опций
13
+ * Валидация опций парсинга
14
14
  * @private
15
15
  */
16
- function validateCsvInput(csv, options) {
17
- // Validate CSV input
18
- if (typeof csv !== 'string') {
19
- throw new ValidationError('Input must be a CSV string');
20
- }
21
-
16
+ function validateCsvOptions(options) {
22
17
  // Validate options
23
18
  if (options && typeof options !== 'object') {
24
19
  throw new ConfigurationError('Options must be an object');
@@ -47,10 +42,27 @@ function validateCsvInput(csv, options) {
47
42
  if (options?.maxRows !== undefined && (typeof options.maxRows !== 'number' || options.maxRows <= 0)) {
48
43
  throw new ConfigurationError('maxRows must be a positive number');
49
44
  }
45
+
46
+ if (options?.warnExtraFields !== undefined && typeof options.warnExtraFields !== 'boolean') {
47
+ throw new ConfigurationError('warnExtraFields must be a boolean');
48
+ }
50
49
 
51
50
  return true;
52
51
  }
53
52
 
53
+ /**
54
+ * Валидация CSV ввода и опций
55
+ * @private
56
+ */
57
+ function validateCsvInput(csv, options) {
58
+ // Validate CSV input
59
+ if (typeof csv !== 'string') {
60
+ throw new ValidationError('Input must be a CSV string');
61
+ }
62
+
63
+ return validateCsvOptions(options);
64
+ }
65
+
54
66
  /**
55
67
  * Парсинг одной строки CSV с правильным экранированием
56
68
  * @private
@@ -205,6 +217,95 @@ function parseCsvValue(value, options) {
205
217
  return result;
206
218
  }
207
219
 
220
+ function isSimpleCsv(csv) {
221
+ return csv.indexOf('"') === -1 && csv.indexOf('\\') === -1;
222
+ }
223
+
224
+ function parseSimpleCsv(csv, delimiter, options) {
225
+ const {
226
+ hasHeaders = true,
227
+ renameMap = {},
228
+ trim = true,
229
+ parseNumbers = false,
230
+ parseBooleans = false,
231
+ maxRows
232
+ } = options;
233
+
234
+ const result = [];
235
+ let headers = null;
236
+ let fieldStart = 0;
237
+ let currentRow = [];
238
+ let rowHasData = false;
239
+ let rowCount = 0;
240
+
241
+ const finalizeRow = (fields) => {
242
+ if (fields.length === 1 && fields[0].trim() === '') {
243
+ return;
244
+ }
245
+
246
+ if (!headers) {
247
+ if (hasHeaders) {
248
+ headers = fields.map(header => {
249
+ const trimmed = trim ? header.trim() : header;
250
+ return renameMap[trimmed] || trimmed;
251
+ });
252
+ return;
253
+ }
254
+
255
+ headers = fields.map((_, index) => `column${index + 1}`);
256
+ }
257
+
258
+ rowCount++;
259
+ if (maxRows && rowCount > maxRows) {
260
+ throw new LimitError(
261
+ `CSV size exceeds maximum limit of ${maxRows} rows`,
262
+ maxRows,
263
+ rowCount
264
+ );
265
+ }
266
+
267
+ const row = {};
268
+ const fieldCount = Math.min(fields.length, headers.length);
269
+ for (let i = 0; i < fieldCount; i++) {
270
+ row[headers[i]] = parseCsvValue(fields[i], { trim, parseNumbers, parseBooleans });
271
+ }
272
+
273
+ result.push(row);
274
+ };
275
+
276
+ let i = 0;
277
+ while (i <= csv.length) {
278
+ const char = i < csv.length ? csv[i] : '\n';
279
+
280
+ if (char !== '\r' && char !== '\n' && char !== ' ' && char !== '\t') {
281
+ rowHasData = true;
282
+ }
283
+
284
+ if (char === delimiter || char === '\n' || char === '\r' || i === csv.length) {
285
+ const field = csv.slice(fieldStart, i);
286
+ currentRow.push(field);
287
+
288
+ if (char === '\n' || char === '\r' || i === csv.length) {
289
+ if (rowHasData || currentRow.length > 1) {
290
+ finalizeRow(currentRow);
291
+ }
292
+ currentRow = [];
293
+ rowHasData = false;
294
+ }
295
+
296
+ if (char === '\r' && csv[i + 1] === '\n') {
297
+ i++;
298
+ }
299
+
300
+ fieldStart = i + 1;
301
+ }
302
+
303
+ i++;
304
+ }
305
+
306
+ return result;
307
+ }
308
+
208
309
  /**
209
310
  * Автоматическое определение разделителя CSV
210
311
  *
@@ -285,7 +386,8 @@ export function csvToJson(csv, options = {}) {
285
386
  trim = true,
286
387
  parseNumbers = false,
287
388
  parseBooleans = false,
288
- maxRows
389
+ maxRows,
390
+ warnExtraFields = true
289
391
  } = opts;
290
392
 
291
393
  // Определение разделителя
@@ -300,6 +402,17 @@ export function csvToJson(csv, options = {}) {
300
402
  return [];
301
403
  }
302
404
 
405
+ if (isSimpleCsv(csv)) {
406
+ return parseSimpleCsv(csv, finalDelimiter, {
407
+ hasHeaders,
408
+ renameMap,
409
+ trim,
410
+ parseNumbers,
411
+ parseBooleans,
412
+ maxRows
413
+ });
414
+ }
415
+
303
416
  // Парсинг CSV с обработкой кавычек и переносов строк
304
417
  const lines = [];
305
418
  let currentLine = '';
@@ -416,7 +529,8 @@ export function csvToJson(csv, options = {}) {
416
529
  }
417
530
 
418
531
  // Предупреждение о лишних полях
419
- if (fields.length > headers.length && process.env.NODE_ENV === 'development') {
532
+ const isDev = typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'development';
533
+ if (fields.length > headers.length && warnExtraFields && isDev) {
420
534
  console.warn(`[jtcsv] Line ${i + 1}: ${fields.length - headers.length} extra fields ignored`);
421
535
  }
422
536
 
@@ -433,10 +547,154 @@ export function csvToJson(csv, options = {}) {
433
547
  }, 'PARSE_FAILED', { function: 'csvToJson' });
434
548
  }
435
549
 
550
+ export async function* csvToJsonIterator(input, options = {}) {
551
+ const opts = options && typeof options === 'object' ? options : {};
552
+ validateCsvOptions(opts);
553
+
554
+ if (typeof input === 'string') {
555
+ const rows = csvToJson(input, options);
556
+ for (const row of rows) {
557
+ yield row;
558
+ }
559
+ return;
560
+ }
561
+
562
+ const {
563
+ delimiter,
564
+ autoDetect = true,
565
+ candidates = [';', ',', '\t', '|'],
566
+ hasHeaders = true,
567
+ renameMap = {},
568
+ trim = true,
569
+ parseNumbers = false,
570
+ parseBooleans = false,
571
+ maxRows
572
+ } = opts;
573
+
574
+ const stream = (input instanceof Blob && input.stream) ? input.stream() : input;
575
+ if (!stream || typeof stream.getReader !== 'function') {
576
+ throw new ValidationError('Input must be a CSV string, Blob/File, or ReadableStream');
577
+ }
578
+
579
+ const reader = stream.getReader();
580
+ const decoder = new TextDecoder('utf-8');
581
+ let buffer = '';
582
+ let insideQuotes = false;
583
+ let headers = null;
584
+ let rowCount = 0;
585
+ let lineNumber = 0;
586
+ let finalDelimiter = delimiter;
587
+ let delimiterResolved = Boolean(finalDelimiter);
588
+
589
+ const processFields = (fields) => {
590
+ if (fields.length === 1 && fields[0].trim() === '') {
591
+ return null;
592
+ }
593
+
594
+ rowCount++;
595
+ if (maxRows && rowCount > maxRows) {
596
+ throw new LimitError(
597
+ `CSV size exceeds maximum limit of ${maxRows} rows`,
598
+ maxRows,
599
+ rowCount
600
+ );
601
+ }
602
+
603
+ const row = {};
604
+ const fieldCount = Math.min(fields.length, headers.length);
605
+ for (let j = 0; j < fieldCount; j++) {
606
+ row[headers[j]] = parseCsvValue(fields[j], { trim, parseNumbers, parseBooleans });
607
+ }
608
+ return row;
609
+ };
610
+
611
+ const processLine = (line) => {
612
+ lineNumber++;
613
+ let cleanLine = line;
614
+ if (cleanLine.endsWith('\r')) {
615
+ cleanLine = cleanLine.slice(0, -1);
616
+ }
617
+
618
+ if (!delimiterResolved) {
619
+ if (!finalDelimiter && autoDetect) {
620
+ finalDelimiter = autoDetectDelimiter(cleanLine, candidates);
621
+ }
622
+ finalDelimiter = finalDelimiter || ';';
623
+ delimiterResolved = true;
624
+ }
625
+
626
+ if (cleanLine.trim() === '') {
627
+ return null;
628
+ }
629
+
630
+ if (!headers) {
631
+ if (hasHeaders) {
632
+ headers = parseCsvLine(cleanLine, lineNumber, finalDelimiter).map(header => {
633
+ const trimmed = trim ? header.trim() : header;
634
+ return renameMap[trimmed] || trimmed;
635
+ });
636
+ return null;
637
+ }
638
+
639
+ const fields = parseCsvLine(cleanLine, lineNumber, finalDelimiter);
640
+ headers = fields.map((_, index) => `column${index + 1}`);
641
+ return processFields(fields);
642
+ }
643
+
644
+ const fields = parseCsvLine(cleanLine, lineNumber, finalDelimiter);
645
+ return processFields(fields);
646
+ };
647
+
648
+ while (true) {
649
+ const { value, done } = await reader.read();
650
+ if (done) {
651
+ break;
652
+ }
653
+
654
+ buffer += decoder.decode(value, { stream: true });
655
+
656
+ let start = 0;
657
+ for (let i = 0; i < buffer.length; i++) {
658
+ const char = buffer[i];
659
+ if (char === '"') {
660
+ if (insideQuotes && buffer[i + 1] === '"') {
661
+ i++;
662
+ continue;
663
+ }
664
+ insideQuotes = !insideQuotes;
665
+ continue;
666
+ }
667
+
668
+ if (char === '\n' && !insideQuotes) {
669
+ const line = buffer.slice(start, i);
670
+ start = i + 1;
671
+ const row = processLine(line);
672
+ if (row) {
673
+ yield row;
674
+ }
675
+ }
676
+ }
677
+
678
+ buffer = buffer.slice(start);
679
+ }
680
+
681
+ if (buffer.length > 0) {
682
+ const row = processLine(buffer);
683
+ if (row) {
684
+ yield row;
685
+ }
686
+ }
687
+
688
+ if (insideQuotes) {
689
+ throw new ParsingError('Unclosed quotes in CSV', lineNumber);
690
+ }
691
+ }
692
+
436
693
  // Экспорт для Node.js совместимости
437
694
  if (typeof module !== 'undefined' && module.exports) {
438
695
  module.exports = {
439
696
  csvToJson,
440
- autoDetectDelimiter
697
+ autoDetectDelimiter,
698
+ csvToJsonIterator
441
699
  };
442
- }
700
+ }
@@ -85,6 +85,23 @@ export class ConfigurationError extends JTCSVError {
85
85
  }
86
86
  }
87
87
 
88
+ export const ERROR_CODES = {
89
+ JTCSV_ERROR: 'JTCSV_ERROR',
90
+ VALIDATION_ERROR: 'VALIDATION_ERROR',
91
+ SECURITY_ERROR: 'SECURITY_ERROR',
92
+ FILE_SYSTEM_ERROR: 'FILE_SYSTEM_ERROR',
93
+ PARSING_ERROR: 'PARSING_ERROR',
94
+ LIMIT_ERROR: 'LIMIT_ERROR',
95
+ CONFIGURATION_ERROR: 'CONFIGURATION_ERROR',
96
+ INVALID_INPUT: 'INVALID_INPUT',
97
+ SECURITY_VIOLATION: 'SECURITY_VIOLATION',
98
+ FILE_NOT_FOUND: 'FILE_NOT_FOUND',
99
+ PARSE_FAILED: 'PARSE_FAILED',
100
+ SIZE_LIMIT: 'SIZE_LIMIT',
101
+ INVALID_CONFIG: 'INVALID_CONFIG',
102
+ UNKNOWN_ERROR: 'UNKNOWN_ERROR'
103
+ };
104
+
88
105
  /**
89
106
  * Безопасное выполнение функции с обработкой ошибок
90
107
  *
@@ -188,7 +205,8 @@ if (typeof module !== 'undefined' && module.exports) {
188
205
  ParsingError,
189
206
  LimitError,
190
207
  ConfigurationError,
208
+ ERROR_CODES,
191
209
  safeExecute,
192
210
  safeExecuteAsync
193
211
  };
194
- }
212
+ }
@@ -2,8 +2,15 @@
2
2
  // Экспортирует все функции с поддержкой браузера
3
3
 
4
4
  import { jsonToCsv, preprocessData, deepUnwrap } from './json-to-csv-browser.js';
5
- import { csvToJson, autoDetectDelimiter } from './csv-to-json-browser.js';
6
- import { downloadAsCsv, parseCsvFile } from './browser-functions.js';
5
+ import { csvToJson, csvToJsonIterator, autoDetectDelimiter } from './csv-to-json-browser.js';
6
+ import {
7
+ downloadAsCsv,
8
+ parseCsvFile,
9
+ parseCsvFileStream,
10
+ jsonToCsvStream,
11
+ jsonToNdjsonStream,
12
+ csvToJsonStream
13
+ } from './browser-functions.js';
7
14
  import { createWorkerPool, parseCSVWithWorker } from './workers/worker-pool.js';
8
15
  import {
9
16
  ValidationError,
@@ -11,9 +18,20 @@ import {
11
18
  FileSystemError,
12
19
  ParsingError,
13
20
  LimitError,
14
- ConfigurationError
21
+ ConfigurationError,
22
+ ERROR_CODES
15
23
  } from './errors-browser.js';
16
24
 
25
+ async function createWorkerPoolLazy(options = {}) {
26
+ const mod = await import('./workers/worker-pool.js');
27
+ return mod.createWorkerPool(options);
28
+ }
29
+
30
+ async function parseCSVWithWorkerLazy(csvInput, options = {}, onProgress = null) {
31
+ const mod = await import('./workers/worker-pool.js');
32
+ return mod.parseCSVWithWorker(csvInput, options, onProgress);
33
+ }
34
+
17
35
  // Основной экспорт
18
36
  const jtcsv = {
19
37
  // JSON to CSV функции
@@ -24,12 +42,19 @@ const jtcsv = {
24
42
 
25
43
  // CSV to JSON функции
26
44
  csvToJson,
45
+ csvToJsonIterator,
27
46
  parseCsvFile,
47
+ parseCsvFileStream,
48
+ jsonToCsvStream,
49
+ jsonToNdjsonStream,
50
+ csvToJsonStream,
28
51
  autoDetectDelimiter,
29
52
 
30
53
  // Web Workers функции
31
54
  createWorkerPool,
32
55
  parseCSVWithWorker,
56
+ createWorkerPoolLazy,
57
+ parseCSVWithWorkerLazy,
33
58
 
34
59
  // Error classes
35
60
  ValidationError,
@@ -38,6 +63,7 @@ const jtcsv = {
38
63
  ParsingError,
39
64
  LimitError,
40
65
  ConfigurationError,
66
+ ERROR_CODES,
41
67
 
42
68
  // Удобные алиасы
43
69
  parse: csvToJson,
@@ -66,14 +92,22 @@ export {
66
92
  downloadAsCsv,
67
93
  deepUnwrap,
68
94
  csvToJson,
95
+ csvToJsonIterator,
69
96
  parseCsvFile,
97
+ parseCsvFileStream,
98
+ jsonToCsvStream,
99
+ jsonToNdjsonStream,
100
+ csvToJsonStream,
70
101
  autoDetectDelimiter,
71
102
  createWorkerPool,
72
103
  parseCSVWithWorker,
104
+ createWorkerPoolLazy,
105
+ parseCSVWithWorkerLazy,
73
106
  ValidationError,
74
107
  SecurityError,
75
108
  FileSystemError,
76
109
  ParsingError,
77
110
  LimitError,
78
- ConfigurationError
79
- };
111
+ ConfigurationError,
112
+ ERROR_CODES
113
+ };