Stéphane De Todaro — tech lead

@super-dev.app · Lead-technique
Actif depuis 2017

Lead technique et architecte full-stack, freelance depuis 2019. Je conçois, industrialise et exploite des plateformes métier sur Azure, et je publie des moteurs logiciels open source.

Retour aux articles
FLUTTER
FLUTTER

Monorepo Flutter avec Melos

Article 3 sur 3 — Flutter en production
Offline-first, Riverpod, monorepo Melos : du mobile sérieux, pas un proto.

Un produit Flutter un peu ambitieux tient rarement dans un seul package. Il y a l'application, un design system partagé, un client d'API, parfois un module par équipe. Répartis dans des dépôts séparés, ces packages imposent à chaque changement transverse une séquence de publications et de bumps de version à ordonner à la main. Melos, l'outil d'Invertase, gère l'autre approche : un seul dépôt Dart/Flutter, plusieurs packages, des commandes qui s'exécutent sur tout le graphe en une passe.

Un dépôt, plusieurs packages

Les packages vivent sous un dossier de la racine, en général packages/ , chacun avec son propre pubspec.yaml . L'application et les modules se référencent entre eux par dépendance de chemin ( path: ../core_api ), et Melos résout ces liens localement au lieu d'aller chercher les versions publiées sur pub.dev.

Dart
1// packages/feature_auth/lib/feature_auth.dart
2import 'package:core_api/core_api.dart';
3
4class AuthRepository {
5 AuthRepository(this._api);
6 final ApiClient _api;
7
8 Future<Session> signIn(String email, String password) =>
9 _api.post('/auth/login', {'email': email, 'password': password});
10}

Ce découpage porte une contrainte d'architecture. feature_auth dépend de core_api , l'inverse ne compile pas. Le graphe des dépendances est écrit noir sur blanc dans les pubspec.yaml , et une dépendance interdite se voit tout de suite : elle ne résout pas. Un dépôt unique garde ces frontières entières et rend seulement leur franchissement plus simple quand il est légitime.

Le fichier melos.yaml

La configuration tient dans un melos.yaml à la racine (les versions récentes acceptent aussi une clé melos: dans le pubspec.yaml de la racine). Il déclare les packages du workspace et des scripts nommés, réutilisables en local comme en CI.

YAML
1name: my_app
2
3packages:
4 - app
5 - packages/**
6
7command:
8 version:
9 linkToCommits: true # add commit links in each CHANGELOG
10 workspaceChangelog: true # also aggregate a root CHANGELOG.md
11 bootstrap:
12 hooks:
13 post: melos run generate # run codegen once linking is done
14
15scripts:
16 analyze:
17 exec: dart analyze .
18 description: Static analysis in every package.
19
20 test:
21 exec: flutter test
22 description: Test only packages that ship a test/ directory.
23 packageFilters:
24 flutter: true
25 dirExists: test
26
27 generate:
28 exec: dart run build_runner build --delete-conflicting-outputs
29 description: Regenerate sources in packages that use build_runner.
30 packageFilters:
31 dependsOn: build_runner

Un script en forme exec: lance sa commande dans chaque package retenu, et packageFilters restreint l'ensemble avant exécution. Ici flutter: true écarte les packages Dart purs, et dirExists: test saute ceux qui n'ont pas de dossier de tests, ce qui évite un échec bruyant sur un package sans suite. La section command: configure les commandes intégrées ; sous version , workspaceChangelog ajoute un changelog agrégé à la racine en plus de celui de chaque package.

Un script se déclenche par melos run analyze ou, sans argument, par un melos run qui propose la liste des scripts au clavier. La commande melos exec reste la porte de sortie pour tout ce qui n'a pas de script attitré.

Bootstrap et liaison locale

melos bootstrap (abrégé melos bs ) est la première commande qu'on lance après un clone. Elle récupère les dépendances de tous les packages en une fois et câble les dépendances de chemin entre eux, sans qu'on ait à enchaîner flutter pub get package par package.

Concrètement, Melos écrit dans chaque package un pubspec_overrides.yaml qui pointe les dépendances internes vers leur dossier local :

YAML
1# packages/feature_auth/pubspec_overrides.yaml (generated by Melos, git-ignored)
2dependency_overrides:
3 core_api:
4 path: ../core_api

Ce fichier est un mécanisme natif de Dart, pas une invention de Melos : pub le lit en surcouche du pubspec.yaml et privilégie la version locale indiquée. On le laisse hors du contrôle de version et on relance bootstrap après chaque modification d'un pubspec.yaml . Sur les versions récentes de Dart, Melos peut aussi s'appuyer sur les workspaces natifs de pub ( resolution: workspace ) pour obtenir la même résolution locale.

Le hook post de la configuration ci-dessus se déclenche à la fin de bootstrap . C'est l'endroit naturel pour la génération de code, très présente dans un projet Flutter avec freezed , json_serializable ou un générateur de providers : le clone est prêt à compiler dès la première commande, sans étape manuelle oubliée. Le filtre dependsOn: build_runner limite la génération aux packages qui en ont réellement besoin.

Cibler un sous-ensemble : les filtres

Le même mécanisme de filtres alimente melos exec , qui exécute une commande arbitraire sur une partie du graphe. Les filtres se combinent :

Bash
1# Run tests only in packages changed since main
2melos exec --diff="origin/main" -- flutter test
3
4# Analyze core_api and everything that depends on it
5melos exec --scope="core_api" --include-dependents -- dart analyze .
6
7# List which packages would run, without executing anything
8melos list --diff="origin/main"

--diff compare l'arbre de travail à une référence git et ne garde que les packages touchés. --scope et --ignore filtrent par nom avec des globs. --depends-on et --include-dependents suivent les arêtes du graphe pour attraper aussi les packages en amont ou en aval d'un changement. En CI, --diff="origin/main" est le levier qui évite de rejouer l'analyse et les tests de tout le dépôt à chaque push : seul le sous-graphe modifié tourne, et le reste garde son résultat précédent.

Versioning coordonné

melos version traduit les commits conventionnels en changements de version. Il lit l'historique git depuis le dernier tag de chaque package, en déduit le bump ( fix: en patch, feat: en minor, un BREAKING CHANGE: en major), met à jour le champ version: du pubspec.yaml concerné et écrit son CHANGELOG.md . Le tag posé porte le nom du package, par exemple core_api-v1.4.0 .

Le point qui justifie l'outil, c'est la propagation. Un fix(core_api): ... fait monter core_api , mais aussi les packages qui en dépendent : Melos ajuste leur contrainte de version et leur applique un incrément. L'ensemble reste cohérent, sans qu'un package référence une version d'un voisin qui n'existe pas encore. Le scope entre parenthèses ( core_api ) rattache le commit au bon package quand un même changement en touche plusieurs.

Une fois les versions posées et taguées, melos publish pousse sur pub.dev les packages publiables. La commande tourne à blanc par défaut et n'agit réellement qu'avec --no-dry-run , et elle saute d'office les packages marqués publish_to: none , comme l'application Flutter elle-même.

L'enchaînement en CI

Un pipeline reprend ces briques dans l'ordre. melos bootstrap installe et relie, puis melos run analyze et melos run test tournent sur les packages modifiés via --diff , et la branche de release ajoute melos version puis melos publish . Chaque étape réutilise la même définition de scripts que celle des développeurs en local, ce qui évite la dérive entre la CI et le poste de travail : ce qui passe sur une machine passe dans le pipeline, avec la même commande.

Un monorepo se juge au coût d'un changement transverse. Avec Melos, toucher un package partagé et ses consommateurs tient dans une seule branche à revoir et à fusionner, quand des dépôts séparés imposeraient une file de publications ordonnée à la main.
Stéphane De Todaro — super-dev.app
// Continuer dans FLUTTER
Sync offline-first en Flutter + Firebase
20 févr. 2026 • 5 min
Une architecture Flutter testable avec Riverpod
22 janv. 2026 • 5 min