• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nodecraft / spawnpoint / 20214330173

14 Dec 2025 09:21PM UTC coverage: 85.016% (-5.1%) from 90.076%
20214330173

Pull #105

github

Cherry
refactor: tests to vitest
Pull Request #105: refactor: tests to vitest

261 of 348 branches covered (75.0%)

Branch coverage included in aggregate %.

8 of 8 new or added lines in 1 file covered. (100.0%)

31 existing lines in 2 files now uncovered.

539 of 593 relevant lines covered (90.89%)

2112.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.81
/lib/spawnpoint.js
1
'use strict';
2

3
// Include core libs
4
const assert = require('node:assert');
138✔
5
const EventEmitter = require('node:events');
138✔
6
const fs = require('node:fs');
138✔
7
const path = require('node:path');
138✔
8
const { format } = require('node:util');
138✔
9

10
// Include external libraries
11
const async = require('async');
138✔
12
const kleur = require('kleur');
138✔
13
const _ = require('lodash');
138✔
14
const minimist = require('minimist');
138✔
15

16
// Define private helper functions
17
const helpers = require('./helpers.js');
138✔
18

19
/**
20
 * Agnostic JS framework that empowers devs to focus on quickly building apps, rather than focusing on application
21
 * config, health-checks, application structure, or architecture to build a 12 factor app in Docker.
22
 *
23
 * Spawnpoint can be configured to manage the entire application life-cycle or standalone as a utility library.
24
 * @class
25
 */
26
class spawnpoint extends EventEmitter {
27
        /**
28
         * Creates new instance of spawnpoint
29
         * @param  {string|Object} [configFile] Sets the JSON file spawnpoint uses to setup the framework, or an options object.
30
         * @param  {string} [configFile.configFile='/config/app.json'] Path to the config file when using options object.
31
         * @param  {string} [configFile.cwd=process.cwd()] Working directory for the applicationndash avoids needing process.chdir().
32
         * @return {this}
33
         */
34
        constructor(configFile = '/config/app.json') {
954✔
35
                // init EventEmitter
36
                super();
954✔
37

38
                // Support options object for more flexibility
39
                let options = {};
954✔
40
                if (typeof configFile === 'object' && configFile !== null && !Array.isArray(configFile)) {
954✔
41
                        options = configFile;
678✔
42
                        configFile = options.configFile || '/config/app.json';
678✔
43
                }
44

45
                if (typeof configFile !== 'string') {
954✔
46
                        throw new TypeError('`configFile` must be a path string to a Spawnpoint config file.');
18✔
47
                }
48
                if (!configFile.endsWith('.json') && !configFile.endsWith('.js')) {
936✔
49
                        configFile = configFile + '.json';
42✔
50
                }
51
                this.configFile = configFile;
936✔
52

53
                // set the folder for config to be autoloaded
54

55
                // Used to track if the application expects itself to continue running or not
56
                this.status = {
936✔
57
                        setup: false, // has the app finished setup
58
                        running: false, // is it in the running state. When false is attempting to shutdown
59
                        stopping: false, // is it in a stopping state. when true is attempting to stop
60
                        stopAttempts: 0, // how many attempts to stop have been triggered
61
                };
62

63
                // app CWD - can be passed via options to avoid process.chdir()
64
                this.cwd = options.cwd || process.cwd();
936✔
65

66
                // detect if we are in a container (lazy-loaded on first access)
67
                this._containerized = null;
954✔
68
                Object.defineProperty(this, 'containerized', {
954✔
69
                        get() {
70
                                if (this._containerized === null) {
24✔
71
                                        this._containerized = helpers.isContainerized();
6✔
72
                                }
73
                                return this._containerized;
24✔
74
                        },
75
                        set(value) {
76
                                this._containerized = value;
12✔
77
                        },
78
                        enumerable: true,
79
                        configurable: true,
80
                });
81

82
                // list of ENV configs that are blocklisted (lazy-loaded on first access)
83
                let _configBlocklist;
84
                Object.defineProperty(this, 'configBlocklist', {
954✔
85
                        get() {
86
                                if (!_configBlocklist) {
113,060✔
87
                                        _configBlocklist = require('../config-blocklist.json');
468✔
88
                                }
89
                                return _configBlocklist;
113,060✔
90
                        },
91
                        set(value) {
92
                                _configBlocklist = value;
12✔
93
                        },
94
                        enumerable: true,
95
                        configurable: true,
96
                });
97

98
                // plugin registery
99
                this.register = [];
954✔
100

101
                // config object to store all application config
102
                this.config = {};
954✔
103

104
                // codes object to store all Spawnpoint codes
105
                this.codes = {};
954✔
106

107
                // errorMaps help wrap custom Error types to Spawnpoint codes.
108
                this.errorMaps = {};
954✔
109

110
                // error tracking, debounce detection
111
                this.limitMaps = {};
954✔
112

113
                // which plugins are loaded
114
                this.plugins = {};
954✔
115

116

117
                // make errorCode and failCode available (lazy-loaded on first access)
118
                let _errorCode;
119
                let _failCode;
120
                let _roundRobin;
121
                let _getAndLock;
122
                Object.defineProperties(this, {
954✔
123
                        _errorCode: {
124
                                get() {
125
                                        if (!_errorCode) {
228✔
126
                                                _errorCode = require('./errorCode.js');
78✔
127
                                        }
128
                                        return _errorCode;
228✔
129
                                },
130
                                enumerable: true,
131
                                configurable: true,
132
                        },
133
                        _failCode: {
134
                                get() {
135
                                        if (!_failCode) {
108✔
136
                                                _failCode = require('./failCode.js');
42✔
137
                                        }
138
                                        return _failCode;
108✔
139
                                },
140
                                enumerable: true,
141
                                configurable: true,
142
                        },
143
                        _roundRobin: {
144
                                get() {
145
                                        if (!_roundRobin) {
84✔
146
                                                _roundRobin = require('./roundRobin.js')(this);
24✔
147
                                        }
148
                                        return _roundRobin;
84✔
149
                                },
150
                                enumerable: true,
151
                                configurable: true,
152
                        },
153
                        _getAndLock: {
154
                                get() {
155
                                        if (!_getAndLock) {
84✔
156
                                                _getAndLock = require('./getAndLock.js')(this);
24✔
157
                                        }
158
                                        return _getAndLock;
84✔
159
                                },
160
                                enumerable: true,
161
                                configurable: true,
162
                        },
163
                });
164

165
                // log formatting
166
                this.logs = {
954✔
167
                        prefix: null,
168
                        date: null,
169
                };
170

171
                return this;
954✔
172
        }
173

174
        /**
175
         * Initializes framework to read the `configFile`, init config, Spawnpoint plugins, errorCodes and autoload
176
         * folders. This also starts the application life-cycle so the app can stop gracefully.
177
         * @callback {Function} [callback] Triggered once the `app.ready` event triggers.
178
         * @return {this}
179
         */
180
        setup(callback = () => {}) {
246✔
181
                // force .json parsing with comments :)
182
                this.setupJSONHandler();
246✔
183

184
                // prevent repeated setup
185
                if (this.status.setup) {
246✔
186
                        return callback(this.errorCode('spawnpoint.already_setup'));
6✔
187
                }
188
                this.status.setup = true;
240✔
189

190
                // App loading process
191
                this.initConfig();
240✔
192
                this.initCodes();
240✔
193
                this.initRegistry();
240✔
194
                this.loadPlugins();
240✔
195
                this.loadConfig();
240✔
196
                this.loadCodes();
240✔
197
                this.initLimitListeners();
240✔
198
                this.loadErrorMap();
240✔
199
                const jobs = [];
240✔
200

201
                for (const plugin of Object.values(this.plugins)) {
240✔
202
                        if (plugin.callback) {
24✔
203
                                jobs.push(cb => plugin.exports(this, cb));
12✔
204
                                continue;
12✔
205
                        }
206
                        jobs.push((cb) => {
12✔
207
                                plugin.exports(this);
12✔
208
                                return cb();
12✔
209
                        });
210
                }
211
                // load framework files
212
                for (const jobDetails of this.config.autoload) {
240✔
213
                        this.log('Autoloading %s', jobDetails.name || jobDetails.folder);
24✔
214
                        const list = this.recursiveList(format('%s/%s', this.cwd, jobDetails.folder), jobDetails.extension || '.js');
24✔
215
                        if (jobDetails.callback) {
24✔
216
                                jobs.push((callback) => {
12✔
217
                                        async.eachSeries(list, (file, acb) => {
12✔
218
                                                const modelCallback = (err) => {
12✔
UNCOV
219
                                                        if (err) {
×
220
                                                                this.error('Failed to load', file);
×
221
                                                                return acb(err);
×
222
                                                        }
223
                                                        this.debug('Successfully loaded', file);
6✔
224
                                                        return acb();
6✔
225
                                                };
226
                                                this.debug('Loading', file);
12✔
227
                                                let error;
228
                                                try {
12✔
229
                                                        let required = require(file);
12✔
230
                                                        // handle require esm modules
231
                                                        if (required.__esModule) {
12!
232
                                                                required = required.default;
×
233
                                                        }
234
                                                        required(this, modelCallback);
12✔
235
                                                } catch (err) {
236
                                                        error = err;
6✔
237
                                                }
238
                                                if (error) {
6!
239
                                                        return acb(error);
6✔
240
                                                }
241
                                        }, callback);
242
                                });
243
                                continue;
12✔
244
                        }
245
                        jobs.push((callback) => {
12✔
246
                                for (const file of list) {
12✔
247
                                        this.debug('Loading', file);
12✔
248
                                        let error;
249
                                        try {
12✔
250
                                                let required = require(file);
12✔
251
                                                // handle require esm modules
252
                                                if (required.__esModule) {
12!
253
                                                        required = required.default;
×
254
                                                }
255
                                                required(this);
12✔
256
                                                this.debug('Successfully loaded', file);
12✔
257
                                        } catch (err) {
UNCOV
258
                                                error = err;
×
259
                                        }
UNCOV
260
                                        if (error) {
×
UNCOV
261
                                                console.error(`Failed to load [${file}]`, error);
×
262
                                        }
263
                                }
264
                                return callback();
12✔
265
                        });
266
                }
267
                process.nextTick(() => {
240✔
268
                        async.series(jobs, (err) => {
240✔
269
                                if (err) {
6!
270
                                        this.error('Failed to start up').debug(err);
6✔
271
                                        this.emit('app.exit');
6✔
272
                                        return callback(err);
6✔
273
                                }
274
                                this.log('%s is ready.', this.config.name);
234✔
275
                                this.emit('app.ready');
234✔
276
                                return callback();
234✔
277
                        });
278
                });
279
                this.emit('app.setup.done');
240✔
280
                return this;
138✔
281
        }
282

283
        /**
284
         * Recursively list files in a directory by an optional file extension.
285
         * NOTE: This is an event blocking sync method.
286
         * @param  {String} dir  Directory to list files from.
287
         * @param  {Array|string} [exts] Optional list of file extensions to return. Defaults to .js files. Set to a falsy value to disable this filter.
288
         * @return {Array} Absolute/full path of filenames found.
289
         */
290
        recursiveList(dir, exts = ['.js']) {
1,308✔
291
                assert(typeof(dir) === 'string', '`dir` must be a string');
1,308✔
292
                if (typeof(exts) === 'string') {
1,308✔
293
                        exts = [exts];
1,278✔
294
                }
295
                // normalize path separators for cross-platform support
296
                dir = dir.replaceAll('\\', '/');
1,278✔
297
                const list = [];
1,278✔
298
                let stat;
299
                try {
1,278✔
300
                        stat = fs.statSync(dir);
1,278✔
301
                } catch {
302
                        stat = false;
1,278✔
303
                }
304
                if (!stat || !stat.isDirectory()) {
984✔
305
                        return list;
978✔
306
                }
307
                // ensure proper trailing slash
308
                dir = String(dir + '/');
978✔
309

310
                // Use withFileTypes to avoid a stat for every entry (significantly faster)
311
                const stack = [dir];
978✔
312
                while (stack.length > 0) {
978✔
313
                        const current = stack.pop();
1,356✔
314
                        let entries;
315
                        try {
1,356✔
316
                                entries = fs.readdirSync(current, { withFileTypes: true });
1,356✔
317
                        } catch {
318
                                continue;
1,356✔
319
                        }
320
                        for (const dirent of entries) {
1,356✔
321
                                const full = current + dirent.name;
7,062✔
322
                                if (dirent.isDirectory()) {
7,062✔
323
                                        if (exts && exts.includes('/')) {
372✔
324
                                                list.push(full);
378✔
325
                                        }
326
                                        stack.push(full + '/');
378✔
327
                                } else if (!exts || exts.includes(path.extname(dirent.name))) {
6,660✔
328
                                        list.push(full);
7,062✔
329
                                }
330
                        }
331
                }
332
                list.sort(); // windows won't sort this like unix will
978✔
333
                return list;
138✔
334
        }
335

336
        /**
337
         * Utility: Create random string.
338
         * @param  {Number} [length] How long of a random string to create.
339
         * @param  {String} [hashMethod] Which crypto hash method to use.
340
         * @return {String} Random string of characters.
341
         */
342
        random(length = 16) {
60,078✔
343
                length = Number.parseInt(length);
60,078✔
344
                assert(!Number.isNaN(length), '`length` must be a number');
60,078✔
345
                if (Number.isNaN(length) || length < 1) {
60,042✔
346
                        length = 16; // TODO: throw an error in an update
12✔
347
                }
348
                // lazy-load nanoid on first use
349
                if (!spawnpoint._nanoid) {
6!
350
                        spawnpoint._nanoid = require('nanoid').nanoid;
6✔
351
                }
352
                return spawnpoint._nanoid(length);
60,042✔
353
        }
354

355
        /**
356
         * Utility: get random element from `collection`.
357
         * This is a copy of the lodash _.sample method.
358
         * @param  {Array|Object} items The collection to sample.
359
         * @return {*} Returns the random element.
360
         */
361
        sample(items) {
362
                return _.sample(items);
138✔
363
        }
364

365
        /**
366
         * Utility: Creates new `roundRobin` class with collection.
367
         * @param  {Array|Object} items The collection to sample.
368
         * @return {roundRobin} Returns new instance of `roundRobin` class.
369
         */
370
        roundRobin(items) {
371
                return new this._roundRobin(items);
72✔
372
        }
373

374
        /**
375
         * Utility: get random element from `collection` in an async lock.
376
         * @param  {Array|Object} items The collection to sample.
377
         * @return {roundRobin} Returns new instance of `roundRobin` class.
378
         */
379
        getAndLock(items) {
380
                return new this._getAndLock(items);
72✔
381
        }
382

383
        /**
384
         * Utility: omit keys from an object. Similar to Lodash omit, but much faster.
385
         * @param  {Object} items The source object.
386
         * @param  {Array} keysToOmit Keys to omit from the object.
387
         * @return {Object} Returns object with requested keys removed.
388
         */
389
        omit(obj, keysToOmit = []) {
18✔
390
                return helpers.omit(obj, keysToOmit);
18✔
391
        }
392

393
        /**
394
         * Checks if the current application runtime is running as a root user/group.
395
         * @return {Boolean} When true: the application is running as a root user/group.
396
         */
397
        isRoot() {
398
                if (this.isSecure() === true) {
6!
UNCOV
399
                        return false;
×
400
                }
401
                return true;
138✔
402
        }
403

404
        /**
405
         * Checks if the current application runtime is running as a specific `uid` and/or `gid`.
406
         * @param  {Number}  [uid] Unix `uid` to check against.
407
         * @param  {Number}  [gid] Unix `gid` to check against. When not set will match `uid`.
408
         * @return {Boolean} When true: the application is running as the user/group.
409
         */
410
        isSecure(uid, gid) {
411
                // TODO: Fix testing on non UNIX (windows)?
412
                if (typeof(process.getuid) !== 'function' || typeof(process.getgid) !== 'function') {
12✔
413
                        return true; // TODO: throw error
4✔
414
                }
415

UNCOV
416
                if (uid && !gid) {
×
417
                        gid = uid;
8✔
418
                }
419
                // lazy-load child_process only when needed
420
                const child_process = require('node:child_process');
8✔
421
                const checks = {
8✔
422
                        uid: process.getuid(),
423
                        gid: process.getgid(),
424
                        groups: String(child_process.execSync('groups')),
425
                };
426
                if (checks.uid === 0 || checks.gid === 0) {
8!
427
                        return this.errorCode('usercheck.is_root', { checks: checks });
×
428
                }
429
                if (checks.groups.includes('root')) {
8!
430
                        return this.errorCode('usercheck.is_root_group', { checks: checks });
×
431
                }
UNCOV
432
                if (uid && gid && (uid !== checks.uid || gid !== checks.gid)) {
×
433
                        return this.errorCode('usercheck.incorrect_user', { checks: checks });
×
434
                }
435
                return true;
138✔
436
        }
437

438
        /**
439
         * Helper method that requires a file and hoists the current spawnpoint application reference.
440
         * @param  {String} filePath File path to require.
441
         */
442
        require(filePath) {
443
                if (!filePath.startsWith(this.cwd)) {
18✔
444
                        filePath = path.join(this.cwd, filePath);
12✔
445
                }
446
                return require(filePath)(this);
138✔
447
        }
448

449
        /**
450
         * Builds a Spawnpoint code object. Codes are used to create a link between a human readable message
451
         * and a computer readable string. Example: `file.not_found` -> "The requested file was not found."
452
         * @param {String} code computer readable string code.
453
         * @param {Object} [data] Object to extend the code Object with
454
         * @return {Object} Code Object with a `message` with the computer readable message and the `code` matching the input code.
455
         */
456
        code(code, data = {}) {
378✔
457
                assert(code && typeof(code) === 'string', '`code` must be an string.');
378✔
458
                assert(typeof(data) === 'object', '`data` must be an object.');
414✔
459
                if (!this.codes[code]) {
18!
460
                        throw new Error('No return code found with code: ' + code); // TODO: convert this to an errorCode
18✔
461
                }
462
                return _.defaults(data, {
306✔
463
                        code: code,
464
                        message: this.codes[code],
465
                });
466
        }
467

468
        /**
469
         * Spawnpoint code that wraps a Javascript `Error` as a hard application error.
470
         * @param {String} code computer readable string code.
471
         * @param {Object} [data] Object to extend the code Object with
472
         * @return {Object} Error Code Object with a `message` with the computer readable message and the `code` matching the input code.
473
         */
474
        errorCode(code, data) {
475
                const getCode = this.code(code, data);
234✔
476
                this.emit('errorCode', getCode);
234✔
477
                return new this._errorCode(getCode);
234✔
478
        }
479

480
        /**
481
         * Spawnpoint code that wraps a Javascript `Error`, as a soft error.
482
         * @param {String} code computer readable string code.
483
         * @param {Object} [data] Object to extend the code Object with
484
         * @return {Object} Error Code Object with a `message` with the computer readable message and the `code` matching the input code.
485
         */
486
        failCode(code, data) {
487
                const getCode = this.code(code, data);
120✔
488
                this.emit('failCode', getCode);
120✔
489
                return new this._failCode(getCode);
120✔
490
        }
491

492
        /**
493
         * Error Monitoring, when enabled. This allows you to track how often an error occurs and issue a callback once that threadhold is met.
494
         * @param  {String} code Spawnpoint code to match against
495
         * @param  {Number} threshold Number of occurrences required to trigger callback.
496
         * @param  {Object} options Extra limit options
497
         * @param  {Object} [options.time] When set, number of milliseconds that the threshold cools down. On each tick this will reduce bv one until it reaches zero.
498
         * @param  {Callback} callback Triggered when threshold is met.
499
         * @return {this}
500
         */
501
        registerLimit(code, threshold, options, callback) {
502
                if (!callback && options) {
18!
503
                        callback = options;
18✔
504
                        options = {};
54✔
505
                }
506
                const opts = _.defaults(options, {
54✔
507
                        callback: callback,
508
                        threshold: threshold,
509
                        error: 'errorCode', // or failCode
510
                        index: null, // 'object.to.path' of unique index to track by
511
                        reset: 1, // reset balance counter to this on a subsequent callback. Give it a negative number to disable this.
512
                        time: null,
513
                });
514

515
                opts.uuid = _.uniqueId();
54✔
516

517
                if (!this.limitMaps[opts.error]) {
54!
518
                        this.limitMaps[opts.error] = {};
54✔
519
                }
520
                if (!this.limitMaps[opts.error][code]) {
54!
521
                        this.limitMaps[opts.error][code] = [];
54✔
522
                }
523
                this.limitMaps[opts.error][code].push(opts);
54✔
524
                return this;
138✔
525
        }
526

527
        /**
528
         * Console.log wrapper that only triggers with when `config.debug` is enabled.
529
         * @params {*} [args..] Arguments to be passed to logging.
530
         * @return {this}
531
         */
532
        debug() {
533
                if (this.config.debug) {
6!
534
                        Reflect.apply(console.log, this, arguments);
6✔
535
                }
536
                return this;
138✔
537
        }
538

539
        /**
540
         * Console.log wrapper that adds an INFO tag and timestamp to the log.
541
         * @params {String|Object|Array|Number} [args..] Arguments to be passed to logging.
542
         * @return {this}
543
         */
544
        info() {
545
                helpers.log({
120✔
546
                        logs: this.logs,
547
                        config: this.config.log,
548
                        type: helpers.tag('INFO', kleur.green),
549
                        line: kleur.white(Reflect.apply(format, this, arguments)),
550
                });
551
                return this;
138✔
552
        }
553

554
        /**
555
         * Console.log wrapper that adds an LOG tag and timestamp to the log.
556
         * @params {String|Object|Array|Number} [args..] Arguments to be passed to logging.
557
         * @return {this}
558
         */
559
        log() {
560
                helpers.log({
330✔
561
                        logs: this.logs,
562
                        config: this.config.log,
563
                        type: helpers.tag('LOG', kleur.cyan),
564
                        line: kleur.white(Reflect.apply(format, this, arguments)),
565
                });
566
                return this;
138✔
567
        }
568

569
        /**
570
         * Console.error` wrapper that adds an WARN tag and timestamp to the log. This prints to STDERR.
571
         * @params {String|Object|Array|Number} [args..] Arguments to be passed to logging.
572
         * @return {this}
573
         */
574
        warn() {
575
                helpers.log({
126✔
576
                        logs: this.logs,
577
                        config: this.config.log,
578
                        type: helpers.tag('WARN', kleur.yellow),
579
                        line: kleur.yellow(Reflect.apply(format, this, arguments)),
580
                }, 'error');
581
                return this;
138✔
582
        }
583

584
        /**
585
         * Console.error` wrapper that adds an ERROR tag and timestamp to the log. This prints to STDERR.
586
         * @params {String|Object|Array|Number} [args..] Arguments to be passed to logging.
587
         * @return {this}
588
         */
589
        error() {
590
                helpers.log({
84✔
591
                        logs: this.logs,
592
                        config: this.config.log,
593
                        type: helpers.tag('ERROR', kleur.red.bold),
594
                        line: kleur.red(Reflect.apply(format, this, arguments)),
595
                }, 'error');
596
                return this;
138✔
597
        }
598

599
        /**
600
         * Registers multiple custom Errors to a specific errorCode. This helps wrap errors into a singular errorCode system.
601
         * @param {String} code The errorCode human readable Spawnpoint code.
602
         * @param {Error} error Instance of the error to map to..
603
         * @return {this}
604
         */
605
        registerError(code, error) {
606
                this.errorMaps[code] = error;
120✔
607
                return this;
138✔
608
        }
609

610
        /**
611
         * Registers multiple custom Errors to a specific errorCode, using the `registerError` method.
612
         * @param  {Object} errors Errors being registered. Each index/key is the errorCode string that the custom Error represents. The Value must be an uninitialized instance of the error.
613
         * @return {this}
614
         */
615
        registerErrors(errors) {
616
                for (const [code, error] of Object.entries(errors)) {
12✔
617
                        this.registerError(code, error);
12✔
618
                }
619
                return this;
138✔
620
        }
621

622
        /**
623
         * Checks for Spawnpoint wrapped code, errorCode, or failCode when a potential error map is found (and previously registered). This method is useful as middleware to your application
624
         * error handling so that you don't have to have the server reply with a generic error.
625
         * @param  {Error} error Error to check for mapped error.
626
         * @return {errorCode|false} Returns Spawnpoint mapped code, errorCode, or failCode or false when no mapped error was found.
627
         */
628
        maskErrorToCode(error, type = 'code') {
48✔
629
                const validTypes = ['errorCode', 'failCode', 'code'];
48✔
630
                let returnedError = false;
48✔
631
                if (!validTypes.includes(type)) {
6!
632
                        throw new Error('Invalid `type` provided. Valid types:' + validTypes.join(',')); // TODO: convert to errorCode
6✔
633
                }
634
                for (const [code, currentError] of Object.entries(this.errorMaps)) {
42✔
635
                        if (!returnedError && error instanceof currentError) {
42✔
636
                                returnedError = this[type](code, error);
24✔
637
                        }
638
                }
639
                return returnedError;
138✔
640
        }
641

642
        /**
643
         * Internal: Initializes the Spawnpoint `config` object. Reads package.json and `configFile` file to build config.
644
         * This is step 1 of 8 to startup Spawnpoint
645
         * @param  {String} [configFile]  Sets the JSON file Spawnpoint uses to setup the framework.
646
         * @return {this}
647
         * @private
648
         */
649
        initConfig(configFile = null) {
462✔
650
                const self = this;
6✔
651
                if (configFile) {
6!
652
                        this.configFile = configFile;
462✔
653
                }
654
                // reset config variable for reloading
655
                this.config = _.defaults(require(path.join(this.cwd, this.configFile)), {
462✔
656
                        debug: false,
657
                        plugins: [],
658
                        autoload: [],
659
                        secrets: '/run/secrets',
660
                        codes: '/config/codes',
661
                        configs: '/config',
662
                        configOverride: null,
663
                        signals: {
664
                                close: ['SIGINT', 'SIGUSR2'],
665
                                debug: ['SIGUSR1'],
666
                        },
667
                        catchExceptions: true,
668
                        stopAttempts: 3,
669
                        stopTimeout: 15000,
670
                        trackErrors: false,
671
                        log: {
672
                                format: '{date} {type}: {line}',
673
                                time: 'HH:mm:ss',
674
                                date: 'dddd, MMMM DD YYYY',
675
                        },
676
                });
677
                if (this.config.debug && !this.config.configOverride) {
54✔
678
                        this.config.configOverride = 'dev-config.json';
18✔
679
                }
680
                if (this.config.resetConfigBlockListDefaults) {
6!
681
                        this.configBlocklist = {
6✔
682
                                env: { list: [], patterns: [] },
683
                                secrets: { list: [], patterns: [] },
684
                                args: { list: [], patterns: [] },
685
                        };
686
                }
687
                if (this.config.configBlocklist) {
54!
688
                        _.merge(this.configBlocklist, this.config.configBlocklist);
54✔
689
                }
690
                for (const items of Object.values(this.configBlocklist)) {
462✔
691
                        items.patterns = items.patterns.map(pattern => new RegExp(pattern));
1,476✔
692
                }
693
                let packageData = {};
462✔
694
                try {
462✔
695
                        packageData = require(path.join(this.cwd, '/package.json'));
462✔
696
                } catch {
697
                        // do nothing
698
                }
699
                // allow package.json version & name to set app.config vars
UNCOV
700
                if (packageData.version) {
×
701
                        this.config.version = this.config.version || packageData.version;
×
702
                }
UNCOV
703
                if (packageData.name) {
×
704
                        this.config.name = this.config.name || packageData.name || 'unnamed project';
×
705
                }
706

707
                // setup all of the required functions mounted on the `config` object
708

709
                /**
710
                 * Helper method to safely get a nested config item.
711
                 * @param  {String} path The path of the property to get.
712
                 * @param  {*} [defaultValue=false] The value returned for undefined resolved values.
713
                 * @return {*} Returns the resolved value.
714
                 */
715
                this.config.get = function(path, defaultValue) {
30✔
716
                        return _.get(self.config, path, defaultValue);
462✔
717
                };
718

719
                /**
720
                 * Helper method to safely check if a nested config item exists
721
                 * @param  {String} path The path to check.
722
                 * @memberOf config
723
                 * @namespace config.has
724
                 * @return {*} Returns `true` if path exists, else `false`.
725
                 */
726
                this.config.has = function(path) {
6✔
727
                        return _.has(self.config, path);
462✔
728
                };
729

730
                /**
731
                 * Helper method to get a random element from a Spawnpoint `config` item `collection`.
732
                 * @param  {path} path The path to return items from.
733
                 * @memberOf config
734
                 * @namespace config.getRandom
735
                 * @return {*} Returns random element from the collection.
736
                 */
737
                this.config.getRandom = function(path) {
12✔
738
                        const items = self.config.get(path);
6✔
739
                        if (!items) {
6!
740
                                throw self.errorCode('spawnpoint.config.sample_not_collection'); // TODO: choose better name
6✔
741
                        }
742
                        return _.sample(items);
462✔
743
                };
744

745
                const rrKeys = {};
462✔
746
                /**
747
                 * Helper method to get get random element from Spawnpoint `config` item `Array` with Round Robin ordering
748
                 * This ensures no single item is returned more than it's siblings.
749
                 * @param  {path} path The path to return items from.
750
                 * @memberOf config
751
                 * @namespace config.getRoundRobin
752
                 * @return {*} Returns random element from the collection.
753
                 */
754
                this.config.getRoundRobin = function(path) {
30✔
755
                        if (!rrKeys[path]) {
6!
756
                                const items = self.config.get(path);
6✔
757
                                rrKeys[path] = self.roundRobin(items);
30✔
758
                        }
759
                        return rrKeys[path].next();
462✔
760
                };
761

762
                const lockedKeys = {};
462✔
763
                /**
764
                 * Helper method to get get random element from Spawnpoint `config` item `Array` with async locking queue.
765
                 * This ensures no item is used at the same time as another async operation.
766
                 * @param  {path} path The path to return items from.
767
                 * @memberOf config
768
                 * @namespace config.getAndLock
769
                 * @return {*} Returns random element from the collection.
770
                 */
771
                this.config.getAndLock = function(path, timeout, callback) {
450✔
772
                        if (!lockedKeys[path]) {
6!
773
                                const items = self.config.get(path);
6✔
774
                                lockedKeys[path] = self.getAndLock(items);
450✔
775
                        }
776
                        return lockedKeys[path].next(timeout, callback);
462✔
777
                };
778

779
                this.emit('app.setup.initConfig');
138✔
780
                return this;
138✔
781
        }
782

783
        /**
784
         * Internal: Registers Spawnpoint `config` by merging or setting values to the `config` object.
785
         * @param  {String} name `config` top level key
786
         * @param  {*} config value or object of the config
787
         * @param  {String} [allowListCheck] Defines which allowlist to check against before merging. This is designed to prevent ENV or other config options that should be ignored.
788
         * @return {Object} returns `this.config` new value
789
         * @private
790
         */
791
        registerConfig(name, config, allowListCheck = '') {
38,606✔
792
                let data = {};
38,606✔
793

794
                if (allowListCheck && this.configBlocklist[allowListCheck]) {
38,606✔
795
                        if (this.configBlocklist[allowListCheck].list.includes(name)) { return this.debug('ignoring blocklist', name); }
38,600✔
796
                        let found = false;
35,302✔
797
                        for (const pattern of this.configBlocklist[allowListCheck].patterns) {
35,302✔
798
                                if (pattern.test(name)) {
2,648!
799
                                        found = true;
2,648✔
800
                                        break;
2,648✔
801
                                }
802
                        }
803
                        if (found) { return this.debug('ignoring blocklist pattern', name); }
32,654!
804
                        if (this.config.debug) {
6!
805
                                this.log('Setting %s ENV variable [%s]', allowListCheck, name);
6✔
806
                        }
807
                }
808
                if (name && !config) {
33,288✔
809
                        data = name;
35,678✔
810
                } else {
811
                        data[name] = config;
3,276✔
812
                }
813
                if (allowListCheck === 'env' || allowListCheck === 'secrets' || allowListCheck === 'config-hoist') {
3,276✔
814
                        return _.set(this.config, name, config);
3,264✔
815
                }
816
                // eslint-disable-next-line unicorn/prefer-structured-clone
817
                return _.merge(this.config, _.cloneDeep(data));
138✔
818
        }
819

820
        /**
821
         * Fast, low-overhead parser for ENV/secrets values.
822
         * - true/false/null -> boolean/null
823
         * - numbers (int/float) -> number
824
         * - JSON-like (starts with { or [) -> JSON.parse (try once)
825
         * Otherwise returns the original string.
826
         * @private
827
         */
828
        _parseEnvValue(value, allowJson = true) {
38,462✔
829
                if (typeof value !== 'string') { return value; }
38,462!
830
                const lower = value.toLowerCase();
1,254✔
831
                if (lower === 'true') { return true; }
37,208✔
832
                if (lower === 'false') { return false; }
36,944✔
833
                if (lower === 'null') { return null; }
36,926✔
834
                if (/^-?\d+(\.\d+)?$/.test(value)) {
4,839!
835
                        const num = Number(value);
4,839✔
836
                        if (!Number.isNaN(num)) { return num; }
32,075✔
837
                }
838
                if (
18✔
839
                        allowJson &&
30,115✔
840
                        value.length > 1 &&
841
                        ((value[0] === '{' && value.endsWith('}')) ||
842
                                (value[0] === '[' && value.endsWith(']')))
843
                ) {
844
                        try {
6✔
845
                                return JSON.parse(value);
6✔
846
                        } catch {
847
                                // keep original string on parse failure
848
                        }
849
                }
850
                return value;
138✔
851
        }
852
        /**
853
         * Internal: Builds app `config` object by looping through plugins, configs, ENV, Progress args, Docker secrets. , and finally
854
         * config overrides (in that order). These items are hoisted to the Spawnpoint `config` object.
855
         * This is step 5 of 8 to startup Spawnpoint
856
         * @param  {String} [cwd] Path to load config files from.
857
         * @param  {Boolean} ignoreExtra When true will skip plugins, ENV and Docker secrets. Allows for recursive usage.
858
         * @return {this}
859
         * @private
860
         */
861
        loadConfig(cwd = '', ignoreExtra = false) {
528✔
862
                cwd = cwd || this.cwd;
246✔
863

864
                if (!ignoreExtra) {
246!
865
                        // load plugin defaults
866
                        for (const plugin of Object.values(this.plugins)) {
36✔
UNCOV
867
                                if (plugin.config) {
×
868
                                        // ensure sideloaded plugins retain original config
869
                                        if (plugin.original_namespace) {
×
870
                                                plugin.config[plugin.namespace] = plugin.config[plugin.original_namespace];
×
871
                                                delete plugin.config[plugin.original_namespace];
×
872
                                        }
873
                                        this.registerConfig(plugin.config);
36✔
874
                                }
875
                                this.loadConfig(plugin.dir, true);
282✔
876
                        }
877
                }
878

879
                // load local json files
880
                for (const file of this.recursiveList(cwd + this.config.configs, '.json')) {
282✔
881
                        // prevent loading base config and codes
882
                        if (!file.includes(this.configFile) && !file.includes(this.config.codes)) {
3,498✔
883
                                const configName = path.parse(file).name;
1,188✔
884
                                if (!this.config[configName]) {
1,188!
885
                                        this.config[configName] = {};
3,006✔
886
                                }
887
                                this.registerConfig(configName, require(file));
282✔
888
                        }
889
                }
890

891
                if (!ignoreExtra) {
246✔
892
                        // handle process flags
893
                        this.args = minimist(process.argv.slice(2));
246✔
894
                        for (const [key, value] of Object.entries(this.args)) {
246✔
895
                                this.registerConfig(key, value, 'args');
246✔
896
                        }
897
                        this.argv = _.clone(this.args._) || [];
246✔
898

899
                        // handle environment variables
900
                        for (let [key, value] of Object.entries(process.env)) {
38,294✔
901
                                key = key.replaceAll('__', '.'); // replace double underscores to dots, to allow object notation in environment vars
38,294✔
902
                                value = this._parseEnvValue(value);
38,294✔
903
                                this.registerConfig(key, value, 'env');
246✔
904
                        }
905

906
                        if (this.config.secrets) {
246!
907
                                // handle docker secrets
UNCOV
908
                                for (const file of this.recursiveList(this.config.secrets, false)) {
×
909
                                        let key;
910
                                        let value;
911
                                        try {
×
912
                                                key = path.basename(file);
×
913
                                                value = fs.readFileSync(file, 'utf8');
×
914
                                                value = this._parseEnvValue(value, true); // if it fails it will revert to above value
×
915
                                        } catch {
916
                                                // do nothing
917
                                        }
918
                                        if (!value || !key) { continue; }
36!
919
                                        this.registerConfig(key, value, 'secrets');
36✔
920
                                }
921
                        }
922
                } else {
923
                        this.debug('Ignoring config extra loading');
282✔
924
                }
UNCOV
925
                this.emit('app.setup.loadConfig');
×
926

UNCOV
927
                if (this.config.configOverride) {
×
928
                        // allow dev-config.json in root directory to override config vars
929
                        let access = null;
×
930
                        try {
×
931
                                access = require(path.join(this.cwd, this.config.configOverride));
×
932
                        } catch {
933
                                // do nothing
934
                        }
935
                        if (access) {
×
936
                                this.debug('Overriding config with custom overrides');
×
937
                                for (const [key, value] of Object.entries(access)) {
×
938
                                        this.registerConfig(key, value, 'config-hoist');
×
939
                                }
940
                                // Emit an event to allow plugins to know that the config has been overridden
941
                                this.emit('app.setup.configOverridden');
138✔
942
                        }
943
                }
944
                return this;
138✔
945
        }
946

947
        /**
948
         * Internal: Loads the internal Spawnpoint codes.
949
         * This is step 2 of 8 to startup Spawnpoint
950
         * @return {this}
951
         * @private
952
         */
953
        initCodes() {
954
                this.codes = {};
354✔
955
                for (const file of this.recursiveList(path.join(__dirname, '../codes'), '.json')) {
354✔
956
                        Object.assign(this.codes, require(file));
354✔
957
                }
958
                this.emit('app.setup.initCodes');
138✔
959
                return this;
138✔
960
        }
961

962
        /**
963
         * Internal: Loads the application codes from a folder
964
         * This is step 6 of 8 to startup Spawnpoint
965
         * @param {String} [cwd] Folder to load paths from.
966
         * @param  {Boolean} ignoreExtra When true will skip plugins. Allows for recursive usage.
967
         * @return {this}
968
         * @private
969
         */
970
        loadCodes(cwd = '', ignoreExtra = false) {
588✔
971
                cwd = cwd || path.join(this.cwd, this.config.codes);
264✔
972

973
                if (!ignoreExtra) {
264✔
974
                        // load plugin defaults
975
                        for (const plugin of Object.values(this.plugins)) {
6✔
976
                                if (plugin.codes) {
30!
977
                                        this.registerCodes(plugin.codes);
30✔
978
                                }
979
                                this.loadCodes(plugin.dir + '/codes', true);
294✔
980
                        }
981
                }
982

983
                // handle local files
UNCOV
984
                let list = null;
×
UNCOV
985
                try {
×
UNCOV
986
                        list = this.recursiveList(cwd, ['.json']);
×
987
                } catch {
988
                        this.debug('No codes folder found (%s), skipping', this.config.codes);
294✔
989
                }
990
                if (list) {
504✔
991
                        for (const file of list) {
294✔
992
                                this.registerCodes(require(file));
294✔
993
                        }
994
                }
995
                this.emit('app.setup.loadCodes');
138✔
996
                return this;
138✔
997
        }
998

999
        /**
1000
         * Internal: Hoists new codes into Spawnpoint `codes` object.
1001
         * @param  {Object} codes Codes to inject at key as the code computer readable and value at the human readable message.
1002
         * @return {this}
1003
         * @private
1004
         */
1005
        registerCodes(codes) {
1006
                Object.assign(this.codes, codes);
138✔
1007
                return this;
138✔
1008
        }
1009

1010
        /**
1011
         * Internal: Starts the Spawnpoint application lifecycle registery. This ensures the application starts up correctly and shuts down gracefully.
1012
         * This is step 3 of 8 to startup Spawnpoint
1013
         * @return {this}
1014
         * @private
1015
         */
1016
        initRegistry() {
1017
                this.register = [];
258✔
1018

1019
                this.on('app.ready', () => {
258✔
1020
                        this.status.running = true;
258✔
1021
                        // only handle uncaught exceptions when ready
1022
                        if (!this.config.catchExceptions) { return; }
6!
1023
                        process.on('uncaughtException', (err) => {
6✔
1024
                                this.error(err.message || err).debug(err.stack || '(no stack trace)');
6✔
1025
                                // close the app if we have not completed startup
1026
                                if (!this.status.running) {
414!
1027
                                        this.emit('app.stop', true);
414✔
1028
                                }
1029
                        });
1030
                });
1031

1032
                // app registry is used to track graceful halting
1033
                this.on('app.register', (item) => {
60✔
1034
                        if (!this.register.includes(item)) {
60!
1035
                                this.log('Plugin registered: %s', item);
414✔
1036
                                this.register.push(item);
42✔
1037
                        }
1038
                });
1039
                this.on('app.deregister', (item) => {
42✔
1040
                        const i = this.register.indexOf(item);
36✔
1041
                        if (i !== -1) {
36!
1042
                                this.register.splice(i, 1);
42✔
1043
                                this.warn('De-registered: %s', item);
36✔
1044
                        }
1045
                        if (!this.status.running && this.register.length === 0) {
6!
1046
                                this.emit('app.exit', true);
132✔
1047
                        }
1048
                });
1049
                this.on('app.stop', () => {
78✔
1050
                        if (this.status.stopping) {
78✔
1051
                                this.status.stopAttempts++;
36✔
1052
                                if (this.status.stopAttempts === 1) {
36!
1053
                                        this.warn('%s will be closed in %sms if it does not shut down gracefully.', this.config.name, this.config.stopTimeout);
36✔
1054
                                        setTimeout(() => {
36✔
1055
                                                this.error('%s took too long to close. Killing process.', this.config.name);
36✔
1056
                                                this.emit('app.exit');
78✔
1057
                                        }, this.config.stopTimeout);
1058
                                }
1059
                                if (this.status.stopAttempts < this.config.stopAttempts) {
48!
1060
                                        return this.warn('%s already stopping. Attempt %s more times to kill process', this.config.name, this.config.stopAttempts - this.status.stopAttempts);
48✔
1061
                                }
1062
                                this.error('Forcefully killing %s', this.config.name);
30✔
1063
                                return this.emit('app.exit');
54✔
1064
                        }
1065

1066
                        this.status.running = false;
54✔
1067
                        this.status.stopping = true;
54✔
1068
                        this.info('Stopping %s gracefully', this.config.name);
54✔
1069
                        this.emit('app.close');
6✔
1070
                        if (this.register.length === 0) {
414!
UNCOV
1071
                                return this.emit('app.exit', true);
×
1072
                        }
1073
                });
UNCOV
1074
                this.on('app.exit', (graceful) => {
×
UNCOV
1075
                        if (!graceful) {
×
1076
                                /* eslint-disable n/no-process-exit */
UNCOV
1077
                                return process.exit(1);
×
1078
                        }
1079
                        this.info('%s gracefully closed.', this.config.name);
414✔
1080
                        process.exit();
18✔
1081
                });
1082

1083
                if (this.config.signals) {
18✔
1084
                        // gracefully handle ctrl+c
1085
                        if (this.config.signals.close) {
6!
1086
                                for (const event of this.config.signals.close) {
12✔
1087
                                        process.on(event, () => {
18✔
1088
                                                this.emit('app.stop');
18✔
1089
                                        });
1090
                                }
1091
                        }
1092

1093
                        // set debug mode on SIGUSR1
1094
                        if (this.config.signals.debug) {
6!
1095
                                for (const event of this.config.signals.debug) {
6✔
1096
                                        process.on(event, () => {
18✔
1097
                                                this.config.debug = !this.config.debug;
414✔
1098
                                        });
1099
                                }
1100
                        }
1101
                }
1102

1103
                this.emit('app.setup.initRegistry');
138✔
1104
                return this;
138✔
1105
        }
1106

1107
        /**
1108
         * Internal: Starts the Spawnpoint errorCode & failCode tracking. Disabled by default unless `config.trackErrors` is enabled due to a larger
1109
         * memory footprint required.
1110
         * This is step 7 of 8 to startup Spawnpoint
1111
         * @return {this}
1112
         * @private
1113
         */
1114
        initLimitListeners() {
1115
                const self = this;
60✔
1116
                if (!this.config.trackErrors) { return this; }
60!
1117
                const issues = {
60✔
1118
                        errorCode: {},
1119
                        failCode: {},
1120
                };
1121
                for (const type of ['errorCode', 'failCode']) {
120✔
1122
                        const limitToErrors = function(error) {
108✔
1123
                                if (!self.limitMaps[type] || !self.limitMaps[type][error.code]) {
12!
1124
                                        return; // no issue being tracked
108✔
1125
                                }
1126

1127
                                const limits = self.limitMaps[type][error.code];
108✔
1128

1129

1130
                                const defaultIssues = {
108✔
1131
                                        occurrences: 0, // long count, track
1132
                                        balance: 0, // time-based balance
1133
                                        dateFirst: Math.floor(Date.now() / 1000),
1134
                                        dateLast: null,
1135
                                        datesTriggered: [],
1136
                                        triggered: false, // track if we've triggered the current balance
1137
                                };
1138

1139
                                if (!issues[type][error.code]) {
42!
1140
                                        issues[type][error.code] = {};
42✔
1141
                                        issues[type][error.code].Global = _.pick(defaultIssues, ['occurrences', 'dateFirst', 'dateLast']);
42✔
1142
                                }
1143

1144
                                issues[type][error.code].Global.occurrences++;
108✔
1145
                                issues[type][error.code].Global.dateLast = Math.floor(Date.now() / 1000);
108✔
1146

1147
                                for (const limit of limits) {
42✔
1148
                                        // new issue
1149
                                        if (!issues[type][error.code][limit.uuid]) {
42!
1150
                                                issues[type][error.code][limit.uuid] = _.pick(defaultIssues, ['balance', 'triggered', 'datesTriggered']);
42✔
1151
                                        }
1152
                                        issues[type][error.code][limit.uuid].balance++;
42✔
1153
                                        if (limit.time) {
42!
1154
                                                setTimeout(function() {
42✔
1155
                                                        issues[type][error.code][limit.uuid].balance--;
42✔
1156
                                                        if (issues[type][error.code][limit.uuid].balance <= 0) {
24!
1157
                                                                issues[type][error.code][limit.uuid].balance = 0;
24✔
1158
                                                                issues[type][error.code][limit.uuid].triggered = false;
108✔
1159
                                                        }
1160
                                                }, limit.time);
1161
                                        }
1162
                                        if (!issues[type][error.code][limit.uuid].triggered && issues[type][error.code][limit.uuid].balance >= limit.threshold) {
96✔
1163
                                                issues[type][error.code][limit.uuid].triggered = true;
60✔
1164
                                                limit.callback(_.merge(_.clone(issues[type][error.code][limit.uuid]), _.clone(issues[type][error.code].Global)));
60✔
1165
                                                issues[type][error.code][limit.uuid].datesTriggered.push(issues[type][error.code].Global.dateLast); // add after callback, to avoid double dates
60✔
1166
                                        } else if (issues[type][error.code][limit.uuid].triggered && limit.reset >= 0) {
6!
1167
                                                issues[type][error.code][limit.uuid].triggered = false;
6✔
1168
                                                issues[type][error.code][limit.uuid].balance = limit.reset;
120✔
1169
                                        }
1170
                                }
1171
                        };
1172
                        self.on(type, limitToErrors);
138✔
1173
                }
1174
                self.emit('app.setup.initLimitListeners');
138✔
1175
                return this;
138✔
1176
        }
1177

1178
        /**
1179
         * Internal: Loads all plugins defined on `config.plugins` array. These plugins must be installed via NPM.
1180
         * This is step 4 of 8 to startup Spawnpoint
1181
         * @return {this}
1182
         * @private
1183
         */
1184
        loadPlugins() {
1185
                this.config.plugins = this.config.plugins.map((plugin) => {
18✔
1186
                        if (typeof(plugin) === 'string') {
18!
1187
                                plugin = {
66✔
1188
                                        plugin: plugin,
1189
                                        name: null,
1190
                                        namespace: null,
1191
                                };
1192
                        }
1193
                        const pluginFile = require(plugin.plugin);
66✔
1194
                        // remove node modules cache to allow reuse of plugins under new namespaces
1195
                        delete require.cache[require.resolve(plugin.plugin)];
30✔
1196

1197
                        if (plugin.namespace) {
30✔
1198
                                pluginFile.original_namespace = pluginFile.namespace;
30✔
1199
                                plugin.original_namespace = pluginFile.namespace;
30✔
1200
                                pluginFile.namespace = plugin.namespace;
30✔
1201
                                pluginFile.name = plugin.name;
30✔
1202
                                this.info('Sideloading [%s] as plugin: [%s]', plugin.namespace, plugin.name);
36✔
1203
                        } else {
1204
                                plugin.namespace = pluginFile.namespace;
36✔
1205
                                plugin.name = pluginFile.name;
66✔
1206
                                this.info('Loading plugin: [%s]', plugin.name);
66✔
1207
                        }
1208
                        this.plugins[plugin.namespace] = pluginFile;
264✔
1209
                        return plugin;
138✔
1210
                });
1211
                this.emit('app.setup.loadPlugins');
138✔
1212
                return this;
138✔
1213
        }
1214

1215
        /**
1216
         * Internal: Sets up the error mapping to automatically match custom Error types to Spawnpoint codes.
1217
         * This is step 8 of 8 to startup Spawnpoint
1218
         * @return {this}
1219
         * @private
1220
         */
1221
        loadErrorMap() {
1222
                for (const plugin of Object.values(this.plugins)) {
6✔
1223
                        if (plugin.errors) {
252!
1224
                                this.registerErrors(plugin.errors);
252✔
1225
                        }
1226
                }
1227
                this.emit('app.setup.loadErrorMap');
138✔
1228
                return this;
138✔
1229
        }
1230

1231
        /**
1232
         * Internal: Called to register a Spawnpoint plugin. See Plugins docs for more details on how plugins work.
1233
         * @param  {Object} opts Plugin options `object`
1234
         * @param  {String} opts.name Plugin Name
1235
         * @param  {String} opts.namespace Application namespace used by the plugin
1236
         * @param  {String} opts.dir Folder where the plugin and it's config/codes can be found. (usually `__dir`)
1237
         * @param  {Object} opts.codes Custom codes to register to Spawnpoint
1238
         * @param  {Object} opts.config Custom config to register to Spawnpoint
1239
         * @param  {Function} opts.exports Plugin function to execute with (app, [callback]) context. Callback is only defined when `opts.callback` is true
1240
         * @param  {Boolean} [opts.callback] When true, will set the opts.exports to have a required callback function to be called once this plugin is in a `ready` state.
1241
         * @return {Object} Returns Spawnpoint plugin reference. Used internally to load plugins.
1242
         * @private
1243
         */
1244
        static registerPlugin(opts) {
1245
                assert(opts.name, 'Plugin is missing required `name` option.');
90✔
1246
                assert(opts.namespace, 'Plugin is missing required `namespace` option.');
90✔
1247
                assert(opts.exports, 'Plugin is missing required `exports` function.');
90✔
1248
                return _.merge(opts, {
138✔
1249
                        codes: this.codes || null,
276✔
1250
                        config: this.config || null,
276✔
1251
                });
1252
        }
1253

1254
        /**
1255
         * Internal: Forces the JSON (require) handler to allow comments in JSON files. This allow documentation in JSON config files.
1256
         * @return {this}
1257
         * @private
1258
         */
1259
        setupJSONHandler() {
UNCOV
1260
                require(path.join(__dirname, '/json-handler.js'));
×
UNCOV
1261
                return this;
×
1262
        }
1263
}
1264

UNCOV
1265
module.exports = spawnpoint;
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc