1- import fs from "fs" ;
21import { PackageJson , TsConfigJson } from "type-fest" ;
32import detectIndent from "detect-indent"
4- import loadJsonFile from "load-json-file"
3+ import stripBom from "strip-bom"
4+ import parseJson from "parse-json"
5+ import fs from "graceful-fs"
56
67interface Options {
78 /** @default utf-8 */
89 encoding : BufferEncoding
910 /**
10- * Will throw in case of FS error or invalid JSON
11+ * If `false`, FS or JSON errors will be ignored
1112 * @default true
1213 * */
1314 throws : boolean
1415 // ideally this lib should integrate with json validator
15- /** @default "throw" (even if throws is false) */
16+ /** @default "throw" (silent if throws: false) */
1617 ifFieldIsMissing : "throw" | "skip" | "add"
1718 /**
18- * - throw - throws (even if throws param is false)
19+ * - throw - throws (silent if throws: false)
1920 *- skip - won't call the function
2021 * - pass - pass the `undefined` value
2122 * @default "throw"
@@ -31,69 +32,83 @@ interface Options {
3132}
3233
3334type GettersDeep < T extends object > = {
34- [ K in keyof T ] : ( oldValue : T [ K ] , json : T ) => unknown
35+ [ K in keyof T ] : ( oldValue : T [ K ] , json : T ) => T [ K ]
3536 //T[K] extends object ? ((oldValue: T[K]) => unknown)/* | GettersDeep<T[K]> */ : (oldValue: T[K]) => unknown
3637}
3738
3839export type ModifyJsonFileFunction < T extends object > = (
3940 path : string ,
40- fields : Partial < T | GettersDeep < T > > ,
41+ modifyFields : Partial < T | GettersDeep < T > > | ( ( oldJson : T ) => T ) ,
4142 options ?: Options
4243) => Promise < void > ;
4344
4445type ModifyJsonFileGenericFunction = < T extends object > (
4546 path : string ,
46- fields : Partial < T | GettersDeep < T > > ,
47+ modifyFields : Partial < T | GettersDeep < T > > | ( ( oldJson : T ) => T ) ,
4748 options ?: Partial < Options >
4849) => Promise < void > ;
4950
51+ /** returns additional info, not only JSON */
52+ const loadJsonFile = async ( filePath : string , { encoding, tabSize } : Pick < Options , "encoding" | "tabSize" > ) => {
53+ const contents = stripBom (
54+ await fs . promises . readFile ( filePath , encoding )
55+ ) ;
56+ return {
57+ json : parseJson ( contents , filePath ) ,
58+ indent : tabSize === "preserve" ?
59+ detectIndent ( contents ) . indent :
60+ tabSize === "hard" ? "\t" : tabSize === null ?
61+ undefined : " " . repeat ( tabSize )
62+ }
63+ } ;
64+
5065/** It's just Error */
51- class InnerError extends Error {
52- innerError = true ;
53- }
66+ // class InnerError extends Error {
67+ // innerError = true;
68+ // }
5469
5570/**
5671 * modifies **original** JSON file
5772 * You can pass generic, that reflects the structure of original JSON file
5873 *
59- * Fields, that are functions will be skipped if they're not preset in original JSON file
74+ * @param modifyFields Function setter or fields to merge
6075 */
6176export const modifyJsonFile : ModifyJsonFileGenericFunction = async (
6277 path ,
63- fields ,
78+ modifyFields ,
6479 options
6580) => {
6681 const {
6782 encoding = "utf-8" ,
6883 throws = true ,
6984 ifFieldIsMissing = "throw" ,
7085 ifFieldIsMissingForSetter = "throw" ,
71- tabSize : tabSizeOption = "preserve"
86+ tabSize = "preserve"
7287 } = options || { } ;
7388 try {
74- const json = await loadJsonFile ( path ) ;
89+ let { json, indent } = await loadJsonFile ( path , { encoding , tabSize } ) ;
7590 // todo remove restriction or not?
7691 if ( ! json || typeof json !== "object" || Array . isArray ( json ) ) throw new TypeError ( `${ path } : JSON root type must be object` ) ;
77- // todo we don't need to read the file twice
78- const rawText = await fs . promises . readFile ( path , encoding ) ;
79- const indent = tabSizeOption === "preserve" ? detectIndent ( rawText ) . indent : tabSizeOption === "hard" ? "\t" : tabSizeOption === null ? undefined : " " . repeat ( tabSizeOption ) ;
80-
81- for ( const [ name , value ] of Object . entries ( fields ) ) {
82- if ( ! ( name in json ) ) {
83- const isSetter = typeof value === "function" ;
84- const generalAction = isSetter ? ifFieldIsMissingForSetter : ifFieldIsMissing ;
85- if ( generalAction === "throw" ) throw new InnerError ( `Property to modify "${ name } " is missing in ${ path } ` ) ;
86- if ( generalAction === "skip" ) continue ;
92+ if ( typeof modifyFields === "function" ) {
93+ json = modifyFields ( json )
94+ } else {
95+ for ( const [ name , value ] of Object . entries ( modifyFields ) ) {
96+ if ( ! ( name in json ) ) {
97+ const isSetter = typeof value === "function" ;
98+ const generalAction = isSetter ? ifFieldIsMissingForSetter : ifFieldIsMissing ;
99+ if ( generalAction === "throw" ) throw new TypeError ( `Property to modify "${ name } " is missing in ${ path } ` ) ;
100+ if ( generalAction === "skip" ) continue ;
101+ }
102+ json [ name as string ] = typeof value === "function" ? value ( json [ name as string ] , json ) : value ;
87103 }
88- json [ name as string ] = typeof value === "function" ? value ( json [ name as string ] , json ) : value ;
89104 }
90105
91106 await fs . promises . writeFile (
92- path ,
107+ path ,
93108 JSON . stringify ( json , undefined , indent )
94109 ) ;
95110 } catch ( err ) {
96- if ( err . innerError ) throw new Error ( err . message ) ;
111+ // if (err.innerError) throw new Error(err.message);
97112 if ( throws ) throw err ;
98113 }
99114}
0 commit comments