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 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 on Github for the final result*
1. Setup a dependency
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 (make sure contributors have access), and push the code there.
Dependency versioning
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
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 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:
- 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
- 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, you can use 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 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 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 for proofreading and feedback.
Top comments (0)