2024-07-01 Starting Node.js with selected packages preloaded

There are several Node.js packages I use fairly often, and often need to debug some code using them. As any Lisper will tell you, there are few better ways to debug/explore code than a REPL. Node.js comes with a decent enough REPL for simple experiments, but what if I want to play around with Lodash or Ramda, not just vanilla JS?

(Ramda has an online REPL to play around with, and it’s pretty nice, but it has one important drawback: it’s a cloud service. I can’t just paste some code from the app I’m working on if said code contains anything even slightly confidential.)

It turns out that Node.js comes with a --require CLI option, which sounds nice in theory, but is next to useless in this case. Even if I say node --require lodash, nothing is actually put in the global scope (and by the way, how would it know to assign lodash to _ as is customary to do?).

Here is one way to start Node.js with Lodash preloaded, ready for experimenting:

node -i -e "const _ = require('lodash')"

This is nice and often enough, but what if I need more than one package? And what if I don’t want to type this again and again, possibly with various packages?

I searched for a bit and I found several packages which do exactly what I need (or so it seems). Unfortunately, I couldn’t get them to work – so I decided to roll out my own. It is really fairly simple, though it was not obvious to me at first how to spawn an interactive REPL from within a Node.js script. It turns out that child_process.exec() and friends are not really suitable for the job (although I’m still not 100% sure why – I suspect they do not set stdin and stdout of the spawned process correctly), but child_process.spawn works fine.

After a few minutes of fiddling, I came up with this little script.

const fs = require('fs');
const path = require('path');
const child_process = require('child_process');

function main() {
        let directory = process.cwd();
        let package_json;
        while (true) {
                try {
                        package_json = JSON.parse(
                                fs.readFileSync(path.join(directory, 'package.json'), {encoding: 'utf8'}),
                        );
                        break;
                } catch(error) {
                        directory = path.dirname(directory);
                        // Break if at top directory
                        if (directory === path.dirname(directory)) {
                                break;
                        }
                }
        }

        if (!package_json) {
                console.error('package.json invalid or not found');
                process.exit(1);
        }

        const {node_repl} = package_json;
        if (!node_repl) {
                console.error('No `node_repl` property in `package.json`')
                process.exit(2);
        }
        const modules = Object.keys(node_repl?.packages).map(
                key => `const ${key} = require('${node_repl?.packages[key]}')`,
        ).join(';\n')
        child_process.spawn('node', ['--interactive', '--eval', modules], {stdio: 'inherit'});
};

main();

I’m not sure if it will work on Windows – I hope the trick I employed to check if we reached the root directory will work there, but I’ll have to check it when I have access to a Windows machine (this should happen later this week). The reason I do the whole “go up the directory tree until you find package.json​” thing is that I need to somehow tell my script what modules you want preloaded and under what names (for example, you’d probably want Lodash to be _ and Ramda to be R, etc.). Introducing another config file just for that seems redundant – especially that you need them in node_modules anyway, so package.json seems fitting for the purpose. And you might want to start your REPL in some subdirectory, so making it work only in the project’s root directory may be a bit too restrictive.

(Initially, I used path.resolve('directory', '..') instead of path.dirname(directory), but they work the same.)

By the way, normally Node.js can do all that directory tree traversing for you, and you can just say const package_json = require('./package.json') (even if you’re deeper in the directory tree). This would not work here, since it would load the package.json file of my script, not the one of the project I’m in!

I hope someone finds it useful. I certainly do, and I put it on npm for you to check out! It is a bit of a mess – it is my first published npm package and I learned as I went through several iterations. That’s why it’s at version 1.0.3, but there are no earlier versions – I fixed a few errors and used git rebase to keep the history clean, but you can’t publish a new version unless you bump the version number. Technically, it’s probably against semver rules to bump only the “patch” portion of the version number while introducing breaking changes, but since it wasn’t advertised at all, and it didn’t even have a proper readme, I guess it should be ok.

Check it out! PRs are welcome. One feature which is definitely missing is support for ESM modules – I might add it one day, but for now only CJS modules are supported.

CategoryEnglish, CategoryBlog, CategoryJavaScript