DEV Community

Heiker
Heiker

Posted on • Edited on

Node.js: using non-local dependencies inside native ES modules

If you were using only CommonJS syntax you would "solve" this by using the NODE_PATH environment variable, but that won't work with native ES modules. The good news is that we can still achieve our goal. The somewhat bad news is that you won't be using the beloved import keyword. What I will show you now is how you can make your own require function that works with native ES modules.

Before I begin let me just say I do not encourage using non-local dependencies in "production apps", or libraries or really anything that is meant to be deployed on a remote server. The use case I have for this is focused more on creating scripts for personal use.

With that out the way, lets go back to what's important.

The module "module"

Did you know there is a module called module? Yeah, and it has everything we need.

Node has a very convenient function called createRequire, it takes a path as an argument and it gives you back a function that acts in the same way require does in CommonJS.

The docs will show you how you can make a "polyfill" of require like this.

import { createRequire } from 'module';
const require = createRequire(import.meta.url);

// sibling-module.js is a CommonJS module.
const siblingModule = require('./sibling-module');
Enter fullscreen mode Exit fullscreen mode

With this new knowledge nothing can stop us from doing something like this.

import { createRequire } from 'module';
const use = createRequire(GLOBAL_MODULES_PATH);

const fetch = use('node-fetch');
Enter fullscreen mode Exit fullscreen mode

Why not call it require? Well, because in this case use doesn't really act like the standard require. All the algorithms and shenanigans it does will be relative to GLOBAL_MODULES_PATH. So doing use('./sibling-module'); will not get you a sibling-module relative to your script, it'll search in the path you gave to createRequire.

Okay, that's it, really. Is how you would get global modules in a relatively safe way inside ES modules.

Usage

How do I use this little piece of knowledge? Funny story... somewhere in my filesystem I have a awesome-js folder with my favorite js libraries installed and ready to go. I also have a script called cli.mjs.

#! /usr/bin/env node

import { resolve } from 'path';
import { createRequire } from 'module';

// Setup function to require "global" modules
global['use'] = createRequire(import.meta.url);

// Get absolute path
const script = resolve(process.argv[2]);

if(script) {
  // Run the script
  await import(script);
} else {
  console.error('Must provide a valid path to a script');
  process.exit(1);
}
Enter fullscreen mode Exit fullscreen mode

The idea is to allow cli.mjs to execute others script that can be anywhere in the filesystem and allow then to use the node_modules of awesome-js. So in the command line I want something like this.

jsm /path/to/script.mjs --some argument
Enter fullscreen mode Exit fullscreen mode

How did I do that? In the package.json of that folder I added a bin property.

  {
    "name": "awesome-js",
    "version": "1.0.0",
+   "bin": {
+     "jsm": "./cli.mjs"
+   },
    "main": "",
    "license": "MIT",
    "dependencies": {
      ...
    }
  } 
Enter fullscreen mode Exit fullscreen mode

And ran the command npm link. On linux this will create a jsm symlink which leads to cli.mjs, and put that in your PATH. On windows it should do the equivalent of that. If I'm not mistaken the executable will be in the prefix you have configured for npm. If you want to know what is that location use:

npm get prefix
Enter fullscreen mode Exit fullscreen mode

I actually do some other hacky stuff inside awesome-js but let's not get into that. If you are curious you can find the code here.


Thank you for your time. If you find this article useful and want to support my efforts, consider leaving a tip in ko-fi.com/vonheikemen.

buy me a coffee

Top comments (0)