jtcsv 1.2.0 → 2.1.1
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 +272 -329
- package/bin/jtcsv.js +1092 -97
- package/cli-tui.js +0 -0
- package/csv-to-json.js +385 -311
- package/dist/jtcsv.cjs.js +1619 -0
- package/dist/jtcsv.cjs.js.map +1 -0
- package/dist/jtcsv.esm.js +1599 -0
- package/dist/jtcsv.esm.js.map +1 -0
- package/dist/jtcsv.umd.js +1625 -0
- package/dist/jtcsv.umd.js.map +1 -0
- package/examples/cli-tool.js +186 -0
- package/examples/express-api.js +167 -0
- package/examples/large-dataset-example.js +185 -0
- package/examples/plugin-excel-exporter.js +407 -0
- package/examples/simple-usage.js +280 -0
- package/examples/streaming-example.js +419 -0
- package/index.d.ts +288 -1
- package/index.js +23 -0
- package/json-save.js +1 -1
- package/json-to-csv.js +130 -89
- package/package.json +139 -13
- package/plugins/README.md +373 -0
- package/plugins/express-middleware/README.md +306 -0
- package/plugins/express-middleware/example.js +136 -0
- package/plugins/express-middleware/index.d.ts +114 -0
- package/plugins/express-middleware/index.js +360 -0
- package/plugins/express-middleware/package.json +52 -0
- package/plugins/fastify-plugin/index.js +406 -0
- package/plugins/fastify-plugin/package.json +55 -0
- package/plugins/nextjs-api/README.md +452 -0
- package/plugins/nextjs-api/examples/ConverterComponent.jsx +386 -0
- package/plugins/nextjs-api/examples/api-convert.js +69 -0
- package/plugins/nextjs-api/index.js +388 -0
- package/plugins/nextjs-api/package.json +63 -0
- package/plugins/nextjs-api/route.js +372 -0
- package/src/browser/browser-functions.js +189 -0
- package/src/browser/csv-to-json-browser.js +442 -0
- package/src/browser/errors-browser.js +194 -0
- package/src/browser/index.js +79 -0
- package/src/browser/json-to-csv-browser.js +309 -0
- package/src/browser/workers/csv-parser.worker.js +359 -0
- package/src/browser/workers/worker-pool.js +467 -0
- package/src/core/delimiter-cache.js +186 -0
- package/src/core/plugin-system.js +472 -0
- package/src/core/transform-hooks.js +350 -0
- package/src/engines/fast-path-engine-new.js +338 -0
- package/src/engines/fast-path-engine.js +836 -0
- package/src/formats/ndjson-parser.js +419 -0
- package/src/formats/tsv-parser.js +336 -0
- package/src/index-with-plugins.js +371 -0
- package/stream-csv-to-json.js +1 -1
- package/stream-json-to-csv.js +1 -1
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NDJSON (Newline Delimited JSON) парсер
|
|
3
|
+
* Поддержка потоковой обработки больших JSON файлов
|
|
4
|
+
*
|
|
5
|
+
* @version 1.0.0
|
|
6
|
+
* @date 2026-01-22
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
class NdjsonParser {
|
|
10
|
+
/**
|
|
11
|
+
* Парсит NDJSON поток и возвращает async iterator
|
|
12
|
+
* @param {ReadableStream|string} input - Входные данные (поток или строка)
|
|
13
|
+
* @param {Object} options - Опции парсинга
|
|
14
|
+
* @returns {AsyncGenerator} Async iterator с объектами JSON
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // Использование с потоком
|
|
18
|
+
* const stream = fs.createReadStream('data.ndjson');
|
|
19
|
+
* for await (const obj of NdjsonParser.parseStream(stream)) {
|
|
20
|
+
* console.log(obj);
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // Использование со строкой
|
|
25
|
+
* const ndjson = '{"name":"John"}\n{"name":"Jane"}';
|
|
26
|
+
* for await (const obj of NdjsonParser.parseStream(ndjson)) {
|
|
27
|
+
* console.log(obj);
|
|
28
|
+
* }
|
|
29
|
+
*/
|
|
30
|
+
static async *parseStream(input, options = {}) {
|
|
31
|
+
const {
|
|
32
|
+
bufferSize: _bufferSize = 64 * 1024, // 64KB буфер
|
|
33
|
+
maxLineLength = 10 * 1024 * 1024, // 10MB максимальная длина строки
|
|
34
|
+
onError = null
|
|
35
|
+
} = options;
|
|
36
|
+
|
|
37
|
+
let buffer = '';
|
|
38
|
+
let lineNumber = 0;
|
|
39
|
+
|
|
40
|
+
// Если входные данные - строка, преобразуем в async iterator
|
|
41
|
+
if (typeof input === 'string') {
|
|
42
|
+
const lines = input.split('\n');
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
lineNumber++;
|
|
45
|
+
if (line.trim()) {
|
|
46
|
+
try {
|
|
47
|
+
yield JSON.parse(line);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (onError) {
|
|
50
|
+
onError(error, line, lineNumber);
|
|
51
|
+
} else {
|
|
52
|
+
console.error(`Ошибка парсинга NDJSON строки ${lineNumber}:`, error.message);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Если входные данные - поток
|
|
61
|
+
const reader = input.getReader ? input.getReader() : input;
|
|
62
|
+
const decoder = new TextDecoder('utf-8');
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
while (true) {
|
|
66
|
+
const { done, value } = await reader.read();
|
|
67
|
+
|
|
68
|
+
if (done) {
|
|
69
|
+
// Обрабатываем оставшиеся данные в буфере
|
|
70
|
+
if (buffer.trim()) {
|
|
71
|
+
const lines = buffer.split('\n');
|
|
72
|
+
for (const line of lines) {
|
|
73
|
+
lineNumber++;
|
|
74
|
+
if (line.trim()) {
|
|
75
|
+
try {
|
|
76
|
+
yield JSON.parse(line);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (onError) {
|
|
79
|
+
onError(error, line, lineNumber);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Добавляем новые данные в буфере
|
|
89
|
+
buffer += decoder.decode(value, { stream: true });
|
|
90
|
+
|
|
91
|
+
// Проверяем длину буфера
|
|
92
|
+
if (buffer.length > maxLineLength) {
|
|
93
|
+
throw new Error(`Строка превышает максимальную длину ${maxLineLength} байт`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Обрабатываем полные строки
|
|
97
|
+
const lines = buffer.split('\n');
|
|
98
|
+
|
|
99
|
+
// Оставляем последнюю (возможно неполную) строку в буфере
|
|
100
|
+
buffer = lines.pop() || '';
|
|
101
|
+
|
|
102
|
+
// Обрабатываем полные строки
|
|
103
|
+
for (const line of lines) {
|
|
104
|
+
lineNumber++;
|
|
105
|
+
if (line.trim()) {
|
|
106
|
+
try {
|
|
107
|
+
yield JSON.parse(line);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (onError) {
|
|
110
|
+
onError(error, line, lineNumber);
|
|
111
|
+
} else {
|
|
112
|
+
console.error(`Ошибка парсинга NDJSON строки ${lineNumber}:`, error.message);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} finally {
|
|
119
|
+
// Освобождаем ресурсы
|
|
120
|
+
if (reader.releaseLock) {
|
|
121
|
+
reader.releaseLock();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Конвертирует массив объектов в NDJSON строку
|
|
128
|
+
* @param {Array} data - Массив объектов
|
|
129
|
+
* @param {Object} options - Опции форматирования
|
|
130
|
+
* @returns {string} NDJSON строка
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* const data = [{ name: 'John' }, { name: 'Jane' }];
|
|
134
|
+
* const ndjson = NdjsonParser.toNdjson(data);
|
|
135
|
+
* // Результат: '{"name":"John"}\n{"name":"Jane"}'
|
|
136
|
+
*/
|
|
137
|
+
static toNdjson(data, options = {}) {
|
|
138
|
+
if (!Array.isArray(data)) {
|
|
139
|
+
throw new Error('Input must be an array');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const {
|
|
143
|
+
replacer = null,
|
|
144
|
+
space = 0
|
|
145
|
+
} = options;
|
|
146
|
+
|
|
147
|
+
return data
|
|
148
|
+
.map(item => JSON.stringify(item, replacer, space))
|
|
149
|
+
.join('\n');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Конвертирует NDJSON строку в массив объектов
|
|
154
|
+
* @param {string} ndjsonString - NDJSON строка
|
|
155
|
+
* @param {Object} options - Опции парсинга
|
|
156
|
+
* @returns {Array} Массив объектов
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* const ndjson = '{"name":"John"}\n{"name":"Jane"}';
|
|
160
|
+
* const data = NdjsonParser.fromNdjson(ndjson);
|
|
161
|
+
* // Результат: [{ name: 'John' }, { name: 'Jane' }]
|
|
162
|
+
*/
|
|
163
|
+
static fromNdjson(ndjsonString, options = {}) {
|
|
164
|
+
const {
|
|
165
|
+
filter = null,
|
|
166
|
+
transform = null,
|
|
167
|
+
onError = null
|
|
168
|
+
} = options;
|
|
169
|
+
|
|
170
|
+
return ndjsonString
|
|
171
|
+
.split('\n')
|
|
172
|
+
.map((line, index) => {
|
|
173
|
+
if (!line.trim()) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const obj = JSON.parse(line);
|
|
179
|
+
|
|
180
|
+
// Применяем фильтр если задан
|
|
181
|
+
if (filter && !filter(obj, index)) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Применяем трансформацию если задана
|
|
186
|
+
return transform ? transform(obj, index) : obj;
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (onError) {
|
|
189
|
+
onError(error, line, index + 1);
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
.filter(obj => obj !== null);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Создает преобразователь NDJSON в CSV
|
|
199
|
+
* @param {Object} options - Опции конвертации
|
|
200
|
+
* @returns {TransformStream} Transform stream
|
|
201
|
+
*/
|
|
202
|
+
static createNdjsonToCsvStream(options = {}) {
|
|
203
|
+
const {
|
|
204
|
+
delimiter = ',',
|
|
205
|
+
includeHeaders = true,
|
|
206
|
+
..._csvOptions
|
|
207
|
+
} = options;
|
|
208
|
+
|
|
209
|
+
let headers = null;
|
|
210
|
+
let firstChunk = true;
|
|
211
|
+
|
|
212
|
+
return new TransformStream({
|
|
213
|
+
async transform(chunk, controller) {
|
|
214
|
+
try {
|
|
215
|
+
const obj = JSON.parse(chunk);
|
|
216
|
+
|
|
217
|
+
// Определяем заголовки при первом объекте
|
|
218
|
+
if (firstChunk && includeHeaders) {
|
|
219
|
+
headers = Object.keys(obj);
|
|
220
|
+
controller.enqueue(headers.join(delimiter) + '\n');
|
|
221
|
+
firstChunk = false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Конвертируем объект в CSV строку
|
|
225
|
+
const row = headers
|
|
226
|
+
? headers.map(header => this._escapeCsvField(obj[header], delimiter))
|
|
227
|
+
: Object.values(obj).map(value => this._escapeCsvField(value, delimiter));
|
|
228
|
+
|
|
229
|
+
controller.enqueue(row.join(delimiter) + '\n');
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.error('Ошибка преобразования NDJSON в CSV:', error);
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
_escapeCsvField(value, delimiter) {
|
|
236
|
+
if (value === null || value === undefined) {
|
|
237
|
+
return '';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const str = String(value);
|
|
241
|
+
|
|
242
|
+
// Экранируем если содержит delimiter, кавычки или перенос строки
|
|
243
|
+
if (str.includes(delimiter) || str.includes('"') || str.includes('\n')) {
|
|
244
|
+
return '"' + str.replace(/"/g, '""') + '"';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return str;
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Создает преобразователь CSV в NDJSON
|
|
254
|
+
* @param {Object} options - Опции конвертации
|
|
255
|
+
* @returns {TransformStream} Transform stream
|
|
256
|
+
*/
|
|
257
|
+
static createCsvToNdjsonStream(options = {}) {
|
|
258
|
+
const {
|
|
259
|
+
delimiter = ',',
|
|
260
|
+
hasHeaders = true,
|
|
261
|
+
..._csvOptions
|
|
262
|
+
} = options;
|
|
263
|
+
|
|
264
|
+
let headers = null;
|
|
265
|
+
let firstLine = true;
|
|
266
|
+
|
|
267
|
+
return new TransformStream({
|
|
268
|
+
transform(chunk, controller) {
|
|
269
|
+
const lines = chunk.toString().split('\n');
|
|
270
|
+
|
|
271
|
+
for (const line of lines) {
|
|
272
|
+
if (!line.trim()) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const fields = this._parseCsvLine(line, delimiter);
|
|
277
|
+
|
|
278
|
+
if (firstLine && hasHeaders) {
|
|
279
|
+
headers = fields;
|
|
280
|
+
firstLine = false;
|
|
281
|
+
} else {
|
|
282
|
+
const obj = headers
|
|
283
|
+
? headers.reduce((acc, header, index) => {
|
|
284
|
+
acc[header] = fields[index] || '';
|
|
285
|
+
return acc;
|
|
286
|
+
}, {})
|
|
287
|
+
: fields.reduce((acc, field, index) => {
|
|
288
|
+
acc[`field_${index}`] = field;
|
|
289
|
+
return acc;
|
|
290
|
+
}, {});
|
|
291
|
+
|
|
292
|
+
controller.enqueue(JSON.stringify(obj) + '\n');
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
_parseCsvLine(line, delimiter) {
|
|
298
|
+
const fields = [];
|
|
299
|
+
let currentField = '';
|
|
300
|
+
let insideQuotes = false;
|
|
301
|
+
|
|
302
|
+
for (let i = 0; i < line.length; i++) {
|
|
303
|
+
const char = line[i];
|
|
304
|
+
const nextChar = line[i + 1];
|
|
305
|
+
|
|
306
|
+
if (char === '"') {
|
|
307
|
+
if (insideQuotes && nextChar === '"') {
|
|
308
|
+
currentField += '"';
|
|
309
|
+
i++;
|
|
310
|
+
} else {
|
|
311
|
+
insideQuotes = !insideQuotes;
|
|
312
|
+
}
|
|
313
|
+
} else if (char === delimiter && !insideQuotes) {
|
|
314
|
+
fields.push(currentField);
|
|
315
|
+
currentField = '';
|
|
316
|
+
} else {
|
|
317
|
+
currentField += char;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
fields.push(currentField);
|
|
322
|
+
return fields;
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Статистика по NDJSON файлу
|
|
329
|
+
* @param {string|ReadableStream} input - Входные данные
|
|
330
|
+
* @returns {Promise<Object>} Статистика
|
|
331
|
+
*/
|
|
332
|
+
static async getStats(input) {
|
|
333
|
+
const stats = {
|
|
334
|
+
totalLines: 0,
|
|
335
|
+
validLines: 0,
|
|
336
|
+
errorLines: 0,
|
|
337
|
+
totalBytes: 0,
|
|
338
|
+
errors: []
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
if (typeof input === 'string') {
|
|
342
|
+
stats.totalBytes = Buffer.byteLength(input, 'utf8');
|
|
343
|
+
const lines = input.split('\n');
|
|
344
|
+
stats.totalLines = lines.length;
|
|
345
|
+
|
|
346
|
+
for (const line of lines) {
|
|
347
|
+
if (line.trim()) {
|
|
348
|
+
try {
|
|
349
|
+
JSON.parse(line);
|
|
350
|
+
stats.validLines++;
|
|
351
|
+
} catch (error) {
|
|
352
|
+
stats.errorLines++;
|
|
353
|
+
stats.errors.push({
|
|
354
|
+
line: stats.totalLines,
|
|
355
|
+
error: error.message,
|
|
356
|
+
content: line.substring(0, 100)
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
// Для потоков
|
|
363
|
+
const reader = input.getReader();
|
|
364
|
+
const decoder = new TextDecoder('utf-8');
|
|
365
|
+
let buffer = '';
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
// eslint-disable-next-line no-constant-condition
|
|
369
|
+
while (true) {
|
|
370
|
+
const { done, value } = await reader.read();
|
|
371
|
+
|
|
372
|
+
if (done) {
|
|
373
|
+
// Обрабатываем оставшийся буфер
|
|
374
|
+
if (buffer.trim()) {
|
|
375
|
+
stats.totalLines++;
|
|
376
|
+
try {
|
|
377
|
+
JSON.parse(buffer.trim());
|
|
378
|
+
stats.validLines++;
|
|
379
|
+
} catch (error) {
|
|
380
|
+
stats.errorLines++;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
stats.totalBytes += value.length;
|
|
387
|
+
buffer += decoder.decode(value, { stream: true });
|
|
388
|
+
|
|
389
|
+
const lines = buffer.split('\n');
|
|
390
|
+
buffer = lines.pop() || '';
|
|
391
|
+
|
|
392
|
+
for (const line of lines) {
|
|
393
|
+
stats.totalLines++;
|
|
394
|
+
if (line.trim()) {
|
|
395
|
+
try {
|
|
396
|
+
JSON.parse(line);
|
|
397
|
+
stats.validLines++;
|
|
398
|
+
} catch (error) {
|
|
399
|
+
stats.errorLines++;
|
|
400
|
+
stats.errors.push({
|
|
401
|
+
line: stats.totalLines,
|
|
402
|
+
error: error.message,
|
|
403
|
+
content: line.substring(0, 100)
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
} finally {
|
|
410
|
+
reader.releaseLock();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
stats.successRate = stats.totalLines > 0 ? (stats.validLines / stats.totalLines) * 100 : 0;
|
|
415
|
+
return stats;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
module.exports = NdjsonParser;
|