Skip to content

Instantly share code, notes, and snippets.

@cptSwing
Created November 27, 2024 11:02
Show Gist options
  • Save cptSwing/0b63288955b30efda603f019ccf00c2c to your computer and use it in GitHub Desktop.
Save cptSwing/0b63288955b30efda603f019ccf00c2c to your computer and use it in GitHub Desktop.
/* eslint-disable no-console */
import { cloneDeep } from 'lodash';
type DevConsoleOptions = {
errorHandlerName?: string;
printStack?: boolean;
clone?: boolean; // clone the argument (to create a snapshot at time of execution) ?
devMode?: boolean; // boolean value to use instead of NODE_ENV
};
const isOptsObject = (arg: unknown) =>
Object.keys(arg as keyof DevConsoleOptions).some(
(key) => key === 'errorHandlerName' || key === 'printStack' || key === 'clone' || key === 'devMode',
);
/** First argument can be an options object with keys 'printStack', 'clone', 'devMode' */
const DevConsole = {
log: (...args: unknown[]) => consoleFn(args[0] && isOptsObject(args[0]) ? { ...args[0], method: 'log' } : { method: 'log' }, ...args),
warn: (...args: unknown[]) =>
consoleFn(args[0] && isOptsObject(args[0]) ? { ...args[0], method: 'warn' } : { method: 'warn' }, ...args),
error: (...args: unknown[]) =>
consoleFn(args[0] && isOptsObject(args[0]) ? { ...args[0], method: 'error' } : { method: 'error' }, ...args),
};
export default DevConsole;
function consoleFn(
opts: DevConsoleOptions & {
method: 'log' | 'warn' | 'error';
},
...args: unknown[]
) {
const { method, errorHandlerName, printStack, clone, devMode } = opts;
/* run only in dev mode */
const isDev = devMode ? devMode : process.env.NODE_ENV === 'production';
if (isDev) {
const cloneArg = clone ? clone : true;
if (args) {
const errorStack = new Error().stack;
const callerFunction = getCallerFromErrorStack(errorStack, errorHandlerName);
let optsString = ' ';
if (args[0] && isOptsObject(args[0])) {
/* remove the opts object from args: */
args.splice(0, 1);
if (printStack !== undefined) {
optsString += `("stack": ${printStack})`;
}
if (clone !== undefined) {
optsString += `("cloned": ${clone})`;
}
if (devMode !== undefined) {
optsString += `("devMode": ${devMode})`;
}
}
const consoleArgs = args.map((arg) => {
if (typeof arg === 'string') {
return arg;
} else if (cloneArg) {
return cloneDeep(arg);
} else {
return arg;
}
});
consoleArgs.unshift(`DC${optsString}:%c[${callerFunction}]`, `color: ${randomHexColor(callerFunction)};`);
if (printStack) {
console[method](...consoleArgs, '\n\n', 'DevConsole.printStack: ', errorStack);
} else {
console[method](...consoleArgs);
}
}
}
}
function getCallerFromErrorStack(errorStack: Error['stack'], errorHandlerName: string | undefined) {
if (!errorStack) {
return 'Undefined';
}
const splitErrorStack = errorStack.split('\n');
let errorStackStartIndex = 3; // 0 = empty line, 1 = consoleFn, 2 = Object.log, so ...
if (errorHandlerName) {
if (splitErrorStack[errorStackStartIndex].includes(errorHandlerName)) {
errorStackStartIndex += 1;
}
}
const initialAttempt = getCaller(errorStackStartIndex, splitErrorStack);
/* Dependant on webpack: `config.devtool = env === 'development' ? 'eval-source-map' : config.devtool` */
if (initialAttempt === 'eval') {
return extractFunctionNameFromEvalResult(splitErrorStack[errorStackStartIndex]);
} else {
return initialAttempt;
}
}
/**
*
* Helper functions:
*
*/
/** Returns a random `#FFFFFF` hex color code if no arguments, or generates a hex code from passed in number/string */
function randomHexColor(num?: string | number) {
if (num) {
let sum = 0;
let numString: string;
if (typeof num === 'number') {
numString = num.toString();
} else {
numString = num;
}
for (let i = 0; i < numString.length; i++) {
sum += numString.charCodeAt(i);
}
/* This is kinda arbitrary */
sum = sum / (numString.length * 1000);
/* The number 16,777,215 is the total possible combinations of RGB(255,255,255) which is 32 bit colour. */
const hexSum = `#${Math.floor(sum * 16777215).toString(16)}`;
return hexSum;
} else {
return `#${Math.floor(Math.random() * 16777215).toString(16)}`;
}
}
function getCaller(errorStackStartIndex: number, splitErrorStack: string[]) {
const a = splitErrorStack[errorStackStartIndex];
const b = a && a.trim(); // trim whitespaces beginning/end
const bSplit = b.split(' '); // split at empty spaces
let c: string;
if (b.includes('async')) {
c = b && bSplit[2];
} else {
c = b && bSplit[1];
}
const d = c && c.match(/\w+/)![0];
return d;
}
/* Function to strip function name (fx, 'useUpdateMesh') for an explicit string in this format: " at eval (webpack-internal:///./src/lib/hooks/useUpdateMesh.tsx:233:78)" */
function extractFunctionNameFromEvalResult(str: string) {
// Extract the part of the string between the last slash and the colon
const start = str.lastIndexOf('/') + 1;
const end = str.lastIndexOf(':');
const substring = str.substring(start, end);
// Split the substring by '.' and get the second last part
const parts = substring.split('.');
const functionName = parts[parts.length - 2];
return functionName;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment