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

nodecraft / spawnpoint / 20210363401

14 Dec 2025 03:46PM UTC coverage: 90.399% (-0.5%) from 90.93%
20210363401

Pull #104

github

web-flow
Merge aae9513d0 into 4d1e8eddf
Pull Request #104: perf: improve startup times

289 of 339 branches covered (85.25%)

Branch coverage included in aggregate %.

74 of 82 new or added lines in 2 files covered. (90.24%)

1 existing line in 1 file now uncovered.

549 of 588 relevant lines covered (93.37%)

2212.86 hits per line

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

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

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

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

16
// Define private helper functions
17
const helpers = require('./helpers.js');
54✔
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} [configFile] Sets the JSON file spawnpoint uses to setup the framework.
30
         * @return {this}
31
         */
32
        constructor(configFile = '/config/app.json') {
360✔
33
                // init EventEmitter
34
                super();
564✔
35

36
                if (typeof(configFile) !== 'string') {
564✔
37
                        throw new TypeError('`configFile` must be a path string to a Spawnpoint config file.');
24✔
38
                }
39
                if (!configFile.endsWith('.json') && !configFile.endsWith('.js')) {
540✔
40
                        configFile = configFile + '.json';
84✔
41
                }
42
                this.configFile = configFile;
540✔
43

44
                // set the folder for config to be autoloaded
45

46
                // Used to track if the application expects itself to continue running or not
47
                this.status = {
540✔
48
                        setup: false, // has the app finished setup
49
                        running: false, // is it in the running state. When false is attempting to shutdown
50
                        stopping: false, // is it in a stopping state. when true is attempting to stop
51
                        stopAttempts: 0, // how many attempts to stop have been triggered
52
                };
53

54
                // app CWD
55
                this.cwd = process.cwd();
540✔
56

57
                // detect if we are in a container (lazy-loaded on first access)
58
                this._containerized = null;
540✔
59
                Object.defineProperty(this, 'containerized', {
540✔
60
                        get() {
NEW
61
                                if (this._containerized === null) {
×
NEW
62
                                        this._containerized = helpers.isContainerized();
×
63
                                }
NEW
64
                                return this._containerized;
×
65
                        },
66
                        set(value) {
NEW
67
                                this._containerized = value;
×
68
                        },
69
                        enumerable: true,
70
                        configurable: true,
71
                });
72

73
                // list of ENV configs that are blocklisted (lazy-loaded on first access)
74
                let _configBlocklist;
75
                Object.defineProperty(this, 'configBlocklist', {
540✔
76
                        get() {
77
                                if (!_configBlocklist) {
84,742✔
78
                                        _configBlocklist = require('../config-blocklist.json');
222✔
79
                                }
80
                                return _configBlocklist;
84,742✔
81
                        },
82
                        set(value) {
83
                                _configBlocklist = value;
6✔
84
                        },
85
                        enumerable: true,
86
                        configurable: true,
87
                });
88

89
                // plugin registery
90
                this.register = [];
540✔
91

92
                // config object to store all application config
93
                this.config = {};
540✔
94

95
                // codes object to store all Spawnpoint codes
96
                this.codes = {};
540✔
97

98
                // errorMaps help wrap custom Error types to Spawnpoint codes.
99
                this.errorMaps = {};
540✔
100

101
                // error tracking, debounce detection
102
                this.limitMaps = {};
540✔
103

104
                // which plugins are loaded
105
                this.plugins = {};
540✔
106

107

108
                // make errorCode and failCode available (lazy-loaded on first access)
109
                let _errorCode;
110
                let _failCode;
111
                let _roundRobin;
112
                let _getAndLock;
113
                Object.defineProperties(this, {
540✔
114
                        _errorCode: {
115
                                get() {
116
                                        if (!_errorCode) {
198✔
117
                                                _errorCode = require('./errorCode.js');
60✔
118
                                        }
119
                                        return _errorCode;
198✔
120
                                },
121
                                enumerable: true,
122
                                configurable: true,
123
                        },
124
                        _failCode: {
125
                                get() {
126
                                        if (!_failCode) {
66✔
127
                                                _failCode = require('./failCode.js');
18✔
128
                                        }
129
                                        return _failCode;
66✔
130
                                },
131
                                enumerable: true,
132
                                configurable: true,
133
                        },
134
                        _roundRobin: {
135
                                get() {
136
                                        if (!_roundRobin) {
72✔
137
                                                _roundRobin = require('./roundRobin.js')(this);
18✔
138
                                        }
139
                                        return _roundRobin;
72✔
140
                                },
141
                                enumerable: true,
142
                                configurable: true,
143
                        },
144
                        _getAndLock: {
145
                                get() {
146
                                        if (!_getAndLock) {
72✔
147
                                                _getAndLock = require('./getAndLock.js')(this);
18✔
148
                                        }
149
                                        return _getAndLock;
72✔
150
                                },
151
                                enumerable: true,
152
                                configurable: true,
153
                        },
154
                });
155

156
                // log formatting
157
                this.logs = {
540✔
158
                        prefix: null,
159
                        date: null,
160
                };
161

162
                return this;
540✔
163
        }
164

165
        /**
166
         * Initializes framework to read the `configFile`, init config, Spawnpoint plugins, errorCodes and autoload
167
         * folders. This also starts the application life-cycle so the app can stop gracefully.
168
         * @callback {Function} [callback] Triggered once the `app.ready` event triggers.
169
         * @return {this}
170
         */
171
        setup(callback = () => {}) {
72✔
172
                // force .json parsing with comments :)
173
                this.setupJSONHandler();
192✔
174

175
                // prevent repeated setup
176
                if (this.status.setup) {
192✔
177
                        return callback(this.errorCode('spawnpoint.already_setup'));
6✔
178
                }
179
                this.status.setup = true;
186✔
180

181
                // App loading process
182
                this.initConfig();
186✔
183
                this.initCodes();
186✔
184
                this.initRegistry();
186✔
185
                this.loadPlugins();
186✔
186
                this.loadConfig();
186✔
187
                this.loadCodes();
186✔
188
                this.initLimitListeners();
186✔
189
                this.loadErrorMap();
186✔
190
                const jobs = [];
186✔
191

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

274
        /**
275
         * Recursively list files in a directory by an optional file extension.
276
         * NOTE: This is an event blocking sync method.
277
         * @param  {String} dir  Directory to list files from.
278
         * @param  {Array|string} [exts] Optional list of file extensions to return. Defaults to .js files. Set to a falsy value to disable this filter.
279
         * @return {Array} Absolute/full path of filenames found.
280
         */
281
        recursiveList(dir, exts = ['.js']) {
42✔
282
                assert(typeof(dir) === 'string', '`dir` must be a string');
918✔
283
                if (typeof(exts) === 'string') {
888✔
284
                        exts = [exts];
474✔
285
                }
286
                const list = [];
888✔
287
                let stat;
288
                try {
888✔
289
                        stat = fs.statSync(dir);
888✔
290
                } catch {
291
                        stat = false;
216✔
292
                }
293
                if (!stat || !stat.isDirectory()) {
888✔
294
                        return list;
216✔
295
                }
296
                // ensure proper trailing slash and normalize path separators
297
                dir = String(dir + '/').replaceAll('\\', '/');
672✔
298

299
                // Use withFileTypes to avoid a stat for every entry (significantly faster)
300
                const stack = [dir];
672✔
301
                while (stack.length > 0) {
672✔
302
                        const current = stack.pop();
966✔
303
                        let entries;
304
                        try {
966✔
305
                                entries = fs.readdirSync(current, { withFileTypes: true });
966✔
306
                        } catch {
307
                                continue;
×
308
                        }
309
                        for (const dirent of entries) {
966✔
310
                                const full = current + dirent.name;
4,686✔
311
                                if (dirent.isDirectory()) {
4,686✔
312
                                        if (exts && exts.includes('/')) {
294✔
313
                                                list.push(full);
84✔
314
                                        }
315
                                        stack.push(full + '/');
294✔
316
                                } else if (!exts || exts.includes(path.extname(dirent.name))) {
4,392✔
317
                                        list.push(full);
3,960✔
318
                                }
319
                        }
320
                }
321
                list.sort(); // windows won't sort this like unix will
672✔
322
                return list;
672✔
323
        }
324

325
        /**
326
         * Utility: Create random string.
327
         * @param  {Number} [length] How long of a random string to create.
328
         * @param  {String} [hashMethod] Which crypto hash method to use.
329
         * @return {String} Random string of characters.
330
         */
331
        random(length = 16) {
60,000✔
332
                length = Number.parseInt(length);
60,048✔
333
                assert(!Number.isNaN(length), '`length` must be a number');
60,048✔
334
                if (Number.isNaN(length) || length < 1) {
60,012✔
335
                        length = 16; // TODO: throw an error in an update
12✔
336
                }
337
                // lazy-load nanoid on first use
338
                if (!spawnpoint._nanoid) {
60,012✔
339
                        spawnpoint._nanoid = require('nanoid').nanoid;
6✔
340
                }
341
                return spawnpoint._nanoid(length);
60,012✔
342
        }
343

344
        /**
345
         * Utility: get random element from `collection`.
346
         * This is a copy of the lodash _.sample method.
347
         * @param  {Array|Object} items The collection to sample.
348
         * @return {*} Returns the random element.
349
         */
350
        sample(items) {
351
                return _.sample(items);
36✔
352
        }
353

354
        /**
355
         * Utility: Creates new `roundRobin` class with collection.
356
         * @param  {Array|Object} items The collection to sample.
357
         * @return {roundRobin} Returns new instance of `roundRobin` class.
358
         */
359
        roundRobin(items) {
360
                return new this._roundRobin(items);
72✔
361
        }
362

363
        /**
364
         * Utility: get random element from `collection` in an async lock.
365
         * @param  {Array|Object} items The collection to sample.
366
         * @return {roundRobin} Returns new instance of `roundRobin` class.
367
         */
368
        getAndLock(items) {
369
                return new this._getAndLock(items);
72✔
370
        }
371

372
        /**
373
         * Utility: omit keys from an object. Similar to Lodash omit, but much faster.
374
         * @param  {Object} items The source object.
375
         * @param  {Array} keysToOmit Keys to omit from the object.
376
         * @return {Object} Returns object with requested keys removed.
377
         */
378
        omit(obj, keysToOmit = []) {
×
379
                return helpers.omit(obj, keysToOmit);
×
380
        }
381

382
        /**
383
         * Checks if the current application runtime is running as a root user/group.
384
         * @return {Boolean} When true: the application is running as a root user/group.
385
         */
386
        isRoot() {
387
                if (this.isSecure() === true) {
6!
388
                        return false;
6✔
389
                }
390
                return true;
×
391
        }
392

393
        /**
394
         * Checks if the current application runtime is running as a specific `uid` and/or `gid`.
395
         * @param  {Number}  [uid] Unix `uid` to check against.
396
         * @param  {Number}  [gid] Unix `gid` to check against. When not set will match `uid`.
397
         * @return {Boolean} When true: the application is running as the user/group.
398
         */
399
        isSecure(uid, gid) {
400
                // TODO: Fix testing on non UNIX (windows)?
401
                if (typeof(process.getuid) !== 'function' || typeof(process.getgid) !== 'function') {
12✔
402
                        return true; // TODO: throw error
4✔
403
                }
404

405
                if (uid && !gid) {
8!
406
                        gid = uid;
×
407
                }
408
                // lazy-load child_process only when needed
409
                const child_process = require('node:child_process');
8✔
410
                const checks = {
8✔
411
                        uid: process.getuid(),
412
                        gid: process.getgid(),
413
                        groups: String(child_process.execSync('groups')),
414
                };
415
                if (checks.uid === 0 || checks.gid === 0) {
8!
416
                        return this.errorCode('usercheck.is_root', { checks: checks });
×
417
                }
418
                if (checks.groups.includes('root')) {
8!
419
                        return this.errorCode('usercheck.is_root_group', { checks: checks });
×
420
                }
421
                if (uid && gid && (uid !== checks.uid || gid !== checks.gid)) {
8!
422
                        return this.errorCode('usercheck.incorrect_user', { checks: checks });
×
423
                }
424
                return true;
8✔
425
        }
426

427
        /**
428
         * Helper method that requires a file and hoists the current spawnpoint application reference.
429
         * @param  {String} filePath File path to require.
430
         */
431
        require(filePath) {
432
                if (!filePath.startsWith(this.cwd)) {
6!
433
                        filePath = path.join(this.cwd, filePath);
6✔
434
                }
435
                return require(filePath)(this);
6✔
436
        }
437

438
        /**
439
         * Builds a Spawnpoint code object. Codes are used to create a link between a human readable message
440
         * and a computer readable string. Example: `file.not_found` -> "The requested file was not found."
441
         * @param {String} code computer readable string code.
442
         * @param {Object} [data] Object to extend the code Object with
443
         * @return {Object} Code Object with a `message` with the computer readable message and the `code` matching the input code.
444
         */
445
        code(code, data = {}) {
324✔
446
                assert(code && typeof(code) === 'string', '`code` must be an string.');
360✔
447
                assert(typeof(data) === 'object', '`data` must be an object.');
270✔
448
                if (!this.codes[code]) {
270✔
449
                        throw new Error('No return code found with code: ' + code); // TODO: convert this to an errorCode
18✔
450
                }
451
                return _.defaults(data, {
252✔
452
                        code: code,
453
                        message: this.codes[code],
454
                });
455
        }
456

457
        /**
458
         * Spawnpoint code that wraps a Javascript `Error` as a hard application error.
459
         * @param {String} code computer readable string code.
460
         * @param {Object} [data] Object to extend the code Object with
461
         * @return {Object} Error Code Object with a `message` with the computer readable message and the `code` matching the input code.
462
         */
463
        errorCode(code, data) {
464
                const getCode = this.code(code, data);
216✔
465
                this.emit('errorCode', getCode);
180✔
466
                return new this._errorCode(getCode);
180✔
467
        }
468

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

481
        /**
482
         * Error Monitoring, when enabled. This allows you to track how often an error occurs and issue a callback once that threadhold is met.
483
         * @param  {String} code Spawnpoint code to match against
484
         * @param  {Number} threshold Number of occurrences required to trigger callback.
485
         * @param  {Object} options Extra limit options
486
         * @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.
487
         * @param  {Callback} callback Triggered when threshold is met.
488
         * @return {this}
489
         */
490
        registerLimit(code, threshold, options, callback) {
491
                if (!callback && options) {
24✔
492
                        callback = options;
6✔
493
                        options = {};
6✔
494
                }
495
                const opts = _.defaults(options, {
24✔
496
                        callback: callback,
497
                        threshold: threshold,
498
                        error: 'errorCode', // or failCode
499
                        index: null, // 'object.to.path' of unique index to track by
500
                        reset: 1, // reset balance counter to this on a subsequent callback. Give it a negative number to disable this.
501
                        time: null,
502
                });
503

504
                opts.uuid = _.uniqueId();
24✔
505

506
                if (!this.limitMaps[opts.error]) {
24!
507
                        this.limitMaps[opts.error] = {};
24✔
508
                }
509
                if (!this.limitMaps[opts.error][code]) {
24!
510
                        this.limitMaps[opts.error][code] = [];
24✔
511
                }
512
                this.limitMaps[opts.error][code].push(opts);
24✔
513
                return this;
24✔
514
        }
515

516
        /**
517
         * Console.log wrapper that only triggers with when `config.debug` is enabled.
518
         * @params {*} [args..] Arguments to be passed to logging.
519
         * @return {this}
520
         */
521
        debug() {
522
                if (this.config.debug) {
6,422✔
523
                        Reflect.apply(console.log, this, arguments);
6✔
524
                }
525
                return this;
6,422✔
526
        }
527

528
        /**
529
         * Console.log wrapper that adds an INFO tag and timestamp to the log.
530
         * @params {String|Object|Array|Number} [args..] Arguments to be passed to logging.
531
         * @return {this}
532
         */
533
        info() {
534
                helpers.log({
108✔
535
                        logs: this.logs,
536
                        config: this.config.log,
537
                        type: helpers.tag('INFO', kleur.green),
538
                        line: kleur.white(Reflect.apply(format, this, arguments)),
539
                });
540
                return this;
108✔
541
        }
542

543
        /**
544
         * Console.log wrapper that adds an LOG tag and timestamp to the log.
545
         * @params {String|Object|Array|Number} [args..] Arguments to be passed to logging.
546
         * @return {this}
547
         */
548
        log() {
549
                helpers.log({
276✔
550
                        logs: this.logs,
551
                        config: this.config.log,
552
                        type: helpers.tag('LOG', kleur.cyan),
553
                        line: kleur.white(Reflect.apply(format, this, arguments)),
554
                });
555
                return this;
276✔
556
        }
557

558
        /**
559
         * Console.error` wrapper that adds an WARN tag and timestamp to the log. This prints to STDERR.
560
         * @params {String|Object|Array|Number} [args..] Arguments to be passed to logging.
561
         * @return {this}
562
         */
563
        warn() {
564
                helpers.log({
126✔
565
                        logs: this.logs,
566
                        config: this.config.log,
567
                        type: helpers.tag('WARN', kleur.yellow),
568
                        line: kleur.yellow(Reflect.apply(format, this, arguments)),
569
                }, 'error');
570
                return this;
126✔
571
        }
572

573
        /**
574
         * Console.error` wrapper that adds an ERROR tag and timestamp to the log. This prints to STDERR.
575
         * @params {String|Object|Array|Number} [args..] Arguments to be passed to logging.
576
         * @return {this}
577
         */
578
        error() {
579
                helpers.log({
84✔
580
                        logs: this.logs,
581
                        config: this.config.log,
582
                        type: helpers.tag('ERROR', kleur.red.bold),
583
                        line: kleur.red(Reflect.apply(format, this, arguments)),
584
                }, 'error');
585
                return this;
84✔
586
        }
587

588
        /**
589
         * Registers multiple custom Errors to a specific errorCode. This helps wrap errors into a singular errorCode system.
590
         * @param {String} code The errorCode human readable Spawnpoint code.
591
         * @param {Error} error Instance of the error to map to..
592
         * @return {this}
593
         */
594
        registerError(code, error) {
595
                this.errorMaps[code] = error;
84✔
596
                return this;
84✔
597
        }
598

599
        /**
600
         * Registers multiple custom Errors to a specific errorCode, using the `registerError` method.
601
         * @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.
602
         * @return {this}
603
         */
604
        registerErrors(errors) {
605
                for (const [code, error] of Object.entries(errors)) {
6✔
606
                        this.registerError(code, error);
6✔
607
                }
608
                return this;
6✔
609
        }
610

611
        /**
612
         * 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
613
         * error handling so that you don't have to have the server reply with a generic error.
614
         * @param  {Error} error Error to check for mapped error.
615
         * @return {errorCode|false} Returns Spawnpoint mapped code, errorCode, or failCode or false when no mapped error was found.
616
         */
617
        maskErrorToCode(error, type = 'code') {
6✔
618
                const validTypes = ['errorCode', 'failCode', 'code'];
24✔
619
                let returnedError = false;
24✔
620
                if (!validTypes.includes(type)) {
24✔
621
                        throw new Error('Invalid `type` provided. Valid types:' + validTypes.join(',')); // TODO: convert to errorCode
6✔
622
                }
623
                for (const [code, currentError] of Object.entries(this.errorMaps)) {
18✔
624
                        if (!returnedError && error instanceof currentError) {
36✔
625
                                returnedError = this[type](code, error);
18✔
626
                        }
627
                }
628
                return returnedError;
18✔
629
        }
630

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

696
                // setup all of the required functions mounted on the `config` object
697

698
                /**
699
                 * Helper method to safely get a nested config item.
700
                 * @param  {String} path The path of the property to get.
701
                 * @param  {*} [defaultValue=false] The value returned for undefined resolved values.
702
                 * @return {*} Returns the resolved value.
703
                 */
704
                this.config.get = function(path, defaultValue) {
228✔
705
                        return _.get(self.config, path, defaultValue);
30✔
706
                };
707

708
                /**
709
                 * Helper method to safely check if a nested config item exists
710
                 * @param  {String} path The path to check.
711
                 * @memberOf config
712
                 * @namespace config.has
713
                 * @return {*} Returns `true` if path exists, else `false`.
714
                 */
715
                this.config.has = function(path) {
228✔
716
                        return _.has(self.config, path);
6✔
717
                };
718

719
                /**
720
                 * Helper method to get a random element from a Spawnpoint `config` item `collection`.
721
                 * @param  {path} path The path to return items from.
722
                 * @memberOf config
723
                 * @namespace config.getRandom
724
                 * @return {*} Returns random element from the collection.
725
                 */
726
                this.config.getRandom = function(path) {
228✔
727
                        const items = self.config.get(path);
12✔
728
                        if (!items) {
12✔
729
                                throw self.errorCode('spawnpoint.config.sample_not_collection'); // TODO: choose better name
6✔
730
                        }
731
                        return _.sample(items);
6✔
732
                };
733

734
                const rrKeys = {};
228✔
735
                /**
736
                 * Helper method to get get random element from Spawnpoint `config` item `Array` with Round Robin ordering
737
                 * This ensures no single item is returned more than it's siblings.
738
                 * @param  {path} path The path to return items from.
739
                 * @memberOf config
740
                 * @namespace config.getRoundRobin
741
                 * @return {*} Returns random element from the collection.
742
                 */
743
                this.config.getRoundRobin = function(path) {
228✔
744
                        if (!rrKeys[path]) {
30✔
745
                                const items = self.config.get(path);
6✔
746
                                rrKeys[path] = self.roundRobin(items);
6✔
747
                        }
748
                        return rrKeys[path].next();
30✔
749
                };
750

751
                const lockedKeys = {};
228✔
752
                /**
753
                 * Helper method to get get random element from Spawnpoint `config` item `Array` with async locking queue.
754
                 * This ensures no item is used at the same time as another async operation.
755
                 * @param  {path} path The path to return items from.
756
                 * @memberOf config
757
                 * @namespace config.getAndLock
758
                 * @return {*} Returns random element from the collection.
759
                 */
760
                this.config.getAndLock = function(path, timeout, callback) {
228✔
761
                        if (!lockedKeys[path]) {
450✔
762
                                const items = self.config.get(path);
6✔
763
                                lockedKeys[path] = self.getAndLock(items);
6✔
764
                        }
765
                        return lockedKeys[path].next(timeout, callback);
450✔
766
                };
767

768
                this.emit('app.setup.initConfig');
228✔
769
                return this;
228✔
770
        }
771

772
        /**
773
         * Internal: Registers Spawnpoint `config` by merging or setting values to the `config` object.
774
         * @param  {String} name `config` top level key
775
         * @param  {*} config value or object of the config
776
         * @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.
777
         * @return {Object} returns `this.config` new value
778
         * @private
779
         */
780
        registerConfig(name, config, allowListCheck = '') {
1,980✔
781
                let data = {};
31,228✔
782

783
                if (allowListCheck && this.configBlocklist[allowListCheck]) {
31,228✔
784
                        if (this.configBlocklist[allowListCheck].list.includes(name)) { return this.debug('ignoring blocklist', name); }
29,248✔
785
                        let found = false;
26,000✔
786
                        for (const pattern of this.configBlocklist[allowListCheck].patterns) {
26,000✔
787
                                if (pattern.test(name)) {
72,240✔
788
                                        found = true;
3,072✔
789
                                        break;
3,072✔
790
                                }
791
                        }
792
                        if (found) { return this.debug('ignoring blocklist pattern', name); }
26,000✔
793
                        if (this.config.debug) {
22,928!
794
                                this.log('Setting %s ENV variable [%s]', allowListCheck, name);
×
795
                        }
796
                }
797
                if (name && !config) {
24,908✔
798
                        data = name;
1,024✔
799
                } else {
800
                        data[name] = config;
23,884✔
801
                }
802
                if (allowListCheck === 'env' || allowListCheck === 'secrets' || allowListCheck === 'config-hoist') {
24,908✔
803
                        return _.set(this.config, name, config);
22,736✔
804
                }
805
                // eslint-disable-next-line unicorn/prefer-structured-clone
806
                return _.merge(this.config, _.cloneDeep(data));
2,172✔
807
        }
808

809
        /**
810
         * Fast, low-overhead parser for ENV/secrets values.
811
         * - true/false/null -> boolean/null
812
         * - numbers (int/float) -> number
813
         * - JSON-like (starts with { or [) -> JSON.parse (try once)
814
         * Otherwise returns the original string.
815
         * @private
816
         */
817
        _parseEnvValue(value, allowJson = true) {
29,056✔
818
                if (typeof value !== 'string') { return value; }
29,056!
819
                const lower = value.toLowerCase();
29,056✔
820
                if (lower === 'true') { return true; }
29,056✔
821
                if (lower === 'false') { return false; }
28,480✔
822
                if (lower === 'null') { return null; }
28,288!
823
                if (/^-?\d+(\.\d+)?$/.test(value)) {
28,288✔
824
                        const num = Number(value);
3,200✔
825
                        if (!Number.isNaN(num)) { return num; }
3,200!
826
                }
827
                if (
25,088✔
828
                        allowJson &&
99,072!
829
                        value.length > 1 &&
830
                        ((value[0] === '{' && value.endsWith('}')) ||
831
                                (value[0] === '[' && value.endsWith(']')))
832
                ) {
833
                        try {
192✔
834
                                return JSON.parse(value);
192✔
835
                        } catch {
836
                                // keep original string on parse failure
837
                        }
838
                }
839
                return value;
24,896✔
840
        }
841
        /**
842
         * Internal: Builds app `config` object by looping through plugins, configs, ENV, Progress args, Docker secrets. , and finally
843
         * config overrides (in that order). These items are hoisted to the Spawnpoint `config` object.
844
         * This is step 5 of 8 to startup Spawnpoint
845
         * @param  {String} [cwd] Path to load config files from.
846
         * @param  {Boolean} ignoreExtra When true will skip plugins, ENV and Docker secrets. Allows for recursive usage.
847
         * @return {this}
848
         * @private
849
         */
850
        loadConfig(cwd = '', ignoreExtra = false) {
384✔
851
                cwd = cwd || this.cwd;
228✔
852

853
                if (!ignoreExtra) {
228✔
854
                        // load plugin defaults
855
                        for (const plugin of Object.values(this.plugins)) {
192✔
856
                                if (plugin.config) {
36!
857
                                        // ensure sideloaded plugins retain original config
858
                                        if (plugin.original_namespace) {
×
859
                                                plugin.config[plugin.namespace] = plugin.config[plugin.original_namespace];
×
860
                                                delete plugin.config[plugin.original_namespace];
×
861
                                        }
862
                                        this.registerConfig(plugin.config);
×
863
                                }
864
                                this.loadConfig(plugin.dir, true);
36✔
865
                        }
866
                }
867

868
                // load local json files
869
                for (const file of this.recursiveList(cwd + this.config.configs, '.json')) {
228✔
870
                        // prevent loading base config and codes
871
                        if (!file.includes(this.configFile) && !file.includes(this.config.codes)) {
2,550✔
872
                                const configName = path.parse(file).name;
1,980✔
873
                                if (!this.config[configName]) {
1,980✔
874
                                        this.config[configName] = {};
498✔
875
                                }
876
                                this.registerConfig(configName, require(file));
1,980✔
877
                        }
878
                }
879

880
                if (!ignoreExtra) {
228✔
881
                        // handle process flags
882
                        this.args = minimist(process.argv.slice(2));
192✔
883
                        for (const [key, value] of Object.entries(this.args)) {
192✔
884
                                this.registerConfig(key, value, 'args');
192✔
885
                        }
886
                        this.argv = _.clone(this.args._) || [];
192!
887

888
                        // handle environment variables
889
                        for (let [key, value] of Object.entries(process.env)) {
192✔
890
                                key = key.replaceAll('__', '.'); // replace double underscores to dots, to allow object notation in environment vars
29,056✔
891
                                value = this._parseEnvValue(value);
29,056✔
892
                                this.registerConfig(key, value, 'env');
29,056✔
893
                        }
894

895
                        if (this.config.secrets) {
192!
896
                                // handle docker secrets
897
                                for (const file of this.recursiveList(this.config.secrets, false)) {
192✔
898
                                        let key;
899
                                        let value;
900
                                        try {
×
901
                                                key = path.basename(file);
×
902
                                                value = fs.readFileSync(file, 'utf8');
×
903
                                                value = this._parseEnvValue(value, true); // if it fails it will revert to above value
×
904
                                        } catch {
905
                                                // do nothing
906
                                        }
NEW
907
                                        if (!value || !key) { continue; }
×
NEW
908
                                        this.registerConfig(key, value, 'secrets');
×
909
                                }
910
                        }
911
                } else {
912
                        this.debug('Ignoring config extra loading');
36✔
913
                }
914
                this.emit('app.setup.loadConfig');
228✔
915

916
                if (this.config.configOverride) {
228!
917
                        // allow dev-config.json in root directory to override config vars
918
                        let access = null;
×
919
                        try {
×
920
                                access = require(path.join(this.cwd, this.config.configOverride));
×
921
                        } catch {
922
                                // do nothing
923
                        }
924
                        if (access) {
×
925
                                this.debug('Overriding config with custom overrides');
×
NEW
926
                                for (const [key, value] of Object.entries(access)) {
×
NEW
927
                                        this.registerConfig(key, value, 'config-hoist');
×
928
                                }
929
                                // Emit an event to allow plugins to know that the config has been overridden
930
                                this.emit('app.setup.configOverridden');
×
931
                        }
932
                }
933
                return this;
228✔
934
        }
935

936
        /**
937
         * Internal: Loads the internal Spawnpoint codes.
938
         * This is step 2 of 8 to startup Spawnpoint
939
         * @return {this}
940
         * @private
941
         */
942
        initCodes() {
943
                this.codes = {};
192✔
944
                for (const file of this.recursiveList(path.join(__dirname, '../codes'), '.json')) {
192✔
945
                        Object.assign(this.codes, require(file));
960✔
946
                }
947
                this.emit('app.setup.initCodes');
192✔
948
                return this;
192✔
949
        }
950

951
        /**
952
         * Internal: Loads the application codes from a folder
953
         * This is step 6 of 8 to startup Spawnpoint
954
         * @param {String} [cwd] Folder to load paths from.
955
         * @param  {Boolean} ignoreExtra When true will skip plugins. Allows for recursive usage.
956
         * @return {this}
957
         * @private
958
         */
959
        loadCodes(cwd = '', ignoreExtra = false) {
372✔
960
                cwd = cwd || path.join(this.cwd, this.config.codes);
210✔
961

962
                if (!ignoreExtra) {
210✔
963
                        // load plugin defaults
964
                        for (const plugin of Object.values(this.plugins)) {
186✔
965
                                if (plugin.codes) {
24!
966
                                        this.registerCodes(plugin.codes);
×
967
                                }
968
                                this.loadCodes(plugin.dir + '/codes', true);
24✔
969
                        }
970
                }
971

972
                // handle local files
973
                let list = null;
210✔
974
                try {
210✔
975
                        list = this.recursiveList(cwd, ['.json']);
210✔
976
                } catch {
977
                        this.debug('No codes folder found (%s), skipping', this.config.codes);
×
978
                }
979
                if (list) {
210!
980
                        for (const file of list) {
210✔
981
                                this.registerCodes(require(file));
372✔
982
                        }
983
                }
984
                this.emit('app.setup.loadCodes');
210✔
985
                return this;
210✔
986
        }
987

988
        /**
989
         * Internal: Hoists new codes into Spawnpoint `codes` object.
990
         * @param  {Object} codes Codes to inject at key as the code computer readable and value at the human readable message.
991
         * @return {this}
992
         * @private
993
         */
994
        registerCodes(codes) {
995
                Object.assign(this.codes, codes);
384✔
996
                return this;
384✔
997
        }
998

999
        /**
1000
         * Internal: Starts the Spawnpoint application lifecycle registery. This ensures the application starts up correctly and shuts down gracefully.
1001
         * This is step 3 of 8 to startup Spawnpoint
1002
         * @return {this}
1003
         * @private
1004
         */
1005
        initRegistry() {
1006
                this.register = [];
360✔
1007

1008
                this.on('app.ready', () => {
360✔
1009
                        this.status.running = true;
204✔
1010
                        // only handle uncaught exceptions when ready
1011
                        if (!this.config.catchExceptions) { return; }
204✔
1012
                        process.on('uncaughtException', (err) => {
12✔
1013
                                this.error(err.message || err).debug(err.stack || '(no stack trace)');
6!
1014
                                // close the app if we have not completed startup
1015
                                if (!this.status.running) {
6!
1016
                                        this.emit('app.stop', true);
6✔
1017
                                }
1018
                        });
1019
                });
1020

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

1055
                        this.status.running = false;
54✔
1056
                        this.status.stopping = true;
54✔
1057
                        this.info('Stopping %s gracefully', this.config.name);
54✔
1058
                        this.emit('app.close');
54✔
1059
                        if (this.register.length === 0) {
54✔
1060
                                return this.emit('app.exit', true);
6✔
1061
                        }
1062
                });
1063
                this.on('app.exit', (graceful) => {
360✔
1064
                        if (!graceful) {
12✔
1065
                                /* eslint-disable n/no-process-exit */
1066
                                return process.exit(1);
6✔
1067
                        }
1068
                        this.info('%s gracefully closed.', this.config.name);
6✔
1069
                        process.exit();
6✔
1070
                });
1071

1072
                if (this.config.signals) {
360✔
1073
                        // gracefully handle ctrl+c
1074
                        if (this.config.signals.close) {
18✔
1075
                                for (const event of this.config.signals.close) {
12✔
1076
                                        process.on(event, () => {
6✔
1077
                                                this.emit('app.stop');
12✔
1078
                                        });
1079
                                }
1080
                        }
1081

1082
                        // set debug mode on SIGUSR1
1083
                        if (this.config.signals.debug) {
18✔
1084
                                for (const event of this.config.signals.debug) {
12✔
1085
                                        process.on(event, () => {
6✔
1086
                                                this.config.debug = !this.config.debug;
6✔
1087
                                        });
1088
                                }
1089
                        }
1090
                }
1091

1092
                this.emit('app.setup.initRegistry');
360✔
1093
                return this;
360✔
1094
        }
1095

1096
        /**
1097
         * Internal: Starts the Spawnpoint errorCode & failCode tracking. Disabled by default unless `config.trackErrors` is enabled due to a larger
1098
         * memory footprint required.
1099
         * This is step 7 of 8 to startup Spawnpoint
1100
         * @return {this}
1101
         * @private
1102
         */
1103
        initLimitListeners() {
1104
                const self = this;
186✔
1105
                if (!this.config.trackErrors) { return this; }
186✔
1106
                const issues = {
30✔
1107
                        errorCode: {},
1108
                        failCode: {},
1109
                };
1110
                for (const type of ['errorCode', 'failCode']) {
30✔
1111
                        const limitToErrors = function(error) {
60✔
1112
                                if (!self.limitMaps[type] || !self.limitMaps[type][error.code]) {
66✔
1113
                                        return; // no issue being tracked
6✔
1114
                                }
1115

1116
                                const limits = self.limitMaps[type][error.code];
60✔
1117

1118

1119
                                const defaultIssues = {
60✔
1120
                                        occurrences: 0, // long count, track
1121
                                        balance: 0, // time-based balance
1122
                                        dateFirst: Math.floor(Date.now() / 1000),
1123
                                        dateLast: null,
1124
                                        datesTriggered: [],
1125
                                        triggered: false, // track if we've triggered the current balance
1126
                                };
1127

1128
                                if (!issues[type][error.code]) {
60✔
1129
                                        issues[type][error.code] = {};
18✔
1130
                                        issues[type][error.code].Global = _.pick(defaultIssues, ['occurrences', 'dateFirst', 'dateLast']);
18✔
1131
                                }
1132

1133
                                issues[type][error.code].Global.occurrences++;
60✔
1134
                                issues[type][error.code].Global.dateLast = Math.floor(Date.now() / 1000);
60✔
1135

1136
                                for (const limit of limits) {
60✔
1137
                                        // new issue
1138
                                        if (!issues[type][error.code][limit.uuid]) {
60✔
1139
                                                issues[type][error.code][limit.uuid] = _.pick(defaultIssues, ['balance', 'triggered', 'datesTriggered']);
18✔
1140
                                        }
1141
                                        issues[type][error.code][limit.uuid].balance++;
60✔
1142
                                        if (limit.time) {
60✔
1143
                                                setTimeout(function() {
30✔
1144
                                                        issues[type][error.code][limit.uuid].balance--;
30✔
1145
                                                        if (issues[type][error.code][limit.uuid].balance <= 0) {
30✔
1146
                                                                issues[type][error.code][limit.uuid].balance = 0;
18✔
1147
                                                                issues[type][error.code][limit.uuid].triggered = false;
18✔
1148
                                                        }
1149
                                                }, limit.time);
1150
                                        }
1151
                                        if (!issues[type][error.code][limit.uuid].triggered && issues[type][error.code][limit.uuid].balance >= limit.threshold) {
60✔
1152
                                                issues[type][error.code][limit.uuid].triggered = true;
30✔
1153
                                                limit.callback(_.merge(_.clone(issues[type][error.code][limit.uuid]), _.clone(issues[type][error.code].Global)));
30✔
1154
                                                issues[type][error.code][limit.uuid].datesTriggered.push(issues[type][error.code].Global.dateLast); // add after callback, to avoid double dates
30✔
1155
                                        } else if (issues[type][error.code][limit.uuid].triggered && limit.reset >= 0) {
30✔
1156
                                                issues[type][error.code][limit.uuid].triggered = false;
6✔
1157
                                                issues[type][error.code][limit.uuid].balance = limit.reset;
6✔
1158
                                        }
1159
                                }
1160
                        };
1161
                        self.on(type, limitToErrors);
60✔
1162
                }
1163
                self.emit('app.setup.initLimitListeners');
30✔
1164
                return this;
30✔
1165
        }
1166

1167
        /**
1168
         * Internal: Loads all plugins defined on `config.plugins` array. These plugins must be installed via NPM.
1169
         * This is step 4 of 8 to startup Spawnpoint
1170
         * @return {this}
1171
         * @private
1172
         */
1173
        loadPlugins() {
1174
                this.config.plugins = this.config.plugins.map((plugin) => {
198✔
1175
                        if (typeof(plugin) === 'string') {
48✔
1176
                                plugin = {
6✔
1177
                                        plugin: plugin,
1178
                                        name: null,
1179
                                        namespace: null,
1180
                                };
1181
                        }
1182
                        const pluginFile = require(plugin.plugin);
48✔
1183
                        // remove node modules cache to allow reuse of plugins under new namespaces
1184
                        delete require.cache[require.resolve(plugin.plugin)];
48✔
1185

1186
                        if (plugin.namespace) {
48✔
1187
                                pluginFile.original_namespace = pluginFile.namespace;
36✔
1188
                                plugin.original_namespace = pluginFile.namespace;
36✔
1189
                                pluginFile.namespace = plugin.namespace;
36✔
1190
                                pluginFile.name = plugin.name;
36✔
1191
                                this.info('Sideloading [%s] as plugin: [%s]', plugin.namespace, plugin.name);
36✔
1192
                        } else {
1193
                                plugin.namespace = pluginFile.namespace;
12✔
1194
                                plugin.name = pluginFile.name;
12✔
1195
                                this.info('Loading plugin: [%s]', plugin.name);
12✔
1196
                        }
1197
                        this.plugins[plugin.namespace] = pluginFile;
48✔
1198
                        return plugin;
48✔
1199
                });
1200
                this.emit('app.setup.loadPlugins');
198✔
1201
                return this;
198✔
1202
        }
1203

1204
        /**
1205
         * Internal: Sets up the error mapping to automatically match custom Error types to Spawnpoint codes.
1206
         * This is step 8 of 8 to startup Spawnpoint
1207
         * @return {this}
1208
         * @private
1209
         */
1210
        loadErrorMap() {
1211
                for (const plugin of Object.values(this.plugins)) {
198✔
1212
                        if (plugin.errors) {
36✔
1213
                                this.registerErrors(plugin.errors);
6✔
1214
                        }
1215
                }
1216
                this.emit('app.setup.loadErrorMap');
198✔
1217
                return this;
198✔
1218
        }
1219

1220
        /**
1221
         * Internal: Called to register a Spawnpoint plugin. See Plugins docs for more details on how plugins work.
1222
         * @param  {Object} opts Plugin options `object`
1223
         * @param  {String} opts.name Plugin Name
1224
         * @param  {String} opts.namespace Application namespace used by the plugin
1225
         * @param  {String} opts.dir Folder where the plugin and it's config/codes can be found. (usually `__dir`)
1226
         * @param  {Object} opts.codes Custom codes to register to Spawnpoint
1227
         * @param  {Object} opts.config Custom config to register to Spawnpoint
1228
         * @param  {Function} opts.exports Plugin function to execute with (app, [callback]) context. Callback is only defined when `opts.callback` is true
1229
         * @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.
1230
         * @return {Object} Returns Spawnpoint plugin reference. Used internally to load plugins.
1231
         * @private
1232
         */
1233
        static registerPlugin(opts) {
1234
                assert(opts.name, 'Plugin is missing required `name` option.');
54✔
1235
                assert(opts.namespace, 'Plugin is missing required `namespace` option.');
54✔
1236
                assert(opts.exports, 'Plugin is missing required `exports` function.');
54✔
1237
                return _.merge(opts, {
54✔
1238
                        codes: this.codes || null,
108✔
1239
                        config: this.config || null,
108✔
1240
                });
1241
        }
1242

1243
        /**
1244
         * Internal: Forces the JSON (require) handler to allow comments in JSON files. This allow documentation in JSON config files.
1245
         * @return {this}
1246
         * @private
1247
         */
1248
        setupJSONHandler() {
1249
                require(path.join(__dirname, '/json-handler.js'));
192✔
1250
                return this;
192✔
1251
        }
1252
}
1253

1254
module.exports = spawnpoint;
54✔
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