import * as os from 'os' ; import * as jju from 'jju' ; import { JsonSchema , IJsonSchemaErrorInfo , IJsonSchemaValidateOptions } from './JsonSchema' ; import { Text , NewlineKind } from './Text' ; import { FileSystem } from './FileSystem' ; remarks public export type JsonObject = any ; remarks If you are designing a new JSON file format, it's a good idea to avoid `null` entirely.  In most cases
 * there are better representations that convey more information about an item that is unknown, omitted, or disabled.
 *
 * To understand why `null` is deprecated, please see the `@rushstack/eslint-plugin` documentation here:
 *
 *  https public export type JsonNull = null ; public export interface IJsonFileStringifyOptions { /**
     * If provided, the specified newline type will be used instead of the default `\r\n`.
     */ newlineConversion ? : NewlineKind ; /**
     * If true, conforms to the standard behavior of JSON.stringify() when a property has the value `undefined`.
     * Specifically, the key will be dropped from the emitted object.
     */ ignoreUndefinedValues ? : boolean ; /**
     * If true, then the "jju" library will be used to improve the text formatting.
     * Note that this is slightly slower than the native JSON.stringify() implementation.
     */ prettyFormatting ? : boolean ; remarks When used with  IJsonFileSaveOptions . updateExistingFile * or  JsonFile . updateString , the header will ONLY be added for a newly created file.
      headerComment ? : string ; } public export interface IJsonFileSaveOptions extends IJsonFileStringifyOptions { /**
     * If there is an existing file, and the contents have not changed, then
     * don't write anything; this preserves the old timestamp.
     */ onlyIfChanged ? : boolean ; /**
     * Creates the folder recursively using FileSystem.ensureFolder()
     * Defaults to false.
     */ ensureFolderExists ? : boolean ; /**
     * If true, use the "jju" library to preserve the existing JSON formatting:  The file will be loaded
     * from the target filename, the new content will be merged in (preserving whitespace and comments),
     * and then the file will be overwritten with the merged contents.  If the target file does not exist,
     * then the file is saved normally.
     */ updateExistingFile ? : boolean ; } const DEFAULT_ENCODING = 'utf8' ; public export class JsonFile { internal public static _formatPathForError : ( path : string ) => string = ( path ) => path ; /**
     * Loads a JSON file.
     */ public static load ( jsonFilename ) { try { const contents = FileSystem . readFile ( jsonFilename ) ; return jju . parse ( contents ) ; } catch ( error ) { if ( FileSystem . isNotExistError ( error as Error ) ) { throw error ; } else { throw new Error ( `Error reading "${ JsonFile . _formatPathForError ( jsonFilename ) }":` + os . EOL + `  ${ ( error as Error ) . message }` ) ; } } } /**
     * An async version of  JsonFile . load .
      public static async loadAsync ( jsonFilename ) { try { const contents = await FileSystem . readFileAsync ( jsonFilename ) ; return jju . parse ( contents ) ; } catch ( error ) { if ( FileSystem . isNotExistError ( error as Error ) ) { throw error ; } else { throw new Error ( `Error reading "${ JsonFile . _formatPathForError ( jsonFilename ) }":` + os . EOL + `  ${ ( error as Error ) . message }` ) ; } } } /**
     * Parses a JSON file's contents.
     */ public static parseString ( jsonContents ) { return jju . parse ( jsonContents ) ; } /**
     * Loads a JSON file and validate its schema.
     */ public static loadAndValidate ( jsonFilename , jsonSchema , options ? ) { const jsonObject = JsonFile . load ( jsonFilename ) ; jsonSchema . validateObject ( jsonObject , jsonFilename , options ) ; return jsonObject ; } /**
     * An async version of  JsonFile . loadAndValidate .
      public static async loadAndValidateAsync ( jsonFilename , jsonSchema , options ? ) { const jsonObject = await JsonFile . loadAsync ( jsonFilename ) ; jsonSchema . validateObject ( jsonObject , jsonFilename , options ) ; return jsonObject ; } remarks public static loadAndValidateWithCallback ( jsonFilename , jsonSchema , errorCallback ) { const jsonObject = JsonFile . load ( jsonFilename ) ; jsonSchema . validateObjectWithCallback ( jsonObject , errorCallback ) ; return jsonObject ; } /**
     * An async version of  JsonFile . loadAndValidateWithCallback .
      public static async loadAndValidateWithCallbackAsync ( jsonFilename , jsonSchema , errorCallback ) { const jsonObject = await JsonFile . loadAsync ( jsonFilename ) ; jsonSchema . validateObjectWithCallback ( jsonObject , errorCallback ) ; return jsonObject ; } param jsonObject param options returns public static stringify ( jsonObject , options ? ) { return JsonFile . updateString ( '' , jsonObject , options ) ; } param jsonObject param options returns public static updateString ( previousJson , newJsonObject , options ? ) { if ( ! options ) { options = { } ; } if ( ! options . ignoreUndefinedValues ) { JsonFile . validateNoUndefinedMembers ( newJsonObject ) ; } let stringified ; if ( previousJson !== '' ) { stringified = jju . update ( previousJson , newJsonObject , { mode : 'cjson' , indent : 2 } ) ; } else if ( options . prettyFormatting ) { stringified = jju . stringify ( newJsonObject , { mode : 'json' , indent : 2 } ) ; if ( options . headerComment !== undefined ) { stringified = JsonFile . _formatJsonHeaderComment ( options . headerComment ) + stringified ; } } else { stringified = JSON . stringify ( newJsonObject , undefined , 2 ) ; if ( options . headerComment !== undefined ) { stringified = JsonFile . _formatJsonHeaderComment ( options . headerComment ) + stringified ; } } stringified = Text . ensureTrailingNewline ( stringified ) ; if ( options && options . newlineConversion ) { stringified = Text . convertTo ( stringified , options . newlineConversion ) ; } return stringified ; } param jsonObject param jsonFilename param options returns public static save ( jsonObject , jsonFilename , options ? ) { if ( ! options ) { options = { } ; } let oldBuffer = undefined ; if ( options . updateExistingFile || options . onlyIfChanged ) { try { oldBuffer = FileSystem . readFileToBuffer ( jsonFilename ) ; } catch ( error ) { if ( ! FileSystem . isNotExistError ( error as Error ) ) { throw error ; } } } let jsonToUpdate = '' ; if ( options . updateExistingFile && oldBuffer ) { jsonToUpdate = oldBuffer . toString ( DEFAULT_ENCODING ) ; } const newJson = JsonFile . updateString ( jsonToUpdate , jsonObject , options ) ; const newBuffer = Buffer . from ( newJson , DEFAULT_ENCODING ) ; if ( options . onlyIfChanged ) { if ( oldBuffer && Buffer . compare ( newBuffer , oldBuffer ) === 0 ) { return false ; } } FileSystem . writeFile ( jsonFilename , newBuffer . toString ( DEFAULT_ENCODING ) , { ensureFolderExists : options . ensureFolderExists } ) ; return true ; } /**
     * An async version of  JsonFile . save .
      public static async saveAsync ( jsonObject , jsonFilename , options ? ) { if ( ! options ) { options = { } ; } let oldBuffer = undefined ; if ( options . updateExistingFile || options . onlyIfChanged ) { try { oldBuffer = await FileSystem . readFileToBufferAsync ( jsonFilename ) ; } catch ( error ) { if ( ! FileSystem . isNotExistError ( error as Error ) ) { throw error ; } } } let jsonToUpdate = '' ; if ( options . updateExistingFile && oldBuffer ) { jsonToUpdate = oldBuffer . toString ( DEFAULT_ENCODING ) ; } const newJson = JsonFile . updateString ( jsonToUpdate , jsonObject , options ) ; const newBuffer = Buffer . from ( newJson , DEFAULT_ENCODING ) ; if ( options . onlyIfChanged ) { if ( oldBuffer && Buffer . compare ( newBuffer , oldBuffer ) === 0 ) { return false ; } } await FileSystem . writeFileAsync ( jsonFilename , newBuffer . toString ( DEFAULT_ENCODING ) , { ensureFolderExists : options . ensureFolderExists } ) ; return true ; } /**
     * Used to validate a data structure before writing.  Reports an error if there
     * are any undefined members.
     */ public static validateNoUndefinedMembers ( jsonObject ) { return JsonFile . _validateNoUndefinedMembers ( jsonObject , [ ] ) ; } private static _validateNoUndefinedMembers ( jsonObject , keyPath ) { if ( ! jsonObject ) { return ; } if ( typeof jsonObject === 'object' ) { for ( const key of Object . keys ( jsonObject ) ) { keyPath . push ( key ) ; const value = jsonObject [ key ] ; if ( value === undefined ) { const fullPath = JsonFile . _formatKeyPath ( keyPath ) ; throw new Error ( `The value for ${ fullPath } is "undefined" and cannot be serialized as JSON` ) ; } JsonFile . _validateNoUndefinedMembers ( value , keyPath ) ; keyPath . pop ( ) ; } } } private static _formatKeyPath ( keyPath ) { let result = '' ; for ( const key of keyPath ) { if ( /^[0-9]+$/ . test ( key ) ) { result += `[${ key }]` ; } else if ( /^[a-z_][a-z_0-9]*$/i . test ( key ) ) { if ( result ) { result += '.' ; } result += `${ key }` ; } else { const escapedKey = key . replace ( /[\\]/g , '\\\\' ) . replace ( /["]/g , '\\' ) ; result += `["${ escapedKey }"]` ; } } return result ; } private static _formatJsonHeaderComment ( headerComment ) { if ( headerComment === '' ) { return '' ; } const lines = headerComment . split ( '\n' ) ; const result = [ ] ; for ( const line of lines ) { if ( ! /^\s*$/ . test ( line ) && ! /^\s*\/\// . test ( line ) ) { throw new Error ( 'The headerComment lines must be blank or start with the "//" prefix.\n' + 'Invalid line' + JSON . stringify ( line ) ) ; } result . push ( Text . replaceAll ( line , '\r' , '' ) ) ; } return lines . join ( '\n' ) + '\n' ; } }