Writing Custom REPLs in Node.js

Updated May 27, 2021

If you’ve ever used the Laravel PHP framework, you’re probably familiar with the php artisan:tinker command. It spins up a PHP REPL (read-evaluate-print loop) that lets you play around with the data in your database using the same model code defined in your app. My team works almost exclusively in TypeScript on Node.js, but many of us have backgrounds as Laravel devs, and we missed the convenience of Tinker. When you’re first starting a project, it’s an easy way to seed test data, and it can be much more convenient than firing up a client to poke around your database. Our home-grown Node application didn’t have this luxury, and I was curious what it would take to implement it. Turns out, it's incredibly easy. You can write custom REPLs in Node in minutes.

The REPL API

I'm a fan of TypeScript, and I think the @types/node package makes this process even easier, but for the sake of simplicity, I'll stick to vanilla JavaScript in these examples. The file for this example will be called repl.js. The first step is to import the built-in repl Node package.

const repl = require('repl');

Create a REPL instance with the start() function. It accepts an object with some basic customization options, including a prompt string (the snippet of text that appears before each line of user input), color options, options for evaluation strictness, enabling expression previews, and more (docs).

const loop = repl.start({
    prompt: "~$ ",
    ignoreUndefined: true,
    breakEvalOnSigint: true, // allow exiting with ctrl-C during evaluation
});

Defining Globals

So far, we haven't accomplished anything that the REPL started by the node command wouldn't. The context property on our loop object gives us one way to make the REPL more interesting. If your app connects to a database, this would be a good place to add a connection pool or an ORM client, like Prisma:

const mysql = require("mysql2");
const { PrismaClient } = require("@prisma/client");

loop.context.db = mysql.createConnection({
  host: 'localhost',
  user: 'root',
  database: 'test'
});

loop.context.prisma = new PrismaClient();

This explicitly assigns the variables to the REPL's global scope. If you start the REPL with node repl.js, these variables are available immediately:

[nate@rosemary repl-example]$ node repl.js 
~$ db.query("SELECT count(id) FROM user");
~$ prisma.user.findMany({});

When connecting to a database, it's a good idea to add any needed teardown to the uncaughtException and SIGINT process event handlers to cleanup if the process crashes.

const teardown = () => {
	loop.context.prisma.$disconnect();
	loop.context.db.end();
};

process.on("uncaughtException", teardown);
process.on("SIGINT", teardown);

Custom Commands

You can add custom, dot-prefixed commands to your REPL with the REPLServer.defineCommand method. These functions are not part of the global scope in the way that the .context properties above are, as those can be cleared with the .clear command at runtime by a user. Additionally, commands defined this way have useful help text that appears as part of the output of .help. It's worth noting that these commands don't accept parameters like normal JS functions do, so if you need parameterized commands, you'll have to attach them to the REPL context like above.

loop.defineCommand('teardown', {
		help: 'Tear down the database pool and Prisma connection.',
		action: teardown,
});

These functions are accessible in the REPL like other global functions, but prefixed with a dot, and without the usual function-call parentheses.

[nate@rosemary repl-example]$ node repl.js 
~$ .teardown

Builtins

All instances of the REPL class support a few dot-prefixed keywords that might be familiar if you've ever run the base Node REPL with the node keyword in your shell. The most useful (and notable) of these are .save, .load, .editor, and .exit. The full list is available in the Node docs here.

async/await

By default, like regular JS files, REPLs do not allow the use of await outside of an async function. To enable await at the prompt, use the flag --experimental-repl-await when launching the REPL.

Other cool stuff to try

If you're interested in creating more advanced REPLs, I recommend checking out the full REPL API docs. Some points of interest are custom evaluator functions and customizing REPL output.


Copyright 2021 Nate Anderson