1
+ import type { ReadStream , WriteStream } from "node:fs" ;
2
+ import type { FileHandle } from "node:fs/promises" ;
3
+
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,72 @@ 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 fileHandle : FileHandle ;
214
+ let fileReadStream : ReadStream ;
215
+ try {
216
+ fileHandle = await fsPromises . open ( absolutePathToFile , "r" ) ;
217
+ fileReadStream = fileHandle . createReadStream ( ) ;
218
+ } catch ( e ) {
219
+ ensureError < NodeJS . ErrnoException > ( e ) ;
220
+
221
+ if ( e . code === "ENOENT" ) {
222
+ throw new FileNotFoundError ( absolutePathToFile , e ) ;
223
+ }
224
+
225
+ if ( e . code === "EISDIR" ) {
226
+ throw new IsDirectoryError ( absolutePathToFile , e ) ;
227
+ }
228
+
229
+ throw new FileSystemAccessError ( e . message , e ) ;
230
+ }
231
+
232
+ const readPipeline = fileReadStream . pipe ( parser ) ;
233
+
234
+ // NOTE: We save only the last result as it contains the fully parsed object
235
+ let result : T | undefined ;
236
+ parser . on ( "data" , ( { value } ) => {
237
+ result = value ;
238
+ } ) ;
239
+
240
+ try {
241
+ await finished ( parser ) ;
242
+ } catch ( e ) {
243
+ ensureError ( e ) ;
244
+ throw new InvalidFileFormatError ( absolutePathToFile , e ) ;
245
+ }
246
+
247
+ try {
248
+ await finished ( readPipeline ) ;
249
+
250
+ await fileHandle . close ( ) ;
251
+ } catch ( e ) {
252
+ ensureError ( e ) ;
253
+ throw new FileSystemAccessError ( absolutePathToFile , e ) ;
254
+ }
255
+
256
+ if ( result === undefined ) {
257
+ throw new InvalidFileFormatError ( absolutePathToFile , new Error ( "No data" ) ) ;
258
+ }
259
+
260
+ return result ;
261
+ }
262
+
190
263
/**
191
264
* Writes an object to a JSON file. The encoding used is "utf8" and the file is overwritten.
192
265
* If part of the path doesn't exist, it will be created.
@@ -211,6 +284,63 @@ export async function writeJsonFile<T>(
211
284
await writeUtf8File ( absolutePathToFile , content ) ;
212
285
}
213
286
287
+ /**
288
+ * Writes an object to a JSON file as stream. The encoding used is "utf8" and the file is overwritten.
289
+ * If part of the path doesn't exist, it will be created.
290
+ * This function should be used when stringifying very large JSON objects.
291
+ *
292
+ * @param absolutePathToFile The path to the file. If the file exists, it will be overwritten.
293
+ * @param object The object to write.
294
+ * @throws JsonSerializationError if the object can't be serialized to JSON.
295
+ * @throws FileSystemAccessError for any other error.
296
+ */
297
+ export async function writeJsonFileAsStream < T > (
298
+ absolutePathToFile : string ,
299
+ object : T ,
300
+ ) : Promise < void > {
301
+ const dirPath = path . dirname ( absolutePathToFile ) ;
302
+ const dirExists = await exists ( dirPath ) ;
303
+ if ( ! dirExists ) {
304
+ await mkdir ( dirPath ) ;
305
+ }
306
+
307
+ let fileHandle : FileHandle ;
308
+ let fileWriteStream : WriteStream ;
309
+ try {
310
+ fileHandle = await fsPromises . open ( absolutePathToFile , "w" ) ;
311
+ fileWriteStream = fileHandle . createWriteStream ( ) ;
312
+ } catch ( e ) {
313
+ ensureError < NodeJS . ErrnoException > ( e ) ;
314
+ // if the directory was created, we should remove it
315
+ if ( dirExists === false ) {
316
+ try {
317
+ await remove ( dirPath ) ;
318
+ // we don't want to override the original error
319
+ } catch ( err ) { }
320
+ }
321
+
322
+ throw new FileSystemAccessError ( e . message , e ) ;
323
+ }
324
+
325
+ const jsonStream = new JsonStreamStringify ( object ) ;
326
+ const writePipeline = jsonStream . pipe ( fileWriteStream ) ;
327
+
328
+ try {
329
+ await finished ( jsonStream ) ;
330
+ } catch ( e ) {
331
+ ensureError ( e ) ;
332
+ throw new JsonSerializationError ( absolutePathToFile , e ) ;
333
+ }
334
+
335
+ try {
336
+ await finished ( writePipeline ) ;
337
+ await fileHandle . close ( ) ;
338
+ } catch ( e ) {
339
+ ensureError ( e ) ;
340
+ throw new FileSystemAccessError ( e . message , e ) ;
341
+ }
342
+ }
343
+
214
344
/**
215
345
* Reads a file and returns its content as a string. The encoding used is "utf8".
216
346
*
@@ -537,7 +667,9 @@ export async function move(source: string, destination: string): Promise<void> {
537
667
// On linux, trying to move a non-empty directory will throw ENOTEMPTY,
538
668
// while on Windows it will throw EPERM.
539
669
if ( e . code === "ENOTEMPTY" || e . code === "EPERM" ) {
540
- throw new DirectoryNotEmptyError ( destination , e ) ;
670
+ if ( await isDirectory ( source ) ) {
671
+ throw new DirectoryNotEmptyError ( destination , e ) ;
672
+ }
541
673
}
542
674
543
675
throw new FileSystemAccessError ( e . message , e ) ;
0 commit comments