How to Setup Monorepos with Git for JavaScript and TypeScript

A guide to setting up monorepos using Git for Javascript or Typescript. With private repositories, proper versioning, and beautiful import paths

Cover photo demonstrates monorepo with submodules

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 a few times. Yalc and Yarn Workspace 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. 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 structure a monorepo, by setting up a dependency, how to structure your project, and configure module aliases. Then discuss the disadvantage I’ve encountered of using this setup.

See git-monorepo-project on Github for the final result

1. Setup a dependency

A dependency is a submodule in a monorepo
A dependency is a submodule in a monorepo
Each dependency is a git submodule

A dependency is a git repository. It can either contain a complete module (i.e with package.json and bundled/transpile JavaScripts files), or it may only have plain JavaScript or Typescript files.

Create a dependency repository

You can create a public or private repository (make sure contributors have access), and push the code there.

Dependency versioning

We often need different versions of the dependency (also called versioning). They allow us to make changes in a specific version, without affecting projects that use other versions.

For versioning, you can use 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

Photo demonstrates dependency module-1 is a submodule
Photo demonstrates dependency module-1 is a submodule
Module-1 is a submodule of the front-end project

The main idea behind 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 as 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
│ └── ...
└── ...

Add a dependency

After creating a dependency repository (in step 1), you can add it as a submodule using the git submodule add command, and store it under the src/packages directory. For 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 on 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 on 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” or “yarn

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, when importing “packages/modules1” from the src/pages/dashboard/profile/ProfileMenu.tsx file, the import path 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 JavaScript with webpack in the webpack.config.js file, using the resolve.alias configuration. For React apps created with CRA, you can use react-app-rewired to override the webpack configurations.

Here’s the webpack configuration example:

// webpack.config.js
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 file 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 file example

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

4. Disadvantages

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

Setting up existing 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 and configure them following the above steps.

Let’s say Project-1 uses Module-A, Module-A uses Module-B. They’re all in a monorepo set up as above.

In that case, I’ll need to install Module-B to Project-1, and configure its module alias. Also, make sure the module aliases should be the same in both Project-1 and Module-A. For example, Module-B should be called module-b 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 for proofreading and feedback.

A developer & hobbyist photographer. Develop a drop and drag website builder www.inverr.com