How to Setup Monorepos with Git for JavaScript and TypeScript
Tags: monorepo, javascript, typescript, programming
Published: 2021-08-15 09:28:49

When your app gets bigger, managing files within a project gets more complex. You may start to have modules shared between front-end and back-end projects. Often, you also need to manage different versions of those modules.

A monorepo is a way to structure your projects to manage that kind of complexity all in one place.

I failed to set up a monorepo with [Lerna](<{https://lerna.js.org/}>) a few times. [Yalc](<{https://github.com/wclr/yalc}>) and [Yarn Workspace](<{https://classic.yarnpkg.com/en/docs/workspaces/}>) can be troublesome when I need to move a project out of the monorepo.

Finally, I found a way to make it work using [git submodules](<{https://git-scm.com/book/en/v2/Git-Tools-Submodules}>). Git is great for resolving code conflicts. Git branches can be used for versioning. You can have unlimited private repositories for free when using Github or Gitlab. Besides, with TypeScript or JavaScript (using webpack), you can configure module aliases to create beautiful import paths.

In this post, I’ll show you how to set up a dependency, structure your project, and configure module aliases for a monorepo. And discuss the disadvantage I’ve encountered of using this setup.

*See [git-monorepo-project](<{https://github.com/hieunc229/git-monorepo-project}>)* on Github for the final result*

## 1. Setup a dependency

![A dependency is a submodule](<https://miami.inverr.com/PiIl=LQwO/WtwH0zl2m.png>)A dependency is a git repository. It can either contain a complete module (i.e with package.json and bundled/transpired JavaScripts files), or it may only have plain JavaScript or Typescript files.

Besides, we often need different versions of the dependency, known as versioning. Which allows us to make changes in a specific version, without affecting projects that use other versions.

### Creating a dependency repository

You can create a [public or private repository](<{https://docs.github.com/en/get-started/quickstart/create-a-repo}>) (make sure contributors have access), and push the code there.

### Dependency versioning

For versioning, you can [use branches](<{https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/making-changes-in-a-branch/managing-branches}>). For example, using the **main** branch for the latest version, **stable@v0.0.1** branch for the stable 0.0.1 version, and so on

## 2. Structure a project

![How to structure monorepo project with Git](<https://miami.inverr.com/PiIl=LQwO/h6U4pR=qU.png>)The main idea of setting up a monorepo with git is to add dependencies (in step 1) as submodules.

In the project structure, a submodule is a local directory. Hence, we can easily import and treat them as a local directory. And because it’s a git repository, any committed changes will also apply to the copies in other projects (after pulling the changes)

### Project structures

One way to structure your project is to have all dependencies under the **src/packages** directory. Here’s a project directory tree example:

```
project-root/
    ├── .gitsubmodules
    ├── package.json
    ├── tsconfig.json
    ├── webpack.config.js
    └── src/
        ├── index.ts
        ├── packages/
        │   ├── module1 (submodule)/ 
        │   │   ├── package.json
        │   │   └── src/
        │   │       └── index.ts
        │   ├── module2 (submodule)/
        │   │   └── index.ts
        │   └── ...
        └── ...
```

See the [git-monorepo-project](<{https://github.com/hieunc229/git-monorepo-project}>) for example

### Add a dependency

After creating a dependency repository, you can add it as a submodule using the **git submodule add** command, and store it under the **src/packages** directory. Here is an example:

```
$ git submodule add https://github.com/username/module-name.git src/packages/module-name
```

To add a specific version of the dependency, use the **--b** flag when adding the submodule. For example:

```
$ git submodule add -b stable@v0.0.1 https://github.com/username/module-name.git src/packages/module-name
```

Now, you can import the new dependency as a local directory. For example, **import Module1 from “../packages/module1”**;

### Working from another computer

After setting up the monorepo, it’s easy to install a project or a dependency in another computer. It's useful when you have many workstations (ie. PC, laptop), or if you have someone working with you.

To set up the monorepo in another computer:

1. Clone the main project with the **--recursive** flag on the new computer. It will download the repository and all the submodules. For example: **git clone --recursive https://github.com/username/main-project.git**
2. Install node modules (if needed) using “**npm install**”

<!-- -->

Now the project should be ready to work on!

## 3. Configure module aliases

A common problem when setting up a monorepo as above is that it results in ugly import paths. For example, the import path in the **src/pages/dashboard/profile/ProfileMenu.tsx** file will be "**../../../packages/module1**".

Luckily, you can set up module aliases for shorter import paths. Note: if you're using webpack to transpile Typescript, you'll need to set up module aliases for both JavaScript and Typescript.

### Configure module aliases for JavaScript

You can configure the module alias for webpack in the **webpack.config.js** file, using the resolve.alias configuration. For React apps created with [CRA](<{https://create-react-app.dev/}>), you can use [react-app-rewired](<{https://www.npmjs.com/package/react-app-rewired}>) to override the webpack configurations.

For example:

```
module.exports = {
    …,
    resolve: {
        alias: {
            // import Module1 from “module1”
            "module1": "path/to/src/packages/module1",

            // this config allow importing any modules 
            // under src/packages directory
            // i.e import Module1 from “packages/module1”
            "packages": "path/to/src/packages",
            ...
        }
    }
}  
```

See the [webpack.config.js](<{https://github.com/hieunc229/https://github.com/hieunc229/git-monorepo-project/blob/master/webpack.config.js}>) file for example

### Configure module aliases for Typescript

You can configure module aliases for Typescript in the **tsconfig.json** file, using the **compilerOptions.paths** configuration.

For example:

```
{
    "compilerOptions": {
        …,
        "baseUrl": "./src",
        "paths": {

            // import Module1 from “module1”
            "module1": "packages/module1",
            "module1/*": "packages/module1/*",

            // this config allow importing any modules 
            // under src/packages directory
            // i.e import Module1 from “packages/module1”
            "packages": "packages",
            "packages/*": "packages/*",
            ...
        }
    }
}
```

Make sure the "**baseUrl**" (as above) is also present. It helps the compiler resolve dependency paths. See the [tsconfig.extends.json](<{https://github.com/hieunc229/git-monorepo-project/blob/master/tsconfig.extends.json}>) file for example

---

Once you have set up repositories for dependencies, structured your project as above, and configured your module aliases - your monorepo is ready!

## 4. Disadvantages

I’ve been using this approach for over a year. Here are a few issues you could encounter, and how to deal with them.

### Making dependencies

In case you’re trying to convert an existing project to a monorepo structure, it might take some time to set up. For example, separate some parts of the code and push them into their own repository.

But afterward, they are should be more independent, make it much easier to work with, or moving around.

### Dealing with dependencies of a dependency

It’s quite common when you’re using a dependency, which depends on other modules. In this case, I’ll install them in the main project.

Let’s say Project-1uses Module-A, Module-A uses Module-B, and they all belong to the monorepo. And Module-B was added to Module-A as above. In this case, I’ll need to do the same for Project-1. This means adding Module-B s a submodule, and config the module alias.

Also, make sure the module aliases should be the same in both Project-1 and Module-A.

## Takeaways

It’s often difficult to manage multiple projects and dependencies in a big app. A monorepo is a way to structure them all in a single repository, making it easier to work with.

Git provides submodules, branches, and the ability to manage code conflicts, which is useful for setting up a monorepo.

You can set up monorepo with git by separating each dependency into its own repository, then adding them as submodules. Besides, we get to configure module aliases to attain nice and readable import paths.

Thanks to [Carl Poppa](<{https://twitter.com/poppacalypse}>) for proofreading and feedback.