Créez, buildez et publiez une librairie Angular réutilisable : workspace multi-projets, ng-packagr, Angular Package Format, peer dependencies et npm publish.
Pourquoi créer une librairie Angular ?
Dès qu'une entreprise dépasse une seule application Angular, une question se pose : comment partager les composants, services, directives et pipes communs entre les projets ? Copier-coller le code d'une application à l'autre est la pire des solutions — la duplication engendre des divergences, des bugs difficiles à corriger de manière centralisée et une dette technique qui explose. La réponse professionnelle est une librairie Angular : un package versionné, testé et distribuable qui encapsule votre logique réutilisable.
ng serve et n'a pas de bootstrapApplication. C'est un ensemble de blocs réutilisables (composants, services, tokens) compilés dans un format standardisé — l'Angular Package Format — que d'autres applications importent comme n'importe quel package npm.
Les cas d'usage typiques d'une librairie sont nombreux dans un contexte professionnel :
- Design system : boutons, cartes, modales, formulaires partagés avec une charte graphique cohérente entre toutes les apps de l'organisation.
- Couche métier commune : services d'authentification, intercepteurs HTTP, gestion des erreurs, guards de routing réutilisés partout.
- SDK client : encapsuler l'accès à une API interne dans un package typé que les équipes consomment sans réécrire les appels HTTP.
- Utilitaires : pipes de formatage, directives d'accessibilité, validators de formulaires réactifs.
- Open source : publier un composant réutilisable sur npm pour la communauté.
Deux grandes stratégies d'organisation s'offrent à vous. Le monorepo regroupe applications et librairies dans un seul workspace (un seul dépôt Git), tandis que le multirepo isole chaque librairie dans son propre dépôt publié indépendamment. Le tableau ci-dessous compare les deux approches.
| Critère | Monorepo | Multirepo |
|---|---|---|
| Refactoring atomique | Facile (un seul commit lib + app) | Difficile (coordination inter-dépôts) |
| Versioning des libs | Souvent implicite (workspace) | Explicite (semver + npm) |
| Isolation des équipes | Faible (code partagé visible) | Forte (frontières claires) |
| CI/CD | Complexe (builds affectés) | Simple (un pipeline par dépôt) |
| Outillage recommandé | Nx, workspace Angular natif | npm registry + semver |
Dans cet article, nous suivrons le flux complet avec le workspace Angular natif — sans outil tiers — puis nous publierons la librairie sur npm pour couvrir le scénario multirepo. Ce socle est la base indispensable, que vous utilisiez ensuite Nx ou non.
Créer le workspace et la librairie
Angular CLI offre un support natif des workspaces multi-projets. L'idée est de créer un workspace vide (sans application), puis d'y générer une librairie. Cette séparation garantit que votre librairie reste indépendante et testable isolément.
Étape 1 : Créer un workspace sans application initiale
# Créer un workspace vide (le flag --no-create-application est clé)
ng new mon-workspace --no-create-application
cd mon-workspace
# Générer la librairie dans projects/ma-lib
ng generate library ma-lib
# Alias court équivalent :
# ng g lib ma-lib
Angular CLI génère alors une arborescence structurée. Voici les fichiers essentiels à comprendre :
# Structure générée
mon-workspace/
├── angular.json # Config du workspace (projet ma-lib enregistré)
├── package.json # Dépendances de dev partagées
├── tsconfig.json # tsconfig de base + paths mapping vers ma-lib
└── projects/
└── ma-lib/
├── ng-package.json # Config ng-packagr (entryFile, dest)
├── package.json # package.json PUBLIÉ de la lib (name, version, peerDeps)
├── tsconfig.lib.json # Compilation de production de la lib
├── tsconfig.lib.prod.json
├── README.md
└── src/
├── public-api.ts # Point d'entrée : ce qui est exporté publiquement
└── lib/
├── ma-lib.service.ts
└── ma-lib.component.ts # (ou .ts selon la version)
Deux fichiers package.json coexistent, ce qui déroute souvent les débutants. Le package.json à la racine du workspace gère les dépendances de développement (Angular CLI, TypeScript, Karma). Le package.json dans projects/ma-lib/ est celui qui sera réellement publié sur npm : il contient le nom du package, sa version et ses peerDependencies.
Le fichier ng-package.json pilote ng-packagr. Voici sa forme minimale :
// projects/ma-lib/ng-package.json
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
// Dossier de sortie du build (relatif à ce fichier)
"dest": "../../dist/ma-lib",
"lib": {
// Point d'entrée principal de la librairie
"entryFile": "src/public-api.ts"
}
}
angular.json enregistre automatiquement ma-lib avec le builder @angular-devkit/build-angular:ng-packagr. C'est ce qui permet à ng build ma-lib de fonctionner sans configuration supplémentaire. Ne modifiez ce fichier que si vous ajoutez des entry points secondaires.
À ce stade, le tsconfig.json racine contient un mapping paths qui pointe les imports ma-lib vers le dossier de build. Cela permet à une application du même workspace de consommer la librairie avant même sa publication — nous y reviendrons dans la section dédiée au test local.
ng-packagr et l'Angular Package Format
ng-packagr est l'outil qui transforme votre code source TypeScript en un package distribuable conforme à l'Angular Package Format (APF). L'APF est une spécification officielle qui définit la structure exacte d'une librairie Angular : quels formats de modules produire, où placer les typings, comment structurer les métadonnées. Respecter l'APF garantit que votre librairie fonctionne avec tous les outils de build (esbuild, webpack) et toutes les versions récentes d'Angular.
Concrètement, quand vous lancez le build, ng-packagr produit plusieurs artefacts dans dist/ma-lib :
| Artefact | Format | Rôle |
|---|---|---|
fesm2022/ |
FESM (Flattened ESM) | Un seul fichier ESM aplati par entry point — optimise le tree-shaking |
esm2022/ |
ESM2022 | Modules ES non aplatis (un fichier par source) |
*.d.ts |
TypeScript typings | Déclarations de types pour l'autocomplétion et la vérification |
package.json |
Métadonnées | Champs module, typings, exports générés |
index.d.ts |
Barrel de types | Point d'entrée des déclarations TypeScript |
Un point crucial de l'APF moderne est la compilation partielle Ivy (partial compilation). Depuis Angular 12, les librairies ne sont plus compilées en code Ivy « complet » figé pour une version d'Angular donnée. Elles sont publiées dans un format intermédiaire — le partial declaration format — que le compilateur de l'application consommatrice finalise lors de son propre build. C'est ce mécanisme qui permet à une librairie compilée avec Angular 17 de fonctionner dans une application Angular 18 ou 19 sans recompilation manuelle.
Le fichier tsconfig.lib.prod.json active ce mode via la propriété compilationMode :
// projects/ma-lib/tsconfig.lib.prod.json
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
"declarationMap": false
},
"angularCompilerOptions": {
// Mode "partial" = compilation Ivy partielle (APF)
// L'app consommatrice finalisera la compilation
"compilationMode": "partial"
}
}
compilationMode: "full" pour une librairie destinée à npm. Le mode full fige la lib sur une version précise du runtime Ivy et provoque des incompatibilités dès que l'application utilise une version d'Angular différente. Le mode partial est le comportement par défaut correct.
Vous n'avez généralement pas à invoquer ng-packagr directement : le builder Angular s'en charge. Mais il est utile de savoir que ng build ma-lib exécute en réalité ng-packagr en lui passant votre ng-package.json. Pour un usage hors Angular CLI, la commande brute serait ng-packagr -p ng-package.json.
Structurer public-api.ts et les entry points
Le fichier public-api.ts est le contrat public de votre librairie. Tout ce qui n'y est pas ré-exporté reste privé et inaccessible aux consommateurs, même si le code existe dans le package. C'est un mécanisme puissant d'encapsulation : vous pouvez avoir des composants internes, des helpers privés et des services techniques qui ne polluent jamais l'API publique.
Un public-api.ts bien structuré :
// projects/ma-lib/src/public-api.ts
/*
* Point d'entrée public de ma-lib.
* Seuls les symboles exportés ici sont visibles par les consommateurs.
*/
// Composants publics
export * from './lib/button/button.component';
export * from './lib/card/card.component';
// Services publics
export * from './lib/services/theme.service';
// Tokens d'injection et interfaces de config
export * from './lib/config/ma-lib.config';
// Directives et pipes
export * from './lib/directives/tooltip.directive';
// ⚠️ On N'EXPORTE PAS les helpers internes :
// export * from './lib/internal/dom-utils'; ← reste privé
public-api.ts comme une frontière stable. Chaque symbole exporté devient une promesse de compatibilité envers vos utilisateurs. Retirer ou renommer un export exporté est un breaking change qui impose une montée de version majeure (semver).
Pour les librairies plus riches, l'APF permet des secondary entry points (points d'entrée secondaires). Ils permettent au consommateur d'importer un sous-ensemble de la librairie via un chemin dédié comme ma-lib/testing, améliorant le tree-shaking et l'organisation. C'est le pattern qu'utilise Angular lui-même avec @angular/common/http.
Créer un entry point secondaire revient à ajouter un sous-dossier avec son propre ng-package.json et son public-api.ts :
# Structure avec un entry point secondaire "testing"
projects/ma-lib/
├── ng-package.json # Entry point principal
├── src/
│ └── public-api.ts
└── testing/ # Entry point secondaire : ma-lib/testing
├── ng-package.json # Config minimale du sous-package
└── src/
└── public-api.ts # Exports du sous-package (mocks, helpers de test)
// projects/ma-lib/testing/ng-package.json
{
// Un entry point secondaire n'a besoin que de son entryFile
"lib": {
"entryFile": "src/public-api.ts"
}
}
// projects/ma-lib/testing/src/public-api.ts
// Mocks et utilitaires réservés aux tests des consommateurs
export * from './lib/mock-theme.service';
export * from './lib/test-harness';
Le consommateur importe alors chaque entry point séparément, ce qui garde le bundle de production léger :
// Dans l'application consommatrice
import { ButtonComponent } from 'ma-lib'; // Entry point principal
import { MockThemeService } from 'ma-lib/testing'; // Entry point secondaire
ng-package.json. Aucune configuration centrale n'est nécessaire — chaque sous-package est autonome.
peerDependencies, versioning et schematics
Le package.json de la librairie (celui dans projects/ma-lib/) est le fichier le plus stratégique pour la distribution. Une erreur fréquente et coûteuse consiste à y déclarer Angular comme une dependency classique. C'est un piège : cela ferait embarquer une seconde copie d'Angular dans l'application consommatrice.
La règle des peerDependencies : Angular et RxJS doivent être des peerDependencies. Une peer dependency signifie « ma librairie a besoin qu'Angular soit présent dans l'application hôte, mais je ne fournis pas ma propre copie — j'utilise la vôtre ». Cela évite les doublons qui provoquent des erreurs comme NullInjectorError ou des instances de services dupliquées.
// projects/ma-lib/package.json
{
"name": "ma-lib",
"version": "1.0.0",
// ✅ peerDependencies : fournies par l'APPLICATION hôte
"peerDependencies": {
"@angular/common": "^17.0.0 || ^18.0.0",
"@angular/core": "^17.0.0 || ^18.0.0",
"rxjs": "^7.8.0"
},
// dependencies : SEULEMENT les libs propres à ma-lib
// (ex. une petite lib utilitaire sans lien avec Angular)
"dependencies": {
"tslib": "^2.6.0"
},
"sideEffects": false
}
Remarquez la plage de versions avec l'opérateur || : "^17.0.0 || ^18.0.0" déclare que la librairie est compatible avec Angular 17 et 18. Élargir cette plage augmente l'audience de votre librairie, mais impose de tester réellement contre chaque version supportée. Grâce à la compilation partielle Ivy vue précédemment, une même librairie peut couvrir plusieurs versions majeures d'Angular.
tslib reste en dependencies (pas peer) car c'est un helper runtime de TypeScript, léger et sans état, dont la duplication est inoffensive. C'est l'exception qui confirme la règle.
Le versioning suit le semver (Semantic Versioning) — un contrat que vos utilisateurs comprennent immédiatement :
| Changement | Incrément | Exemple |
|---|---|---|
| Correction de bug rétrocompatible | PATCH | 1.0.0 → 1.0.1 |
| Nouvelle fonctionnalité rétrocompatible | MINOR | 1.0.1 → 1.1.0 |
| Breaking change (API retirée/modifiée) | MAJOR | 1.1.0 → 2.0.0 |
Les schematics et ng-add (survol) : Une librairie professionnelle peut fournir des schematics — des scripts de génération de code exécutés par Angular CLI. Le plus courant est ng-add, déclenché quand l'utilisateur tape ng add ma-lib. Il automatise l'installation : ajout des imports, configuration de app.config.ts, ajout de styles.
// projects/ma-lib/package.json — déclarer les schematics
{
"name": "ma-lib",
// Pointe vers le collection.json des schematics
"schematics": "./schematics/collection.json",
"ng-add": {
"save": "dependencies"
}
}
// projects/ma-lib/schematics/collection.json
{
"$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {
"ng-add": {
"description": "Configure ma-lib dans le projet",
"factory": "./ng-add/index#ngAdd"
}
}
}
ng add transforme une installation manuelle en une seule commande — un vrai atout pour l'adoption d'un design system interne.
Builder et consommer la lib en local
Avant toute publication, vous devez builder la librairie et la tester dans une vraie application. Ne publiez jamais une librairie que vous n'avez pas consommée localement au préalable — c'est le moyen le plus sûr de détecter les problèmes d'API publique et de peer dependencies.
Étape 1 : Builder la librairie
# Build de production (mode partial Ivy par défaut)
ng build ma-lib
# Le résultat est produit dans dist/ma-lib/
# Pour rebuild automatique pendant le développement :
ng build ma-lib --watch
Vous disposez ensuite de deux méthodes principales pour consommer cette librairie dans une application de test. La première, la plus simple dans un même workspace, exploite le path mapping tsconfig déjà configuré par Angular CLI.
Méthode A — Path mapping (même workspace) :
// tsconfig.json (racine du workspace) — généré automatiquement
{
"compilerOptions": {
"paths": {
// Angular CLI mappe l'import "ma-lib" vers le build
"ma-lib": ["./dist/ma-lib"],
"ma-lib/testing": ["./dist/ma-lib/testing"]
}
}
}
Avec ce mapping, une application créée dans le même workspace (ng generate application demo) importe simplement import { ButtonComponent } from 'ma-lib'. Aucune installation npm n'est nécessaire — le compilateur résout l'import vers dist/ma-lib.
Méthode B — npm link (workspaces séparés) : Si votre application de test vit dans un autre dépôt, npm link crée un lien symbolique global vers votre build.
# 1. Dans le dossier du build de la librairie
cd dist/ma-lib
npm link
# → crée un lien global vers ce package
# 2. Dans l'application consommatrice (autre projet)
cd ../../mon-app-de-test
npm link ma-lib
# → node_modules/ma-lib pointe désormais vers dist/ma-lib
npm link, l'application peut charger deux copies d'Angular (celle de l'app et celle résolue depuis le lien), ce qui provoque des erreurs NG0203 ou des injections cassées. La solution est d'ajouter un paths mapping dans le tsconfig de l'app pointant Angular vers un seul node_modules, ou tout simplement de préférer la méthode A (path mapping) quand c'est possible.
Méthode C — npm pack (simulation de publication) : Pour tester exactement ce qui sera publié, générez un tarball comme le fera npm et installez-le. C'est la méthode la plus fidèle.
# Créer le tarball .tgz depuis le build
cd dist/ma-lib
npm pack
# → génère ma-lib-1.0.0.tgz
# Installer ce tarball dans l'app de test
cd ../../mon-app-de-test
npm install ../mon-workspace/dist/ma-lib/ma-lib-1.0.0.tgz
npm pack comme ultime vérification avant npm publish. Le tarball contient exactement les fichiers qui seront publiés — vérifiez qu'il ne contient pas de fichiers sources superflus ni, à l'inverse, qu'il n'oublie pas un asset essentiel.
Publier la librairie sur npm
La publication rend votre librairie disponible pour le monde entier (ou pour votre organisation en registry privé). La règle absolue à ne jamais oublier : on publie toujours le dossier de build (dist/ma-lib), jamais le dossier source projects/ma-lib.
Étape 1 : S'authentifier sur npm
# Se connecter à son compte npm (crée un token dans ~/.npmrc)
npm login
# Vérifier l'utilisateur connecté
npm whoami
Étape 2 : Publier depuis le dossier de build
# Toujours rebuild avant de publier
ng build ma-lib
# Se placer dans le dossier de BUILD (crucial !)
cd dist/ma-lib
# Publier
npm publish
Les packages scopés (@org/ma-lib) sont recommandés en entreprise : ils regroupent vos packages sous un namespace et évitent les collisions de noms sur le registry public. Modifiez le name dans projects/ma-lib/package.json :
// projects/ma-lib/package.json
{
// Package scopé sous l'organisation @acme
"name": "@acme/ui-kit",
"version": "1.0.0"
}
--access public. Sans ce flag, la commande échoue avec une erreur 402 Payment Required.
# Publier un package scopé en accès public (gratuit)
cd dist/ma-lib
npm publish --access public
Les dist-tags permettent de publier des versions parallèles sans écraser la version stable. Par défaut, npm publish pose le tag latest. Pour une préversion (beta, next), utilisez un tag dédié :
# Publier une beta sous le tag "next" (ne devient PAS latest)
npm publish --tag next
# → les utilisateurs installent avec : npm install @acme/ui-kit@next
# Promouvoir plus tard une beta en version stable
npm dist-tag add @acme/ui-kit@2.0.0-beta.3 latest
Enfin, maîtrisez le contenu du tarball publié. Le champ files du package.json ou un fichier .npmignore contrôle ce qui est inclus. ng-packagr génère un package.json propre dans dist/, mais vérifiez toujours avec un dry run :
# Simuler la publication SANS rien envoyer (liste les fichiers inclus)
cd dist/ma-lib
npm publish --dry-run
# → affiche la taille du package et chaque fichier inclus
npm unpublish n'est autorisée que dans les 72 heures et est fortement découragée car elle casse les projets qui dépendent de la version.
CI/CD, documentation et bonnes pratiques
Une librairie professionnelle ne se publie pas à la main depuis un poste de développeur. L'automatisation via CI/CD garantit des builds reproductibles, des tests systématiques et une publication déclenchée par un simple tag Git. Voyons un pipeline GitHub Actions type.
Pipeline GitHub Actions de publication npm :
# .github/workflows/publish.yml
name: Publish Library
# Déclenché quand on pousse un tag de version (ex. v1.2.0)
on:
push:
tags:
- 'v*'
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
# Configure l'URL du registry pour l'auth
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm ci
- name: Test
run: npm run test -- --watch=false --browsers=ChromeHeadless
- name: Build library
run: npm run build ma-lib
- name: Publish to npm
# Publie depuis le dossier de build
run: npm publish ./dist/ma-lib --access public
env:
# Token stocké dans les secrets GitHub (jamais en clair !)
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Le point de sécurité central est le NPM_TOKEN. Générez un token d'automatisation (Automation token) depuis votre compte npm, puis stockez-le dans les secrets du dépôt GitHub (Settings → Secrets → Actions). Ne le committez jamais en clair. La variable d'environnement NODE_AUTH_TOKEN est automatiquement lue par npm grâce au registry-url configuré par setup-node.
La documentation conditionne l'adoption. Une librairie sans README clair ne sera pas utilisée, même excellente. Soignez le README.md publié (celui de projects/ma-lib/) avec : installation, exemple minimal, table des composants/services exportés, et lien vers une démo. Pour les grosses librairies, générez la doc d'API automatiquement avec Compodoc.
Le changelog documente l'évolution version par version. Adoptez la convention Keep a Changelog et, idéalement, automatisez sa génération à partir de commits conventionnels (feat:, fix:, BREAKING CHANGE:) avec des outils comme semantic-release ou changesets. Ces outils déduisent même le bon incrément semver automatiquement.
# Exemple de commits conventionnels → incrément semver auto
git commit -m "fix: corrige le focus du bouton" # → PATCH
git commit -m "feat: ajoute la variante outline" # → MINOR
git commit -m "feat!: renomme l'input color" # → MAJOR (breaking)
Checklist avant publication
- Librairie buildée avec
ng build ma-lib(dossierdist/ma-libà jour) - Angular et RxJS déclarés en
peerDependencies, jamais endependencies -
compilationMode: "partial"conservé pour la compatibilité multi-versions -
public-api.tsne ré-exporte que l'API réellement publique - Version incrémentée selon le semver (PATCH / MINOR / MAJOR)
- Testé localement via
npm packou path mapping avant publication -
npm publish --dry-runvérifié (contenu du tarball correct) -
--access publicajouté pour un package scopé open source - README à jour et changelog complété pour la version
- Token npm stocké dans les secrets CI (jamais en clair dans le dépôt)
En résumé, créer une librairie Angular professionnelle suit une chaîne claire : générer un workspace multi-projets, laisser ng-packagr produire un package conforme à l'Angular Package Format, structurer rigoureusement l'API publique via public-api.ts, déclarer Angular en peerDependencies, tester en local avant de publier, puis automatiser la publication npm en CI/CD. Maîtriser ce flux, c'est se donner les moyens de bâtir un design system durable ou de partager du code entre toutes les applications de votre organisation — sans jamais recopier une ligne.