Why You Should Use Babel Resolvers

Or how to avoid ../../../ and similar circles of hell.

Izaak Schroeder
Bootstart
5 min readDec 4, 2018

--

Your new best friend.

Have you hated seeing ../../../ everywhere in your code? Me too. We’ll look at why and how to use custom resolvers, how to evaluate and choose a good set of resolution rules, and how to keep third-party tools (e.g. flow, eslint) working within such a setup.

The Problem

You’re developing a project with a sprawling directory structure and you have a ton of imports. Somewhere in there you’ve probably found yourself writing code like this:

This is messy, looking at ../../../ doesn’t really tell you where the import lives, other than it’s far away. Even worse, if you ever decide to move folders or you wish to copy paste an import into a different file adjusting the number of ../ entries becomes a huge pain.

Babel to the rescue

There is a babel plugin for doing custom module resolving available here: https://github.com/tleunen/babel-plugin-module-resolver. After installing it you can configure it:

And then you start saving yourself a boat load of extra typing:

However, this is only half the story. Choosing the correct aliasing strategy is also important, and the majority of configurations I’ve seen miss out on this crucial point.

A correct aliasing strategy

As soon as people realize they have to type less in order to import things, all bets are off and aliases are created for everything with totally arbitrary values. This is generally recognized as bad.

A lot of aliases I’ve seen end up looking like modules. In the node world anything that starts with [A-Za-z0-9@] is considered a module. foo, foo/bar, @10xjs/form, etc. are all things that node’s resolver looks for in node_modules. Do NOT break this learned behavior by choosing aliases for project files that look like modules.

First, select a “core” alias mapped to /. This alias points to the most essential part of your project, which in most small or single projects is usually just the ./src directory. The configuration in .babelrc for this looks like:

In cases where you have a mono-repo or multiple projects linked together, your core alias points to whatever shared base all those other projects are using. If that shared base is used very infrequently or there is no shared base, you can have one “core” alias per embedded project. Again, this all depends on where you’re importing the bulk of your code from. Figure that out and select that high-traffic path as your “core” alias. Your code will then end up having a lot of things like:

And it’s immediately obvious where Button lives, it saves typing and it does not conflict with the node module system. For the pedants, / usually represents the root of the filesystem, and in our case we essentially use it to represent the root our project — a nice semantic parallel. And while the behavior of things like:

Is defined in node, this is a behavior you would NEVER want to see in a real project, and so using / as a prefix here is arguably beneficial, since it also prevents a bad practice.

For 95% of projects this single core alias is enough.

All other aliases should be prefixed with a non-module character (%, #, &, etc.) and mapped to a particular category or purpose. Maybe # in your project represents views or if you’re in a multi-project repo maybe it represents a specific sub-project. For example, in a large shopping system:

Mapping a prefix to a category makes it easy for people to understand how the resolution process is taking place because it applies equally to the entire category; the behavior of the prefix token (# in the above example) is deterministic and predictable.

Linting resolver imports

If you have tooling that verifies you’re importing correct/existing module paths like eslint-plugin-import then you also have to make sure it is setup correctly to handle these aliases.

Then in your .eslintrc:

After that everything should “just work”, although debugging this can be incredibly time consuming and annoying since any error (missing module, babel version mismatch, etc.) will result in module not found linting error. If you want a setup that works out of the box you can have a look at this config: https://github.com/metalabdesign/eslint-config-metalab.

Keeping flow happy

If you use the flow type checker then you also need to configure it to understand how to resolve aliased modules. Thankfully flow provides a module.name_mapper configuration directive which you can use to port over your alias rules. Our example might look something like:

It can be a bit tricky to convert between alias expressions and flow’s regular expression syntax, but otherwise it’s a fairly straightforward process.

Aside: Alternative by using webpack

For completeness, you can probably also use webpack to achieve this; it has a resolve.alias configuration option which allows for similar behaviour and has a corresponding eslint resolver plugin available. Generally I shy away from a webpack solution when a babel solution is viable, since many more projects and tools use babel (either standalone or with webpack). The part where the webpack version shines is when you need to hijack entire modules in your build (e.g. mapping third party dependency react to inferno). Things like this are typically not done with babel since babel is rarely configured to run on things inside the node_modules folder.

Onward & upward

Custom module resolvers will save you time and the frustration of dealing with ../ splattered everywhere. They take a bit to setup and ensure full cooperation with existing tooling, but the result and visual satisfaction of never having to see ../../../../../.. is well worth the initial outlay on big projects.

If you have too much ../ 🍝 in your life give custom resolvers a shot on your next project. 🙏

--

--