Migrating Next.js (+Jest/Storybook/Cypress) to use module path aliases, instead of relative paths
Tags: nextjs, react, webpack, cypress
Published: 2021-01-18 12:20:27

Most projects use the well-known relative paths to resolve files and modules. For instance: `import '../config.ts';`

Sometimes, especially on big project, it becomes this:
`import { NRN_DEFAULT_THEME } from '../../common/constants';`

But, did you know you can convert to something like this:
`import { NRN_DEFAULT_THEME } from '@/common/constants';`

This is a ["module path alias"](https://nextjs.org/docs/advanced-features/module-path-aliases), and Next.js has support for those [since 9.4](https://nextjs.org/blog/next-9-4#absolute-imports-and-aliases) (May, 2020).

The main advantages to module path aliases are:
- Avoids "../" spaghetti code
- Avoid breaking your imports when moving them around (sure, IDE help but when you move a lot of files around, they get confused too)
- Avoid changing the file content when moving the file around, which in turn leads to avoiding Git conflicts! (this is, by far, the main advantage, IMHO)
- You can copy/paste your code, and it'll work as-it, even if you pasted it into a file on another folder level (and that's great)
- You can still use relative imports if you want, it's not a lock-in!
- Do you see more?

So, why not using them? Why don't we see that more often?
- It requires configuration in several tools (babel, webpack)
- It affects all tools in your project, because all file loaders must be compatible, and configured properly (this is, by far, the main reason why it's so complicated to get right)

If you use a very simple Next.js project, you can simply change your `tsconfig.json` to something like this:

```ts
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/common/*": [
        "src/common/*"
      ],
      "@/components/*": [
        "src/common/components/*"
      ],
      "@/utils/*": [
        "src/common/utils/*"
      ],
      "@/modules/*": [
        "src/modules/*"
      ]
    },
```

> The above mapping is what [we're thinking of using in Next Right Now](https://github.com/UnlyEd/next-right-now/discussions/183), but you probably want something specific to your app and way of doing things!

Using a mapping as described above will be enough for Next.js to use `import { NRN_DEFAULT_THEME } from '@/common/constants';` properly!

## How to convert all relative paths to module paths?

The `relative-to-alias` NPM module helped us a ton. 

It was hard to configure properly because it wasn't doing anything at first (due to a misconfiguration on our side), but we eventually managed to make it work.

`npx relative-to-alias --src 'src' --alias '@/common' --alias-path './src/common' --extensions 'ts,tsx'  --language 'typescript'`

Running this replaced all relative paths to `@/common/...`. We eventually [wrote a script for this](https://github.com/UnlyEd/next-right-now/blob/5cd7a3cc1470a0dac5ffb0f34530a5d7b3ceb69a/package.json#L84-L87).

This is a quick and easy way to migrate an existing project. 

> Make sure to commit all changes before running the  `codemod:module-path-aliases`! It'll change lots of files and should be done in a dedicated commit/PR.

## Is that it?

Depending on how big your project is, it might... But:
- If you're using Jest (you probably are)
- If you're using Cypress
- If you're using Storybook

Then, you'll need to configure them, so they understand what `@/` means!

And, unfortunately, that's the most complicated part to figure out by yourself... Because they all use a different way of doing so. They don't all rely on `tsconfig.json`, and you don't necessarily only use TypeScript, but JavaScript as well!

Here is an overview of where you'll need to make changes:
- [`tsconfig.json`](https://github.com/UnlyEd/next-right-now/pull/265/files#diff-b55cdbef4907b7045f32cc5360d48d262cca5f94062e353089f189f4460039e0)
- [`jest.config.js`](https://github.com/UnlyEd/next-right-now/pull/265/files#diff-1e058ca1442e46581b13571fb8d261f9e1f5657e26c96634d4c1072f0f0347f1)
- [`cypress/tsconfig.json`](https://github.com/UnlyEd/next-right-now/pull/265/files#diff-7c73a5b98f30e40574b7149a83a19e335c5ee48b8c9c58e4e6cbfe6a07777037)
- [`.storybook/jsconfig.json`](https://github.com/UnlyEd/next-right-now/pull/265/files#diff-88916def1b0ce362d14fad4ff9e7ef8a93dc5eb184c798aac06a4564a9ac4ec9)
- [`cypress/jsconfig.json`](https://github.com/UnlyEd/next-right-now/pull/265/files#diff-962be72e9a91e7a94e8a2eb9ecb637f7e4a399fbf80b2a55c623ee477c632715)
- [`.storybook/main.js:webpackFinal`](https://github.com/UnlyEd/next-right-now/pull/265/files#diff-cafe2123a72c4ce3a9f7e9ee4b0e188256eb02d7ec1e54fa2cadfac99b22f92b)
- [`jest.config.js:moduleNameMapper`](https://github.com/UnlyEd/next-right-now/pull/265/files#diff-1e058ca1442e46581b13571fb8d261f9e1f5657e26c96634d4c1072f0f0347f1)

The `jsconfig.json` are necessary for WebStorm to resolve the aliases used within `.js` files. (Otherwise, it compiles but WebStorm displays "Module not found")

Jest has a particular way to understand aliases. Storybook doesn't have tsconfig.json and it must be configured via their Webpack override.

---

In conclusion, it can be quite complicated (with a lot of duplicated configuration) to make Module aliases work on a whole project. It takes some time to understand how each 3rd party needs to be configured. 

> At this time, I don't have any feedback experience regarding actual usage. This is still [under peer-review](https://github.com/UnlyEd/next-right-now/pull/265), and it might take a while until we get familiar with those new paths.

Also, it is **concerning how complicated the configuration is**. It's being **duplicated** in no less than **7 different files**! And one might be concerned about what'd happen if it ever breaks.

But, in such event, I guess we can solve it with another codemod that converts all module aliases back to relative paths!