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.
- package/dist/async.d.ts +4 -0
- package/dist/async.js +150 -0
- package/dist/boma.d.ts +11 -0
- package/dist/boma.js +130 -0
- package/dist/{index.d.ts → common.d.ts} +27 -3
- package/dist/{index.js → common.js} +10 -122
- package/package.json +12 -11
- package/readme.md +416 -35
package/dist/async.d.ts
ADDED
|
@@ -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
|
|
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
|
|
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.
|
|
4
|
-
"description": "
|
|
5
|
-
"main": "./dist/
|
|
6
|
-
"types": "./dist/
|
|
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
|
-
"
|
|
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
|
-
|
|
3
|
+
[](https://www.npmjs.org/package/boma)
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
Super-simple helper for reading, saving and updating JSON files.
|
|
7
6
|
|
|
8
|
-
|
|
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.
|
|
41
|
+
filePath: '/support.json',
|
|
11
42
|
createIfNotFound: {},
|
|
12
43
|
parseJSON: true,
|
|
13
|
-
silent: true, // Do not log warnings
|
|
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: {
|
|
58
|
+
objToSave: {
|
|
59
|
+
any: {},
|
|
60
|
+
key: {},
|
|
61
|
+
},
|
|
25
62
|
format: true,
|
|
26
63
|
logSaving: false,
|
|
27
|
-
silent: true,
|
|
28
|
-
});
|
|
29
|
-
|
|
64
|
+
silent: true,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/*
|
|
68
|
+
Will save:
|
|
69
|
+
|
|
30
70
|
{
|
|
31
|
-
"any": {},
|
|
71
|
+
"any": {},
|
|
32
72
|
"key": {}
|
|
33
73
|
}
|
|
34
74
|
*/
|
|
75
|
+
```
|
|
35
76
|
|
|
77
|
+
```typescript
|
|
36
78
|
addToJSON({
|
|
37
|
-
filePath: '/support.
|
|
38
|
-
dataToAdd: {
|
|
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
|
-
|
|
44
|
-
|
|
90
|
+
|
|
91
|
+
/*
|
|
92
|
+
Will save:
|
|
93
|
+
|
|
94
|
+
{
|
|
95
|
+
"6sdf89g7dghg": {
|
|
96
|
+
"any": "data",
|
|
97
|
+
"someFunc": "function"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
45
100
|
*/
|
|
46
101
|
```
|
|
47
102
|
|
|
48
|
-
|
|
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 = {
|
|
55
|
-
|
|
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?:
|
|
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:
|
|
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:
|
|
75
|
-
|
|
397
|
+
dataToAdd:
|
|
398
|
+
| Record<string, unknown>
|
|
399
|
+
| unknown[];
|
|
400
|
+
format?: boolean;
|
|
76
401
|
logSaving?: boolean;
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
```
|