mm_watcher 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/example/demo.js +22 -18
- package/index.js +12 -1
- package/lib/watcher.js +29 -31
- package/lib/watcher_hub.js +104 -55
- package/package.json +5 -2
package/example/demo.js
CHANGED
|
@@ -15,32 +15,37 @@ function delay(ms) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
18
|
+
* 演示目录监听:忽略目录、忽略文件、add/set/del/reload 事件
|
|
19
19
|
* @returns {Promise<void>} 演示完成
|
|
20
20
|
*/
|
|
21
21
|
async function demoDirWatch() {
|
|
22
22
|
const dir = path.join(os.tmpdir(), 'mm_watcher_demo_dir');
|
|
23
23
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
24
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
25
24
|
fs.mkdirSync(path.join(dir, 'node_modules'), { recursive: true });
|
|
25
|
+
fs.writeFileSync(path.join(dir, 'a.json'), '{}');
|
|
26
|
+
|
|
27
|
+
const watcher = new Watcher({ target: dir, ignore: ['node_modules', '*.tmp'] });
|
|
28
|
+
watcher.on('add', (p) => console.log('add ', p));
|
|
29
|
+
watcher.on('set', (p) => console.log('set ', p));
|
|
30
|
+
watcher.on('del', (p) => console.log('del ', p));
|
|
31
|
+
watcher.on('reload', (p) => console.log('reload', p));
|
|
26
32
|
|
|
27
|
-
const
|
|
28
|
-
const watcher = new Watcher(dir, { ignore: ['node_modules', '*.tmp'] });
|
|
29
|
-
const ok = watcher.start((list) => changes.push(...list));
|
|
33
|
+
const ok = watcher.listen();
|
|
30
34
|
console.log('目录监听:', ok, '| 目标:', dir);
|
|
31
35
|
|
|
32
|
-
fs.writeFileSync(path.join(dir, '
|
|
36
|
+
fs.writeFileSync(path.join(dir, 'b.json'), '{}');
|
|
33
37
|
fs.writeFileSync(path.join(dir, 'cache.tmp'), 'x');
|
|
34
|
-
fs.writeFileSync(path.join(dir, 'node_modules
|
|
38
|
+
fs.writeFileSync(path.join(dir, 'node_modules', 'skip.json'), '{}');
|
|
39
|
+
await delay(300);
|
|
40
|
+
|
|
41
|
+
fs.writeFileSync(path.join(dir, 'a.json'), '{"v":2}');
|
|
35
42
|
await delay(300);
|
|
36
|
-
console.log('变更事件:', changes);
|
|
37
43
|
|
|
38
44
|
fs.rmSync(path.join(dir, 'a.json'));
|
|
39
|
-
changes.length = 0;
|
|
40
45
|
await delay(300);
|
|
41
|
-
|
|
46
|
+
|
|
47
|
+
fs.mkdirSync(path.join(dir, 'sub'), { recursive: true });
|
|
42
48
|
await delay(300);
|
|
43
|
-
console.log('删除后重建:', changes.map((one) => one.type + ' ' + one.path));
|
|
44
49
|
|
|
45
50
|
watcher.dispose();
|
|
46
51
|
console.log('解绑后状态:', watcher.active);
|
|
@@ -48,7 +53,7 @@ async function demoDirWatch() {
|
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
/**
|
|
51
|
-
*
|
|
56
|
+
* 演示单文件监听:set 与 del 事件
|
|
52
57
|
* @returns {Promise<void>} 演示完成
|
|
53
58
|
*/
|
|
54
59
|
async function demoFileWatch() {
|
|
@@ -58,19 +63,18 @@ async function demoFileWatch() {
|
|
|
58
63
|
const file = path.join(dir, 'config.json');
|
|
59
64
|
fs.writeFileSync(file, '{}');
|
|
60
65
|
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
const watcher = new Watcher({ target: file });
|
|
67
|
+
watcher.on('set', (p) => console.log('set ', p));
|
|
68
|
+
watcher.on('del', (p) => console.log('del ', p));
|
|
69
|
+
|
|
70
|
+
const ok = watcher.listen();
|
|
64
71
|
console.log('单文件监听:', ok, '| 目标:', file);
|
|
65
72
|
|
|
66
73
|
fs.writeFileSync(file, '{"v":1}');
|
|
67
74
|
await delay(300);
|
|
68
|
-
console.log('修改事件:', changes.map((one) => one.type + ' ' + one.path));
|
|
69
75
|
|
|
70
|
-
changes.length = 0;
|
|
71
76
|
fs.rmSync(file);
|
|
72
77
|
await delay(300);
|
|
73
|
-
console.log('删除事件:', changes.map((one) => one.type + ' ' + one.path));
|
|
74
78
|
|
|
75
79
|
watcher.dispose();
|
|
76
80
|
fs.rmSync(dir, { recursive: true, force: true });
|
package/index.js
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
const Watcher = require('./lib/watcher');
|
|
2
|
+
const Admin = require('mm_corejs').Admin;
|
|
3
|
+
|
|
4
|
+
const admin = new Admin();
|
|
5
|
+
admin.setClass(Watcher);
|
|
2
6
|
|
|
3
7
|
module.exports = {
|
|
4
|
-
Watcher
|
|
8
|
+
Watcher,
|
|
9
|
+
admin,
|
|
10
|
+
watcherAdmin(scope, config) {
|
|
11
|
+
if (!scope) {
|
|
12
|
+
scope = 'os';
|
|
13
|
+
}
|
|
14
|
+
return admin.get(scope, config);
|
|
15
|
+
}
|
|
5
16
|
};
|
package/lib/watcher.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { EventEmitter } = require('events');
|
|
3
4
|
const hub = require('./watcher_hub');
|
|
4
5
|
const { DISPATCH, DEACTIVATE } = require('./symbols');
|
|
5
6
|
|
|
@@ -8,10 +9,11 @@ const DEFAULT_DEBOUNCE = 100;
|
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* 文件监听器
|
|
11
|
-
* @description
|
|
12
|
+
* @description 监听目录(递归)或单个文件,事件经防抖与忽略过滤后按类型分发:
|
|
13
|
+
* on('add') 新增文件、on('set') 修改文件、on('del') 删除文件、on('reload') 目录变化或无法定位需全量重扫;
|
|
12
14
|
* 相同目标与忽略配置的多个监听器共享底层句柄,监听不可用时静默降级
|
|
13
15
|
*/
|
|
14
|
-
class Watcher {
|
|
16
|
+
class Watcher extends EventEmitter {
|
|
15
17
|
/** 监听目标绝对路径 */
|
|
16
18
|
#target;
|
|
17
19
|
/** 共享句柄的监听键 */
|
|
@@ -21,49 +23,45 @@ class Watcher {
|
|
|
21
23
|
/** 防抖窗口(毫秒) */
|
|
22
24
|
#debounce;
|
|
23
25
|
/** 出错停用时的外部回调 */
|
|
24
|
-
#
|
|
25
|
-
/** 变更回调 */
|
|
26
|
-
#callback = null;
|
|
26
|
+
#on_error;
|
|
27
27
|
/** 是否处于监听状态 */
|
|
28
28
|
#active = false;
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* 创建监听器
|
|
32
|
-
* @param {string} target
|
|
33
|
-
* @param {{ ignore?: Array<string>, debounce?: number, onError?: () => void }} [options] 监听选项
|
|
32
|
+
* @param {{ target: string, ignore?: Array<string>, debounce?: number, onError?: () => void }} [config] 监听配置,target 为目录或文件绝对路径
|
|
34
33
|
*/
|
|
35
|
-
constructor(
|
|
36
|
-
|
|
37
|
-
this.#
|
|
34
|
+
constructor(config = {}) {
|
|
35
|
+
super();
|
|
36
|
+
this.#target = String(config.target).replace(/\\/g, '/');
|
|
37
|
+
this.#ignore = Array.isArray(config.ignore) ? [...config.ignore] : [];
|
|
38
38
|
this.#debounce =
|
|
39
|
-
Number.isFinite(
|
|
40
|
-
?
|
|
39
|
+
Number.isFinite(config.debounce) && config.debounce >= 0
|
|
40
|
+
? config.debounce
|
|
41
41
|
: DEFAULT_DEBOUNCE;
|
|
42
|
-
this.#
|
|
42
|
+
this.#on_error = typeof config.onError === 'function' ? config.onError : null;
|
|
43
43
|
this.#key = [this.#target, this.#ignore.join(',')].join('\u0001');
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* 开启监听
|
|
48
|
-
* @param {(changes: Array<{ type: string, path: string }>) => void} [callback] 变更回调,type 为 change/unlink/refresh
|
|
49
48
|
* @returns {boolean} 监听是否可用
|
|
50
49
|
*/
|
|
51
|
-
|
|
50
|
+
listen() {
|
|
52
51
|
if (this.#active) return true;
|
|
53
52
|
if (!hub.add(this)) return false;
|
|
54
|
-
this.#callback = typeof callback === 'function' ? callback : null;
|
|
55
53
|
this.#active = true;
|
|
56
54
|
return true;
|
|
57
55
|
}
|
|
58
56
|
|
|
59
57
|
/**
|
|
60
|
-
*
|
|
58
|
+
* 关闭监听并清空全部事件监听器
|
|
61
59
|
* @returns {void}
|
|
62
60
|
*/
|
|
63
61
|
dispose() {
|
|
64
62
|
if (!this.#active) return;
|
|
65
63
|
this.#active = false;
|
|
66
|
-
this
|
|
64
|
+
this.removeAllListeners();
|
|
67
65
|
hub.del(this);
|
|
68
66
|
}
|
|
69
67
|
|
|
@@ -79,18 +77,18 @@ class Watcher {
|
|
|
79
77
|
get debounce() { return this.#debounce; }
|
|
80
78
|
|
|
81
79
|
/**
|
|
82
|
-
*
|
|
83
|
-
* @param {Array<{ type: string, path: string }>}
|
|
80
|
+
* 接收中枢分发的一组变更并逐条发出事件
|
|
81
|
+
* @param {Array<{ type: string, path: string }>} events 变更列表,type 为 add/set/del/reload
|
|
84
82
|
* @returns {void}
|
|
85
83
|
*/
|
|
86
|
-
[DISPATCH](
|
|
87
|
-
if (!this.#active
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
84
|
+
[DISPATCH](events) {
|
|
85
|
+
if (!this.#active) return;
|
|
86
|
+
for (let i = 0; i < events.length; i++) {
|
|
87
|
+
try {
|
|
88
|
+
this.emit(events[i].type, events[i].path);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error('[mm_watcher] 事件处理失败:', error);
|
|
91
|
+
}
|
|
94
92
|
}
|
|
95
93
|
}
|
|
96
94
|
|
|
@@ -100,9 +98,9 @@ class Watcher {
|
|
|
100
98
|
*/
|
|
101
99
|
[DEACTIVATE]() {
|
|
102
100
|
this.#active = false;
|
|
103
|
-
this
|
|
104
|
-
if (this.#
|
|
105
|
-
this.#
|
|
101
|
+
this.removeAllListeners();
|
|
102
|
+
if (this.#on_error) {
|
|
103
|
+
this.#on_error();
|
|
106
104
|
}
|
|
107
105
|
}
|
|
108
106
|
|
package/lib/watcher_hub.js
CHANGED
|
@@ -10,10 +10,10 @@ const { DISPATCH, DEACTIVATE } = require('./symbols');
|
|
|
10
10
|
/**
|
|
11
11
|
* 监听中枢
|
|
12
12
|
* @description 按 目标+忽略配置 共享底层监听句柄,防抖窗口内合并事件,
|
|
13
|
-
*
|
|
13
|
+
* 依据基线文件集合将路径事件分类为 add/set/del/reload 并过滤忽略项,最后分发给各监听器
|
|
14
14
|
*/
|
|
15
15
|
class WatcherHub {
|
|
16
|
-
/** 监听键 -> { key, target, is_dir, debounce, matcher, watchers, timer, pending, handle } */
|
|
16
|
+
/** 监听键 -> { key, target, is_dir, debounce, matcher, known, watchers, timer, pending, handle } */
|
|
17
17
|
#entries = new Map();
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -57,23 +57,7 @@ class WatcherHub {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
*
|
|
61
|
-
* @param {string} key 监听键
|
|
62
|
-
* @returns {void}
|
|
63
|
-
* @private
|
|
64
|
-
*/
|
|
65
|
-
_teardown(key) {
|
|
66
|
-
const entry = this.#entries.get(key);
|
|
67
|
-
if (!entry) return;
|
|
68
|
-
const watchers = [...entry.watchers];
|
|
69
|
-
this._closeEntry(entry);
|
|
70
|
-
for (const watcher of watchers) {
|
|
71
|
-
watcher[DEACTIVATE]();
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* 为新监听器打开共享句柄
|
|
60
|
+
* 为新监听器打开共享句柄并建立基线文件集合
|
|
77
61
|
* @param {object} watcher 监听器实例
|
|
78
62
|
* @returns {boolean} 打开成功返回 true
|
|
79
63
|
* @private
|
|
@@ -88,12 +72,12 @@ class WatcherHub {
|
|
|
88
72
|
is_dir,
|
|
89
73
|
debounce: watcher.debounce,
|
|
90
74
|
matcher: new IgnoreMatcher(watcher.ignorePatterns),
|
|
75
|
+
known: new Set(),
|
|
91
76
|
watchers: new Set([watcher]),
|
|
92
77
|
timer: null,
|
|
93
78
|
pending: new Set(),
|
|
94
79
|
handle: null,
|
|
95
80
|
};
|
|
96
|
-
this.#entries.set(entry.key, entry);
|
|
97
81
|
|
|
98
82
|
const handle = new FsHandle();
|
|
99
83
|
const ok = handle.open(
|
|
@@ -102,11 +86,11 @@ class WatcherHub {
|
|
|
102
86
|
(event_type, filename) => this._onEvent(entry.key, filename),
|
|
103
87
|
() => this._onError(entry.key)
|
|
104
88
|
);
|
|
105
|
-
if (!ok)
|
|
106
|
-
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
89
|
+
if (!ok) return false;
|
|
90
|
+
|
|
109
91
|
entry.handle = handle;
|
|
92
|
+
this.#entries.set(entry.key, entry);
|
|
93
|
+
entry.known = this._scanKnown(entry);
|
|
110
94
|
return true;
|
|
111
95
|
}
|
|
112
96
|
|
|
@@ -122,6 +106,22 @@ class WatcherHub {
|
|
|
122
106
|
this.#entries.delete(entry.key);
|
|
123
107
|
}
|
|
124
108
|
|
|
109
|
+
/**
|
|
110
|
+
* 关闭条目并停用其全部监听器
|
|
111
|
+
* @param {string} key 监听键
|
|
112
|
+
* @returns {void}
|
|
113
|
+
* @private
|
|
114
|
+
*/
|
|
115
|
+
_teardown(key) {
|
|
116
|
+
const entry = this.#entries.get(key);
|
|
117
|
+
if (!entry) return;
|
|
118
|
+
const watchers = [...entry.watchers];
|
|
119
|
+
this._closeEntry(entry);
|
|
120
|
+
for (const watcher of watchers) {
|
|
121
|
+
watcher[DEACTIVATE]();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
125
|
/**
|
|
126
126
|
* 读取目标状态
|
|
127
127
|
* @param {string} target 目标绝对路径
|
|
@@ -136,6 +136,53 @@ class WatcherHub {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* 建立基线文件集合:目录目标递归收集未忽略文件,单文件目标记录文件本身
|
|
141
|
+
* @param {object} entry 监听条目
|
|
142
|
+
* @returns {Set<string>} 相对路径集合
|
|
143
|
+
* @private
|
|
144
|
+
*/
|
|
145
|
+
_scanKnown(entry) {
|
|
146
|
+
const known = new Set();
|
|
147
|
+
if (!entry.is_dir) {
|
|
148
|
+
known.add(path.basename(entry.target));
|
|
149
|
+
return known;
|
|
150
|
+
}
|
|
151
|
+
this._walkKnown(entry, entry.target, '', known);
|
|
152
|
+
return known;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 递归收集目录内未忽略的相对文件路径
|
|
157
|
+
* @param {object} entry 监听条目
|
|
158
|
+
* @param {string} dir 当前目录绝对路径
|
|
159
|
+
* @param {string} prefix 相对路径前缀
|
|
160
|
+
* @param {Set<string>} known 收集集合
|
|
161
|
+
* @returns {void}
|
|
162
|
+
* @private
|
|
163
|
+
*/
|
|
164
|
+
_walkKnown(entry, dir, prefix, known) {
|
|
165
|
+
let dirents = [];
|
|
166
|
+
try {
|
|
167
|
+
dirents = fs.readdirSync(dir, { withFileTypes: true });
|
|
168
|
+
} catch {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
for (let i = 0; i < dirents.length; i++) {
|
|
172
|
+
const dirent = dirents[i];
|
|
173
|
+
const rel_path = prefix ? prefix + '/' + dirent.name : dirent.name;
|
|
174
|
+
if (dirent.isDirectory()) {
|
|
175
|
+
if (!entry.matcher.isIgnoredDir(dirent.name)) {
|
|
176
|
+
this._walkKnown(entry, path.join(dir, dirent.name), rel_path, known);
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (!dirent.isFile()) continue;
|
|
181
|
+
if (entry.matcher.isIgnoredFile(dirent.name)) continue;
|
|
182
|
+
known.add(rel_path);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
139
186
|
/**
|
|
140
187
|
* 底层监听出错:关闭条目并停用全部监听器
|
|
141
188
|
* @param {string} key 监听键
|
|
@@ -183,58 +230,60 @@ class WatcherHub {
|
|
|
183
230
|
const list = [...entry.pending];
|
|
184
231
|
entry.pending.clear();
|
|
185
232
|
|
|
186
|
-
const
|
|
233
|
+
const events = [];
|
|
187
234
|
for (let i = 0; i < list.length; i++) {
|
|
188
235
|
const event = this._classify(entry, list[i]);
|
|
189
236
|
if (!event) continue;
|
|
190
|
-
|
|
191
|
-
if (event.type === '
|
|
237
|
+
events.push(event);
|
|
238
|
+
if (event.type === 'reload') break;
|
|
192
239
|
}
|
|
193
|
-
if (!
|
|
240
|
+
if (!events.length) return;
|
|
194
241
|
|
|
195
242
|
for (const watcher of entry.watchers) {
|
|
196
|
-
watcher[DISPATCH](
|
|
243
|
+
watcher[DISPATCH](events);
|
|
197
244
|
}
|
|
198
245
|
}
|
|
199
246
|
|
|
200
247
|
/**
|
|
201
|
-
*
|
|
202
|
-
* @description
|
|
203
|
-
*
|
|
248
|
+
* 将单个路径事件分类为监听变更并更新基线集合
|
|
249
|
+
* @description 无法定位与目录事件分类为 reload 并重建基线;忽略项返回 null;
|
|
250
|
+
* 基线外的新增分类为 add,基线内的修改分类为 set,消失的路径分类为 del
|
|
204
251
|
* @param {object} entry 监听条目
|
|
205
252
|
* @param {string} rel_path 规范化后的相对路径,空串表示无法定位
|
|
206
253
|
* @returns {{ type: string, path: string }|null} 变更事件,忽略时返回 null
|
|
207
254
|
* @private
|
|
208
255
|
*/
|
|
209
256
|
_classify(entry, rel_path) {
|
|
210
|
-
if (!rel_path)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return this._toEvent(this._stat(entry.target), rel_path);
|
|
257
|
+
if (!rel_path) {
|
|
258
|
+
entry.known = this._scanKnown(entry);
|
|
259
|
+
return { type: 'reload', path: '' };
|
|
214
260
|
}
|
|
215
261
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
262
|
+
if (entry.is_dir) {
|
|
263
|
+
const parts = rel_path.split('/');
|
|
264
|
+
const basename = parts[parts.length - 1];
|
|
265
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
266
|
+
if (entry.matcher.isIgnoredDir(parts[i])) return null;
|
|
267
|
+
}
|
|
268
|
+
if (entry.matcher.isIgnoredFile(basename)) return null;
|
|
220
269
|
}
|
|
221
|
-
if (entry.matcher.isIgnoredFile(basename)) return null;
|
|
222
270
|
|
|
223
|
-
|
|
224
|
-
}
|
|
271
|
+
const stat = this._stat(entry.is_dir ? path.join(entry.target, rel_path) : entry.target);
|
|
225
272
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (stat
|
|
236
|
-
|
|
237
|
-
|
|
273
|
+
if (stat && stat.isDirectory()) {
|
|
274
|
+
entry.known = this._scanKnown(entry);
|
|
275
|
+
return { type: 'reload', path: rel_path };
|
|
276
|
+
}
|
|
277
|
+
if (stat && stat.isFile()) {
|
|
278
|
+
const type = entry.known.has(rel_path) ? 'set' : 'add';
|
|
279
|
+
entry.known.add(rel_path);
|
|
280
|
+
return { type, path: rel_path };
|
|
281
|
+
}
|
|
282
|
+
if (!stat) {
|
|
283
|
+
entry.known.delete(rel_path);
|
|
284
|
+
return { type: 'del', path: rel_path };
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
238
287
|
}
|
|
239
288
|
}
|
|
240
289
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mm_watcher",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "文件监听器,listen 监听目录(递归)或单个文件,on 订阅 add/set/del/reload 事件,支持忽略目录与文件(精确名/扩展名/通配符)、事件防抖合并、同目标多监听者共享底层句柄。",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://gitee.com/qiuwenwu91/mm_watcher.git"
|
|
@@ -36,5 +36,8 @@
|
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"jest": "^30.5.2"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"mm_corejs": "^1.1.5"
|
|
39
42
|
}
|
|
40
43
|
}
|