Créer et publier un module Angular

Publié le

Cet article s’adresse à des développeurs/ses expérimenté/e/s, qui connaissent déjà les concepts principaux d’Angular et qui savent construire une application basique.

Note : il y a désormais des outils pour vous aider dans la création d’une librairie Angular, cet article est donc obsolète.

Lors d’un précédent article, je vous parlais du module que j’ai créé : angular-async-local-storage. Ce fut assez simple de créer un module Angular et de l’utiliser directement dans mon application. Mais comme cela pouvait aider d’autres développeurs, je voulais le transformer en module réutilisable comme n’importe quel autre module Angular.

Cette étape fut assez laborieuse. Je n’ai trouvé quasiment aucune documentation sur le sujet, alors j’ai tenté de copier le fonctionnement du module Http officiel. Maintenant que j’y suis parvenu, je tenais à partager mon expérience sur comment créer et publier un module Angular.

Créer un module Angular : les pièges

Cette partie est presque identique à la création d’un module dans votre application : importez les modules dont vous avez besoin, déclarez vos composants, directives et pipes, ou fournissez des services. Il y a seulement quelques détails auxquels il faut faire attention.

D’abord, n’importez jamais BrowerModule. Votre module est un feature module (un sous-module), seul l’utilisateur final devra importer BrowserModule dans le module root (principal) de son application. Si vous avez besoin des directives courantes (*ngIf, *ngFor…), importez CommonModule.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@NgModule({
imports: [CommonModule]
})
export class AmazingModule {}

Si votre module consiste à créer de nouveaux composants, directives ou pipes, n’oubliez pas de les exporter. Se limiter à les déclarer les rend utilisables seulement à l’intérieur de ce module, pas à l’extérieur.

import { NgModule } from '@angular/core';
import { PrivateComponent } from './private.component';
import { PublicComponent } from './public.component';
@NgModule({
declarations: [
PrivateComponent,
PublicComponent
],
exports: [PublicComponent]
})
export class AmazingModule {}

Plus important, évitez de mélanger des composants/directives/pipes et des services dans le même module. Pourquoi ?

  • Un service fourni dans un module sera utilisable partout dans l’application. Il faudra ainsi importer le module une seule fois, dans le module principal (comme le HttpModule).
  • Un composant/directive/pipe exporté sera utilisable seulement dans le module qui importe le vôtre. L’utilisateur devra donc réimporter votre module dans tous les modules (le principal et les sous-modules) qui en ont besoin (comme le CommonModule).

Si cela n’est pas clair pour vous, lisez notre article « Comprendre les modules Angular (NgModule) ». C’est un point important (et qui porte à confusion) dans Angular.

Enfin, respectez la règle d’or d’Angular : n’utilisez jamais directement des APIs spécifiques au navigateur (comme le DOM). Si vous le faites, votre module ne sera pas compatible avec la génération côté serveur (Universal) and d’autres options avancées d’Angular. Si vous n’avez pas le choix (par exemple pour le localStorage), vous devez intercepter les erreurs avec des try/catch.

Exporter l’API publique

Quand vous utilisez un module Angular officiel, vous accédez à un unique point d’entrée pour importer tout ce dont vous avez besoin (comme '@angular/http').

Vous devez donc créer un fichier index.ts, qui exporte toute l’API publique de votre module. Il devrait au minimum contenir votre NgModule, et vos composants/directives/pipes et services (l’utilisateur aura besoin de les importer pour les injecter là où il en a besoin).

export { AmazingModule } from './amazing.module';
export { AmazingComponent } from './amazing.component';
export { AmazingService } from './amazing.service';
view raw index.ts hosted with ❤ by GitHub

Les composants/directives/pipes ne seront pas importés directement par l’utilisateur, mais vous devez les exporter pour être compatible avec la pré-compilation AoT.

Build tools

C’est la partie qui a été la plus difficile. J’ai donc pris modèle sur un module Angular officiel. Sont utilisés :

npm install @angular/compiler @angular/compiler-cli typescript rollup uglify-js –save-dev
view raw npm.sh hosted with ❤ by GitHub

Configuration TypeScript

Voici le tsconfig.json de mon module :

{
"compilerOptions": {
"baseUrl": ".",
"declaration": true,
"stripInternal": true,
"experimentalDecorators": true,
"strictNullChecks": true,
"noImplicitAny": true,
"module": "es2015",
"moduleResolution": "node",
"paths": {
"@angular/core": ["node_modules/@angular/core"],
"rxjs/*": ["node_modules/rxjs/*"]
},
"rootDir": ".",
"outDir": "dist",
"sourceMap": true,
"inlineSources": true,
"target": "es5",
"skipLibCheck": true,
"lib": [
"es2015",
"dom"
]
},
"files": [
"index.ts"
],
"angularCompilerOptions": {
"strictMetadataEmit": true
}
}
view raw tsconfig.json hosted with ❤ by GitHub

Il y a des différences importantes avec votre tsconfig.json habituel :

  • des chemins explicites ("paths") sont nécessaires, car le package final ne les inclura pas directement (plus de précisions à ce sujet ci-dessous) ;
  • "angularCompilerOptions": { "strictMetadataEmit": true } est nécessaire pour être compatible avec la pré-compilation AoT ;
  • "declaration": true est important pour générer des fichiers de définitions, qui permettront à l’utilisateur d’avoir l’auto-complétion pour votre module ;
  • "noImplicitAny": true et "strictNullChecks": true sont recommandés pour réduire le risque d’erreurs et être compatible avec toutes les configurations utilisateur ; "noImplicitAny": true doit être respecté depuis Angular 4.0, et "strictNullChecks": true à partir d’Angular 4.1.
  • "module": "es2015" est important pour les performances, et "sourceMap": true pour le debug, mais rien de spécifique ici ;
  • "stripInternal": true évite du code inutile venant d’APIs internes et "skipLibCheck": true évite de voir la compilation bloquée par des erreurs (bénignes) venant des librairies que vous utilisez.

Configuration Rollup

Les modules Angular sont fournis au format UMD. Votre rollup.config.js doit donc être configuré en conséquence. Voici un exemple :

export default {
entry: 'dist/index.js',
dest: 'dist/bundles/amazing.umd.js',
sourceMap: false,
format: 'umd',
moduleName: 'ng.amazing',
globals: {
'@angular/core': 'ng.core',
'rxjs/Observable': 'Rx',
'rxjs/ReplaySubject': 'Rx',
'rxjs/add/operator/map': 'Rx.Observable.prototype',
'rxjs/add/operator/mergeMap': 'Rx.Observable.prototype',
'rxjs/add/observable/fromEvent': 'Rx.Observable',
'rxjs/add/observable/of': 'Rx.Observable'
}
}

Le script en entrée ("entry") est votre fichier index.ts transpilé, cela doit donc correspondre à votre configuration TypeScript. bundles/modulename.umd.js est le chemin et le nom conventionnels utilisés par les modules Angular officiels.

Rollup nécessite un moduleName pour le format UMD. Cela sera un objet JavaScript, n’utilisez donc pas de caractères spéciaux (notamment les tirets).

Il s’agit ensuite d’un point central : votre module utilise des fonctionnalités d’Angular (au minimum le décorateur NgModule), mais votre package ne doit pas inclure Angular.

Pourquoi ? Angular sera déjà inclus dans l’application de l’utilisateur. Si votre module l’inclus aussi, il sera chargé deux fois, provoquant une erreur fatale (et incompréhensible).

Vous devez donc définir Angular en tant que global. Et pour cela, vous devez connaître le nom UMD de chaque module. Cela suit cette convention : ng.modulename (ng.core, ng.common, ng.http…).

Il en est de même pour RxJS, si votre module l’utilise. Les noms UMD sont plus complexes. Pour les classes (Observable…), c’est Rx. Pour les opérateurs (map, filter…), c’est Rx.Observable.prototype. Pour les méthodes directes de classes (of, fromEvent…), c’est Rx.ClassName (par exemple Rx.Observable).

Enfin, la compilation

Vous pouvez maintenant compiler votre module. Vous pouvez enregistrer les lignes de commandes dans des scripts npm :

{
"scripts": {
"transpile": "ngc",
"package": "rollup -c",
"minify": "uglifyjs dist/bundles/amazing.umd.js –screw-ie8 –compress –mangle –comments –output dist/bundles/amazing.umd.min.js",
"build": "npm run transpile && npm run package && npm run minify"
}
}
view raw package.json hosted with ❤ by GitHub

Ensuite :

npm run build
view raw npm.sh hosted with ❤ by GitHub

Notez que la transpilation n’est pas faite directement par TypeScript, vous devez utiliser le compileur Angular (ngc). Il s’agit de TypeScript, saupoudré d’un peu de magie Angular.

Publishing on npm

Ne publiez pas tout sur npm, seulement le répertoire dist.

Vous devez créer un fichier dist/package.json spécifique. Par exemple :

{
"name": "angular-amazing",
"version": "1.0.0",
"description": "An amazing module for Angular.",
"main": "bundles/amazing.umd.js",
"module": "index.js",
"typings": "index.d.ts",
"keywords": [
"angular",
"angular2",
"angular 2",
"angular4"
],
"author": "Your name",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/youraccount/angular-amazing.git"
},
"homepage": "https://github.com/youraccount/angular-amazing",
"bugs": {
"url": "https://github.com/youraccount/angular-amazing/issues"
},
"peerDependencies": {
"@angular/core": "^2.4.0 || ^4.0.0",
"rxjs": "^5.0.1"
}
}
view raw package.json hosted with ❤ by GitHub

Quelques points importants :

  • vous devez suivre le semantic versioning. Toute cassure de rétro-compatibilité (même s’il s’agit d’un changement minime) signifie une incrémentation du numéro majeur (le premier : 2.0.0). Et quand vous modifierez votre module pour rester à jour avec Angular, il s’agira d’une incrémentation du numéro mineur (le second : 1.1.0) ;
  • les chemins "main" et "module" sont nécessaires pour les imports utilisateurs, et celui des "typings" pour l’auto-complétion ;
  • "licence": "MIT" : une licence open-source est importante, ou votre module sera inutilisable par les autres. Angular utilise la licence MIT, vous devriez vous y tenir ;
  • les modules Angular que vous avez utilisés doivent être listés dans les peerDependencies. Continuez à suivre le semver, avec le signe ^, ou votre module deviendra obsolète à chaque mise à jour d’Angular. Pour les librairies encore en beta (zone.js…), vous pouvez voir les pré-requis actuels d’Angular ici.

N’oubliez pas d’écrire un README, avec la documentation de votre API. Autrement, votre module ne sert à rien. Vous pouvez utilisez une librairie comme copyfiles pour copier votre README depuis votre répertoire racine (celui qui sera utilisé sur Github) vers le répertoire dist (qui sera utilisé sur npm).

Avec un compte npm configuré, vous pouvez maintenant publier votre module :

cd dist
npm publish
view raw npm.sh hosted with ❤ by GitHub

Et à chaque fois que vous mettrez à jour votre module, relancez la compilation, modifiez le numéro de version, mettez à jour le changelog et publiez à nouveau.

Devenez un Pro!

Cet article vous a aidé ? Et vous voulez soutenir mes contributions open source (avec notamment un outil utilisé par 600 000 développeurs) ? Faites-vous une faveur à vous-même : Schematics Pro est un outil d’automatisation de code pour Angular, React, Vue et tous les autres frameworks JavaScript, qui vous fait gagner en productivité et vous aide à vous assurer du suivi des bonnes pratiques.

En savoir plus sur Schematics Pro

Découvrez ma formation Angular