Using grunt to 

AUTO GENERATE

Gruntfiles

Joel Kemp 
@mrjoelkemp
Bēhance/Adobe

agenda

  • The Complicated Front-end
  • What is Grunt.js and what is a Gruntfile?
  • I just want to SASS and Coffee
  • I just want to Requirejs or Browserify
  • What is Esprima and what is an AST?
  • Automating front-end build tooling

the complicated front-end

So... many.... things:
  • Preprocess CSS and HTML abstractions
    • SASS/LESS/Stylus and Jade/Slim
  • Preprocess JavaScript abstractions
    • Coffee, Typescript, Clojurescript
  • Precompile templates
    • Handlebars or Mustache
  • JSHint and JSCS
  • Run unit and integration tests
  • Browserify or Requirejs bundling
  • Manage vendor dependencies

so we use tools like Grunt

  • Or Gulp or Broccoli
  • Task runner
    • Task: a plugin and configuration options
  • Plugins perform front-end build tasks
    • grunt-contrib-sass 
    • grunt-contrib-requirejs
  • A Gruntfile is a declarative way of stating your tasks
    • Which plugins need to load
    • The configuration options for plugins


SAMPLE GRUNTFILE

 module.exports = function(grunt) {
  grunt.initConfig({
    sass: {
      dist: {
        files: { expand: true, src: ['**/*.scss'], dest: 'css/' }
      }
    },
    coffee: {
      dist: {
        files: { expand: true, src: ['**/*.cofee'], dest: 'js/' }
      }
    },    watch: {      coffee: {
        files: [**/*.coffee],
        tasks: ['coffee']      }
    }
  });};

About these gruntfiles...

  • They get large – fast
  • Yet another thing to maintain
  • Boilerplate aside from custom plugins
  • They're a waste of time
    • All we really wanted to do was build our app
  • The ideal: turn on Grunt and just code. 
    • Let Grunt figure out: 
      • what you're using
      • which plugins/libraries it needs
      • how to configure the tasks
      • how to generate its Gruntfile
      • how to perform those tasks on file changes

How can we avoid writing Gruntfiles?

  • Yeoman
    • Generators give you a skeleton
    • Assumes every app has the same structure
    • Assumes you want to use the same toolset
    • Assumes you want the kitchen sink
    • Similar: HTML5 Boilerplate, Backbone Layout Manager
  • Codekit or LiveReload (premium solutions)
    • Great for adapting to your structure
    • Limited utility
      • Preprocessors
      • Super-fragile as a JS bundling solution
  • YA: generates your Gruntfile as you build front-end apps

I just want sass and coffee

  • So how can we make Grunt handle it?
  • Grunt has a watcher
    • Listen for new files being added and get the extension

    • If it's .scss
      • npm install grunt-contrib-sass
      • Set up a compile task
      • Set up a watch task
      • There's your gruntfile

    • If it's .coffee
      • npm install grunt-contrib-coffee
      • Set up compile and watch tasks
      • Regenerate the gruntfile with the new settings

Hook into the watcher

grunt.event.on('watch', function(action, filepath) {
  var ext = path.extname(filepath);

  if (action === 'added') {
    console.log('EXTADDED:' + ext);
  }
});
Pre-determined settings for SASS files
{
  lib: 'grunt-contrib-sass',
  target: {
    sass: {
      dist: {
        files: [{
          expand: true,
          src: ['**/*.{scss, sass}', '!node_modules/**/*.{scss, sass}'],
          ext: '.css'
        }]
      }
    }
  }
}

Gruntfile so far

grunt.initConfig({  "sass": {    "dist": {
      "files": [{
        "expand": true,
        "src": ["**/*.{scss, sass}", "!node_modules/**/*.{scss, sass}"],
        "ext": ".css"
      }]    }
  },
"watch": { "sass": { "files": [ "**/*.scss", "!node_modules/**/*.scss", "!.git/**/*.scss", "!bower_components/**/*.scss", "!vendor/**/*.scss" ], "tasks": ["newer:sass"] } }
});

That Was easy

  • Throw Compass into the mix...
    • Need to detect if compass is installed
    • If so, 
      • detect if compass' directory structure was used
        • Separation of sass/ and stylesheets/
      • Use grunt-contrib-compass instead
      • Task configuration for SASS is dynamic
  • Still fairly straightforward
  • Stylus and LESS use the same approach
  • Just create SASS files and Grunt will take care of them for you

I just want to use AMD or CommonJS

Why worry about how to bundle your apps?
  • Pick your syntax and just code 
  • So how can we make Grunt figure that out?
  • When you add a JS file: 
    • Determine if it's AMD or CommonJS
      • If AMD, use grunt-contrib-requirejs
      • If CommonJS, use grunt-browserify
    • Set up the task to bundle the app(s)
      • Need to determine the entry point ("root") of the app
    • Set up the watch task to auto-bundle on change/addition
      • Auto-name the bundle
    • Generate the configuration

Detect if it's Amd or commonjs

Static analysis via Esprima
  • Esprima takes a JS files and generates an AST
  • We can traverse the AST to look for patterns
  • Those patterns determine the module type
  • Sample module:
  • define([
      './a'
    ], function(a) {
      'use strict';
    
    });

Its ast

"expression": {
  "type": "CallExpression",
  "callee": {
    "type": "Identifier",
    "name": "define"
  },
  "arguments": [{
    "type": "ArrayExpression",
    "elements": [{
      "type": "Literal",
      "value": "./a",
      "raw": "'./a'"
    }]
  }]
}
Full AST: http://bit.ly/esprima-example
define([
  './a'
], function(a) {
  'use strict';

});

Is it amd or Commonjs?

It's AMD if it has a define call (or is a driver script):

  • mrjoelkemp/node-ast-module-types
  • mrjoelkemp/module-definition









"expression": {
  "type": "CallExpression",
  "callee": {
    "type": "Identifier",
    "name": "define"
  },
  "arguments": [{
    "type": "ArrayExpression",
    "elements": [{
      "type": "Literal",
      "value": "./a",
      "raw": "'./a'"
    }]
  }]
}
  • // Is the current node an AMD define() call?
    module.exports.isDefine = function (node) {
      var c = node.callee;
    
      return c &&
        node.type === 'CallExpression' &&
        c.type    === 'Identifier' &&
        c.name    === 'define';
    };
    

Identify the root of the app

Why do I have to tell these plugins the entry point?
  • A "root" is a module that has no dependents
    • No other module requires it
  • Every root represents a separate app/bundle
  • The algorithm:
    • Maintain a list of used/required modules
    • For each JS module:
      • Get its dependencies
      • Mark each dependency as "used"
    • The JS modules that aren't in the list are roots
  • Relevant libraries: 
    • mrjoelkemp/node-app-root,  substack/detective,  mrjoelkemp/detective-amd

Our gruntfile now



Yay, Grunt!

Now what about the rest of the front-end tasks?
  • Precompile templates
    • detect .mustache/.handlebars files
    • use grunt-contrib-handlebars
  • JSHint and JSCS
    • detect .jshintrc or .jscsrc files
    • use grunt-contrib-jshint or grunt-jscs
  • Run tests
    • Traverse AST looking for 'describe' or 'it' CallExpression
  • Manage vendor dependencies
    • Detect CallExpression with identifier $ for jQuery
    • npm install jquery
    • But what about shims?

How can we use these techniques?

  • npm install ya.js
    • Beta/Experimental
    • Is an almighty tool the answer?
  • Can we build these techniques into Grunt?
    • Or Browserify and RequireJS?
  • Gulp could use this ability too.


Take the grunt-work out of using Grunt.



Thanks :)


@mrjoelkemp

Using grunt to autogenerate Gruntfiles

By Joel Kemp

Using grunt to autogenerate Gruntfiles

  • 1,012