A NodeJS “Preprocessor” Can Add Any Functionality Including Easy Logging

Ever forget one of these: An image showing a console.log statement that was supposed to be removed from the submitted LeetCode test runner A console.log statement that needs to be removed before submitting from the LeetCode test runner. Yes, I know we should all be good enough to not need to ever use console.log statements, or to even iterate for that matter but sometimes I find myself tired and working on a project and just want to double check my data structure is in the correct state.

The issue however, is that I find it very annoying to:

  • Have to remove these console.log statements all the time when submitting to the LeetCode test runner
  • Sometimes I want to keep them around in case I want to come back and iterate on this data structure or algorithm later
  • It isn't always clear right away if your submission passes but is incrediblys slow if you just forgot to remove a console.log or if your code is sub par

Hence the creation of nodelog! A NodeJS preprocessor that will not only allow you to console.log anything that has a String representation but that uses comments to dictate what to log so that whenever the code is ran without the preprocessor, the log statements are ignored.

Behold:

An animated gif showing the usage of the NodeJS "preprocessor"

And all this requires is a single Javascript file which is only ran whenever you use whichever alias you use to include this file. In this example, I use nodelog as my alias which usesa a NodeJS argument to require a Javascript file before running the actual code.

So most languages have what are called preprocessors. This is a compilation step where your source code is taken and modified before it is actually compiled.

For exmaple, a C preprocessor is a tool that processes source code before the actual compilation begins. It handles directives that begin with #, such as #include to include files, #define to create macros, and #ifdef for conditional compilation. The preprocessor replaces macros (all those #define lines), includes the contents of header files, and conditionally compiles code, producing a modified source file that is then passed to the compiler.

In other languages, macros are usually great ways to re use code without needing a function and as a result inlinnig the code defined by the macro.

NodeJS doesn't have any sort of preprocessor step. IT does however have an option to run a file before execution. And with this you can do some things like maybe alias console.log to a function cl:

global.cl = console.log

But I was having issues where I would write some code for LeetCode and maybe leave in a few console.log calls which severely slows down execution time.

I also wanted a way that would amke it easier to log out different values without having to write so much code.

To do so I created my own NodejS "PreProcessor" which will add the ability for me to easily log out anything I want with a comment starting with: //log - followed by whatever statements I want logged.

This way, when I run locally, I can see the output of the logs, but when I submit to LeetCode or anywhere else for that matter, the log lines are just ignored.

I also created a shell/zsh alias so that this preprocessor is only used when I run nodelog instead of node. Just to be safe and not mess with my actual node setup.

I created this file in ~/.config/nodelog.js and added the following code:

const fs = require("fs");
const vm = require("vm");

const BOLD = `\\x1b[1m`;

// Basic Text Color Codes
const BLACK = `\\x1b[30m`;
const RED = `\\x1b[31m`;
const GREEN = `\\x1b[32m`;
const YELLOW = `\\x1b[33m`;
const BLUE = `\\x1b[34m`;
const MAGENTA = `\\x1b[35m`;
const CYAN = `\\x1b[36m`;
const WHITE = `\\x1b[37m`;

// Bright Text Color Codes
const BRIGHT_BLACK = `\\x1b[90m`;
const BRIGHT_RED = `\\x1b[91m`;
const BRIGHT_GREEN = `\\x1b[92m`;
const BRIGHT_YELLOW = `\\x1b[93m`;
const BRIGHT_BLUE = `\\x1b[94m`;
const BRIGHT_MAGENTA = `\\x1b[95m`;
const BRIGHT_CYAN = `\\x1b[96m`;
const BRIGHT_WHITE = `\\x1b[97m`;

// Reset Code
const RESET = `\\x1b[0m`;

function preprocessAndRunJavaScript(inputFile) {
  // Read the content of the input file
  const fileContent = fs.readFileSync(inputFile, "utf-8");

  // Regular expression to match lines with the `// log -` pattern
  const logRegex = /\/\/\s*log\s*-\s*(.+)/g;

  // Replace matched lines with the corresponding console.log statement
  const processedContent = fileContent.replace(logRegex, (match, p1) => {
    const variables = p1.split(",");

    // Determine the maximum length of variable names for proper alignment
    const maxLength = Math.max(...variables.map(v => v.trim().length));

    const logStatement = variables
      .map((v) => {
        // Pad variable names to align the columns
        const paddedVar = v.trim().padEnd(maxLength);
        return `"${BRIGHT_WHITE}${BOLD}${paddedVar}${RESET}: ", ${v}, "\\t"`;
      })
      .join(", ");

    return `console.log(${logStatement});`;
  });

  // Create a new script from the processed content
  const script = new vm.Script(processedContent);

  // Run the script in the current context
  script.runInThisContext();
  process.exit(0);
}

// Get the file path from the command line arguments
const inputFile = process.argv[1];

if (!inputFile) {
  console.error("Please provide an input file");
  process.exit(1);
}
preprocessAndRunJavaScript(inputFile);

Then I edited my ~/.zshrc file to include:

function nodelog() {
    NODE_OPTIONS="$NODE_OPTIONS --require $HOME/.config/nodelog.js" node $@
}

Now I can write code like (filename is dailyTemperatures.js):

var dailyTemperatures = function (temps) {
    const memo = {};
    const answer = [];
    for (let i = 1; i < temps.length; i++) {
        // log - temps[i], temps[i - 1], i
etc...

I use my nodelog alias:

nodelog dailyTemperatures.js

And in the console I see:

temps[i]:  80   temps[i - 1]:  34   i:  1

And best of all, I no longer need to worry about removing any console.log statements if I am just hacking through a LeetCode problem anymore.