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
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ValidationError,
|
|
3
|
+
ConfigurationError,
|
|
4
|
+
LimitError
|
|
5
|
+
} from './errors-browser.js';
|
|
6
|
+
import { csvToJsonIterator } from './csv-to-json-browser.js';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_MAX_CHUNK_SIZE = 64 * 1024;
|
|
9
|
+
|
|
10
|
+
function isReadableStream(value) {
|
|
11
|
+
return value && typeof value.getReader === 'function';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isAsyncIterable(value) {
|
|
15
|
+
return value && typeof value[Symbol.asyncIterator] === 'function';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isIterable(value) {
|
|
19
|
+
return value && typeof value[Symbol.iterator] === 'function';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createReadableStreamFromIterator(iterator) {
|
|
23
|
+
return new ReadableStream({
|
|
24
|
+
async pull(controller) {
|
|
25
|
+
try {
|
|
26
|
+
const { value, done } = await iterator.next();
|
|
27
|
+
if (done) {
|
|
28
|
+
controller.close();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
controller.enqueue(value);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
controller.error(error);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
cancel() {
|
|
37
|
+
if (iterator.return) {
|
|
38
|
+
iterator.return();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function detectInputFormat(input, options) {
|
|
45
|
+
if (options && options.inputFormat) {
|
|
46
|
+
return options.inputFormat;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (typeof input === 'string') {
|
|
50
|
+
const trimmed = input.trim();
|
|
51
|
+
if (trimmed.startsWith('[')) {
|
|
52
|
+
return 'json-array';
|
|
53
|
+
}
|
|
54
|
+
if (trimmed.includes('\n')) {
|
|
55
|
+
return 'ndjson';
|
|
56
|
+
}
|
|
57
|
+
return 'json-array';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (input instanceof Blob || isReadableStream(input)) {
|
|
61
|
+
return 'ndjson';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return 'json-array';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function* parseNdjsonText(text) {
|
|
68
|
+
const lines = text.split(/\r?\n/);
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
const trimmed = line.trim();
|
|
71
|
+
if (!trimmed) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
yield JSON.parse(trimmed);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function* parseNdjsonStream(stream) {
|
|
79
|
+
const reader = stream.getReader();
|
|
80
|
+
const decoder = new TextDecoder('utf-8');
|
|
81
|
+
let buffer = '';
|
|
82
|
+
|
|
83
|
+
while (true) {
|
|
84
|
+
const { value, done } = await reader.read();
|
|
85
|
+
if (done) {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
buffer += decoder.decode(value, { stream: true });
|
|
90
|
+
const lines = buffer.split(/\r?\n/);
|
|
91
|
+
buffer = lines.pop() || '';
|
|
92
|
+
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
const trimmed = line.trim();
|
|
95
|
+
if (!trimmed) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
yield JSON.parse(trimmed);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (buffer.trim()) {
|
|
103
|
+
yield JSON.parse(buffer.trim());
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function* normalizeJsonInput(input, options = {}) {
|
|
108
|
+
const format = detectInputFormat(input, options);
|
|
109
|
+
|
|
110
|
+
if (Array.isArray(input)) {
|
|
111
|
+
for (const item of input) {
|
|
112
|
+
yield item;
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (isAsyncIterable(input)) {
|
|
118
|
+
for await (const item of input) {
|
|
119
|
+
yield item;
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (isIterable(input)) {
|
|
125
|
+
for (const item of input) {
|
|
126
|
+
yield item;
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (typeof input === 'string') {
|
|
132
|
+
if (format === 'ndjson') {
|
|
133
|
+
yield* parseNdjsonText(input);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const parsed = JSON.parse(input);
|
|
138
|
+
if (Array.isArray(parsed)) {
|
|
139
|
+
for (const item of parsed) {
|
|
140
|
+
yield item;
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
yield parsed;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (input instanceof Blob) {
|
|
149
|
+
if (format === 'ndjson') {
|
|
150
|
+
yield* parseNdjsonStream(input.stream());
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const text = await input.text();
|
|
155
|
+
const parsed = JSON.parse(text);
|
|
156
|
+
if (Array.isArray(parsed)) {
|
|
157
|
+
for (const item of parsed) {
|
|
158
|
+
yield item;
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
yield parsed;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (isReadableStream(input)) {
|
|
167
|
+
if (format !== 'ndjson') {
|
|
168
|
+
throw new ValidationError('ReadableStream input requires inputFormat="ndjson"');
|
|
169
|
+
}
|
|
170
|
+
yield* parseNdjsonStream(input);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
throw new ValidationError('Input must be an array, iterable, string, Blob, or ReadableStream');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function validateStreamOptions(options) {
|
|
178
|
+
if (options && typeof options !== 'object') {
|
|
179
|
+
throw new ConfigurationError('Options must be an object');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (options?.delimiter && typeof options.delimiter !== 'string') {
|
|
183
|
+
throw new ConfigurationError('Delimiter must be a string');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (options?.delimiter && options.delimiter.length !== 1) {
|
|
187
|
+
throw new ConfigurationError('Delimiter must be a single character');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (options?.renameMap && typeof options.renameMap !== 'object') {
|
|
191
|
+
throw new ConfigurationError('renameMap must be an object');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (options?.maxRecords !== undefined) {
|
|
195
|
+
if (typeof options.maxRecords !== 'number' || options.maxRecords <= 0) {
|
|
196
|
+
throw new ConfigurationError('maxRecords must be a positive number');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function escapeCsvValue(value, options) {
|
|
202
|
+
const {
|
|
203
|
+
delimiter,
|
|
204
|
+
preventCsvInjection = true,
|
|
205
|
+
rfc4180Compliant = true
|
|
206
|
+
} = options;
|
|
207
|
+
|
|
208
|
+
if (value === null || value === undefined || value === '') {
|
|
209
|
+
return '';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const stringValue = String(value);
|
|
213
|
+
let escapedValue = stringValue;
|
|
214
|
+
if (preventCsvInjection && /^[=+\-@]/.test(stringValue)) {
|
|
215
|
+
escapedValue = "'" + stringValue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const needsQuoting = rfc4180Compliant
|
|
219
|
+
? (escapedValue.includes(delimiter) ||
|
|
220
|
+
escapedValue.includes('"') ||
|
|
221
|
+
escapedValue.includes('\n') ||
|
|
222
|
+
escapedValue.includes('\r'))
|
|
223
|
+
: (escapedValue.includes(delimiter) ||
|
|
224
|
+
escapedValue.includes('"') ||
|
|
225
|
+
escapedValue.includes('\n') ||
|
|
226
|
+
escapedValue.includes('\r'));
|
|
227
|
+
|
|
228
|
+
if (needsQuoting) {
|
|
229
|
+
return `"${escapedValue.replace(/"/g, '""')}"`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return escapedValue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function buildHeaderState(keys, options) {
|
|
236
|
+
const renameMap = options.renameMap || {};
|
|
237
|
+
const template = options.template || {};
|
|
238
|
+
const originalKeys = Array.isArray(options.headers) ? options.headers : keys;
|
|
239
|
+
const headers = originalKeys.map((key) => renameMap[key] || key);
|
|
240
|
+
|
|
241
|
+
const reverseRenameMap = {};
|
|
242
|
+
originalKeys.forEach((key, index) => {
|
|
243
|
+
reverseRenameMap[headers[index]] = key;
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
let finalHeaders = headers;
|
|
247
|
+
if (Object.keys(template).length > 0) {
|
|
248
|
+
const templateHeaders = Object.keys(template).map(key => renameMap[key] || key);
|
|
249
|
+
const extraHeaders = headers.filter(h => !templateHeaders.includes(h));
|
|
250
|
+
finalHeaders = [...templateHeaders, ...extraHeaders];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
headers: finalHeaders,
|
|
255
|
+
reverseRenameMap
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function* jsonToCsvChunkIterator(input, options = {}) {
|
|
260
|
+
validateStreamOptions(options);
|
|
261
|
+
|
|
262
|
+
const opts = options && typeof options === 'object' ? options : {};
|
|
263
|
+
const {
|
|
264
|
+
delimiter = ';',
|
|
265
|
+
includeHeaders = true,
|
|
266
|
+
maxRecords,
|
|
267
|
+
maxChunkSize = DEFAULT_MAX_CHUNK_SIZE,
|
|
268
|
+
headerMode
|
|
269
|
+
} = opts;
|
|
270
|
+
|
|
271
|
+
let headerState = null;
|
|
272
|
+
let buffer = '';
|
|
273
|
+
let recordCount = 0;
|
|
274
|
+
const lineEnding = opts.rfc4180Compliant === false ? '\n' : '\r\n';
|
|
275
|
+
|
|
276
|
+
if (Array.isArray(input) && !opts.headers && (!headerMode || headerMode === 'all')) {
|
|
277
|
+
const allKeys = new Set();
|
|
278
|
+
for (const item of input) {
|
|
279
|
+
if (!item || typeof item !== 'object') {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
Object.keys(item).forEach((key) => allKeys.add(key));
|
|
283
|
+
}
|
|
284
|
+
headerState = buildHeaderState(Array.from(allKeys), opts);
|
|
285
|
+
if (includeHeaders && headerState.headers.length > 0) {
|
|
286
|
+
buffer += headerState.headers.join(delimiter) + lineEnding;
|
|
287
|
+
}
|
|
288
|
+
} else if (Array.isArray(opts.headers)) {
|
|
289
|
+
headerState = buildHeaderState(opts.headers, opts);
|
|
290
|
+
if (includeHeaders && headerState.headers.length > 0) {
|
|
291
|
+
buffer += headerState.headers.join(delimiter) + lineEnding;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
for await (const item of normalizeJsonInput(input, opts)) {
|
|
296
|
+
if (!item || typeof item !== 'object') {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!headerState) {
|
|
301
|
+
headerState = buildHeaderState(Object.keys(item), opts);
|
|
302
|
+
if (includeHeaders && headerState.headers.length > 0) {
|
|
303
|
+
buffer += headerState.headers.join(delimiter) + lineEnding;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
recordCount += 1;
|
|
308
|
+
if (maxRecords && recordCount > maxRecords) {
|
|
309
|
+
throw new LimitError(
|
|
310
|
+
`Data size exceeds maximum limit of ${maxRecords} records`,
|
|
311
|
+
maxRecords,
|
|
312
|
+
recordCount
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const row = headerState.headers.map((header) => {
|
|
317
|
+
const originalKey = headerState.reverseRenameMap[header] || header;
|
|
318
|
+
return escapeCsvValue(item[originalKey], {
|
|
319
|
+
delimiter,
|
|
320
|
+
preventCsvInjection: opts.preventCsvInjection !== false,
|
|
321
|
+
rfc4180Compliant: opts.rfc4180Compliant !== false
|
|
322
|
+
});
|
|
323
|
+
}).join(delimiter);
|
|
324
|
+
|
|
325
|
+
buffer += row + lineEnding;
|
|
326
|
+
|
|
327
|
+
if (buffer.length >= maxChunkSize) {
|
|
328
|
+
yield buffer;
|
|
329
|
+
buffer = '';
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (buffer.length > 0) {
|
|
334
|
+
yield buffer;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function* jsonToNdjsonChunkIterator(input, options = {}) {
|
|
339
|
+
validateStreamOptions(options);
|
|
340
|
+
for await (const item of normalizeJsonInput(input, options)) {
|
|
341
|
+
if (item === undefined) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
yield JSON.stringify(item) + '\n';
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function* csvToJsonChunkIterator(input, options = {}) {
|
|
349
|
+
const outputFormat = options.outputFormat || 'ndjson';
|
|
350
|
+
const asArray = outputFormat === 'json-array' || outputFormat === 'array' || outputFormat === 'json';
|
|
351
|
+
let first = true;
|
|
352
|
+
|
|
353
|
+
if (asArray) {
|
|
354
|
+
yield '[';
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for await (const row of csvToJsonIterator(input, options)) {
|
|
358
|
+
const payload = JSON.stringify(row);
|
|
359
|
+
if (asArray) {
|
|
360
|
+
yield (first ? '' : ',') + payload;
|
|
361
|
+
} else {
|
|
362
|
+
yield payload + '\n';
|
|
363
|
+
}
|
|
364
|
+
first = false;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (asArray) {
|
|
368
|
+
yield ']';
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function jsonToCsvStream(input, options = {}) {
|
|
373
|
+
const iterator = jsonToCsvChunkIterator(input, options);
|
|
374
|
+
return createReadableStreamFromIterator(iterator);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function jsonToNdjsonStream(input, options = {}) {
|
|
378
|
+
const iterator = jsonToNdjsonChunkIterator(input, options);
|
|
379
|
+
return createReadableStreamFromIterator(iterator);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function csvToJsonStream(input, options = {}) {
|
|
383
|
+
const iterator = csvToJsonChunkIterator(input, options);
|
|
384
|
+
return createReadableStreamFromIterator(iterator);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
388
|
+
module.exports = {
|
|
389
|
+
jsonToCsvStream,
|
|
390
|
+
jsonToNdjsonStream,
|
|
391
|
+
csvToJsonStream
|
|
392
|
+
};
|
|
393
|
+
}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import { csvToJson } from '../csv-to-json-browser.js';
|
|
6
6
|
import { jsonToCsv } from '../json-to-csv-browser.js';
|
|
7
7
|
|
|
8
|
+
const textDecoder = new TextDecoder('utf-8');
|
|
9
|
+
|
|
8
10
|
// Кеш для повторного использования результатов
|
|
9
11
|
const cache = new Map();
|
|
10
12
|
const CACHE_MAX_SIZE = 50;
|
|
@@ -217,6 +219,19 @@ function clearCache() {
|
|
|
217
219
|
stats.cacheMisses = 0;
|
|
218
220
|
}
|
|
219
221
|
|
|
222
|
+
function decodeCsvInput(input) {
|
|
223
|
+
if (typeof input === 'string') {
|
|
224
|
+
return input;
|
|
225
|
+
}
|
|
226
|
+
if (input instanceof ArrayBuffer) {
|
|
227
|
+
return textDecoder.decode(new Uint8Array(input));
|
|
228
|
+
}
|
|
229
|
+
if (ArrayBuffer.isView(input)) {
|
|
230
|
+
return textDecoder.decode(input);
|
|
231
|
+
}
|
|
232
|
+
throw new Error('Invalid CSV input type');
|
|
233
|
+
}
|
|
234
|
+
|
|
220
235
|
// Обработчик сообщений от основного потока
|
|
221
236
|
self.onmessage = function (event) {
|
|
222
237
|
const { data } = event;
|
|
@@ -261,7 +276,8 @@ function handleExecute(commandData) {
|
|
|
261
276
|
try {
|
|
262
277
|
switch (method) {
|
|
263
278
|
case 'parseCSV': {
|
|
264
|
-
const [
|
|
279
|
+
const [csvInput, parseOptions] = args;
|
|
280
|
+
const csv = decodeCsvInput(csvInput);
|
|
265
281
|
|
|
266
282
|
// Функция отправки прогресса
|
|
267
283
|
const sendProgress = (progress) => {
|
|
@@ -339,7 +355,9 @@ function handleExecute(commandData) {
|
|
|
339
355
|
type: 'ERROR',
|
|
340
356
|
taskId,
|
|
341
357
|
message: error.message,
|
|
342
|
-
stack: error.stack
|
|
358
|
+
stack: error.stack,
|
|
359
|
+
code: error.code,
|
|
360
|
+
details: error.details
|
|
343
361
|
});
|
|
344
362
|
}
|
|
345
363
|
}
|