Last active
October 21, 2022 13:42
-
-
Save theprojectsomething/47093d0c2825bdcd8e8461831419db3b to your computer and use it in GitHub Desktop.
Cloudflare Wrangler Pretty Console
This file contains 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
/*** | |
* Cloudflare Wrangler Pretty Console 🌈 | |
* === | |
* | |
* Console.log with rainbow-like ease, with allowances for circular references, | |
* maps, sets and proxies ... none of which log very well otherwise. | |
* | |
* - use console.log and console.error, knowing it will be prettier (than nothing) | |
* - adds a nice little timestamp and reference to the calling function | |
* - designed specifically for @cloudflare/wrangler but will probably work with node too | |
* - import at the top of your worker entry point | |
* - comment out the import prior to publishing ... unless you know how to selectively | |
* import a module with wrangler, in which case do that (and tell me how) | |
* - ✌️ | |
* | |
* MIT License | |
* Copyright (c) 2022 theprojectsomething | |
* */ | |
// a dirty hijack | |
const log = console.log.bind(console); | |
const error = console.error.bind(console); | |
// byo rainbow: https://stackoverflow.com/q/9781218/720204 | |
const colours = { | |
default: '\x1b[0;2;36m', | |
key: '\x1b[0;0;36m', | |
function: '\x1b[0;0;33m', | |
string: '\x1b[0;0;32m', | |
date: '\x1b[0;0;37m', | |
undef: '\x1b[0;2;37m', | |
bool: '\x1b[0;0;35m', | |
number: '\x1b[0;0;94m', | |
end: '\x1b[0m', | |
} | |
// errors can be shades of red | |
const errorColours = { | |
default: '\x1b[2;31m', | |
key: '\x1b[0;0;31m', | |
string: '\x1b[0;0;91m', | |
} | |
// circular references are a nightmare part 3: the memory cleanup | |
const cleanup = (object, refs) => { | |
const ref = object && refs.get(object); | |
if (!ref || !ref.children.size) { | |
return; | |
} | |
for (const child of ref.children) { | |
if (refs.has(child)) { | |
const childref = refs.get(child); | |
// delete the reference | |
refs.delete(child); | |
// cleanup - possibly overkill | |
delete childref.parent; | |
childref.children.clear(); | |
} | |
} | |
ref.children.clear(); | |
} | |
// circular references are a nightmare part 2: leave those kids alone | |
const checkCircular = (val, parent, refs) => { | |
if (typeof val === 'object') { | |
// cleanup any refs already under the parent (start this tree fresh) | |
cleanup(parent, refs); | |
// full inception | |
if (parent === val || refs.has(val)) { | |
return true; | |
} | |
// add the object to the refs | |
refs.set(val, { parent, children: new Set(), id: Math.random().toString(36).slice(2) }); | |
// get the parent ref (exists on all but top-level) | |
let parentref = parent && refs.get(parent); | |
// crawl back up the parent heirarchy, adding the child to each ref | |
while (parentref) { | |
parentref.children.add(val); | |
parentref = parentref.parent && refs.get(parentref.parent); | |
} | |
} | |
} | |
// circular references are a nightmare part 1: | |
// let's use the replacer function to highlight some specific object types | |
// ... this can easily be extended for other primitives | |
export function stringify(object, replacer, space) { | |
const refs = new WeakMap(); | |
const stringified = JSON.stringify(object, function (key, val) { | |
// annoyingly we'll update a "new" value rather than abstracting further | |
let newVal = val; | |
if (typeof val === 'function') { | |
newVal = `${val.name || `function`}()`; | |
} else if (val instanceof Map || val instanceof Set) { | |
const isMap = val instanceof Map; | |
newVal = { | |
[`[object ${isMap ? 'Map' : 'Set'}(${val.size})]`]: isMap ? Object.fromEntries(val) : Array.from(val), | |
}; | |
} else if (val === undefined || val === null) { | |
newVal = `[${val === null ? 'null' : 'undefined'}]`; | |
} | |
// mark of the beast | |
if (checkCircular(newVal, newVal !== object && this, refs)) { | |
newVal = '[circular]'; | |
} | |
// our annoying "new" value allows for a second-tier replacer - that we aren't utilising :( | |
return typeof replacer === 'function' ? replacer.bind(this)(key, newVal) : newVal; | |
}, space); | |
// cleanup parent object refs - possibly overkill | |
if (typeof object === 'object') { | |
cleanup(object, refs); | |
refs.delete(object); | |
} | |
return stringified; | |
} | |
// our colouriser, lot's of pretty regexes tied to our undocumented replacer format | |
const colourise = (args, coloursAdjust={}) => { | |
const c = { ...colours, ...coloursAdjust }; | |
const jsonlist = []; | |
// we'll iterate over each argument to be logged and stringify it using our custom replacer | |
for (const arg of args) { | |
const jsonarg = stringify(arg, null, ' ') | |
// then colourise the resulting string based on some pseuodo-complex regex logic (the order somewhat matters) | |
// this one is for functions | |
.replace(/"(\S+\(\)|\[circular\])"(?=$|,?\n)/g, `${c.function}$1${c.default}`) | |
// maps and sets are fun | |
.replace(/(\n(\s+)\S+: ){\n(\s+)"\[object ([^\]]+)\]": ([\s\S]+?)\n\2}/g, (match, prefix, indent0, indent1, key, val) => | |
`${prefix}${c.function}${key}${c.default} ${val.replace(/^(\s+)(.*)$/gm, (submatch, space, subval) => | |
`${space.slice(0, indent0.length - indent1.length)}${subval}`)}`) | |
// object keys | |
.replace(/^(\s+)"([^"]+)"(?=:)/gm, `$1${c.key}$2${c.default}`) | |
// dates | |
.replace(/(^| )"([\d-T:.]+Z)"(?=,?$)/gm, (match, prefix, date) => `${prefix}${c.date}Date: ${new Date(date).toLocaleString()}${c.default}`) | |
// undefined | |
.replace(/(^| )"\[(undefined|null)\]"(?=,?$)/gm, `$1${c.undef}$2${c.default}`) | |
// strings | |
.replace(/(".*?)(?=,?$)/gm, `${c.string}$1${c.default}`) | |
// booleans | |
.replace(/(^| )(true|false)(?=,?$)/gm, `$1${c.bool}$2${c.default}`) | |
// and my favourite, numbers | |
.replace(/(^| )([\d.]*?(?=,?$))/gm, (match, prefix, num) => `${prefix}${c.number}${(+num).toLocaleString()}${c.default}`) | |
jsonlist.push(`${c.default}${jsonarg}${c.end}`); | |
} | |
// return the list for logging | |
return jsonlist; | |
} | |
// gives us a nice timestring for each of our logs | |
const timestring = (style='\x1b[2m') => | |
`${style}${new Date().toTimeString().replace(/ .*/, '')} ${getCaller(4)} »${colours.end}`; | |
// retrieves the calling function (line numbers are useless in a compiled worker) | |
const getCaller = (depth=3) => | |
(new Error).stack.split('\n')[depth].replace(/^\s*at (\S+).*$/, '@$1'); | |
// overrides the default console log/error methods | |
export const enable = () => { | |
console.log = (...args) => log(timestring(), ...colourise(args)); | |
console.error = (...args) => error(timestring(errorColours.key), ...colourise(args, errorColours)); | |
} | |
// removes the overrides from the default console log/error methods | |
export const disable = () => { | |
console.log = log; | |
console.error = error; | |
} | |
// let's not muck around | |
enable(); | |
export default { enable, disable }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment