Lab 27 : NX — Monorepo from scratch
📖 Ressources
🚀 Code de départ
Pas de code de départ — vous allez créer un workspace NX from scratch.
Ce lab couvre les mêmes objectifs que le Lab 26 — bibliothèques partagées, alias de chemin, graphe de dépendances et affected — mais démarre depuis un workspace vide plutôt que depuis le preset Angular. Cette approche vous donne un contrôle total sur ce qui est installé et généré.
📝 Instructions
Étape 1 : Créer un workspace vide
npx create-nx-workspace@latest my-workspace --preset=apps
Quand demandé, choisissez No pour Nx Cloud.
--preset=apps crée un workspace minimal sans code de démo, sans plugin de framework, sans application générée — juste la structure :
my-workspace/
├── packages/ ← placeholder (racine npm workspaces)
├── nx.json
├── tsconfig.base.json
├── tsconfig.json
└── package.json
Les dossiers apps/ et libs/ n'existent pas encore — ils sont créés lors de l'exécution des générateurs dans les étapes suivantes.
Comparez avec le preset Angular (Lab 26), qui pré-génère une application shop et installe @nx/angular pour vous. Ici, rien n'est supposé.
L'approche --preset=apps est utile quand :
- Vous voulez mixer des frameworks (Angular + React dans le même monorepo)
- Vous voulez auditer exactement ce qui est installé
- Vous configurez un monorepo et ajouterez les frameworks progressivement
Étape 2 : Ajouter le support Angular
Le workspace vide n'a aucun support de framework. Commencez par naviguer dans le workspace et exporter une variable d'environnement qui supprime un avertissement de compatibilité TypeScript :
cd my-workspace
export NX_IGNORE_UNSUPPORTED_TS_SETUP=true
Nx 23 crée les workspaces avec les project references TypeScript activées par défaut. Angular ne supporte pas les project references (voir angular#37276). Cette variable permet à tous les générateurs Angular de s'exécuter sans blocage. Gardez-la active pour toute la session du lab.
Ajoutez maintenant le plugin Angular :
npx nx add @nx/angular
Cela installe @nx/angular et l'enregistre dans nx.json. Cela ne génère pas d'application — c'est votre prochaine étape.
Étape 3 : Générer la première application
nx g @nx/angular:app shop
Quand demandé, choisissez votre format de feuille de style (CSS ou SCSS).
Dans Nx 23, les applications sont générées à la racine du workspace (pas dans un sous-dossier apps/) :
shop/
shop-e2e/
Lancez-la :
nx serve shop
Ouvrez http://localhost:4200 — une application Angular vierge sans contenu de démo.
Étape 4 : Générer une deuxième application
nx g @nx/angular:app admin
Vous avez maintenant deux applications indépendantes dans le même dépôt :
shop/
shop-e2e/
admin/
admin-e2e/
Lancez-les indépendamment :
nx serve shop # http://localhost:4200
nx serve admin # http://localhost:4201
Étape 5 : Générer une bibliothèque UI partagée
nx g @nx/angular:lib --name=ui --directory=libs/ui
Note (Nx v23+) : Les arguments positionnels ne sont plus supportés par ce générateur — utilisez toujours
--name=uiexplicitement.--directory=libs/uiplace la bibliothèque danslibs/ui/à la racine du workspace.
La bibliothèque est créée dans libs/ui/ avec son propre project.json. Le générateur crée aussi un composant par défaut dans libs/ui/src/lib/ui/ — vous pouvez le conserver ou le supprimer ; il n'est pas utilisé dans ce lab.
Pourquoi libs/ et non src/ ?
Dans un Integrated Monorepo Nx, la convention est de placer le code partageable dans le dossier libs/ racine afin que plusieurs applications (shop, admin, …) puissent toutes l'importer. Chaque bibliothèque est un projet TypeScript indépendant avec son propre project.json.
libs/ui vs libs/shop/shared-ui
| Chemin | Portée | Quand utiliser… |
|---|---|---|
libs/ui/ | Workspace entier | Primitives UI génériques utilisées par toute app |
libs/shop/shared-ui/ | Portée shop uniquement | Composants spécifiques au domaine shop |
Vérifiez l'alias de chemin généré par Nx
Après la génération, ouvrez tsconfig.base.json :
{
"compilerOptions": {
"paths": {
"@org/ui": ["./libs/ui/src/index.ts"]
}
}
}
TypeScript résout @org/ui vers libs/ui/src/index.ts. Pas besoin d'alias webpack.
⚠️ Le préfixe dépend de l'organisation npm définie à la création du workspace. Lisez toujours
tsconfig.base.json → compilerOptions.pathspour trouver votre alias exact avant de l'utiliser dans les imports.
Tags et frontières de modules
Ouvrez libs/ui/project.json et ajoutez des tags :
{
"tags": ["scope:shared", "type:ui"]
}
Les tags sont appliqués par @nx/enforce-module-boundaries dans eslint.config.mjs. Sans scope:shared, ESLint bloquera les imports depuis les apps une fois les contraintes configurées :
"@nx/enforce-module-boundaries": [
"error",
{
"depConstraints": [
{ "sourceTag": "scope:shop", "onlyDependOnLibsWithTags": ["scope:shop", "scope:shared"] },
{ "sourceTag": "scope:admin", "onlyDependOnLibsWithTags": ["scope:admin", "scope:shared"] }
]
}
]
Étape 6 : Créer un composant partagé
nx g @nx/angular:component libs/ui/src/lib/button/button --export
Note (Nx v23+) : Passez le chemin complet — le flag
--projecta été supprimé. Le flag--exportajoute automatiquement l'export danslibs/ui/src/index.ts.
Dans Angular 20, les composants générés utilisent la convention de nommage courte : le fichier est button.ts (pas button.component.ts) et la classe est Button (pas ButtonComponent). standalone est la valeur par défaut et n'a pas besoin d'être déclaré.
Remplacez le contenu de libs/ui/src/lib/button/button.ts par :
import { Component, input } from '@angular/core';
@Component({
selector: 'lib-button',
template: `
<button class="btn" [class]="variant()">
<ng-content />
</button>
`,
styles: [`
.btn { padding: 8px 16px; border: none; cursor: pointer; border-radius: 4px; }
.primary { background: #007bff; color: white; }
.secondary { background: #6c757d; color: white; }
`]
})
export class Button {
variant = input<'primary' | 'secondary'>('primary');
}
Le flag --export a déjà ajouté l'export dans libs/ui/src/index.ts. Vérifiez qu'il pointe vers le bon fichier :
export * from './lib/button/button';
Étape 7 : Utiliser la bibliothèque dans l'app shop
Dans shop/src/app/app.component.ts :
import { Button } from '@org/ui'; // vérifiez tsconfig.base.json pour votre préfixe exact
@Component({
selector: 'app-root',
imports: [Button],
template: `
<h1>Shop</h1>
<lib-button variant="primary">Ajouter au panier</lib-button>
<lib-button variant="secondary">Voir les détails</lib-button>
`
})
export class AppComponent {}
nx serve shop
Le bouton partagé s'affiche dans l'app shop. L'app admin peut importer le même composant depuis le même chemin — une seule source de vérité.
Étape 8 : Visualiser le graphe de dépendances
nx graph
Vérifiez :
shopdépend deuiadminn'a pas encore de dépendances
Étape 9 : Exécuter les commandes affected
Faites une modification dans Button, puis :
nx affected --target=build
nx affected --target=test
Nx détermine que modifier ui affecte shop — les deux sont reconstruits. admin n'est pas touché. En CI vous ne reconstruisez et retestez que ce qui a vraiment changé.
🔄 Comparaison des presets
| Lab 26 (preset Angular) | Lab 27 (preset apps) | |
|---|---|---|
| Configuration | Prompts interactifs | Flag --preset=apps |
| Plugin Angular | Pré-installé | npx nx add @nx/angular |
| Première app | Auto-générée (shop) | Générée manuellement |
| Emplacement des apps | shop/ à la racine du workspace | shop/ à la racine du workspace |
| Code de démo | Inclus | Aucun |
| Idéal pour | Démarrage Angular rapide | Multi-framework ou setup contrôlé |