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');
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');
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);
}
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
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": {
...
}
}
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
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.
Top comments (0)