> Thousands of *candles* can be *lit* from a single *candle*, and the *life* of the *candle* will not be shortened. — @FakeBuddha @@@ > Thousands of *tasks* can be *run* from a single *task*, and the *duration* of the *task* will not be shortened. — @FakeBuddha
## Good afternoon! @@@ ## Pâté-baguette-fromage ! (Bonjour !)
![](../../img/avatar.jpg)
## *Thomas* Parisot [thom4.net](https://thom4.net) – [@thom4parisot](https://twitter.com/thom4parisot) @@@ ![Pardon my French](../../images/pardon-my-french.jpg) @@@ ![Full Stack JavaScript](../../images/javascript.png) @@@ ## BBC R&D [github.com/bbcrd](https://github.com/bbcrd) [bbc.co.uk/rd](http://bbc.co.uk/rd) @@@ ![I published](../../images/npm.png) [npmjs.org/~thom4](https://npmjs.org/~thom4) @@@ [![Sud Web](../../images/sudweb.png)](http://sudweb.fr)
# Solid Grunt ## From spaghetti to rock solid code 6th June 2014 — [Scotch on the Rocks](http://www.sotr.eu/)
~~~~ I hate routine. Well, I thought I hated routine. @@@ ~~~~ One day I was waiting for the tramway, I took this picture. I just had my phone so I used it. @@@ ~~~~ The day after I was waiting for the tramway to go and see a customer. Again, I took a picture. @@@ @@@ ~~~~ And I did it, again and again. Each time I added a thought, a memory or a feeling I had this day. @@@ ~~~~ I realised what I feared in the word *routine* was the implied passiveness of an action. @@@ @@@ ## Turning *routine* into an active *task* ~~~~ I discovered I could turn the routine into an *active task* of creativity. And that what supposed to be boring with a *poor* tooling @@@ ## What about our Web development *tasks*? @@@ ## Our *tedious* Web development *tasks*? ~~~~ So is compiling CSS, optimising images, generating documentation, deploying Web pages, checking the quality of the code, running tests on 5 browsers at the same time or stripping unused frontend code. We do not want to think about it. We want those tasks to be performed. Not us to perform them.
## Level 0: stone age @@@ ## Sparse and manual ~~~~ Because this is how it was back in the days. How did you enforce a JavaScript code style in 2010? How did you check the sanity of the code before deploying it? We delegated what we could delegate to tools like YUICompressor or Sass. When they eventually became available. @@@ ~~~~ You needed to have the proper version of Java to be available in the `$PATH`. You needed the proper command line arguments to be given to those tools. Writing a `Makefile` was probably the best solution to hide the complexity. (If you were not using Windows). @@@ ## Tools evolved @@@ ## GUIs ~~~~ Some got some love with GUIs. @@@ ## Integration ~~~~ Some got integrated in our coding editors and IDEs. @@@ ## What are our problems already? @@@ ## Install @@@ ## Consistency @@@ ## Problem *solved*, huh? ### They simply *evolved*.
## Level 1: using a *task* runner @@@ ~~~~ So enters Grunt. The last time I gave a talk about grunt, the first question during the Q&A was… *what is grunt?* But you should already know it with the past 3 or 4 talks mentioning this tool ;-) @@@ ## Grunt is a *task* runner But what is a *task*? @@@ ## A *task* is a *repeatable* action with a *predictable* outcome. @@@ ## A *bridge* between your codebase and your workflow. ~~~~ Grunt acts as a *bridge* between your *project codebase* and a *set of actions*. Grunt has no opinion on the nature of the tasks you should perform. Grunt neither enforces a particular workflow. @@@ ## *Less* One *task* amongst ~3000. @@@ ## Setup ```bash npm i -g grunt-cli npm i --save grunt grunt-contrib-less ``` @@@ ## Configuration ```javascript /* Gruntfile.js */ module.exports = function(grunt){ grunt.initConfig({ less: { all: { src: 'src/less/**/*.less', dest: 'dist/stylesheets' } } }); }; ``` @@@ ## Running the task ```bash grunt less ``` @@@ ## Job done! Configuration + workflow are shared <3 ~~~~ Now the configuration is *shared* amongst your project. Anybody who has access to your code can perform any task they want or need. Sharing your tooling is sharing the knowledge. It acts as a documentation of the *process*. And anybody comfortable with JavaScript can run and/or configure tasks. Which broadens and eases the adoption of prior painful and cumbersome tasks. @@@ ## But what *if*…
## Level 1: *not* using Grunt @@@ ## Tasks *without* a task runner? ~~~~ What if we would not use grunt for simple tasks. A simple task is an action which has only a couple of different configurations. Anybody knows a good alternative for that? @@@ ## `npm run` ~~~~ Simple. Native. Less dependencies. Easier configuration. @@@ ## `npm` vs `grunt-http` ~~~~ Yesterday someone talked about `grunt-http`. This is something you can do without grunt. @@@ ## Configuration ```javascript /* package.json */ { "scripts": { "reload": "curl --head http://coldfusion-host:8000/cfm?action=reload" } } ``` @@@ ## Running the task ```bash npm run reload ```
## Level 2: everyday configuration ~~~~ Now we are using Grunt, you might need to scale its usage within your working group. Next up are tips and organisation tricks based on my experience and on a two weeks spike at BBC News. It is not meant to be exhaustive: I just like them because they are simple and easy to remember. Thus to reproduce over and over. @@@ ## Logical target naming @@@ ```javascript grunt.initConfig({ less: { all: { src: ['src/**/*.css'], dest: 'dist/all.min.css', options: { compress: true } } } }); ``` ~~~~ Having a single target is easy and fast to write at first. It can become long and tedious to maintain on the long run. @@@ ```javascript grunt.initConfig({ less: { all: { src: [ 'bower_components/**/*.less', '!doc/**/*.less', 'src/**/*.less', 'src/**/*.css', '!src/vendor/doc.less' ], dest: 'dist/all.min.css', options: { compress: true } } } }); ``` ~~~~ It is better to reflect the intent of a target. The UNIX-style filename patterns are also here to keep things simple. @@@ ```javascript grunt.initConfig({ less: { files: { 'dist/main.min.css': [ 'src/stylesheets/**/*.{css,less}' ], 'dist/vendors.min.css': [ 'bower_components/**/*.less', 'src/vendor/*.less', '!src/vendor/doc.less' ] }, options: { compress: true } } }); ``` @@@ ## Task alias and execution order ~~~~ The next step after the target are the tasks. @@@ ## Configuration ```javascript grunt.registerTask('build-assets', ['less', 'uglify']); ``` ~~~~ You can create your own simply by referring to the existing ones name. In that case, `build-assets` will obviously care about building he assets for your frontend. Through its name, you know that task's responsibility is to do so. You do not necessarily need to call all the targets of a task as well. @@@ ## Fine grained configuration ```javascript grunt.registerTask('build-front', ['less:frontend', 'uglify:frontend', 'jshint', 'karma']); ``` ~~~~ And again, as the things grow up, we might want to either reduce the time or to not repeat all the tasks, all the time. So we can split them up as *full* tasks and *fast* tasks. @@@ ## Full and fast tasks ```javascript grunt.registerTask('deploy-fast', ['rsync:production']); grunt.registerTask('deploy', ['build-assets', 'deploy-fast', 'cleanup']); ``` ~~~~ The fast tasks are supposed to be triggered manually. Because you know you satisfy the prerequisites. Because you want to save some unneeded extra seconds of processing. @@@ ## Logical task file organisation ~~~~ We do all our best to keep things as small as possible but sometimes, you end up doing really a lot of things in a Gruntfile. You have several options to reduce the overall size of a Gruntfile. @@@ ## Several Gruntfiles ```bash grunt --gruntfile Gruntfile-production.js grunt --gruntfile config/grunt/frontend.js ``` ~~~~ You can either have several Gruntfiles. Each one can represent separate environments, different topics or whatsoever. Organise them in a way which truly speaks within your team. @@@ ## Modular Gruntfile ```javascript grunt.initConfig({ less: require('./grunt/less.js'), watch: require('./grunt/watch.js') }); ``` ~~~~ Another solution is to keep a single Gruntfile and to split its content. As a reminder, a Gruntfile is JavaScript and the `grunt.initConfig` expects to receive a JavaScript. So we only need to care about providing a JavaScript object as a configuration file, really. @@@ ## Domain based modular Gruntfile ```javascript grunt.initConfig([ require('./grunt/frontend.js'), require('./grunt/performance.js'), require('./grunt/deploy.js'), ].reduce(_.merge, {}); ``` ~~~~ If you want to organise your files in a domain-oriented fashion, we can split up the configuration this way and augment the configuration object. This way, each required file would return a JavaScript object which would be merged with the previous one. This approach is naive but you get the idea of what you can do. @@@ ## Variables ~~~~ As the name states, a variable is a way to store a reusable value in different places. @@@ ```javascript grunt.initConfig({ less: { main: { src: 'src/**/*.less', dest: 'dist/assets/main.css' } }, watch: { stylesheets: { src: '<%= less.main.src %>', tasks: ['less'] } } }); ``` ~~~~ As they are evaluated after the `initConfig` step, it means you have access to every other part of the Gruntfile configuration at any moment. @@@ ## Variables in globbing patterns ```javascript grunt.initConfig({ now: (new Date).getFullYear(), folders: '{2008..<%= now %>}', // … }); ``` ~~~~ You can define your own variables and use them in globbing patterns for instance @@@ ## Over and over… ```javascript grunt.initConfig({ now: (new Date).getFullYear(), folders: '{2008..<%= now %>}', assemble: { slides: { src: '<%= folders %>/*/index.md', dest: 'dist/' } }, mdlint: { all: { src: '<%= assemble.slides.src %>' } } }); ``` @@@ ## Command line variables ```javascript grunt.initConfig({ year: grunt.option('year') || '*', assemble: { posts: { src: 'src/<%= year %>/*.md', dest: 'dist/' } } }); ``` ~~~~ Grunt exposes the `grunt.option` API Did you know the `<%= templates %>` are evaluated JavaScript expressions? So you could use that to reduce the amount of processed documents. @@@ ```bash # everything grunt assemble # just 2014 blog posts grunt assemble --year=2014 ``` ~~~~ So we could regenerate all the blog posts or only a subset of them based on a year value. @@@ ## Alternative command line ```bash grunt assemble:posts:2014 ``` @@@ ```javascript grunt.initConfig({ assemble: { year: '<%= grunt.task.current.args[0] || "*" %>', posts: { src: 'src/<%= year %>/*.md', dest: 'dist/' } } }); ``` @@@ ## Asynchronous task loading ~~~~ Grunt 0.4 performs a lot of actions in a blocking fashion. If your Gruntfile starts to become massive, running a task can become slow just because Grunt needs to load every task before being able to run one. Every. Single. Time. @@@ ## From ```javascript grunt.loadNpmTasks('grunt-contrib-imagemin'); grunt.registerTask('images', ['copy:standardImages', 'responsive_images', 'imagemin']); ``` ## To ```javascript grunt.registerTask('images', [], function () { grunt.loadNpmTasks('grunt-contrib-imagemin'); grunt.task.run('copy:standardImages', 'responsive_images', 'imagemin'); }); ``` ~~~~ [As coined by Tom Maslen](https://github.com/gruntjs/grunt/issues/975#issuecomment-29058707) in the Grunt issue #975: simply create a task which registers the heavy ones. @@@ ## `jit-grunt` Automatic task lazy loading. ~~~~ Otherwise, `jit-grunt` is an npm package which will auto load your grunt plugins when they are needed. > https://github.com/shootaroo/jit-grunt @@@ ## Parallel tasks @@@ ~~~~ Sometimes you also wait (and waste time) because some tasks are long and performed sequentially. If they do not rely on each other, run them in parallel to save time. I mean, why waiting for your Sass files to be compiled before processing your images, or your JavaScript? @@@ ## `grunt-concurrent` ```javascript grunt.initConfig({ concurrent: { frontend: [ 'sass:main', 'sass:widgets', 'assemble:posts' ], deploy: [ 'rsync:aws', 'rsync:ssh' ] } }); ```
## Level 3: your own task ~~~~ Even though the Grunt ecosystem is extremely wide and varied, you will not necessarily find a plugin which does what you want, or the way you want. Maybe you should consider contributing to a relevant plugin before starting your own flavour of it. Otherwise, let's create our first task. @@@ ## i18n sync from a Spreadsheet task ~~~~ Let's say we have an online spreadsheet which contains translations. This is handy because it has all the needed UI to handle collaboration and so on. We would like to retrieve it to create a translation catalogue back in our application. @@@ ```bash grunt i18n:scottish ``` ~~~~ But wait a moment, if we write a Grunt task, how can we switch over the newest and trendiest tool ever? Does it imply we have to rewrite everything over and over? A kind of tedious task we are actually fighting against… Hopefully no we do not need to. @@@ ## Reconsidering a *task* - configuration (the Gruntfile) - orchestration (the `grunt` package) - business logic (this is you) - tests (this is you, again) - documentation (this is you, still) @@@ ## Configuration ```javascript module.exports = function(grunt){ grunt.initConfig({ 'i18n': { url: 'https://drive.google.com/…' } }); grunt.loadTasks('./lib/grunt'); }; ``` ~~~~ We simply needs to declare the custom task in the Gruntfile. This will load any JavaScript file within this folder and will call each module while passing `grunt` as an argument. @@@ ## Orchestration ```javascript /* ./lib/grunt/remote-i18n.js */ module.exports = function(grunt){ grunt.registerTask('i18n', function(lang){ grunt.requiresConfig('url'); var url = grunt.config.get('url'); var done = this.async(); if (!lang) { grunt.fail.fatal('No lang provided.'); } require('./i18n.js').run(url, lang, done); }); }; ``` ~~~~ This is where the orchestration part falls into. We want that file to be responsible of dealing with the task, and nothing else. We want it to be the last piece of code to contain references to `grunt` or anything related to its API. @@@ ## Business logic ```javascript var async = require('async'); module.exports = { run: function(url, lang, done){ var self = this; async.waterfall([ self.download.bind(self, url), self.locateLanguage.bind(self, lang), self.writeJSON.bind(self, lang) ], done); } }; ``` ~~~~ And here is how we expose the `i18n.js` module's business logic. @@@ ## Overall business logic view ```javascript var async = require('async'); module.exports = { run: function(url, lang, done){}, download: function(url, done){}, getSheetFromCSV: function(lang, csv, done){}, writeJSON: function(lang, sheet, done){} }; ``` @@@ ```javascript var request = require('request'); var util = require('./csv-utils.js'); module.exports = { download: function(url, done){ 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); done(null, csv); }); } }; ``` @@@ ## Tests ```javascript var download = require('../lib/i18n.js').download; describe('download', function(){ it('should fail if host does not exist', function(done){ download('http://dummyHost', function(error){ expect(error).to.be.an(Error); done(); }); }); it('should raise an error on unexpected spreadsheet format', function(){ // … }); }); ``` ~~~~ What do we test? The whole Grunt task? Grunt is already tested by its team. So we are only interested to test our API. Because this is what we designed. An API. @@@ ## Stub (I/O and third parties) ```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; ``` ~~~~ Prevent I/O hits and simulates all the possible use cases! @@@ ## Yield edge cases ```javascript 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}); }); ``` @@@ ## Documentation ~~~~ Even if it an internal codebase, a lightweight documentation is more than welcome. The task of writing a documentation (like a README) is your incentive to reduce misuses of your code. This is also a way to make people working faster and better, sooner. And if you feel proud and okay with it, you are now not so far from running the `npm publish` command ;-)
## Okay let's recap! @@@ ## If we design *modular* tasks @@@ ## If we play the *non-blocking* game @@@ ## If we think in term of *APIs* @@@ ## And if we do not *tight* our code to *Grunt* @@@ ## We *become free* of the hype hassle @@@ ## And we *solved* the problem @@@ ## Installing ## Sharing ## Running @@@ ## No bad routine anymore yay!