Search code examples
javascriptnode.jsrequire

Dynamically load function from files at runtime


I have some nodejs code that will act as an arbiter to a game played by bots.

Every bot will be it's own file, with a few predefined functions with fixed names (every bot will call its functions the same names and arguments, like PlayRound()).

Now I would like to, at runtime, add the bots in the game. Like I would tell the arbiter botName1, and it would look for a file called botName1.js inside the bots folder, then be able to call botName1.PlayRound() later on.

Since require only seems to work with literal static strings, and won't work with runtime values, is there even a way to do this?

sample code:

const readline = require('readline');
const readLine = readline.createInterface({ input: process.stdin });

var players = []
var playerFiles = [];

readLine.on('line', (ln) => {

    var ws = ln.split(' ');

    if (ws[0].toLowerCase() === 'add') {
        players[players.length] = ws[1];
        // I would like to add the file corresponding to the name here
    } else if (ws[0].toLowerCase() === 'list'){
        console.log('There are currently ' + players.length + ' players registered:');
        for (var p in players) {
            console.log(players[p]);
        }
    } else if (ws[0].toLowerCase() === 'start'){
        // I would like to do this here
        for (var playerFile in playerFiles) {
            playerFiles[playerFile].PlayRound();
        }
    }

});

Solution

  • As @Kaito suggested, its possible to use dynamic requires. But I would never prefer that, well definitely not if you are unaware of what might go wrong. Dynamic requires leaves your application open to runtime errors that you might not have handled like requiring a file that is not present (the most common one).

    I would like to build upon what @Kaito and @Yash have suggested/provided.

    Solution

    1. Accumulate all your bot files/functions in a map that maps botname to the botfilepath
    2. In your logic first check if you have a botfile/botfunction related to the bot that is joining
    3. If yes then you can safely require the botfile at runtime.

    In the example below I am assuming that all the bot files will be stored in bots directory.

    Example

    const fs        = require( "fs" ),
          path      = require( "path" ),
          botsDir   = path.join( __dirname, "bots" );
    
    /** One time read to fetch all the bot files in bots dir */
    const files         = fs.readdirSync( botsDir ),
          /** Here we create the map of bot and bot file path */
          availableBots = files
                            .reduce( ( botMap, file ) => Object.assign( botMap, { 
                                [ file.replace(".js", "") ] : path.join( botsDir, file ) 
                            } ), {} );
    
    // Your code
    const botThatWillBeJoiningAtRuntime = "BotC"; // This would be some calculation to determine bot that joins at runtime.
    
    /** Here we safely require the bot file only if we have one corresponding to the bot that is joining */
    if ( availableBots[botThatWillBeJoiningAtRuntime] ) {
        const botFunc = require( availableBots[botThatWillBeJoiningAtRuntime] );
    }
    

    Benefit of this approach -

    You make file op once throughout the application lifetime and accumulate the botfiles thereby reducing costly file ios and then the if part safely requires the botfiles depending upon whether the application have a bot file for the bot joining in.

    Downside is -

    You need to make sure that the bot that is joining has the same name as the botfile in the bots directory.