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.
- package/LICENSE +1 -1
- package/README.md +60 -341
- package/bin/jtcsv.js +2462 -1372
- package/csv-to-json.js +35 -26
- package/dist/jtcsv.cjs.js +807 -133
- package/dist/jtcsv.cjs.js.map +1 -1
- package/dist/jtcsv.esm.js +800 -134
- package/dist/jtcsv.esm.js.map +1 -1
- package/dist/jtcsv.umd.js +807 -133
- package/dist/jtcsv.umd.js.map +1 -1
- package/errors.js +20 -0
- package/examples/browser-vanilla.html +37 -0
- package/examples/cli-batch-processing.js +38 -0
- package/examples/error-handling.js +324 -0
- package/examples/ndjson-processing.js +434 -0
- package/examples/react-integration.jsx +637 -0
- package/examples/schema-validation.js +640 -0
- package/examples/simple-usage.js +10 -7
- package/examples/typescript-example.ts +486 -0
- package/examples/web-workers-advanced.js +28 -0
- package/index.d.ts +2 -0
- package/json-save.js +2 -1
- package/json-to-csv.js +171 -131
- package/package.json +20 -4
- package/plugins/README.md +41 -467
- package/plugins/express-middleware/README.md +32 -274
- package/plugins/hono/README.md +16 -13
- package/plugins/nestjs/README.md +13 -11
- package/plugins/nextjs-api/README.md +28 -423
- package/plugins/nextjs-api/index.js +1 -2
- package/plugins/nextjs-api/route.js +1 -2
- package/plugins/nuxt/README.md +6 -7
- package/plugins/remix/README.md +9 -9
- package/plugins/sveltekit/README.md +8 -8
- package/plugins/trpc/README.md +8 -5
- package/src/browser/browser-functions.js +33 -3
- package/src/browser/csv-to-json-browser.js +269 -11
- package/src/browser/errors-browser.js +19 -1
- package/src/browser/index.js +39 -5
- package/src/browser/streams.js +393 -0
- package/src/browser/workers/csv-parser.worker.js +20 -2
- package/src/browser/workers/worker-pool.js +507 -447
- package/src/core/plugin-system.js +4 -0
- package/src/engines/fast-path-engine.js +31 -23
- package/src/errors.js +26 -0
- package/src/formats/ndjson-parser.js +54 -5
- package/src/formats/tsv-parser.js +4 -1
- package/src/utils/schema-validator.js +594 -0
- package/src/utils/transform-loader.js +205 -0
- package/src/web-server/index.js +683 -0
- package/stream-csv-to-json.js +16 -87
- package/stream-json-to-csv.js +18 -86
|
@@ -112,7 +112,9 @@ class PluginManager {
|
|
|
112
112
|
// Проверяем обязательные поля
|
|
113
113
|
const required = ['name', 'version'];
|
|
114
114
|
required.forEach(field => {
|
|
115
|
+
/* istanbul ignore next */
|
|
115
116
|
if (!plugin[field]) {
|
|
117
|
+
/* istanbul ignore next */
|
|
116
118
|
throw new Error(`Plugin missing required field: ${field}`);
|
|
117
119
|
}
|
|
118
120
|
});
|
|
@@ -287,6 +289,7 @@ class PluginManager {
|
|
|
287
289
|
* @param {Object} options - Опции
|
|
288
290
|
* @returns {Object} Контекст
|
|
289
291
|
*/
|
|
292
|
+
/* istanbul ignore next */
|
|
290
293
|
createContext(operation, input, options = {}) {
|
|
291
294
|
return {
|
|
292
295
|
operation,
|
|
@@ -403,6 +406,7 @@ class PluginManager {
|
|
|
403
406
|
}
|
|
404
407
|
|
|
405
408
|
plugin.enabled = enabled;
|
|
409
|
+
/* istanbul ignore next */
|
|
406
410
|
console.log(`🔧 Plugin "${pluginName}" ${enabled ? 'включен' : 'выключен'}`);
|
|
407
411
|
}
|
|
408
412
|
|
|
@@ -138,10 +138,11 @@ class FastPathEngine {
|
|
|
138
138
|
|
|
139
139
|
const finalScore = score / (variance + 1);
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
141
|
+
/* istanbul ignore next */
|
|
142
|
+
if (finalScore > bestScore) {
|
|
143
|
+
bestScore = finalScore;
|
|
144
|
+
bestDelimiter = delimiter;
|
|
145
|
+
}
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
return bestDelimiter;
|
|
@@ -417,9 +418,10 @@ class FastPathEngine {
|
|
|
417
418
|
|
|
418
419
|
return (csv) => {
|
|
419
420
|
const rows = [];
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
421
|
+
/* istanbul ignore next */
|
|
422
|
+
const iterator = hasBackslashes
|
|
423
|
+
? this._quoteAwareEscapedRowsGenerator(csv, delimiter, hasEscapedQuotes)
|
|
424
|
+
: this._quoteAwareRowsGenerator(csv, delimiter, hasEscapedQuotes);
|
|
423
425
|
|
|
424
426
|
for (const row of iterator) {
|
|
425
427
|
rows.push(row);
|
|
@@ -512,9 +514,10 @@ class FastPathEngine {
|
|
|
512
514
|
currentField = '';
|
|
513
515
|
|
|
514
516
|
if (char === '\n' || char === '\r') {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
517
|
+
/* istanbul ignore next */
|
|
518
|
+
if (rowHasData) {
|
|
519
|
+
yield currentRow;
|
|
520
|
+
}
|
|
518
521
|
currentRow = [];
|
|
519
522
|
rowHasData = false;
|
|
520
523
|
lineNumber++;
|
|
@@ -541,9 +544,10 @@ class FastPathEngine {
|
|
|
541
544
|
|
|
542
545
|
if (currentField !== '' || currentRow.length > 0) {
|
|
543
546
|
currentRow.push(currentField);
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
+
/* istanbul ignore next */
|
|
548
|
+
if (rowHasData) {
|
|
549
|
+
yield currentRow;
|
|
550
|
+
}
|
|
547
551
|
}
|
|
548
552
|
}
|
|
549
553
|
|
|
@@ -645,9 +649,10 @@ class FastPathEngine {
|
|
|
645
649
|
currentField = '';
|
|
646
650
|
|
|
647
651
|
if (char === '\n' || char === '\r') {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
652
|
+
/* istanbul ignore next */
|
|
653
|
+
if (rowHasData) {
|
|
654
|
+
yield currentRow;
|
|
655
|
+
}
|
|
651
656
|
currentRow = [];
|
|
652
657
|
rowHasData = false;
|
|
653
658
|
lineNumber++;
|
|
@@ -665,9 +670,10 @@ class FastPathEngine {
|
|
|
665
670
|
i++;
|
|
666
671
|
}
|
|
667
672
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
673
|
+
/* istanbul ignore next */
|
|
674
|
+
if (escapeNext) {
|
|
675
|
+
currentField += '\\';
|
|
676
|
+
}
|
|
671
677
|
|
|
672
678
|
if (insideQuotes) {
|
|
673
679
|
const error = new Error('Unclosed quotes in CSV');
|
|
@@ -678,9 +684,10 @@ class FastPathEngine {
|
|
|
678
684
|
|
|
679
685
|
if (currentField !== '' || currentRow.length > 0) {
|
|
680
686
|
currentRow.push(currentField);
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
687
|
+
/* istanbul ignore next */
|
|
688
|
+
if (rowHasData) {
|
|
689
|
+
yield currentRow;
|
|
690
|
+
}
|
|
684
691
|
}
|
|
685
692
|
}
|
|
686
693
|
|
|
@@ -800,7 +807,8 @@ class FastPathEngine {
|
|
|
800
807
|
/**
|
|
801
808
|
* Parses CSV and emits rows via a callback to reduce memory usage.
|
|
802
809
|
*/
|
|
803
|
-
|
|
810
|
+
/* istanbul ignore next */
|
|
811
|
+
parseRows(csv, options = {}, onRow) {
|
|
804
812
|
for (const row of this.iterateRows(csv, options)) {
|
|
805
813
|
onRow(row);
|
|
806
814
|
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class ValidationError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'ValidationError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
class SecurityError extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'SecurityError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class ConfigurationError extends Error {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'ConfigurationError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
ValidationError,
|
|
24
|
+
SecurityError,
|
|
25
|
+
ConfigurationError
|
|
26
|
+
};
|
|
@@ -6,6 +6,29 @@
|
|
|
6
6
|
* @date 2026-01-22
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
function createTextDecoder() {
|
|
10
|
+
if (typeof TextDecoder !== 'undefined') {
|
|
11
|
+
return new TextDecoder('utf-8');
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const { TextDecoder: UtilTextDecoder } = require('util');
|
|
15
|
+
return new UtilTextDecoder('utf-8');
|
|
16
|
+
} catch (error) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getTransformStream() {
|
|
22
|
+
if (typeof TransformStream !== 'undefined') {
|
|
23
|
+
return TransformStream;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
return require('stream/web').TransformStream;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
9
32
|
class NdjsonParser {
|
|
10
33
|
/**
|
|
11
34
|
* Парсит NDJSON поток и возвращает async iterator
|
|
@@ -46,6 +69,8 @@ class NdjsonParser {
|
|
|
46
69
|
try {
|
|
47
70
|
yield JSON.parse(line);
|
|
48
71
|
} catch (error) {
|
|
72
|
+
/* istanbul ignore next */
|
|
73
|
+
/* istanbul ignore next */
|
|
49
74
|
if (onError) {
|
|
50
75
|
onError(error, line, lineNumber);
|
|
51
76
|
} else {
|
|
@@ -59,7 +84,10 @@ class NdjsonParser {
|
|
|
59
84
|
|
|
60
85
|
// Если входные данные - поток
|
|
61
86
|
const reader = input.getReader ? input.getReader() : input;
|
|
62
|
-
const decoder =
|
|
87
|
+
const decoder = createTextDecoder();
|
|
88
|
+
if (!decoder) {
|
|
89
|
+
throw new Error('TextDecoder is not available in this environment');
|
|
90
|
+
}
|
|
63
91
|
|
|
64
92
|
try {
|
|
65
93
|
while (true) {
|
|
@@ -71,10 +99,12 @@ class NdjsonParser {
|
|
|
71
99
|
const lines = buffer.split('\n');
|
|
72
100
|
for (const line of lines) {
|
|
73
101
|
lineNumber++;
|
|
102
|
+
/* istanbul ignore next */
|
|
74
103
|
if (line.trim()) {
|
|
75
104
|
try {
|
|
76
105
|
yield JSON.parse(line);
|
|
77
106
|
} catch (error) {
|
|
107
|
+
/* istanbul ignore next */
|
|
78
108
|
if (onError) {
|
|
79
109
|
onError(error, line, lineNumber);
|
|
80
110
|
}
|
|
@@ -102,10 +132,12 @@ class NdjsonParser {
|
|
|
102
132
|
// Обрабатываем полные строки
|
|
103
133
|
for (const line of lines) {
|
|
104
134
|
lineNumber++;
|
|
135
|
+
/* istanbul ignore next */
|
|
105
136
|
if (line.trim()) {
|
|
106
137
|
try {
|
|
107
138
|
yield JSON.parse(line);
|
|
108
139
|
} catch (error) {
|
|
140
|
+
/* istanbul ignore next */
|
|
109
141
|
if (onError) {
|
|
110
142
|
onError(error, line, lineNumber);
|
|
111
143
|
} else {
|
|
@@ -117,6 +149,7 @@ class NdjsonParser {
|
|
|
117
149
|
}
|
|
118
150
|
} finally {
|
|
119
151
|
// Освобождаем ресурсы
|
|
152
|
+
/* istanbul ignore next */
|
|
120
153
|
if (reader.releaseLock) {
|
|
121
154
|
reader.releaseLock();
|
|
122
155
|
}
|
|
@@ -185,7 +218,7 @@ class NdjsonParser {
|
|
|
185
218
|
// Применяем трансформацию если задана
|
|
186
219
|
return transform ? transform(obj, index) : obj;
|
|
187
220
|
} catch (error) {
|
|
188
|
-
if (onError) {
|
|
221
|
+
/* istanbul ignore next */ if (onError) {
|
|
189
222
|
onError(error, line, index + 1);
|
|
190
223
|
}
|
|
191
224
|
return null;
|
|
@@ -209,7 +242,12 @@ class NdjsonParser {
|
|
|
209
242
|
let headers = null;
|
|
210
243
|
let firstChunk = true;
|
|
211
244
|
|
|
212
|
-
|
|
245
|
+
const TransformStreamCtor = getTransformStream();
|
|
246
|
+
if (!TransformStreamCtor) {
|
|
247
|
+
throw new Error('TransformStream is not available in this environment');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return new TransformStreamCtor({
|
|
213
251
|
async transform(chunk, controller) {
|
|
214
252
|
try {
|
|
215
253
|
const obj = JSON.parse(chunk);
|
|
@@ -264,7 +302,12 @@ class NdjsonParser {
|
|
|
264
302
|
let headers = null;
|
|
265
303
|
let firstLine = true;
|
|
266
304
|
|
|
267
|
-
|
|
305
|
+
const TransformStreamCtor = getTransformStream();
|
|
306
|
+
if (!TransformStreamCtor) {
|
|
307
|
+
throw new Error('TransformStream is not available in this environment');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return new TransformStreamCtor({
|
|
268
311
|
transform(chunk, controller) {
|
|
269
312
|
const lines = chunk.toString().split('\n');
|
|
270
313
|
|
|
@@ -361,7 +404,10 @@ class NdjsonParser {
|
|
|
361
404
|
} else {
|
|
362
405
|
// Для потоков
|
|
363
406
|
const reader = input.getReader();
|
|
364
|
-
const decoder =
|
|
407
|
+
const decoder = createTextDecoder();
|
|
408
|
+
if (!decoder) {
|
|
409
|
+
throw new Error('TextDecoder is not available in this environment');
|
|
410
|
+
}
|
|
365
411
|
let buffer = '';
|
|
366
412
|
|
|
367
413
|
try {
|
|
@@ -371,6 +417,7 @@ class NdjsonParser {
|
|
|
371
417
|
|
|
372
418
|
if (done) {
|
|
373
419
|
// Обрабатываем оставшийся буфер
|
|
420
|
+
/* istanbul ignore next */
|
|
374
421
|
if (buffer.trim()) {
|
|
375
422
|
stats.totalLines++;
|
|
376
423
|
try {
|
|
@@ -391,6 +438,7 @@ class NdjsonParser {
|
|
|
391
438
|
|
|
392
439
|
for (const line of lines) {
|
|
393
440
|
stats.totalLines++;
|
|
441
|
+
/* istanbul ignore next */
|
|
394
442
|
if (line.trim()) {
|
|
395
443
|
try {
|
|
396
444
|
JSON.parse(line);
|
|
@@ -411,6 +459,7 @@ class NdjsonParser {
|
|
|
411
459
|
}
|
|
412
460
|
}
|
|
413
461
|
|
|
462
|
+
/* istanbul ignore next */
|
|
414
463
|
stats.successRate = stats.totalLines > 0 ? (stats.validLines / stats.totalLines) * 100 : 0;
|
|
415
464
|
return stats;
|
|
416
465
|
}
|
|
@@ -319,11 +319,14 @@ class TsvParser {
|
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
+
/* istanbul ignore next */
|
|
323
|
+
const totalColumns = columnCounts[0] || 0;
|
|
324
|
+
|
|
322
325
|
return {
|
|
323
326
|
valid: errors.length === 0,
|
|
324
327
|
stats: {
|
|
325
328
|
totalLines: lines.length,
|
|
326
|
-
totalColumns
|
|
329
|
+
totalColumns,
|
|
327
330
|
minColumns: Math.min(...columnCounts),
|
|
328
331
|
maxColumns: Math.max(...columnCounts),
|
|
329
332
|
consistentColumns: new Set(columnCounts).size === 1
|