boma 2.0.5 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ import type { addToJSONProps, ReadJSONProps, ReadJSONResult, SaveJSONProps } from './common.js';
2
+ export declare const saveJSONAsync: (saveInput: SaveJSONProps) => Promise<void>;
3
+ export declare const readJSONAsync: <T = any>(props: ReadJSONProps) => Promise<ReadJSONResult<T>>;
4
+ export declare const addToJSONAsync: (saveInput: addToJSONProps) => Promise<void>;
package/dist/async.js ADDED
@@ -0,0 +1,119 @@
1
+ import { readFile, writeFile } from 'fs/promises';
2
+ import { getDate, getIssueMessage, getSerializationIssues, isErrorWithCode, isPlainMergeableObject, isSyntaxError, logSerializationIssues, sanitizeNonSerializable, } from './common.js';
3
+ export const saveJSONAsync = async (saveInput) => {
4
+ const { filePath, objToSave, format = false, logSaving = false, replaceNonSerializable = false, silent = true, } = saveInput;
5
+ const issues = getSerializationIssues(objToSave);
6
+ if (issues.length > 0) {
7
+ !silent && logSerializationIssues(issues);
8
+ if (!replaceNonSerializable && !silent) {
9
+ const firstIssue = issues[0];
10
+ const errorMessage = `[${getDate()}] Boma got non JSON-serializable object at saveJSON! ${getIssueMessage(firstIssue)}`;
11
+ console.error(errorMessage);
12
+ throw new Error(errorMessage);
13
+ }
14
+ }
15
+ const valueToSave = replaceNonSerializable && issues.length > 0
16
+ ? sanitizeNonSerializable(objToSave)
17
+ : objToSave;
18
+ try {
19
+ const json = format ? JSON.stringify(valueToSave, null, 2) : JSON.stringify(valueToSave);
20
+ await writeFile(filePath, json, 'utf8');
21
+ !silent && logSaving && console.log(`[${getDate()}] Write file ${filePath} successfully`);
22
+ }
23
+ catch (error) {
24
+ if (isErrorWithCode(error)) {
25
+ console.error(`[${getDate()}] Boma got filesystem error for ${filePath}:`, error);
26
+ }
27
+ throw error;
28
+ }
29
+ };
30
+ export const readJSONAsync = async (props) => {
31
+ const { filePath, createIfNotFound = false, parseJSON = true, silent = true } = props;
32
+ try {
33
+ const savedfile = await readFile(filePath, 'utf8');
34
+ if (parseJSON) {
35
+ return savedfile.trim() === '' ? null : JSON.parse(savedfile);
36
+ }
37
+ return savedfile;
38
+ }
39
+ catch (err) {
40
+ if (isErrorWithCode(err)) {
41
+ switch (err.code) {
42
+ case 'ENOENT':
43
+ if (createIfNotFound) {
44
+ !silent && console.log('Try to create: ', filePath);
45
+ try {
46
+ const initialContent = typeof createIfNotFound === 'boolean' ? '{}' : JSON.stringify(createIfNotFound);
47
+ await writeFile(filePath, initialContent, 'utf8');
48
+ }
49
+ catch (writeErr) {
50
+ console.error(`Error creating file ${filePath}: `, writeErr, '\n');
51
+ }
52
+ return (typeof createIfNotFound === 'boolean' ? {} : createIfNotFound);
53
+ }
54
+ console.info("Boma can't find file: ", filePath);
55
+ return parseJSON ? {} : null;
56
+ case 'EACCES':
57
+ console.error(`Access denied for ${filePath}`);
58
+ return null;
59
+ default:
60
+ console.error(`Some filesystem error when try readJSON: ${filePath}`);
61
+ return null;
62
+ }
63
+ }
64
+ if (isSyntaxError(err)) {
65
+ console.error('File ', filePath, ' has incorrect JSON syntax', '\n');
66
+ return null;
67
+ }
68
+ console.error('Function readJSON error:', err, '\n');
69
+ return null;
70
+ }
71
+ };
72
+ export const addToJSONAsync = async (saveInput) => {
73
+ const { filePath, dataToAdd, format = false, logSaving = false, replaceNonSerializable = false, silent = true, } = saveInput;
74
+ const oldJSON = await readJSONAsync({ filePath, createIfNotFound: true, silent });
75
+ if (oldJSON === null || typeof oldJSON !== 'object') {
76
+ return saveJSONAsync({
77
+ filePath,
78
+ objToSave: dataToAdd,
79
+ format,
80
+ logSaving,
81
+ replaceNonSerializable,
82
+ silent
83
+ });
84
+ }
85
+ if (Array.isArray(oldJSON) && Array.isArray(dataToAdd)) {
86
+ await saveJSONAsync({
87
+ filePath,
88
+ objToSave: [...oldJSON, ...dataToAdd],
89
+ format,
90
+ logSaving,
91
+ replaceNonSerializable,
92
+ silent
93
+ });
94
+ return;
95
+ }
96
+ if (isPlainMergeableObject(oldJSON) && isPlainMergeableObject(dataToAdd)) {
97
+ await saveJSONAsync({
98
+ filePath,
99
+ objToSave: { ...oldJSON, ...dataToAdd },
100
+ format,
101
+ logSaving,
102
+ replaceNonSerializable,
103
+ silent
104
+ });
105
+ return;
106
+ }
107
+ if (Array.isArray(dataToAdd) && isPlainMergeableObject(oldJSON) && Object.keys(oldJSON).length === 0) {
108
+ await saveJSONAsync({
109
+ filePath,
110
+ objToSave: [...dataToAdd],
111
+ format,
112
+ logSaving,
113
+ replaceNonSerializable,
114
+ silent
115
+ });
116
+ return;
117
+ }
118
+ throw new Error('Cannot merge array with object');
119
+ };
package/dist/boma.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { AddToJSONAsyncProps, addToJSONProps, AddToJSONSyncProps, ReadJSONAsyncProps, ReadJSONProps, ReadJSONResult, ReadJSONSyncProps, SaveJSONAsyncProps, SaveJSONProps, SaveJSONSyncProps } from './common.js';
2
+ export * from './common.js';
3
+ export declare function saveJSON(props: SaveJSONAsyncProps): Promise<void>;
4
+ export declare function saveJSON(props: SaveJSONSyncProps): void;
5
+ export declare function saveJSON(props: SaveJSONProps): void | Promise<void>;
6
+ export declare function readJSON<T = any>(props: ReadJSONAsyncProps): Promise<ReadJSONResult<T>>;
7
+ export declare function readJSON<T = any>(props: ReadJSONSyncProps): ReadJSONResult<T>;
8
+ export declare function readJSON<T = any>(props: ReadJSONProps): ReadJSONResult<T> | Promise<ReadJSONResult<T>>;
9
+ export declare function addToJSON(props: AddToJSONAsyncProps): Promise<void>;
10
+ export declare function addToJSON(props: AddToJSONSyncProps): void;
11
+ export declare function addToJSON(props: addToJSONProps): void | Promise<void>;
package/dist/boma.js ADDED
@@ -0,0 +1,123 @@
1
+ import { readFileSync, writeFileSync } from 'fs';
2
+ import { addToJSONAsync, readJSONAsync, saveJSONAsync } from './async.js';
3
+ import { getDate, getIssueMessage, getSerializationIssues, isErrorWithCode, isPlainMergeableObject, isSyntaxError, logSerializationIssues, sanitizeNonSerializable } from './common.js';
4
+ export * from './common.js';
5
+ const saveJSONSync = (saveInput) => {
6
+ const { filePath, objToSave, format = false, logSaving = false, replaceNonSerializable = false, silent = true, } = saveInput;
7
+ const issues = getSerializationIssues(objToSave);
8
+ if (issues.length > 0) {
9
+ !silent && logSerializationIssues(issues);
10
+ if (!replaceNonSerializable && !silent) {
11
+ const firstIssue = issues[0];
12
+ const errorMessage = `[${getDate()}] Boma got non JSON-serializable object at saveJSON! ${getIssueMessage(firstIssue)}`;
13
+ console.error(errorMessage);
14
+ throw new Error(errorMessage);
15
+ }
16
+ }
17
+ const valueToSave = replaceNonSerializable && issues.length > 0
18
+ ? sanitizeNonSerializable(objToSave)
19
+ : objToSave;
20
+ try {
21
+ const json = format ? JSON.stringify(valueToSave, null, 2) : JSON.stringify(valueToSave);
22
+ writeFileSync(filePath, json, 'utf8');
23
+ !silent && logSaving && console.log(`[${getDate()}] Write file ${filePath} successfully`);
24
+ }
25
+ catch (error) {
26
+ if (isErrorWithCode(error)) {
27
+ console.error(`[${getDate()}] Boma got filesystem error for ${filePath}:`, error);
28
+ }
29
+ throw error;
30
+ }
31
+ };
32
+ const readJSONSync = (props) => {
33
+ const { filePath, createIfNotFound = false, parseJSON = true, silent = true } = props;
34
+ try {
35
+ const savedfile = readFileSync(filePath, 'utf8');
36
+ if (parseJSON) {
37
+ return savedfile.trim() === '' ? null : JSON.parse(savedfile);
38
+ }
39
+ return savedfile;
40
+ }
41
+ catch (err) {
42
+ if (isErrorWithCode(err)) {
43
+ switch (err.code) {
44
+ case 'ENOENT':
45
+ if (createIfNotFound) {
46
+ !silent && console.log('Try to create: ', filePath);
47
+ try {
48
+ const initialContent = typeof createIfNotFound === 'boolean' ? '{}' : JSON.stringify(createIfNotFound);
49
+ writeFileSync(filePath, initialContent, 'utf8');
50
+ }
51
+ catch (writeErr) {
52
+ console.error(`Error creating file ${filePath}: `, writeErr, '\n');
53
+ }
54
+ return (typeof createIfNotFound === 'boolean' ? {} : createIfNotFound);
55
+ }
56
+ console.info("Boma can't find file: ", filePath);
57
+ return parseJSON ? {} : null;
58
+ case 'EACCES':
59
+ console.error(`Access denied for ${filePath}`);
60
+ return null;
61
+ default:
62
+ console.error(`Some filesystem error when try readJSON: ${filePath}`);
63
+ return null;
64
+ }
65
+ }
66
+ if (isSyntaxError(err)) {
67
+ console.error('File ', filePath, ' has incorrect JSON syntax', '\n');
68
+ return null;
69
+ }
70
+ console.error('Function readJSON error:', err, '\n');
71
+ return null;
72
+ }
73
+ };
74
+ const addToJSONSync = (saveInput) => {
75
+ const { filePath, dataToAdd, format = false, logSaving = false, replaceNonSerializable = false, silent = true, } = saveInput;
76
+ const oldJSON = readJSONSync({ filePath, createIfNotFound: true, silent });
77
+ if (oldJSON === null || typeof oldJSON !== 'object') {
78
+ return saveJSONSync({ filePath, objToSave: dataToAdd, format, logSaving, replaceNonSerializable, silent });
79
+ }
80
+ if (Array.isArray(oldJSON) && Array.isArray(dataToAdd)) {
81
+ saveJSONSync({
82
+ filePath,
83
+ objToSave: [...oldJSON, ...dataToAdd],
84
+ format,
85
+ logSaving,
86
+ replaceNonSerializable,
87
+ silent
88
+ });
89
+ return;
90
+ }
91
+ if (isPlainMergeableObject(oldJSON) && isPlainMergeableObject(dataToAdd)) {
92
+ saveJSONSync({
93
+ filePath,
94
+ objToSave: { ...oldJSON, ...dataToAdd },
95
+ format,
96
+ logSaving,
97
+ replaceNonSerializable,
98
+ silent
99
+ });
100
+ return;
101
+ }
102
+ if (Array.isArray(dataToAdd) && isPlainMergeableObject(oldJSON) && Object.keys(oldJSON).length === 0) {
103
+ saveJSONSync({
104
+ filePath,
105
+ objToSave: [...dataToAdd],
106
+ format,
107
+ logSaving,
108
+ replaceNonSerializable,
109
+ silent
110
+ });
111
+ return;
112
+ }
113
+ throw new Error('Cannot merge array with object');
114
+ };
115
+ export function saveJSON(props) {
116
+ return props.async === true ? saveJSONAsync(props) : saveJSONSync(props);
117
+ }
118
+ export function readJSON(props) {
119
+ return props.async === true ? readJSONAsync(props) : readJSONSync(props);
120
+ }
121
+ export function addToJSON(props) {
122
+ return props.async === true ? addToJSONAsync(props) : addToJSONSync(props);
123
+ }
@@ -11,6 +11,7 @@ export interface ReadJSONProps {
11
11
  parseJSON?: boolean;
12
12
  createIfNotFound?: boolean | SerializableObject | SerializableArray;
13
13
  silent?: boolean;
14
+ async?: boolean;
14
15
  }
15
16
  export interface SaveJSONProps {
16
17
  filePath: string;
@@ -19,6 +20,7 @@ export interface SaveJSONProps {
19
20
  logSaving?: boolean;
20
21
  replaceNonSerializable?: boolean;
21
22
  silent?: boolean;
23
+ async?: boolean;
22
24
  }
23
25
  export interface addToJSONProps {
24
26
  filePath: string;
@@ -27,7 +29,27 @@ export interface addToJSONProps {
27
29
  logSaving?: boolean;
28
30
  replaceNonSerializable?: boolean;
29
31
  silent?: boolean;
32
+ async?: boolean;
30
33
  }
34
+ export type ReadJSONSyncProps = ReadJSONProps & {
35
+ async?: false;
36
+ };
37
+ export type ReadJSONAsyncProps = ReadJSONProps & {
38
+ async: true;
39
+ };
40
+ export type SaveJSONSyncProps = SaveJSONProps & {
41
+ async?: false;
42
+ };
43
+ export type SaveJSONAsyncProps = SaveJSONProps & {
44
+ async: true;
45
+ };
46
+ export type AddToJSONSyncProps = addToJSONProps & {
47
+ async?: false;
48
+ };
49
+ export type AddToJSONAsyncProps = addToJSONProps & {
50
+ async: true;
51
+ };
52
+ export type ReadJSONResult<T = any> = T | string | null;
31
53
  export type ErrorWithCode = Error & {
32
54
  code: string;
33
55
  };
@@ -44,12 +66,12 @@ export interface SerializationIssue {
44
66
  export declare const isErrorWithCode: (error: unknown) => error is ErrorWithCode;
45
67
  export declare const isErrorWithMessage: (error: unknown) => error is ErrorWithMessage;
46
68
  export declare const isSyntaxError: (error: unknown) => error is SyntaxError;
69
+ export declare const getDate: () => string;
47
70
  export declare const isObjectLike: (value: unknown) => value is object;
48
71
  export declare const isPlainMergeableObject: (value: unknown) => value is Record<string, unknown>;
72
+ export declare const getIssueMessage: (issue: SerializationIssue) => string;
49
73
  export declare const getSerializationIssues: (value: unknown, path?: string, ancestors?: WeakMap<object, string>, issues?: SerializationIssue[]) => SerializationIssue[];
50
74
  export declare const isSerializable: (value: unknown) => value is Serializable;
51
75
  export declare const sanitizeNonSerializable: (value: unknown, path?: string, ancestors?: WeakMap<object, string>) => Serializable;
52
- export declare const saveJSON: (saveInput: SaveJSONProps) => void;
53
- export declare const readJSON: (props: ReadJSONProps) => any;
54
- export declare const addToJSON: (saveInput: addToJSONProps) => void;
76
+ export declare const logSerializationIssues: (issues: SerializationIssue[]) => void;
55
77
  export {};
@@ -1,4 +1,3 @@
1
- import { readFileSync, writeFileSync } from 'fs';
2
1
  export const isErrorWithCode = (error) => {
3
2
  return error instanceof Error && 'code' in error;
4
3
  };
@@ -8,7 +7,7 @@ export const isErrorWithMessage = (error) => {
8
7
  export const isSyntaxError = (error) => {
9
8
  return error instanceof SyntaxError;
10
9
  };
11
- const getDate = () => {
10
+ export const getDate = () => {
12
11
  const castedDate = new Date();
13
12
  return castedDate.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
14
13
  };
@@ -26,7 +25,7 @@ const pathJoin = (basePath, key) => {
26
25
  ? `${basePath}.${key}`
27
26
  : `${basePath}[${JSON.stringify(key)}]`;
28
27
  };
29
- const getIssueMessage = (issue) => {
28
+ export const getIssueMessage = (issue) => {
30
29
  if (issue.kind === 'circular') {
31
30
  return `Field "${issue.path}" contains circular reference${issue.circularTo ? ` to "${issue.circularTo}"` : ''}`;
32
31
  }
@@ -144,16 +143,16 @@ const typeFlagFromValue = (value) => {
144
143
  return String(t);
145
144
  };
146
145
  export const sanitizeNonSerializable = (value, path = '$', ancestors = new WeakMap()) => {
147
- if (value === null || typeof value === 'string' || typeof value === 'boolean') {
146
+ if (value === null) {
147
+ return null;
148
+ }
149
+ if (typeof value === 'string' || typeof value === 'boolean') {
148
150
  return value;
149
151
  }
150
152
  if (typeof value === 'number') {
151
153
  return Number.isFinite(value) ? value : 'non-finite-number';
152
154
  }
153
- if (typeof value === 'undefined' ||
154
- typeof value === 'function' ||
155
- typeof value === 'symbol' ||
156
- typeof value === 'bigint') {
155
+ if (['undefined', 'function', 'symbol', 'bigint'].includes(typeof value)) {
157
156
  return typeFlagFromValue(value);
158
157
  }
159
158
  if (Array.isArray(value)) {
@@ -179,125 +178,8 @@ export const sanitizeNonSerializable = (value, path = '$', ancestors = new WeakM
179
178
  }
180
179
  return typeFlagFromValue(value);
181
180
  };
182
- const logSerializationIssues = (issues) => {
181
+ export const logSerializationIssues = (issues) => {
183
182
  for (const issue of issues) {
184
183
  console.info(`[${getDate()}] Boma serialization info: ${getIssueMessage(issue)}`);
185
184
  }
186
185
  };
187
- export const saveJSON = (saveInput) => {
188
- const { filePath, objToSave, format = false, logSaving = false, replaceNonSerializable = false, silent = true, } = saveInput;
189
- const issues = getSerializationIssues(objToSave);
190
- if (issues.length > 0) {
191
- logSerializationIssues(issues);
192
- if (!replaceNonSerializable) {
193
- const firstIssue = issues[0];
194
- const errorMessage = `[${getDate()}] Boma got non JSON-serializable object at saveJSON! ${getIssueMessage(firstIssue)}`;
195
- console.error(errorMessage);
196
- throw new Error(errorMessage);
197
- }
198
- }
199
- const valueToSave = replaceNonSerializable && issues.length > 0
200
- ? sanitizeNonSerializable(objToSave)
201
- : objToSave;
202
- try {
203
- const json = format ? JSON.stringify(valueToSave, null, 2) : JSON.stringify(valueToSave);
204
- writeFileSync(filePath, json, 'utf8');
205
- !silent && logSaving && console.log(`[${getDate()}] Write file ${filePath} successfully`);
206
- }
207
- catch (error) {
208
- if (isErrorWithCode(error)) {
209
- console.error(`[${getDate()}] Boma got filesystem error for ${filePath}:`, error);
210
- }
211
- throw error;
212
- }
213
- };
214
- export const readJSON = (props) => {
215
- const { filePath, createIfNotFound = false, parseJSON = true, silent = true } = props;
216
- try {
217
- const savedfile = readFileSync(filePath, 'utf8');
218
- if (parseJSON) {
219
- return savedfile.trim() === '' ? null : JSON.parse(savedfile);
220
- }
221
- return savedfile;
222
- }
223
- catch (err) {
224
- if (isErrorWithCode(err)) {
225
- switch (err.code) {
226
- case 'ENOENT':
227
- if (createIfNotFound) {
228
- !silent && console.log('Try to create: ', filePath);
229
- try {
230
- const initialContent = typeof createIfNotFound === 'boolean' ? '{}' : JSON.stringify(createIfNotFound);
231
- writeFileSync(filePath, initialContent, 'utf8');
232
- }
233
- catch (writeErr) {
234
- console.error(`Error creating file ${filePath}: `, writeErr, '\n');
235
- }
236
- return typeof createIfNotFound === 'boolean' ? {} : createIfNotFound;
237
- }
238
- console.info("Boma can't find file: ", filePath);
239
- return parseJSON ? {} : null;
240
- case 'EACCES':
241
- console.error(`Access denied for ${filePath}`);
242
- return null;
243
- default:
244
- console.error(`Some filesystem error when try readJSON: ${filePath}`);
245
- return null;
246
- }
247
- }
248
- if (isSyntaxError(err)) {
249
- console.error('File ', filePath, ' has incorrect JSON syntax', '\n');
250
- return null;
251
- }
252
- console.error('Function readJSON error:', err, '\n');
253
- return null;
254
- }
255
- };
256
- export const addToJSON = (saveInput) => {
257
- const { filePath, dataToAdd, format = false, logSaving = false, replaceNonSerializable = false, silent = true, } = saveInput;
258
- const oldJSON = readJSON({ filePath, createIfNotFound: true, silent });
259
- if (oldJSON === null || typeof oldJSON !== 'object') {
260
- return saveJSON({
261
- filePath,
262
- objToSave: dataToAdd,
263
- format,
264
- logSaving,
265
- replaceNonSerializable,
266
- silent
267
- });
268
- }
269
- if (Array.isArray(oldJSON) && Array.isArray(dataToAdd)) {
270
- saveJSON({
271
- filePath,
272
- objToSave: [...oldJSON, ...dataToAdd],
273
- format,
274
- logSaving,
275
- replaceNonSerializable,
276
- silent
277
- });
278
- return;
279
- }
280
- if (isPlainMergeableObject(oldJSON) && isPlainMergeableObject(dataToAdd)) {
281
- saveJSON({
282
- filePath,
283
- objToSave: { ...oldJSON, ...dataToAdd },
284
- format,
285
- logSaving,
286
- replaceNonSerializable,
287
- silent
288
- });
289
- return;
290
- }
291
- if (Array.isArray(dataToAdd) && isPlainMergeableObject(oldJSON) && Object.keys(oldJSON).length === 0) {
292
- saveJSON({
293
- filePath,
294
- objToSave: [...dataToAdd],
295
- format,
296
- logSaving,
297
- replaceNonSerializable,
298
- silent
299
- });
300
- return;
301
- }
302
- throw new Error('Cannot merge array with object');
303
- };
package/package.json CHANGED
@@ -1,9 +1,16 @@
1
1
  {
2
2
  "name": "boma",
3
- "version": "2.0.5",
4
- "description": "Primitive JSON helper",
5
- "main": "./dist/index.js",
6
- "types": "./dist/index.d.ts",
3
+ "version": "2.1.0",
4
+ "description": "Simple JSON helper",
5
+ "main": "./dist/boma.js",
6
+ "types": "./dist/boma.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/boma.d.ts",
10
+ "import": "./dist/boma.js",
11
+ "default": "./dist/boma.js"
12
+ }
13
+ },
7
14
  "type": "module",
8
15
  "scripts": {
9
16
  "build": "tsc",
@@ -13,13 +20,7 @@
13
20
  "type": "git",
14
21
  "url": "git+https://github.com/Psychosynthesis/Boma.git"
15
22
  },
16
- "exports": {
17
- ".": {
18
- "types": "./dist/index.d.ts",
19
- "import": "./dist/index.js",
20
- "default": "./dist/index.js"
21
- }
22
- },
23
+ "files": [ "dist", "readme.md", "LICENSE" ],
23
24
  "author": "Nick G.",
24
25
  "license": "MIT",
25
26
  "bugs": {
package/readme.md CHANGED
@@ -1,88 +1,456 @@
1
1
  # Boma JSON helper
2
- Super-simple helper for working with JSON
3
2
 
4
- Work only in synchronous mode for now.
3
+ Super-simple helper for reading, saving and updating JSON files.
5
4
 
6
- # Usage
5
+ Works in synchronous mode by default.
6
+ Pass `async: true` to use the asynchronous implementation.
7
7
 
8
- ```JS
8
+ Old synchronous calls remain unchanged.
9
+
10
+ # Install
11
+
12
+ ```bash
13
+ npm install boma
14
+ ```
15
+
16
+ # Import
17
+
18
+ ```typescript
19
+ import { readJSON, saveJSON, addToJSON } from 'boma';
20
+ ```
21
+
22
+ # Synchronous usage
23
+
24
+ Without the `async` option, all functions work synchronously.
25
+
26
+ ```typescript
9
27
  const logs = readJSON({
10
- filePath: '/support.log',
28
+ filePath: '/support.json',
11
29
  createIfNotFound: {},
12
30
  parseJSON: true,
13
- silent: true, // Do not log warnings (only file not found will be logged)
31
+ silent: true, // Do not log warnings
14
32
  });
33
+
15
34
  /*
16
35
  logs = {
17
- any: {}, //
36
+ any: {},
18
37
  key: {}
19
38
  }
20
39
  */
40
+ ```
21
41
 
42
+ ```typescript
22
43
  saveJSON({
23
44
  filePath: '/test.json',
24
- objToSave: { any: {}, key: {} },
45
+ objToSave: {
46
+ any: {},
47
+ key: {},
48
+ },
25
49
  format: true,
26
50
  logSaving: false,
27
- silent: true, // Do not log warnings
28
- }); // Format for save line break's
29
- /* Will save:
51
+ silent: true,
52
+ });
53
+
54
+ /*
55
+ Will save:
56
+
30
57
  {
31
- "any": {}, // saved line break's
58
+ "any": {},
32
59
  "key": {}
33
60
  }
34
61
  */
62
+ ```
35
63
 
64
+ ```typescript
36
65
  addToJSON({
37
- filePath: '/support.log',
38
- dataToAdd: { 6sdf89g7dghg: { any: "data", someFunc: () => {} }},
66
+ filePath: '/support.json',
67
+ dataToAdd: {
68
+ '6sdf89g7dghg': {
69
+ any: 'data',
70
+ someFunc: () => {},
71
+ },
72
+ },
39
73
  format: false,
40
74
  logSaving: false,
41
- replaceNonSerializable: true
75
+ replaceNonSerializable: true,
42
76
  });
43
- /* Will save:
44
- "{ "6sdf89g7dghg": { "any": "data", "someFunc": "function" } }"
77
+
78
+ /*
79
+ Will save:
80
+
81
+ {
82
+ "6sdf89g7dghg": {
83
+ "any": "data",
84
+ "someFunc": "function"
85
+ }
86
+ }
45
87
  */
46
88
  ```
47
89
 
48
- ## Types
90
+ # Asynchronous usage
91
+
92
+ Pass `async: true` to any main function.
93
+
94
+ In this mode:
95
+
96
+ * `readJSON` returns a `Promise` with the read value;
97
+ * `saveJSON` returns `Promise<void>`;
98
+ * `addToJSON` returns `Promise<void>`.
99
+
100
+ ```typescript
101
+ const logs = await readJSON({
102
+ filePath: '/support.json',
103
+ createIfNotFound: {},
104
+ parseJSON: true,
105
+ silent: true,
106
+ async: true,
107
+ });
108
+ ```
109
+
110
+ ```typescript
111
+ await saveJSON({
112
+ filePath: '/test.json',
113
+ objToSave: {
114
+ any: {},
115
+ key: {},
116
+ },
117
+ format: true,
118
+ logSaving: false,
119
+ silent: true,
120
+ async: true,
121
+ });
122
+ ```
123
+
124
+ ```typescript
125
+ await addToJSON({
126
+ filePath: '/support.json',
127
+ dataToAdd: {
128
+ newKey: {
129
+ any: 'data',
130
+ },
131
+ },
132
+ format: true,
133
+ silent: true,
134
+ async: true,
135
+ });
136
+ ```
137
+
138
+ The same functions are used in both modes. Separate async imports are not required.
139
+
140
+ # Reading JSON
141
+
142
+ ```typescript
143
+ const result = readJSON({
144
+ filePath: '/test.json',
145
+ });
146
+ ```
147
+
148
+ Default options:
149
+
150
+ ```typescript
151
+ {
152
+ parseJSON: true,
153
+ createIfNotFound: false,
154
+ silent: true,
155
+ async: false
156
+ }
157
+ ```
158
+
159
+ When `parseJSON` is `true`, file content is parsed using `JSON.parse`.
160
+
161
+ When `parseJSON` is `false`, raw file content is returned as a string.
162
+
163
+ ```typescript
164
+ const rawContent = readJSON({
165
+ filePath: '/test.json',
166
+ parseJSON: false,
167
+ });
168
+ ```
169
+
170
+ When `createIfNotFound` is `true`, a missing file is created with an empty object:
171
+
172
+ ```typescript
173
+ const result = readJSON({
174
+ filePath: '/test.json',
175
+ createIfNotFound: true,
176
+ });
177
+ ```
178
+
179
+ You can also provide the initial object or array:
180
+
181
+ ```typescript
182
+ const result = readJSON({
183
+ filePath: '/test.json',
184
+ createIfNotFound: {
185
+ created: true,
186
+ },
187
+ });
188
+ ```
189
+
190
+ # Saving JSON
191
+
192
+ ```typescript
193
+ saveJSON({
194
+ filePath: '/test.json',
195
+ objToSave: {
196
+ any: 'data',
197
+ },
198
+ });
199
+ ```
200
+
201
+ Use `format: true` to save formatted JSON with indentation and line breaks:
202
+
203
+ ```typescript
204
+ saveJSON({
205
+ filePath: '/test.json',
206
+ objToSave: {
207
+ any: 'data',
208
+ },
209
+ format: true,
210
+ });
211
+ ```
212
+
213
+ Use `logSaving: true` to log successful file saving when `silent` is `false`.
214
+
215
+ # Non-serializable values
216
+
217
+ JSON does not support values such as:
218
+
219
+ * `undefined`;
220
+ * functions;
221
+ * symbols;
222
+ * `bigint`;
223
+ * `NaN` and `Infinity`;
224
+ * circular references.
225
+
226
+ Use `replaceNonSerializable: true` to replace such values with string type flags:
227
+
228
+ ```typescript
229
+ saveJSON({
230
+ filePath: '/test.json',
231
+ objToSave: {
232
+ callback: () => {},
233
+ missing: undefined,
234
+ largeNumber: BigInt(10),
235
+ invalidNumber: NaN,
236
+ },
237
+ replaceNonSerializable: true,
238
+ format: true,
239
+ });
240
+ ```
241
+
242
+ Will save:
243
+
244
+ ```json
245
+ {
246
+ "callback": "function",
247
+ "missing": "undefined",
248
+ "largeNumber": "bigint",
249
+ "invalidNumber": "non-finite-number"
250
+ }
251
+ ```
252
+
253
+ Circular references are replaced with `"circular"`.
254
+
255
+ # Adding data to JSON
256
+
257
+ `addToJSON` reads the existing file, merges the new data and saves the result.
258
+
259
+ Objects are shallow merged:
260
+
261
+ ```typescript
262
+ // Existing file:
263
+ {
264
+ "first": 1,
265
+ "second": 2
266
+ }
267
+ ```
268
+
269
+ ```typescript
270
+ addToJSON({
271
+ filePath: '/test.json',
272
+ dataToAdd: {
273
+ second: 20,
274
+ third: 3,
275
+ },
276
+ });
277
+ ```
278
+
279
+ Result:
280
+
281
+ ```json
282
+ {
283
+ "first": 1,
284
+ "second": 20,
285
+ "third": 3
286
+ }
287
+ ```
288
+
289
+ Existing object keys are overwritten by values from `dataToAdd`.
290
+
291
+ Arrays are concatenated:
292
+
293
+ ```typescript
294
+ // Existing file:
295
+ [1, 2]
296
+ ```
297
+
298
+ ```typescript
299
+ addToJSON({
300
+ filePath: '/test.json',
301
+ dataToAdd: [3, 4],
302
+ });
303
+ ```
304
+
305
+ Result:
306
+
307
+ ```json
308
+ [1, 2, 3, 4]
309
+ ```
310
+
311
+ An object cannot be merged with an array. In this case, `addToJSON` throws:
312
+
313
+ ```text
314
+ Cannot merge array with object
315
+ ```
316
+
317
+ A missing file is created automatically.
318
+
319
+ # Typed reading
320
+
321
+ A result type can be passed to `readJSON`:
322
+
323
+ ```typescript
324
+ interface Config {
325
+ port: number;
326
+ production: boolean;
327
+ }
328
+
329
+ const config = readJSON<Config>({
330
+ filePath: '/config.json',
331
+ });
332
+ ```
333
+
334
+ Async mode uses the same generic:
335
+
336
+ ```typescript
337
+ const config = await readJSON<Config>({
338
+ filePath: '/config.json',
339
+ async: true,
340
+ });
341
+ ```
342
+
343
+ Because `parseJSON: false` returns a string and reading errors can return `null`, the complete result type also includes `string | null`.
344
+
345
+ # Types
346
+
49
347
  Main types:
50
348
 
51
349
  ```typescript
52
350
  export type SerializablePrimitive = string | number | boolean | null;
53
351
  export type SerializableArray = Serializable[];
54
- export type SerializableObject = { [key: string]: Serializable };
55
- export type Serializable = SerializablePrimitive | SerializableArray | SerializableObject;
352
+ export type SerializableObject = {
353
+ [key: string]: Serializable;
354
+ };
355
+
356
+ export type Serializable =
357
+ | SerializablePrimitive
358
+ | SerializableArray
359
+ | SerializableObject;
56
360
 
57
361
  export interface ReadJSONProps {
58
362
  filePath: string;
59
363
  parseJSON?: boolean;
60
- createIfNotFound?: boolean | SerializableObject | SerializableArray; // Файл с чем создать, если не создан
364
+ createIfNotFound?:
365
+ | boolean
366
+ | SerializableObject
367
+ | SerializableArray;
368
+ silent?: boolean;
369
+ async?: boolean;
61
370
  }
62
371
 
63
372
  export interface SaveJSONProps {
64
373
  filePath: string;
65
- objToSave: Serializable;
66
- format?: boolean
374
+ objToSave: unknown;
375
+ format?: boolean;
67
376
  logSaving?: boolean;
68
- replaceNonSerializable?: boolean; // Заменять несереализуемые значения их типом
69
- silent?: boolean; // Не выводим предупреждения
377
+ replaceNonSerializable?: boolean;
378
+ silent?: boolean;
379
+ async?: boolean;
70
380
  }
71
381
 
72
382
  export interface addToJSONProps {
73
383
  filePath: string;
74
- dataToAdd: SerializableObject | SerializableArray;
75
- format?: boolean
384
+ dataToAdd:
385
+ | Record<string, unknown>
386
+ | unknown[];
387
+ format?: boolean;
76
388
  logSaving?: boolean;
77
- silent?: boolean; // Не выводим предупреждения
389
+ replaceNonSerializable?: boolean;
390
+ silent?: boolean;
391
+ async?: boolean;
78
392
  }
393
+ ```
394
+
395
+ Types used for synchronous and asynchronous overloads are also exported:
396
+
397
+ ```typescript
398
+ export type ReadJSONSyncProps =
399
+ ReadJSONProps & { async?: false };
400
+
401
+ export type ReadJSONAsyncProps =
402
+ ReadJSONProps & { async: true };
403
+
404
+ export type SaveJSONSyncProps =
405
+ SaveJSONProps & { async?: false };
406
+
407
+ export type SaveJSONAsyncProps =
408
+ SaveJSONProps & { async: true };
409
+
410
+ export type AddToJSONSyncProps =
411
+ addToJSONProps & { async?: false };
412
+
413
+ export type AddToJSONAsyncProps =
414
+ addToJSONProps & { async: true };
415
+
416
+ export type ReadJSONResult<T = any> =
417
+ T | string | null;
418
+ ```
419
+
420
+ # Helpers
79
421
 
80
- // We also export some helpers:
81
- export const isSerializable = (value: unknown) => boolean;
82
- export const isObjectLike = (value: unknown) => boolean;
83
- export const isPlainMergeableObject = (value: unknown) => boolean;
422
+ The package also exports serialization and type-checking helpers:
84
423
 
85
- export const isErrorWithCode = (error: unknown) => boolean;
86
- export const isErrorWithMessage = (error: unknown) => boolean;
87
- export const isSyntaxError = (error: unknown) => boolean;
424
+ ```typescript
425
+ isSerializable(value);
426
+ sanitizeNonSerializable(value);
427
+ getSerializationIssues(value);
428
+
429
+ isObjectLike(value);
430
+ isPlainMergeableObject(value);
431
+
432
+ isErrorWithCode(error);
433
+ isErrorWithMessage(error);
434
+ isSyntaxError(error);
435
+ ```
436
+
437
+ `getSerializationIssues` returns all detected serialization problems with their object paths:
438
+
439
+ ```typescript
440
+ const issues = getSerializationIssues({
441
+ user: {
442
+ callback: () => {},
443
+ },
444
+ });
445
+
446
+ /*
447
+ [
448
+ {
449
+ path: '[OBJECT].user.callback',
450
+ kind: 'function',
451
+ message:
452
+ 'Field "[OBJECT].user.callback" has non-serializable value of type "function"'
453
+ }
454
+ ]
455
+ */
88
456
  ```