@@ -2,8 +2,20 @@ import * as fs from "fs/promises"
22import * as fsSync from "fs"
33import * as path from "path"
44import * as lockfile from "proper-lockfile"
5- import Disassembler from "stream-json/Disassembler"
6- import Stringer from "stream-json/Stringer"
5+ import { JsonStreamStringify } from "json-stream-stringify"
6+
7+ /**
8+ * Options for safeWriteJson function
9+ */
10+ export interface SafeWriteJsonOptions {
11+ /**
12+ * Whether to pretty-print the JSON output with indentation.
13+ * When true, uses tab characters for indentation.
14+ * When false or undefined, outputs compact JSON.
15+ * @default false
16+ */
17+ prettyPrint ?: boolean
18+ }
719
820/**
921 * Safely writes JSON data to a file.
@@ -12,13 +24,15 @@ import Stringer from "stream-json/Stringer"
1224 * - Writes to a temporary file first.
1325 * - If the target file exists, it's backed up before being replaced.
1426 * - Attempts to roll back and clean up in case of errors.
27+ * - Supports pretty-printing with indentation while maintaining streaming efficiency.
1528 *
1629 * @param {string } filePath - The absolute path to the target file.
1730 * @param {any } data - The data to serialize to JSON and write.
31+ * @param {SafeWriteJsonOptions } options - Optional configuration for JSON formatting.
1832 * @returns {Promise<void> }
1933 */
2034
21- async function safeWriteJson ( filePath : string , data : any ) : Promise < void > {
35+ async function safeWriteJson ( filePath : string , data : any , options ?: SafeWriteJsonOptions ) : Promise < void > {
2236 const absoluteFilePath = path . resolve ( filePath )
2337 let releaseLock = async ( ) => { } // Initialized to a no-op
2438
@@ -75,7 +89,7 @@ async function safeWriteJson(filePath: string, data: any): Promise<void> {
7589 `.${ path . basename ( absoluteFilePath ) } .new_${ Date . now ( ) } _${ Math . random ( ) . toString ( 36 ) . substring ( 2 ) } .tmp` ,
7690 )
7791
78- await _streamDataToFile ( actualTempNewFilePath , data )
92+ await _streamDataToFile ( actualTempNewFilePath , data , options ?. prettyPrint )
7993
8094 // Step 2: Check if the target file exists. If so, rename it to a backup path.
8195 try {
@@ -182,53 +196,27 @@ async function safeWriteJson(filePath: string, data: any): Promise<void> {
182196 * Helper function to stream JSON data to a file.
183197 * @param targetPath The path to write the stream to.
184198 * @param data The data to stream.
199+ * @param prettyPrint Whether to format the JSON with indentation.
185200 * @returns Promise<void>
186201 */
187- async function _streamDataToFile ( targetPath : string , data : any ) : Promise < void > {
202+ async function _streamDataToFile ( targetPath : string , data : any , prettyPrint = false ) : Promise < void > {
188203 // Stream data to avoid high memory usage for large JSON objects.
189204 const fileWriteStream = fsSync . createWriteStream ( targetPath , { encoding : "utf8" } )
190- const disassembler = Disassembler . disassembler ( )
191- // Output will be compact JSON as standard Stringer is used.
192- const stringer = Stringer . stringer ( )
193-
194- return new Promise < void > ( ( resolve , reject ) => {
195- let errorOccurred = false
196- const handleError = ( _streamName : string ) => ( err : Error ) => {
197- if ( ! errorOccurred ) {
198- errorOccurred = true
199- if ( ! fileWriteStream . destroyed ) {
200- fileWriteStream . destroy ( err )
201- }
202- reject ( err )
203- }
204- }
205205
206- disassembler . on ( "error" , handleError ( "Disassembler" ) )
207- stringer . on ( "error" , handleError ( "Stringer" ) )
208- fileWriteStream . on ( "error" , ( err : Error ) => {
209- if ( ! errorOccurred ) {
210- errorOccurred = true
211- reject ( err )
212- }
213- } )
214-
215- fileWriteStream . on ( "finish" , ( ) => {
216- if ( ! errorOccurred ) {
217- resolve ( )
218- }
219- } )
220-
221- disassembler . pipe ( stringer ) . pipe ( fileWriteStream )
206+ // JsonStreamStringify traverses the object and streams tokens directly
207+ // The 'spaces' parameter adds indentation during streaming, not via a separate pass
208+ // Convert undefined to null for valid JSON serialization (undefined is not valid JSON)
209+ const stringifyStream = new JsonStreamStringify (
210+ data === undefined ? null : data ,
211+ undefined , // replacer
212+ prettyPrint ? "\t" : undefined , // spaces for indentation
213+ )
222214
223- // stream-json's Disassembler might error if `data` is undefined.
224- // JSON.stringify(undefined) would produce the string "undefined" if it's the root value.
225- // Writing 'null' is a safer JSON representation for a root undefined value.
226- if ( data === undefined ) {
227- disassembler . write ( null )
228- } else {
229- disassembler . write ( data )
230- }
231- disassembler . end ( )
215+ return new Promise < void > ( ( resolve , reject ) => {
216+ stringifyStream . on ( "error" , reject )
217+ fileWriteStream . on ( "error" , reject )
218+ fileWriteStream . on ( "finish" , resolve )
219+ stringifyStream . pipe ( fileWriteStream )
232220 } )
233221}
234222
0 commit comments