# Solid Grunt ## Du code spaghetti au code béton 23 novembre 2013 — [FranceJS.in(Toulouse)](http://lanyrd.com/2013/francejs/)
![](../../img/avatar.jpg) ## Thomas Parisot Frontend / JavaScript Engineer [![BBC R&D](../../img/RD-logo_500.png)](http://www.bbc.co.uk/rd)
[thom4.net](https://thom4.net) – [@thom4parisot](https://twitter.com/thom4parisot) – [github.com/thom4parisot](https://github.com/thom4parisot) @@@ @@@ @@@ @@@
## Apprentissages @@@ ### J'aime les ~~`while()`~~ boucles. @@@ ### J'aime les ~~`pattern`~~ répétitions. @@@ ### J'aime les ~~`border-radius`~~ lignes et courbes. @@@ ### Photographie Elle s'est nourrie de mon code. Elle a nourri mon code.
## ![BBC News](images/bbc-news.jpeg) @@@ ## Contexte - 2 semaines dans le même lieu - aucune connaissance du workflow - aucune connaissance du code @@@ ## Volonté - apprendre d’une équipe réputée - livrer en production - ne rien casser - nouer des liens
## Pull Request #1 Dépendances frontend versionnées. -20K lignes de code.
## Pull Request #2 ~~doublon~~ + rationalisation. -10K lignes de code.
## Pull Request #3 @@@ ```javascript grunt.initConfig({ sass: { /* content dynamically generated */ }, generateJS: { dest: '<%= dir.static_js %>/module/translations/', src: 'https://docs.google.com/spreadsheet/pub?key=xxx&output=csv' }, generateINI: { dest: './core/src/main/BBC/News/Translation/', src: 'https://docs.google.com/spreadsheet/pub?key=xxx&output=csv' } }); grunt.registerTask('default', ['jshint', 'sass_compile:dev']); ``` @@@ ## Analyse - 17 tâches maison - des “surtâches” - des potentiels doublons pour “faire juste ce qu’on veut” - des configurations implicites @@@ ## Bref ### *Envie de tout réécrire* Syndrôme classique ;-) @@@ ## Objectifs ### *Challenger* l’idée par l’équipe ### Ouvrir la PR le plus tôt possible Permet de susciter des remarques des principaux intéressés. ### Améliorer la qualité Et éduquer par l’exemple.
## Pull Request #4 Automatiser les fichiers de traduction. @@@ ```javascript grunt.registerTask('generateJS', 'Generate JS translation file for chosen language.', function(language) { function normaliseInput(string) { var length = string.length, i = 0, newString = ""; while (i < length) { if (i === 0) { newString += string.charAt(i).toUpperCase(); } else { newString += string.charAt(i).toLowerCase() } i++ } return newString; } var language = language || 'English'; language = normaliseInput(language); var csv = grunt.file.read('./webapp/static/js/module/translations/variables.csv'); // This will parse a delimited string into an array of // arrays. The default delimiter is the comma, but this // can be overriden in the second argument. function CSVToArray( strData, strDelimiter ){ // Check to see if the delimiter is defined. If not, // then default to comma. strDelimiter = (strDelimiter || ","); // Create a regular expression to parse the CSV values. var objPattern = new RegExp( ( // Delimiters. "(\\" + strDelimiter + "|\\r?\\n|\\r|^)" + // Quoted fields. "(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" + // Standard fields. "([^\"\\" + strDelimiter + "\\r\\n]*))" ), "gi" ); // Create an array to hold our data. Give the array // a default empty first row. var arrData = [[]]; // Create an array to hold our individual pattern // matching groups. var arrMatches = null; // Keep looping over the regular expression matches // until we can no longer find a match. while (arrMatches = objPattern.exec( strData )){ // Get the delimiter that was found. var strMatchedDelimiter = arrMatches[ 1 ]; // Check to see if the given delimiter has a length // (is not the start of string) and if it matches // field delimiter. If id does not, then we know // that this delimiter is a row delimiter. if ( strMatchedDelimiter.length && (strMatchedDelimiter != strDelimiter) ){ // Since we have reached a new row of data, // add an empty row to our data array. arrData.push( [] ); } // Now that we have our delimiter out of the way, // let's check to see which kind of value we // captured (quoted or unquoted). if (arrMatches[ 2 ]){ // We found a quoted value. When we capture // this value, unescape any double quotes. var strMatchedValue = arrMatches[ 2 ].replace( new RegExp( "\"\"", "g" ), "\"" ); } else { // We found a non-quoted value. var strMatchedValue = arrMatches[ 3 ]; } // Now that we have our value string, let's add // it to the data array. arrData[ arrData.length - 1 ].push( strMatchedValue ); } // Return the parsed data. return( arrData ); } var array = CSVToArray(csv); var propertyNames = array[0]; var length = array.length, i = 0; var languageWanted; // locate the language wanted for (i ; i < length; i++) { var currentLanguage = array[i][1]; if (currentLanguage == language) { languageWanted = array[i]; break; } } //create the file needed for /define/'language'.js var languageString = 'define({'; var fourSpaces = " "; length = propertyNames.length; //make i = 2 to skip language.code and language.name being saved into the file i = 2; for (i ; i < length; i++) { languageString += "\n"; languageString += fourSpaces + propertyNames[i] + " : '" + languageWanted[i] + "'"; if (i + 1 !== length) { languageString += ","; } else { languageString += "\n"; } } languageString += "});"; var languageCode = languageWanted[0]; grunt.file.write('./webapp/static/js/module/translations/' + languageCode + '.js', languageString); grunt.file.delete('./webapp/static/js/module/translations/variables.csv'); }); ``` @@@ ## Le cas typique Et le MVP idéal. @@@ ## Réinvente la roue Parsing CSV. @@@ ## Test par l'exécution `assert(user.rant("ça marche pas"))` @@@ ## Manque de clarté du code Lecture linéaire. Code peu expressif quant à ses choix/opinions. @@@ ## Pourquoi ? Manque d'expérience / temps à consacrer. Peu de littérature à ce sujet. Mimétisme.
## Structurer une tâche Grunt @@@ ### Configuration + ### Orchestration + ### Librairie + ### Documentation @@@ ## Configuration ```bash grunt translateJS:english ``` @@@ ## Orchestration ```javascript // ./tasks/translate.js var async = require('async'); module.exports = function(grunt){ var steps = require('./lib/i18n.js')(grunt); // callable as `grunt translateJS:` grunt.registerTask('translateJS', function(lang){ grunt.config.set('language', lang); async.waterfall([ steps.downloadSpreadsheet.bind(steps), steps.locateLanguage.bind(steps), steps.createJSFile.bind(steps) ], this.async()); }); }; ``` @@@ ## Librairie ```javascript // ./lib/i18n.js var request = require('request'); module.exports = function i18nTask(grunt){ return { downloadSpreadsheet: function(done){ // … }, locatelang: function(csv, props, done){ // … }, createJSFile: function(sheet, props, done){ // … } }; }; ``` @@@ ```javascript // ./lib/i18n.js module.exports = function i18nTask(grunt){ return { downloadSpreadsheet: function(done) { var url = grunt.config.get('i18nUrl'); request.get(url, function (error, response, body) { if (error) throw Error('Request error'); if (response.statusCode !== 200) throw Error('Could not find translation spreadsheet'); if (!body) throw Error('Spreadsheet body is empty'); var csv = util.CSVToArray(body); var props = spreadsheet[0].slice(2); done(null, csv, props); }); } }; ``` @@@ ## Tester ```javascript // ./test/unit/lib/i18n.js describe('i18nTask.downloadSpreadsheet', function(){ it('should parse properly a remote document', function(){ // … }); it('should raise an error on unexpected spreadsheet format', function(){ // … }); it('should fail if remote document is unavailable', function(){ // … }); }); ``` @@@ ## Stubber Simuler des erreurs I/O. Isoler les appels I/O. Fonctionner localement, sans infrastructure externe. @@@ ```javascript var sinon = request('sinon'); var gruntStub = sinon.stub(grunt.file, 'write'); expect( gruntStub.calledWith( 'dummy config valueen-GB.js', 'define('+dummyOutput+');' ) ).to.be.ok; ``` @@@ ```javascript // ./test/unit/lib/i18n.js var requestStub = sinon.stub(request, 'get'); it('should parse properly a remote document', function(){ requestStub.yields(null, {statusCode: 200}, validCSVContent); }); it('should raise an error on unexpected spreadsheet format', function(){ requestStub.yields(null, {statusCode: 200}, 'I am not CSV'); }); it('should fail if remote document is unavailable', function(){ requestStub.yields(null, {statusCode: 500}); }); ```
## Optimiser une tâche @@@ ## Implicite `grunt sass_compile:arabic` ```javascript grunt.registerTask('sass_compile', function(){ … }); grunt.initConfig({ sass: { /* content dynamically generated by custom task */ } }); ``` @@@ ## Explicite et verbeux `grunt sass:arabic` ```javascript grunt.initConfig({ sass: { news: { … }, arabic: { … }, journalism: { … }, portuguese: { … }, // 20 autres services production: { … } } }); ``` @@@ ## Dynamique `grunt sass:service:arabic` ```javascript grunt.initConfig({ sass: { service: { expand: true, ext: '.css', cwd: 'webapp/static/sass/', dest: 'webapp/static/stylesheets/', src: 'services/<%= grunt.task.current.args[0] %>/*.scss' } } }); ``` cf. [Dynamic Grunt Targets Using Templates](https://thom4.net/2013/dynamic-grunt-targets-using-templates/)
## Intégration continue CI avec Maven. @@@ ```xml ``` @@@ ## `npm test` @@@ ## `package.json` ```javascript { "scripts": { "test": "grunt jshint && grunt test" }, "dependencies": { "grunt": "~0.4.1", "grunt-cli": "~0.1.9" } } ``` @@@ ### `npm install -g grunt-cli` vs. ### `npm install --save grunt-cli` @@@ ## Isolation du contexte global `npm run ` regarde dans `./node_modules`. Permet de maitriser la stack. `npm` devient la seule dépendance système.
## `npm run` @@@ ### `npm run jshint` vs. ### `grunt jshint` @@@ ## `npm run jshint` ```bash npm install --save jshint ``` ```javascript { "scripts": { "jshint": "jshint --config .jshintrc lib/**/*.js" } } ``` @@@ ## `grunt jshint` ```bash npm install --save grunt grunt-contrib-jshint ``` ```javascript grunt.initConfig({ jshint: { all: { src: 'lib/**/*.js' }, options: { jshintrc: '.jshintrc' } } }); ```
## Questions ? ## Conversations ?