csv-to-ndjson 1.0.0 → 2.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.
Files changed (3) hide show
  1. package/README.md +23 -6
  2. package/index.js +117 -28
  3. package/package.json +11 -13
package/README.md CHANGED
@@ -1,10 +1,12 @@
1
- # csv-to-ndjson [![Build Status](https://travis-ci.org/SimonJang/csv-to-ndjson.svg?branch=master)](https://travis-ci.org/SimonJang/csv-to-ndjson)
1
+ # csv-to-ndjson [![CI](https://github.com/SimonJang/csv-to-ndjson/actions/workflows/ci.yml/badge.svg?branch=master&event=push)](https://github.com/SimonJang/csv-to-ndjson/actions/workflows/ci.yml?query=branch%3Amaster+event%3Apush)
2
2
 
3
3
  > Convert a CSV file to [ndjson](http://ndjson.org/) format stream or file.
4
4
 
5
5
 
6
6
  ## Install
7
7
 
8
+ Requires Node.js 26 or later.
9
+
8
10
  ```
9
11
  $ npm install csv-to-ndjson
10
12
  ```
@@ -15,20 +17,35 @@ $ npm install csv-to-ndjson
15
17
  ```js
16
18
  const csvToNdjson = require('csv-to-ndjson');
17
19
 
18
- csvToNdjson('financialdata.csv', {
19
- delimiter: ';'
20
+ const ndjsonStream = csvToNdjson('financialdata.csv', {
21
+ delimiter: ';',
20
22
  header: ['Q1', 'Q2', 'Q3', 'Q4']
21
- }).pipe(// Some pipe operation);
23
+ });
24
+ ndjsonStream.pipe(process.stdout);
22
25
  // => Returns a readable stream of ndjson
23
26
 
24
27
  csvToNdjson('financialdata.csv', {
25
28
  delimiter: ';',
26
29
  destination: 'financialdata.json',
27
30
  header: ['Q1', 'Q2', 'Q3', 'Q4']
28
- }).then(succes => // do something on success)
31
+ }).then(() => {
32
+ // The destination file has been written.
33
+ });
29
34
  // => Returns a promise when the file is written
30
35
  ```
31
36
 
37
+ Both the returned stream and destination file contain one JSON object per line.
38
+ Each record ends with LF (`\n`) on every platform, including Windows.
39
+
40
+ ## Migrating to 2.0.0
41
+
42
+ - Node.js 26 or later is required. Earlier Node.js versions are no longer supported.
43
+ - On Windows, output now uses LF (`\n`) instead of CRLF (`\r\n`). Update consumers
44
+ that compare exact bytes or split records using the platform's line ending.
45
+ - CSV parsing errors now follow csv-parse 7. If you inspect error codes or messages,
46
+ update those checks; inconsistent field counts use
47
+ `CSV_RECORD_INCONSISTENT_FIELDS_LENGTH`.
48
+
32
49
  ## API
33
50
 
34
51
  ### csvToNdjson(input, [options])
@@ -37,7 +54,7 @@ csvToNdjson('financialdata.csv', {
37
54
 
38
55
  Type: `string`
39
56
 
40
- Path of the CSV file to be read. The file has to end with the `.csv` file extenstion.
57
+ Path of the CSV file to be read. The file has to end with the `.csv` file extension.
41
58
 
42
59
  #### options
43
60
 
package/index.js CHANGED
@@ -1,13 +1,36 @@
1
1
  'use strict';
2
- const fs = require('fs');
3
- const eol = require('os').EOL;
4
- const csv = require('csv');
2
+ const fs = require('node:fs');
3
+ const Transform = require('node:stream').Transform;
5
4
  const isValidPath = require('is-valid-path');
6
- const pify = require('pify');
5
+ const {parse} = require('csv-parse');
7
6
 
8
- const validateHeader = header => header.filter(x => typeof x !== 'string').length === 0;
7
+ const validateHeader = header => {
8
+ for (let index = 0; index < header.length; index++) {
9
+ if (typeof header[index] !== 'string') {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ return true;
15
+ };
9
16
  const delimiters = [',', ';'];
10
- const pattern = /(.*?)\.(csv)/i;
17
+ const pattern = /\.csv$/i;
18
+
19
+ const teardown = streams => {
20
+ for (const stream of streams) {
21
+ if (typeof stream.unpipe === 'function') {
22
+ stream.unpipe();
23
+ }
24
+ }
25
+
26
+ for (const stream of streams) {
27
+ if (typeof stream.destroy === 'function') {
28
+ stream.destroy();
29
+ } else if (typeof stream.end === 'function') {
30
+ stream.end();
31
+ }
32
+ }
33
+ };
11
34
 
12
35
  module.exports = (path, options) => {
13
36
  let getHeader = false;
@@ -46,32 +69,98 @@ module.exports = (path, options) => {
46
69
  }
47
70
  }
48
71
 
49
- try {
50
- const stream = fs.createReadStream(path)
51
- .pipe(csv.parse({delimiter: options && options.delimiter ? options.delimiter : ','}))
52
- .pipe(csv.transform(record => {
53
- const result = {};
72
+ const source = fs.createReadStream(path);
73
+ const parser = parse({
74
+ bom: true,
75
+ delimiter: options && options.delimiter ? options.delimiter : ',',
76
+ relax_column_count_less: true,
77
+ on_record(record, {error}) {
78
+ // Preserve the legacy parser's exception for a single empty field.
79
+ if (error && !(record.length === 1 && record[0] === '')) {
80
+ throw error;
81
+ }
82
+ return record;
83
+ }
84
+ });
85
+ const stream = new Transform({objectMode: true});
86
+ stream._transform = (record, encoding, callback) => {
87
+ try {
88
+ const result = Object.create(null);
54
89
 
55
- if (!getHeader) {
56
- getHeader = true;
57
- csvHeader = record.map(item => item.toLowerCase());
58
- return;
59
- }
90
+ if (!getHeader) {
91
+ getHeader = true;
92
+ csvHeader = record.map(item => item.toLowerCase());
93
+ callback();
94
+ return;
95
+ }
96
+
97
+ for (const attribute of csvHeader) {
98
+ result[attribute] = record[csvHeader.indexOf(attribute)];
99
+ }
100
+ callback(null, JSON.stringify(result) + '\n');
101
+ } catch (error) {
102
+ callback(error);
103
+ }
104
+ };
60
105
 
61
- for (const attribute of csvHeader) {
62
- result[attribute] = record[csvHeader.indexOf(attribute)];
106
+ if (options && options.destination) {
107
+ const destination = fs.createWriteStream(options.destination);
108
+ const completion = new Promise((resolve, reject) => {
109
+ let settled = false;
110
+ const fail = error => {
111
+ if (!settled) {
112
+ settled = true;
113
+ teardown([source, parser, stream, destination]);
114
+ reject(error);
115
+ }
116
+ };
117
+ const finish = () => {
118
+ if (!settled) {
119
+ settled = true;
120
+ resolve();
63
121
  }
64
- return JSON.stringify(result) + eol;
65
- }));
122
+ };
66
123
 
67
- if (options && options.destination) {
68
- stream.pipe(fs.createWriteStream(options.destination));
69
- return pify(stream).on('end');
70
- }
124
+ source.on('error', fail);
125
+ parser.on('error', fail);
126
+ stream.on('error', fail);
127
+ destination.on('error', fail);
128
+ destination.on('finish', finish);
129
+ });
71
130
 
72
- return stream;
73
- } catch (err) {
74
- console.error(err);
75
- throw new Error('Tranforming CSV to ndjson failed');
131
+ source.pipe(parser).pipe(stream).pipe(destination);
132
+ return completion;
76
133
  }
134
+
135
+ let completed = false;
136
+ let failed = false;
137
+ const forwardFailure = error => {
138
+ if (!failed) {
139
+ failed = true;
140
+ teardown([source, parser, stream]);
141
+ stream.emit('error', error);
142
+ }
143
+ };
144
+ const closeOnTransformFailure = () => {
145
+ if (!failed) {
146
+ failed = true;
147
+ teardown([source, parser, stream]);
148
+ }
149
+ };
150
+ const closeUpstreamOnCancellation = () => {
151
+ if (!completed && !failed) {
152
+ failed = true;
153
+ teardown([source, parser]);
154
+ }
155
+ };
156
+
157
+ source.on('error', forwardFailure);
158
+ parser.on('error', forwardFailure);
159
+ stream.on('end', () => {
160
+ completed = true;
161
+ });
162
+ stream.on('error', closeOnTransformFailure);
163
+ stream.on('close', closeUpstreamOnCancellation);
164
+ source.pipe(parser).pipe(stream);
165
+ return stream;
77
166
  };
package/package.json CHANGED
@@ -1,19 +1,24 @@
1
1
  {
2
2
  "name": "csv-to-ndjson",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Convert a CSV file to ndjson format stream",
5
5
  "license": "MIT",
6
- "repository": "SimonJang/csv-to-ndjson",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/SimonJang/csv-to-ndjson.git"
9
+ },
10
+ "packageManager": "npm@12.0.1",
7
11
  "author": {
8
12
  "name": "Simon",
9
13
  "email": "simon.jang.contact@gmail.com",
10
14
  "url": "github.com/SimonJang"
11
15
  },
12
16
  "engines": {
13
- "node": ">=4"
17
+ "node": ">=26"
14
18
  },
15
19
  "scripts": {
16
- "test": "xo && ava"
20
+ "test": "node --test",
21
+ "prepublishOnly": "npm test"
17
22
  },
18
23
  "files": [
19
24
  "index.js"
@@ -26,14 +31,7 @@
26
31
  "file"
27
32
  ],
28
33
  "dependencies": {
29
- "csv": "^1.1.1",
30
- "is-valid-path": "^0.1.1",
31
- "pify": "^3.0.0"
32
- },
33
- "devDependencies": {
34
- "ava": "^0.20.0",
35
- "get-stream": "^3.0.0",
36
- "tempy": "^0.1.0",
37
- "xo": "^0.18.2"
34
+ "csv-parse": "^7.0.2",
35
+ "is-valid-path": "^0.1.1"
38
36
  }
39
37
  }