jtcsv 2.2.3 → 2.2.5
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/package.json +21 -18
- package/src/core/node-optimizations.js +408 -0
- package/src/formats/ndjson-parser.js +5 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jtcsv",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.5",
|
|
4
4
|
"description": "Complete JSON<->CSV and CSV<->JSON converter for Node.js and Browser with streaming, security, Web Workers, TypeScript support, and optional ecosystem (zero-deps core)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"browser": "dist/jtcsv.umd.js",
|
|
@@ -183,7 +183,7 @@
|
|
|
183
183
|
},
|
|
184
184
|
"homepage": "https://github.com/Linol-Hamelton/jtcsv#readme",
|
|
185
185
|
"engines": {
|
|
186
|
-
"node": ">=
|
|
186
|
+
"node": ">=12.0.0"
|
|
187
187
|
},
|
|
188
188
|
"files": [
|
|
189
189
|
"index.js",
|
|
@@ -200,25 +200,28 @@
|
|
|
200
200
|
"examples/",
|
|
201
201
|
"plugins/"
|
|
202
202
|
],
|
|
203
|
-
"dependencies": {
|
|
204
|
-
|
|
203
|
+
"dependencies": {},
|
|
204
|
+
"optionalDependencies": {
|
|
205
|
+
"glob": "^11.0.0"
|
|
205
206
|
},
|
|
206
207
|
"devDependencies": {
|
|
207
|
-
"@babel/core": "^7.
|
|
208
|
-
"@babel/preset-env": "^7.
|
|
209
|
-
"@
|
|
210
|
-
"@rollup/plugin-
|
|
211
|
-
"@rollup/plugin-
|
|
212
|
-
"@rollup/plugin-
|
|
213
|
-
"@
|
|
208
|
+
"@babel/core": "^7.26.0",
|
|
209
|
+
"@babel/preset-env": "^7.26.0",
|
|
210
|
+
"@eslint/js": "^9.18.0",
|
|
211
|
+
"@rollup/plugin-babel": "^6.0.4",
|
|
212
|
+
"@rollup/plugin-commonjs": "^28.0.0",
|
|
213
|
+
"@rollup/plugin-node-resolve": "^16.0.0",
|
|
214
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
215
|
+
"@size-limit/preset-small-lib": "^11.1.0",
|
|
214
216
|
"blessed": "^0.1.81",
|
|
215
|
-
"blessed-contrib": "4.11.0",
|
|
216
|
-
"eslint": "
|
|
217
|
-
"
|
|
218
|
-
"jest
|
|
219
|
-
"
|
|
220
|
-
"
|
|
221
|
-
"
|
|
217
|
+
"blessed-contrib": "^4.11.0",
|
|
218
|
+
"eslint": "^9.18.0",
|
|
219
|
+
"globals": "^15.14.0",
|
|
220
|
+
"jest": "^29.7.0",
|
|
221
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
222
|
+
"rollup": "^4.30.0",
|
|
223
|
+
"size-limit": "^11.1.0",
|
|
224
|
+
"typedoc": "^0.27.0"
|
|
222
225
|
},
|
|
223
226
|
"type": "commonjs",
|
|
224
227
|
"size-limit": [
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js Runtime Optimizations
|
|
3
|
+
*
|
|
4
|
+
* Detects Node.js version and provides optimized implementations
|
|
5
|
+
* for modern runtimes while maintaining backward compatibility.
|
|
6
|
+
*
|
|
7
|
+
* Optimized for: Node 20, 22, 24
|
|
8
|
+
* Compatible with: Node 12+
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Parse Node.js version
|
|
12
|
+
const nodeVersion = process.versions?.node || '12.0.0';
|
|
13
|
+
const [major, minor] = nodeVersion.split('.').map(Number);
|
|
14
|
+
|
|
15
|
+
// Feature detection flags
|
|
16
|
+
const features = {
|
|
17
|
+
// Node 14.17+ / 16+
|
|
18
|
+
hasAbortController: typeof AbortController !== 'undefined',
|
|
19
|
+
|
|
20
|
+
// Node 15+
|
|
21
|
+
hasPromiseAny: typeof Promise.any === 'function',
|
|
22
|
+
|
|
23
|
+
// Node 16+
|
|
24
|
+
hasArrayAt: typeof Array.prototype.at === 'function',
|
|
25
|
+
hasObjectHasOwn: typeof Object.hasOwn === 'function',
|
|
26
|
+
|
|
27
|
+
// Node 17+
|
|
28
|
+
hasStructuredClone: typeof globalThis.structuredClone === 'function',
|
|
29
|
+
|
|
30
|
+
// Node 18+
|
|
31
|
+
hasFetch: typeof globalThis.fetch === 'function',
|
|
32
|
+
|
|
33
|
+
// Node 20+
|
|
34
|
+
hasWebStreams: typeof globalThis.ReadableStream !== 'undefined' && major >= 20,
|
|
35
|
+
hasArrayGroup: typeof Array.prototype.group === 'function',
|
|
36
|
+
|
|
37
|
+
// Node 21+
|
|
38
|
+
hasSetMethods: typeof Set.prototype.union === 'function',
|
|
39
|
+
|
|
40
|
+
// Node 22+
|
|
41
|
+
hasImportMeta: major >= 22,
|
|
42
|
+
hasExplicitResourceManagement: major >= 22,
|
|
43
|
+
|
|
44
|
+
// Version checks
|
|
45
|
+
isNode20Plus: major >= 20,
|
|
46
|
+
isNode22Plus: major >= 22,
|
|
47
|
+
isNode24Plus: major >= 24
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Optimized Object.hasOwn polyfill for older Node versions
|
|
52
|
+
*/
|
|
53
|
+
const hasOwn = features.hasObjectHasOwn
|
|
54
|
+
? Object.hasOwn
|
|
55
|
+
: (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Optimized deep clone function
|
|
59
|
+
* Uses structuredClone on Node 17+ for best performance
|
|
60
|
+
*/
|
|
61
|
+
const deepClone = features.hasStructuredClone
|
|
62
|
+
? (obj) => structuredClone(obj)
|
|
63
|
+
: (obj) => JSON.parse(JSON.stringify(obj));
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Optimized array access with at() method
|
|
67
|
+
*/
|
|
68
|
+
const arrayAt = features.hasArrayAt
|
|
69
|
+
? (arr, index) => arr.at(index)
|
|
70
|
+
: (arr, index) => {
|
|
71
|
+
const len = arr.length;
|
|
72
|
+
const normalizedIndex = index < 0 ? len + index : index;
|
|
73
|
+
return normalizedIndex >= 0 && normalizedIndex < len ? arr[normalizedIndex] : undefined;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* High-performance string builder for large CSV generation
|
|
78
|
+
* Uses different strategies based on Node version
|
|
79
|
+
*/
|
|
80
|
+
class StringBuilderOptimized {
|
|
81
|
+
constructor(initialCapacity = 1024) {
|
|
82
|
+
this.parts = [];
|
|
83
|
+
this.length = 0;
|
|
84
|
+
this.initialCapacity = initialCapacity;
|
|
85
|
+
|
|
86
|
+
// Node 20+ uses more aggressive chunking
|
|
87
|
+
this.chunkSize = features.isNode20Plus ? 65536 : 16384;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
append(str) {
|
|
91
|
+
if (str) {
|
|
92
|
+
this.parts.push(str);
|
|
93
|
+
this.length += str.length;
|
|
94
|
+
}
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
toString() {
|
|
99
|
+
return this.parts.join('');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
clear() {
|
|
103
|
+
this.parts = [];
|
|
104
|
+
this.length = 0;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Optimized row buffer for streaming CSV parsing
|
|
110
|
+
* Minimizes allocations on modern Node versions
|
|
111
|
+
*/
|
|
112
|
+
class RowBuffer {
|
|
113
|
+
constructor(initialSize = 100) {
|
|
114
|
+
this.rows = [];
|
|
115
|
+
this.currentRow = [];
|
|
116
|
+
this.rowCount = 0;
|
|
117
|
+
|
|
118
|
+
// Pre-allocate on Node 20+
|
|
119
|
+
if (features.isNode20Plus) {
|
|
120
|
+
this.rows = new Array(initialSize);
|
|
121
|
+
this.rows.length = 0;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
addField(field) {
|
|
126
|
+
this.currentRow.push(field);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
commitRow() {
|
|
130
|
+
if (this.currentRow.length > 0) {
|
|
131
|
+
this.rows.push(this.currentRow);
|
|
132
|
+
this.rowCount++;
|
|
133
|
+
this.currentRow = [];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
getRows() {
|
|
138
|
+
return this.rows;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
clear() {
|
|
142
|
+
this.rows = features.isNode20Plus ? new Array(100) : [];
|
|
143
|
+
this.rows.length = 0;
|
|
144
|
+
this.currentRow = [];
|
|
145
|
+
this.rowCount = 0;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Optimized field parser with char code comparisons
|
|
151
|
+
* Faster than string comparisons on all Node versions
|
|
152
|
+
*/
|
|
153
|
+
const CHAR_CODES = {
|
|
154
|
+
QUOTE: 34, // "
|
|
155
|
+
COMMA: 44, // ,
|
|
156
|
+
SEMICOLON: 59, // ;
|
|
157
|
+
TAB: 9, // \t
|
|
158
|
+
PIPE: 124, // |
|
|
159
|
+
NEWLINE: 10, // \n
|
|
160
|
+
CARRIAGE: 13, // \r
|
|
161
|
+
SPACE: 32, // space
|
|
162
|
+
EQUALS: 61, // =
|
|
163
|
+
PLUS: 43, // +
|
|
164
|
+
MINUS: 45, // -
|
|
165
|
+
AT: 64, // @
|
|
166
|
+
BACKSLASH: 92, // \
|
|
167
|
+
APOSTROPHE: 39 // '
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Fast delimiter detection using char codes
|
|
172
|
+
*/
|
|
173
|
+
function fastDetectDelimiter(sample, candidates = [';', ',', '\t', '|']) {
|
|
174
|
+
const firstLineEnd = sample.indexOf('\n');
|
|
175
|
+
const firstLine = firstLineEnd > -1 ? sample.slice(0, firstLineEnd) : sample;
|
|
176
|
+
|
|
177
|
+
const candidateCodes = candidates.map(c => c.charCodeAt(0));
|
|
178
|
+
const counts = new Array(candidateCodes.length).fill(0);
|
|
179
|
+
|
|
180
|
+
// Use fast char code iteration on Node 20+
|
|
181
|
+
const len = Math.min(firstLine.length, 10000);
|
|
182
|
+
|
|
183
|
+
for (let i = 0; i < len; i++) {
|
|
184
|
+
const code = firstLine.charCodeAt(i);
|
|
185
|
+
for (let j = 0; j < candidateCodes.length; j++) {
|
|
186
|
+
if (code === candidateCodes[j]) {
|
|
187
|
+
counts[j]++;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let maxCount = 0;
|
|
193
|
+
let maxIndex = 0;
|
|
194
|
+
|
|
195
|
+
for (let i = 0; i < counts.length; i++) {
|
|
196
|
+
if (counts[i] > maxCount) {
|
|
197
|
+
maxCount = counts[i];
|
|
198
|
+
maxIndex = i;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return candidates[maxIndex];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Optimized batch processor for large datasets
|
|
207
|
+
* Uses different chunk sizes based on Node version
|
|
208
|
+
*/
|
|
209
|
+
function createBatchProcessor(processor, options = {}) {
|
|
210
|
+
const batchSize = options.batchSize || (features.isNode20Plus ? 10000 : 5000);
|
|
211
|
+
const parallelism = options.parallelism || (features.isNode22Plus ? 4 : 2);
|
|
212
|
+
|
|
213
|
+
return async function* processBatches(items) {
|
|
214
|
+
const batches = [];
|
|
215
|
+
|
|
216
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
217
|
+
batches.push(items.slice(i, i + batchSize));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Process batches with limited parallelism
|
|
221
|
+
for (let i = 0; i < batches.length; i += parallelism) {
|
|
222
|
+
const chunk = batches.slice(i, i + parallelism);
|
|
223
|
+
const results = await Promise.all(chunk.map(batch => processor(batch)));
|
|
224
|
+
|
|
225
|
+
for (const result of results) {
|
|
226
|
+
yield* result;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Memory-efficient object pool for row objects
|
|
234
|
+
* Reduces GC pressure on large CSV files
|
|
235
|
+
*/
|
|
236
|
+
class ObjectPool {
|
|
237
|
+
constructor(factory, initialSize = 100) {
|
|
238
|
+
this.factory = factory;
|
|
239
|
+
this.pool = [];
|
|
240
|
+
this.inUse = 0;
|
|
241
|
+
|
|
242
|
+
// Pre-warm pool on Node 20+
|
|
243
|
+
if (features.isNode20Plus) {
|
|
244
|
+
for (let i = 0; i < initialSize; i++) {
|
|
245
|
+
this.pool.push(factory());
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
acquire() {
|
|
251
|
+
this.inUse++;
|
|
252
|
+
if (this.pool.length > 0) {
|
|
253
|
+
return this.pool.pop();
|
|
254
|
+
}
|
|
255
|
+
return this.factory();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
release(obj) {
|
|
259
|
+
this.inUse--;
|
|
260
|
+
// Clear object properties before returning to pool
|
|
261
|
+
for (const key in obj) {
|
|
262
|
+
if (hasOwn(obj, key)) {
|
|
263
|
+
delete obj[key];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
this.pool.push(obj);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
getStats() {
|
|
270
|
+
return {
|
|
271
|
+
poolSize: this.pool.length,
|
|
272
|
+
inUse: this.inUse
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Fast string escape for CSV values
|
|
279
|
+
* Uses pre-computed regex on all versions
|
|
280
|
+
*/
|
|
281
|
+
const QUOTE_REGEX = /"/g;
|
|
282
|
+
|
|
283
|
+
function fastEscapeValue(value, delimiterCode) {
|
|
284
|
+
if (value === null || value === undefined || value === '') {
|
|
285
|
+
return '';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const str = typeof value === 'string' ? value : String(value);
|
|
289
|
+
const len = str.length;
|
|
290
|
+
|
|
291
|
+
// Quick scan for special characters using char codes
|
|
292
|
+
let needsQuoting = false;
|
|
293
|
+
let hasQuote = false;
|
|
294
|
+
|
|
295
|
+
for (let i = 0; i < len; i++) {
|
|
296
|
+
const code = str.charCodeAt(i);
|
|
297
|
+
if (code === CHAR_CODES.QUOTE) {
|
|
298
|
+
hasQuote = true;
|
|
299
|
+
needsQuoting = true;
|
|
300
|
+
} else if (code === delimiterCode || code === CHAR_CODES.NEWLINE || code === CHAR_CODES.CARRIAGE) {
|
|
301
|
+
needsQuoting = true;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!needsQuoting) {
|
|
306
|
+
return str;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const escaped = hasQuote ? str.replace(QUOTE_REGEX, '""') : str;
|
|
310
|
+
return `"${escaped}"`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Async iterator utilities for streaming
|
|
315
|
+
*/
|
|
316
|
+
const asyncIterUtils = {
|
|
317
|
+
/**
|
|
318
|
+
* Map over async iterator with concurrency control (Node 20+)
|
|
319
|
+
*/
|
|
320
|
+
async *mapConcurrent(iterator, mapper, concurrency = 4) {
|
|
321
|
+
const pending = [];
|
|
322
|
+
|
|
323
|
+
for await (const item of iterator) {
|
|
324
|
+
pending.push(mapper(item));
|
|
325
|
+
|
|
326
|
+
if (pending.length >= concurrency) {
|
|
327
|
+
const results = await Promise.all(pending.splice(0, concurrency));
|
|
328
|
+
for (const result of results) {
|
|
329
|
+
yield result;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (pending.length > 0) {
|
|
335
|
+
const results = await Promise.all(pending);
|
|
336
|
+
for (const result of results) {
|
|
337
|
+
yield result;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Batch items from async iterator
|
|
344
|
+
*/
|
|
345
|
+
async *batch(iterator, size = 1000) {
|
|
346
|
+
let batch = [];
|
|
347
|
+
|
|
348
|
+
for await (const item of iterator) {
|
|
349
|
+
batch.push(item);
|
|
350
|
+
|
|
351
|
+
if (batch.length >= size) {
|
|
352
|
+
yield batch;
|
|
353
|
+
batch = [];
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (batch.length > 0) {
|
|
358
|
+
yield batch;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Get runtime optimization hints
|
|
365
|
+
*/
|
|
366
|
+
function getOptimizationHints() {
|
|
367
|
+
return {
|
|
368
|
+
nodeVersion: `${major}.${minor}`,
|
|
369
|
+
features,
|
|
370
|
+
recommendations: {
|
|
371
|
+
useWebStreams: features.hasWebStreams,
|
|
372
|
+
useStructuredClone: features.hasStructuredClone,
|
|
373
|
+
useLargerBatches: features.isNode20Plus,
|
|
374
|
+
useHigherParallelism: features.isNode22Plus,
|
|
375
|
+
preferredChunkSize: features.isNode24Plus ? 131072 : (features.isNode20Plus ? 65536 : 16384)
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
module.exports = {
|
|
381
|
+
// Feature detection
|
|
382
|
+
features,
|
|
383
|
+
nodeVersion: { major, minor },
|
|
384
|
+
|
|
385
|
+
// Polyfills and optimized functions
|
|
386
|
+
hasOwn,
|
|
387
|
+
deepClone,
|
|
388
|
+
arrayAt,
|
|
389
|
+
|
|
390
|
+
// Classes
|
|
391
|
+
StringBuilderOptimized,
|
|
392
|
+
RowBuffer,
|
|
393
|
+
ObjectPool,
|
|
394
|
+
|
|
395
|
+
// Constants
|
|
396
|
+
CHAR_CODES,
|
|
397
|
+
|
|
398
|
+
// Functions
|
|
399
|
+
fastDetectDelimiter,
|
|
400
|
+
fastEscapeValue,
|
|
401
|
+
createBatchProcessor,
|
|
402
|
+
|
|
403
|
+
// Async utilities
|
|
404
|
+
asyncIterUtils,
|
|
405
|
+
|
|
406
|
+
// Diagnostics
|
|
407
|
+
getOptimizationHints
|
|
408
|
+
};
|
|
@@ -13,7 +13,7 @@ function createTextDecoder() {
|
|
|
13
13
|
try {
|
|
14
14
|
const { TextDecoder: UtilTextDecoder } = require('util');
|
|
15
15
|
return new UtilTextDecoder('utf-8');
|
|
16
|
-
} catch (
|
|
16
|
+
} catch (_error) {
|
|
17
17
|
return null;
|
|
18
18
|
}
|
|
19
19
|
}
|
|
@@ -24,7 +24,7 @@ function getTransformStream() {
|
|
|
24
24
|
}
|
|
25
25
|
try {
|
|
26
26
|
return require('stream/web').TransformStream;
|
|
27
|
-
} catch (
|
|
27
|
+
} catch (_error) {
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -411,10 +411,9 @@ class NdjsonParser {
|
|
|
411
411
|
let buffer = '';
|
|
412
412
|
|
|
413
413
|
try {
|
|
414
|
-
// eslint-disable-next-line no-constant-condition
|
|
415
414
|
while (true) {
|
|
416
415
|
const { done, value } = await reader.read();
|
|
417
|
-
|
|
416
|
+
|
|
418
417
|
if (done) {
|
|
419
418
|
// Обрабатываем оставшийся буфер
|
|
420
419
|
/* istanbul ignore next */
|
|
@@ -423,7 +422,7 @@ class NdjsonParser {
|
|
|
423
422
|
try {
|
|
424
423
|
JSON.parse(buffer.trim());
|
|
425
424
|
stats.validLines++;
|
|
426
|
-
} catch (
|
|
425
|
+
} catch (_error) {
|
|
427
426
|
stats.errorLines++;
|
|
428
427
|
}
|
|
429
428
|
}
|
|
@@ -465,4 +464,4 @@ class NdjsonParser {
|
|
|
465
464
|
}
|
|
466
465
|
}
|
|
467
466
|
|
|
468
|
-
module.exports = NdjsonParser;
|
|
467
|
+
module.exports = NdjsonParser;
|