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,683 @@
1
+ /**
2
+ * JTCSV Web Server
3
+ *
4
+ * Simple web server for browser-based CSV/JSON conversion
5
+ * with REST API endpoints
6
+ */
7
+
8
+ const http = require('http');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const url = require('url');
12
+ const jtcsv = require('../../index.js');
13
+
14
+ const PORT = process.env.PORT || 3000;
15
+ const HOST = process.env.HOST || 'localhost';
16
+
17
+ /**
18
+ * Parse JSON body from request
19
+ */
20
+ function parseBody(req) {
21
+ return new Promise((resolve, reject) => {
22
+ let body = '';
23
+ req.on('data', chunk => {
24
+ body += chunk.toString();
25
+ });
26
+ req.on('end', () => {
27
+ try {
28
+ resolve(JSON.parse(body));
29
+ } catch (error) {
30
+ reject(new Error('Invalid JSON'));
31
+ }
32
+ });
33
+ req.on('error', reject);
34
+ });
35
+ }
36
+
37
+ /**
38
+ * Send JSON response
39
+ */
40
+ function sendJson(res, statusCode, data) {
41
+ res.statusCode = statusCode;
42
+ res.setHeader('Content-Type', 'application/json');
43
+ res.setHeader('Access-Control-Allow-Origin', '*');
44
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
45
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
46
+ res.end(JSON.stringify(data));
47
+ }
48
+
49
+ /**
50
+ * Send error response
51
+ */
52
+ function sendError(res, statusCode, message) {
53
+ sendJson(res, statusCode, {
54
+ success: false,
55
+ error: message
56
+ });
57
+ }
58
+
59
+ /**
60
+ * API Handler: Convert JSON to CSV
61
+ */
62
+ async function handleJsonToCsv(req, res) {
63
+ try {
64
+ const body = await parseBody(req);
65
+ const { data, options = {} } = body;
66
+
67
+ if (!Array.isArray(data)) {
68
+ return sendError(res, 400, 'Data must be an array of objects');
69
+ }
70
+
71
+ const csv = jtcsv.jsonToCsv(data, {
72
+ delimiter: options.delimiter || ',',
73
+ includeHeaders: options.includeHeaders !== false,
74
+ preventCsvInjection: options.preventCsvInjection !== false,
75
+ rfc4180Compliant: options.rfc4180Compliant !== false
76
+ });
77
+
78
+ sendJson(res, 200, {
79
+ success: true,
80
+ result: csv,
81
+ records: data.length,
82
+ bytes: csv.length
83
+ });
84
+ } catch (error) {
85
+ sendError(res, 500, error.message);
86
+ }
87
+ }
88
+
89
+ /**
90
+ * API Handler: Convert CSV to JSON
91
+ */
92
+ async function handleCsvToJson(req, res) {
93
+ try {
94
+ const body = await parseBody(req);
95
+ const { data, options = {} } = body;
96
+
97
+ if (typeof data !== 'string') {
98
+ return sendError(res, 400, 'Data must be a CSV string');
99
+ }
100
+
101
+ const json = jtcsv.csvToJson(data, {
102
+ delimiter: options.delimiter,
103
+ autoDetect: options.autoDetect !== false,
104
+ hasHeaders: options.hasHeaders !== false,
105
+ trim: options.trim !== false,
106
+ parseNumbers: options.parseNumbers === true,
107
+ parseBooleans: options.parseBooleans === true
108
+ });
109
+
110
+ sendJson(res, 200, {
111
+ success: true,
112
+ result: json,
113
+ rows: json.length
114
+ });
115
+ } catch (error) {
116
+ sendError(res, 500, error.message);
117
+ }
118
+ }
119
+
120
+ /**
121
+ * API Handler: Validate data
122
+ */
123
+ async function handleValidate(req, res) {
124
+ try {
125
+ const body = await parseBody(req);
126
+ const { data, format } = body;
127
+
128
+ let isValid = false;
129
+ let errors = [];
130
+
131
+ if (format === 'json') {
132
+ if (Array.isArray(data)) {
133
+ isValid = data.every(item => typeof item === 'object' && item !== null);
134
+ if (!isValid) {
135
+ errors.push('All items must be objects');
136
+ }
137
+ } else {
138
+ errors.push('Data must be an array');
139
+ }
140
+ } else if (format === 'csv') {
141
+ if (typeof data === 'string') {
142
+ try {
143
+ const parsed = jtcsv.csvToJson(data);
144
+ isValid = Array.isArray(parsed) && parsed.length > 0;
145
+ } catch (error) {
146
+ errors.push(error.message);
147
+ }
148
+ } else {
149
+ errors.push('Data must be a string');
150
+ }
151
+ } else {
152
+ errors.push('Format must be "json" or "csv"');
153
+ }
154
+
155
+ sendJson(res, 200, {
156
+ success: true,
157
+ valid: isValid,
158
+ errors: errors
159
+ });
160
+ } catch (error) {
161
+ sendError(res, 500, error.message);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * API Handler: Convert NDJSON to CSV
167
+ */
168
+ async function handleNdjsonToCsv(req, res) {
169
+ try {
170
+ const body = await parseBody(req);
171
+ const { data, options = {} } = body;
172
+
173
+ if (typeof data !== 'string') {
174
+ return sendError(res, 400, 'Data must be an NDJSON string');
175
+ }
176
+
177
+ const json = jtcsv.ndjsonToJson(data);
178
+ const csv = jtcsv.jsonToCsv(json, {
179
+ delimiter: options.delimiter || ',',
180
+ includeHeaders: options.includeHeaders !== false
181
+ });
182
+
183
+ sendJson(res, 200, {
184
+ success: true,
185
+ result: csv,
186
+ records: json.length
187
+ });
188
+ } catch (error) {
189
+ sendError(res, 500, error.message);
190
+ }
191
+ }
192
+
193
+ /**
194
+ * API Handler: Convert CSV to NDJSON
195
+ */
196
+ async function handleCsvToNdjson(req, res) {
197
+ try {
198
+ const body = await parseBody(req);
199
+ const { data, options = {} } = body;
200
+
201
+ if (typeof data !== 'string') {
202
+ return sendError(res, 400, 'Data must be a CSV string');
203
+ }
204
+
205
+ const json = jtcsv.csvToJson(data, {
206
+ delimiter: options.delimiter,
207
+ autoDetect: options.autoDetect !== false,
208
+ hasHeaders: options.hasHeaders !== false
209
+ });
210
+
211
+ const ndjson = jtcsv.jsonToNdjson(json);
212
+
213
+ sendJson(res, 200, {
214
+ success: true,
215
+ result: ndjson,
216
+ records: json.length
217
+ });
218
+ } catch (error) {
219
+ sendError(res, 500, error.message);
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Serve static HTML page
225
+ */
226
+ function serveHomePage(res) {
227
+ const html = `<!DOCTYPE html>
228
+ <html lang="en">
229
+ <head>
230
+ <meta charset="UTF-8">
231
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
232
+ <title>JTCSV Web Interface</title>
233
+ <style>
234
+ * { margin: 0; padding: 0; box-sizing: border-box; }
235
+ body {
236
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
237
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
238
+ min-height: 100vh;
239
+ padding: 20px;
240
+ }
241
+ .container {
242
+ max-width: 1200px;
243
+ margin: 0 auto;
244
+ background: white;
245
+ border-radius: 12px;
246
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
247
+ overflow: hidden;
248
+ }
249
+ .header {
250
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
251
+ color: white;
252
+ padding: 30px;
253
+ text-align: center;
254
+ }
255
+ .header h1 { font-size: 2.5em; margin-bottom: 10px; }
256
+ .header p { font-size: 1.1em; opacity: 0.9; }
257
+ .main { padding: 30px; }
258
+ .controls {
259
+ display: grid;
260
+ grid-template-columns: 1fr 1fr;
261
+ gap: 20px;
262
+ margin-bottom: 20px;
263
+ }
264
+ .control-group {
265
+ display: flex;
266
+ flex-direction: column;
267
+ gap: 8px;
268
+ }
269
+ label {
270
+ font-weight: 600;
271
+ color: #333;
272
+ font-size: 0.9em;
273
+ }
274
+ select, input, button {
275
+ padding: 12px;
276
+ border: 2px solid #e0e0e0;
277
+ border-radius: 6px;
278
+ font-size: 1em;
279
+ transition: border-color 0.3s;
280
+ }
281
+ select:focus, input:focus {
282
+ outline: none;
283
+ border-color: #667eea;
284
+ }
285
+ .textarea-group {
286
+ display: grid;
287
+ grid-template-columns: 1fr 1fr;
288
+ gap: 20px;
289
+ margin-bottom: 20px;
290
+ }
291
+ .textarea-wrapper { display: flex; flex-direction: column; }
292
+ .textarea-wrapper label { margin-bottom: 8px; }
293
+ textarea {
294
+ width: 100%;
295
+ height: 300px;
296
+ padding: 12px;
297
+ border: 2px solid #e0e0e0;
298
+ border-radius: 6px;
299
+ font-family: 'Courier New', monospace;
300
+ font-size: 0.9em;
301
+ resize: vertical;
302
+ transition: border-color 0.3s;
303
+ }
304
+ textarea:focus {
305
+ outline: none;
306
+ border-color: #667eea;
307
+ }
308
+ .button-group {
309
+ display: flex;
310
+ gap: 15px;
311
+ justify-content: center;
312
+ margin-bottom: 20px;
313
+ }
314
+ button {
315
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
316
+ color: white;
317
+ border: none;
318
+ padding: 14px 30px;
319
+ font-weight: 600;
320
+ cursor: pointer;
321
+ transition: transform 0.2s, box-shadow 0.2s;
322
+ }
323
+ button:hover {
324
+ transform: translateY(-2px);
325
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
326
+ }
327
+ button:disabled {
328
+ opacity: 0.6;
329
+ cursor: not-allowed;
330
+ transform: none;
331
+ }
332
+ .info-box {
333
+ background: #f5f5f5;
334
+ padding: 15px;
335
+ border-radius: 6px;
336
+ border-left: 4px solid #667eea;
337
+ margin-bottom: 20px;
338
+ font-size: 0.9em;
339
+ color: #666;
340
+ }
341
+ .error {
342
+ background: #fee;
343
+ border-left-color: #f44;
344
+ color: #c33;
345
+ }
346
+ .success {
347
+ background: #efe;
348
+ border-left-color: #4a4;
349
+ color: #363;
350
+ }
351
+ .stats {
352
+ display: grid;
353
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
354
+ gap: 15px;
355
+ margin-top: 20px;
356
+ }
357
+ .stat-card {
358
+ background: #f9f9f9;
359
+ padding: 15px;
360
+ border-radius: 6px;
361
+ text-align: center;
362
+ }
363
+ .stat-value {
364
+ font-size: 2em;
365
+ font-weight: bold;
366
+ color: #667eea;
367
+ }
368
+ .stat-label {
369
+ font-size: 0.85em;
370
+ color: #666;
371
+ margin-top: 5px;
372
+ }
373
+ </style>
374
+ </head>
375
+ <body>
376
+ <div class="container">
377
+ <div class="header">
378
+ <h1>šŸ”„ JTCSV Web Interface</h1>
379
+ <p>High-performance CSV ↔ JSON converter</p>
380
+ </div>
381
+
382
+ <div class="main">
383
+ <div class="controls">
384
+ <div class="control-group">
385
+ <label for="operation">Operation:</label>
386
+ <select id="operation">
387
+ <option value="json-to-csv">JSON → CSV</option>
388
+ <option value="csv-to-json">CSV → JSON</option>
389
+ <option value="ndjson-to-csv">NDJSON → CSV</option>
390
+ <option value="csv-to-ndjson">CSV → NDJSON</option>
391
+ </select>
392
+ </div>
393
+
394
+ <div class="control-group">
395
+ <label for="delimiter">CSV Delimiter:</label>
396
+ <select id="delimiter">
397
+ <option value=",">Comma (,)</option>
398
+ <option value=";">Semicolon (;)</option>
399
+ <option value="\t">Tab</option>
400
+ <option value="|">Pipe (|)</option>
401
+ </select>
402
+ </div>
403
+ </div>
404
+
405
+ <div class="control-group" style="margin-bottom: 20px;">
406
+ <label>
407
+ <input type="checkbox" id="parseNumbers" style="width: auto; margin-right: 8px;">
408
+ Parse Numbers (CSV → JSON)
409
+ </label>
410
+ <label>
411
+ <input type="checkbox" id="parseBooleans" style="width: auto; margin-right: 8px;">
412
+ Parse Booleans (CSV → JSON)
413
+ </label>
414
+ <label>
415
+ <input type="checkbox" id="includeHeaders" checked style="width: auto; margin-right: 8px;">
416
+ Include Headers (JSON → CSV)
417
+ </label>
418
+ </div>
419
+
420
+ <div class="textarea-group">
421
+ <div class="textarea-wrapper">
422
+ <label for="input">Input:</label>
423
+ <textarea id="input" placeholder="Paste your JSON or CSV data here..."></textarea>
424
+ </div>
425
+
426
+ <div class="textarea-wrapper">
427
+ <label for="output">Output:</label>
428
+ <textarea id="output" placeholder="Converted data will appear here..." readonly></textarea>
429
+ </div>
430
+ </div>
431
+
432
+ <div class="button-group">
433
+ <button id="convertBtn">Convert</button>
434
+ <button id="clearBtn" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">Clear</button>
435
+ <button id="copyBtn" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">Copy Output</button>
436
+ </div>
437
+
438
+ <div id="messageBox" style="display: none;"></div>
439
+
440
+ <div class="stats" id="stats" style="display: none;">
441
+ <div class="stat-card">
442
+ <div class="stat-value" id="recordsCount">0</div>
443
+ <div class="stat-label">Records</div>
444
+ </div>
445
+ <div class="stat-card">
446
+ <div class="stat-value" id="bytesCount">0</div>
447
+ <div class="stat-label">Bytes</div>
448
+ </div>
449
+ <div class="stat-card">
450
+ <div class="stat-value" id="timeCount">0</div>
451
+ <div class="stat-label">ms</div>
452
+ </div>
453
+ </div>
454
+ </div>
455
+ </div>
456
+
457
+ <script>
458
+ const API_URL = 'http://${HOST}:${PORT}/api';
459
+
460
+ const elements = {
461
+ operation: document.getElementById('operation'),
462
+ delimiter: document.getElementById('delimiter'),
463
+ parseNumbers: document.getElementById('parseNumbers'),
464
+ parseBooleans: document.getElementById('parseBooleans'),
465
+ includeHeaders: document.getElementById('includeHeaders'),
466
+ input: document.getElementById('input'),
467
+ output: document.getElementById('output'),
468
+ convertBtn: document.getElementById('convertBtn'),
469
+ clearBtn: document.getElementById('clearBtn'),
470
+ copyBtn: document.getElementById('copyBtn'),
471
+ messageBox: document.getElementById('messageBox'),
472
+ stats: document.getElementById('stats'),
473
+ recordsCount: document.getElementById('recordsCount'),
474
+ bytesCount: document.getElementById('bytesCount'),
475
+ timeCount: document.getElementById('timeCount')
476
+ };
477
+
478
+ function showMessage(message, type = 'info') {
479
+ elements.messageBox.textContent = message;
480
+ elements.messageBox.className = 'info-box ' + type;
481
+ elements.messageBox.style.display = 'block';
482
+ setTimeout(() => {
483
+ elements.messageBox.style.display = 'none';
484
+ }, 5000);
485
+ }
486
+
487
+ function updateStats(records, bytes, time) {
488
+ elements.recordsCount.textContent = records || 0;
489
+ elements.bytesCount.textContent = bytes || 0;
490
+ elements.timeCount.textContent = time || 0;
491
+ elements.stats.style.display = 'grid';
492
+ }
493
+
494
+ async function convert() {
495
+ const input = elements.input.value.trim();
496
+ if (!input) {
497
+ showMessage('Please enter input data', 'error');
498
+ return;
499
+ }
500
+
501
+ elements.convertBtn.disabled = true;
502
+ elements.convertBtn.textContent = 'Converting...';
503
+
504
+ try {
505
+ const operation = elements.operation.value;
506
+ const options = {
507
+ delimiter: elements.delimiter.value,
508
+ parseNumbers: elements.parseNumbers.checked,
509
+ parseBooleans: elements.parseBooleans.checked,
510
+ includeHeaders: elements.includeHeaders.checked
511
+ };
512
+
513
+ let endpoint = '';
514
+ let requestData = {};
515
+
516
+ if (operation === 'json-to-csv') {
517
+ endpoint = '/json-to-csv';
518
+ requestData = { data: JSON.parse(input), options };
519
+ } else if (operation === 'csv-to-json') {
520
+ endpoint = '/csv-to-json';
521
+ requestData = { data: input, options };
522
+ } else if (operation === 'ndjson-to-csv') {
523
+ endpoint = '/ndjson-to-csv';
524
+ requestData = { data: input, options };
525
+ } else if (operation === 'csv-to-ndjson') {
526
+ endpoint = '/csv-to-ndjson';
527
+ requestData = { data: input, options };
528
+ }
529
+
530
+ const startTime = Date.now();
531
+ const response = await fetch(API_URL + endpoint, {
532
+ method: 'POST',
533
+ headers: { 'Content-Type': 'application/json' },
534
+ body: JSON.stringify(requestData)
535
+ });
536
+
537
+ const result = await response.json();
538
+ const elapsed = Date.now() - startTime;
539
+
540
+ if (result.success) {
541
+ if (operation.includes('json')) {
542
+ elements.output.value = JSON.stringify(result.result, null, 2);
543
+ } else {
544
+ elements.output.value = result.result;
545
+ }
546
+ showMessage('Conversion successful!', 'success');
547
+ updateStats(result.records || result.rows || 0, result.bytes || 0, elapsed);
548
+ } else {
549
+ showMessage('Error: ' + result.error, 'error');
550
+ }
551
+ } catch (error) {
552
+ showMessage('Error: ' + error.message, 'error');
553
+ } finally {
554
+ elements.convertBtn.disabled = false;
555
+ elements.convertBtn.textContent = 'Convert';
556
+ }
557
+ }
558
+
559
+ function clear() {
560
+ elements.input.value = '';
561
+ elements.output.value = '';
562
+ elements.stats.style.display = 'none';
563
+ elements.messageBox.style.display = 'none';
564
+ }
565
+
566
+ function copyToClipboard() {
567
+ const output = elements.output.value;
568
+ if (!output) {
569
+ showMessage('Nothing to copy', 'error');
570
+ return;
571
+ }
572
+
573
+ navigator.clipboard.writeText(output).then(() => {
574
+ showMessage('Copied to clipboard!', 'success');
575
+ }).catch(() => {
576
+ showMessage('Failed to copy', 'error');
577
+ });
578
+ }
579
+
580
+ elements.convertBtn.addEventListener('click', convert);
581
+ elements.clearBtn.addEventListener('click', clear);
582
+ elements.copyBtn.addEventListener('click', copyToClipboard);
583
+
584
+ // Load example data
585
+ elements.input.value = JSON.stringify([
586
+ { name: "Alice", age: 30, city: "New York" },
587
+ { name: "Bob", age: 25, city: "London" },
588
+ { name: "Charlie", age: 35, city: "Paris" }
589
+ ], null, 2);
590
+ </script>
591
+ </body>
592
+ </html>`;
593
+
594
+ res.statusCode = 200;
595
+ res.setHeader('Content-Type', 'text/html');
596
+ res.end(html);
597
+ }
598
+
599
+ /**
600
+ * Request handler
601
+ */
602
+ function handleRequest(req, res) {
603
+ const parsedUrl = url.parse(req.url, true);
604
+ const pathname = parsedUrl.pathname;
605
+
606
+ // Handle CORS preflight
607
+ if (req.method === 'OPTIONS') {
608
+ res.statusCode = 204;
609
+ res.setHeader('Access-Control-Allow-Origin', '*');
610
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
611
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
612
+ res.end();
613
+ return;
614
+ }
615
+
616
+ // Home page
617
+ if (pathname === '/' && req.method === 'GET') {
618
+ return serveHomePage(res);
619
+ }
620
+
621
+ // API endpoints
622
+ if (req.method === 'POST') {
623
+ if (pathname === '/api/json-to-csv') {
624
+ return handleJsonToCsv(req, res);
625
+ }
626
+ if (pathname === '/api/csv-to-json') {
627
+ return handleCsvToJson(req, res);
628
+ }
629
+ if (pathname === '/api/validate') {
630
+ return handleValidate(req, res);
631
+ }
632
+ if (pathname === '/api/ndjson-to-csv') {
633
+ return handleNdjsonToCsv(req, res);
634
+ }
635
+ if (pathname === '/api/csv-to-ndjson') {
636
+ return handleCsvToNdjson(req, res);
637
+ }
638
+ }
639
+
640
+ // 404
641
+ sendError(res, 404, 'Not found');
642
+ }
643
+
644
+ /**
645
+ * Start server
646
+ */
647
+ function startServer(options = {}) {
648
+ const port = options.port || PORT;
649
+ const host = options.host || HOST;
650
+
651
+ const server = http.createServer(handleRequest);
652
+
653
+ server.listen(port, host, () => {
654
+ console.log(`\n🌐 JTCSV Web Server started!`);
655
+ console.log(`\nšŸ“ URL: http://${host}:${port}`);
656
+ console.log(`\nšŸ“” API Endpoints:`);
657
+ console.log(` POST /api/json-to-csv`);
658
+ console.log(` POST /api/csv-to-json`);
659
+ console.log(` POST /api/ndjson-to-csv`);
660
+ console.log(` POST /api/csv-to-ndjson`);
661
+ console.log(` POST /api/validate`);
662
+ console.log(`\n✨ Press Ctrl+C to stop\n`);
663
+ });
664
+
665
+ server.on('error', (error) => {
666
+ if (error.code === 'EADDRINUSE') {
667
+ console.error(`\nāŒ Error: Port ${port} is already in use`);
668
+ console.error(` Try a different port: jtcsv web --port=3001\n`);
669
+ } else {
670
+ console.error(`\nāŒ Server error: ${error.message}\n`);
671
+ }
672
+ process.exit(1);
673
+ });
674
+
675
+ return server;
676
+ }
677
+
678
+ module.exports = { startServer };
679
+
680
+ // Run as standalone
681
+ if (require.main === module) {
682
+ startServer();
683
+ }