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,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
|
+
}
|