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.