@lowdep/csv2json 1.0.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rushabh Shah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # csv2json
2
+
3
+ ![Zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen) ![Node](https://img.shields.io/badge/node-%3E%3D14-blue) ![License: MIT](https://img.shields.io/badge/license-MIT-green) ![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey)
4
+
5
+
6
+ Convert between CSV and JSON in **both directions**, with automatic type inference and a proper RFC 4180 parser (quoted fields, escaped quotes, embedded newlines). Zero dependencies.
7
+
8
+ The companion to [`csv-peek`](https://github.com/Rushabh5000/csv-peek) (which previews CSVs). Unlike `csvkit` (Python) or paste-it-here websites, this runs locally and never uploads your data.
9
+
10
+ ---
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install -g csv2json
16
+ ```
17
+
18
+ Or without installing:
19
+
20
+ ```bash
21
+ npx csv2json data.csv
22
+ ```
23
+
24
+ ---
25
+
26
+ ## Usage
27
+
28
+ ```bash
29
+ csv2json users.csv # CSV → JSON on stdout
30
+ csv2json users.csv -o users.json # CSV → JSON file
31
+ csv2json users.json -o users.csv # JSON → CSV
32
+ csv2json data.tsv -d tab # Tab-separated input
33
+ cat export.csv | csv2json - --compact # From stdin, single-line JSON
34
+ ```
35
+
36
+ Direction is inferred from the file extension (or the `-o` target), and falls back to sniffing the content. Force it with `--from csv|json`.
37
+
38
+ ---
39
+
40
+ ## Example
41
+
42
+ `users.csv`:
43
+ ```csv
44
+ id,name,active,score
45
+ 1,Alice,true,9.5
46
+ 2,Bob,false,7
47
+ ```
48
+
49
+ `csv2json users.csv`:
50
+ ```json
51
+ [
52
+ { "id": 1, "name": "Alice", "active": true, "score": 9.5 },
53
+ { "id": 2, "name": "Bob", "active": false, "score": 7 }
54
+ ]
55
+ ```
56
+
57
+ Note that `1`→number, `true`→boolean, `9.5`→float automatically. Disable with `--no-type`.
58
+
59
+ Reverse it — `csv2json users.json -o users.csv` — and you get the CSV back.
60
+
61
+ ---
62
+
63
+ ## Type Inference
64
+
65
+ | CSV value | JSON value |
66
+ |---|---|
67
+ | `42` | `42` (number) |
68
+ | `3.14` | `3.14` (number) |
69
+ | `true` / `false` | boolean |
70
+ | `null` | `null` |
71
+ | empty cell | `""` (or `null` with `--empty-as-null`) |
72
+ | anything else | string |
73
+
74
+ Turn it off with `--no-type` to keep every value a string.
75
+
76
+ ---
77
+
78
+ ## Options
79
+
80
+ | Flag | Description |
81
+ |---|---|
82
+ | `--from <csv\|json>` | Force input format (for stdin / odd extensions) |
83
+ | `-o, --out <file>` | Write to a file (format from its extension) |
84
+ | `-d, --delimiter <d>` | `,` `;` `\|` or `tab` (default: auto-detect) |
85
+ | `--no-header` | CSV has no header → arrays of values |
86
+ | `--no-type` | Keep all values as strings |
87
+ | `--empty-as-null` | Empty CSV cells become `null` |
88
+ | `--skip-empty` | Omit empty cells from JSON objects |
89
+ | `--pretty` / `--compact` | JSON formatting (pretty is default) |
90
+
91
+ ---
92
+
93
+ ## Nested Values
94
+
95
+ When converting **JSON → CSV**, nested objects/arrays in a cell are serialized as JSON strings (and properly quoted). The header row is the union of all keys across every record, in first-seen order.
96
+
97
+ ---
98
+
99
+ ## License
100
+
101
+ MIT
102
+
103
+ ---
104
+
105
+ ## Keywords
106
+
107
+ `csv to json` · `json to csv` · `convert csv` · `csv json converter` · `csvkit alternative` · `rfc 4180` · `parse csv` · `tsv to json` · `zero dependencies` · `cli`
108
+
109
+ ---
110
+
111
+ <div align="center">
112
+
113
+ **Built to solve, shared to help — Rushabh Shah 🛠️✨**
114
+
115
+ <sub>One of 40+ zero-dependency developer CLI tools — no <code>node_modules</code>, ever.</sub>
116
+
117
+ </div>
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const VERSION = '1.0.0';
8
+
9
+ // ─── ANSI ─────────────────────────────────────────────────────────────────────
10
+ const isTTY = process.stderr.isTTY;
11
+ const c = (code, t) => isTTY ? `\x1b[${code}m${t}\x1b[0m` : t;
12
+ const bold = t => c('1', t);
13
+ const dim = t => c('2', t);
14
+ const red = t => c('31', t);
15
+ const green = t => c('32', t);
16
+ const yellow = t => c('33', t);
17
+ const cyan = t => c('36', t);
18
+
19
+ // ─── CSV parser (RFC 4180, quoted fields, escaped quotes, CRLF) ───────────────
20
+ function parseCSV(content, delimiter) {
21
+ const rows = [];
22
+ let row = [], field = '', inQuotes = false;
23
+
24
+ for (let i = 0; i < content.length; i++) {
25
+ const ch = content[i], next = content[i + 1];
26
+ if (inQuotes) {
27
+ if (ch === '"' && next === '"') { field += '"'; i++; }
28
+ else if (ch === '"') { inQuotes = false; }
29
+ else { field += ch; }
30
+ } else {
31
+ if (ch === '"') { inQuotes = true; }
32
+ else if (ch === delimiter) { row.push(field); field = ''; }
33
+ else if (ch === '\n' || (ch === '\r' && next === '\n')) {
34
+ if (ch === '\r') i++;
35
+ row.push(field); field = '';
36
+ rows.push(row); row = [];
37
+ } else field += ch;
38
+ }
39
+ }
40
+ if (field !== '' || row.length) { row.push(field); rows.push(row); }
41
+ // Drop a trailing empty row produced by a final newline
42
+ if (rows.length && rows[rows.length - 1].length === 1 && rows[rows.length - 1][0] === '') rows.pop();
43
+ return rows;
44
+ }
45
+
46
+ function detectDelimiter(firstLine) {
47
+ const counts = {
48
+ ',': (firstLine.match(/,/g) || []).length,
49
+ '\t': (firstLine.match(/\t/g) || []).length,
50
+ ';': (firstLine.match(/;/g) || []).length,
51
+ '|': (firstLine.match(/\|/g) || []).length,
52
+ };
53
+ return Object.entries(counts).sort(([, a], [, b]) => b - a)[0][0] || ',';
54
+ }
55
+
56
+ // ─── Value coercion ───────────────────────────────────────────────────────────
57
+ function coerce(val, opts) {
58
+ if (opts.noType) return val;
59
+ if (val === '') return opts.emptyAsNull ? null : '';
60
+ const t = val.trim();
61
+ if (t === '') return val;
62
+ if (/^-?\d+$/.test(t)) return parseInt(t, 10);
63
+ if (/^-?\d*\.\d+([eE][+-]?\d+)?$/.test(t)) return parseFloat(t);
64
+ if (/^-?\d+\.\d+$/.test(t)) return parseFloat(t);
65
+ const low = t.toLowerCase();
66
+ if (low === 'true') return true;
67
+ if (low === 'false') return false;
68
+ if (low === 'null') return null;
69
+ return val;
70
+ }
71
+
72
+ // ─── CSV → JSON ───────────────────────────────────────────────────────────────
73
+ function csvToJson(content, opts) {
74
+ const delimiter = opts.delimiter || detectDelimiter(content.split('\n')[0]);
75
+ const rows = parseCSV(content, delimiter);
76
+ if (!rows.length) return [];
77
+
78
+ if (opts.noHeader) {
79
+ return rows.map(r => r.map(v => coerce(v, opts)));
80
+ }
81
+
82
+ const headers = rows[0].map(h => h.trim());
83
+ const out = [];
84
+ for (let i = 1; i < rows.length; i++) {
85
+ const r = rows[i];
86
+ const obj = {};
87
+ headers.forEach((h, j) => {
88
+ if (opts.skipEmpty && (r[j] === undefined || r[j] === '')) return;
89
+ obj[h] = coerce(r[j] !== undefined ? r[j] : '', opts);
90
+ });
91
+ out.push(obj);
92
+ }
93
+ return out;
94
+ }
95
+
96
+ // ─── JSON → CSV ───────────────────────────────────────────────────────────────
97
+ function jsonToCsv(data, opts) {
98
+ const delimiter = opts.delimiter || ',';
99
+ let rows;
100
+
101
+ if (Array.isArray(data)) rows = data;
102
+ else if (data && typeof data === 'object') rows = [data];
103
+ else throw new Error('JSON must be an array of objects (or a single object)');
104
+
105
+ if (!rows.length) return '';
106
+
107
+ // Collect the union of all keys, preserving first-seen order
108
+ const allArrays = rows.every(r => Array.isArray(r));
109
+ if (allArrays) {
110
+ return rows.map(r => r.map(v => csvCell(v, delimiter)).join(delimiter)).join('\n') + '\n';
111
+ }
112
+
113
+ const headers = [];
114
+ const seen = new Set();
115
+ for (const r of rows) {
116
+ if (r && typeof r === 'object' && !Array.isArray(r)) {
117
+ for (const k of Object.keys(r)) if (!seen.has(k)) { seen.add(k); headers.push(k); }
118
+ }
119
+ }
120
+
121
+ const lines = [];
122
+ if (!opts.noHeader) lines.push(headers.map(h => csvCell(h, delimiter)).join(delimiter));
123
+ for (const r of rows) {
124
+ lines.push(headers.map(h => csvCell(r ? r[h] : undefined, delimiter)).join(delimiter));
125
+ }
126
+ return lines.join('\n') + '\n';
127
+ }
128
+
129
+ function csvCell(v, delimiter) {
130
+ if (v === undefined || v === null) return '';
131
+ let s;
132
+ if (typeof v === 'object') s = JSON.stringify(v);
133
+ else s = String(v);
134
+ // Quote if it contains delimiter, quote, or newline
135
+ if (s.includes(delimiter) || s.includes('"') || s.includes('\n') || s.includes('\r')) {
136
+ s = '"' + s.replace(/"/g, '""') + '"';
137
+ }
138
+ return s;
139
+ }
140
+
141
+ // ─── CLI ──────────────────────────────────────────────────────────────────────
142
+ const args = process.argv.slice(2);
143
+ const VALUE_FLAGS = new Set(['-d', '--delimiter', '-o', '--out']);
144
+
145
+ function getFlag(...names) {
146
+ for (const f of names) { const i = args.indexOf(f); if (i !== -1) return args[i + 1]; }
147
+ return null;
148
+ }
149
+ function hasFlag(...names) { return names.some(f => args.includes(f)); }
150
+
151
+ const positional = [];
152
+ for (let i = 0; i < args.length; i++) {
153
+ if (args[i].startsWith('-') && args[i] !== '-') { if (VALUE_FLAGS.has(args[i])) i++; }
154
+ else positional.push(args[i]);
155
+ }
156
+
157
+ if (hasFlag('--version', '-v')) {
158
+ console.log(`csv2json v${VERSION}`); process.exit(0);
159
+ }
160
+
161
+ if (hasFlag('--help', '-h') || !positional.length) {
162
+ console.log(`
163
+ ${bold('csv2json')} — Convert between CSV and JSON, both directions, zero deps
164
+
165
+ ${bold('USAGE')}
166
+ csv2json <file.csv> Convert CSV → JSON (auto-detected by extension)
167
+ csv2json <file.json> Convert JSON → CSV
168
+ cat data.csv | csv2json - Read from stdin (assumes CSV unless --from)
169
+
170
+ ${bold('OPTIONS')}
171
+ --from <csv|json> Force input format (needed for stdin / odd extensions)
172
+ -o, --out <file> Write to a file (format inferred from its extension)
173
+ -d, --delimiter <d> CSV delimiter: , ; | or "tab" (default: auto-detect)
174
+ --no-header CSV has no header row → arrays of values
175
+ --no-type Don't coerce numbers/booleans/null (keep strings)
176
+ --empty-as-null Convert empty CSV cells to null instead of ""
177
+ --skip-empty Omit empty cells from JSON objects entirely
178
+ --pretty Pretty-print JSON output (default for TTY)
179
+ --compact Compact single-line JSON
180
+ --version Show version
181
+
182
+ ${bold('EXAMPLES')}
183
+ csv2json users.csv # → JSON on stdout
184
+ csv2json users.csv -o users.json # → JSON file
185
+ csv2json users.json -o users.csv # JSON → CSV
186
+ csv2json data.tsv -d tab # Tab-separated input
187
+ cat export.csv | csv2json - --compact # From stdin, one-line JSON
188
+ `);
189
+ process.exit(positional.length ? 0 : 1);
190
+ }
191
+
192
+ let delimiter = getFlag('-d', '--delimiter');
193
+ if (delimiter === 'tab') delimiter = '\t';
194
+ const outFile = getFlag('-o', '--out');
195
+ const forceFrom = getFlag('--from');
196
+ const noHeader = hasFlag('--no-header');
197
+ const noType = hasFlag('--no-type');
198
+ const emptyAsNull = hasFlag('--empty-as-null');
199
+ const skipEmpty = hasFlag('--skip-empty');
200
+ const compact = hasFlag('--compact');
201
+ const pretty = hasFlag('--pretty') || (!compact && (isTTY || outFile));
202
+
203
+ // Read input
204
+ const inPath = positional[0];
205
+ let content, sourceName;
206
+ if (inPath === '-') {
207
+ content = fs.readFileSync(0, 'utf8'); // stdin
208
+ sourceName = '(stdin)';
209
+ } else {
210
+ const full = path.resolve(inPath);
211
+ if (!fs.existsSync(full)) { console.error(red(`File not found: ${full}`)); process.exit(1); }
212
+ content = fs.readFileSync(full, 'utf8');
213
+ sourceName = path.basename(inPath);
214
+ }
215
+
216
+ // Determine direction
217
+ let direction = forceFrom
218
+ ? (forceFrom === 'json' ? 'json2csv' : 'csv2json')
219
+ : null;
220
+ if (!direction) {
221
+ if (outFile && /\.csv$/i.test(outFile)) direction = 'json2csv';
222
+ else if (outFile && /\.json$/i.test(outFile)) direction = 'csv2json';
223
+ else if (/\.json$/i.test(inPath || '')) direction = 'json2csv';
224
+ else if (/\.(csv|tsv|txt)$/i.test(inPath || '')) direction = 'csv2json';
225
+ else {
226
+ // Sniff: starts with { or [ → JSON
227
+ direction = /^\s*[[{]/.test(content) ? 'json2csv' : 'csv2json';
228
+ }
229
+ }
230
+
231
+ const opts = { delimiter, noHeader, noType, emptyAsNull, skipEmpty };
232
+
233
+ let output;
234
+ try {
235
+ if (direction === 'csv2json') {
236
+ const json = csvToJson(content, opts);
237
+ output = JSON.stringify(json, null, pretty ? 2 : 0);
238
+ } else {
239
+ const data = JSON.parse(content);
240
+ output = jsonToCsv(data, opts);
241
+ }
242
+ } catch (e) {
243
+ console.error(red(`\nConversion failed: ${e.message}\n`)); process.exit(1);
244
+ }
245
+
246
+ if (outFile) {
247
+ fs.writeFileSync(path.resolve(outFile), output.endsWith('\n') ? output : output + '\n');
248
+ const arrow = direction === 'csv2json' ? 'CSV → JSON' : 'JSON → CSV';
249
+ process.stderr.write(`${green('✓')} ${arrow} ${dim(sourceName)} → ${cyan(outFile)}\n`);
250
+ } else {
251
+ process.stdout.write(output.endsWith('\n') ? output : output + '\n');
252
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@lowdep/csv2json",
3
+ "version": "1.0.0",
4
+ "description": "Convert between CSV and JSON in both directions with type inference — RFC 4180 parser, zero dependencies",
5
+ "bin": {
6
+ "csv2json": "bin/csv2json.js"
7
+ },
8
+ "keywords": [
9
+ "csv",
10
+ "json",
11
+ "convert",
12
+ "csv2json",
13
+ "json2csv",
14
+ "tsv",
15
+ "cli",
16
+ "data",
17
+ "zero-dependencies",
18
+ "csv to json",
19
+ "json to csv",
20
+ "convert csv",
21
+ "csv json converter",
22
+ "csvkit alternative",
23
+ "rfc 4180",
24
+ "parse csv",
25
+ "tsv to json",
26
+ "zero dependencies"
27
+ ],
28
+ "author": "Rushabh Shah",
29
+ "license": "MIT",
30
+ "engines": {
31
+ "node": ">=14"
32
+ },
33
+ "files": [
34
+ "bin/"
35
+ ],
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/Rushabh5000/csv2json.git"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/Rushabh5000/csv2json/issues"
42
+ },
43
+ "homepage": "https://github.com/Rushabh5000/csv2json#readme",
44
+ "publishConfig": {
45
+ "access": "public"
46
+ }
47
+ }