1
+ import type { JsonTypes , ParsedElementInfo } from "@streamparser/json-node" ;
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 { pipeline } 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,76 @@ 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
+ let fileHandle : FileHandle | undefined ;
212
+
213
+ try {
214
+ fileHandle = await fsPromises . open ( absolutePathToFile , "r" ) ;
215
+
216
+ const fileReadStream = fileHandle . createReadStream ( ) ;
217
+
218
+ // NOTE: We set a separator to disable self-closing to be able to use the parser
219
+ // in the stream.pipeline context; see https://github.com/juanjoDiaz/streamparser-json/issues/47
220
+ const jsonParser = new JSONParser ( {
221
+ separator : "" ,
222
+ } ) ;
223
+
224
+ const result : T | undefined = await pipeline (
225
+ fileReadStream ,
226
+ jsonParser ,
227
+ async (
228
+ elements : AsyncIterable < ParsedElementInfo . ParsedElementInfo > ,
229
+ ) : Promise < any | undefined > => {
230
+ let value : JsonTypes . JsonPrimitive | JsonTypes . JsonStruct | undefined ;
231
+ for await ( const element of elements ) {
232
+ value = element . value ;
233
+ }
234
+ return value ;
235
+ } ,
236
+ ) ;
237
+
238
+ if ( result === undefined ) {
239
+ throw new Error ( "No data" ) ;
240
+ }
241
+
242
+ return result ;
243
+ } catch ( e ) {
244
+ ensureError < NodeJS . ErrnoException > ( e ) ;
245
+
246
+ if ( e . code === "ENOENT" ) {
247
+ throw new FileNotFoundError ( absolutePathToFile , e ) ;
248
+ }
249
+
250
+ if ( e . code === "EISDIR" ) {
251
+ throw new IsDirectoryError ( absolutePathToFile , e ) ;
252
+ }
253
+
254
+ // If the code is defined, we assume the error to be related to the file system
255
+ if ( e . code !== undefined ) {
256
+ throw new FileSystemAccessError ( absolutePathToFile , e ) ;
257
+ }
258
+
259
+ // Otherwise, we assume the error to be related to the file formatting
260
+ throw new InvalidFileFormatError ( absolutePathToFile , e ) ;
261
+ } finally {
262
+ // Explicitly closing the file handle to fully release the underlying resources
263
+ await fileHandle ?. close ( ) ;
264
+ }
265
+ }
266
+
190
267
/**
191
268
* Writes an object to a JSON file. The encoding used is "utf8" and the file is overwritten.
192
269
* If part of the path doesn't exist, it will be created.
@@ -211,6 +288,59 @@ export async function writeJsonFile<T>(
211
288
await writeUtf8File ( absolutePathToFile , content ) ;
212
289
}
213
290
291
+ /**
292
+ * Writes an object to a JSON file as stream. The encoding used is "utf8" and the file is overwritten.
293
+ * If part of the path doesn't exist, it will be created.
294
+ * This function should be used when stringifying very large JSON objects.
295
+ *
296
+ * @param absolutePathToFile The path to the file. If the file exists, it will be overwritten.
297
+ * @param object The object to write.
298
+ * @throws JsonSerializationError if the object can't be serialized to JSON.
299
+ * @throws FileSystemAccessError for any other error.
300
+ */
301
+ export async function writeJsonFileAsStream < T > (
302
+ absolutePathToFile : string ,
303
+ object : T ,
304
+ ) : Promise < void > {
305
+ const dirPath = path . dirname ( absolutePathToFile ) ;
306
+ const dirExists = await exists ( dirPath ) ;
307
+ if ( ! dirExists ) {
308
+ await mkdir ( dirPath ) ;
309
+ }
310
+
311
+ let fileHandle : FileHandle | undefined ;
312
+
313
+ try {
314
+ fileHandle = await fsPromises . open ( absolutePathToFile , "w" ) ;
315
+
316
+ const jsonStream = new JsonStreamStringify ( object ) ;
317
+ const fileWriteStream = fileHandle . createWriteStream ( ) ;
318
+
319
+ await pipeline ( jsonStream , fileWriteStream ) ;
320
+ } catch ( e ) {
321
+ ensureError < NodeJS . ErrnoException > ( e ) ;
322
+ // if the directory was created, we should remove it
323
+ if ( dirExists === false ) {
324
+ try {
325
+ await remove ( dirPath ) ;
326
+ // we don't want to override the original error
327
+ } catch ( err ) { }
328
+ }
329
+
330
+ // If the code is defined, we assume the error to be related to the file system
331
+ if ( e . code !== undefined ) {
332
+ throw new FileSystemAccessError ( e . message , e ) ;
333
+ }
334
+
335
+ // Otherwise, we assume the error to be related to the file formatting
336
+ throw new JsonSerializationError ( absolutePathToFile , e ) ;
337
+ } finally {
338
+ // NOTE: Historically, not closing the file handle caused issues on Windows,
339
+ // for example, when trying to move the file previously written to by this function
340
+ await fileHandle ?. close ( ) ;
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