Aller au contenu principal

Lab 26 : NX — Architecture Monorepo

📖 Ressources

🚀 Code de départ

Aucun — vous créerez un workspace NX depuis zéro.

Dans ce lab, vous mettrez en place un monorepo NX, générerez deux applications Angular, créerez une bibliothèque de composants partagés et utiliserez l'alias de chemin pour la consommer. Vous explorerez également le graphe de dépendances et la commande affected.

📝 Instructions

Étape 1 : Créer le workspace NX

npx create-nx-workspace@latest my-workspace

Quand demandé :

  • Stack : Angular
  • Type : Integrated Monorepo
  • Nom de l'application : shop
  • Format des styles : CSS (ou SCSS)
  • Activer le cache distribué (Nx Cloud) : Non
my-workspace/
├── apps/
│ └── shop/
├── libs/
├── nx.json
├── tsconfig.base.json
└── package.json

Étape 2 : Démarrer l'application

cd my-workspace
nx serve shop

Ouvrez http://localhost:4200 — l'application Angular par défaut s'exécute.

Étape 3 : Générer une deuxième application

nx g @nx/angular:app admin

Vous avez maintenant deux applications indépendantes partageant le même dépôt :

apps/
├── shop/
└── admin/

Exécutez chacune indépendamment :

nx serve shop    # http://localhost:4200
nx serve admin # http://localhost:4201

Étape 4 : Générer une bibliothèque UI partagée

nx g @nx/angular:lib ui --directory=libs/ui

Note (Nx v22+) : Passez ui comme nom positionnel et --directory=libs/ui comme chemin de sortie complet. Évitez de combiner --name=ui et --directory=libs/ui ensemble — Nx traite --directory comme le chemin complet, et les deux flags peuvent produire un dossier libs/ui/ui/ imbriqué au lieu du libs/ui/ attendu.

Pourquoi libs/ et pas src/ ?

Un projet Angular standard garde tout dans src/. Dans un Integrated Monorepo Nx, la convention est de placer le code partageable dans le dossier racine libs/ afin que plusieurs applications (shop, admin, …) puissent l'importer sans artifices de chemins circulaires. Chaque bibliothèque est un projet TypeScript indépendant avec son propre project.json.

libs/ui vs libs/shop/shared-ui

CheminPortéeÀ utiliser quand…
libs/ui/Workspace entierPrimitives UI génériques (boutons, icônes, typographie) utilisées par toutes les applications
libs/shop/shared-ui/Scope shop uniquementComposants spécifiques au domaine shop qui ne doivent pas déborder dans d'autres scopes

Utilisez libs/ui pour des briques pouvant appartenir à un design system. Utilisez libs/shop/shared-ui pour des widgets de panier, des cartes produit, ou tout ce qui est propre au shop.

Vérifier l'alias de chemin généré par Nx

Après l'exécution du générateur, ouvrez tsconfig.base.json et cherchez la nouvelle entrée sous paths :

{
"compilerOptions": {
"paths": {
"@my-workspace/ui": ["libs/ui/src/index.ts"]
}
}
}

C'est ainsi que TypeScript résout @my-workspace/ui vers les fichiers source réels dans libs/ui/src/index.ts. Aucun alias webpack ni résolveur personnalisé n'est nécessaire — il s'agit du mappage de chemins TypeScript standard.

⚠️ Le préfixe dépend du nom d'organisation saisi lors du create-nx-workspace. Si vous avez choisi acme, l'alias sera @acme/ui. Lisez toujours tsconfig.base.json → compilerOptions.paths pour trouver l'alias exact de votre workspace avant de l'utiliser dans vos imports.

Tags et frontières de modules

Nx applique des règles d'import via @nx/enforce-module-boundaries dans eslint.config.mjs. Les bibliothèques doivent porter des tags pour que la règle sache quels scopes peuvent les importer.

Ouvrez libs/ui/project.json et vérifiez (ou ajoutez) le champ tags :

{
"tags": ["scope:shared", "type:ui"]
}

Sans scope:shared, ESLint bloquera toute application qui tente d'importer cette bibliothèque dès que des depConstraints sont configurées. Vous pouvez inspecter les règles de frontières dans eslint.config.mjs :

// eslint.config.mjs (extrait)
"@nx/enforce-module-boundaries": [
"error",
{
"depConstraints": [
{ "sourceTag": "scope:shop", "onlyDependOnLibsWithTags": ["scope:shop", "scope:shared"] },
{ "sourceTag": "scope:admin", "onlyDependOnLibsWithTags": ["scope:admin", "scope:shared"] }
]
}
]

Cela signifie que shop ne peut importer que des libs taguées scope:shop ou scope:shared. Taguez libs/ui avec scope:shared pour que les deux applications puissent l'utiliser librement.

Étape 5 : Créer un composant partagé

Générez un composant bouton dans la bibliothèque :

nx g @nx/angular:component libs/ui/src/lib/button/button --export

Note (Nx v22+) : Le flag --project a été supprimé du générateur de composants. Passez le chemin complet vers l'endroit où le composant doit être créé à la place.

Modifiez libs/ui/src/lib/button/button.component.ts :

import { Component, input } from '@angular/core';

@Component({
selector: 'lib-button',
standalone: true,
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 ButtonComponent {
variant = input<'primary' | 'secondary'>('primary');
}

Assurez-vous qu'il est exporté depuis libs/ui/src/index.ts :

export * from './lib/button/button.component';

Étape 6 : Utiliser la bibliothèque dans l'application shop

Dans apps/shop/src/app/app.component.ts, importez en utilisant l'alias trouvé dans tsconfig.base.json :

import { ButtonComponent } from '@my-workspace/ui'; // remplacez le préfixe si le vôtre est différent

@Component({
selector: 'app-root',
standalone: true,
imports: [ButtonComponent],
template: `
<h1>Boutique</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'application shop.

Étape 7 : Visualiser le graphe de dépendances

nx graph

Le navigateur ouvre un graphe interactif. Vérifiez :

  • shop dépend de ui
  • admin n'a pas encore de dépendances

Étape 8 : Exécuter les commandes affected

Faites une modification dans ButtonComponent, puis :

# Ne builder que ce qui a changé + ce qui en dépend
nx affected --target=build

# Ne tester que ce qui a changé + ce qui en dépend
nx affected --target=test

NX détermine que modifier ui affecte shop (qui en dépend) — les deux sont donc reconstruits. admin n'est pas touché.

C'est le principal avantage en CI/CD : vous ne payez que pour ce qui a changé.