boma 2.0.3 → 2.0.5
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/index.d.ts +22 -3
- package/dist/index.js +224 -41
- package/package.json +1 -1
- package/readme.md +35 -4
package/dist/index.d.ts
CHANGED
|
@@ -4,22 +4,29 @@ export type SerializableObject = {
|
|
|
4
4
|
[key: string]: Serializable;
|
|
5
5
|
};
|
|
6
6
|
export type Serializable = SerializablePrimitive | SerializableArray | SerializableObject;
|
|
7
|
+
type JSONLikeObject = Record<string, unknown>;
|
|
8
|
+
type JSONLikeArray = unknown[];
|
|
7
9
|
export interface ReadJSONProps {
|
|
8
10
|
filePath: string;
|
|
9
11
|
parseJSON?: boolean;
|
|
10
12
|
createIfNotFound?: boolean | SerializableObject | SerializableArray;
|
|
13
|
+
silent?: boolean;
|
|
11
14
|
}
|
|
12
15
|
export interface SaveJSONProps {
|
|
13
16
|
filePath: string;
|
|
14
|
-
objToSave:
|
|
17
|
+
objToSave: unknown;
|
|
15
18
|
format?: boolean;
|
|
16
19
|
logSaving?: boolean;
|
|
20
|
+
replaceNonSerializable?: boolean;
|
|
21
|
+
silent?: boolean;
|
|
17
22
|
}
|
|
18
23
|
export interface addToJSONProps {
|
|
19
24
|
filePath: string;
|
|
20
|
-
dataToAdd:
|
|
25
|
+
dataToAdd: JSONLikeObject | JSONLikeArray;
|
|
21
26
|
format?: boolean;
|
|
22
27
|
logSaving?: boolean;
|
|
28
|
+
replaceNonSerializable?: boolean;
|
|
29
|
+
silent?: boolean;
|
|
23
30
|
}
|
|
24
31
|
export type ErrorWithCode = Error & {
|
|
25
32
|
code: string;
|
|
@@ -27,10 +34,22 @@ export type ErrorWithCode = Error & {
|
|
|
27
34
|
export type ErrorWithMessage = Error & {
|
|
28
35
|
message: any;
|
|
29
36
|
};
|
|
37
|
+
export type SerializationIssueKind = 'undefined' | 'function' | 'symbol' | 'bigint' | 'non-finite-number' | 'circular';
|
|
38
|
+
export interface SerializationIssue {
|
|
39
|
+
path: string;
|
|
40
|
+
kind: SerializationIssueKind;
|
|
41
|
+
message: string;
|
|
42
|
+
circularTo?: string;
|
|
43
|
+
}
|
|
30
44
|
export declare const isErrorWithCode: (error: unknown) => error is ErrorWithCode;
|
|
31
45
|
export declare const isErrorWithMessage: (error: unknown) => error is ErrorWithMessage;
|
|
32
46
|
export declare const isSyntaxError: (error: unknown) => error is SyntaxError;
|
|
33
|
-
export declare const
|
|
47
|
+
export declare const isObjectLike: (value: unknown) => value is object;
|
|
48
|
+
export declare const isPlainMergeableObject: (value: unknown) => value is Record<string, unknown>;
|
|
49
|
+
export declare const getSerializationIssues: (value: unknown, path?: string, ancestors?: WeakMap<object, string>, issues?: SerializationIssue[]) => SerializationIssue[];
|
|
50
|
+
export declare const isSerializable: (value: unknown) => value is Serializable;
|
|
51
|
+
export declare const sanitizeNonSerializable: (value: unknown, path?: string, ancestors?: WeakMap<object, string>) => Serializable;
|
|
34
52
|
export declare const saveJSON: (saveInput: SaveJSONProps) => void;
|
|
35
53
|
export declare const readJSON: (props: ReadJSONProps) => any;
|
|
36
54
|
export declare const addToJSON: (saveInput: addToJSONProps) => void;
|
|
55
|
+
export {};
|
package/dist/index.js
CHANGED
|
@@ -12,57 +12,211 @@ const getDate = () => {
|
|
|
12
12
|
const castedDate = new Date();
|
|
13
13
|
return castedDate.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
|
|
14
14
|
};
|
|
15
|
-
export const
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
export const isObjectLike = (value) => {
|
|
16
|
+
return typeof value === 'object' && value !== null;
|
|
17
|
+
};
|
|
18
|
+
export const isPlainMergeableObject = (value) => {
|
|
19
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
20
|
+
};
|
|
21
|
+
const pathJoin = (basePath, key) => {
|
|
22
|
+
if (typeof key === 'number') {
|
|
23
|
+
return `${basePath}[${key}]`;
|
|
24
|
+
}
|
|
25
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
|
|
26
|
+
? `${basePath}.${key}`
|
|
27
|
+
: `${basePath}[${JSON.stringify(key)}]`;
|
|
28
|
+
};
|
|
29
|
+
const getIssueMessage = (issue) => {
|
|
30
|
+
if (issue.kind === 'circular') {
|
|
31
|
+
return `Field "${issue.path}" contains circular reference${issue.circularTo ? ` to "${issue.circularTo}"` : ''}`;
|
|
32
|
+
}
|
|
33
|
+
if (issue.kind === 'non-finite-number') {
|
|
34
|
+
return `Field "${issue.path}" contains non-finite number (NaN / Infinity)`;
|
|
35
|
+
}
|
|
36
|
+
return `Field "${issue.path}" has non-serializable value of type "${issue.kind}"`;
|
|
37
|
+
};
|
|
38
|
+
export const getSerializationIssues = (value, path = '[OBJECT]', ancestors = new WeakMap(), issues = []) => {
|
|
39
|
+
if (value === null) {
|
|
40
|
+
return issues;
|
|
41
|
+
}
|
|
42
|
+
const valueType = typeof value;
|
|
43
|
+
if (valueType === 'string' || valueType === 'boolean') {
|
|
44
|
+
return issues;
|
|
45
|
+
}
|
|
46
|
+
if (valueType === 'number') {
|
|
47
|
+
if (!Number.isFinite(value)) {
|
|
48
|
+
issues.push({
|
|
49
|
+
path,
|
|
50
|
+
kind: 'non-finite-number',
|
|
51
|
+
message: `Field "${path}" contains non-finite number (NaN / Infinity)`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return issues;
|
|
55
|
+
}
|
|
56
|
+
if (valueType === 'undefined') {
|
|
57
|
+
issues.push({
|
|
58
|
+
path,
|
|
59
|
+
kind: 'undefined',
|
|
60
|
+
message: `Field "${path}" has non-serializable value of type "undefined"`,
|
|
61
|
+
});
|
|
62
|
+
return issues;
|
|
63
|
+
}
|
|
64
|
+
if (valueType === 'function') {
|
|
65
|
+
issues.push({
|
|
66
|
+
path,
|
|
67
|
+
kind: 'function',
|
|
68
|
+
message: `Field "${path}" has non-serializable value of type "function"`,
|
|
69
|
+
});
|
|
70
|
+
return issues;
|
|
71
|
+
}
|
|
72
|
+
if (valueType === 'symbol') {
|
|
73
|
+
issues.push({
|
|
74
|
+
path,
|
|
75
|
+
kind: 'symbol',
|
|
76
|
+
message: `Field "${path}" has non-serializable value of type "symbol"`,
|
|
77
|
+
});
|
|
78
|
+
return issues;
|
|
79
|
+
}
|
|
80
|
+
if (valueType === 'bigint') {
|
|
81
|
+
issues.push({
|
|
82
|
+
path,
|
|
83
|
+
kind: 'bigint',
|
|
84
|
+
message: `Field "${path}" has non-serializable value of type "bigint"`,
|
|
85
|
+
});
|
|
86
|
+
return issues;
|
|
18
87
|
}
|
|
19
88
|
if (Array.isArray(value)) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
89
|
+
if (ancestors.has(value)) {
|
|
90
|
+
const circularTo = ancestors.get(value);
|
|
91
|
+
issues.push({
|
|
92
|
+
path,
|
|
93
|
+
kind: 'circular',
|
|
94
|
+
circularTo,
|
|
95
|
+
message: `Field "${path}" contains circular reference${circularTo ? ` to "${circularTo}"` : ''}`,
|
|
96
|
+
});
|
|
97
|
+
return issues;
|
|
24
98
|
}
|
|
25
|
-
|
|
99
|
+
ancestors.set(value, path);
|
|
100
|
+
for (let i = 0; i < value.length; i++) {
|
|
101
|
+
getSerializationIssues(value[i], pathJoin(path, i), ancestors, issues);
|
|
102
|
+
}
|
|
103
|
+
ancestors.delete(value);
|
|
104
|
+
return issues;
|
|
26
105
|
}
|
|
27
|
-
if (
|
|
28
|
-
if (
|
|
29
|
-
|
|
106
|
+
if (isObjectLike(value)) {
|
|
107
|
+
if (ancestors.has(value)) {
|
|
108
|
+
const circularTo = ancestors.get(value);
|
|
109
|
+
issues.push({
|
|
110
|
+
path,
|
|
111
|
+
kind: 'circular',
|
|
112
|
+
circularTo,
|
|
113
|
+
message: `Field "${path}" contains circular reference${circularTo ? ` to "${circularTo}"` : ''}`,
|
|
114
|
+
});
|
|
115
|
+
return issues;
|
|
30
116
|
}
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
if (!isSerializable(obj[key], seen)) {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
117
|
+
ancestors.set(value, path);
|
|
118
|
+
for (const key of Object.keys(value)) {
|
|
119
|
+
getSerializationIssues(value[key], pathJoin(path, key), ancestors, issues);
|
|
37
120
|
}
|
|
38
|
-
|
|
121
|
+
ancestors.delete(value);
|
|
122
|
+
return issues;
|
|
123
|
+
}
|
|
124
|
+
return issues;
|
|
125
|
+
};
|
|
126
|
+
export const isSerializable = (value) => {
|
|
127
|
+
return getSerializationIssues(value).length === 0;
|
|
128
|
+
};
|
|
129
|
+
const typeFlagFromValue = (value) => {
|
|
130
|
+
if (value === null)
|
|
131
|
+
return null;
|
|
132
|
+
if (Array.isArray(value))
|
|
133
|
+
return 'array';
|
|
134
|
+
const t = typeof value;
|
|
135
|
+
if (String(t) === 'number' && !Number.isFinite(value)) {
|
|
136
|
+
return 'non-finite-number';
|
|
137
|
+
}
|
|
138
|
+
else if (['undefined', 'function', 'symbol', 'bigint'].includes(String(t))) {
|
|
139
|
+
return String(t);
|
|
140
|
+
}
|
|
141
|
+
else if (String(t) === 'object') {
|
|
142
|
+
return 'object';
|
|
143
|
+
}
|
|
144
|
+
return String(t);
|
|
145
|
+
};
|
|
146
|
+
export const sanitizeNonSerializable = (value, path = '$', ancestors = new WeakMap()) => {
|
|
147
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') {
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
if (typeof value === 'number') {
|
|
151
|
+
return Number.isFinite(value) ? value : 'non-finite-number';
|
|
152
|
+
}
|
|
153
|
+
if (typeof value === 'undefined' ||
|
|
154
|
+
typeof value === 'function' ||
|
|
155
|
+
typeof value === 'symbol' ||
|
|
156
|
+
typeof value === 'bigint') {
|
|
157
|
+
return typeFlagFromValue(value);
|
|
158
|
+
}
|
|
159
|
+
if (Array.isArray(value)) {
|
|
160
|
+
if (ancestors.has(value)) {
|
|
161
|
+
return 'circular';
|
|
162
|
+
}
|
|
163
|
+
ancestors.set(value, path);
|
|
164
|
+
const result = value.map((item, index) => sanitizeNonSerializable(item, pathJoin(path, index), ancestors));
|
|
165
|
+
ancestors.delete(value);
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
if (isObjectLike(value)) {
|
|
169
|
+
if (ancestors.has(value)) {
|
|
170
|
+
return 'circular';
|
|
171
|
+
}
|
|
172
|
+
ancestors.set(value, path);
|
|
173
|
+
const result = {};
|
|
174
|
+
for (const key of Object.keys(value)) {
|
|
175
|
+
result[key] = sanitizeNonSerializable(value[key], pathJoin(path, key), ancestors);
|
|
176
|
+
}
|
|
177
|
+
ancestors.delete(value);
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
return typeFlagFromValue(value);
|
|
181
|
+
};
|
|
182
|
+
const logSerializationIssues = (issues) => {
|
|
183
|
+
for (const issue of issues) {
|
|
184
|
+
console.info(`[${getDate()}] Boma serialization info: ${getIssueMessage(issue)}`);
|
|
39
185
|
}
|
|
40
|
-
return false;
|
|
41
186
|
};
|
|
42
187
|
export const saveJSON = (saveInput) => {
|
|
43
|
-
const { filePath, objToSave, format = false, logSaving = false } = saveInput;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
+
}
|
|
47
198
|
}
|
|
199
|
+
const valueToSave = replaceNonSerializable && issues.length > 0
|
|
200
|
+
? sanitizeNonSerializable(objToSave)
|
|
201
|
+
: objToSave;
|
|
48
202
|
try {
|
|
49
|
-
const json = format ? JSON.stringify(
|
|
203
|
+
const json = format ? JSON.stringify(valueToSave, null, 2) : JSON.stringify(valueToSave);
|
|
50
204
|
writeFileSync(filePath, json, 'utf8');
|
|
51
|
-
logSaving && console.log(`[${getDate()}] Write file ${filePath} successfully`);
|
|
205
|
+
!silent && logSaving && console.log(`[${getDate()}] Write file ${filePath} successfully`);
|
|
52
206
|
}
|
|
53
207
|
catch (error) {
|
|
54
208
|
if (isErrorWithCode(error)) {
|
|
55
|
-
console.error(`[${getDate()}] Boma
|
|
209
|
+
console.error(`[${getDate()}] Boma got filesystem error for ${filePath}:`, error);
|
|
56
210
|
}
|
|
57
211
|
throw error;
|
|
58
212
|
}
|
|
59
213
|
};
|
|
60
214
|
export const readJSON = (props) => {
|
|
61
|
-
const { filePath, createIfNotFound = false, parseJSON = true } = props;
|
|
215
|
+
const { filePath, createIfNotFound = false, parseJSON = true, silent = true } = props;
|
|
62
216
|
try {
|
|
63
217
|
const savedfile = readFileSync(filePath, 'utf8');
|
|
64
218
|
if (parseJSON) {
|
|
65
|
-
return savedfile.trim() ===
|
|
219
|
+
return savedfile.trim() === '' ? null : JSON.parse(savedfile);
|
|
66
220
|
}
|
|
67
221
|
return savedfile;
|
|
68
222
|
}
|
|
@@ -71,7 +225,7 @@ export const readJSON = (props) => {
|
|
|
71
225
|
switch (err.code) {
|
|
72
226
|
case 'ENOENT':
|
|
73
227
|
if (createIfNotFound) {
|
|
74
|
-
console.log('Try to create: ', filePath);
|
|
228
|
+
!silent && console.log('Try to create: ', filePath);
|
|
75
229
|
try {
|
|
76
230
|
const initialContent = typeof createIfNotFound === 'boolean' ? '{}' : JSON.stringify(createIfNotFound);
|
|
77
231
|
writeFileSync(filePath, initialContent, 'utf8');
|
|
@@ -81,7 +235,7 @@ export const readJSON = (props) => {
|
|
|
81
235
|
}
|
|
82
236
|
return typeof createIfNotFound === 'boolean' ? {} : createIfNotFound;
|
|
83
237
|
}
|
|
84
|
-
console.
|
|
238
|
+
console.info("Boma can't find file: ", filePath);
|
|
85
239
|
return parseJSON ? {} : null;
|
|
86
240
|
case 'EACCES':
|
|
87
241
|
console.error(`Access denied for ${filePath}`);
|
|
@@ -100,21 +254,50 @@ export const readJSON = (props) => {
|
|
|
100
254
|
}
|
|
101
255
|
};
|
|
102
256
|
export const addToJSON = (saveInput) => {
|
|
103
|
-
const { filePath, dataToAdd, format = false, logSaving = false } = saveInput;
|
|
104
|
-
const oldJSON = readJSON({ filePath, createIfNotFound: true });
|
|
257
|
+
const { filePath, dataToAdd, format = false, logSaving = false, replaceNonSerializable = false, silent = true, } = saveInput;
|
|
258
|
+
const oldJSON = readJSON({ filePath, createIfNotFound: true, silent });
|
|
105
259
|
if (oldJSON === null || typeof oldJSON !== 'object') {
|
|
106
|
-
return saveJSON({
|
|
260
|
+
return saveJSON({
|
|
261
|
+
filePath,
|
|
262
|
+
objToSave: dataToAdd,
|
|
263
|
+
format,
|
|
264
|
+
logSaving,
|
|
265
|
+
replaceNonSerializable,
|
|
266
|
+
silent
|
|
267
|
+
});
|
|
107
268
|
}
|
|
108
269
|
if (Array.isArray(oldJSON) && Array.isArray(dataToAdd)) {
|
|
109
|
-
saveJSON({
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
270
|
+
saveJSON({
|
|
271
|
+
filePath,
|
|
272
|
+
objToSave: [...oldJSON, ...dataToAdd],
|
|
273
|
+
format,
|
|
274
|
+
logSaving,
|
|
275
|
+
replaceNonSerializable,
|
|
276
|
+
silent
|
|
277
|
+
});
|
|
278
|
+
return;
|
|
113
279
|
}
|
|
114
|
-
|
|
115
|
-
saveJSON({
|
|
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;
|
|
116
290
|
}
|
|
117
|
-
|
|
118
|
-
|
|
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;
|
|
119
301
|
}
|
|
302
|
+
throw new Error('Cannot merge array with object');
|
|
120
303
|
};
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
# Usage
|
|
7
7
|
|
|
8
8
|
```JS
|
|
9
|
-
const logs = readJSON({
|
|
9
|
+
const logs = readJSON({
|
|
10
|
+
filePath: '/support.log',
|
|
11
|
+
createIfNotFound: {},
|
|
12
|
+
parseJSON: true,
|
|
13
|
+
silent: true, // Do not log warnings (only file not found will be logged)
|
|
14
|
+
});
|
|
10
15
|
/*
|
|
11
16
|
logs = {
|
|
12
17
|
any: {}, //
|
|
@@ -14,7 +19,13 @@ logs = {
|
|
|
14
19
|
}
|
|
15
20
|
*/
|
|
16
21
|
|
|
17
|
-
saveJSON({
|
|
22
|
+
saveJSON({
|
|
23
|
+
filePath: '/test.json',
|
|
24
|
+
objToSave: { any: {}, key: {} },
|
|
25
|
+
format: true,
|
|
26
|
+
logSaving: false,
|
|
27
|
+
silent: true, // Do not log warnings
|
|
28
|
+
}); // Format for save line break's
|
|
18
29
|
/* Will save:
|
|
19
30
|
{
|
|
20
31
|
"any": {}, // saved line break's
|
|
@@ -22,13 +33,21 @@ saveJSON({ filePath: '/test.json', objToSave: { any: {}, key: {} }, format: true
|
|
|
22
33
|
}
|
|
23
34
|
*/
|
|
24
35
|
|
|
25
|
-
addToJSON({
|
|
36
|
+
addToJSON({
|
|
37
|
+
filePath: '/support.log',
|
|
38
|
+
dataToAdd: { 6sdf89g7dghg: { any: "data", someFunc: () => {} }},
|
|
39
|
+
format: false,
|
|
40
|
+
logSaving: false,
|
|
41
|
+
replaceNonSerializable: true
|
|
42
|
+
});
|
|
26
43
|
/* Will save:
|
|
27
|
-
"{ "
|
|
44
|
+
"{ "6sdf89g7dghg": { "any": "data", "someFunc": "function" } }"
|
|
28
45
|
*/
|
|
29
46
|
```
|
|
30
47
|
|
|
31
48
|
## Types
|
|
49
|
+
Main types:
|
|
50
|
+
|
|
32
51
|
```typescript
|
|
33
52
|
export type SerializablePrimitive = string | number | boolean | null;
|
|
34
53
|
export type SerializableArray = Serializable[];
|
|
@@ -46,6 +65,8 @@ export interface SaveJSONProps {
|
|
|
46
65
|
objToSave: Serializable;
|
|
47
66
|
format?: boolean
|
|
48
67
|
logSaving?: boolean;
|
|
68
|
+
replaceNonSerializable?: boolean; // Заменять несереализуемые значения их типом
|
|
69
|
+
silent?: boolean; // Не выводим предупреждения
|
|
49
70
|
}
|
|
50
71
|
|
|
51
72
|
export interface addToJSONProps {
|
|
@@ -53,5 +74,15 @@ export interface addToJSONProps {
|
|
|
53
74
|
dataToAdd: SerializableObject | SerializableArray;
|
|
54
75
|
format?: boolean
|
|
55
76
|
logSaving?: boolean;
|
|
77
|
+
silent?: boolean; // Не выводим предупреждения
|
|
56
78
|
}
|
|
79
|
+
|
|
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;
|
|
84
|
+
|
|
85
|
+
export const isErrorWithCode = (error: unknown) => boolean;
|
|
86
|
+
export const isErrorWithMessage = (error: unknown) => boolean;
|
|
87
|
+
export const isSyntaxError = (error: unknown) => boolean;
|
|
57
88
|
```
|