How Node.js modules work
When you require
a module, Node will search some paths in a certain order. Which paths? See module.paths
in your Node REPL.
> pwd
/Users/denhox/Projects/Sandbox/test-node
> node
Welcome to Node.js v18.17.0.
Type ".help" for more information.
> module.paths
[
'/Users/denhox/Projects/Sandbox/test-node/repl/node_modules',
'/Users/denhox/Projects/Sandbox/test-node/node_modules',
'/Users/denhox/Projects/Sandbox/node_modules',
'/Users/denhox/Projects/node_modules',
'/Users/denhox/node_modules',
'/Users/node_modules',
'/node_modules',
'/Users/denhox/.node_modules',
'/Users/denhox/.node_libraries'
]
repl
) is particularly interesting. I assume it’s there to conveniently override a module when using the Node REPL.But how do modules (packages) in node_modules work? We see node_modules contains folders but how does Node’s require
work with them?
To be loaded by Node’s require
, a module must be:
- A JS file, or
- A folder with an index.js file, or
- A folder with a package.json file that has a
main
field
Related: https://docs.npmjs.com/about-packages-and-modules#about-modules
Here’s a sample project structure that demonstrates these three.
my-app/
āā index.js
āā package.json
āā node_modules/
ā āā a.js
ā āā b/
ā ā āā index.js
ā āā c/
ā ā āā package.json
ā ā āā foobar.js
Requiring a JS file
When you require
a module by its name only (no relative or absolute directory), it’ll work just fine if it’s a single JS file.
// node_modules/a.js
module.exports = {
sayHello: () => console.log("Hello from A")
}
// index.js
const { sayHello } = require('a')
sayHello();
// Output:
// Hello from A
Requiring a folder with an index.js file
Same thing works even if the module is a folder with an index.js file. You don’t need a package.json in this case, because Node already defaults to index.js
// node_modules/b/index.js
module.exports = {
sayHello: () => console.log("Hello from B")
}
// index.js
const { sayHello } = require('b')
sayHello();
// Output:
// Hello from B
Requiring a folder with a package.json file
This is the most common way that modules are published and imported in Node. Packages in node_modules are in the form of a directory with a package.json file that defines the file name of the package’s entry point:
{
"name": "c",
"version": "1.0.0",
"main": "foobar.js"
}
So when you import package ācā via require("c")
, Node will find ./node_modules/c, reads the main
field, and resolves foobar.js
.
// node_modules/c/foobar.js
module.exports = {
sayHello: () => console.log("Hello from C")
}
// index.js
const { sayHello } = require('c')
sayHello();
// Output:
// Hello from C
What happens if package ācā has no package.json?
// node_modules/c/foobar.js
module.exports = {
sayHello: () => console.log("Hello from C")
}
// index.js
const { sayHello } = require('c')
// Output:
/*
node:internal/modules/cjs/loader:1080
throw err;
^
Error: Cannot find module 'c'
Require stack:
- /Users/denhox/Projects/Sandbox/test-node/index.js
at Module._resolveFilename (node:internal/modules/cjs/loader:1077:15)
at Module._load (node:internal/modules/cjs/loader:922:27)
at Module.require (node:internal/modules/cjs/loader:1143:19)
at require (node:internal/modules/cjs/helpers:110:18)
at Object.<anonymous> (/Users/denhox/Projects/Sandbox/test-node/index.js:3:17)
at Module._compile (node:internal/modules/cjs/loader:1256:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
at Module.load (node:internal/modules/cjs/loader:1119:32)
at Module._load (node:internal/modules/cjs/loader:960:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12) {
code: 'MODULE_NOT_FOUND',
requireStack: [ '/Users/denhox/Projects/Sandbox/test-node/index.js' ]
}
*/
OK cool. Lets try something else: if there’s also an index.js
file, will Node ignore it and go straight for the main
file?
// node_modules/c/foobar.js
module.exports = {
sayHello: () => console.log("Hello from C")
}
// node_modules/c/index.js
module.exports = {
sayHello: () => console.log("Hello from C, but the index.js file")
}
// index.js
const { sayHello } = require('c')
// Output:
// Hello from C
Good, it ignores it. That makes sense.
What happens if we remove foobar.js
, rendering the main
field invalid?
// node_modules/c/foobar.js
module.exports = {
sayHello: () => console.log("Hello from C")
}
// node_modules/c/index.js
module.exports = {
sayHello: () => console.log("Hello from C, but the index.js file")
}
// index.js
const { sayHello } = require('c')
// Output:
// Hello from C, but the index.js file
// (node:38885) [DEP0128] DeprecationWarning: Invalid 'main' field in '/Users/denhox/Projects/Sandbox/test-node/node_modules/c/package.json' of 'bla.js'. Please either fix that or report it to the module author
Node definitely lets us know, and it falls back to the index.js file!