Skip to content

Instantly share code, notes, and snippets.

@enten
Last active August 14, 2017 04:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save enten/77eb6a9d92e9cf465ae2daf43d087c83 to your computer and use it in GitHub Desktop.
Save enten/77eb6a9d92e9cf465ae2daf43d087c83 to your computer and use it in GitHub Desktop.
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