1
+ import type { ReadStream , WriteStream } from "node:fs" ;
2
+
3
+ import { createReadStream , createWriteStream } from "node:fs" ;
1
4
import fsPromises from "node:fs/promises" ;
2
5
import path from "node:path" ;
6
+ import { finished } from "node:stream/promises" ;
7
+
8
+ import { JSONParser } from "@streamparser/json-node" ;
9
+ import { JsonStreamStringify } from "json-stream-stringify" ;
3
10
4
11
import { ensureError } from "./error.js" ;
5
12
import {
@@ -187,6 +194,65 @@ export async function readJsonFile<T>(absolutePathToFile: string): Promise<T> {
187
194
}
188
195
}
189
196
197
+ /**
198
+ * Reads a JSON file as a stream and parses it. The encoding used is "utf8".
199
+ * This function should be used when parsing very large JSON files.
200
+ *
201
+ * @param absolutePathToFile The path to the file.
202
+ * @returns The parsed JSON object.
203
+ * @throws FileNotFoundError if the file doesn't exist.
204
+ * @throws InvalidFileFormatError if the file is not a valid JSON file.
205
+ * @throws IsDirectoryError if the path is a directory instead of a file.
206
+ * @throws FileSystemAccessError for any other error.
207
+ */
208
+ export async function readJsonFileAsStream < T > (
209
+ absolutePathToFile : string ,
210
+ ) : Promise < T > {
211
+ const parser = new JSONParser ( ) ;
212
+
213
+ let fileContents : ReadStream ;
214
+ try {
215
+ fileContents = createReadStream ( absolutePathToFile , {
216
+ encoding : "utf8" ,
217
+ autoClose : true ,
218
+ flags : "r" ,
219
+ } ) ;
220
+ } catch ( e ) {
221
+ ensureError < NodeJS . ErrnoException > ( e ) ;
222
+
223
+ if ( e . code === "ENOENT" ) {
224
+ throw new FileNotFoundError ( absolutePathToFile , e ) ;
225
+ }
226
+
227
+ if ( e . code === "EISDIR" ) {
228
+ throw new IsDirectoryError ( absolutePathToFile , e ) ;
229
+ }
230
+
231
+ throw new FileSystemAccessError ( e . message , e ) ;
232
+ }
233
+
234
+ const readPipeline = fileContents . pipe ( parser ) ;
235
+
236
+ // NOTE: We save only the last result as it contains the fully parsed object
237
+ let result : T | undefined ;
238
+ readPipeline . on ( "data" , ( { value } ) => {
239
+ result = value ;
240
+ } ) ;
241
+
242
+ try {
243
+ await finished ( readPipeline ) ;
244
+ } catch ( e ) {
245
+ ensureError ( e ) ;
246
+ throw new InvalidFileFormatError ( absolutePathToFile , e ) ;
247
+ }
248
+
249
+ if ( result === undefined ) {
250
+ throw new InvalidFileFormatError ( absolutePathToFile , new Error ( "No data" ) ) ;
251
+ }
252
+
253
+ return result ;
254
+ }
255
+
190
256
/**
191
257
* Writes an object to a JSON file. The encoding used is "utf8" and the file is overwritten.
192
258
* If part of the path doesn't exist, it will be created.
@@ -211,6 +277,65 @@ export async function writeJsonFile<T>(
211
277
await writeUtf8File ( absolutePathToFile , content ) ;
212
278
}
213
279
280
+ /**
281
+ * Writes an object to a JSON file as stream. The encoding used is "utf8" and the file is overwritten.
282
+ * If part of the path doesn't exist, it will be created.
283
+ * This function should be used when stringifying very large JSON objects.
284
+ *
285
+ * @param absolutePathToFile The path to the file. If the file exists, it will be overwritten.
286
+ * @param object The object to write.
287
+ * @throws JsonSerializationError if the object can't be serialized to JSON.
288
+ * @throws FileSystemAccessError for any other error.
289
+ */
290
+ export async function writeJsonFileAsStream < T > (
291
+ absolutePathToFile : string ,
292
+ object : T ,
293
+ ) : Promise < void > {
294
+ const dirPath = path . dirname ( absolutePathToFile ) ;
295
+ const dirExists = await exists ( dirPath ) ;
296
+ if ( ! dirExists ) {
297
+ await mkdir ( dirPath ) ;
298
+ }
299
+
300
+ let fileContents : WriteStream ;
301
+ try {
302
+ fileContents = createWriteStream ( absolutePathToFile , {
303
+ encoding : "utf8" ,
304
+ autoClose : true ,
305
+ mode : 0o666 ,
306
+ flags : "w" ,
307
+ } ) ;
308
+ } catch ( e ) {
309
+ ensureError < NodeJS . ErrnoException > ( e ) ;
310
+ // if the directory was created, we should remove it
311
+ if ( dirExists === false ) {
312
+ try {
313
+ await remove ( dirPath ) ;
314
+ // we don't want to override the original error
315
+ } catch ( err ) { }
316
+ }
317
+
318
+ throw new FileSystemAccessError ( e . message , e ) ;
319
+ }
320
+
321
+ const jsonStream = new JsonStreamStringify ( object ) ;
322
+ const writePipeline = jsonStream . pipe ( fileContents ) ;
323
+
324
+ try {
325
+ await finished ( jsonStream ) ;
326
+ } catch ( e ) {
327
+ ensureError ( e ) ;
328
+ throw new JsonSerializationError ( absolutePathToFile , e ) ;
329
+ }
330
+
331
+ try {
332
+ await finished ( writePipeline ) ;
333
+ } catch ( e ) {
334
+ ensureError ( e ) ;
335
+ throw new FileSystemAccessError ( e . message , e ) ;
336
+ }
337
+ }
338
+
214
339
/**
215
340
* Reads a file and returns its content as a string. The encoding used is "utf8".
216
341
*
@@ -537,7 +662,9 @@ export async function move(source: string, destination: string): Promise<void> {
537
662
// On linux, trying to move a non-empty directory will throw ENOTEMPTY,
538
663
// while on Windows it will throw EPERM.
539
664
if ( e . code === "ENOTEMPTY" || e . code === "EPERM" ) {
540
- throw new DirectoryNotEmptyError ( destination , e ) ;
665
+ if ( await isDirectory ( source ) ) {
666
+ throw new DirectoryNotEmptyError ( destination , e ) ;
667
+ }
541
668
}
542
669
543
670
throw new FileSystemAccessError ( e . message , e ) ;
0 commit comments