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
@@ -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 [csv, parseOptions] = args;
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
  }