11import * as fs from "fs/promises"
2+ import * as fsSync from "fs"
23import * as path from "path"
34import * as lockfile from "proper-lockfile"
5+ import Disassembler from "stream-json/Disassembler"
6+ import Stringer from "stream-json/Stringer"
47
58/**
69 * Safely writes JSON data to a file.
@@ -13,12 +16,8 @@ import * as lockfile from "proper-lockfile"
1316 * @param {any } data - The data to serialize to JSON and write.
1417 * @returns {Promise<void> }
1518 */
16- async function safeWriteJson (
17- filePath : string ,
18- data : any ,
19- replacer ?: ( key : string , value : any ) => any ,
20- space : string | number = 2 ,
21- ) : Promise < void > {
19+
20+ async function safeWriteJson ( filePath : string , data : any ) : Promise < void > {
2221 const absoluteFilePath = path . resolve ( filePath )
2322 const lockPath = `${ absoluteFilePath } .lock`
2423 let releaseLock = async ( ) => { } // Initialized to a no-op
@@ -59,8 +58,8 @@ async function safeWriteJson(
5958 path . dirname ( absoluteFilePath ) ,
6059 `.${ path . basename ( absoluteFilePath ) } .new_${ Date . now ( ) } _${ Math . random ( ) . toString ( 36 ) . substring ( 2 ) } .tmp` ,
6160 )
62- const jsonData = JSON . stringify ( data , replacer , space )
63- await fs . writeFile ( actualTempNewFilePath , jsonData , "utf8" )
61+
62+ await _streamDataToFile ( actualTempNewFilePath , data )
6463
6564 // Step 2: Check if the target file exists. If so, rename it to a backup path.
6665 try {
@@ -159,4 +158,58 @@ async function safeWriteJson(
159158 }
160159}
161160
161+ /**
162+ * Helper function to stream JSON data to a file.
163+ * @param targetPath The path to write the stream to.
164+ * @param data The data to stream.
165+ * @returns Promise<void>
166+ */
167+ async function _streamDataToFile ( targetPath : string , data : any ) : Promise < void > {
168+ // Stream data to avoid high memory usage for large JSON objects.
169+ const fileWriteStream = fsSync . createWriteStream ( targetPath , { encoding : "utf8" } )
170+ const disassembler = Disassembler . disassembler ( )
171+ // Output will be compact JSON as standard Stringer is used.
172+ const stringer = Stringer . stringer ( )
173+
174+ return new Promise < void > ( ( resolve , reject ) => {
175+ let errorOccurred = false
176+ const handleError = ( _streamName : string ) => ( err : Error ) => {
177+ if ( ! errorOccurred ) {
178+ errorOccurred = true
179+ if ( ! fileWriteStream . destroyed ) {
180+ fileWriteStream . destroy ( err )
181+ }
182+ reject ( err )
183+ }
184+ }
185+
186+ disassembler . on ( "error" , handleError ( "Disassembler" ) )
187+ stringer . on ( "error" , handleError ( "Stringer" ) )
188+ fileWriteStream . on ( "error" , ( err : Error ) => {
189+ if ( ! errorOccurred ) {
190+ errorOccurred = true
191+ reject ( err )
192+ }
193+ } )
194+
195+ fileWriteStream . on ( "finish" , ( ) => {
196+ if ( ! errorOccurred ) {
197+ resolve ( )
198+ }
199+ } )
200+
201+ disassembler . pipe ( stringer ) . pipe ( fileWriteStream )
202+
203+ // stream-json's Disassembler might error if `data` is undefined.
204+ // JSON.stringify(undefined) would produce the string "undefined" if it's the root value.
205+ // Writing 'null' is a safer JSON representation for a root undefined value.
206+ if ( data === undefined ) {
207+ disassembler . write ( null )
208+ } else {
209+ disassembler . write ( data )
210+ }
211+ disassembler . end ( )
212+ } )
213+ }
214+
162215export { safeWriteJson }
0 commit comments