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
- 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
- Generate the configuration
Detect if it's Amd or commonjs
Static analysis via Esprima
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'"
}]
}]
}
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
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.