A monorepo (or shared codebase) is a software development strategy in which multiple projects share a single git repository. Assuming you are familiar with the concept of a git repository and have some understanding of a monorepo, this article aims to provide guidance on effectively managing a monorepo. However, to ensure clarity, I will briefly dwell on the "What".
In a monorepo, multiple projects share a single codebase, which should not be confused with a monolithic architecture. These projects have a well-defined relationship with each other. Another software development approach is a polyrepo, also known as a multi-repository. It is an alternative approach where each project exists in its own separate repository and is independently managed. The purpose of this article is not to go into detailed explanations of these concepts, so I will refrain from providing an in-depth explanation of what a monolith or polyrepo is. If you are unfamiliar with these terms, I recommend referring to the resources provided at the end of this article. It is assumed that you already have a basic understanding of a monorepo, although additional resources will be provided for further information. Let's get down to business.
This is what a monorepo would look like in a dart project.
my-repo/ |- ... |- `packages/` |- |- *package-1/* |- |- |- lib/ |- |- |- pubspec.yaml |- |- |- ... |- |- *package-2/* |- |- |- lib/ |- |- |- pubspec.yaml |- |- |- ... |- pubspec.yaml |- ...
(Ignore the formatting - the use of `` and **).
In any dart project, you will always find a
pubspec.yaml file among other things. In the folder structure above, notice that
pubspec.yaml appears more than once, indicating the presence of multiple projects. You have the root project, and inside your root project you have a directory
packages/ with what appears to contain two projects -
package-2 both containing
This is a common way that a monorepo (a project with sub-projects) will be structured in a dart project. The use of the words packages, package-1 and package-2 are subjective and can be replaced to give a tailored meaning to a project. You would commonly find packages replaced with shared_libraries, and both could sometimes co-exist.
Splitting a single project into sub-projects introduces new challenges which can slow down development time and reduce productivity. Overall, it can affect development workflow if not properly managed. This is what this article aims to cover - the "How to" of managing a monorepo. One automatically inherits some challenges when using a monorepo approach, but I'll discuss more in the context of Flutter and Dart - no pun intended. One significant challenge that arises when utilizing a monorepo approach includes:
- Fetching dependencies: This poses a challenge because if your project was broken down into four sub-projects, you would need to run
flutter pub getin all four packages.
This is also the same when a good number of sub-projects depend on code generation; this is an ugly experience for any new collaborator on your project. This is where melos comes in.
Melos - The "What"
So far I've introduced some maybe-foreign concepts, but what exactly is melos. Melos is a tool.
Melos is a tool for managing Dart & Flutter repositories with multiple packages (monorepo)....
There are other tools to manage monorepos outside the context of flutter and dart - again no pun intended.
Getting started setting up melos
As of the time of writing, according to the melos documentation, there are a few steps that are required to start using melos
Install melos as a global package by running the following command in a terminal.
dart pub global activate melos
Add the melos to your dev dependency in the
pubspec.yamlat the root of your main project - in our folder structure above, that will be the
pubspec.yamlfile directly inside
my_repo/. You can add it by running the following command:
In a dart project:
dart pub add melos --dev
In a Flutter project:
flutter pub add melos --dev
lib/directory of the project, it should be added as a regular dependency. On the other hand, if the dependency is only required for development-related tasks, such as build automation or testing, it should be added as a dev dependency. By following this approach, you can maintain a clear separation between the dependencies needed for production code functionality and those used solely for development purposes. This helps in keeping the production build lean and efficient, while still having the necessary development tools and utilities available during the development process. For more detailed information, you can refer to the resources provided here and here.
Next is to configure your Melos workspace. To configure your Melos workspace, you need to create a melos.yaml file at the root of your repository. In the melos.yaml file:
name: <project> packages: - packages/package-1/ - packages/package-2/
The name field is required and should be set to the name of your project. The packages field specifies the list of sub-projects that will be managed by melos, and should contain paths to individual packages in the repository.
Note: To provide more flexibility in defining the package paths, you can use glob patterns instead of explicitly listing each package. Glob patterns allow you to specify patterns that match multiple files or directories based on specific criteria. So we replace the packages field with:
... packages: - packages/*
packages/* pattern matches all directories within the packages directory, effectively including all directories under packages. It simply means include all packages inside the
This approach allows for easier management of package paths, especially when dealing with a larger number of sub-projects within your monorepo.
The next thing to do is to bootstrap melos. From the documentation, bootstrapping serves two purposes:
Installing package dependencies: The bootstrap process ensures that all package dependencies are installed. It runs the equivalent of
flutter pub getor
dart pub getacross all packages in your monorepo, fetching the required dependencies for each package.
Locally linking packages: Additionally, the bootstrap process establishes local linking between packages. This linking allows you to depend on a package within your monorepo without explicitly using a file path. Instead, the package is treated as if it were fetched from a remote source like Pub, but it is available locally within your monorepo.
To bootstrap Melos, you can use the following command:
By running melos bootstrap whenever there are new packages added to your monorepo, you ensure that the dependencies are correctly installed and linked, maintaining a consistent development environment across all packages.
Once bootstrapping is completed, the packages within the monorepo can establish dependencies on each other without the need for explicit file paths. This enables
package-1 to depend on
package-2 directly in its
However, it's important to avoid circular dependencies, where
package-1 depends on
package-2 and vice versa. Circular dependencies can lead to code complexities and make it harder to maintain the codebase.
To mitigate circular dependencies, it's recommended to abstract common libraries or shared functionality into a separate package. This shared package can then be included as a dependency in both
package-2, allowing them to access the common libraries without creating a circular dependency.
By abstracting shared functionality into a separate package, you promote code reuse and maintain a modular and manageable monorepo structure. It also helps in avoiding conflicts and inconsistencies that may arise from circular dependencies.
By adhering to this practice, you can create a well-organized monorepo where packages have clear and independent responsibilities, reducing the complexity and improving the maintainability of your codebase.
The melos file has another field
scripts. Let's dive into a practical example of how the
scripts field in the melos.yaml file can be used to streamline package-wide commands.
Suppose you want to run dart pub get across all packages simultaneously. Without Melos, you would need to navigate to each package directory and execute
dart pub get individually, which can be time-consuming and repetitive.
With Melos and the
scripts field, you can define a custom script that runs the command across all packages at once.
Here's an example of how you can define a
get script in melos.yaml file:
Declare another field called
... scripts: get: exec: dart pub get
We have defined a script called
get. Melos will execute the defined script for each package in the monorepo, effectively running
dart pub get across all packages simultaneously.
By leveraging scripts in Melos, you can significantly speed up development time by executing commands across packages without the need for manual navigation. This helps maintain a consistent and efficient workflow within your monorepo.
To run this script, simply open the terminal and run
melos run get
There are two ways to define and run a script with slight differences. Using
exec. We can rewrite the script above with:
... scripts: get: run: dart pub get
If we wanted to run this script across all packages, we would run in the terminal:
melos exec "melos run get"
This syntax can also be used to run multiple commands:
melos exec "melos run get && <another script>"
exec can also be used together when defining a script. If you wanted to define options for your script, you will use exec to define the options and run for the command. An example is shown in the melos docs:
... scripts: get: run: dart pub get exec: concurrency: 1
concurrency option defines how many packages will execute the command
dart pub get at a time. It defaults to 5. Another option is
failFast, this is a boolean. It basically says, if executing a script fails in one package, should I continue executing the script in other packages or not? The default value is false.
Another useful option is
orderDependents, more about it here.
melos exec "dart analyze". This will execute the dart analyze command across packages.
Note that you can define multiple scripts for different commands.
All the instructions provided here are readily available in the official documentation and offer more comprehensive details. This article aims to serve as a guiding resource to help you grasp the benefits of using Melos for managing your monorepo. For a comprehensive understanding and access to additional information, I strongly encourage you to read the official documentation. It provides in-depth explanations, further instructions, and advanced features that may not be covered in this guide. Familiarizing yourself with the official documentation will enable you to leverage Melos to its full potential and enhance your monorepo management workflow effectively.
Don't forget to include the melos badge in your README file as suggested here.
If you have any observations or insights you would like to share, please feel free to do so. Your input is valuable.
While writing this article, I encountered an issue with bootstrapping melos. After trying to find out what I was doing wrong, I realized my project structure was the problem, this was after I went through different projects on pub that are managed by melos. It appears it is how a dart repo with sub-projects should be structured (the example at the beginning) but I could not find anything in the melos or dart documentation about project structure. I would appreciate any information or some clarity about this.