Perché un monorepo
Quando un progetto Flutter cresce, prima o poi ci si ritrova con più artefatti da mantenere: l'app mobile, magari una variante "admin", un design system condiviso, un client HTTP, un pacchetto di modelli di dominio. Tenerli in repository separati significa gestire pubblicazioni, tag di versione e allineamento delle dipendenze a mano, con il rischio classico: la modifica al pacchetto core rompe l'app e ce ne accorgiamo tre giorni dopo.
Il monorepo risolve il problema mettendo tutto sotto lo stesso versionamento Git: una singola PR può modificare contemporaneamente il pacchetto condiviso e le app che lo usano, e la CI valida il tutto insieme. Il prezzo da pagare è la gestione operativa: eseguire flutter pub get in dieci cartelle, lanciare i test ovunque, tenere in ordine i changelog. È esattamente qui che entra in gioco Melos.
Che cos'è Melos
Melos è un CLI scritto in Dart (mantenuto da Invertase) che orchestra monorepo Dart e Flutter. In sintesi fa quattro cose:
- collega i pacchetti locali tra loro senza dover scrivere a mano decine di
path:nelle dipendenze; - esegue comandi in parallelo su tutti i pacchetti, con filtri potenti;
- gestisce versioni e changelog a partire dai Conventional Commits;
- pubblica i pacchetti pubblici su pub.dev.
Installazione globale:
dart pub global activate melos
melos --version
Struttura tipica di un workspace
my_workspace/
├── melos.yaml
├── pubspec.yaml # root (publish_to: none)
├── apps/
│ ├── mobile/ # app Flutter principale
│ └── admin/ # seconda app Flutter
└── packages/
├── core_models/ # pacchetto Dart puro
├── core_network/ # client HTTP condiviso
└── design_system/ # widget e tema condivisi
La regola pratica: dentro packages/ mettiamo codice riutilizzabile e senza dipendenze dalle app, dentro apps/ gli eseguibili. Un pacchetto Dart puro (senza flutter) si testa più velocemente e si riusa anche lato backend o CLI.
Il file melos.yaml
È il cuore della configurazione:
name: my_workspace
packages:
- apps/**
- packages/**
ignore:
- "**/example/**"
command:
bootstrap:
# dipendenze applicate a TUTTI i pacchetti del workspace
environment:
sdk: ">=3.5.0 <4.0.0"
flutter: ">=3.24.0"
dependencies:
collection: ^1.18.0
dev_dependencies:
very_good_analysis: ^6.0.0
version:
linkToCommits: true
workspaceChangelog: true
branch: main
message: "chore(release): publish packages\n\n{new_package_versions}"
Due punti importanti:
- la sezione
command.bootstrap.dependenciespermette di centralizzare le versioni delle dipendenze comuni: Melos le scrive neipubspec.yamldei pacchetti durante il bootstrap, evitando derive di versione tra un pacchetto e l'altro; ignoreesclude cartelle che non vogliamo trattare come pacchetti (tipicamente gliexampledei plugin).
Da Melos 6.x la configurazione può vivere anche direttamente nel
pubspec.yamldi root sotto la chiavemelos:, utile se preferite un file in meno.
Bootstrap: il comando che userete ogni giorno
melos bootstrap # oppure: melos bs
bootstrap esegue il pub get su tutti i pacchetti e, soprattutto, collega i pacchetti locali generando un file pubspec_overrides.yaml in ciascuna cartella:
# packages/design_system/pubspec_overrides.yaml (generato da Melos)
dependency_overrides:
core_models:
path: ../core_models
Il vantaggio rispetto ai vecchi override scritti a mano è che il pubspec.yaml resta pulito e pubblicabile: dentro dichiarate la versione reale (core_models: ^1.2.0), mentre in locale lavorate sempre sul codice del repository. Ricordatevi di aggiungere a .gitignore:
pubspec_overrides.yaml
Nel codice non cambia nulla: gli import restano quelli di un normale pacchetto.
import 'package:core_models/core_models.dart';
import 'package:design_system/design_system.dart';
class ProductTile extends StatelessWidget {
const ProductTile({required this.product, super.key});
final Product product; // definito in core_models
@override
Widget build(BuildContext context) {
return DsCard( // widget del design system condiviso
title: product.name,
subtitle: product.formattedPrice,
);
}
}
Script: automatizzare i task ripetitivi
La sezione scripts trasforma Melos in un piccolo task runner condiviso da tutto il team.
scripts:
analyze:
description: Analisi statica su tutti i pacchetti
run: melos exec -c 5 -- dart analyze . --fatal-infos
format:
run: dart format --set-exit-if-changed .
test:
description: Test unitari e widget
run: melos exec -c 4 --dir-exists=test -- flutter test --coverage
gen:
description: build_runner sui pacchetti che lo usano
run: melos exec -c 1 --depends-on=build_runner -- \
dart run build_runner build --delete-conflicting-outputs
ci:
description: Pipeline locale completa
run: melos run format && melos run analyze && melos run test
Esecuzione:
melos run analyze
melos run test
melos run gen
Alcune opzioni utili di melos exec:
-c N/--concurrency N: quanti pacchetti in parallelo (usate-c 1per i comandi che scrivono file, comebuild_runner);--fail-fast: interrompe al primo errore;--order-dependents: rispetta l'ordine del grafo delle dipendenze.
Filtri: lavorare solo su ciò che serve
Su un monorepo grande, rieseguire tutto è uno spreco. Melos offre filtri combinabili:
# solo il design system
melos exec --scope="design_system" -- flutter test
# tutto tranne le app
melos exec --ignore="apps/**" -- dart analyze .
# solo i pacchetti che dipendono da core_models
melos exec --depends-on="core_models" -- flutter test
# solo i pacchetti modificati rispetto a main (perfetto in CI)
melos exec --diff=origin/main -- flutter test
# solo i pacchetti pubblicabili
melos exec --no-private -- dart pub publish --dry-run
Il filtro --diff è quello che fa davvero la differenza sui tempi di build: nella PR eseguite i test solo sui pacchetti toccati e sui loro dipendenti.
Versionamento e changelog automatici
Melos legge i Conventional Commits e calcola le nuove versioni:
feat(core_network): aggiunge interceptor di retry
fix(design_system): corregge il padding di DsCard
refactor(core_models)!: rinomina Product.price in Product.amount
fix:→ bump patchfeat:→ bump minor!oBREAKING CHANGE:→ bump major
Poi basta:
melos version # calcola versioni, aggiorna CHANGELOG, crea commit e tag
melos version --dry-run # anteprima senza scrivere nulla
melos publish # dry-run di default; --no-dry-run per pubblicare davvero
Melos propaga automaticamente il bump ai pacchetti dipendenti: se core_models passa a 2.0.0, i vincoli in design_system e nelle app vengono aggiornati e viene generata anche una voce di changelog "dependency update". Con workspaceChangelog: true ottenete inoltre un CHANGELOG.md di root con il riepilogo della release.
Se i pacchetti non vanno su pub.dev, ricordatevi publish_to: none nei loro pubspec.yaml: Melos li tratterà come privati e li escluderà dalla pubblicazione pur continuando a versionarli.
Melos e i pub workspaces di Dart 3.6
Da Dart 3.6 il tool pub supporta nativamente i workspaces: una sola risoluzione delle dipendenze e un unico .dart_tool/package_config.json per l'intero repository, con pub get molto più rapido. Si configura così nel pubspec.yaml di root:
name: my_workspace
publish_to: none
environment:
sdk: ^3.6.0
workspace:
- apps/mobile
- apps/admin
- packages/core_models
- packages/core_network
- packages/design_system
E in ogni pacchetto figlio:
name: core_models
environment:
sdk: ^3.6.0
resolution: workspace
I due strumenti non sono alternativi: le versioni recenti di Melos riconoscono i pub workspaces e, quando sono attivi, delegano a pub la risoluzione (niente più pubspec_overrides.yaml generati), continuando a offrire script, filtri, versionamento e changelog. È la combinazione consigliata sui progetti nuovi con SDK ≥ 3.6.
Un avvertimento pratico: con i workspaces la risoluzione è unica, quindi tutti i pacchetti devono essere compatibili con lo stesso set di versioni. Se avete bisogno di versioni divergenti della stessa dipendenza, restate sull'approccio classico con gli override.
Integrazione con la CI
Un workflow GitHub Actions minimale ma efficace:
name: CI
on:
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # necessario per --diff
- uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- name: Install Melos
run: dart pub global activate melos
- name: Bootstrap
run: melos bootstrap
- name: Format & analyze
run: melos run format && melos run analyze
- name: Test (solo pacchetti modificati)
run: melos exec --diff=origin/main --dir-exists=test -- flutter test
Note operative:
fetch-depth: 0è indispensabile perché--diffpossa confrontare i branch;- mettete in cache
~/.pub-cacheper accorciare i tempi; - se pubblicate su pub.dev, aggiungete un job separato su
mainche eseguemelos versionemelos publish --no-dry-runcon le credenziali OIDC.
Supporto negli IDE
Melos genera automaticamente le configurazioni di avvio per VS Code e IntelliJ/Android Studio:
ide:
intellij:
enabled: true
Su VS Code conviene inoltre creare un file my_workspace.code-workspace con le cartelle dei singoli pacchetti: l'analyzer di Dart lavora meglio quando ogni pacchetto è una root del workspace, evitando falsi errori sugli import.
Errori comuni da evitare
- Dimenticare
melos bootstrapdopo ungit pull: se qualcuno ha aggiunto un pacchetto, gli override locali sono obsoleti. Rendetelo parte del vostromake setupo di un hookpost-merge. - Committare i
pubspec_overrides.yaml: sono artefatti locali, vanno in.gitignore. - Dipendenze circolari tra pacchetti: Melos le segnala, ma il vero problema è di design. Tenete un grafo aciclico:
app → feature → core, mai il contrario. - Un unico pacchetto "utils" tuttofare: diventa in fretta il collo di bottiglia che invalida la cache di build ad ogni modifica. Meglio pacchetti piccoli e coesi.
build_runnerin parallelo: usate sempre-c 1o--order-dependentsper evitare conflitti sui file generati.- Versionare a mano: se scrivete i Conventional Commits, lasciate calcolare tutto a
melos version; i changelog manuali si disallineano sempre.
Conclusione
Melos non è un framework né cambia il modo in cui scrivete Flutter: è l'infrastruttura che rende sostenibile un monorepo. Con un melos.yaml di venti righe ottenete link automatici tra pacchetti, script condivisi da tutto il team, esecuzione mirata dei test in CI e release versionate senza intervento manuale.
Se state avviando un progetto nuovo su Dart 3.6 o superiore, partite direttamente con i pub workspaces più Melos per script e versionamento: è la configurazione più veloce e con meno file generati. Su progetti esistenti, il passaggio a Melos si può fare in modo incrementale, un pacchetto alla volta, iniziando a estrarre il design system o i modelli di dominio.