NodeJS: Interactive shell with hot reload

published on in category JavaScript , Tags: nodejs javascript repl

I’m currently working on a project in NodeJS. It’s an REST API server for a web application. Since it supports multi-tenancy but all tenants are created manually, I needed a quick and easy way to be able to call the createTenant method whenever I need it. I could have written a command-line tool that would do it for me, but actually I needed something like rails console for Ruby on Rails apps: A shell where I can just execute code in the context of my app.

I also need this for development from time to time, so I decided to include some code that will hot-reload modules as I change them.

The result is simple: Create a REPL (Read Eval Print Loop), attach stdin and stdout and assign some modules to the context of this REPL instance. All exposed context variables will be available inside the shell. Then I run my appInitializer that will take care of all the route and database setup.

Additionally, I’ve pulled in gaze, which allows to watch for changes in the filesystem recursively based on a glob expression. Whenever the watcher is triggered, it will call reloadModules(), which will invalidate Node’s require cache, load all the modules again and push it to the context object.

Here’s the code for my shell.js:

#!/usr/bin/env node
const repl = require('repl');
const gaze = require('gaze');
const appInitializer = require('./initializers/app');

function invalidateCache(file) {
    delete require.cache[require.resolve(file)];
}

function reloadModule(ctx, key, file) {
    invalidateCache(file);
    ctx[key] = require(file);
}

function reloadModules(ctx, moduleList) {
    Object.keys(moduleList).forEach(module => {
        reloadModule(ctx, module, modules[module]);
    });
}

function watchForChanges() {
    gaze('**/*.js', { mode: 'poll' }, (err, watcher) => {
        watcher.on('all', (event, filepath) => {
            console.log('Reloading due to change in', filepath);
    
            reloadModules(ctx, modules);
        });
    });
}

const modules = {
    backup: './app/backup/backup.service',
    tenant: './app/tenant/tenant.service'
};

appInitializer.run()
    .then((app) => {
        const ctx = repl.start({
            input: process.stdin,
            output: process.stdout
        })
            .context;
        
        // Cannot be reloaded as it's set up during initialization
        ctx.app = app;  

        watchForChanges();
        reloadModules(ctx, modules);
    });

Now, if I want to create a new tenant, I can run my shell.js and execute tenant.createTenant('foo').then(console.log); and it will do it’s job.