Skip to content

Instantly share code, notes, and snippets.

@theprojectsomething
Last active October 21, 2022 13:42
Show Gist options
  • Save theprojectsomething/47093d0c2825bdcd8e8461831419db3b to your computer and use it in GitHub Desktop.
Save theprojectsomething/47093d0c2825bdcd8e8461831419db3b to your computer and use it in GitHub Desktop.
Cloudflare Wrangler Pretty Console
/***
* 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