Grunt is king. It is the ubiquitous task runner used for most nodejs projects and has quickly expanded to conquer other software ecosystems. However, its kingdom is vulnerable. Grunt does not align well with many nodejs paradigms and is notorious for its harsh learning curve. Meet Gulp, the challenger in the taskrunner revolution. Gulp’s easy configuration produces an easy learning curve, and its alignment with nodejs paradigms eliminates the friction. Grab your ticket, your foam finger, and your team-colored face paint and witness the battle, the revolution, and the crowning of Gulp.
Author:
Jay Harris
Problem Solver | Arana Software
[email protected] | www.aranasoft.com
www.twitter.com/jayharris
#dethroningGrunt
S I M P L E A N D E F F E C T I V E B U I L D S
W I T H G U L P. J S
@ j a y h a r r i s
what is a
taskrunner?
that is a lot of things to do
task runners simplify to one command
gulp
grunt
focusing on five tasks
demo code
queenseight.com
aranasoft/queenseight
//
Project
Specific
Tasks
grunt.loadNpmTasks('grunt-‐bower-‐task');
grunt.loadNpmTasks('grunt-‐coffeelint');
grunt.loadNpmTasks('grunt-‐contrib-‐clean');
grunt.loadNpmTasks('grunt-‐contrib-‐coffee');
grunt.loadNpmTasks('grunt-‐contrib-‐concat');
grunt.loadNpmTasks('grunt-‐contrib-‐connect');
grunt.loadNpmTasks('grunt-‐contrib-‐copy');
grunt.loadNpmTasks('grunt-‐contrib-‐cssmin');
grunt.loadNpmTasks('grunt-‐contrib-‐jade');
grunt.loadNpmTasks('grunt-‐contrib-‐jshint');
grunt.loadNpmTasks('grunt-‐contrib-‐less');
grunt.loadNpmTasks('grunt-‐contrib-‐uglify');
grunt.loadNpmTasks('grunt-‐contrib-‐watch');
//
General-‐Purpose
Tasks
grunt.loadNpmTasks('grunt-‐bower-‐task');
grunt.loadNpmTasks('grunt-‐coffeelint');
grunt.loadNpmTasks('grunt-‐contrib-‐clean');
grunt.loadNpmTasks('grunt-‐contrib-‐coffee');
grunt.loadNpmTasks('grunt-‐contrib-‐concat');
grunt.loadNpmTasks('grunt-‐contrib-‐connect');
grunt.loadNpmTasks('grunt-‐contrib-‐copy');
grunt.loadNpmTasks('grunt-‐contrib-‐cssmin');
grunt.loadNpmTasks('grunt-‐contrib-‐jade');
grunt.loadNpmTasks('grunt-‐contrib-‐jshint');
grunt.loadNpmTasks('grunt-‐contrib-‐less');
grunt.loadNpmTasks('grunt-‐contrib-‐uglify');
grunt.loadNpmTasks('grunt-‐contrib-‐watch');
//
13
tasks
in
all
grunt.loadNpmTasks('grunt-‐bower-‐task');
grunt.loadNpmTasks('grunt-‐coffeelint');
grunt.loadNpmTasks('grunt-‐contrib-‐clean');
grunt.loadNpmTasks('grunt-‐contrib-‐coffee');
grunt.loadNpmTasks('grunt-‐contrib-‐concat');
grunt.loadNpmTasks('grunt-‐contrib-‐connect');
grunt.loadNpmTasks('grunt-‐contrib-‐copy');
grunt.loadNpmTasks('grunt-‐contrib-‐cssmin');
grunt.loadNpmTasks('grunt-‐contrib-‐jade');
grunt.loadNpmTasks('grunt-‐contrib-‐jshint');
grunt.loadNpmTasks('grunt-‐contrib-‐less');
grunt.loadNpmTasks('grunt-‐contrib-‐uglify');
grunt.loadNpmTasks('grunt-‐contrib-‐watch');
timer
=
require
"grunt-‐timer"
fs
=
require
'fs'
path
=
require
'path'
server
=
require
'./config/server'
urlrouter
=
require
'urlrouter'
module.exports
=
(grunt)
-‐>
timer.init
grunt
#
Project
configuration.
grunt.initConfig
pkg:
grunt.file.readJSON
'package.json'
files:
coffee:
app:
"app/js/**/*.coffee"
generated:
"generated/js/app.coffee.js"
css:
vendor:
"vendor/css/**/*.css"
app:
"app/css/**/*.css"
concatenated:
"generated/css/app.css"
minified:
"dist/css/app.css"
minifiedWebRelative:
"css/app.css"
img:
root:
"img"
jade:
pages:
"**/*.jade"
pageRoot:
"app/pages/"
js:
app:
"app/js/**/*.js"
vendor:
[
"vendor/components/jquery/jquery.js"
"vendor/components/underscore/underscore.js"
"vendor/components/angular/angular.js"
"vendor/js/**/*.js"
]
concatenatedVendor:
"generated/js/vendor.js"
minifiedVendor:
"dist/js/vendor.js"
minifiedVendorWebRelative:
"js/vendor.js"
concatenated:
"generated/js/app.js"
minified:
"dist/js/app.js"
minifiedWebRelative:
"js/app.js"
less:
app:
"app/css/app.less"
vendor:
"vendor/css/**/*.less"
generatedApp:
"generated/css/app.less.css"
generatedVendor:
"generated/css/vendor.less.css"
watch:
"app/css/**/*.less"
webfonts:
root:
"fonts"
bower:
install:
options:
copy:
false
coffee:
compile:
files:
"<%=
files.coffee.generated
%>":
"<%=
files.coffee.app
%>"
coffeelint:
app:
[
"<%=
files.coffee.app
%>"
]
concat:
css:
src:
[
"<%=
files.less.generatedVendor
%>"
"<%=
files.css.vendor
%>"
"<%=
files.less.generatedApp
%>"
"<%=
files.css.app
%>"
]
dest:
"<%=
files.css.concatenated
%>"
js:
src:
[
"<%=
files.coffee.generated
%>"
"<%=
files.js.app
%>"
]
dest:
"<%=
files.js.concatenated
%>"
jsVendor:
src:
["<%=
files.js.vendor
%>"]
dest:
"<%=
files.js.concatenatedVendor
%>"
connect:
server:
options:
port:
8000
base:
'generated'
open:
true
middleware:
(connect,
options)
-‐>
middlewares
=
[];
if
(!Array.isArray(options.base))
options.base
=
[options.base]
directory
=
options.directory
||
options.base[options.base.length
-‐
1]
options.base.forEach
(base)
-‐>
#
Serve
static
files.
middlewares.push(connect.static(base))
middlewares.push
urlrouter(server.drawRoutes)
#
Make
directory
browse-‐able.
middlewares.push
connect.directory(directory)
middlewares
copy:
imagesDev:
files:
[{
expand:
true
cwd:
"app/img/"
src:
"**"
dest:
"generated/<%=
files.img.root
%>/"
}
{
expand:
true
cwd:
"vendor/img/"
src:
"**"
dest:
"generated/<%=
files.img.root
%>/"
}]
imagesDist:
files:
[{
expand:
true
cwd:
"app/img/"
src:
"**"
dest:
"dist/<%=
files.img.root
%>/"
}
{
expand:
true
cwd:
"vendor/img/"
src:
"**"
dest:
"dist/<%=
files.img.root
%>/"
}]
staticDev:
files:
[
expand:
true
cwd:
"app/static"
src:
"**"
dest:
'generated'
]
staticDist:
files:
[
expand:
true
cwd:
"app/static"
src:
"**"
dest:
'dist'
]
webfontsDev:
files:
[{
expand:
true
cwd:
"vendor/webfonts/"
src:
"**"
dest:
"generated/<%=
files.webfonts.root
%>/"
}
{
expand:
true
cwd:
"vendor/components/font-‐awesome/fonts/"
src:
"**"
dest:
"generated/<%=
files.webfonts.root
%>/"
}]
webfontsDist:
files:
[{
expand:
true
cwd:
"vendor/webfonts/"
src:
"**"
dest:
"dist/<%=
files.webfonts.root
%>/"
}
{
expand:
true
cwd:
"vendor/components/font-‐awesome/fonts/"
src:
"**"
dest:
"dist/<%=
files.webfonts.root
%>/"
}]
cssmin:
compress:
files:
"<%=
files.css.minified
%>":
"<%=
files.css.concatenated
%>"
jade:
dev:
options:
pretty:
true
data:
js:
"<%=
files.js.minifiedWebRelative
%>"
jsVendor:
"<%=
files.js.minifiedVendorWebRelative
%>"
css:
"<%=
files.css.minifiedWebRelative
%>"
pkg:
"<%=
pkg
%>"
files:
[{
expand:
true
src:
"<%=
files.jade.pages
%>"
cwd:
"<%=
files.jade.pageRoot
%>"
dest:
"generated/"
ext:
".html"
}]
dist:
options:
data:
js:
"<%=
minifiedWebRelative
%>"
jsVendor:
"<%=
minifiedVendorWebRelative
%>"
css:
"<%=
files.css.minifiedWebRelative
%>"
pkg:
"<%=
pkg
%>"
files:
[{
expand:
true
src:
"<%=
files.jade.pages
%>"
cwd:
"<%=
files.jade.pageRoot
%>"
dest:
"dist/"
ext:
".html"
}]
jshint:
files:
["<%=
files.js.app
%>"]
options:
#
enforcing
options
curly:
true
eqeqeq:
true
latedef:
true
newcap:
true
noarg:
true
#
relaxing
options
boss:
true
eqnull:
true
sub:
true
#
environment/globals
browser:
true
less:
options:
paths:
["app/css",
"vendor/css"]
compile:
files:
"<%=
files.less.generatedVendor
%>":
"<%=
files.less.vendor
%>"
"<%=
files.less.generatedApp
%>":
"<%=
files.less.app
%>"
uglify:
options:
banner:
'/*!
<%=
pkg.name
%>
<%=
grunt.template.today("yyyy-‐mm-‐dd")
%>
*/\n'
js:
files:
"<%=
files.js.minified
%>":
"<%=
files.js.concatenated
%>"
jsVendor:
files:
"<%=
files.js.minifiedVendor
%>":
"<%=
files.js.concatenatedVendor
%>"
clean:
bower:
src:
bowerDirectory
grunt
js:
src:
"<%=
files.js.concatenated
%>"
css:
src:
"<%=
files.css.concatenated
%>"
dist:
src:
["dist",
"generated"]
watch:
coffee:
files:
"<%=
files.coffee.app
%>"
tasks:
["coffeelint",
"coffee",
"concat:js"]
css:
files:
["<%=
files.css.vendor
%>",
"<%=
files.css.app
%>"]
tasks:
["concat:css"]
images:
files:
["app/img/**/*.*",
"vendor/img/**/*.*"]
tasks:
["copy:imagesDev"]
jade:
files:
["<%=
files.jade.pageRoot
%>/<%=
files.jade.pages
%>"]
tasks:
["jade:dev"]
js:
files:
["<%=
files.js.vendor
%>",
"<%=
files.js.app
%>"]
tasks:
["concat:js"]
less:
files:
[
"<%=
files.less.vendor
%>"
"<%=
files.less.watch
%>"
]
tasks:
["less",
"concat:css"]
lint:
files:
"<%=
files.js.app
%>"
tasks:
["jshint"]
webfonts:
files:
["vendor/webfonts/**/*.*",
"vendor/components/font-‐awesome/fonts/**/*.*"]
tasks:
["copy:webfontsDev"]
livereload:
options:
livereload:
true
files:
"dist/**/*.*"
grunt.loadNpmTasks
'grunt-‐bower-‐task'
grunt.loadNpmTasks
'grunt-‐coffeelint'
grunt.loadNpmTasks
'grunt-‐contrib-‐clean'
grunt.loadNpmTasks
'grunt-‐contrib-‐coffee'
grunt.loadNpmTasks
'grunt-‐contrib-‐concat'
grunt.loadNpmTasks
'grunt-‐contrib-‐connect'
grunt.loadNpmTasks
'grunt-‐contrib-‐copy'
grunt.loadNpmTasks
'grunt-‐contrib-‐cssmin'
grunt.loadNpmTasks
'grunt-‐contrib-‐jshint'
grunt.loadNpmTasks
'grunt-‐contrib-‐less'
grunt.loadNpmTasks
'grunt-‐contrib-‐jade'
grunt.loadNpmTasks
'grunt-‐contrib-‐uglify'
grunt.loadNpmTasks
'grunt-‐contrib-‐watch'
grunt.registerTask
'default',
[
'common'
'dev'
]
grunt.registerTask
'common',
[
'bower'
'coffeelint'
'jshint'
'coffee'
'less'
'concat'
'copy:staticDev'
'copy:imagesDev'
'copy:webfontsDev'
'jade:dev'
]
grunt.registerTask
'dev',
[
'connect'
'watch'
]
grunt.registerTask
'dist',
[
'uglify'
'cssmin'
'copy:staticDist'
'copy:imagesDist'
'copy:webfontsDist'
'jade:dist'
]
bowerDirectory
=
(grunt)
-‐>
bowerrc
=
path.join(process.cwd(),
".bowerrc")
bowerConfig
=
grunt.file.readJSON(bowerrc)
unless
!fs.existsSync(bowerrc)
bowerConfig?.directory
||
"vendor/components"
gruntfile.coffee
timer
=
require
"grunt-‐timer"
fs
=
require
'fs'
path
=
require
'path'
server
=
require
'./config/server'
urlrouter
=
require
'urlrouter'
module.exports
=
(grunt)
-‐>
timer.init
grunt
#
Project
configuration.
grunt.initConfig
pkg:
grunt.file.readJSON
'package.json'
files:
coffee:
app:
"app/js/**/*.coffee"
generated:
"generated/js/app.coffee.js"
css:
vendor:
"vendor/css/**/*.css"
app:
"app/css/**/*.css"
concatenated:
"generated/css/app.css"
minified:
"dist/css/app.css"
minifiedWebRelative:
"css/app.css"
img:
root:
"img"
jade:
pages:
"**/*.jade"
pageRoot:
"app/pages/"
js:
app:
"app/js/**/*.js"
vendor:
[
"vendor/components/jquery/jquery.js"
"vendor/components/underscore/underscore.js"
"vendor/components/angular/angular.js"
"vendor/js/**/*.js"
]
concatenatedVendor:
"generated/js/vendor.js"
minifiedVendor:
"dist/js/vendor.js"
minifiedVendorWebRelative:
"js/vendor.js"
concatenated:
"generated/js/app.js"
minified:
"dist/js/app.js"
minifiedWebRelative:
"js/app.js"
less:
app:
"app/css/app.less"
vendor:
"vendor/css/**/*.less"
generatedApp:
"generated/css/app.less.css"
generatedVendor:
"generated/css/vendor.less.css"
watch:
"app/css/**/*.less"
webfonts:
root:
"fonts"
bower:
install:
options:
copy:
false
coffee:
compile:
files:
"<%=
files.coffee.generated
%>":
"<%=
files.coffee.app
%>"
coffeelint:
app:
[
"<%=
files.coffee.app
%>"
]
concat:
css:
src:
[
"<%=
files.less.generatedVendor
%>"
"<%=
files.css.vendor
%>"
"<%=
files.less.generatedApp
%>"
"<%=
files.css.app
%>"
]
dest:
"<%=
files.css.concatenated
%>"
js:
src:
[
"<%=
files.coffee.generated
%>"
"<%=
files.js.app
%>"
]
dest:
"<%=
files.js.concatenated
%>"
jsVendor:
src:
["<%=
files.js.vendor
%>"]
dest:
"<%=
files.js.concatenatedVendor
%>"
connect:
server:
options:
port:
8000
base:
'generated'
open:
true
middleware:
(connect,
options)
-‐>
middlewares
=
[];
if
(!Array.isArray(options.base))
options.base
=
[options.base]
directory
=
options.directory
||
options.base[options.base.length
-‐
1]
options.base.forEach
(base)
-‐>
#
Serve
static
files.
middlewares.push(connect.static(base))
middlewares.push
urlrouter(server.drawRoutes)
#
Make
directory
browse-‐able.
middlewares.push
connect.directory(directory)
middlewares
copy:
imagesDev:
files:
[{
expand:
true
cwd:
"app/img/"
src:
"**"
dest:
"generated/<%=
files.img.root
%>/"
}
{
expand:
true
cwd:
"vendor/img/"
src:
"**"
dest:
"generated/<%=
files.img.root
%>/"
}]
imagesDist:
files:
[{
expand:
true
cwd:
"app/img/"
src:
"**"
dest:
"dist/<%=
files.img.root
%>/"
}
{
expand:
true
cwd:
"vendor/img/"
src:
"**"
dest:
"dist/<%=
files.img.root
%>/"
}]
staticDev:
files:
[
expand:
true
cwd:
"app/static"
src:
"**"
dest:
'generated'
]
staticDist:
files:
[
expand:
true
cwd:
"app/static"
src:
"**"
dest:
'dist'
]
webfontsDev:
files:
[{
expand:
true
cwd:
"vendor/webfonts/"
src:
"**"
dest:
"generated/<%=
files.webfonts.root
%>/"
}
{
expand:
true
cwd:
"vendor/components/font-‐awesome/fonts/"
src:
"**"
dest:
"generated/<%=
files.webfonts.root
%>/"
}]
webfontsDist:
files:
[{
expand:
true
cwd:
"vendor/webfonts/"
src:
"**"
dest:
"dist/<%=
files.webfonts.root
%>/"
}
{
expand:
true
cwd:
"vendor/components/font-‐awesome/fonts/"
src:
"**"
dest:
"dist/<%=
files.webfonts.root
%>/"
}]
cssmin:
compress:
files:
"<%=
files.css.minified
%>":
"<%=
files.css.concatenated
%>"
jade:
dev:
options:
pretty:
true
data:
js:
"<%=
files.js.minifiedWebRelative
%>"
jsVendor:
"<%=
files.js.minifiedVendorWebRelative
%>"
css:
"<%=
files.css.minifiedWebRelative
%>"
pkg:
"<%=
pkg
%>"
files:
[{
expand:
true
src:
"<%=
files.jade.pages
%>"
cwd:
"<%=
files.jade.pageRoot
%>"
dest:
"generated/"
ext:
".html"
}]
dist:
options:
data:
js:
"<%=
minifiedWebRelative
%>"
jsVendor:
"<%=
minifiedVendorWebRelative
%>"
css:
"<%=
files.css.minifiedWebRelative
%>"
pkg:
"<%=
pkg
%>"
files:
[{
expand:
true
src:
"<%=
files.jade.pages
%>"
cwd:
"<%=
files.jade.pageRoot
%>"
dest:
"dist/"
ext:
".html"
}]
jshint:
files:
["<%=
files.js.app
%>"]
options:
#
enforcing
options
curly:
true
eqeqeq:
true
latedef:
true
newcap:
true
noarg:
true
#
relaxing
options
boss:
true
eqnull:
true
sub:
true
#
environment/globals
browser:
true
less:
options:
paths:
["app/css",
"vendor/css"]
compile:
files:
"<%=
files.less.generatedVendor
%>":
"<%=
files.less.vendor
%>"
"<%=
files.less.generatedApp
%>":
"<%=
files.less.app
%>"
uglify:
options:
banner:
'/*!
<%=
pkg.name
%>
<%=
grunt.template.today("yyyy-‐mm-‐dd")
%>
*/\n'
js:
files:
"<%=
files.js.minified
%>":
"<%=
files.js.concatenated
%>"
jsVendor:
files:
"<%=
files.js.minifiedVendor
%>":
"<%=
files.js.concatenatedVendor
%>"
clean:
bower:
src:
bowerDirectory
grunt
js:
src:
"<%=
files.js.concatenated
%>"
css:
src:
"<%=
files.css.concatenated
%>"
dist:
src:
["dist",
"generated"]
watch:
coffee:
files:
"<%=
files.coffee.app
%>"
tasks:
["coffeelint",
"coffee",
"concat:js"]
css:
files:
["<%=
files.css.vendor
%>",
"<%=
files.css.app
%>"]
tasks:
["concat:css"]
images:
files:
["app/img/**/*.*",
"vendor/img/**/*.*"]
tasks:
["copy:imagesDev"]
jade:
files:
["<%=
files.jade.pageRoot
%>/<%=
files.jade.pages
%>"]
tasks:
["jade:dev"]
js:
files:
["<%=
files.js.vendor
%>",
"<%=
files.js.app
%>"]
tasks:
["concat:js"]
less:
files:
[
"<%=
files.less.vendor
%>"
"<%=
files.less.watch
%>"
]
tasks:
["less",
"concat:css"]
lint:
files:
"<%=
files.js.app
%>"
tasks:
["jshint"]
webfonts:
files:
["vendor/webfonts/**/*.*",
"vendor/components/font-‐awes
tasks:
["copy:webfontsDev"]
livereload:
options:
livereload:
true
files:
"dist/**/*.*"
grunt.loadNpmTasks
'grunt-‐bower-‐task'
grunt.loadNpmTasks
'grunt-‐coffeelint'
grunt.loadNpmTasks
'grunt-‐contrib-‐clean'
grunt.loadNpmTasks
'grunt-‐contrib-‐coffee'
grunt.loadNpmTasks
'grunt-‐contrib-‐concat'
grunt.loadNpmTasks
'grunt-‐contrib-‐connect'
grunt.loadNpmTasks
'grunt-‐contrib-‐copy'
grunt.loadNpmTasks
'grunt-‐contrib-‐cssmin'
grunt.loadNpmTasks
'grunt-‐contrib-‐jshint'
grunt.loadNpmTasks
'grunt-‐contrib-‐less'
grunt.loadNpmTasks
'grunt-‐contrib-‐jade'
grunt.loadNpmTasks
'grunt-‐contrib-‐uglify'
grunt.loadNpmTasks
'grunt-‐contrib-‐watch'
grunt.registerTask
'default',
[
'common'
'dev'
]
grunt.registerTask
'common',
[
'bower'
'coffeelint'
'jshint'
'coffee'
'less'
'concat'
'copy:staticDev'
'copy:imagesDev'
'copy:webfontsDev'
'jade:dev'
]
grunt.registerTask
'dev',
[
'connect'
'watch'
]
grunt.registerTask
'dist',
[
'uglify'
'cssmin'
'copy:staticDist'
'copy:imagesDist'
'copy:webfontsDist'
'jade:dist'
]
bowerDirectory
=
(grunt)
-‐>
bowerrc
=
path.join(process.cwd(),
".bowerrc")
bowerConfig
=
grunt.file.readJSON(bowerrc)
unless
!fs.existsSync(bow
bowerConfig?.directory
||
"vendor/components"
gruntfile.coffee
├──
css
│
├──
app.less
│
├──
mixins.less
│
└──
variables.less
├──
img
│
├──
arana-‐software.png
│
├──
arana-‐[email protected]
│
├──
banner-‐lg.png
│
├──
banner-‐md.png
│
├──
banner-‐sm.png
│
├──
crown.png
│
├──
[email protected]
│
├──
[email protected]
│
└──
wood.png
├──
js
│
├──
app.coffee
│
├──
controllers
│
│
└──
board.coffee
│
├──
directives
│
│
└──
board.js
│
└──
templates
│
└──
board.coffee
├──
pages
│
└──
index.jade
└──
static
└──
favicon.ico
'use
strict';
var
util
=
require('util');
var
Orchestrator
=
require('orchestrator');
var
gutil
=
require('gulp-‐util');
var
deprecated
=
require('deprecated');
var
vfs
=
require('vinyl-‐fs');
function
Gulp(){
Orchestrator.call(this);
}
util.inherits(Gulp,
Orchestrator);
Gulp.prototype.task
=
Gulp.prototype.add;
Gulp.prototype.run
=
function(){
//
run()
is
deprecated
as
of
3.5
and
will
be
removed
in
4.0
//
use
task
dependencies
instead
//
impose
our
opinion
of
"default"
tasks
onto
orchestrator
var
tasks
=
arguments.length
?
arguments
:
['default'];
this.start.apply(this,
tasks);
};
Gulp.prototype.src
=
vfs.src;
Gulp.prototype.dest
=
vfs.dest;
Gulp.prototype.watch
=
function
(glob,
opt,
fn)
{
if
(!fn)
{
fn
=
opt;
opt
=
null;
}
//
array
of
tasks
given
if
(Array.isArray(fn))
{
return
vfs.watch(glob,
opt,
function(){
this.start.apply(this,
fn);
}.bind(this));
}
return
vfs.watch(glob,
opt,
fn);
};
//
let
people
use
this
class
from
our
instance
Gulp.prototype.Gulp
=
Gulp;
//
deprecations
deprecated.field('gulp.env
has
been
deprecated.
Use
gulp-‐util.env
or
your
own
CLI
parser
instead.',
console.log,
Gulp.prototype,
'env',
gutil.env);
Gulp.prototype.run
=
deprecated.method('gulp.run()
has
been
deprecated.
Use
task
dependencies
or
gulp.watch
task
triggering
instead.',
console.log,
Gulp.prototype.run);
var
inst
=
new
Gulp();
module.exports
=
inst;
.src(globs[, options])
.dest(path)
.task(name[, deps], fn)
.watch(glob [, options], tasks)
.pipe(destination)
level up
grok streams
a task: read, concatenate, write
additional steps add overhead
extraneous disk I/O
extraneous configuration
streams pipe from task to task
additional steps without overhead
level up
the first gulp
npm install -g gulp
npm install -D gulp
touch gulpfile.js
//gulpfile.js
var
gulp
=
require('gulp');
var
coffee
=
require('gulp-‐coffee');
var
concat
=
require('gulp-‐concat');
var
uglify
=
require('gulp-‐uglify');
gulp.src('app/js/**/*.coffee')
gulp.src('app/js/**/*.coffee')
.pipe(gulp.dest('dist/js'));
gulp.src('app/js/**/*.coffee')
.pipe(coffee())
.pipe(concat('app.js'))
.pipe(uglify())
.pipe(gulp.dest('dist/js'));
gulp.src('app/js/**/*.coffee')
.pipe(coffee())
.pipe(concat('app.js'))
.pipe(gulp.dest('test/js'))
.pipe(uglify())
.pipe(gulp.dest('dist/js'));
gulp.task('coffee',
function()
{
gulp.src('app/js/**/*.coffee')
.pipe(coffee())
.pipe(concat('app.js'))
.pipe(gulp.dest('test/js'))
.pipe(uglify())
.pipe(gulp.dest('dist/js'));
});
gulp.task('watch',
function()
{
gulp.watch('app/js/**/*.coffee',
['coffee']);
});
gulp.task('default',
['coffee','watch']);
level up
dependencies
gulp.task('css',
function()
{
gulp.src('app/css/app.less')
.pipe(less())
.pipe(cssmin())
.pipe(gulp.dest('dist/css'));
});
var
bower
=
require('gulp-‐bower');
gulp.task('install',
function()
{
bower();
});
gulp.task('css',
['install'],
function()
{
gulp.src('app/css/app.less')
.pipe(less())
.pipe(cssmin())
.pipe(gulp.dest('dist/css'));
});
var
bower
=
require('gulp-‐bower');
gulp.task('install',
function()
{
bower();
});
//
Doesn't
work
as
expected
our five tasks
┌
│
┤
│
└
┬
│
└
─
expected dependency tree
┌
│
├
│
┼
│
├
│
└
actual tree
gulp.task('stuff',
function()
{
doStuff();
});
//
Option
1:
Callback
gulp.task('stuff',
function(done)
{
doSyncStuff()
done(err);
});
var
Q
=
require('q');
//
Option
2:
Promise
gulp.task('stuff',
function()
{
var
deferred
=
Q.defer();
doAsyncStuff(deferred.resolve);
return
deferred.promise;
});
//
Option
3:
Return
stream
gulp.task('stuff',
function()
{
var
stream
=
doStreamStuff();
return
stream;
});
gulp.task
'stuff',
()
-‐>
doStreamStuff()
#
Get
return
stream
for
free!
gulp.task
'stuff',
()
-‐>
doStreamStuff()
gulp.task('stuff',
function()
{
return
doStreamStuff();
});
var
bower
=
require('gulp-‐bower');
gulp.task('install',
function()
{
bower();
});
var
bower
=
require('gulp-‐bower');
gulp.task('install',
function()
{
return
bower();
//
Win!
});
//
Back
to
where
we
were
gulp.task('css',
function()
{
gulp.src('app/css/app.less')
.pipe(less())
.pipe(cssmin())
.pipe(gulp.dest('dist/css'));
});
gulp.task('css',
['install'],
function()
{
gulp.src('app/css/app.less')
.pipe(less())
.pipe(cssmin())
.pipe(gulp.dest('dist/css'));
});
level up
using coffee
//gulpfile.js
gulp.task('css',
['install'],
function()
{
gulp.src('app/css/app.less')
.pipe(less())
.pipe(cssmin())
.pipe(gulp.dest('dist/css'));
});
#
gulpfile.coffee
gulp.task
'css',
['install'],
()
-‐>
gulp.src
'app/css/app.less'
.pipe
less()
.pipe
cssmin()
.pipe
gulp.dest('dist/css')
gulp --require coffee-script/register
//gulpfile.js
require('coffee-‐script');
require('./gulpfile.coffee');
gulp
more gulpfile.js
level up
merging streams
gulp.task
'js',
()
-‐>
gulp.src
files.coffee
.pipe
coffee()
.pipe
concat('app.js')
.pipe
uglify()
.pipe
gulp.dest('dist/js')
evtstream
=
require
'event-‐stream'
#
and/or
streamq
=
require
'streamqueue'
gulp.task
'js',
()
-‐>
gulp.src(files.coffee)
.pipe(coffee())
.pipe
concat('app.js')
.pipe
uglify()
.pipe
gulp.dest('dist/js')
gulp.task
'js',
()
-‐>
evtstream.concat(
gulp.src(files.coffee)
.pipe(coffee()),
gulp.src(files.js))
.pipe
concat('app.js')
.pipe
uglify()
.pipe
gulp.dest('dist/js')
gulp.task
'js',
()
-‐>
es.concat(
gulp.src(files.coffee)
.pipe(coffee()),
gulp.src(files.js))
.pipe
concat('app.js')
.pipe
uglify()
.pipe
gulp.dest('dist/js')
gulp.task
'js',
()
-‐>
sq
=
streamq
{objectmode:true}
sq.queue
gulp.src(files.coffee)
.pipe(coffee()
sq.queue
gulp.src(files.js)
sq.done().pipe
concat('app.js')
.pipe
uglify()
.pipe
gulp.dest('dist/js')
level up
error handling
gulp.task
'css',
['install'],
()
-‐>
gulp.src
files.less
.pipe
less()
.pipe
cssmin()
.pipe
gulp.dest('dist/css')
//
app.less
.mayhem
{
font-‐weight:
bold;
color:
@red;
}
}
//
too
many
'{'
===
BOOM!
[gulp]
Running
'css'...
events.js:72
throw
er;
//
Unhandled
'error'
^
Error:
missing
opening
`{`
in
file
./c
gulp.task
'css',
['install'],
()
-‐>
gulp.src
files.less
.pipe
less()
.pipe
cssmin()
.pipe
gulp.dest('dist/css')
gulp.task
'css',
['install'],
()
-‐>
gulp.src
files.less
.pipe
less().on('error',(err)-‐>
console.log(''+err)
if
err
)
#
Ick.
.pipe
cssmin()
.pipe
gulp.dest('dist/css')
plumber
=
require
'gulp-‐plumber'
gulp.task
'css',
['install'],
()
-‐>
gulp.src
files.less
.pipe
plumber()
#Win!
.pipe
less()
.pipe
cssmin()
.pipe
gulp.dest('dist/css')
[gulp]
Running
'css'...
[gulp]
Error
in
plugin
'gulp-‐less':
missing
opening
`{`
in
file
app.less
[gulp]
Finished
'css'
in
21
ms
level up
master class
gutil
=
require
'gulp-‐util'
gulp.src
'app/css/*.css'
.pipe
concat('app.css')
.pipe(if
gutil.env.dest
==
'prod'
then
cssmin()
else
gutil.noop())
.pipe
gulp.dest('dist/css')
gulp.src
'app/css/*.css'
.pipe
concat('app.css')
.pipe(if
gutil.env.dest
==
'prod'
then
cssmin()
else
gutil.noop())
.pipe
gulp.dest('dist/css')
gulp
-‐-‐port=8000
-‐-‐dest=prod
gutil.env.port
===
8000
gutil.env.dest
===
'prod'
echo
Total
Files:
$(
\
find
.
-‐type
f
-‐print\
|
wc
-‐l)
Total
Files:
1000
echo
watch
will
vomit
level up
comparison
timer
=
require
"grunt-‐timer"
fs
=
require
'fs'
path
=
require
'path'
server
=
require
'./config/server'
urlrouter
=
require
'urlrouter'
module.exports
=
(grunt)
-‐>
timer.init
grunt
#
Project
configuration.
grunt.initConfig
pkg:
grunt.file.readJSON
'package.json'
files:
coffee:
app:
"app/js/**/*.coffee"
generated:
"generated/js/app.coffee.js"
css:
vendor:
"vendor/css/**/*.css"
app:
"app/css/**/*.css"
concatenated:
"generated/css/app.css"
minified:
"dist/css/app.css"
minifiedWebRelative:
"css/app.css"
img:
root:
"img"
jade:
pages:
"**/*.jade"
pageRoot:
"app/pages/"
js:
app:
"app/js/**/*.js"
vendor:
[
"vendor/components/jquery/jquery.js"
"vendor/components/underscore/underscore.js"
"vendor/components/angular/angular.js"
"vendor/js/**/*.js"
]
concatenatedVendor:
"generated/js/vendor.js"
minifiedVendor:
"dist/js/vendor.js"
minifiedVendorWebRelative:
"js/vendor.js"
concatenated:
"generated/js/app.js"
minified:
"dist/js/app.js"
minifiedWebRelative:
"js/app.js"
less:
app:
"app/css/app.less"
vendor:
"vendor/css/**/*.less"
generatedApp:
"generated/css/app.less.css"
generatedVendor:
"generated/css/vendor.less.css"
watch:
"app/css/**/*.less"
webfonts:
root:
"fonts"
bower:
install:
options:
copy:
false
coffee:
compile:
files:
"<%=
files.coffee.generated
%>":
"<%=
files.coffee.app
%>"
coffeelint:
app:
[
"<%=
files.coffee.app
%>"
]
concat:
css:
src:
[
"<%=
files.less.generatedVendor
%>"
"<%=
files.css.vendor
%>"
"<%=
files.less.generatedApp
%>"
"<%=
files.css.app
%>"
]
dest:
"<%=
files.css.concatenated
%>"
js:
src:
[
"<%=
files.coffee.generated
%>"
"<%=
files.js.app
%>"
]
dest:
"<%=
files.js.concatenated
%>"
jsVendor:
src:
["<%=
files.js.vendor
%>"]
dest:
"<%=
files.js.concatenatedVendor
%>"
connect:
server:
options:
port:
8000
base:
'generated'
open:
true
middleware:
(connect,
options)
-‐>
middlewares
=
[];
if
(!Array.isArray(options.base))
options.base
=
[options.base]
directory
=
options.directory
||
options.base[options.base.length
-‐
1]
options.base.forEach
(base)
-‐>
#
Serve
static
files.
middlewares.push(connect.static(base))
middlewares.push
urlrouter(server.drawRoutes)
#
Make
directory
browse-‐able.
middlewares.push
connect.directory(directory)
middlewares
copy:
imagesDev:
files:
[{
expand:
true
cwd:
"app/img/"
src:
"**"
dest:
"generated/<%=
files.img.root
%>/"
}
{
expand:
true
cwd:
"vendor/img/"
src:
"**"
dest:
"generated/<%=
files.img.root
%>/"
}]
imagesDist:
files:
[{
expand:
true
cwd:
"app/img/"
src:
"**"
dest:
"dist/<%=
files.img.root
%>/"
}
{
expand:
true
cwd:
"vendor/img/"
src:
"**"
dest:
"dist/<%=
files.img.root
%>/"
}]
staticDev:
files:
[
expand:
true
cwd:
"app/static"
src:
"**"
dest:
'generated'
]
staticDist:
files:
[
expand:
true
cwd:
"app/static"
src:
"**"
dest:
'dist'
]
webfontsDev:
files:
[{
expand:
true
cwd:
"vendor/webfonts/"
src:
"**"
dest:
"generated/<%=
files.webfonts.root
%>/"
}
{
expand:
true
cwd:
"vendor/components/font-‐awesome/fonts/"
src:
"**"
dest:
"generated/<%=
files.webfonts.root
%>/"
}]
webfontsDist:
files:
[{
expand:
true
cwd:
"vendor/webfonts/"
src:
"**"
dest:
"dist/<%=
files.webfonts.root
%>/"
}
{
expand:
true
cwd:
"vendor/components/font-‐awesome/fonts/"
src:
"**"
dest:
"dist/<%=
files.webfonts.root
%>/"
}]
cssmin:
compress:
files:
"<%=
files.css.minified
%>":
"<%=
files.css.concatenated
%>"
jade:
dev:
options:
pretty:
true
data:
js:
"<%=
files.js.minifiedWebRelative
%>"
jsVendor:
"<%=
files.js.minifiedVendorWebRelative
%>"
css:
"<%=
files.css.minifiedWebRelative
%>"
pkg:
"<%=
pkg
%>"
files:
[{
expand:
true
src:
"<%=
files.jade.pages
%>"
cwd:
"<%=
files.jade.pageRoot
%>"
dest:
"generated/"
ext:
".html"
}]
dist:
options:
data:
js:
"<%=
minifiedWebRelative
%>"
jsVendor:
"<%=
minifiedVendorWebRelative
%>"
css:
"<%=
files.css.minifiedWebRelative
%>"
pkg:
"<%=
pkg
%>"
files:
[{
expand:
true
src:
"<%=
files.jade.pages
%>"
cwd:
"<%=
files.jade.pageRoot
%>"
dest:
"dist/"
ext:
".html"
}]
jshint:
files:
["<%=
files.js.app
%>"]
options:
#
enforcing
options
curly:
true
eqeqeq:
true
latedef:
true
newcap:
true
noarg:
true
#
relaxing
options
boss:
true
eqnull:
true
sub:
true
#
environment/globals
browser:
true
less:
options:
paths:
["app/css",
"vendor/css"]
compile:
files:
"<%=
files.less.generatedVendor
%>":
"<%=
files.less.vendor
%>"
"<%=
files.less.generatedApp
%>":
"<%=
files.less.app
%>"
uglify:
options:
banner:
'/*!
<%=
pkg.name
%>
<%=
grunt.template.today("yyyy-‐mm-‐dd")
%>
*/\n'
js:
files:
"<%=
files.js.minified
%>":
"<%=
files.js.concatenated
%>"
jsVendor:
files:
"<%=
files.js.minifiedVendor
%>":
"<%=
files.js.concatenatedVendor
%>"
clean:
bower:
src:
bowerDirectory
grunt
js:
src:
"<%=
files.js.concatenated
%>"
css:
src:
"<%=
files.css.concatenated
%>"
dist:
src:
["dist",
"generated"]
watch:
coffee:
files:
"<%=
files.coffee.app
%>"
tasks:
["coffeelint",
"coffee",
"concat:js"]
css:
files:
["<%=
files.css.vendor
%>",
"<%=
files.css.app
%>"]
tasks:
["concat:css"]
images:
files:
["app/img/**/*.*",
"vendor/img/**/*.*"]
tasks:
["copy:imagesDev"]
jade:
files:
["<%=
files.jade.pageRoot
%>/<%=
files.jade.pages
%>"]
tasks:
["jade:dev"]
js:
files:
["<%=
files.js.vendor
%>",
"<%=
files.js.app
%>"]
tasks:
["concat:js"]
less:
files:
[
"<%=
files.less.vendor
%>"
"<%=
files.less.watch
%>"
]
tasks:
["less",
"concat:css"]
lint:
files:
"<%=
files.js.app
%>"
tasks:
["jshint"]
webfonts:
files:
["vendor/webfonts/**/*.*",
"vendor/components/font-‐awesome/fonts/**/*.*"]
tasks:
["copy:webfontsDev"]
livereload:
options:
livereload:
true
files:
"dist/**/*.*"
grunt.loadNpmTasks
'grunt-‐bower-‐task'
grunt.loadNpmTasks
'grunt-‐coffeelint'
grunt.loadNpmTasks
'grunt-‐contrib-‐clean'
grunt.loadNpmTasks
'grunt-‐contrib-‐coffee'
grunt.loadNpmTasks
'grunt-‐contrib-‐concat'
grunt.loadNpmTasks
'grunt-‐contrib-‐connect'
grunt.loadNpmTasks
'grunt-‐contrib-‐copy'
grunt.loadNpmTasks
'grunt-‐contrib-‐cssmin'
grunt.loadNpmTasks
'grunt-‐contrib-‐jshint'
grunt.loadNpmTasks
'grunt-‐contrib-‐less'
grunt.loadNpmTasks
'grunt-‐contrib-‐jade'
grunt.loadNpmTasks
'grunt-‐contrib-‐uglify'
grunt.loadNpmTasks
'grunt-‐contrib-‐watch'
grunt.registerTask
'default',
[
'common'
'dev'
]
grunt.registerTask
'common',
[
'bower'
'coffeelint'
'jshint'
'coffee'
'less'
'concat'
'copy:staticDev'
'copy:imagesDev'
'copy:webfontsDev'
'jade:dev'
]
grunt.registerTask
'dev',
[
'connect'
'watch'
]
grunt.registerTask
'dist',
[
'uglify'
'cssmin'
'copy:staticDist'
'copy:imagesDist'
'copy:webfontsDist'
'jade:dist'
]
bowerDirectory
=
(grunt)
-‐>
bowerrc
=
path.join(process.cwd(),
".bowerrc")
bowerConfig
=
grunt.file.readJSON(bowerrc)
unless
!fs.existsSync(bowerrc)
bowerConfig?.directory
||
"vendor/components"
gruntfile.coffee
gulp
=
require
'gulp'
gutil
=
require
'gulp-‐util'
fs
=
require
'fs'
path
=
require
'path'
bower
=
require
'gulp-‐bower'
clean
=
require
'gulp-‐clean'
coffee
=
require
'gulp-‐coffee'
coffeelint
=
require
'gulp-‐coffeelint'
concat
=
require
'gulp-‐concat'
jade
=
require
'gulp-‐jade'
jslint
=
require
'gulp-‐jshint'
jslintReporter
=
require
'jshint-‐stylish'
less
=
require
'gulp-‐less'
cssmin
=
require
'gulp-‐minify-‐css'
connect
=
require
'gulp-‐connect'
uglify
=
require
'gulp-‐uglify'
es
=
require
'event-‐stream'
pkg
=
require
'./package.json'
server
=
require
'./config/server'
urlrouter
=
require
'urlrouter'
output
=
css:
'css/app.css'
jsApp:
'js/app.js'
jsVendor:
'js/vendor.js'
files
=
coffee:
'app/js/**/*.coffee'
img:
'app/img/**/*.*'
static:
'app/static/**/*.*'
webfonts:
[
'vendor/webfonts/**/*.*'
'vendor/components/font-‐awesome/fonts/**/*.*'
]
jade:
'app/pages/**/*.jade'
js:
app:
['app/js/**/*.js']
vendor:
[
'vendor/components/jquery/jquery.min.js'
'vendor/components/underscore/underscore-‐min.js'
'vendor/components/angular/angular.min.js'
'vendor/js/**/*.js'
]
less:
app:
'app/css/app.less'
watch:
[
'app/css/**'
'vendor/components/bootstrap/less/**'
]
config
=
jshint:
#
enforcing
options
curly:
true
eqeqeq:
true
latedef:
true
newcap:
true
noarg:
true
#
relaxing
options
boss:
true
eqnull:
true
sub:
true
#
environment/globals
browser:
true
jade:
pretty:
true
data:
js:
output.jsApp
jsVendor:
output.jsVendor
css:
output.css
pkg:
pkg
server:
port:
8000
base:
'generated'
livereload:
true
open:
true
middleware:
(connect,
options)
-‐>
middlewares
=
[];
if
(!Array.isArray(options.base))
options.base
=
[options.base]
directory
=
options.directory
||
options.base[options.base.length
-‐
1]
options.base.forEach
(base)
-‐>
#
Serve
static
files.
middlewares.push(connect.static(base))
middlewares.push
urlrouter(server.drawRoutes)
#
Make
directory
browse-‐able.
middlewares.push
connect.directory(directory)
middlewares
gulp.task
'default',
['lint','build']
gulp.task
'run',
['lint','build','server','watch']
gulp.task
'build',
[
'install'
'js'
'css'
'jade'
'copy'
]
gulp.task
'install',
()
-‐>
bower()
gulp.task
'lint',
['coffeelint','jslint']
gulp.task
'coffeelint',
()
-‐>
gulp.src(files.coffee)
.pipe(coffeelint())
.pipe(coffeelint.reporter())
gulp.task
'jslint',
()
-‐>
gulp.src(files.js.app)
.pipe(jslint(config.jshint))
.pipe(jslint.reporter(jslintReporter))
gulp.task
'jade',
()
-‐>
gulp.src(files.jade)
.pipe(jade(config.jade))
.pipe(gulp.dest('./generated'))
.pipe(gulp.dest('./dist'))
gulp.task
'jsApp',
()
-‐>
es.concat(
gulp.src(files.coffee).pipe(coffee()),
gulp.src(files.js.app)
).pipe(concat(output.jsApp))
.pipe(gulp.dest('./generated'))
.pipe(uglify())
.pipe(gulp.dest('./dist'))
gulp.task
'jsVendor',
['install'],
()
-‐>
gulp.src(files.js.vendor)
.pipe(concat(output.jsVendor))
.pipe(gulp.dest('./generated'))
.pipe(gulp.dest('./dist'))
gulp.task
'js',
['jsApp','jsVendor']
gulp.task
'css',
['install'],
()
-‐>
gulp.src(files.less.app)
.pipe(plumber())
.pipe(less())
.pipe(concat(output.css))
.pipe(gulp.dest('./generated'))
.pipe(cssmin())
.pipe(gulp.dest('./dist'))
gulp.task
'clean',
()
-‐>
gulp.src(['./dist','./generated',
bowerDirectory()])
.pipe(clean())
gulp.task
'copy',
['install'],
()
-‐>
es.concat(
gulp.src(files.img)
.pipe(gulp.dest('./generated/img'))
.pipe(gulp.dest('./dist/img')),
gulp.src(files.static)
.pipe(gulp.dest('./generated/'))
.pipe(gulp.dest('./dist/')),
gulp.src(files.webfonts)
.pipe(gulp.dest('./generated/fonts'))
.pipe(gulp.dest('./dist/fonts'))
)
gulp.task
'watch',
()
-‐>
gulp.watch
files.coffee,
['coffeelint','jsApp']
gulp.watch
files.js.app,
['jslint','jsApp']
gulp.watch
files.js.vendor,
['jsVendor']
gulp.watch
files.jade.pages,
['jade']
gulp.watch
files.less.watch,
['css']
gulp.watch
[files.img,
files.webfonts,
files.static],
['copy']
gulp.task
'server',
['build'],
connect.server(config.server)
bowerDirectory
=
()
-‐>
bowerpath
=
path.join(process.cwd(),
".bowerrc")
bowerrc
=
fs.readFileSync(bowerpath)
unless
!fs.existsSync
bowerpath
bowerConfig
=
JSON.parse(bowerrc)
if
bowerrc?
bowerConfig?.directory
||
"vendor/components"
gulpfile.coffee
grunt.loadNpmTasks('grunt-‐bower-‐task');
grunt.loadNpmTasks('grunt-‐coffeelint');
grunt.loadNpmTasks('grunt-‐contrib-‐clean');
grunt.loadNpmTasks('grunt-‐contrib-‐coffee');
grunt.loadNpmTasks('grunt-‐contrib-‐concat');
grunt.loadNpmTasks('grunt-‐contrib-‐connect');
grunt.loadNpmTasks('grunt-‐contrib-‐copy');
grunt.loadNpmTasks('grunt-‐contrib-‐cssmin');
grunt.loadNpmTasks('grunt-‐contrib-‐jade');
grunt.loadNpmTasks('grunt-‐contrib-‐jshint');
grunt.loadNpmTasks('grunt-‐contrib-‐less');
grunt.loadNpmTasks('grunt-‐contrib-‐uglify');
grunt.loadNpmTasks('grunt-‐contrib-‐watch');
bower
=
require
'gulp-‐bower'
coffeelint
=
require
'gulp-‐coffeelint'
clean
=
require
'gulp-‐clean'
coffee
=
require
'gulp-‐coffee'
concat
=
require
'gulp-‐concat'
connect
=
require
'gulp-‐connect'
#
copy
included
via
.dest
cssmin
=
require
'gulp-‐minify-‐css'
jade
=
require
'gulp-‐jade'
jslint
=
require
'gulp-‐jshint'
less
=
require
'gulp-‐less'
uglify
=
require
'gulp-‐uglify'
#
watch
included
via
.watch
git
co
grunt
time
grunt
>
/dev/null
real
0m5.114s
user
0m4.802s
sys
0m0.214s
git
co
gulp
time
gulp
>
/dev/null
real
0m2.811s
user
0m2.601s
sys
0m0.241s
git
co
grunt
time
grunt
>
/dev/null
real
0m5.114s
user
0m4.802s
sys
0m0.214s
learn more
gulpjs.com
gulpjs/gulp
gulpjs
demo code
queenseight.com
aranasoft/queenseight
git checkout grunt
git checkout gulp
npm install gulp
N E X T S T E P S
dethrone grunt
[email protected]
@ j a y h a r r i s
# d e t h r o n i n g G r u n t
thank you
@ j a y h a r r i s
# d e t h r o n i n g G r u n t