Andrew Welch
Published , updated · 5 min read · RSS Feed
Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.
A Gulp Workflow for Frontend Development Automation
Gulp is a workflow automation tool that helps you build cool stuff faster; here’s how to use it for frontend development.
As websites become more complicated to build, a frontend workflow automation tool of some sort becomes a necessity. Gulp is one such tool:
gulp is a toolkit for automating painful or time-consuming tasks in your development workflow, so you can stop messing around and build something.
The above quote from the GulpJS.com website says it well. But before we get into using Gulp specifically, let’s talk about why we even need a thing that automates our frontend workflow.
Back in the day, we created some CSS, we edited some HTML, and maybe included a JavaScript or two on the website, and called it a day. Websites have grown up significantly since their days as “online brochures”, and now we are creating somewhat complicated software applications that happen to be deployed via the web.
To help manage this complexity, we can use a frontend workflow automation tool that works like an assembly line, putting all of the pieces together for us.
Computers are really, really good at being given a list of things to do, and executing them in a deterministic manner every single time. This happens to be something that humans are not that great at; we are better at higher level architecture and creative thinking.
So let’s have computer do what they’re good at, and allow us to focus on making cool stuff
While each website we work on is different in terms of style and content, at an abstract level they all contain the same “things” that end up getting built and combined to make the final website.
From a conceptual level, this type of “digital assembly line” used to build stuff has been around since the Make tool was created in 1976 by Stuart Feldman. Whenever a project reaches a certain level of complexity, the time spent building an automation workflow ends up saving you time.
Modern websites reached that level of complexity some time ago. As stated in the Frontend Dev Best Practices for 2017 article, “you really need a frontend workflow automation tool if you’re going to keep your sanity.”
Link What does our Gulpfile do?
So what exactly does our Gulp frontend workflow automation do for us? Here’s a rundown:
- Handles our CSS
- Compiles all of our SCSS down to CSS, using a cache to make it faster
- Auto-prefixes our CSS for the browsers we support
- Creates SourceMaps for our CSS to make debugging easy
- Pulls in CSS from any third-party modules/packages we use
- Combines and minifies all of our CSS
- Handles our JavaScript
- Transpiles all of the JavaScript we write from ES6 to something web browsers support
- Pulls in JavaScripts from any third-party modules/packages we use
- Uglifies our JavaScripts
- Pulls in any JavaScripts we need to inline in our HTML separately
- Handles live reloading
- CSS/SCSS changes cause an immediate browser repaint without a page load
- JavaScript changes cause the browser to reload the page
- Changes to our Twig/HTML templates cause the browser to reload the page
- Generates CriticalCSS for our website
- Runs an accessibility audit on our website
- Generates a custom icon font using only the glyphs we use via Fontello
- Generates all of the various favicons for our website (and the HTML code for them) from a single source image
- Losslessly minimizes all of the the images our website uses via imagemin
…and a bit more too! But that’s a quick overview of what a workflow automation can do for you. And it’s portable from project to project, because of separation of concerns that puts the data into our package.json.
For JavaScript-centric projects that use React or Vue for JAMstack-style websites, you’ll inevitably want to use webpack, because of all of the scaffolding that exists around it, and advanced features like code splitting, hot module reloading, etc. But for webpack to be used effectively, it really needs to be embraced whole-hog and used not just as a module bundler, but as a module loader as well.
There’s also an older frontend workflow automation tool called Grunt, and it works fine, but it can be somewhat more verbose to configure. It’s also in general slower than Gulp for building things, due to its file-oriented approach.
There’s even Laravel Mix, which adds a layer on top of webpack. I’ve found that it’s fantastic for bootstrapping projects, but whenever the project grows to any scale, its been necessary to have more control over the build process.
Viget has built a tool called Blendid! that uses a hybrid approach of utilizing both Gulp and webpack, and it looks pretty well done. But similar to Laravel Mix, I prefer a bit more control over the build process when necessary.
At some point, the layers upon layers ends up getting a bit silly as well.
In the end, all of these tools (Gulp included) simply execute Node.js JavaScript packages on the command line. Gulp just adds an API and streaming layer on top that makes doing typical frontend builds easier.
You could also just use npm scripts to execute the various Node.js modules directly, but I find the convenience layer that Gulp provides to be worth the tradeoff of another level of dependency.
There is even a GUI tool called CodeKit that offers some nice functionality, but I think ultimately it will end up being the DreamWeaver of the frontend automation tool world. Some things simply can’t be expressed as efficiently via a GUI, and there’s little chance it will be able to keep pace with the Node.js ecosystem.
There is no one true “best” build system tool, so don’t be dogmatic about it, and pick whatever the best tool for the job is.
For most frontend development projects, I find that Gulp strikes a nice mix between flexibility and automation. I can re-use my gulpfile.js and package.json for just about every project I do, and for the times when customization is needed, I can easily do so.
Tangent: Why are all of these frontend automation tools using Node.js? They certainly don’t have to be, they could be written in PHP, Perl, Ruby, Go, shell scripts (if you’re a masochist), or any language really. The reason is simply that frontend developers were already familiar with JavaScript, and it was natural to write the frontend automation tools they needed in JavaScript running via Node.
Link General Philosophy of Building Websites
Before we get into the nitty gritty of the actual gulpfile.js, it’s important to understand the general philosophy that I’m using to build websites. The gulpfile.js is something that helps me to build these websites, so understanding my overall approach is instructive.
In general, the websites that I work on follow the PRPL pattern, in that we want to load only what is needed to to render the initial “above the fold” web content, prefetch other likely needed resources, and then lazy load everything else asynchronously.
You can read more in detail about what this means in the Implementing Critical CSS on your website & ServiceWorkers and Offline Browsing articles, and more broadly the topic of website performance in the A Pretty Website Isn’t Enough & Creating Optimized Images in Craft CMS articles.
On a practical level, this means that we combine all of our site-wide CSS into a single site.combined.min.css file that gets loaded asynchronously, and we provide the initial page styling via Critical CSS.
The site-wide CSS consists of SCSS I author that gets built down to CSS, combined with CSS from any other third-party packages that I might use, and that all gets auto-prefixed and minimized.
We also inline a core set of JavaScript into each page that we use to load other things (CSS, JavaScript, etc.) asynchronously.
The JavaScript that I write is done using ES6 syntax, and then transpiled down to something that all web browsers can understand via Babel.
All third-party packages I use (whether CSS or JavaScript) are declared as dependencies in my package.json, and npm or yarn is used to install and/or update them via semver. Once again, you’ll find detail on all of this in the A Better package.json for the Frontend article.
Finally, the JavaScript we use gets uglified individually, and loaded asynchronously only on the pages where they are needed via dependency management. See the LoadJS as a Lightweight JavaScript Loader article for details on that.
Trying to do all of this “by hand” would be pretty much impossible; and yet this is how modern, performant webpages are built.
Link Project Tree
It’s also useful to see what the project looks like in terms of what the various directories are used for, and the overall organization.
Here’s what the root of my project directory looks like:
vagrant@homestead:~/sites/nystudio107$ tree -a -L 2 -I "node_modules|.git|scripts|.DS_Store|.idea" .
.
├── .babelrc
├── browserslist
├── build
│ ├── fonts
│ ├── html
│ └── js
├── craft
├── .csslintrc
├── .env.php
├── example.env.php
├── .git
├── .gitignore
├── .gitmodules
├── gulpfile.js
├── node_modules
├── package.json
├── public
│ ├── css
│ ├── favicon.ico
│ ├── favicon.png
│ ├── fonts
│ ├── htaccess
│ ├── imager
│ ├── img
│ ├── index.php
│ ├── js
│ ├── webappmanifest.json
│ └── web.config
├── readme.txt
├── scripts
├── src
│ ├── conf
│ ├── css
│ ├── fontello
│ ├── fonts
│ ├── img
│ ├── js
│ └── json
├── templates -> craft/templates/
└── yarn.lock
27 directories, 20 files
A couple directories deserve mention (these paths are all defined in the package.json):
- src/ — this is the directory where all of the things you author go. You own it, it’s the source for things that get built. The directory structure here mirrors that of public/
- build/ — an intermedia directory created by the build system for temporary file builds. The directory structure here mirrors that of public/
- public/js/ — where the built public distribution JavaScript gets put by the build system
- public/css/ — where the build public distribution CSS gets put by the build system
- node_modules/ — NPM packages downloaded via npm/yarn that are listed in the package.json that contain third party CSS/JS used for the frontend as well as NPM packages used for the build system itself
Link Taking a Gulp
So let’s have a look at how Gulp can be used to make our lives easier. This article is really a sister to the A Better package.json for the Frontend article, so if you haven’t read that yet, please do.
N.B.: This article gets pretty in the thick of things pretty quick. If you’re entirely new to Gulp, check out the Gulp for Beginners article for a primer.
The gulpfile.js presented here is what is used to build this very website that you’re reading now, and combined with the package.json file, they offer a nice baseline to start any project with.
This article assumes that you already have Node.js, NPM (or perhaps Yarn as well), and Gulp globally installed in your development environment.
We’ll present various chunks of our gulpfile.js out of order, but at the end, the full gulpfile.js will be shown for your reference.
Link Gulpfile.js Preamble
The very top of our gulpfile.js looks like this:
// package vars
const pkg = require("./package.json");
// gulp
const gulp = require("gulp");
// load all plugins in "devDependencies" into the variable $
const $ = require("gulp-load-plugins")({
pattern: ["*"],
scope: ["devDependencies"]
});
const onError = (err) => {
console.log(err);
};
const banner = [
"/**",
" * @project <%= pkg.name %>",
" * @author <%= pkg.author %>",
" * @build " + $.moment().format("llll") + " ET",
" * @release " + $.gitRevSync.long() + " [" + $.gitRevSync.branch() + "]",
" * @copyright Copyright (c) " + $.moment().format("YYYY") + ", <%= pkg.copyright %>",
" *",
" */",
""
].join("\n");
Much of this is discussed in-depth in the A Better package.json for the Frontend article, but briefly:
- First we require our package.json into the pkg constant, so we can access everything declared in the package.json conveniently from our gulpfile.js
- Next we require gulp, so that we have access to its streaming API and can utilize the various Gulp modules
- Then we use the gulp-load-plugins module to load in all of the npm modules that are listed as devDependencies, namespaced under the $ variable. This just makes our package.json tidy, without requiring dozens of require() statements for all of the modules that we use
- We set the onError constant to be an anonymous function that simply logs errors to console, again for convenience sake
- Finally, we set the banner constant to be nice banner that we can add to the top of our JavaScript/CSS that indicates when it was built, and so on.
If you’re coming from the frontend world, don’t get confused by the use of $ as a variable. It may look jQuery-ish, but it’s just a variable that could be named anything.
Link Primary Gulp tasks
So that was all of our gulpfile.js preamble, let’s jump down to the bottom of the file and look at the two major tasks that we execute from the command line:
// Default task
gulp.task("default", ["css", "js"], () => {
$.livereload.listen();
gulp.watch([pkg.paths.src.scss + "**/*.scss"], ["css"]);
gulp.watch([pkg.paths.src.css + "**/*.css"], ["css"]);
gulp.watch([pkg.paths.src.js + "**/*.js"], ["js"]);
gulp.watch([pkg.paths.templates + "**/*.{html,htm,twig}"], () => {
gulp.src(pkg.paths.templates)
.pipe($.plumber({errorHandler: onError}))
.pipe($.livereload());
});
});
// Production build
gulp.task("build", ["download", "default", "favicons", "imagemin", "fonts", "criticalcss"]);
These two tasks are typically all that we use to set our frontend automation into motion. They are roughly divided into two separate things:
- default — tasks that we use every day, which operate quickly
- build — tasks that we rarely use (usually just when doing an initial build, or a final deploy to production build), and can take some time to do their thing
A typical day involves sitting down, typing gulp and then working on various HTML, Twig, CSS, SCSS, JavaScript, etc. The default task builds our CSS & JavaScript, and then sits there watching our CSS/SCSS/JS files. If we change any of them, it rebuilds our site CSS or JavaScript as appropriate.
The default task also looks for any changes to our templates, and auto-reloads the web browser gulp-livereload; you simply need the livereload Chrome extension installed.
Link CSS Gulp Tasks
Let’s have a look at our css task, and any sub-tasks it triggers:
// scss - build the scss to the build folder, including the required paths, and writing out a sourcemap
gulp.task("scss", () => {
$.fancyLog("-> Compiling scss");
return gulp.src(pkg.paths.src.scss + pkg.vars.scssName)
.pipe($.plumber({errorHandler: onError}))
.pipe($.sourcemaps.init({loadMaps: true}))
.pipe($.sass({
includePaths: pkg.paths.scss
})
.on("error", $.sass.logError))
.pipe($.cached("sass_compile"))
.pipe($.autoprefixer())
.pipe($.sourcemaps.write("./"))
.pipe($.size({gzip: true, showFiles: true}))
.pipe(gulp.dest(pkg.paths.build.css));
});
// css task - combine & minimize any distribution CSS into the public css folder, and add our banner to it
gulp.task("css", ["scss"], () => {
$.fancyLog("-> Building css");
return gulp.src(pkg.globs.distCss)
.pipe($.plumber({errorHandler: onError}))
.pipe($.newer({dest: pkg.paths.dist.css + pkg.vars.siteCssName}))
.pipe($.print())
.pipe($.sourcemaps.init({loadMaps: true}))
.pipe($.concat(pkg.vars.siteCssName))
.pipe($.cssnano({
discardComments: {
removeAll: true
},
discardDuplicates: true,
discardEmpty: true,
minifyFontValues: true,
minifySelectors: true
}))
.pipe($.header(banner, {pkg: pkg}))
.pipe($.sourcemaps.write("./"))
.pipe($.size({gzip: true, showFiles: true}))
.pipe(gulp.dest(pkg.paths.dist.css))
.pipe($.filter("**/*.css"))
.pipe($.livereload());
});
The first parameter to a gulp.task() method is the name of the task; the second parameter are any dependences (or deps). Dependencies are tasks that need to be run before this task executes. In this way, you can chain tasks together.
So the first thing our css task does is it runs our scss task, to make sure all of our SCSS is compiled. Our scss task initializes our CSS sourcemaps, and then compiles our SCSS with any include paths, and caches the result. In this way, we don’t bother recompiling our SCSS if nothing has changed.
If something has changed, then we run our autoprefixer that looks for a browserslist file in our project root to determine what autoprefixing it should be doing:
# Supported browsers
last 3 versions
iOS >= 8
Then it writes out the sourcemaps, logs some useful sizing information, and writes out the built CSS to pkg.paths.build.css. We use an intermediate build file here so that our css task can just include the compiled CSS like any other file for our site-wide CSS bundle.
Our css task then makes sure that each file in the pkg.globs.distCss is newer than our built site-wide CSS, otherwise it doesn’t bother rebuilding it. Assuming we do have newer file(s), it initializes our sourcemaps, combines all of our CSS together, then minifies it all via cssnano, adds our banner as a header, writes out our sourcemaps, and builds the full site-wide CSS to our public distribution folder at pkg.paths.dist.css.
The css task also automatically refreshes the webpage’s CSS without a full browser reload via gulp-livereload; you simply need the livereload Chrome extension installed.
Link JS Gulp Tasks
Now let’s have a look at our js & associated tasks:
// Prism js task - combine the prismjs Javascript & config file into one bundle
gulp.task("prism-js", () => {
$.fancyLog("-> Building prism.min.js...");
return gulp.src(pkg.globs.prismJs)
.pipe($.plumber({errorHandler: onError}))
.pipe($.newer({dest: pkg.paths.build.js + "prism.min.js"}))
.pipe($.concat("prism.min.js"))
.pipe($.uglify())
.pipe($.size({gzip: true, showFiles: true}))
.pipe(gulp.dest(pkg.paths.build.js));
});
// babel js task - transpile our Javascript into the build directory
gulp.task("js-babel", () => {
$.fancyLog("-> Transpiling Javascript via Babel...");
return gulp.src(pkg.globs.babelJs)
.pipe($.plumber({errorHandler: onError}))
.pipe($.newer({dest: pkg.paths.build.js}))
.pipe($.babel())
.pipe($.size({gzip: true, showFiles: true}))
.pipe(gulp.dest(pkg.paths.build.js));
});
// inline js task - minimize the inline Javascript into _inlinejs in the templates path
gulp.task("js-inline", () => {
$.fancyLog("-> Copying inline js");
return gulp.src(pkg.globs.inlineJs)
.pipe($.plumber({errorHandler: onError}))
.pipe($.if(["*.js", "!*.min.js"],
$.newer({dest: pkg.paths.templates + "_inlinejs", ext: ".min.js"}),
$.newer({dest: pkg.paths.templates + "_inlinejs"})
))
.pipe($.if(["*.js", "!*.min.js"],
$.uglify()
))
.pipe($.if(["*.js", "!*.min.js"],
$.rename({suffix: ".min"})
))
.pipe($.size({gzip: true, showFiles: true}))
.pipe(gulp.dest(pkg.paths.templates + "_inlinejs"));
});
// js task - minimize any distribution Javascript into the public js folder, and add our banner to it
gulp.task("js", ["js-inline", "js-babel", "prism-js"], () => {
$.fancyLog("-> Building js");
return gulp.src(pkg.globs.distJs)
.pipe($.plumber({errorHandler: onError}))
.pipe($.if(["*.js", "!*.min.js"],
$.newer({dest: pkg.paths.dist.js, ext: ".min.js"}),
$.newer({dest: pkg.paths.dist.js})
))
.pipe($.if(["*.js", "!*.min.js"],
$.uglify()
))
.pipe($.if(["*.js", "!*.min.js"],
$.rename({suffix: ".min"})
))
.pipe($.header(banner, {pkg: pkg}))
.pipe($.size({gzip: true, showFiles: true}))
.pipe(gulp.dest(pkg.paths.dist.js))
.pipe($.filter("**/*.js"))
.pipe($.livereload());
});
The first thing that happens when our js task is run is that the dependent tasks are executed:
- js-inline — takes certain JavaScripts and puts them in our Craft templates directory so that we can source or include them. These are JavaScripts that load other things (so we want them inlined in our HTML) and/or need to be parsed as Twig templates
- js-babel — takes any JavaScript we author from the pkg.globs.babelJs and transpiles them via Babel into pkg.paths.build.js for later processing
- prism-js — this is one of the few functions in my gulpfile.js that I don’t use on all projects; it builds a custom JavaScript bundle for PrismJS that includes only what we need. It’s used for displaying the fancy formatted code samples on the website
It’s important to note that for Babel to work correctly, you’ll need a .babelrc file in your project root to tell it what to transpile things down to. Here’s what mine looks like:
{
"presets": ["es2015"],
"compact": true
}
Finally, our js task runs and takes everything from pkg.globs.distJs, uglifies it, adds .min.js if necessary (it may already be uglified), adds our banner header, and writes out the individual JavaScript files to our public distribution folder at pkg.paths.dist.js.
The js task also looks for any changes to our JavaScripts, and auto-reloads the web browser gulp-livereload; you simply need the livereload Chrome extension installed.
Link Misc Gulp Tasks
In addition to the basic CSS/JS building, there are a number of other useful functions in our gulpfile.js that work generically based on the data in our package.json.
In no particular order:
//favicons-generate task
gulp.task("favicons-generate", () => {
$.fancyLog("-> Generating favicons");
return gulp.src(pkg.paths.favicon.src).pipe($.favicons({
appName: pkg.name,
appDescription: pkg.description,
developerName: pkg.author,
developerURL: pkg.urls.live,
background: "#FFFFFF",
path: pkg.paths.favicon.path,
url: pkg.site_url,
display: "standalone",
orientation: "portrait",
version: pkg.version,
logging: false,
online: false,
html: pkg.paths.build.html + "favicons.html",
replace: true,
icons: {
android: false, // Create Android homescreen icon. `boolean`
appleIcon: true, // Create Apple touch icons. `boolean`
appleStartup: false, // Create Apple startup images. `boolean`
coast: true, // Create Opera Coast icon. `boolean`
favicons: true, // Create regular favicons. `boolean`
firefox: true, // Create Firefox OS icons. `boolean`
opengraph: false, // Create Facebook OpenGraph image. `boolean`
twitter: false, // Create Twitter Summary Card image. `boolean`
windows: true, // Create Windows 8 tile icons. `boolean`
yandex: true // Create Yandex browser icon. `boolean`
}
})).pipe(gulp.dest(pkg.paths.favicon.dest));
});
//copy favicons task
gulp.task("favicons", ["favicons-generate"], () => {
$.fancyLog("-> Copying favicon.ico");
return gulp.src(pkg.globs.siteIcon)
.pipe($.size({gzip: true, showFiles: true}))
.pipe(gulp.dest(pkg.paths.dist.base));
});
The favicons task generates the myriad of website favicons from a single source image, and also generates the HTML necessary to display/include them. This makes it super easy to generate all of the various favicon formats that I honestly have a hard time keeping up with.
// imagemin task
gulp.task("imagemin", () => {
return gulp.src(pkg.paths.dist.img + "**/*.{png,jpg,jpeg,gif,svg}")
.pipe($.imagemin({
progressive: true,
interlaced: true,
optimizationLevel: 7,
svgoPlugins: [{removeViewBox: false}],
verbose: true,
use: []
}))
.pipe(gulp.dest(pkg.paths.dist.img));
});
The imagemin task optimizes all of the images in your pkg.paths.dist.img glob in situ. These are images that are part of the site itself, and are checked into your git repo. For images that the client will upload, they should be optimized server-side as per the Creating Optimized Images in Craft CMS article.
//generate-fontello task
gulp.task("generate-fontello", () => {
return gulp.src(pkg.paths.src.fontello + "config.json")
.pipe($.fontello())
.pipe($.print())
.pipe(gulp.dest(pkg.paths.build.fontello))
});
//copy fonts task
gulp.task("fonts", ["generate-fontello"], () => {
return gulp.src(pkg.globs.fonts)
.pipe(gulp.dest(pkg.paths.dist.fonts));
});
The fonts task first generates a custom icon font via fontello via a config.json file that contains only the glyphs we need. Please don’t include a massive 294k FontAwesome font when you’re just using a half-dozen social media icons.
Then it just copies the fontello font, and any other fonts from pkg.globs.fonts into our public distribution folder at pkg.paths.dist.fonts.
// Process data in an array synchronously, moving onto the n+1 item only after the nth item callback
function doSynchronousLoop(data, processData, done) {
if (data.length > 0) {
const loop = (data, i, processData, done) => {
processData(data[i], i, () => {
if (++i < data.length) {
loop(data, i, processData, done);
} else {
done();
}
});
};
loop(data, 0, processData, done);
} else {
done();
}
}
// Process the critical path CSS one at a time
function processCriticalCSS(element, i, callback) {
const criticalSrc = pkg.urls.critical + element.url;
const criticalDest = pkg.paths.templates + element.template + "_critical.min.css";
let criticalWidth = 1200;
let criticalHeight = 1200;
if (element.template.indexOf("amp_") !== -1) {
criticalWidth = 600;
criticalHeight = 19200;
}
$.fancyLog("-> Generating critical CSS: " + $.chalk.cyan(criticalSrc) + " -> " + $.chalk.magenta(criticalDest));
$.critical.generate({
src: criticalSrc,
dest: criticalDest,
inline: false,
ignore: [],
css: [
pkg.paths.dist.css + pkg.vars.siteCssName,
],
minify: true,
width: criticalWidth,
height: criticalHeight
}, (err, output) => {
if (err) {
$.fancyLog($.chalk.magenta(err));
}
callback();
});
}
//critical css task
gulp.task("criticalcss", ["css"], (callback) => {
doSynchronousLoop(pkg.globs.critical, processCriticalCSS, () => {
// all done
callback();
});
});
The criticalcss task generates our Critical CSS that has the styles needed to render our “above the fold content”. I won’t go into it here, as it’s described in detail in the Implementing Critical CSS on your website article.
// Run pa11y accessibility tests on each template
function processAccessibility(element, i, callback) {
const accessibilitySrc = pkg.urls.critical + element.url;
const cliReporter = require('./node_modules/pa11y/reporter/cli.js');
const options = {
log: cliReporter,
ignore:
[
'notice',
'warning'
],
};
const test = $.pa11y(options);
$.fancyLog("-> Checking Accessibility for URL: " + $.chalk.cyan(accessibilitySrc));
test.run(accessibilitySrc, (error, results) => {
cliReporter.results(results, accessibilitySrc);
callback();
});
}
// accessibility task
gulp.task("a11y", (callback) => {
doSynchronousLoop(pkg.globs.critical, processAccessibility, () => {
// all done
callback();
});
});
The a11y task runs an accessibility audit on all of our website templates. I won’t go into it here, as it’s described in detail in the Making Websites Better through Accessibility article.
// Process the downloads one at a time
function processDownload(element, i, callback) {
const downloadSrc = element.url;
const downloadDest = element.dest;
$.fancyLog("-> Downloading URL: " + $.chalk.cyan(downloadSrc) + " -> " + $.chalk.magenta(downloadDest));
$.download(downloadSrc)
.pipe(gulp.dest(downloadDest));
callback();
}
// download task
gulp.task("download", (callback) => {
doSynchronousLoop(pkg.globs.download, processDownload, () => {
// all done
callback();
});
});
The download task downloads third party JavaScript (such as Google Analytics) that I want to serve myself, so I have control over the expires header. This is getting a bit off in the weeds, but it strikes a nice balance between utilizing third party JavaScripts, while still controlling how they are served and delivered.
In addition to letting me control the expires header, it also ensures that fewer DNS lookups need to be done in order to load everything on the webpage.
Link What’s Left Out?
There may be a few things left out of my frontend workflow that some people might want or expect to be included:
- CSS/SCSS linting — I use PhpStorm as and editor, so my CSS/SCSS linting is done in-editor. If you want, you can easily add CSS/SCSS linting as a step of the css task.
- Browsersync — I use livereload, but it wouldn’t be hard to swap in Browsersync in the default task instead if you prefer it for multi-device testing.
I’m sure there are other niceties that people use as part of their build process; I’m just presenting a minimal set of what has worked out well for me.
Link The Full Monty
I realize that all of this is a lot to digest, but hopefully you’ve found it helpful. The key take away is that your gulpfile.js has the code that does the building of your websites, and the package.json contains what’s unique to that particular website.
Done in this way, you’ll rarely have to be changing or adding anything to your gulpfile.js, and as such can use it as the basis for all of the sites you build. However, you have the flexibility to add to it or modify it if you have specific behavior that you need.
Without further ado, here’s the “full monty” of the entire gulpfile.js used to build this very website:
// package vars
const pkg = require("./package.json");
// gulp
const gulp = require("gulp");
// load all plugins in "devDependencies" into the variable $
const $ = require("gulp-load-plugins")({
pattern: ["*"],
scope: ["devDependencies"]
});
const onError = (err) => {
console.log(err);
};
const banner = [
"/**",
" * @project <%= pkg.name %>",
" * @author <%= pkg.author %>",
" * @build " + $.moment().format("llll") + " ET",
" * @release " + $.gitRevSync.long() + " [" + $.gitRevSync.branch() + "]",
" * @copyright Copyright (c) " + $.moment().format("YYYY") + ", <%= pkg.copyright %>",
" *",
" */",
""
].join("\n");
// scss - build the scss to the build folder, including the required paths, and writing out a sourcemap
gulp.task("scss", () => {
$.fancyLog("-> Compiling scss");
return gulp.src(pkg.paths.src.scss + pkg.vars.scssName)
.pipe($.plumber({errorHandler: onError}))
.pipe($.sourcemaps.init({loadMaps: true}))
.pipe($.sass({
includePaths: pkg.paths.scss
})
.on("error", $.sass.logError))
.pipe($.cached("sass_compile"))
.pipe($.autoprefixer())
.pipe($.sourcemaps.write("./"))
.pipe($.size({gzip: true, showFiles: true}))
.pipe(gulp.dest(pkg.paths.build.css));
});
// css task - combine & minimize any distribution CSS into the public css folder, and add our banner to it
gulp.task("css", ["scss"], () => {
$.fancyLog("-> Building css");
return gulp.src(pkg.globs.distCss)
.pipe($.plumber({errorHandler: onError}))
.pipe($.newer({dest: pkg.paths.dist.css + pkg.vars.siteCssName}))
.pipe($.print())
.pipe($.sourcemaps.init({loadMaps: true}))
.pipe($.concat(pkg.vars.siteCssName))
.pipe($.cssnano({
discardComments: {
removeAll: true
},
discardDuplicates: true,
discardEmpty: true,
minifyFontValues: true,
minifySelectors: true
}))
.pipe($.header(banner, {pkg: pkg}))
.pipe($.sourcemaps.write("./"))
.pipe($.size({gzip: true, showFiles: true}))
.pipe(gulp.dest(pkg.paths.dist.css))
.pipe($.filter("**/*.css"))
.pipe($.livereload());
});
// Prism js task - combine the prismjs Javascript & config file into one bundle
gulp.task("prism-js", () => {
$.fancyLog("-> Building prism.min.js...");
return gulp.src(pkg.globs.prismJs)
.pipe($.plumber({errorHandler: onError}))
.pipe($.newer({dest: pkg.paths.build.js + "prism.min.js"}))
.pipe($.concat("prism.min.js"))
.pipe($.uglify())
.pipe($.size({gzip: true, showFiles: true}))
.pipe(gulp.dest(pkg.paths.build.js));
});
// babel js task - transpile our Javascript into the build directory
gulp.task("js-babel", () => {
$.fancyLog("-> Transpiling Javascript via Babel...");
return gulp.src(pkg.globs.babelJs)
.pipe($.plumber({errorHandler: onError}))
.pipe($.newer({dest: pkg.paths.build.js}))
.pipe($.babel())
.pipe($.size({gzip: true, showFiles: true}))
.pipe(gulp.dest(pkg.paths.build.js));
});
// components - build .vue VueJS components
gulp.task("components", () => {
$.fancyLog("-> Compiling Vue Components");
return gulp.src(pkg.globs.components)
.pipe($.plumber({errorHandler: onError}))
.pipe($.newer({dest: pkg.paths.build.js, ext: ".js"}))
.pipe($.vueify({}))
.pipe($.size({gzip: true, showFiles: true}))
.pipe(gulp.dest(pkg.paths.build.js));
});
// inline js task - minimize the inline Javascript into _inlinejs in the templates path
gulp.task("js-inline", () => {
$.fancyLog("-> Copying inline js");
return gulp.src(pkg.globs.inlineJs)
.pipe($.plumber({errorHandler: onError}))
.pipe($.if(["*.js", "!*.min.js"],
$.newer({dest: pkg.paths.templates + "_inlinejs", ext: ".min.js"}),
$.newer({dest: pkg.paths.templates + "_inlinejs"})
))
.pipe($.if(["*.js", "!*.min.js"],
$.uglify()
))
.pipe($.if(["*.js", "!*.min.js"],
$.rename({suffix: ".min"})
))
.pipe($.size({gzip: true, showFiles: true}))
.pipe(gulp.dest(pkg.paths.templates + "_inlinejs"))
.pipe($.filter("**/*.js"))
.pipe($.livereload());
});
// js task - minimize any distribution Javascript into the public js folder, and add our banner to it
gulp.task("js", ["js-inline", "js-babel", "prism-js"], () => {
$.fancyLog("-> Building js");
return gulp.src(pkg.globs.distJs)
.pipe($.plumber({errorHandler: onError}))
.pipe($.if(["*.js", "!*.min.js"],
$.newer({dest: pkg.paths.dist.js, ext: ".min.js"}),
$.newer({dest: pkg.paths.dist.js})
))
.pipe($.if(["*.js", "!*.min.js"],
$.uglify()
))
.pipe($.if(["*.js", "!*.min.js"],
$.rename({suffix: ".min"})
))
.pipe($.header(banner, {pkg: pkg}))
.pipe($.size({gzip: true, showFiles: true}))
.pipe(gulp.dest(pkg.paths.dist.js))
.pipe($.filter("**/*.js"))
.pipe($.livereload());
});
// Process data in an array synchronously, moving onto the n+1 item only after the nth item callback
function doSynchronousLoop(data, processData, done) {
if (data.length > 0) {
const loop = (data, i, processData, done) => {
processData(data[i], i, () => {
if (++i < data.length) {
loop(data, i, processData, done);
} else {
done();
}
});
};
loop(data, 0, processData, done);
} else {
done();
}
}
// Process the critical path CSS one at a time
function processCriticalCSS(element, i, callback) {
const criticalSrc = pkg.urls.critical + element.url;
const criticalDest = pkg.paths.templates + element.template + "_critical.min.css";
let criticalWidth = 1200;
let criticalHeight = 1200;
if (element.template.indexOf("amp_") !== -1) {
criticalWidth = 600;
criticalHeight = 19200;
}
$.fancyLog("-> Generating critical CSS: " + $.chalk.cyan(criticalSrc) + " -> " + $.chalk.magenta(criticalDest));
$.critical.generate({
src: criticalSrc,
dest: criticalDest,
inline: false,
ignore: [],
css: [
pkg.paths.dist.css + pkg.vars.siteCssName,
],
minify: true,
width: criticalWidth,
height: criticalHeight
}, (err, output) => {
if (err) {
$.fancyLog($.chalk.magenta(err));
}
callback();
});
}
//critical css task
gulp.task("criticalcss", ["css"], (callback) => {
doSynchronousLoop(pkg.globs.critical, processCriticalCSS, () => {
// all done
callback();
});
});
// Process the downloads one at a time
function processDownload(element, i, callback) {
const downloadSrc = element.url;
const downloadDest = element.dest;
$.fancyLog("-> Downloading URL: " + $.chalk.cyan(downloadSrc) + " -> " + $.chalk.magenta(downloadDest));
$.download(downloadSrc)
.pipe(gulp.dest(downloadDest));
callback();
}
// download task
gulp.task("download", (callback) => {
doSynchronousLoop(pkg.globs.download, processDownload, () => {
// all done
callback();
});
});
// Run pa11y accessibility tests on each template
function processAccessibility(element, i, callback) {
const accessibilitySrc = pkg.urls.critical + element.url;
const cliReporter = require('./node_modules/pa11y/reporter/cli.js');
const options = {
log: cliReporter,
ignore:
[
'notice',
'warning'
],
};
const test = $.pa11y(options);
$.fancyLog("-> Checking Accessibility for URL: " + $.chalk.cyan(accessibilitySrc));
test.run(accessibilitySrc, (error, results) => {
cliReporter.results(results, accessibilitySrc);
callback();
});
}
// accessibility task
gulp.task("a11y", (callback) => {
doSynchronousLoop(pkg.globs.critical, processAccessibility, () => {
// all done
callback();
});
});
//favicons-generate task
gulp.task("favicons-generate", () => {
$.fancyLog("-> Generating favicons");
return gulp.src(pkg.paths.favicon.src).pipe($.favicons({
appName: pkg.name,
appDescription: pkg.description,
developerName: pkg.author,
developerURL: pkg.urls.live,
background: "#FFFFFF",
path: pkg.paths.favicon.path,
url: pkg.site_url,
display: "standalone",
orientation: "portrait",
version: pkg.version,
logging: false,
online: false,
html: pkg.paths.build.html + "favicons.html",
replace: true,
icons: {
android: false, // Create Android homescreen icon. `boolean`
appleIcon: true, // Create Apple touch icons. `boolean`
appleStartup: false, // Create Apple startup images. `boolean`
coast: true, // Create Opera Coast icon. `boolean`
favicons: true, // Create regular favicons. `boolean`
firefox: true, // Create Firefox OS icons. `boolean`
opengraph: false, // Create Facebook OpenGraph image. `boolean`
twitter: false, // Create Twitter Summary Card image. `boolean`
windows: true, // Create Windows 8 tile icons. `boolean`
yandex: true // Create Yandex browser icon. `boolean`
}
})).pipe(gulp.dest(pkg.paths.favicon.dest));
});
//copy favicons task
gulp.task("favicons", ["favicons-generate"], () => {
$.fancyLog("-> Copying favicon.ico");
return gulp.src(pkg.globs.siteIcon)
.pipe($.size({gzip: true, showFiles: true}))
.pipe(gulp.dest(pkg.paths.dist.base));
});
// imagemin task
gulp.task("imagemin", () => {
return gulp.src(pkg.paths.dist.img + "**/*.{png,jpg,jpeg,gif,svg}")
.pipe($.imagemin({
progressive: true,
interlaced: true,
optimizationLevel: 7,
svgoPlugins: [{removeViewBox: false}],
verbose: true,
use: []
}))
.pipe(gulp.dest(pkg.paths.dist.img));
});
//generate-fontello task
gulp.task("generate-fontello", () => {
return gulp.src(pkg.paths.src.fontello + "config.json")
.pipe($.fontello())
.pipe($.print())
.pipe(gulp.dest(pkg.paths.build.fontello))
});
//copy fonts task
gulp.task("fonts", ["generate-fontello"], () => {
return gulp.src(pkg.globs.fonts)
.pipe(gulp.dest(pkg.paths.dist.fonts));
});
// Default task
gulp.task("default", ["css", "js"], () => {
$.livereload.listen();
gulp.watch([pkg.paths.src.scss + "**/*.scss"], ["css"]);
gulp.watch([pkg.paths.src.css + "**/*.css"], ["css"]);
gulp.watch([pkg.paths.src.js + "**/*.js"], ["js"]);
gulp.watch([pkg.paths.templates + "**/*.{html,htm,twig}"], () => {
gulp.src(pkg.paths.templates)
.pipe($.plumber({errorHandler: onError}))
.pipe($.livereload());
});
});
// Production build
gulp.task("build", ["download", "default", "favicons", "imagemin", "fonts", "criticalcss"]);