Créer la structure de base

 
Sélectionnez

# Create a directory
$ mkdir sleep-sort
# cd into it
$ cd sleep-sort
# Initialize the Git repo
$ git init

Parfait, je viens d'initialiser un dépôt et je n'ai plus qu'à le remplir. J'aime beaucoup le concept de Readme Driven Development, dans cet esprit, je vais donc commencer par créer un fichier Readme.

 
Sélectionnez

# Create a Readme file with my favorite editor
$ vi Readme.md
$ cat Readme.md 
# Sleep Sort

`sleep-sort` works by creating a task for every number to be sorted. The task
will "sleep" n number of milliseconds and then push the number onto an array.
When all tasks are done, the array is returned sorted.

# Add all files to the repo
$ git add .
# Commit 
$ git commit -m 'Added Readme'

J'ai donc mis en place un dépôt local et fait un commit du fichier Readme. Maintenant, je dois créer le dépôt distant sur GitHub. Avec github gem, rien de plus facile.

 
Sélectionnez

# Create the remote repo at Github with the Github gem.
$ gh create-from-local
Counting objects: 3, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 359 bytes, done.
Total 3 (delta 0), reused 0 (delta 0)
To git@github.com:andersjanmyr/sleep-sort.git
 * [new branch]      master -> master

Le code ci-dessus a créé le dépôt distant, envoyé les modifications locales et affecté l'origine. Bien sûr, tout cela peut être fait manuellement, mais pourquoi se compliquer la tâche ?

Créer le module

Même si dans cet exemple le module sera relativement simple, je vais créer des répertoires pour y placer les différents fichiers que je vais utiliser.

 
Sélectionnez

# Create lib, test, and bin dirs
$ mkdir lib test bin

J'ai donc créé un répertoire lib pour les fichiers source, un répertoire test pour les tests et un répertoire bin pour la commande shell, sleep-sort.

Je crée maintenant les fichiers.

 
Sélectionnez

$ touch lib/main.js test/main.js
$ git add lib test
$ git commit -m 'Added initial files'

Très bien. Les répertoires et les fichiers existent, il est maintenant temps de construire le module. Pour cela, j'ai besoin d'un fichier package.json contenant les métadonnées concernant le module. Une façon simple pour faire cela est d'utiliser npm init.

 
Sélectionnez

$ npm init
Package name: (sleep-sort) 
Description: Implementation of the sleep-sort algorithm.
Package version: (0.0.0) 0.0.1
Project homepage: (none) 
Project git repository: (git://github.com/andersjanmyr/sleep-sort.git) 
Author name: Anders Janmyr
Author email: (none) anders@email.com
Author url: (none) http://anders.janmyr.com
Main module/entry point: (none) lib/main.js
Test command: (none) mocha test/*.js
What versions of node does it run on? (~0.6.14) 
About to write to /Users/andersjanmyr/Projects/sleep-sort/package.json

{
  "author": "Anders Janmyr <anders@email.com> (http://anders.janmyr.com)",
  "name": "sleep-sort",
  "description": "Implementation of the sleep-sort algorithm.",
  "version": "0.0.1",
  "repository": {
    "type": "git",
    "url": "git://github.com/andersjanmyr/sleep-sort.git"
  },
  "main": "lib/main.js",
  "scripts": {
    "test": "mocha test/*.js"
  },
  "engines": {
    "node": "~0.6.14"
  },
  "dependencies": {},
  "devDependencies": {}
}

Is this ok? (yes) yes

npm init devine certaines des informations, je n'ai donc pas besoin de les entrer moi-même. Une fois la commande exécutée, j'obtiens un fichier package.json à la racine du projet, je n'ai plus qu'à faire un commit.

 
Sélectionnez

$ git add package.json
$ git commit -m 'Initial package.json'

Notez que j'ai défini lib/main.js comme fichier principal. Ce sera le fichier demandé par Node lorsque j'inclurai le module plus tard. J'ai aussi ajouté le script de test mocha test/*.js, ce script pourra être appelé avec la commande npm test.

Mais pour que cela fonctionne, je dois ajouter mocha en tant que dépendance de développement dans package.json. Tant que j'y suis, je vais aussi ajouter la dépendance should.js.

 
Sélectionnez

//package.json
{
 ...
 "devDependencies": {
   // Use any version
   "mocha": "",
   "should": ""
 }
}

J'installe ces dépendances avec la commande npm install.

 
Sélectionnez

$ npm install
npm http GET https://registry.npmjs.org/should
npm http GET https://registry.npmjs.org/mocha
...
should@0.6.0 ./node_modules/should 
mocha@1.0.0 ./node_modules/mocha 
|-- growl@1.5.0
|-- debug@0.6.0
|-- commander@0.5.2
|-- diff@1.0.2
|-- jade@0.20.3

Je peux maintenant essayer la commande npm test et constater que ça ne fonctionne pas, puisque je n'ai pas encore de tests valides.

La commande npm install a créé un répertoire appelé node_modules contenant les dépendances. Je ne veux pas inclure ce répertoire au niveau du contrôle de version, j'ajoute donc un fichier .gitignore.

 
Sélectionnez

$ echo node_modules/ > .gitignore
$ git add package.json .gitignore
$ git commit -m 'Added and installed dependencies and ignored them.'

Un peu de code

Voilà déjà beaucoup de choses sans avoir écrit une seule ligne de code. C'est bien suffisant. Je vais utiliser la technique développement piloté par les tests (Test Driven Development) et commencer par écrire un test.

 
Sélectionnez

// test/main.js
var should = require('should');
var sleepsort = require('../lib/main');

describe('sleepsort', function() {
    describe('with no arguments', function() {
        it('returns an empty array', function() {
            var result = sleepsort();
            result.should.eql([]);
        });
    });
});

Si j'exécute ce code (npm test), je vais obtenir une erreur puisque je n'ai pas encore implémenté de module dans lib/main.js. C'est le bon moment de le faire. Puisque je vais exporter une seule fonction, sleepsort, depuis ce module, je vais le faire avec module.exports au lieu de exports.sleepsort.

 
Sélectionnez

function sleepsort() {
  return [];
}

module.exports = sleepsort;

Le code ci-dessus permet de passer le premier test. Notez que j'ai demandé directement la fonction avec require('sleepsort') dans le test. Cela fonctionne du fait de la méthode d'exportation décrite plus haut.

 
Sélectionnez

$ npm test
> sleep-sort@0.0.1 test /Users/andersjanmyr/Projects/sleep-sort
> mocha test/*.js
  .
    OK 1 tests complete (2ms)

# Add the files, and commit them.
$ git commit -a 'Added a first running test for sleepsort'

Maintenant que j'ai un test fonctionnel, il est temps de passer à l'intégration continue.

Intégration continue

La façon la plus simple de faire cela est d'utiliser Travis-CI. J'ai donc besoin de faire trois choses (voir une description plus détaillée).

  1. M'inscrire sur Travis avec mon compte GitHub.
  2. Aller sur la page profile de Travis.
    Trouver mon dépôt, sleep-sort et l'activer. Cela activera aussi le commit GitHub de Travis et lancera mon test à chaque envoi sur GitHub. Travis lancera la commande précisée dans package.json dans scripts/test avec npm test.
  3. Ajouter un fichier .travis.yml.

Créez le fichier .travis.yml comme ceci :

 
Sélectionnez

language: node_js
node_js:
  - 0.6
  - 0.7

J'indique à Travis de lancer les tests avec les deux versions 0.6 (stable) et 0.7 (instable). Puis faites un commit de ce fichier et un push sur GitHub.

 
Sélectionnez

$ git add .travis.yml
$ git commit -m 'Added .travis.yml to run agains node 0.6 and 0.7.'
$ git push

Vous ne tarderez pas à recevoir un e-mail vous indiquant que les tests ont réussi. Profitez de la beauté de Node, de Travis et de la vie !

Enfin, vous pouvez indiquer que vous êtes un citoyen responsable qui utilise l'intégration continue en ajoutant une image du statut Travis dans le fichier Readme. Elle ressemble à cela : Image non disponible

Le binaire

Désormais, tout est en place pour les tests et l'intégration continue. J'ai une fonction capable de trier un tableau vide (et instantanément en plus !) mais je n'ai pas encore de script pour l'utiliser. Je crée donc bin/sleep-sort.

 
Sélectionnez

#!/usr/bin/env node
var sleepsort = require('../lib/main');

var args = process.argv.slice(2);
console.log(sleepsort(args));

J'ai aussi besoin d'ajouter le script dans package.json comme binaire. J'ajoute donc la ligne suivante :

 
Sélectionnez

"bin": { "sleep-sort": "./bin/sleep-sort" },

que je rends exécutable, puis je teste, ajoute et commit.

 
Sélectionnez

$ chmod a+x bin/sleep-sort
$ bin/sleep-sort
[]
$ git add bin
$ git commit -m 'Added a binary to the module'

Le reste du code

Cela fait quand même beaucoup de travail pour une fonction uniquement capable de trier un tableau vide. De plus, il y a quelque chose de louche... Cette fonction me semble très synchrone pour tout dire, alors que Node est supposé être asynchrone !

Allez, je vais rectifier ça tout de suite.

D'abord, je modifie le test. Puisque le code va désormais être asynchrone, je vais utiliser quelques-unes des capacités de mocha. Si je transmets à la fonction de rappel de it un paramètre done, mocha va injecter une fonction que je pourrais appeler quand le test sera terminé.

 
Sélectionnez

describe('with an empty array argument', function() {
    it('calls the callback with an empty array', function(done) {
        var result = sleepsort([], function(result) {
            result.should.eql([]);
            done();
        });
    });
});

Si je n'avais pas appelé done, mocha aurait tourné en rond et le test aurait échoué. Simple, mais puissant. Le test échoue malgré tout, je mets donc le code à jour.

 
Sélectionnez

function sleepsort(array, callback) {
    return process.nextTick(function() {callback([]);});
}

process.nextTick indique à Node que je veux que la fonction soit appelée au prochain traitement de la pile d'événements. Le test réussi de nouveau et maintenant, le tri de mon tableau vide se fait de façon asynchrone. Merveilleux !

Je fais un commit puis un push et Travis vérifie cela puis m'envoie un e-mail m'indiquant que tout est parfait.

Il est temps d'ajouter un test supplémentaire avec un tableau contenant un élément.

 
Sélectionnez

describe('with a single element array', function() {
    it('calls the callback with a single element array', function(done) {
        var result = sleepsort([1], function(result) {
            result.should.eql([1]);
            done();
        });
    });
});

Le test échoue, mais je suis déjà prêt à le réparer. Voici la fonction qui passe le test :

 
Sélectionnez

function sleepsort(array, callback) {
    if (!array || array.length === 0)
        return process.nextTick(function() {callback([]);});
    var result = [];
    function appendResult(n) {
        return function() {
            result.push(n);
            callback(result);
        };
    }
    for(var i = 0; i < array.length; i++) 
        setTimeout(appendResult(array[i]), array[i]);
}

Regardons de plus près appendResult. Il s'agit d'une fonction qui crée une nouvelle fonction et la renvoie. La fonction anonyme créée est celle qui sera exécutée après le délai de setTimeout.

Ma fonction sait maintenant aussi trier un tableau d'une seule valeur. Voyons si elle peut en trier plus. Un nouveau test est nécessaire.

 
Sélectionnez

describe('with an unsorted two element array', function() {
    it('calls the callback with a sorted two element array', function(done) {
        var result = sleepsort([2, 1], function(result) {
            result.should.eql([1, 2]);
            done();
        });
    });
});

Le test échoue. Je dois m'assurer que la fonction de rappel n'est appelée qu'une fois le résultat disponible. Comment savoir que l'exécution est terminée ? Lorsque le tableau du résultat a la même taille que le tableau initial. Voici la fonction finale.

 
Sélectionnez

function sleepsort(array, callback) {
    if (!array || array.length === 0)
        return process.nextTick(function() {callback([]);});
    var result = [];
    function appendResult(n) {
        return function() {
            result.push(n);
            if (array.length === result.length) 
                callback(result);
        };
    }
    for(var i = 0; i < array.length; i++) 
        setTimeout(appendResult(array[i]), array[i]);
}

Tout ce qu'il me reste à faire est de mettre à jour le script pour utiliser le mode asynchrone. C'est assez simple, je définis juste console.log comme fonction de rappel de sleepsort.

 
Sélectionnez

#!/usr/bin/env node
// bin/sleep-sort
var sleepsort = require('../lib/main');

var args = process.argv.slice(2);
sleepsort(args, console.log);

Et voilà ! Mon programme fonctionne. J'ai un algorithme fonctionnel de tri de complexité linéaire.

Évidemment, je ne peux pas garder ce puissant algorithme pour moi tout seul, il faut que je le publie afin que le monde puisse en prendre connaissance ! Comment faire ? Avec npm bien sûr.

Publier le module

Avant de le publier, je le teste en l'installant localement.

 
Sélectionnez

$ npm install . # from the project dir
$ sleep-sort 5 3 7 9 1
[1,3,5,7,9]

Il fonctionne sur mon poste. Peut-être pas sur tous en fonction des variables d'environnement. Si cela ne fonctionne pas, essayez une installation globale avec npm install -g . puis réessayez.

Une fois que cela fonctionne, c'est le moment de le publier. Si c'est votre première publication avec npm, vous devez d'abord vous ajouter comme utilisateur.

 
Sélectionnez

$ npm adduser
...

Avant de publier, je précise la version 1.0.0 dans package.js pour spécifier que le module est utilisable en production.

 
Sélectionnez

$ npm publish .
npm http PUT https://registry.npmjs.org/sleep-sort
npm http 409 https://registry.npmjs.org/sleep-sort
npm http GET https://registry.npmjs.org/sleep-sort
npm http 200 https://registry.npmjs.org/sleep-sort
npm http PUT https://registry.npmjs.org/sleep-sort/1.0.1/-tag/latest
npm http 201 https://registry.npmjs.org/sleep-sort/1.0.1/-tag/latest
npm http GET https://registry.npmjs.org/sleep-sort
npm http 200 https://registry.npmjs.org/sleep-sort
npm http PUT https://registry.npmjs.org/sleep-sort/-/sleep-sort-1.0.1.tgz/-rev/4-9dcdcb2bab4176ca5aad6f13c479994e
npm http 201 https://registry.npmjs.org/sleep-sort/-/sleep-sort-1.0.1.tgz/-rev/4-9dcdcb2bab4176ca5aad6f13c479994e
+ sleep-sort@1.0.1

C'en est terminé. Le code source est disponible sur GitHub (ou vous pouvez télécharger l'archive) si vous voulez vous amuser un peu avec.

Remerciements

Cet article a été traduit avec l'aimable autorisation de Anders Janmyr. L'article original : Writing a Node Module peut être vu sur le site The Tapir's Tale.

Nous tenons à remercier ClaudeLELOUP pour sa relecture attentive de cet article.