boma 2.0.6 → 2.1.1

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