Making TypeScript monorepos play nice with other tools

Andrei Picus
6 min readJul 20, 2020

--

In my previous blog post, How to set up a TypeScript monorepo with Lerna, I covered setting up a simple TypeScript monorepo and focused on making code navigation in an IDE and publishing packages work without any surprises. The solution revolved around path aliases that let us map package names to their location inside the monorepo.

In this follow up post I’ll cover some of the most common tools and frameworks that need some extra config to work with monorepos that use path aliases:

Fully working examples for each of the above can be found in the following GitHub repo:

Let’s dig in.

ts-node

ts-node is a drop-in replacement for node which compiles your TypeScript files on-the-fly as you import them. I recommend using it over node in your TypeScript projects, especially since making monorepos work with it is easier than making them work with node. The switch is as simple as replacing node dist/index.js with ts-node src/index.ts.

If you try running ts-node on a monorepo that uses path aliases then you will most likely encounter the following error:

> ts-node src/index.tsError: Cannot find module '@nighttrax/foo'

This is because the TypeScript compiler doesn’t actually rewrite imports according to the paths defined in tsconfig.json. The aliases are only used by the type checker and the import statements are left untouched in the output.

Compiled output from running tsc. Notice how the import statement was left untouched.

Fret not, as there’s a very simple way to make the aliases work at runtime — the tsconfig-paths package. It provides a hook for Node’s require function that intercepts your imports, checks them against the aliases you defined in tsconfig.json, and rewrites them accordingly at runtime.

Use tsconfig-paths when running ts-node.

You can see the full example in the repo here.

Babel

If you use Babel with @babel/preset-typescript then you can simply add babel-plugin-module-resolver to the plugins list and specify the path aliases in your babel.config.js:

babel-plugin-module-resolver makes supporting aliases in Babel straightforward.

You can specify each path alias individually or, if you use a scope for your packages, you can use a RegExp to match them all.

You can see the full example in the repo here.

Webpack

If you use babel-loader then you can copy the solution above from the Babel section using babel-plugin-module-resolver.

Alternatively, you can use webpack’s built-in support for aliases and define the path aliases in a webpack.config.js using the resolve.alias section:

To avoid duplication we can use tsconfig-paths-webpack-plugin to automatically apply all the aliases from tsconfig.json:

Use tsconfig-paths-webpack-plugin to convert the path aliases to webpack’s resolve aliases.

You can see the full example in the repo here.

Jest

If you use Jest with Babel then you can apply the same solution from the Babel section above using babel-plugin-module-resolver.

If you use ts-jest instead (and you should because it enables type checking) then you need to define the aliases using moduleNameMapper in your jest.config.js:

You can use a RegExp to match all of your aliases, or you can list them individually. To avoid duplicating the paths from tsconfig.json you can use the pathsToModuleNameMapper helper to automatically apply all of them:

Use ts-jest to convert the path aliases to jest’s module name map.

You can see the full example in the repo here.

create-react-app

create-react-app does not natively support path aliases so we’re going to have to do a bit of work for this one.

You can either eject the CRA config and apply the following changes to the ejected config/webpack.config.js, or you can use the react-app-rewired package to extend CRA’s config without ejecting.

Here’s what we’re going to do:

  1. We’ll use the same tsconfig-paths-webpack-plugin package to get webpack to resolve our path aliases.
  2. We’re going to change CRA’s webpack config to allow compiling files outside of the src/ folder.

I’ll be using react-app-rewired to apply the changes and you can follow its simple instructions to get things started. In the root config-overrides.js I’ll add the tsconfig-paths-webpack-plugin to resolve the path aliases:

Add the tsconfig-paths plugin to resolve the path aliases.

If you try to run npm start again, you’ll be greeted with the following error:

You attempted to import packages/foo/src which falls outside of the project src/ directory. Relative imports outside of src/ are not supported.
You can either move it inside src/, or add a symlink to it from project's node_modules/.

This is where the second part starts: CRA is set up to only compile the src/ folder and uses a plugin called ModuleScopePlugin to throw an error if you try to import anything outside of that folder. The path aliases will resolve to files inside the monorepo that are outside of the app so we need to remove the compilation restriction and this plugin. We can do the latter easily by just popping from config.resolve.plugins:

Remove the ModuleScopePlugin to enable the next step.

If we rerun the start script now we’ll see a different error:

../components/src/button.tsx 3:28
Module parse failed: Unexpected token (3:28)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| import React from "react";
|
> export const Button = () => <button onClick={() => alert('you clicked me')}>Click me</button>;

This confirms that the path aliases are resolved correctly and that CRA is not preventing the imports anymore, but webpack is still set up to not compile files outside src/. CRA sets the include directive of the Babel loader to only the app’s src/ folder, causing it to only compile the files in there and nothing else.

We can either adjust the include directive to include the other monorepo packages as well, or remove it completely. If we go with the latter we have to add an exclude directive to tell webpack to avoid compiling node_modules, otherwise performance will suffer greatly.

The final rewired config.

I went with the include/exclude combo as I think it’s more maintainable and, unless you’re doing something weird, shouldn’t cause any issues.

Now everything should be working. You can see the full example in the repo here.

NextJS

NextJS supports path aliases out of the box, though you need a little bit of extra config to tell NextJS to compile your dependencies, similar to the setup for webpack and create-react-app :

You can see the full example in the repo here.

Note: If you’re using at least version 10.1.0 you can toggle an experimental flag in your config to avoid manually tweaking webpack:

NestJS

There a couple of options here, and both involve keeping your path aliases in both the tsconfig.json file and the tsconfig.build.json one (you can use extends to avoid duplicating them).

The first solution is to use webpack (which will automatically use the tsconfig-paths plugin) by updating the nest-cli.json file:

If you don’t want to use webpack, or you’re running into problems with it finding NestJS core packages (see issue #61 in the GitHub project), then you can keep using tsc and make a small adjustment to the nest-cli.json:

Above you can see the entryFile option which points NestJS to the location of the compiled main.js file. Since we’re using tsc with path aliases, the build output will contain the parent folder structure for all of our dependencies, including the NestJS project.

examples/nestjs/dist
├── examples
│ └── nestjs
│ └── src
│ └── main.js
├── packages
│ └── foo
│ └── src
│ └── index.js

This is because tsc will match the output to the path aliases inside the monorepo. Knowing this, we can just tell NestJS to load the main.js file from the right path.

You can see the full example in the repo here.

Thanks for reading and I hope you found this article helpful. You can find full examples of all of the above in the following repo:

If you enjoyed this article then maybe you’ll enjoy reading my other articles or checking out some of my other open source projects:

--

--