11import csvParse from 'csv-parse' ;
2- import { stringify } from 'csv-stringify/sync' ;
2+ import { stringify , Options } from 'csv-stringify/sync' ;
33import * as fs from 'fs-extra' ;
44
55const csvOptions : csvParse . Options = {
@@ -83,12 +83,35 @@ export async function readHeaders(file: string) {
8383}
8484
8585/** Write a CSV file */
86- export async function writeCsv ( file : string , rows : string [ ] [ ] ) {
86+ export async function writeCsv ( file : string , rows : string [ ] [ ] , options ?: Options ) {
8787 // TODO(danvk): make this less memory-intensive
88- const output = stringify ( rows ) ;
88+ const output = stringify ( rows , options ) ;
8989 await fs . writeFile ( file , output , { encoding : 'utf8' } ) ;
9090}
9191
92+ const LF = '\n' . charCodeAt ( 0 ) ;
93+ const CR = '\r' . charCodeAt ( 0 ) ;
94+
95+ /** Determine the type of line endings a file uses by looking for the first one. */
96+ export function detectLineEnding ( path : string ) {
97+ const f = fs . openSync ( path , 'r' ) ;
98+ const SIZE = 10_000 ;
99+ const buffer = Buffer . alloc ( SIZE ) ;
100+ const n = fs . readSync ( f , buffer , 0 , SIZE , 0 ) ;
101+ fs . closeSync ( f ) ;
102+ for ( let i = 0 ; i < n - 1 ; i ++ ) {
103+ const [ a , b ] = [ buffer [ i ] , buffer [ i + 1 ] ] ;
104+ if ( a == CR && b == LF ) {
105+ return '\r\n' ; // Windows
106+ } else if ( a == LF ) {
107+ return '\n' ; // Unix
108+ } else if ( a == CR ) {
109+ return '\r' ; // Old Mac
110+ }
111+ }
112+ return undefined ;
113+ }
114+
92115/**
93116 * Append one row to a CSV file.
94117 *
@@ -103,6 +126,7 @@ export async function appendRow(file: string, row: {[column: string]: string}) {
103126 return writeCsv ( file , rows ) ;
104127 }
105128
129+ const lineEnding = detectLineEnding ( file ) ;
106130 const lines = readRows ( file ) ;
107131 const headerRow = await lines . next ( ) ;
108132 if ( headerRow . done ) {
@@ -130,22 +154,25 @@ export async function appendRow(file: string, row: {[column: string]: string}) {
130154 rows . push ( row . concat ( emptyCols ) ) ;
131155 }
132156 rows . push ( fullHeaders . map ( k => row [ k ] || '' ) ) ;
133- await writeCsv ( file , rows ) ;
157+ await writeCsv ( file , rows , { record_delimiter : lineEnding } ) ;
134158 } else {
135159 // write the new row
136160 const newRow = headers . map ( k => row [ k ] || '' ) ;
137161 await lines . return ( ) ; // close the file for reading.
138162 // Add a newline if the file doesn't end with one.
139163 const f = fs . openSync ( file , 'a+' ) ;
140164 const { size} = fs . fstatSync ( f ) ;
141- const { buffer} = await fs . read ( f , Buffer . alloc ( 1 ) , 0 , 1 , size - 1 ) ;
142- const hasTrailingNewline = buffer [ 0 ] == '\n' . charCodeAt ( 0 ) ;
143- const lineStr = ( hasTrailingNewline ? '' : '\n' ) + stringify ( [ newRow ] ) ;
165+ const { buffer} = await fs . read ( f , Buffer . alloc ( 2 ) , 0 , 2 , size - 2 ) ;
166+ const tail = buffer . toString ( 'utf8' ) ;
167+ const hasTrailingNewline = tail . endsWith ( lineEnding ?? '\n' ) ;
168+ const lineStr =
169+ ( hasTrailingNewline ? '' : lineEnding ) + stringify ( [ newRow ] , { record_delimiter : lineEnding } ) ;
144170 await fs . appendFile ( f , lineStr ) ;
145171 await fs . close ( f ) ;
146172 }
147173}
148174
175+ // Note: this might change line endings in the file.
149176export async function deleteLastRow ( file : string ) {
150177 const rows = [ ] ;
151178 for await ( const row of readRows ( file ) ) {
0 commit comments