Unified JavaScript module resolution
How to coordinate module lookup between Webpack, Jest, and TypeScript
In JavaScript we have two types of module references: relative and absolute (‘non-relative’ in some material online). For absolute module references, modules are normally looked for in the node_modules
directory.
This is only the default, though, and Webpack has always been offering a way to change this. Here we will take a look at what the alternative setup may be, and how to synchronize different parts of our build to work consistently.
Problem: complicated relative import paths
Say you have a directory structure like this:
src/
views/
Menu/
Menu.ts
Menu.css
List/
List.ts
List.css
util/
assign-path.ts
service/
xhr.ts
Now, say you are in Menu.ts
and you want to import List.ts
, assign-path.ts
, and xhr.ts
. It would normally look something like this:
import List from "../List/List";
import assignPath from "../../util/assign-path";
import XHR from "../service/xhr";
I call this double-dot hell. I made a typo, too, and the xhr
module could not be imported. I needed to go one more level up.
This is annoying. Even if your build system and editor are able to trap these import issues on time, fixing them is still not so convenient.
Solution: module resolution configuration
What if we could do the imports like this:
import List from "views/List/List";
import assignPath from "util/assign-path";
import XHR from "service/xhr";
What this gives us is we no longer need to remember how many levels we need to go up before we can go back down towards directories like views
, util
, or service
, and thus absolves us from the double-dot hell. Essentially, we’ve converted a bunch of local module references into absolute module references — as if they were installed via NPM.
To get this, we need to configure all the moving parts that deal with modules. This may include Webpack, TypeScript compiler, or Jest, for example. If we configure only one of them, the other parts of the build system will not work correctly, so it’s important that they are all on the same page.
I will show you how to handle these three components in the text that follows.
Webpack configuration
Webpack supports resolve.modules configuration option. It is an array of paths that will used as the base directory for module resolution, when using absolute module references.
In our case, it would look something like this:
// webpack.config.js
module.exports = {
// ....
resolve: {
modules: [path.join(__dirname, "src"), "node_modules"],
}
};
The configuration in the example will instruct Webpack to find absolute module references under the src
directory, and fall back on node_modules
if not found in src
.
Note that doing this means that absolute module references will shadow the modules in node_modules
— those that are installed via NPM.
TypeScript configuration
TypeScript is a bit nicer as it will always look at node_modules
regardless of the configuration. All we need to do is add the baseUrl
option, which is documented in the documentation section for Compiler Options as well as in the section about the Module Resolution.
In our case, baseUrl
configuration is quite simple. The tsconfig.json
should look like this:
{
"compilerOptions": {
...
"baseUrl": "src"
},
...
}
As with the Webpack configuration, modules in node_modules
are shadowed by modules in the src
.
Jest configuration
Jest is configured similarly to Webpack. We provide a list of look-up paths using the modulePaths
configuration options documented here.
In our package.json
, we will add something like this:
{
...
"jest": {
...
"modulePaths": [
"<rootDir>/src/",
"<rootDir>/node_modules/"
]
}
}
Conclusion
To recap what we’ve done:
- Convert relative module references within our source tree into absolute references
- Make sure all programs in our build system resolve module references the same way
- Keep in mind that absolute references of the modules in our code will shadow absolute references of the modules in
node_modules
I hope this was useful to you. If so, don’t forget to recommend. :)