Dart & Flutter Monorepos: Pub Workspaces and Melos

Dart & Flutter Monorepos: Pub Workspaces and Melos

A monorepo is a development strategy that keeps code for all projects within a single repository. It provides the benefits of code reuse, atomic changes, and simplified dependencies.

Table of Contents

  1. Do I need a Monorepo?
  2. Local and Public Packages
  3. Pub Workspaces
  4. Reusable Scripts
  5. Melos

Do I need a Monorepo?

Deciding whether to use a monorepo comes down to your project's structure and goals. However, here are a few questions that may help you to choose:

  • Do you have multiple related projects or packages that would benefit from being developed and versioned together? Perhaps they are interdependent and share code, or require the same set of dependencies and SDK versions?
  • Do you need to make atomic changes? When you change a shared library, do you want to update all the projects that use it in a single commit to avoid version conflicts and ensure everything stays in sync?
  • Do you need to launch development servers locally to debug client-server communication? For example, running a Flutter app and its Dart backend simultaneously?

If you answered 'yes' to any of these, a monorepo is worth considering. By the way, Google and Meta store all their projects in a monorepo. At that scale, having thousands of projects in one place is much more manageable than keeping them separate.

If you would like to know more about the advantages of a monorepo approach, please give a read to Dan Luu's article.

Local and Public Packages

A monorepo can contain local and public packages:

  • Local Packages: These are private modules used exclusively within the monorepo. They are perfect for organizing a large application into modular features (e.g., feature_feed for the feed tab) or for sharing code between internal projects (e.g., a common_ui package with shared widgets).
  • Public Packages: These are libraries designed for distribution on pub.dev or other registries. Common examples include packages for logging, authentication, analytics, or UI kits.

While local packages don't require special treatment, public packages need to be versioned and published. When you add a new feature, you need to increment the package version, then update the dependent packages and changelogs, and finally publish. To learn more about versioning, go to the Versioning and Publishing Packages chapter.

Pub Workspaces

Fortunately, you don't need complex tooling to get started. Dart 3.6+ and Flutter 3.27+ have native support for monorepos through Pub Workspaces. To set it up, create a pubspec.yaml file at the root of your monorepo and list the paths to your packages:

# root/pubspec.yaml
name: my_monorepo

workspace:
  - 'apps/customer_app'
  - 'apps/driver_app'
  - 'packages/shared_ui'

# apps/customer_app
name: customer_app
resolution: workspace

You then need to indicate in each package's pubspec.yaml that it should use the workspace's dependency resolution:

# apps/customer_app/pubspec.yaml
name: customer_app

# This tells pub to resolve dependencies from the root pubspec.lock
resolution: workspace

Now, if you run dart pub get, the tool will download all dependencies from all the packages in a workspace. If you run dart pub update dependency, it will update the dependency version across the entire workspace.

There are also a few other benefits Pub Workspaces give:

  • Faster Analysis: The Dart analyzer sees all your projects as a single unit. This improves performance, leading to faster code completion, error highlighting, and navigation in your IDE.
  • Simplified Dependencies: Only one version of any dependency is allowed across the entire codebase. There is a single pubspec.lock file at the root, which eliminates version conflicts.
  • Seamless Local Development: When one of your packages depends on another, the pub tool automatically uses the local version from your workspace rather than fetching it from pub.dev. Changes to a shared package are instantly reflected in the apps that use it, with no need for publishing or manual path overrides.

To learn more, check out the official Pub Workspaces documentation.

Reusable Scripts

In every project, there is always a list of operations we frequently do. For example, bootstrapping a project, running the analyzer, tests, building the app, and publishing the packages. You can use whatever format suits you & team, but I prefer Bash.

Bootstrap Script

This script gets all dependencies for the workspace and runs code generation for any packages that need it.

#!/bin/bash

# Get workspace dependencies
flutter pub get

# For each package in 'packages/' that uses build_runner, run code generation
for dir in packages/*; do
  if [ -f "$dir/pubspec.yaml" ]; then
    if grep -q build_runner "$dir/pubspec.yaml"; then
      pushd $dir || exit
      dart run build_runner build --delete-conflicting-outputs
      popd || exit
    fi
  fi
done

Clean Script

This script cleans the root project and all child packages, which can help fix local caching issues.

# Clean workspace
flutter clean

# Clean packages
for dir in core feature; do
  if [ -f "$dir/pubspec.yaml" ]; then
    pushd $dir
    flutter clean
    popd
  fi
done

Test Script

This script automatically finds any package with a test directory and runs all the tests with coverage.

#!/bin/bash

# Enable error handling
set -e

# Find directories with a pubspec.yaml and a test/ folder
find_test_dirs() {
  find . -type f -name "pubspec.yaml" -exec dirname {} \; | while read -r dir; do
    if [ -d "$dir/test" ]; then
      echo "$dir"
    fi
  done
}

# Capture the output of find_test_dirs and pass it to flutter test
test_dirs=$(find_test_dirs)
if [ -n "$test_dirs" ]; then
  flutter test $test_dirs --no-pub --coverage --file-reporter json:reports/tests.json
else
  echo "No directories with pubspec.yaml and test/ folder found."
fi

Melos

Melos is a tool that simplifies the management of packages in a monorepo. Its core features include:

  • Executing scripts across multiple packages.
  • Optimizing CI/CD by running checks only on packages that have changed.
  • Automating the versioning and publishing of public packages.

Getting Started

To start using Melos, you will need to configure the Pub Workspaces. After that, you can add a Melos section to the root pubspec.yaml file, in which you can add Melos configurations or scripts.

name: root
description: "Workspace root"

environment:
  sdk: ">=3.9.2 <4.0.0"
  flutter: ">=3.35.3 <4.0.0"

dev_dependencies:
  sizzle_lints: ^2.1.7

workspace:
  - app
  # Core
  - core/common
  - core/analytics

melos:
  command:
    bootstrap:
      hooks:
        post: dart run build_runner build -d

Post-hook for a bootstrap command that runs codegen

Melos comes with a built-in 'bootstrap' command that sets up everything needed to launch the project, including downloading dependencies, running codegen, and so forth. You can add whatever logic you need by adding a post-hook to a bootstrap.

Running Scripts for Changed Packages

Melos allows for automatic understanding of which packages changed between commits – for example, what changed in the last commit or between the current branch and main.

This is a useful technique for CI/CD, as there is little to no sense in running tests/analyzer checks for packages that do not change or don't depend on changed ones. Here is what it looks like:

# Run `flutter analyze` on all packages that are different between current
# branch and the specified commit hash.
melos exec --diff=<commit hash> -- flutter analyze

# Run `flutter analyze` on all packages that are different between remote
# `main` branch and HEAD.
melos exec --diff=origin/main...HEAD -- flutter analyze

Versioning and Publishing Packages

Melos offers a version command that automatically versions packages based on the git commit history. It uses conventional commits to calculate the new version for a package and adds the commit title as an entry to a changelog.

Here's how it works: Imagine you have an authentication package with version 1.0.0, and you push a new commit titled "feat: added a phone login method". Based on this, Melos can determine that a new feature has been added and that the package needs a minor version update. For the 'fix' prefix, it will make a patch version bump. If an update contains breaking changes, there should be an exclamation mark or the word 'BREAKING' after the prefix.

Once you have versioned the packages, push the commit to the repository and run melos publish, which will send artefacts to the registry. Ideally, this process should also be automated so that a job can be run in your pipeline to version and publish packages.

For more information, reach out to the Automated Releases guide.

Read more