Last active
August 14, 2017 04:23
-
-
Save enten/77eb6a9d92e9cf465ae2daf43d087c83 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const nodeNotifier = require('node-notifier') | |
const supportsColor = require('supports-color') | |
const { | |
inspect, | |
format | |
} = require('util') | |
const DEFAULT = { | |
colors: !!supportsColor, | |
defaultLevel: 'info', | |
defaultStream: 'stdout', | |
levels: { | |
silent: { | |
n: Infinity, | |
stream: 'stderr', | |
}, | |
error: { | |
n: 5000, | |
label: 'ERR!', | |
labelStyles: ['bgRed', 'yellow'], | |
notifIcon: '✗', | |
notifIconStyles: ['red'], | |
notifStyles: ['bold'], | |
stream: 'stderr', | |
}, | |
warn: { | |
n: 4000, | |
label: 'WARN', | |
labelStyles: ['bgYellow', 'red'], | |
notifIcon: '❗', | |
notifIconStyles: ['yellow'], | |
notifStyles: ['bold'], | |
stream: 'stderr' | |
}, | |
http: { | |
n: 3000, | |
label: 'http', | |
labelStyles: ['magenta'], | |
notifIcon: '➚', | |
notifIconStyles: ['magenta'], | |
notifStyles: ['bold'], | |
stream: 'stdout' | |
}, | |
info: { | |
n: 2000, | |
label: 'info', | |
labelStyles: ['green'], | |
notifIcon: '✓', | |
notifIconStyles: ['green'], | |
notifStyles: ['bold'], | |
stream: 'stdout' | |
}, | |
verbose: { | |
n: 1000, | |
label: 'verb', | |
labelStyles: ['cyan'], | |
notifStyles: ['bold'], | |
stream: 'stdout' | |
}, | |
silly: { | |
n: -Infinity, | |
label: 'sill', | |
labelStyles: ['blackBright', 'inverse'], | |
notifStyles: ['blackBright', 'bold'], | |
stream: 'stdout', | |
styles: ['blackBright'] | |
} | |
}, | |
nodeLogKey: 'NODE_LOG', | |
notifier: nodeNotifier, | |
notifs: false, | |
notifsTitle: `${process.pid} ${process.title}`, | |
notifsUrlBullet: '➥' | |
} | |
const STREAM = [ | |
'stdout', | |
'stderr' | |
] | |
const STYLE = { | |
bold: decorateText.bind(null, '\u001b[1m', '\u001b[22m'), | |
dim: decorateText.bind(null, '\u001b[2m', '\u001b[22m'), | |
italic: decorateText.bind(null, '\u001b[3m', '\u001b[23m'), | |
underline: decorateText.bind(null, '\u001b[4m', '\u001b[24m'), | |
blink: decorateText.bind(null, '\u001b[5m', '\u001b[25m'), | |
inverse: decorateText.bind(null, '\u001b[7m', '\u001b[27m'), | |
hidden: decorateText.bind(null, '\u001b[8m', '\u001b[28m'), | |
strikethrough: decorateText.bind(null, '\u001b[9m', '\u001b[29m'), | |
black: decorateText.bind(null, '\u001b[30m', '\u001b[39m'), | |
blackBright: decorateText.bind(null, '\u001b[90m', '\u001b[39m'), | |
bgBlack: decorateText.bind(null, '\u001b[40m', '\u001b[49m'), | |
bgBlackBright: decorateText.bind(null, '\u001b[100m', '\u001b[49m'), | |
red: decorateText.bind(null, '\u001b[31m', '\u001b[39m'), | |
redBright: decorateText.bind(null, '\u001b[91m', '\u001b[39m'), | |
bgRed: decorateText.bind(null, '\u001b[41m', '\u001b[49m'), | |
bgRedBright: decorateText.bind(null, '\u001b[101m', '\u001b[49m'), | |
green: decorateText.bind(null, '\u001b[32m', '\u001b[39m'), | |
greenBright: decorateText.bind(null, '\u001b[92m', '\u001b[39m'), | |
bgGreen: decorateText.bind(null, '\u001b[42m', '\u001b[49m'), | |
bgGreenBright: decorateText.bind(null, '\u001b[102m', '\u001b[49m'), | |
yellow: decorateText.bind(null, '\u001b[33m', '\u001b[39m'), | |
yellowBright: decorateText.bind(null, '\u001b[93m', '\u001b[39m'), | |
bgYellow: decorateText.bind(null, '\u001b[43m', '\u001b[49m'), | |
bgYellowBright: decorateText.bind(null, '\u001b[103m', '\u001b[49m'), | |
blue: decorateText.bind(null, '\u001b[34m', '\u001b[39m'), | |
blueBright: decorateText.bind(null, '\u001b[94m', '\u001b[39m'), | |
bgBlue: decorateText.bind(null, '\u001b[44m', '\u001b[49m'), | |
bgBlueBright: decorateText.bind(null, '\u001b[104m', '\u001b[49m'), | |
magenta: decorateText.bind(null, '\u001b[35m', '\u001b[39m'), | |
magentaBright: decorateText.bind(null, '\u001b[95m', '\u001b[39m'), | |
bgMagenta: decorateText.bind(null, '\u001b[45m', '\u001b[49m'), | |
bgMagentaBright: decorateText.bind(null, '\u001b[105m', '\u001b[49m'), | |
cyan: decorateText.bind(null, '\u001b[36m', '\u001b[39m'), | |
cyanBright: decorateText.bind(null, '\u001b[96m', '\u001b[39m'), | |
bgCyan: decorateText.bind(null, '\u001b[46m', '\u001b[49m'), | |
bgCyanBright: decorateText.bind(null, '\u001b[106m', '\u001b[49m'), | |
white: decorateText.bind(null, '\u001b[37m', '\u001b[39m'), | |
whiteBright: decorateText.bind(null, '\u001b[97m', '\u001b[39m'), | |
bgWhite: decorateText.bind(null, '\u001b[47m', '\u001b[49m'), | |
bgWhiteBright: decorateText.bind(null, '\u001b[107m', '\u001b[49m') | |
} | |
STYLE.grey = STYLE.gray = STYLE.blackBright // fix humans | |
STYLE.bgGrey = STYLE.bgGray = STYLE.bgBlackBright // fix humans | |
function Logger (options) { | |
if (typeof options === 'string') { | |
options = {level: options} | |
} | |
options = Object.assign({}, DEFAULT, {styles: STYLE}, options) // out of scope | |
if (!options.level && options.nodeLogKey) { | |
options.level = (options.proc || process).env[options.nodeLogKey] // global | |
} | |
if (options.level && !options.levels.hasOwnProperty(options.level)) { | |
options.level = options.defaultLevel | |
} | |
options.level = options.level || options.defaultLevel | |
const api = (level, ...args) => { | |
let printer = api.log | |
if (typeof api[level] === 'function') { | |
printer = api[level] | |
} else { | |
args.unshift(level) | |
} | |
printer(...args) | |
} | |
const log = (...args) => { | |
emitMessage(api, { | |
showable: !isLoggerSilent(api), | |
raw: args | |
}) | |
} | |
const dir = (obj, inspectOptions) => { | |
emitMessage(api, createMessageInspection(api, null, obj, inspectOptions, { | |
showable: !isLoggerSilent(api) | |
})) | |
} | |
const trace = (...args) => { | |
emitMessage(api, createMessageTrace(api, null, args, { | |
showable: !isLoggerSilent(api) | |
})) | |
} | |
Object.defineProperties(api, { | |
'name': { | |
configurable: true, | |
enumerable: false, | |
get: () => `Logger<${api.options.level}>` | |
}, | |
'options': { | |
configurable: true, | |
enumerable: true, | |
value: { | |
buffer: [], | |
paused: false | |
}, | |
writable: true | |
} | |
}) | |
Object.assign(api, { | |
configure: configureLogger.bind(null, api), | |
dir, | |
log, | |
pause: pauseLogger.bind(null, api), | |
resume: resumeLogger.bind(null, api), | |
trace | |
}) | |
configureLogger(api, options) // side effect | |
return api | |
} | |
function configureLogger (api, options) { | |
if (typeof options === 'string' && arguments.length > 2) { | |
options = {[options]: arguments[2]} | |
} | |
Object.assign(api.options, options) // side effect | |
if (options.levels) { | |
Object.keys(options.levels).forEach((key) => { | |
const level = createLoggerLevel(api, key) | |
api[key] = level // side effect | |
}) | |
} | |
return api | |
} | |
function createLoggerLevel (logger, level) { | |
const api = logMessage.bind(null, logger, level) | |
Object.defineProperties(api, { | |
'name': { | |
configurable: true, | |
enumerable: false, | |
value: level, | |
writable: true | |
}, | |
'options': { | |
configurable: true, | |
enumerable: false, | |
get: () => logger.options.levels[level] | |
} | |
}) | |
;[ | |
['dir', logMessageInspection], | |
['nocolor', logMessageNoColors], | |
['nocolors', logMessageNoColors], // fix humans | |
['notify', logMessageNotification], | |
['raw', logMessageRaw], | |
['styles', logMessageStyles], | |
['trace', logMessageTrace], | |
].forEach(([key, fn]) => { | |
Object.defineProperty(api, key, { | |
configurable: false, | |
value: fn.bind(null, logger, level), | |
writable: true | |
}) | |
}) | |
return api | |
} | |
function createMessage (logger, level, options) { | |
if (Array.isArray(options)) { | |
options = Object.assign({raw: options}, arguments[3]) | |
} | |
const levelOptions = logger.options.levels[level] || {} | |
const showable = isLevelShowable(logger, level) | |
return Object.assign({}, levelOptions, { | |
colors: logger.options.colors, | |
level, | |
label: levelOptions.label || level, | |
showable | |
}, options) | |
} | |
function createMessageInspection (logger, level, obj, inspectOptions, options) { | |
inspectOptions = Object.assign({ | |
colors: logger.options.colors | |
}, inspectOptions) | |
const inspection = inspect(obj, inspectOptions) | |
return createMessage(logger, level, [inspection], options) | |
} | |
function createMessageTrace (logger, level, args, options) { | |
const err = { | |
name: 'Trace', | |
message: format.apply(null, args) | |
} | |
Error.captureStackTrace(err) // side effect | |
return createMessage(logger, level, [err.stack], options) | |
} | |
function decorateText (open, close, text) { | |
return [open, text, close].join('') | |
} | |
function emitMessage (logger, msg) { | |
if (logger.options.paused) { | |
logger.options.buffer.push(msg) // side effect | |
} else { | |
printMessage(logger, msg) | |
} | |
} | |
function isLevelShowable (logger, level) { | |
if (isLoggerSilent(logger)) { | |
return false | |
} | |
const {levels} = logger.options | |
const currentLevel = levels[logger.options.level] | |
const testLevel = levels[level] | |
if (testLevel && testLevel.n === Infinity) { | |
return false | |
} | |
return !currentLevel || !testLevel || currentLevel.n <= testLevel.n | |
} | |
function isLoggerSilent (logger) { | |
const {levels} = logger.options | |
const currentLevel = levels[logger.options.level] | |
const {n} = currentLevel | |
return n === Infinity | |
} | |
function logMessage (logger, level, ...args) { | |
const msg = createMessage(logger, level, args) | |
emitMessage(logger, msg) | |
} | |
function logMessageInspection (logger, level, obj, options) { | |
if (typeof options === 'string') { | |
options = {title: options} | |
} | |
const msg = createMessageInspection(logger, level, obj, options) | |
if (options && options.title) { | |
logMessageStyles(logger, level, 'bold', options.title) | |
} | |
emitMessage(logger, msg) | |
} | |
function logMessageNoColors (logger, level, ...args) { | |
const msg = createMessage(logger, level, args, {colors: false}) | |
emitMessage(logger, msg) | |
} | |
function logMessageNotification (logger, level, options, notifCb) { | |
if (typeof options === 'string') { | |
options = {message: options} | |
} | |
if (typeof notifCb === 'string') { | |
options.open = notifCb | |
notifCb = undefined | |
} | |
if (!options.message) { | |
return | |
} | |
const msg = createMessage(logger, level, { | |
notif: options, | |
notifCb, | |
raw: [options.message] | |
}) | |
emitMessage(logger, msg) | |
if (options.open) { | |
const urlBullet = options.notifsUrlBullet || logger.options.notifsUrlBullet | |
const open = [].concat(urlBullet || [], options.open).join(' ') | |
logMessage(logger, level, open) | |
} | |
} | |
function logMessageRaw (logger, level, ...args) { | |
const msg = createMessage(logger, level, args, { | |
label: null | |
}) | |
emitMessage(logger, msg) | |
} | |
function logMessageStyles (logger, level, styles, ...args) { | |
const msg = createMessage(logger, level, args, {styles}) | |
emitMessage(logger, msg) | |
} | |
function logMessageTrace (logger, level, ...args) { | |
const msg = createMessageTrace(logger, level, args) | |
emitMessage(logger, msg) | |
} | |
function pauseLogger (logger) { | |
logger.options.paused = true // side effect | |
return logger | |
} | |
function printMessage (logger, msg) { | |
if (!msg.showable) { | |
return | |
} | |
let { | |
colors, | |
label, | |
labelStyles, | |
level, | |
notif, | |
raw, | |
stream, | |
styles | |
} = msg | |
stream = stream || logger.options.defaultStream | |
if (typeof stream === 'string') { | |
stream = ~STREAM.indexOf(stream) ? process[stream] : process.stdout // out of scope + global | |
} | |
const stylize = stylizeText.bind(null, logger.options.styles) | |
let text = raw.length > 1 ? format.apply(null, raw) : raw[0] | |
if (colors && logger.options.styles) { | |
label = stylize(labelStyles, label) | |
text = stylize(styles, text) | |
} | |
if (notif) { | |
let { | |
notifIcon = msg.notifIcon, | |
notifIconStyles = msg.notifIconStyles, | |
notifStyles = msg.notifStyles, | |
title = msg.notifsTitle || logger.options.notifsTitle | |
} = notif | |
title = [].concat(title || [], '-', level, notifIcon || []).join(' ') | |
text = colors ? stylize(notifStyles, notif.message) : text | |
if (notifIcon) { | |
notifIcon = colors ? stylize(notifIconStyles, notifIcon) : notifIcon | |
text = [notifIcon, text].join(' ') | |
} | |
if (logger.options.notifs) { | |
notif = Object.assign({}, notif, {title}) | |
logger.options.notifier.notify(notif, msg.notifCb) | |
} | |
} | |
const chunks = [text, '\n'] | |
label && chunks.unshift(label, ' ') | |
stream.write(chunks.join('')) | |
} | |
function resumeLogger (logger) { | |
const {buffer} = logger.options | |
logger.options.buffer = [] // side effect | |
logger.options.paused = false // side effect | |
buffer.forEach((msg) => emitMessage(logger, msg)) | |
return logger | |
} | |
function stylizeText (brushs, styles, text) { | |
if (!styles || !text) { | |
return text | |
} | |
return [].concat(styles || []).reduce((acc, style) => { | |
if (brushs && typeof style === 'string') { | |
style = brushs[style] | |
} | |
if (typeof style === 'function') { | |
acc = style(acc) | |
} | |
return acc | |
}, text) | |
} | |
module.exports = Object.assign(Logger.bind(), { | |
DEFAULT, | |
STREAM, | |
STYLE, | |
Logger, | |
configureLogger, | |
createLoggerLevel, | |
createMessage, | |
createMessageInspection, | |
createMessageTrace, | |
decorateText, | |
emitMessage, | |
isLevelShowable, | |
isLoggerSilent, | |
logMessage, | |
logMessageInspection, | |
logMessageNoColors, | |
logMessageNotification, | |
logMessageRaw, | |
logMessageStyles, | |
logMessageTrace, | |
pauseLogger, | |
resumeLogger, | |
stylizeText | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment