boma 2.1.0 → 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.js +52 -21
- package/dist/boma.js +22 -15
- package/dist/common.d.ts +2 -0
- package/dist/common.js +3 -0
- package/package.json +1 -1
- package/readme.md +13 -0
package/dist/async.js
CHANGED
|
@@ -1,14 +1,30 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'fs/promises';
|
|
2
|
+
import { resolve } from 'path';
|
|
2
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
|
+
};
|
|
3
20
|
export const saveJSONAsync = async (saveInput) => {
|
|
4
21
|
const { filePath, objToSave, format = false, logSaving = false, replaceNonSerializable = false, silent = true, } = saveInput;
|
|
5
22
|
const issues = getSerializationIssues(objToSave);
|
|
6
23
|
if (issues.length > 0) {
|
|
7
24
|
!silent && logSerializationIssues(issues);
|
|
8
|
-
if (!replaceNonSerializable
|
|
25
|
+
if (!replaceNonSerializable) {
|
|
9
26
|
const firstIssue = issues[0];
|
|
10
27
|
const errorMessage = `[${getDate()}] Boma got non JSON-serializable object at saveJSON! ${getIssueMessage(firstIssue)}`;
|
|
11
|
-
console.error(errorMessage);
|
|
12
28
|
throw new Error(errorMessage);
|
|
13
29
|
}
|
|
14
30
|
}
|
|
@@ -28,7 +44,7 @@ export const saveJSONAsync = async (saveInput) => {
|
|
|
28
44
|
}
|
|
29
45
|
};
|
|
30
46
|
export const readJSONAsync = async (props) => {
|
|
31
|
-
const { filePath, createIfNotFound = false, parseJSON = true, silent = true } = props;
|
|
47
|
+
const { filePath, createIfNotFound = false, parseJSON = true, silent = true, throwError = false, } = props;
|
|
32
48
|
try {
|
|
33
49
|
const savedfile = await readFile(filePath, 'utf8');
|
|
34
50
|
if (parseJSON) {
|
|
@@ -37,41 +53,54 @@ export const readJSONAsync = async (props) => {
|
|
|
37
53
|
return savedfile;
|
|
38
54
|
}
|
|
39
55
|
catch (err) {
|
|
56
|
+
if (throwError)
|
|
57
|
+
throw err;
|
|
40
58
|
if (isErrorWithCode(err)) {
|
|
41
59
|
switch (err.code) {
|
|
42
60
|
case 'ENOENT':
|
|
43
61
|
if (createIfNotFound) {
|
|
44
62
|
!silent && console.log('Try to create: ', filePath);
|
|
63
|
+
const initialValue = typeof createIfNotFound === 'boolean' ? {} : createIfNotFound;
|
|
64
|
+
const initialContent = JSON.stringify(initialValue);
|
|
45
65
|
try {
|
|
46
|
-
|
|
47
|
-
await writeFile(filePath, initialContent, 'utf8');
|
|
66
|
+
await writeFile(filePath, initialContent, { encoding: 'utf8', flag: 'wx' });
|
|
48
67
|
}
|
|
49
68
|
catch (writeErr) {
|
|
50
|
-
|
|
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');
|
|
51
75
|
}
|
|
52
|
-
return (
|
|
76
|
+
return (parseJSON ? initialValue : initialContent);
|
|
53
77
|
}
|
|
54
|
-
console.info("Boma can't find file: ", filePath);
|
|
78
|
+
!silent && console.info("Boma can't find file: ", filePath);
|
|
55
79
|
return parseJSON ? {} : null;
|
|
56
80
|
case 'EACCES':
|
|
57
|
-
console.error(`Access denied for ${filePath}`);
|
|
81
|
+
!silent && console.error(`[${getDate()}] Access denied error for ${filePath}`);
|
|
58
82
|
return null;
|
|
59
83
|
default:
|
|
60
|
-
console.error(`
|
|
84
|
+
!silent && console.error(`[${getDate()}] Boma get some filesystem error when try readJSON: ${filePath}`, err);
|
|
61
85
|
return null;
|
|
62
86
|
}
|
|
63
87
|
}
|
|
64
88
|
if (isSyntaxError(err)) {
|
|
65
|
-
console.error('File ', filePath, ' has incorrect JSON syntax', '\n');
|
|
89
|
+
!silent && console.error('File ', filePath, ' has incorrect JSON syntax', '\n');
|
|
66
90
|
return null;
|
|
67
91
|
}
|
|
68
|
-
console.error('Function readJSON error:', err, '\n');
|
|
92
|
+
!silent && console.error('Function readJSON error:', err, '\n');
|
|
69
93
|
return null;
|
|
70
94
|
}
|
|
71
95
|
};
|
|
72
|
-
export const addToJSONAsync = async (
|
|
73
|
-
const { filePath, dataToAdd, format = false, logSaving = false, replaceNonSerializable = false, silent = true, } = saveInput;
|
|
74
|
-
const oldJSON = await readJSONAsync({
|
|
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
|
+
});
|
|
75
104
|
if (oldJSON === null || typeof oldJSON !== 'object') {
|
|
76
105
|
return saveJSONAsync({
|
|
77
106
|
filePath,
|
|
@@ -79,7 +108,7 @@ export const addToJSONAsync = async (saveInput) => {
|
|
|
79
108
|
format,
|
|
80
109
|
logSaving,
|
|
81
110
|
replaceNonSerializable,
|
|
82
|
-
silent
|
|
111
|
+
silent,
|
|
83
112
|
});
|
|
84
113
|
}
|
|
85
114
|
if (Array.isArray(oldJSON) && Array.isArray(dataToAdd)) {
|
|
@@ -89,7 +118,7 @@ export const addToJSONAsync = async (saveInput) => {
|
|
|
89
118
|
format,
|
|
90
119
|
logSaving,
|
|
91
120
|
replaceNonSerializable,
|
|
92
|
-
silent
|
|
121
|
+
silent,
|
|
93
122
|
});
|
|
94
123
|
return;
|
|
95
124
|
}
|
|
@@ -100,20 +129,22 @@ export const addToJSONAsync = async (saveInput) => {
|
|
|
100
129
|
format,
|
|
101
130
|
logSaving,
|
|
102
131
|
replaceNonSerializable,
|
|
103
|
-
silent
|
|
132
|
+
silent,
|
|
104
133
|
});
|
|
105
134
|
return;
|
|
106
135
|
}
|
|
107
|
-
if (Array.isArray(dataToAdd)
|
|
136
|
+
if (Array.isArray(dataToAdd)
|
|
137
|
+
&& isPlainMergeableObject(oldJSON)
|
|
138
|
+
&& Object.keys(oldJSON).length === 0) {
|
|
108
139
|
await saveJSONAsync({
|
|
109
140
|
filePath,
|
|
110
141
|
objToSave: [...dataToAdd],
|
|
111
142
|
format,
|
|
112
143
|
logSaving,
|
|
113
144
|
replaceNonSerializable,
|
|
114
|
-
silent
|
|
145
|
+
silent,
|
|
115
146
|
});
|
|
116
147
|
return;
|
|
117
148
|
}
|
|
118
149
|
throw new Error('Cannot merge array with object');
|
|
119
|
-
};
|
|
150
|
+
});
|
package/dist/boma.js
CHANGED
|
@@ -3,14 +3,13 @@ import { addToJSONAsync, readJSONAsync, saveJSONAsync } from './async.js';
|
|
|
3
3
|
import { getDate, getIssueMessage, getSerializationIssues, isErrorWithCode, isPlainMergeableObject, isSyntaxError, logSerializationIssues, sanitizeNonSerializable } from './common.js';
|
|
4
4
|
export * from './common.js';
|
|
5
5
|
const saveJSONSync = (saveInput) => {
|
|
6
|
-
const { filePath, objToSave, format = false, logSaving = false, replaceNonSerializable = false, silent = true
|
|
6
|
+
const { filePath, objToSave, format = false, logSaving = false, replaceNonSerializable = false, silent = true } = saveInput;
|
|
7
7
|
const issues = getSerializationIssues(objToSave);
|
|
8
8
|
if (issues.length > 0) {
|
|
9
9
|
!silent && logSerializationIssues(issues);
|
|
10
|
-
if (!replaceNonSerializable
|
|
10
|
+
if (!replaceNonSerializable) {
|
|
11
11
|
const firstIssue = issues[0];
|
|
12
12
|
const errorMessage = `[${getDate()}] Boma got non JSON-serializable object at saveJSON! ${getIssueMessage(firstIssue)}`;
|
|
13
|
-
console.error(errorMessage);
|
|
14
13
|
throw new Error(errorMessage);
|
|
15
14
|
}
|
|
16
15
|
}
|
|
@@ -30,7 +29,7 @@ const saveJSONSync = (saveInput) => {
|
|
|
30
29
|
}
|
|
31
30
|
};
|
|
32
31
|
const readJSONSync = (props) => {
|
|
33
|
-
const { filePath, createIfNotFound = false, parseJSON = true, silent = true } = props;
|
|
32
|
+
const { filePath, createIfNotFound = false, parseJSON = true, silent = true, throwError = false, } = props;
|
|
34
33
|
try {
|
|
35
34
|
const savedfile = readFileSync(filePath, 'utf8');
|
|
36
35
|
if (parseJSON) {
|
|
@@ -39,41 +38,49 @@ const readJSONSync = (props) => {
|
|
|
39
38
|
return savedfile;
|
|
40
39
|
}
|
|
41
40
|
catch (err) {
|
|
41
|
+
if (throwError)
|
|
42
|
+
throw err;
|
|
42
43
|
if (isErrorWithCode(err)) {
|
|
43
44
|
switch (err.code) {
|
|
44
45
|
case 'ENOENT':
|
|
45
46
|
if (createIfNotFound) {
|
|
46
47
|
!silent && console.log('Try to create: ', filePath);
|
|
48
|
+
const initialValue = typeof createIfNotFound === 'boolean' ? {} : createIfNotFound;
|
|
49
|
+
const initialContent = JSON.stringify(initialValue);
|
|
47
50
|
try {
|
|
48
|
-
|
|
49
|
-
writeFileSync(filePath, initialContent, 'utf8');
|
|
51
|
+
writeFileSync(filePath, initialContent, { encoding: 'utf8', flag: 'wx' });
|
|
50
52
|
}
|
|
51
53
|
catch (writeErr) {
|
|
52
|
-
|
|
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');
|
|
53
60
|
}
|
|
54
|
-
return (
|
|
61
|
+
return (parseJSON ? initialValue : initialContent);
|
|
55
62
|
}
|
|
56
|
-
console.info("Boma can't find file: ", filePath);
|
|
63
|
+
!silent && console.info("Boma can't find file: ", filePath);
|
|
57
64
|
return parseJSON ? {} : null;
|
|
58
65
|
case 'EACCES':
|
|
59
|
-
console.error(`Access denied for ${filePath}`);
|
|
66
|
+
!silent && console.error(`Access denied for ${filePath}`);
|
|
60
67
|
return null;
|
|
61
68
|
default:
|
|
62
|
-
console.error(`Some filesystem error when try readJSON: ${filePath}
|
|
69
|
+
!silent && console.error(`Some filesystem error when try readJSON: ${filePath}`, err);
|
|
63
70
|
return null;
|
|
64
71
|
}
|
|
65
72
|
}
|
|
66
73
|
if (isSyntaxError(err)) {
|
|
67
|
-
console.error('File ', filePath, ' has incorrect JSON syntax', '\n');
|
|
74
|
+
!silent && console.error('File ', filePath, ' has incorrect JSON syntax', '\n');
|
|
68
75
|
return null;
|
|
69
76
|
}
|
|
70
|
-
console.error('Function readJSON error:', err, '\n');
|
|
77
|
+
!silent && console.error('Function readJSON error:', err, '\n');
|
|
71
78
|
return null;
|
|
72
79
|
}
|
|
73
80
|
};
|
|
74
81
|
const addToJSONSync = (saveInput) => {
|
|
75
|
-
const { filePath, dataToAdd, format = false, logSaving = false, replaceNonSerializable = false, silent = true, } = saveInput;
|
|
76
|
-
const oldJSON = readJSONSync({ filePath, createIfNotFound: true, silent });
|
|
82
|
+
const { filePath, dataToAdd, format = false, logSaving = false, replaceNonSerializable = false, silent = true, throwError = false, } = saveInput;
|
|
83
|
+
const oldJSON = readJSONSync({ filePath, createIfNotFound: true, silent, throwError });
|
|
77
84
|
if (oldJSON === null || typeof oldJSON !== 'object') {
|
|
78
85
|
return saveJSONSync({ filePath, objToSave: dataToAdd, format, logSaving, replaceNonSerializable, silent });
|
|
79
86
|
}
|
package/dist/common.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface ReadJSONProps {
|
|
|
11
11
|
parseJSON?: boolean;
|
|
12
12
|
createIfNotFound?: boolean | SerializableObject | SerializableArray;
|
|
13
13
|
silent?: boolean;
|
|
14
|
+
throwError?: boolean;
|
|
14
15
|
async?: boolean;
|
|
15
16
|
}
|
|
16
17
|
export interface SaveJSONProps {
|
|
@@ -29,6 +30,7 @@ export interface addToJSONProps {
|
|
|
29
30
|
logSaving?: boolean;
|
|
30
31
|
replaceNonSerializable?: boolean;
|
|
31
32
|
silent?: boolean;
|
|
33
|
+
throwError?: boolean;
|
|
32
34
|
async?: boolean;
|
|
33
35
|
}
|
|
34
36
|
export type ReadJSONSyncProps = ReadJSONProps & {
|
package/dist/common.js
CHANGED
|
@@ -168,6 +168,9 @@ export const sanitizeNonSerializable = (value, path = '$', ancestors = new WeakM
|
|
|
168
168
|
if (ancestors.has(value)) {
|
|
169
169
|
return 'circular';
|
|
170
170
|
}
|
|
171
|
+
if (typeof value.toJSON === 'function') {
|
|
172
|
+
return sanitizeNonSerializable(value.toJSON(), path, ancestors);
|
|
173
|
+
}
|
|
171
174
|
ancestors.set(value, path);
|
|
172
175
|
const result = {};
|
|
173
176
|
for (const key of Object.keys(value)) {
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Boma JSON helper
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.org/package/boma)
|
|
4
|
+
|
|
3
5
|
Super-simple helper for reading, saving and updating JSON files.
|
|
4
6
|
|
|
5
7
|
Works in synchronous mode by default.
|
|
@@ -7,6 +9,17 @@ Pass `async: true` to use the asynchronous implementation.
|
|
|
7
9
|
|
|
8
10
|
Old synchronous calls remain unchanged.
|
|
9
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
|
+
|
|
10
23
|
# Install
|
|
11
24
|
|
|
12
25
|
```bash
|