Créer une CLI pour un projet modulaire avec Commander.js
Relu par : Emmanuelle Gouvart | Maxime Deroullers | Edouard Cattez
Dragee.io est un nouveau projet lié à l'architecture logicielle, permettant entre autres l'analyse d'une architecture dans la vision Craft portée par HoppR. Nos ambitions pour ce projet sont fortes, celui-ci se voulant une solution complète et constituée de nombreuses fonctionnalités. Nous aurons d’ailleurs d’autres occasions de vous partager son avancée.
Les fonctionnalités allant être ajoutées de manière itérative, nous avons étudié la meilleure façon de faire grandir le projet. Nous sommes donc partis sur une CLI (Command-Line Interface) présentant des commandes simples à l’utilisateur. Elle sera le cœur de notre solution, auquel viendront se greffer les futurs nouveaux modules.
Dans cet article, je vais vous présenter notre manière d’intégrer ces modules dans l’écosystème Dragee.io, en créant des commandes qui leurs sont propres, et en les ajoutant à celles déjà existantes dans la CLI centrale.
Présentation de Commander.js
Tout d’abord, nous avons besoin d’un outil pour développer cette CLI.
Nous avons pour cela choisi le projet Commander.js, qui permet d’en créer une facilement, sur des environnements d’exécution JS/TS tels que Node.js, ou, dans le cas du projet Dragee.io, Bun.
Cela nous permet de concevoir Dragee.io avec une stack où nos développeurs, principalement issus du monde du web, seront à l’aise, plutôt que d’investir dans une solution non maîtrisée (par exemple Go) ou pas encore assez mature.
Un package npm étant disponible, nous allons ici pouvoir installer Commander.js en dépendance de nos projets :
bun add commander
Commander.js va nous permettre de gérer le parsing des arguments, de traiter les cas d’erreurs et d’afficher une documentation (description, aide, etc.) pour chaque commande. Cet outil contient beaucoup d’options de configuration, nous resterons ici sur une utilisation simple.
Création d’une commande
Dans la galaxie des projets Dragee.io, nous disposons donc d’un projet central, celui exposant cette fameuse CLI, le bien nommé dragee-cli.
Il contient de base deux commandes pour nos besoins fonctionnels : une pour générer des rapports (nommée report), l’autre pour générer des modélisations d’architecture (draw). Écrivons la première, et plaçons-là dans notre index.ts.
import { Command } from 'commander';
import { reportCommandhandler } from './src/commands/report-command.handler.ts';
const report = new Command('report')
.alias('r')
.summary('builds asserters rules report')
.description(
'Builds asserters rules report.\n' +
'- Lookups dragees in [--from-dir] directory\n' +
'- Downloads asserters for dragees namespaces\n' +
'- Executes rules from asserters\n' +
'- Builds reports in [--to-dir] directory'
)
.option('--from, --from-dir <path-to-dir>', 'directory in where to lookup for dragees', '.')
.option('--to --to-dir <path-to-dir>', 'directory in where to store reports', './dragee/reports')
.action(reportCommandhandler);
Cette commande contient un certain nombre d’informations (description, options), et surtout l’action à réaliser, une fonction passée en paramètre, ici nommée reportCommandhandler.
La bonne pratique est ici de l’importer d’un fichier à part. En effet, la commande est à la CLI ce que le contrôleur est à nos applications web. L’action est équivalente à un service métier, placée ailleurs et appelée par notre commande, dont seule la définition est décrite dans l’index.
Nous allons ensuite, sur le même modèle, créer la commande draw. Pour finaliser notre CLI, nous devons également créer la commande de plus haut niveau, incorporant ces deux commandes :
new Command()
.addCommand(report)
.addCommand(draw)
.showHelpAfterError()
.showSuggestionAfterError()
.parse(process.argv);
L’exécutable de Dragee.io est ensuite construit à l’aide d’une commande native de Bun.
> bun run build
$ bun build index.ts --target bun --compile --outfile dist/dragee-cli
[237ms] bundle 151 modules
[51ms] compile dist/dragee-cli.exe
Et voici ce que tout cela donne, avec les options de documentation (help) sur la commande parent, puis sur la commande draw :
> ./dragee-cli --help
Usage: index [options] [command]
Options:
-h, --help display help for command
Commands:
report|r [options] builds asserters rules report
draw|d [options] builds graphers graphs models
help [command] display help for command
> ./dragee-cli report --help
Usage: index report|r [options]
Builds asserters rules report.
- Lookups dragees in [--from-dir] directory
- Downloads asserters for dragees namespaces
- Executes rules from asserters
- Builds reports in [--to-dir] directory
Options:
--from, --from-dir <path-to-dir> directory in where to lookup for dragees (default: ".")
--to --to-dir <path-to-dir> directory in where to store reports (default: "./dragee/reports")
-h, --help display help for command
Ce qui donne à l’usage :
> ./dragee-cli report --from-dir ./test/approval/sample/ --to-dir ./output
Looking up for dragees in directory: ./test/approval/sample/
Looking up for namespaces
Looking up for projects
...
Architecture modulaire
Module enfant
Maintenant que notre projet CLI est créé et fonctionnel, nous allons nous attaquer à un nouveau module, dragee-asserter-generator. Celui-ci a pour mission de générer un squelette d’asserter pour le projet Dragee.io, comme ddd-asserter par exemple.
Après ajout de la dépendance Commander à ce second projet, on crée la commande nécessaire dans dragee-asserter-generator :
import { Command } from 'commander';
export const generateAsserter = new Command('generate-asserter')
.alias('ga')
.summary('generates a new asserter project')
.description('Generates a new asserter project.\nBased on a standard dragee asserter template, with mandatory dependancies and a sample rule.')
.requiredOption('-n, --name <string>', 'name of the new asserter project')
.requiredOption('-od, --output-dir <string>', 'output dir for the new asserter project')
.action(generatorHandler);
Notez ici que l’on exporte la commande generate-asserter. Cela va nous permettre de la rendre accessible pour dragee-cli, et c’est justement ce que nous allons faire à présent.
Module parent
Nous allons ajouter la dépendance à dragee-asserter-generator dans dragee-cli. Nous ferons ainsi pour chaque autre module à rattacher à notre CLI, comme dragee-grapher-generator par exemple.
Une fois cela fait, nous ajoutons tout simplement ces commandes au CLI central, après import :
import { generateAsserter } from '@dragee-io/asserter-generator';
import { generateGrapher } from '@dragee-io/grapher-generator';
...
new Command()
.addCommand(generateAsserter)
.addCommand(generateGrapher)
.addCommand(report)
.addCommand(draw)
.showHelpAfterError()
.showSuggestionAfterError()
.parse(process.argv);
Et voilà !
> ./dragee-cli --help
Usage: index [options] [command]
Options:
-h, --help display help for command
Commands:
generate-asserter|ga [options] generates a new asserter project
generate-grapher|gg [options] generates a new grapher project
report|r [options] builds asserters rules report
draw|d [options] builds graphers graphs models
help [command] display help for command
Conclusion
La CLI comme cœur de notre système va nous permettre d’accroître les capacités de Dragee.io au fur et à mesure. La modularité alliée à Commander.js nous permet de développer, tester et valider indépendamment chaque fonctionnalité. L’intégration des commandes dans la CLI centrale est simple, comme nous l’avons vu dans cet article. Enfin, le runtime de Bun nous permet une exécution rapide des commandes (démarrage 4 fois plus rapide que Node.js par exemple).
N’hésitez pas à consulter les autres articles en lien avec Dragee.io, pour découvrir d’autres solutions techniques à des problématiques posées lors de son développement, et voir avec nous ce beau projet grandir.
Cet article vous a inspiré ?
Vous avez des questions ou des défis techniques suite à cet article ? Nos experts sont là pour vous aider à trouver des solutions concrètes à vos problématiques.