Module Resolution or Import Alias: The Final Guide
Tags: typescript, javascript, tooling
Published: 2020-11-29 17:23:16

Hello, devs.

This is might be an old topic but I think it still can be a bit confused when you try to do this configuration:

> How can I add aliases to my imports?

The idea here is to do not stitch in any specific framework/tool but to give you an idea of knowing what and how to do, based on your application setup.

First, let's check what problem we're trying to solve, a solution in a more abstract way and how to configure your project to support that.


## Table of Content

- [Problem](#problem)
- [Solution](#solution)
- [Configuration](#configuration)
 - [Editor completion](#editor-completion)
 - [Babel](#babel)
 - [Rollup](#rollup)
 - [Webpack](#webpack)
 - [Jest](#jest)
 - [Typescript](#typescript)
 - [Other Tools](#other-tools)
- [Prefix Strategy](#prefix-strategy)
- [Conclusion](#conclusion)

## Problem

In well-structured JavaScript applications it's common we organize our codebase in a way that makes explicit what these files do or each domain they belong to.

Notwithstanding we don't have a standard of "how to structure", we always try to organize like `components`, `helpers`, `models`, etc. and sometimes, creating subfolders inside these folders and as consequence, creating a deeply nested structure.

Let's see an example of a tiny and not too nested app folder structure:

```markdown
.
├── package.json
├── src
│   ├── components
│   │   └── Button
│   │       ├── Button.js
│   │       └── index.js
│   ├── helpers
│   │   └── text.js
│   └── index.js
└── yarn-error.log
```

This is a very basic setup where:

1. We have our `src` (source) which holds our app code;
2. `src/components` which holds all our app components;
3. `src/helpers`, which holds all our helpers/utilities which can be used anywhere in our code

Now let's say we to import a function called `sanitizeText` from our helper text inside our Button component. We would do something like:

```jsx
import React from "react";
import { sanitizeText } from "../../helpers/text";

export const Button = ({ text }) => {
  return <button>{sanitizeText(text)}</button>;
};
```

It's not so bad, but as soon as you start having more and more imports from different folders and levels, it starts to become more confusing.

Also, you **always** need to guess how many levels you have to go up and down to import your code. Of course, modern code editors can help you out with that by just showing you which level you are and sometimes even completing it for you, but still.

Another problem is that if you eventually create a subfolder (for whatever reason you might have), you need to fix ALL imports by adding another "go up" level in the path.

That's not too much work but isn't by far optimal. We have a better way to do that and it's via `module resolution` or `import alias`

## Solution

Module resolution or import alias is a way we can emulate the same way we import `node_modules` but with our internal code.

We can say to the tool we're using:

> Hey, when you encounter "helpers/text", could you consider "./src/helpers/text" please?

In the same example above we would have some code like this:

```jsx
import React from "react";
import { sanitizeText } from "helpers/text";

export const Button = ({ text }) => {
  return <button>{sanitizeText(text)}</button>;
};
```

Much cooler, right?

You'll import from `helpers` and it does not matter how deep you are in your app structure. Even if you move around this file, the imports will be always correct.

At the end of the day it's like import a library in our project. Imagine if you have to every time go up and down inside `node_modules` until you find your dependency:

```jsx
import React from "react";
import { sanitizeText } from "helpers/text";
import { camelCase } from '../../../node_modules/lodash'

export const Button = ({ text }) => {
  return <button>{camelCase(sanitizeText(text))}</button>;
};
```

This would be hell. A lot of waste of energy having to navigate to the code.

## Configuration

JavaScript itself does not allow us doing that those imports natively. But luckily we are always using a tool that supports that (e.g. Babel, Webpack, etc).

You might think:

> Nice, I only need to do once this configuration!

And the answer is: it depends... but probably not.

The more tooling you introduce in your application, the more complicated it becomes to get it configured. Here some examples where this could be tricky:

- If you're working on a JS project only with **Babel** and add **jest** to test your app, you'll need to add aliases in **both** places.
- If you're working in a JS project with some built-in **Webpack** config and you add **Storybook**, you'll need to add an alias in both `webpack.config.js` and also customize Storybook `babel.config.js`.
- If you're working on a JS project with all these configurations and want to move to **TypeScript**, you'll need to keep this configuration everywhere plus configure the `tsconfig.json` file to let TS aware of how to resolve those aliases.

As you can see this can be puzzling but here I want to give you an understanding of each possible tool. After that, you'll check how your project works and do all configurations needed to enable this feature into your project.

### Editor completion

Before we dive deep into the configs, let's talk about Developer Experience (DX).

If you use VSCode, you probably already notice that when you need to import something, because VSCode uses TS, and it does a lot of inference, usually it's possible either to automatically imports the method you're trying to use or have an autocompletion for the imports, right?

When you just configure alias in Babel, for example, you kinda lost that and that's sucks.

If we want to let VSCode aware of how to suggest these modules, we need to create a file called `jsconfig.json` (in root level), which it's a JavaScript version of `tsconfig.json` and also declare those alias there:

```json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "*": [
        "./src"
      ],
      // Your custom aliases here
      "helpers/*": [
        "helpers"
      ],
      "models/*": [
        "models"
      ]
    }
  }
}
```

Every time you add a new alias to your project, all you need to do is come to this file and add your new alias.

If you use Typescript you don't need this file because this configuration will be made there.

Now, let's dive deep into the specifics.

------

### Babel

#### Description

If your project has a `babel.config.js` or `.babelrc` file at the root level, you'll probably need to do this configuration.

#### Configuration

First, you need to install the plugin `babel-plugin-module-resolver`:

```bash
yarn install -D babel-plugin-module-resolver
```

Then, add this plugin into your babel config file:

```json
{
  "presets": [
    // ....
  ],
  "plugins": [
    // ....
    [
      "babel-plugin-module-resolver",
      {
        "root": [
          "./src"
        ],
        "alias": {
		  // Add your aliases here
          "helpers": "./src/helpers",
          "models": "./src/models"
        }
      }
    ]
  ]
}
```

After this, you'll be able to import files from `helpers/<file>` and `models/<file>`.

#### Resources

- Babel Only Demo: https://github.com/raulfdm/aliases-post-demo/tree/main/babel-only

- babel-plugin-module-resolver: https://www.npmjs.com/package/babel-plugin-module-resolver

------

### Rollup

#### Description

Rollup is one of the most annoying tools to do this configuration. That's because the official plugin to do that isn't smart enough to recognize `index.js` imports.

If you try to do something like `models/User/` the plugin will complain saying that "models/User" is a folder, not a file. In that sense, you'll need to give full import, like `models/User/index`.

After some tests, I've realized that isn't necessarily the plugin's fault. If we try to enable this feature via `@rollup/plugin-babel`, we still need to import `index` at the end of our import. It seems that that's the way Rollup handles import and we cannot do anything about it.

#### Configuration

The first step is installing `@rollup/plugin-alias`.

After that, in your `rollup.config.js` you import it and set it up:

```javascript
import alias from "@rollup/plugin-alias";
import path from "path";

export default {
  input: "src/index.js",
  output: {
    format: "cjs",
    dir: "dist",
  },
  plugins: [
    alias({
      /**
       * For custom files extension you might want to add "customerResolver"
       * https://github.com/rollup/plugins/tree/master/packages/alias#custom-resolvers
       *
       * By doing that this plugin can read different kind of files.
       */
      entries: [
        {
          find: "models",
          replacement: path.resolve(__dirname, "src/models"),
        },
        {
          find: "helpers",
          replacement: path.resolve(__dirname, "src/helpers"),
        },
      ],
    }),
  ],
};

```

> Remember: imports from `index.js` files DOES NEED to be full imports

#### Resources

- Rollup Demo: https://github.com/raulfdm/aliases-post-demo/tree/main/rollup
- Rollup + Babel Demo: https://github.com/raulfdm/aliases-post-demo/tree/main/rollup-babel
- @rollup/plugin-alias: https://github.com/rollup/plugins/tree/master/packages/alias#readme

------

### Webpack

#### Description

Webpack allows us to do the same as Babel but via its configuration. In case you're working on a project which already had a `webpack.config.js` somewhere in the code, it's there you'll need to tweak.

#### Configuration

```js
const path = require('path');

module.exports = {
  // ...
  resolve: {
    alias: {
      // Your custom aliases here
      // __dirname will stands for your root directory level
      // path.resolve will concatenate your project folder location with the aliased folder
      helpers: path.resolve(__dirname, 'src/helpers/'),
      models: path.resolve(__dirname, 'src/models/')
    }
  }
};
```

If you're working with a framework/platform which works with Webpack under the hood and allows you to extend its configuration, the solution will be slightly different but the same concept.

For example, I'll show you a `next.config.js` file from NextJS which allow us to extend their configuration:

```js
const path = require('path');

module.exports = {
  webpack: (config) => {
    // Mutate the alias configuration
    config.resolve.alias = {
      // Spread everything to avoid remove any alias they might have
      ...config.resolve.alias,
      // Your custom aliases here
      helpers: path.resolve(__dirname, 'src/helpers/'),
      models: path.resolve(__dirname, 'src/models/')
    }

    // Important: return the modified config
    return config
  },
}
```

#### Resources

- Demo Webpack only: https://github.com/raulfdm/aliases-post-demo/tree/main/webpack
- Extending Webpack NextJS: https://github.com/raulfdm/aliases-post-demo/tree/main/next-webpack

- Webpack Docs: https://webpack.js.org/configuration/resolve/#resolvealias

------

### Jest

#### Description

Now that we already have our aliases working in our regular environment, let's see how can we make Jest aware of how to resolve our aliases

Unfortunately, their API for that is by far one of the most complicated ones. That's because they use for stub out mocks and their test stuff and not for our organized code. But luckily we can grasp it.

To do that, first, you need to understand where your jest config is. It can be inside of `package.json` or in a `jest.config.js` file in the root level of your directory.

#### Configuration



```js
module.exports = {
  // ...
  moduleNameMapper: {
    // Your custom aliases here
    "^helpers/(.*)": "<rootDir>/src/helpers/$1",
    "^modules/(.*)": "<rootDir>/src/modules/$1",
  },
};
```

#### Resources

- Jest Docs: https://jestjs.io/docs/en/configuration.html#modulenamemapper-objectstring-string--arraystring
- Jest Demo: https://github.com/raulfdm/aliases-post-demo/tree/main/jest

------

### TypeScript

#### Description

For TS projects, we usually have to attack in two ways:

1. Configure the transpiler/compiler/bundler tool (e.g. Webpack, Babel)
2. Configure `tsconfig.json`

That's because those tools use a lot of internal tools AND Typescript to generate your final files and for Typescript, what matters is your `tsconfig.json` file.

Also, you might want to add `tsc --noEmit` as a fancy linter to be sure that at least your code has no errors and will compile right.

#### Configuration

Open your `tsconfig.json` file and set a `baseUrl` and `paths`:

```json
{
  "compilerOptions": {
    // ...

    /* Base URL is a MUST. */
    "baseUrl": ".",
    /* Your aliases will live here */
    "paths": {
      "models/*": [
        "src/models/*"
      ],
      "helpers/*": [
        "src/helpers/*"
      ],
    }
  }
}
```

> Tip: If you're using VSCode, usually you'll probably need to restart it to be able to navigate these aliased imports.

Just to be clear, `baseUrl: "."` will tell Typescript to consider the **root level** of your project to build the paths. In that sense, TS will find "models"  and consider something like `<root>/src/models/*`.

It's also common for people defining `baseUrl` to be `src`. If you do so, you can remove `src` from your paths:

```json
{
  "compilerOptions": {
    // ...

    /* Base URL is a MUST. */
    "baseUrl": "src",
    /* Your aliases will live here */
    "paths": {
      "models/*": [
        "models/*"
      ],
      "helpers/*": [
        "helpers/*"
      ],
    }
  }
}
```

#### Resources

- Typescript demo: https://github.com/raulfdm/aliases-post-demo/tree/main/typescript
- Typescript Docs: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#base-url

------

### Other tools

I tried to focus on the most common tools we use independently a specific framework but each one can have some specificities on how to extend or do this in an easy way.

My suggestion for you to figure this out is: always search at Google for `<framework-or-tool-name> import alias`. Usually, you'll find the answer in one of the top 3 results.

## Prefix Strategy

In all examples, I just use regular names for our aliases but it's also a common practice is adding a prefix (a character before) to them.

Personally, I'm very fan of the prefix `@` just because it's really nice to read `import something "at" components`  but some people don't like this strategy because this special character is very popular for `orgs`, like `@babel`, `@rollup`, and it can mislead developers to think that this import is from an external resource.

As you notice, this is optional. Feel free to use or not whatever special char to give more clarity about internal X external imports.

## Conclusion

I hope at the end of this article you feel comfortable to tweak your configs and enable this feature if you feel useful somehow.