Front-end

- Créer une librairie Angular : ng-packagr et npm

Angular Librairie-Angular Ng-Packagr Angular-Package-Format Npm Monorepo Peer-Dependencies Public-Api Versioning Semver Publication-Npm Workspace Front-End Bonnes-Pratiques
Créer une librairie Angular : ng-packagr et npm

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.

À retenir : Une librairie Angular n'est pas une application. Elle ne se lance pas avec 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"
    }
}
Note : Le champ 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"
    }
}
Erreur classique : Ne configurez jamais 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é
Règle d'or : Considérez votre 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
Note : ng-packagr découvre automatiquement les entry points secondaires en scannant les sous-dossiers contenant un 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.

À propos de tslib : 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"
        }
    }
}
Note : Écrire des schematics complets dépasse le cadre de ce guide, mais retenez que 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
Piège npm link : Avec 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
Conseil : Utilisez 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"
}
Le flag --access public est obligatoire pour un scope : Par défaut, npm considère les packages scopés comme privés, ce qui nécessite un abonnement payant. Pour publier gratuitement un package scopé en open source, ajoutez explicitement --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
Note sécurité : Une version publiée sur npm ne peut jamais être réécrite avec un contenu différent. Si vous vous trompez, vous devez publier une nouvelle version (ex. 1.0.1). La commande 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.

Bonnes pratiques de token : Utilisez un token de type Automation (non soumis à la 2FA) pour la CI, avec la portée la plus restreinte possible. Faites-le tourner (rotation) régulièrement et révoquez-le immédiatement en cas de fuite. Pour aller plus loin, npm supporte le Trusted Publishing via OIDC, qui évite tout token stocké.

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 (dossier dist/ma-lib à jour)
  • Angular et RxJS déclarés en peerDependencies, jamais en dependencies
  • compilationMode: "partial" conservé pour la compatibilité multi-versions
  • public-api.ts ne ré-exporte que l'API réellement publique
  • Version incrémentée selon le semver (PATCH / MINOR / MAJOR)
  • Testé localement via npm pack ou path mapping avant publication
  • npm publish --dry-run vérifié (contenu du tarball correct)
  • --access public ajouté 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.

Partager