larvitcms 2.0.0 → 3.0.2
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/.eslintrc +85 -87
- package/.github/workflows/ci.yml +14 -0
- package/README.md +2 -0
- package/cms.js +82 -181
- package/dataWriter.js +60 -312
- package/dbmigration/5.js +11 -21
- package/package.json +14 -19
- package/renovate.json +2 -7
- package/test/00test.js +135 -221
- package/.travis.yml +0 -36
- package/test/98lint.js +0 -9
- package/test/99close.js +0 -7
package/dataWriter.js
CHANGED
@@ -1,25 +1,15 @@
|
|
1
1
|
'use strict';
|
2
2
|
|
3
|
-
const
|
4
|
-
const
|
5
|
-
const
|
6
|
-
const slugify = require('larvitslugify');
|
7
|
-
const LUtils = require('larvitutils');
|
8
|
-
const amsync = require('larvitamsync');
|
9
|
-
const async = require('async');
|
3
|
+
const { DbMigration } = require('larvitdbmigration');
|
4
|
+
const { slugify } = require('larvitslugify');
|
5
|
+
const { Utils, Log } = require('larvitutils');
|
10
6
|
|
11
|
-
|
12
|
-
let readyInProgress = false;
|
13
|
-
let emitter = new EventEmitter();
|
7
|
+
const topLogPrefix = 'larvitcms: dataWriter.js: ';
|
14
8
|
|
15
9
|
class DataWriter {
|
16
|
-
static get emitter() { return emitter; }
|
17
|
-
|
18
10
|
constructor(options) {
|
19
11
|
if (!options.log) {
|
20
|
-
|
21
|
-
|
22
|
-
options.log = new tmpLUtils.Log();
|
12
|
+
options.log = new Log();
|
23
13
|
}
|
24
14
|
|
25
15
|
this.options = options;
|
@@ -28,339 +18,113 @@ class DataWriter {
|
|
28
18
|
this[key] = options[key];
|
29
19
|
}
|
30
20
|
|
31
|
-
this.lUtils = new
|
32
|
-
|
33
|
-
this.listenToQueue();
|
34
|
-
}
|
35
|
-
|
36
|
-
listenToQueue(retries, cb) {
|
37
|
-
const logPrefix = topLogPrefix + 'listenToQueue() - ';
|
38
|
-
const options = {exchange: this.exchangeName};
|
39
|
-
const tasks = [];
|
40
|
-
|
41
|
-
let listenMethod;
|
42
|
-
|
43
|
-
if (typeof retries === 'function') {
|
44
|
-
cb = retries;
|
45
|
-
retries = 0;
|
46
|
-
}
|
47
|
-
|
48
|
-
if (typeof cb !== 'function') {
|
49
|
-
cb = function () {};
|
50
|
-
}
|
51
|
-
|
52
|
-
if (retries === undefined) {
|
53
|
-
retries = 0;
|
54
|
-
}
|
55
|
-
|
56
|
-
tasks.push(cb => {
|
57
|
-
if (this.mode === 'master') {
|
58
|
-
listenMethod = 'consume';
|
59
|
-
options.exclusive = true; // It is important no other client tries to sneak
|
60
|
-
// out messages from us, and we want "consume"
|
61
|
-
// since we want the queue to persist even if this
|
62
|
-
// minion goes offline.
|
63
|
-
} else if (this.mode === 'slave' || this.mode === 'noSync') {
|
64
|
-
listenMethod = 'subscribe';
|
65
|
-
} else {
|
66
|
-
const err = new Error('Invalid this.mode. Must be either "master", "slave" or "noSync"');
|
67
|
-
|
68
|
-
this.log.error(logPrefix + err.message);
|
69
|
-
|
70
|
-
return cb(err);
|
71
|
-
}
|
72
|
-
|
73
|
-
this.log.info(logPrefix + 'listenMethod: ' + listenMethod);
|
74
|
-
|
75
|
-
cb();
|
76
|
-
});
|
77
|
-
|
78
|
-
tasks.push(cb => {
|
79
|
-
this.ready(cb);
|
80
|
-
});
|
81
|
-
|
82
|
-
tasks.push(cb => {
|
83
|
-
this.intercom[listenMethod](options, (message, ack, deliveryTag) => {
|
84
|
-
ack(); // Ack first, if something goes wrong we log it and handle it manually
|
85
|
-
|
86
|
-
if (typeof message !== 'object') {
|
87
|
-
this.log.error(logPrefix + 'intercom.' + listenMethod + '() - Invalid message received, is not an object! deliveryTag: "' + deliveryTag + '"');
|
88
|
-
|
89
|
-
return;
|
90
|
-
}
|
91
|
-
|
92
|
-
if (typeof this[message.action] === 'function') {
|
93
|
-
this[message.action](message.params, deliveryTag, message.uuid);
|
94
|
-
} else {
|
95
|
-
this.log.warn(logPrefix + 'intercom.' + listenMethod + '() - Unknown message.action received: "' + message.action + '"');
|
96
|
-
}
|
97
|
-
}, cb);
|
98
|
-
});
|
99
|
-
|
100
|
-
async.series(tasks, cb);
|
21
|
+
this.lUtils = new Utils({log: this.log});
|
101
22
|
}
|
102
23
|
|
103
|
-
|
104
|
-
|
105
|
-
const logPrefix = topLogPrefix + 'ready() - ';
|
106
|
-
const tasks = [];
|
107
|
-
|
108
|
-
if (typeof retries === 'function') {
|
109
|
-
cb = retries;
|
110
|
-
retries = 0;
|
111
|
-
}
|
112
|
-
|
113
|
-
if (typeof cb !== 'function') {
|
114
|
-
cb = function () {};
|
115
|
-
}
|
116
|
-
|
117
|
-
if (retries === undefined) {
|
118
|
-
retries = 0;
|
119
|
-
}
|
24
|
+
async runDbMigrations() {
|
25
|
+
const options = {};
|
120
26
|
|
121
|
-
|
27
|
+
options.dbType = 'mariadb';
|
28
|
+
options.dbDriver = this.db;
|
29
|
+
options.tableName = 'cms_db_version';
|
30
|
+
options.migrationScriptsPath = __dirname + '/dbmigration';
|
31
|
+
const dbMigration = new DbMigration(options);
|
122
32
|
|
123
|
-
|
124
|
-
DataWriter.emitter.on('ready', cb);
|
125
|
-
|
126
|
-
return;
|
127
|
-
}
|
128
|
-
|
129
|
-
readyInProgress = true;
|
130
|
-
|
131
|
-
tasks.push(cb => {
|
132
|
-
if (this.mode === 'slave') {
|
133
|
-
this.log.verbose(logPrefix + 'this.mode: "' + this.mode + '", so read');
|
134
|
-
|
135
|
-
amsync.mariadb({
|
136
|
-
exchange: this.exchangeName + '_dataDump',
|
137
|
-
intercom: this.intercom
|
138
|
-
}, cb);
|
139
|
-
} else {
|
140
|
-
cb();
|
141
|
-
}
|
142
|
-
});
|
143
|
-
|
144
|
-
// Migrate database
|
145
|
-
tasks.push(cb => {
|
146
|
-
const options = {};
|
147
|
-
|
148
|
-
let dbMigration;
|
149
|
-
|
150
|
-
options.dbType = 'mariadb';
|
151
|
-
options.dbDriver = this.db;
|
152
|
-
options.tableName = 'cms_db_version';
|
153
|
-
options.migrationScriptsPath = __dirname + '/dbmigration';
|
154
|
-
dbMigration = new DbMigration(options);
|
155
|
-
|
156
|
-
dbMigration.run(err => {
|
157
|
-
if (err) {
|
158
|
-
this.log.error(logPrefix + 'Database error: ' + err.message);
|
159
|
-
}
|
160
|
-
|
161
|
-
cb(err);
|
162
|
-
});
|
163
|
-
});
|
164
|
-
|
165
|
-
async.series(tasks, err => {
|
166
|
-
if (err) return;
|
167
|
-
|
168
|
-
isReady = true;
|
169
|
-
DataWriter.emitter.emit('ready');
|
170
|
-
|
171
|
-
if (this.mode === 'master') {
|
172
|
-
this.runDumpServer(cb);
|
173
|
-
} else {
|
174
|
-
cb();
|
175
|
-
}
|
176
|
-
});
|
33
|
+
await dbMigration.run();
|
177
34
|
}
|
178
35
|
|
179
|
-
|
180
|
-
const options = {
|
181
|
-
exchange: this.exchangeName + '_dataDump',
|
182
|
-
host: this.options.amsync ? this.options.amsync.host : null,
|
183
|
-
minPort: this.options.amsync ? this.options.amsync.minPort : null,
|
184
|
-
maxPort: this.options.amsync ? this.options.amsync.maxPort : null
|
185
|
-
};
|
186
|
-
|
187
|
-
const args = [];
|
188
|
-
|
189
|
-
if (this.db.conf.host) {
|
190
|
-
args.push('-h');
|
191
|
-
args.push(this.db.conf.host);
|
192
|
-
}
|
193
|
-
|
194
|
-
args.push('-u');
|
195
|
-
args.push(this.db.conf.user);
|
196
|
-
|
197
|
-
if (this.db.conf.password) {
|
198
|
-
args.push('-p' + this.db.conf.password);
|
199
|
-
}
|
200
|
-
|
201
|
-
args.push('--single-transaction');
|
202
|
-
args.push('--hex-blob');
|
203
|
-
args.push(this.db.conf.database);
|
204
|
-
|
205
|
-
// Tables
|
206
|
-
args.push('cms_db_version');
|
207
|
-
args.push('cms_pages');
|
208
|
-
args.push('cms_pagesData');
|
209
|
-
args.push('cms_snippets');
|
210
|
-
|
211
|
-
options.dataDumpCmd = {
|
212
|
-
command: 'mysqldump',
|
213
|
-
args: args
|
214
|
-
};
|
215
|
-
|
216
|
-
options['Content-Type'] = 'application/sql';
|
217
|
-
options.intercom = this.intercom;
|
218
|
-
|
219
|
-
new amsync.SyncServer(options, cb);
|
220
|
-
}
|
221
|
-
|
222
|
-
rmPage(params, deliveryTag, msgUuid) {
|
36
|
+
async rmPage(uuid) {
|
223
37
|
const logPrefix = topLogPrefix + 'rmPage() - ';
|
224
|
-
const
|
225
|
-
const uuidBuffer = this.lUtils.uuidToBuffer(options.uuid);
|
226
|
-
const tasks = [];
|
38
|
+
const uuidBuffer = this.lUtils.uuidToBuffer(uuid);
|
227
39
|
|
228
|
-
if (
|
40
|
+
if (uuid === undefined) {
|
229
41
|
const err = new Error('pageUuid not provided');
|
230
|
-
|
231
42
|
this.log.warn(logPrefix + err.message);
|
232
|
-
|
233
|
-
return DataWriter.emitter.emit(msgUuid, err);
|
43
|
+
throw err;
|
234
44
|
}
|
235
45
|
|
236
46
|
if (uuidBuffer === false) {
|
237
47
|
const err = new Error('Inavlid pageUuid provided');
|
238
|
-
|
239
48
|
this.log.warn(logPrefix + err.message);
|
240
|
-
|
241
|
-
return DataWriter.emitter.emit(msgUuid, err);
|
49
|
+
throw err;
|
242
50
|
}
|
243
51
|
|
244
|
-
|
245
|
-
|
246
|
-
tasks.push(cb => {
|
247
|
-
this.db.query('DELETE FROM cms_pagesData WHERE pageUuid = ?', [uuidBuffer], cb);
|
248
|
-
});
|
249
|
-
|
250
|
-
tasks.push(cb => {
|
251
|
-
this.db.query('DELETE FROM cms_pages WHERE uuid = ?', [uuidBuffer], cb);
|
252
|
-
});
|
253
|
-
|
254
|
-
async.series(tasks, err => {
|
255
|
-
DataWriter.emitter.emit(msgUuid, err);
|
256
|
-
});
|
52
|
+
await this.db.query('DELETE FROM cms_pagesData WHERE pageUuid = ?', [uuidBuffer]);
|
53
|
+
await this.db.query('DELETE FROM cms_pages WHERE uuid = ?', [uuidBuffer]);
|
257
54
|
}
|
258
55
|
|
259
|
-
rmSnippet(
|
56
|
+
async rmSnippet(name) {
|
260
57
|
const logPrefix = topLogPrefix + 'rmSnippet() - ';
|
261
|
-
const options = params.data;
|
262
|
-
const tasks = [];
|
263
58
|
|
264
|
-
if (!
|
59
|
+
if (!name) {
|
265
60
|
const err = new Error('snippet name not provided');
|
266
|
-
|
267
61
|
this.log.warn(logPrefix + err.message);
|
268
|
-
|
269
|
-
return DataWriter.emitter.emit(msgUuid, err);
|
62
|
+
throw err;
|
270
63
|
}
|
271
64
|
|
272
|
-
|
273
|
-
|
274
|
-
tasks.push(cb => {
|
275
|
-
this.db.query('DELETE FROM cms_snippets WHERE name = ?', [options.name], cb);
|
276
|
-
});
|
277
|
-
|
278
|
-
async.series(tasks, err => {
|
279
|
-
DataWriter.emitter.emit(msgUuid, err);
|
280
|
-
});
|
65
|
+
await this.db.query('DELETE FROM cms_snippets WHERE name = ?', [name]);
|
281
66
|
}
|
282
67
|
|
283
|
-
savePage(
|
68
|
+
async savePage(options) {
|
284
69
|
const logPrefix = topLogPrefix + 'savePage() - ';
|
285
|
-
const options = params.data;
|
286
70
|
const uuidBuffer = this.lUtils.uuidToBuffer(options.uuid);
|
287
|
-
const tasks = [];
|
288
|
-
|
289
|
-
let lang;
|
290
71
|
|
291
72
|
if (options.uuid === undefined) {
|
292
73
|
const err = new Error('pageUuid not provided');
|
293
|
-
|
294
74
|
this.log.warn(logPrefix + err.message);
|
295
|
-
|
296
|
-
return DataWriter.emitter.emit(msgUuid, err);
|
75
|
+
throw err;
|
297
76
|
}
|
298
77
|
|
299
78
|
if (uuidBuffer === false) {
|
300
79
|
const err = new Error('Inavlid pageUuid provided');
|
301
|
-
|
302
80
|
this.log.warn(logPrefix + err.message);
|
303
|
-
|
304
|
-
return DataWriter.emitter.emit(msgUuid, err);
|
81
|
+
throw err;
|
305
82
|
}
|
306
83
|
|
307
|
-
this.log.debug(logPrefix + 'Running with data. "' + JSON.stringify(
|
84
|
+
this.log.debug(logPrefix + 'Running with data. "' + JSON.stringify(options) + '"');
|
308
85
|
|
309
|
-
|
86
|
+
await this.db.query('DELETE FROM cms_pagesData WHERE pageUuid = ?', [uuidBuffer]);
|
87
|
+
await this.db.query('DELETE FROM cms_pages WHERE uuid = ?', [uuidBuffer]);
|
310
88
|
|
311
|
-
|
312
|
-
|
313
|
-
});
|
89
|
+
// Insert page
|
90
|
+
const dbFields = [uuidBuffer, options.name];
|
314
91
|
|
315
|
-
|
316
|
-
this.db.query('DELETE FROM cms_pages WHERE uuid = ?', [uuidBuffer], cb);
|
317
|
-
});
|
92
|
+
let sql = 'INSERT INTO cms_pages (uuid,name';
|
318
93
|
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
let sql = 'INSERT INTO cms_pages (uuid,name';
|
323
|
-
|
324
|
-
if (options.published) {
|
325
|
-
sql += ', published';
|
326
|
-
}
|
94
|
+
if (options.published) {
|
95
|
+
sql += ', published';
|
96
|
+
}
|
327
97
|
|
328
|
-
|
329
|
-
|
330
|
-
|
98
|
+
if (options.template) {
|
99
|
+
sql += ', template';
|
100
|
+
}
|
331
101
|
|
332
|
-
|
102
|
+
sql += ') VALUES(?,?';
|
333
103
|
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
104
|
+
if (options.published) {
|
105
|
+
sql += ',?';
|
106
|
+
dbFields.push(options.published);
|
107
|
+
}
|
338
108
|
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
109
|
+
if (options.template) {
|
110
|
+
sql += ',?';
|
111
|
+
dbFields.push(options.template);
|
112
|
+
}
|
343
113
|
|
344
|
-
|
114
|
+
sql += ');';
|
345
115
|
|
346
|
-
|
347
|
-
});
|
116
|
+
await this.db.query(sql, dbFields);
|
348
117
|
|
349
118
|
// We need to declare this outside the loop because of async operations
|
350
|
-
const addEntryData = (lang, htmlTitle, body1, body2, body3, body4, body5, body6, slug) => {
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
const sql = 'INSERT INTO cms_pagesData (pageUuid, lang, htmlTitle, body1, body2, body3, body4, body5, body6, slug) VALUES(?,?,?,?,?,?,?,?,?,?);';
|
356
|
-
|
357
|
-
this.db.query(sql, dbFields, cb);
|
358
|
-
});
|
119
|
+
const addEntryData = async (lang, htmlTitle, body1, body2, body3, body4, body5, body6, slug) => {
|
120
|
+
const dbFields = [uuidBuffer, lang, htmlTitle, body1, body2, body3, body4, body5, body6, slug];
|
121
|
+
const sql = 'INSERT INTO cms_pagesData (pageUuid, lang, htmlTitle, body1, body2, body3, body4, body5, body6, slug) VALUES(?,?,?,?,?,?,?,?,?,?);';
|
122
|
+
await this.db.query(sql, dbFields);
|
359
123
|
};
|
360
124
|
|
361
125
|
// Add content data
|
362
126
|
if (options.langs) {
|
363
|
-
for (lang in options.langs) {
|
127
|
+
for (const lang in options.langs) {
|
364
128
|
if (options.langs[lang].slug) {
|
365
129
|
options.langs[lang].slug = slugify(options.langs[lang].slug, {save: ['_', '-', '/']});
|
366
130
|
}
|
@@ -377,39 +141,23 @@ class DataWriter {
|
|
377
141
|
if (!options.langs[lang].body5) options.langs[lang].body5 = '';
|
378
142
|
if (!options.langs[lang].body6) options.langs[lang].body6 = '';
|
379
143
|
|
380
|
-
addEntryData(lang, options.langs[lang].htmlTitle, options.langs[lang].body1, options.langs[lang].body2, options.langs[lang].body3, options.langs[lang].body4, options.langs[lang].body5, options.langs[lang].body6, options.langs[lang].slug);
|
144
|
+
await addEntryData(lang, options.langs[lang].htmlTitle, options.langs[lang].body1, options.langs[lang].body2, options.langs[lang].body3, options.langs[lang].body4, options.langs[lang].body5, options.langs[lang].body6, options.langs[lang].slug);
|
381
145
|
}
|
382
146
|
}
|
383
|
-
|
384
|
-
async.series(tasks, err => {
|
385
|
-
DataWriter.emitter.emit(msgUuid, err);
|
386
|
-
});
|
387
147
|
}
|
388
148
|
|
389
|
-
saveSnippet(
|
149
|
+
async saveSnippet(options) {
|
390
150
|
const logPrefix = topLogPrefix + 'saveSnippet() - ';
|
391
|
-
const options = params.data;
|
392
151
|
const sql = 'REPLACE INTO cms_snippets (body, name, lang) VALUES(?,?,?);';
|
393
152
|
const dbFields = [options.body, options.name, options.lang];
|
394
|
-
const tasks = [];
|
395
153
|
|
396
154
|
if (options.name === undefined) {
|
397
155
|
const err = new Error('Name not provided');
|
398
|
-
|
399
156
|
this.log.warn(logPrefix + err.message);
|
400
|
-
|
401
|
-
return DataWriter.emitter.emit(msgUuid, err);
|
157
|
+
throw err;
|
402
158
|
}
|
403
159
|
|
404
|
-
|
405
|
-
|
406
|
-
tasks.push(cb => {
|
407
|
-
this.db.query(sql, dbFields, cb);
|
408
|
-
});
|
409
|
-
|
410
|
-
async.series(tasks, err => {
|
411
|
-
DataWriter.emitter.emit(msgUuid, err);
|
412
|
-
});
|
160
|
+
await this.db.query(sql, dbFields);
|
413
161
|
}
|
414
162
|
}
|
415
163
|
|
package/dbmigration/5.js
CHANGED
@@ -1,30 +1,20 @@
|
|
1
1
|
'use strict';
|
2
2
|
|
3
3
|
const uuidLib = require('uuid');
|
4
|
-
const
|
5
|
-
const async = require('async');
|
6
|
-
const db = require('larvitdb');
|
4
|
+
const { Utils } = require('larvitutils');
|
7
5
|
|
8
|
-
exports = module.exports =
|
9
|
-
const
|
6
|
+
exports = module.exports = async context => {
|
7
|
+
const { db } = context;
|
10
8
|
|
11
|
-
db.query('SELECT id FROM cms_pages WHERE uuid IS NULL'
|
12
|
-
|
9
|
+
const { rows } = await db.query('SELECT id FROM cms_pages WHERE uuid IS NULL');
|
10
|
+
if (rows.length === 0) return;
|
13
11
|
|
14
|
-
|
12
|
+
const lUtils = new Utils();
|
15
13
|
|
16
|
-
|
17
|
-
|
14
|
+
for (const r of rows) {
|
15
|
+
const uuid = lUtils.uuidToBuffer(uuidLib.v1());
|
18
16
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
tasks.push(function (cb) {
|
24
|
-
db.query('UPDATE cms_pagesData SET pageUuid = ? WHERE pageId = ?', [uuid, r.id], cb);
|
25
|
-
});
|
26
|
-
}
|
27
|
-
|
28
|
-
async.series(tasks, cb);
|
29
|
-
});
|
17
|
+
await db.query('UPDATE cms_pages SET uuid = ? WHERE id = ?', [uuid, r.id]);
|
18
|
+
await db.query('UPDATE cms_pagesData SET pageUuid = ? WHERE pageId = ?', [uuid, r.id]);
|
19
|
+
}
|
30
20
|
};
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "larvitcms",
|
3
|
-
"version": "
|
3
|
+
"version": "3.0.2",
|
4
4
|
"description": "Simple blog module with admin GUI for larvitadmingui",
|
5
5
|
"author": {
|
6
6
|
"name": "Mikael 'Lilleman' Göransson",
|
@@ -9,25 +9,17 @@
|
|
9
9
|
},
|
10
10
|
"private": false,
|
11
11
|
"dependencies": {
|
12
|
-
"
|
13
|
-
"
|
14
|
-
"
|
15
|
-
"
|
16
|
-
"
|
17
|
-
"
|
18
|
-
"larvitslugify": "^1.0.0",
|
19
|
-
"larvitutils": "^2.1.0",
|
20
|
-
"lodash": "^4.17.4",
|
21
|
-
"randomstring": "^1.1.5",
|
22
|
-
"uuid": "^3.1.0"
|
12
|
+
"larvitdb": "3.2.4",
|
13
|
+
"larvitdbmigration": "7.0.1",
|
14
|
+
"larvitslugify": "2.0.1",
|
15
|
+
"larvitutils": "5.0.5",
|
16
|
+
"lodash": "4.17.21",
|
17
|
+
"uuid": "8.3.2"
|
23
18
|
},
|
24
19
|
"devDependencies": {
|
25
|
-
"
|
26
|
-
"mocha": "
|
27
|
-
"
|
28
|
-
"request": "2.88.0",
|
29
|
-
"validator": "11.0.0",
|
30
|
-
"wait-for-port": "0.0.2"
|
20
|
+
"eslint": "8.13.0",
|
21
|
+
"mocha": "9.2.2",
|
22
|
+
"nyc": "15.1.0"
|
31
23
|
},
|
32
24
|
"keywords": [
|
33
25
|
"cms"
|
@@ -43,7 +35,10 @@
|
|
43
35
|
},
|
44
36
|
"homepage": "https://github.com/larvit/larvitcms",
|
45
37
|
"scripts": {
|
46
|
-
"
|
38
|
+
"lint": "eslint *.js test/*.js dbmigration/*.js",
|
39
|
+
"test:unit": "mocha --exit --bail './test/*.js'",
|
40
|
+
"coverage": "nyc --reporter lcov npm run test:unit",
|
41
|
+
"test": "npm run lint && npm run coverage"
|
47
42
|
},
|
48
43
|
"license": "MIT",
|
49
44
|
"maintainers": [
|