Créer un CLI Node.js distribuable comme un exécutable
Un outil en ligne de commande peut accélérer une tâche répétitive, standardiser un workflow ou rendre un service accessible sans interface graphique. Avec Node.js, quelques dépendances suffisent pour transformer un script en véritable CLI, doté d’options lisibles, d’une aide intégrée et de codes de sortie cohérents.
Commander.js simplifie la déclaration des commandes et le traitement des arguments. La bibliothèque prend en charge l’affichage de l’aide, les options booléennes, les paramètres obligatoires et les sous-commandes. Elle permet donc de se concentrer sur la logique métier plutôt que sur l’analyse manuelle de process.argv.
Un projet Node.js reste toutefois dépendant de la présence de Node sur la machine cible. Pour distribuer un outil à des utilisateurs qui ne souhaitent pas installer l’environnement d’exécution, pkg peut empaqueter l’application et le runtime dans un binaire autonome pour plusieurs systèmes.
L’exemple présenté ici construit un générateur de fichiers JSON. Il montre comment organiser le code, gérer les erreurs, préparer les ressources incluses dans le paquet puis publier des exécutables adaptés à Linux, macOS et Windows.
Poser les bases du projet
Commencez par créer un dossier et initialisez un paquet npm. Commander.js sera utilisé comme dépendance de production, tandis que pkg peut rester une dépendance de développement, car il sert principalement au processus de compilation.
mkdir cli-donnees
cd cli-donnees
npm init -y
npm install commander
npm install --save-dev pkg
Le fichier package.json doit déclarer un point d’entrée et un nom de commande. Le champ bin indique à npm quel fichier exécuter lorsque le paquet est installé globalement ou utilisé comme outil local.
{
"name": "cli-donnees",
"version": "1.0.0",
"description": "Générateur de fichiers JSON en ligne de commande",
"main": "src/index.js",
"bin": {
"donnees": "src/index.js"
},
"scripts": {
"start": "node src/index.js",
"build": "pkg . --targets node20-linux-x64,node20-macos-x64,node20-win-x64 --output dist/donnees"
},
"dependencies": {
"commander": "^12.1.0"
},
"devDependencies": {
"pkg": "^5.8.1"
}
}
Ajoutez ensuite le shebang au début de src/index.js afin que le système puisse exécuter directement le fichier lorsque le paquet est installé comme commande globale. La version de Node ciblée doit rester cohérente avec les fonctionnalités utilisées par l’application et avec la version prise en charge par l’outil d’empaquetage.
Déclarer les commandes avec Commander.js
Commander.js fournit une API fluide pour décrire l’interface du programme. Une option courte, une valeur par défaut et une validation élémentaire suffisent à créer une expérience claire pour l’utilisateur.
#!/usr/bin/env node
const { Command } = require('commander');
const fs = require('node:fs/promises');
const path = require('node:path');
const program = new Command();
program
.name('donnees')
.description('Génère un fichier JSON de données de démonstration')
.version('1.0.0');
program
.command('generer')
.description('Crée un fichier JSON')
.option('-n, --nombre <entier>', 'nombre d’éléments', '10')
.option('-o, --sortie <fichier>', 'chemin du fichier', 'data.json')
.action(async (options) => {
const nombre = Number.parseInt(options.nombre, 10);
if (!Number.isInteger(nombre) || nombre < 1) {
throw new Error('Le nombre doit être un entier positif.');
}
const donnees = Array.from({ length: nombre }, (_, index) => ({
id: index + 1,
nom: `Utilisateur ${index + 1}`,
email: `utilisateur${index + 1}@exemple.fr`
}));
const destination = path.resolve(options.sortie);
await fs.writeFile(destination, JSON.stringify(donnees, null, 2));
console.log(`${nombre} éléments écrits dans ${destination}`);
});
program.parseAsync().catch((erreur) => {
console.error(`Erreur : ${erreur.message}`);
process.exitCode = 1;
});
L’appel parseAsync() est important dès qu’une action utilise des fonctions asynchrones. Il permet de capturer les rejets de promesse dans un seul gestionnaire d’erreur. Le code de sortie 1 signale l’échec au shell, à un script d’intégration continue ou à un pipeline automatisé.
Le modèle peut ensuite accueillir des sous-commandes comme importer, exporter ou configurer. Pour un générateur plus réaliste, une bibliothèque de données fictives peut remplacer les chaînes statiques ; cet exemple de générateur de données factices constitue une base utile pour enrichir les objets produits.
Tester l’expérience utilisateur
Avant de fabriquer un exécutable, vérifiez les différents chemins d’utilisation depuis le terminal. L’aide intégrée doit expliquer le rôle de la commande, ses options et les valeurs attendues sans obliger l’utilisateur à consulter le code source.
node src/index.js --help
node src/index.js generer --help
node src/index.js generer --nombre 25 --sortie ./tmp/utilisateurs.json
node src/index.js generer --nombre abc
La validation ne doit pas se limiter aux types. Contrôlez aussi les valeurs négatives, les chemins impossibles à écrire et les fichiers déjà présents si leur remplacement n’est pas souhaité. Une interface prévisible réduit les erreurs dans les scripts et facilite le diagnostic sur une machine distante.
Vous pouvez ajouter des tests automatisés autour de la fonction de génération en séparant celle-ci de la définition Commander. Cette séparation évite de lancer le parseur au moment de l’import et rend la logique métier testable avec Node.js, Vitest ou Jest.
Empaqueter l’application avec pkg
Une fois l’outil fonctionnel, pkg analyse le point d’entrée et rassemble le code JavaScript avec un runtime Node.js. La commande suivante produit trois fichiers dans dist, selon les cibles déclarées dans le script npm.
npm run build
Les noms de cibles suivent généralement le format nodeVERSION-plateforme-architecture. Les plateformes les plus courantes sont linux, macos et win, avec des architectures comme x64 ou arm64. La disponibilité exacte dépend de la version de Node et de celle de pkg; il est prudent de tester les binaires générés sur les systèmes réellement visés.
Les fichiers externes exigent une attention particulière. Un modèle, une configuration ou un répertoire de ressources ne sera pas forcément détecté automatiquement. Dans ce cas, déclarez-le explicitement dans package.json, puis utilisez un chemin compatible avec l’exécution empaquetée.
{
"pkg": {
"assets": [
"templates/**/*",
"config/*.json"
]
}
}
Les modules natifs qui compilent du code, les imports construits dynamiquement et certaines dépendances qui calculent leurs chemins à l’exécution peuvent aussi poser problème. Une compilation réussie ne garantit donc pas que chaque fonctionnalité fonctionnera dans le binaire final.
Publier un binaire fiable
La distribution peut se faire dans une archive contenant le fichier exécutable, un exemple de commande et une courte documentation. Vous pouvez également publier les artefacts dans une version GitHub Release, un registre interne ou un gestionnaire de paquets adapté à votre organisation.
Avant publication, testez chaque binaire dans un environnement propre qui ne possède pas Node.js. Vérifiez la création des fichiers, les permissions, les messages d’erreur, les chemins relatifs et le comportement avec des arguments inattendus. Cette étape révèle les dépendances implicites qui restent invisibles sur la machine de développement.
Dépendances à maîtriser
- Commander.js pour les commandes, options et sous-commandes
fs/promisespour les opérations de fichiers asynchronespkgpour produire les exécutables autonomes- npm pour les scripts, versions et métadonnées
Vérifications avant livraison
- Tester l’aide et la version sur chaque plateforme
- Valider les codes de sortie en cas d’erreur
- Contrôler les ressources déclarées dans
pkg.assets - Exécuter les binaires sans installation préalable de Node.js
Les versions de Node, Commander.js et pkg doivent être figées autant que possible. Un fichier de verrouillage, une construction exécutée dans un environnement reproductible et un nommage explicite des artefacts facilitent les retours arrière. Pour signaler un problème de compatibilité ou partager un cas d’usage, vous pouvez contacter la rédaction.
Un CLI destiné à plusieurs équipes mérite aussi une stratégie de versionnement sémantique. Les changements d’options, de format de sortie ou de codes d’erreur peuvent casser des scripts existants ; documentez-les dans un journal des modifications et conservez une période de compatibilité lorsque cela est nécessaire.
Créez maintenant le dossier du projet, ajoutez la commande generer, puis compilez un premier binaire dans dist. En partant de ce socle, vous pourrez intégrer une configuration utilisateur, des sous-commandes spécialisées et une publication automatisée dans votre pipeline CI/CD.