Suite à la toute récente sortie d’Angular 6 (mai 2018), cette nouvelle version intègre désormais TypeScript 2.7 et RxJS 6. Les principaux ‘breaking changes’ proviennent de RxJS. Nous allons présenter rapidement les évolutions apportées, puis montrer comment migrer simplement son code à l’aide de rxjs-tslint, et voir comment sont définies ces règles TSLint à travers cet outil bien pratique.
Nouveautés RxJS 6
L’objectif est de montrer principalement les nouveautés qui entraînent une modification de syntaxe de notre code.
- La définition des Imports et Operators a été simplifiée :
import { Observable, Subject, pipe } from 'rxjs';
import { map, takeUntil, tap } from 'rxjs/operators';
Ce changement de structure de package permet d’importer que ce qui est nécessaire afin d’avoir des bundles plus légers, notamment lors du tree-shaking d’Angular (mécanisme de suppression du code inutile).
- Le chaînage des opérateurs doit se faire à l’aide de la fonction
pipe()
:
myObs
.pipe(
tap(console.log),
map(x => x * 2)
)
.subscribe(x => {
console.log('Value is', x);
});
L’opérateur pipe est apparu avec RxJS 5.5. Il permet de chaîner des “lettable operators”. Ce sont des fonctions pures qui prennent en entrée et retournent un observable. L’intérêt de ces fonctions est qu’elles bénéficient du tree-shaking. Tandis que si l’on utilise directement des opérateurs d’Observable, on se voit importer Observable et toutes ses dépendances.
- Quelques opérateurs ont été renommés, (afin d'éviter les confusions avec les mots clés JavaScript) :
RxJS 5.5 | RxJS 6 |
---|---|
do | -> tap |
catch | -> catchError |
switch | -> switchAll |
finally | -> finalize |
throw | -> throwError |
- Rétrocompatibilité avec
rxjs-compat
.
rxjs-compat
est une librairie qui peut être installée pour garder une rétrocompatibilité avec l’ancienne syntaxe, le temps de mettre à jour notre code. Elle est présente par défaut, lors de la génération d’un nouveau projet avec Angular CLI 6.0.0.
npm i --save rxjs-compat
Nous allons voir comment nous passer de rxjs-compat à l’aide d’un outil nous facilitant la migration vers RXJS 6.
Présentation de rxjs-tslint
rxjs-tslint
est un outil qui permet de migrer son code RxJS 5 en version 6. Il s’appuie sur TSLint qui permet de vérifier, mais aussi de modifier la syntaxe du code.
Pour cela rien de plus simple, comme le décrit la documentation :
Installer RxJS TSLint :
npm i -g rxjs-tslint
Migrer son code à l’aide de la commande :
rxjs-5-to-6-migrate -p src/tsconfig.app.json
Mais il est aussi possible de déclarer nos règles dans le fichier tsconfig.conf de la manière suivante, afin de visualiser les erreurs directement dans notre IDE. Ce dernier pourra proposer de corriger l’erreur.
{
"rulesDirectory": [
"node_modules/rxjs-tslint"
],
"rules": {
"rxjs-collapse-imports": true,
"rxjs-pipeable-operators-only": true,
"rxjs-no-static-observable-methods": true,
"rxjs-proper-imports": true
}
}
Ou de lancer manuellement TSLint en ligne de commande.
./node_modules/.bin/tslint -c tslint.json -p tsconfig.json
Cet outil comprend 4 règles qui vont permettre de migrer le code
Rule | Description |
---|---|
rxjs-collapse-imports | Détecte et fusionne les imports multiples de rxjs sur une seule ligne d’import. |
rxjs-pipeable-operators-only | Détecte les chaînes d’observable et utilise l’opérateur pipe pour chaîner par des lettable operators. |
rxjs-no-static-observable-methods | Supprime les imports statiques des méthodes de la classe Observable. |
rxjs-proper-imports | Mise à jour des imports de RxJS 5.x.vers RxJS 6.0 |
Voici un exemple de pull request de migration sur une application simple en Angular 5, en utilisant l’outil de migration rxjs-tslint. On peut y comparer les modifications apportées : la mise à jour des imports, l’utilisation de pipe, la suppression des imports statiques.
https://github.com/sfoubert/angular5-example-app/commit/837678e29d69d90ff6810c87cf194be434a9ebdc
Note : Il a toutefois été nécessaire de retoucher légèrement certaines parties du code (car quelques issues subsistent). Ceci afin de pouvoir lancer l’application sans régression, en profitant au passage de migrer à Angular 6.
Nous allons voir désormais comment écrire nos propres régles à la manière de rxjs-tslint
.
Définition de rules customisés pour TSLint
TSLint fournit des règles préconfigurées et disponibles par défaut. Mais il est possible de créer ses propres règles pour des besoins plus spécifiques, comme cela a été fait avec rxjs-tslint.
- Par convention une règle doit être définie dans un fichier se terminant par Rule (ex : sampleNoImportRule.ts en camel case) ;
- Par défaut elle aura donc pour identifiant sample-no-import en kebab case ;
- Il est possible de surcharger les metadatas par défaut ;
- La classe exportée Rule doit étendre la classe
Lint.Rules.AbstractRule
.
La première solution la plus simple pour parcourir les fichiers sources et leur contenu s’appuie sur le pattern “visiteur”.
Voici un exemple très simple d’utilisation en Typescript qui permet de détecter l’usage d’import comme une erreur :
import * as ts from "typescript";
import * as Lint from "tslint";
export class Rule extends Lint.Rules.AbstractRule {
public static FAILURE_STRING = "import statement forbidden";
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(new NoImportsWalker(sourceFile, this.getOptions()));
}
}
class NoImportsWalker extends Lint.RuleWalker {
public visitImportDeclaration(node: ts.ImportDeclaration) {
// create a fixer for this failure
const fix = new Lint.Replacement(node.getStart(), node.getWidth(), "");
// create a failure at the current position
this.addFailureAt(node.getStart(), node.getWidth(), Rule.FAILURE_STRING, fix);
// call the base version of this visitor to actually parse this node
super.visitImportDeclaration(node);
}
}
La méthode applyWithWalker
, comme son nom l’indique, applique la classe Walker, qui étend Lint.RuleWalker
et qui va parcourir chaque noeud du fichier source sans le modifier en utilisant AST.
AST (Abstract Syntax Tree) permet de décrire le contenu d’un fichier source sous forme d’arborescence. Ainsi un composant Angular sera représenté de la manière suivante :
ImportDeclaration
correspond à la ligne d’import. NamedImports
, aux noms des composants importés. Ou encore ClassDeclaration > Identifier correspond au nom de la classe, etc.
Pour revenir à notre exemple de règle, on surcharge la méthode visitImportDeclaration
, mais de nombreuses méthodes de type visit…
sont fournies (cf. syntaxWalker.ts). Voici un second exemple qui interdit l’usage du mot console
en parcourant chaque expression du code source à l’aide de visitExpressionStatement
.
class NoConsoleWalker extends Lint.RuleWalker {
public visitExpressionStatement(node: ts.ExpressionStatement) {
if (node.getText().indexOf("console") > -1) {
// create a failure at the current position
this.addFailureAt(node.getStart(), node.getWidth(), Rule.FAILURE_STRING);
}
super.visitExpressionStatement(node);
}
}
On peut ajouter notre propre message d’erreur et définir sa position à l’aide de addFailureAt
, mais aussi écrire un fix à l’aide Lint.Replacement
, pour définir comment rectifier l’erreur lint, lors du lancement de tslint avec l’option --fix
.
Pour ajouter ces règles à un projet, il suffit, comme pour rxjs-tslint
, de déclarer le répertoire contenant celles-ci et la clé de chacune d’elles à activer, dans le fichier tslint.conf
:
{
"rulesDirectory": [
"node_modules/codelyzer",
"node_modules/sample-tslint"
],
...
"sample-no-import": false,
"sample-no-console": true,
"sample-exclude-commons": true,
}
}
Ainsi, les erreurs sont détectées dans notre IDE, ou en ligne de commande avec tslint.
La seconde méthode pour parser les fichiers sources, plus bas niveau mais plus verbeuse, consiste à utiliser la méthode applyWithFunction
qui permet de passer en paramètre une fonction (walk
) qu’il faudra développer. Cette dernière va parcourir les fichiers sources et chaque noeud de chaque fichier de manière récursive.
La librairie tsutils
fournit des méthodes utilitaires qui vont permettre de parser le fichier source avec AST.
const FAILURE_STRING = 'commons library is deprecated';
export class Rule extends Lint.Rules.AbstractRule {
static metadata: Lint.IRuleMetadata = {
ruleName: 'sample-exclude-commons',
description: `exclude commons library`,
rationale: '',
options: null,
optionsDescription: '',
type: 'style',
typescriptOnly: true
};
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk);
}
}
const COMMONS_IMPORTS = '@atlas/commons';
function walk(ctx: Lint.WalkContext<void>) {
for (const statement of ctx.sourceFile.statements) {
if (!tsutils.isImportDeclaration(statement)) {
continue;
}
if (!statement.importClause) {
continue;
}
if (!statement.importClause.namedBindings) {
continue;
}
if (!tsutils.isNamedImports(statement.importClause.namedBindings)) {
continue;
}
if (!tsutils.isLiteralExpression(statement.moduleSpecifier)) {
continue;
}
const moduleSpecifier = statement.moduleSpecifier.text;
if (moduleSpecifier.startsWith(COMMONS_IMPORTS)) {
const fix = Lint.Replacement.deleteFromTo(statement.getStart(), statement.getEnd());
ctx.addFailureAtNode(statement, FAILURE_STRING, fix);
continue;
}
}
}
Bien sûr, les règles de RxJS TSLint sont plus complexes que ces exemples. Vous pouvez aller jeter un oeil sur les sources sur GitHub.
Conclusion
RxJS TSLint est un outil pratique qui s’appuie sur la définition de règles TSLint customisées, et qui va permettre de migrer en une ligne de commande notre code vers RxJS 6. Quelques bugs persistent à la génération et quelques corrections manuelles doivent être appliquées. Toutefois, dans le cadre d’un projet Angular avec un nombre important de composants, il permet de gagner un temps considérable. Je vous encourage vivement à l’utiliser si vous n’êtes pas encore passé à Angular 6.
Bibliographie
- Migration RxJS6
https://github.com/ReactiveX/rxjs/blob/master/MIGRATION.md - Pipeable Operators
https://github.com/ReactiveX/rxjs/blob/91088dae1df097be2370c73300ffa11b27fd0100/doc/pipeable-operators.md - RxJS TsLint
https://github.com/ReactiveX/rxjs-tslint - Développer des Rules pour TSLint
https://palantir.github.io/tslint/develop/custom-rules/
https://medium.com/@rokerkony/prevent-coding-mistakes-write-a-tslint-rule-specific-for-your-own-project-987d26f91647
https://spin.atomicobject.com/2018/01/04/create-tslint-rules/