axe 12.2.3 → 12.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js ADDED
@@ -0,0 +1,634 @@
1
+ // eslint-disable-next-line import/no-unassigned-import
2
+ require('console-polyfill');
3
+
4
+ // eslint-disable-next-line unicorn/prefer-node-protocol
5
+ const os = require('os');
6
+ const format = require('@ladjs/format-util');
7
+ const formatSpecifiers = require('format-specifiers');
8
+ const get = require('@strikeentco/get');
9
+ const isError = require('iserror');
10
+ const isSymbol = require('is-symbol');
11
+ const mergeOptions = require('merge-options');
12
+ const pMapSeries = require('p-map-series');
13
+ const parseAppInfo = require('parse-app-info');
14
+ const parseErr = require('parse-err');
15
+ const pickDeep = require('pick-deep');
16
+ const set = require('@strikeentco/set');
17
+ const unset = require('unset-value');
18
+ const { boolean } = require('boolean');
19
+ const pkg = require('../package.json');
20
+
21
+ const silentSymbol = Symbol.for('axe.silent');
22
+ const omittedLoggerKeys = new Set(['config', 'log']);
23
+ const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
24
+ const aliases = { warning: 'warn', err: 'error' };
25
+ const levelError = `\`level\` invalid, must be: ${levels.join(', ')}`;
26
+ const name =
27
+ process.env.NODE_ENV === 'development'
28
+ ? false
29
+ : process.env.HOSTNAME || os.hostname();
30
+
31
+ // <https://github.com/sindresorhus/is-plain-obj/blob/main/index.js>
32
+ function isPlainObject(value) {
33
+ if (typeof value !== 'object' || value === null) {
34
+ return false;
35
+ }
36
+
37
+ const prototype = Object.getPrototypeOf(value);
38
+ return (
39
+ (prototype === null ||
40
+ prototype === Object.prototype ||
41
+ Object.getPrototypeOf(prototype) === null) &&
42
+ !(Symbol.toStringTag in value) &&
43
+ !(Symbol.iterator in value) &&
44
+ !isSymbol(value)
45
+ );
46
+ }
47
+
48
+ // <https://github.com/GeenenTijd/dotify/blob/master/dotify.js>
49
+ function dotifyToArray(obj) {
50
+ const res = [];
51
+ function recurse(obj, current) {
52
+ for (const key of Reflect.ownKeys(obj)) {
53
+ const value = obj[key];
54
+ const convertedKey = isSymbol(key)
55
+ ? Symbol.keyFor(key) || key.description
56
+ : key;
57
+ const newKey = current ? current + '.' + convertedKey : convertedKey; // joined key with dot
58
+ // if (value && typeof value === 'object' && !(value instanceof Date) && !ObjectID.isValid(value)) {
59
+ if (isPlainObject(value) && res.indexOf(convertedKey) === -1) {
60
+ res.push(convertedKey);
61
+ recurse(value, newKey); // it's a nested object, so do it again
62
+ } else if (res.indexOf(newKey) === -1) {
63
+ res.push(newKey);
64
+ }
65
+ }
66
+ }
67
+
68
+ recurse(obj);
69
+ return res;
70
+ }
71
+
72
+ // <https://stackoverflow.com/a/43233163>
73
+ function isEmpty(value) {
74
+ return (
75
+ value === undefined ||
76
+ value === null ||
77
+ (typeof value === 'object' && Reflect.ownKeys(value).length === 0) ||
78
+ (typeof value === 'string' && value.trim().length === 0)
79
+ );
80
+ }
81
+
82
+ function isNull(value) {
83
+ return value === null;
84
+ }
85
+
86
+ function isUndefined(value) {
87
+ return value === undefined;
88
+ }
89
+
90
+ function isObject(value) {
91
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
92
+ }
93
+
94
+ function isString(value) {
95
+ return typeof value === 'string';
96
+ }
97
+
98
+ function isFunction(value) {
99
+ return typeof value === 'function';
100
+ }
101
+
102
+ function getFunction(value) {
103
+ return isFunction(value) ? value : null;
104
+ }
105
+
106
+ class Axe {
107
+ // eslint-disable-next-line complexity
108
+ constructor(config = {}) {
109
+ const remappedFields = {};
110
+ if (process.env.AXE_REMAPPED_META_FIELDS) {
111
+ const fields = process.env.AXE_REMAPPED_META_FIELDS;
112
+ const arr = fields.split(',').map((v) => v.split(':'));
113
+ for (const [prop, value] of arr) {
114
+ remappedFields[prop] = value;
115
+ }
116
+ }
117
+
118
+ // envify does not support conditionals well enough so we declare vars outside
119
+ let omittedFields = process.env.AXE_OMIT_META_FIELDS;
120
+ if (typeof omittedFields === 'string')
121
+ omittedFields = omittedFields.split(',').map((s) => s.trim());
122
+ if (!Array.isArray(omittedFields)) omittedFields = [];
123
+
124
+ let pickedFields = process.env.AXE_PICK_META_FIELDS;
125
+ if (typeof pickedFields === 'string')
126
+ pickedFields = pickedFields.split(',').map((s) => s.trim());
127
+ if (!Array.isArray(pickedFields)) pickedFields = [];
128
+
129
+ this.config = mergeOptions(
130
+ {
131
+ showStack: process.env.AXE_SHOW_STACK
132
+ ? boolean(process.env.AXE_SHOW_STACK)
133
+ : true,
134
+ meta: Object.assign(
135
+ {
136
+ show: process.env.AXE_SHOW_META
137
+ ? boolean(process.env.AXE_SHOW_META)
138
+ : true,
139
+ remappedFields,
140
+ omittedFields,
141
+ pickedFields,
142
+ cleanupRemapping: true,
143
+ hideHTTP: 'is_http',
144
+ // implemented mainly for @ladjs/graceful to
145
+ // suppress unnecessary meta output to console
146
+ hideMeta: 'hide_meta'
147
+ },
148
+ typeof config.meta === 'object' ? config.meta : {}
149
+ ),
150
+ version: pkg.version,
151
+ silent: false,
152
+ logger: console,
153
+ name,
154
+ level: 'info',
155
+ levels: ['info', 'warn', 'error', 'fatal'],
156
+ appInfo: process.env.AXE_APP_INFO
157
+ ? boolean(process.env.AXE_APP_INFO)
158
+ : true,
159
+ hooks: Object.assign(
160
+ {
161
+ pre: [],
162
+ post: []
163
+ },
164
+ typeof config.hooks === 'object' ? config.hooks : {}
165
+ )
166
+ },
167
+ config
168
+ );
169
+
170
+ this.appInfo = this.config.appInfo
171
+ ? isFunction(parseAppInfo)
172
+ ? parseAppInfo()
173
+ : false
174
+ : false;
175
+
176
+ this.log = this.log.bind(this);
177
+
178
+ // Inherit methods from parent logger
179
+ const methods = Object.keys(this.config.logger).filter(
180
+ (key) => !omittedLoggerKeys.has(key)
181
+ );
182
+ for (const element of methods) {
183
+ this[element] = this.config.logger[element];
184
+ }
185
+
186
+ // Bind helper functions for each log level
187
+ for (const element of levels) {
188
+ // Ensure function exists in logger passed
189
+ if (element === 'fatal') {
190
+ this.config.logger.fatal =
191
+ getFunction(this.config.logger[element]) ||
192
+ getFunction(this.config.logger.error) ||
193
+ getFunction(this.config.logger.info) ||
194
+ getFunction(this.config.logger.log);
195
+ } else {
196
+ this.config.logger[element] =
197
+ getFunction(this.config.logger[element]) ||
198
+ getFunction(this.config.logger.info) ||
199
+ getFunction(this.config.logger.log);
200
+ }
201
+
202
+ if (!isFunction(this.config.logger[element])) {
203
+ throw new Error(`\`${element}\` must be a function on the logger.`);
204
+ }
205
+
206
+ // Bind log handler which normalizes args and populates meta
207
+ this[element] = (...args) =>
208
+ this.log(element, ...Array.prototype.slice.call(args));
209
+ }
210
+
211
+ this.setLevel = this.setLevel.bind(this);
212
+ this.getNormalizedLevel = this.getNormalizedLevel.bind(this);
213
+ this.setName = this.setName.bind(this);
214
+
215
+ // Set the logger name
216
+ if (this.config.name) this.setName(this.config.name);
217
+
218
+ // Set the logger level
219
+ this.setLevel(this.config.level);
220
+
221
+ // Aliases
222
+ this.err = this.error;
223
+ this.warning = this.warn;
224
+
225
+ // Pre and Post Hooks
226
+ this.pre = function (level, fn) {
227
+ this.config.hooks.pre.push(function (_level, ...args) {
228
+ if (level !== _level) return [...args];
229
+ return fn(...args);
230
+ });
231
+ };
232
+
233
+ this.post = function (level, fn) {
234
+ this.config.hooks.post.push(function (_level, ...args) {
235
+ if (level !== _level) return [...args];
236
+ return fn(...args);
237
+ });
238
+ };
239
+ }
240
+
241
+ setLevel(level) {
242
+ if (!isString(level) || levels.indexOf(level) === -1)
243
+ throw new Error(levelError);
244
+ // Support signale logger and other loggers that use `logLevel`
245
+ if (isString(this.config.logger.logLevel))
246
+ this.config.logger.logLevel = level;
247
+ else this.config.logger.level = level;
248
+ // Adjusts `this.config.levels` array
249
+ // so that it has all proceeding (inclusive)
250
+ this.config.levels = levels.slice(levels.indexOf(level));
251
+ }
252
+
253
+ getNormalizedLevel(level) {
254
+ if (!isString(level)) return 'info';
255
+ if (isString(aliases[level])) return aliases[level];
256
+ if (levels.indexOf(level) === -1) return 'info';
257
+ return level;
258
+ }
259
+
260
+ setName(name) {
261
+ if (!isString(name)) throw new Error('`name` must be a String');
262
+ // Support signale logger and other loggers that use `scope`
263
+ if (isString(this.config.logger.scope)) this.config.logger.scope = name;
264
+ else this.config.logger.name = name;
265
+ }
266
+
267
+ // eslint-disable-next-line complexity
268
+ log(level, message, meta, ...args) {
269
+ const originalArgs = [];
270
+ const errors = [];
271
+ let hasMessage = false;
272
+ let hasLevel = true;
273
+
274
+ if (!isUndefined(level)) originalArgs.push(level);
275
+ if (!isUndefined(message)) originalArgs.push(message);
276
+ if (!isUndefined(meta)) originalArgs.push(meta);
277
+ for (const arg of Array.prototype.slice.call(args)) {
278
+ originalArgs.push(arg);
279
+ }
280
+
281
+ let modifier = 0;
282
+
283
+ if (isString(level) && isString(aliases[level])) {
284
+ level = aliases[level];
285
+ } else if (isError(level)) {
286
+ hasLevel = false;
287
+ meta = message;
288
+ message = level;
289
+ level = 'error';
290
+ } else if (!isString(level) || levels.indexOf(level) === -1) {
291
+ hasLevel = false;
292
+ meta = message;
293
+ message = level;
294
+ level = this.getNormalizedLevel(level);
295
+ modifier = -1;
296
+ }
297
+
298
+ // Return early if it is not a valid logging level
299
+ if (this.config.levels.indexOf(level) === -1) return;
300
+
301
+ // Bunyan support (meta, message, ...args)
302
+ let isBunyan = false;
303
+ if ((isObject(message) || Array.isArray(message)) && isString(meta)) {
304
+ isBunyan = true;
305
+ const _meta = meta;
306
+ meta = message;
307
+ message =
308
+ isString(_meta) && originalArgs.length >= 3 + modifier
309
+ ? format(...originalArgs.slice(2 + modifier))
310
+ : _meta;
311
+ }
312
+
313
+ // If message was undefined then set it to level
314
+ if (isUndefined(message)) message = level;
315
+
316
+ // If only `message` was passed then if it was an Object
317
+ // preserve it as an Object by setting it as meta
318
+ if (
319
+ originalArgs.slice(1 + modifier).length === 1 &&
320
+ !isString(message) &&
321
+ !isError(message)
322
+ ) {
323
+ meta = { message };
324
+ message = level;
325
+ } else if (!isBunyan && originalArgs.length >= 4 + modifier) {
326
+ message = undefined;
327
+ meta = {};
328
+ const messages = [];
329
+ for (const arg of originalArgs.slice(
330
+ hasLevel && modifier === 0 ? 1 : 0
331
+ )) {
332
+ if (isError(arg)) errors.push(arg);
333
+ // pushes number, object, string, etc for formatting
334
+ else messages.push(arg);
335
+ }
336
+
337
+ if (messages.length > 0) {
338
+ message = format(...messages);
339
+ hasMessage = true;
340
+ }
341
+
342
+ if (errors.length > 0 && level === 'log') level = 'error';
343
+ } else if (
344
+ !isBunyan &&
345
+ originalArgs.length === 3 + modifier &&
346
+ isString(message) &&
347
+ formatSpecifiers.some((t) => message.indexOf(t) !== -1)
348
+ ) {
349
+ // Otherwise if there are three args and if the `message` contains
350
+ // a placeholder token (e.g. '%s' or '%d' - see above `formatSpecifiers` variable)
351
+ // then we can infer that the `meta` arg passed is used for formatting
352
+ message = format(message, meta);
353
+ meta = {};
354
+ } else if (!isError(message)) {
355
+ if (isError(meta)) {
356
+ errors.push(meta);
357
+ meta = {};
358
+ } else if (!isObject(meta) && !isUndefined(meta) && !isNull(meta)) {
359
+ // If the `meta` variable passed was not an Object then convert it
360
+ message = format(message, meta);
361
+ meta = {};
362
+ } else if (!isString(message)) {
363
+ // If the message is not a string then we should run `util.format` on it
364
+ // assuming we're formatting it like it was another argument
365
+ // (as opposed to using something like fast-json-stringify)
366
+ message = format(message);
367
+ }
368
+ } else if (isError(meta)) {
369
+ errors.push(meta);
370
+ // handle additional args
371
+ const messages = [];
372
+ if (isError(message)) {
373
+ errors.unshift(message);
374
+ message = undefined;
375
+ }
376
+
377
+ for (const arg of originalArgs.slice(2 + modifier)) {
378
+ // should skip this better with slice and modifier adjustment
379
+ if (meta === arg) continue;
380
+ if (isError(arg)) errors.push(arg);
381
+ else messages.push(arg);
382
+
383
+ if (messages.length > 0) {
384
+ message = format(...messages);
385
+ hasMessage = true;
386
+ }
387
+ }
388
+
389
+ if (level === 'log') level = 'error';
390
+
391
+ meta = {};
392
+ }
393
+
394
+ if (!isUndefined(meta) && !isObject(meta)) meta = { original_meta: meta };
395
+ else if (!isObject(meta)) meta = {};
396
+
397
+ if (isError(message)) {
398
+ errors.unshift(message);
399
+ message = undefined;
400
+ }
401
+
402
+ //
403
+ // rewrite `meta.err` to `meta.original_err` for consistency
404
+ // (in case someone has an object with `.err` property on it with an error)
405
+ //
406
+ if (isObject(meta.err)) {
407
+ if (isError(meta.err)) errors.push(meta.err);
408
+ meta.original_err = isError(meta.err) ? parseErr(meta.err) : meta.err;
409
+ }
410
+
411
+ let err;
412
+ if (errors.length > 0) {
413
+ if (errors.length === 1) {
414
+ err = errors[0];
415
+ } else {
416
+ err = new Error(
417
+ [...new Set(errors.map((e) => e.message).filter(Boolean))].join('; ')
418
+ );
419
+ err.stack = [
420
+ ...new Set(errors.map((e) => e.stack).filter(Boolean))
421
+ ].join('\n\n');
422
+ err.errors = errors;
423
+ }
424
+
425
+ meta.err = parseErr(err);
426
+ if (!isString(message)) message = err.message;
427
+ }
428
+
429
+ //
430
+ // NOTE: this was removed in v10.2.2 due to circular reference issues
431
+ // (the workaround would involve safeStringify and then JSON.parse which would be perf bloat)
432
+ // (there still might be another workaround or perhaps we don't add all the args here except additional)
433
+ //
434
+ // Set `args` prop with original arguments passed
435
+ // meta.args = originalArgs;
436
+
437
+ // Set default level on meta
438
+ meta.level = level;
439
+
440
+ // Add `app` object to metadata
441
+ if (this.appInfo) meta.app = this.appInfo;
442
+
443
+ //
444
+ // determine log method to use
445
+ //
446
+ // if we didn't pass a level as a method
447
+ // (e.g. console.info), then we should still
448
+ // use the logger's `log` method to output
449
+ //
450
+ // and fatal should use error (e.g. in browser)
451
+ //
452
+ const method = modifier === -1 ? 'log' : level;
453
+
454
+ // pre-hooks
455
+ for (const hook of this.config.hooks.pre) {
456
+ [err, message, meta] = hook(method, err, message, meta);
457
+ }
458
+
459
+ //
460
+ // NOTE: using lodash _.omit and _.pick would have been _very slow_
461
+ //
462
+ // const omittedAndPickedFields = {
463
+ // ..._.omit(meta, this.config.meta.omittedFields),
464
+ // ..._.pick(meta, this.config.meta.pickedFields)
465
+ // };
466
+ //
467
+ // also we don't want to mutate anything in `meta`
468
+ // and ideally we only want to pick exactly what we need
469
+ // (and not have two operations, one for omit, and one for pick)
470
+ //
471
+
472
+ // set a boolean flag if we had the silent symbol or not
473
+ const hadTrueSilentSymbol = boolean(meta[silentSymbol]);
474
+
475
+ if (!isEmpty(this.config.meta.remappedFields)) {
476
+ for (const key of Reflect.ownKeys(this.config.meta.remappedFields)) {
477
+ set(meta, this.config.meta.remappedFields[key], get(meta, key));
478
+ unset(meta, key);
479
+ // cleanup empty objects after remapping
480
+ if (this.config.meta.cleanupRemapping) {
481
+ const index = key.lastIndexOf('.');
482
+ if (index === -1) continue;
483
+ const parentKey = key.slice(0, index);
484
+ if (isEmpty(get(meta, parentKey))) unset(meta, parentKey);
485
+ }
486
+ }
487
+ }
488
+
489
+ if (
490
+ !isEmpty(this.config.meta.omittedFields) ||
491
+ !isEmpty(this.config.meta.pickedFields)
492
+ ) {
493
+ const dotified = dotifyToArray(meta);
494
+ // dotified = [
495
+ // 'err.name',
496
+ // 'err.message',
497
+ // 'err.stack',
498
+ // 'level',
499
+ // 'app.name',
500
+ // 'app.version',
501
+ // 'app.node',
502
+ // 'app.hash',
503
+ // // ...
504
+ // ]
505
+
506
+ if (!isEmpty(this.config.meta.omittedFields)) {
507
+ for (const prop of this.config.meta.omittedFields) {
508
+ // <https://stackoverflow.com/a/9882349>
509
+ let i = dotified.length;
510
+ while (i--) {
511
+ if (
512
+ dotified[i] === prop ||
513
+ (!isSymbol(dotified[i]) && dotified[i].indexOf(`${prop}.`) === 0)
514
+ )
515
+ dotified.splice(i, 1);
516
+ }
517
+ }
518
+ }
519
+
520
+ const pickedSymbols = [];
521
+
522
+ if (!isEmpty(this.config.meta.pickedFields)) {
523
+ for (const prop of this.config.meta.pickedFields) {
524
+ // response.headers.boop
525
+ // response.headers.beep
526
+ // response.body
527
+ // response.text
528
+ // response
529
+ //
530
+ // so we need to split by the first period and omit any keys from dotified starting with it
531
+ if (isSymbol(prop)) {
532
+ if (meta[prop]) pickedSymbols.push([prop, meta[prop]]);
533
+ } else {
534
+ const index = prop.indexOf('.');
535
+ const key = prop.slice(0, index + 1);
536
+ if (index !== -1) {
537
+ let i = dotified.length;
538
+ while (i--) {
539
+ if (dotified[i] === key.slice(0, -1)) dotified.splice(i, 1);
540
+ else if (dotified[i].indexOf(key) === 0) dotified.splice(i, 1);
541
+ }
542
+ }
543
+ }
544
+
545
+ // finally add it if it did not already exist
546
+ if (dotified.indexOf(prop) === -1) dotified.push(prop);
547
+ }
548
+ }
549
+
550
+ //
551
+ // iterate over all dotified values to check for symbols
552
+ //
553
+ // NOTE: this does not take into account that they could already be in `pickedSymbols`
554
+ // (and it doesn't also do bigints yet)
555
+ //
556
+ for (const prop of dotified) {
557
+ if (isSymbol(prop)) {
558
+ if (meta[prop] !== undefined) pickedSymbols.push([prop, meta[prop]]);
559
+ } else if (meta[Symbol.for(prop)] !== undefined) {
560
+ pickedSymbols.push([Symbol.for(prop), meta[Symbol.for(prop)]]);
561
+ }
562
+ }
563
+
564
+ //
565
+ // now we call pick-deep using the final array
566
+ //
567
+ // NOTE: this does not pick symbols, bigints, nor streams
568
+ // <https://github.com/strikeentco/pick-deep/issues/2>
569
+ // <https://github.com/strikeentco/pick-deep/issues/2>
570
+ //
571
+ // NOTE: this is wrapped in try/catch in case similar errors occur
572
+ // <https://github.com/stripe/stripe-node/issues/1796>
573
+ //
574
+ try {
575
+ meta = pickDeep(meta, dotified);
576
+ } catch (err) {
577
+ this.config.logger.error(err);
578
+ }
579
+
580
+ //
581
+ // if there were any top-level symbols to be
582
+ // picked then we need to add them back here to the list
583
+ //
584
+ // NOTE: we'd probably want to do the same for bigints as symbols
585
+ //
586
+ if (pickedSymbols.length > 0) {
587
+ for (const [key, value] of pickedSymbols) {
588
+ meta[key] = value;
589
+ }
590
+ }
591
+ }
592
+
593
+ // only invoke logger methods if it was not silent
594
+ if (!this.config.silent && !hadTrueSilentSymbol) {
595
+ // Show stack trace if necessary (along with any metadata)
596
+ if (isError(err) && this.config.showStack) {
597
+ if (!this.config.meta.show || isEmpty(meta)) {
598
+ this.config.logger[method](...(hasMessage ? [message, err] : [err]));
599
+ } else if (
600
+ this.config.meta.hideMeta &&
601
+ meta[this.config.meta.hideMeta]
602
+ ) {
603
+ this.config.logger[method](...(hasMessage ? [message, err] : [err]));
604
+ } else {
605
+ this.config.logger[method](
606
+ ...(hasMessage ? [message, err, meta] : [err, meta])
607
+ );
608
+ }
609
+ } else if (!this.config.meta.show || isEmpty(meta)) {
610
+ this.config.logger[method](message);
611
+ } else if (
612
+ (this.config.meta.hideMeta && meta[this.config.meta.hideMeta]) ||
613
+ (this.config.meta.hideHTTP && meta[this.config.meta.hideHTTP])
614
+ ) {
615
+ this.config.logger[method](message);
616
+ } else {
617
+ this.config.logger[method](message, meta);
618
+ }
619
+ }
620
+
621
+ // post-hooks
622
+ if (this.config.hooks.post.length === 0)
623
+ return { method, err, message, meta };
624
+ return pMapSeries(this.config.hooks.post, (hook) =>
625
+ hook(method, err, message, meta)
626
+ )
627
+ .then()
628
+ .catch((err) => {
629
+ this.config.logger.error(err);
630
+ });
631
+ }
632
+ }
633
+
634
+ module.exports = Axe;