Ever since switching from Grunt to Gulp, one of my first steps when starting a new JavaScript project has been to copy the Gulpfile
from my previous JavaScript project, and modify it accordingly. Modifications are necessary firstly to suit the subtly different needs of each new project, and secondly of course, for taste.
While comparing Gulpfile
s over time is an interesting little window into how my style and preferences have evolved, it pains me to have so much subtly duplicated functionality spread across so many projects, none of which are kept consistently up-to-date.
Aside from my own meandering laziness, I believe this is partly owing to Gulp's design, and partly what Gulp was created in reaction to. Whereas Grunt expected you to cram every varying combination of options into declarative configuration, which is easy to store and move around, but not very expressive, Gulp encourages composition through the definition of many small sub-tasks, which is highly expressive but tends to result in tightly-coupled processes & repeated steps, and managing configuration is left up to the developer. Both approaches leave something to be desired.
The middle path
Furthermore, while Gulp's chaining style makes for terse syntax, and its factory-function style makes it easy to configure individual process steps, it presents no clear way to configure the process itself. The ideal is a unified way to configure both the steps and the process. Also, a way to cleanly separate the process from the configuration, compose them in one place, and extract out any project-specific details, so that one Gulpfile
can rule them all.
Fortunately, most of our projects have similar needs, and follow a relatively consistent directory structure:
build/
: intermediate build filesdist/
: production-ready distributable filesspec/
: BDD spec files, suffixed as*Spec.js
src/
: library/application source files
Most new JS projects are written in ES6 (ES2015), so our needs for each project generally look something like:
- Compile source & tests to a particular module format (usually CommonJS or UMD)
- Run tests using built files
- Generate minified and non-minified production builds
- Generate sourcemaps
- Watch source & tests for changes during development, re-run tests
- Run a dev server to serve files for in-browser testing (sometimes)
After reviewing a few of our most recent projects, I was able to come up with a Gulpfile
that meets the above requirements, but also satisfies my desired constraints on process & configuration. Without further ado, weighing in at 55 lines (at the time of this writing), here is the listing in its entirety:
Important things to note:
- As of Gulp 3.9, files named
Gulpfile.babel.js
are automatically transpiled from ES6, which for us means thatGulpfile
s are no longer the one bit of a project where our brains must revert to old-school JavaScript - Almost all of the configuration is encapsulated in 2 blocks: one for file and directory paths, one for build settings
- Project-specific configuration, such as the name of the build file and the port on which to run the dev server (where applicable) has been extracted out to
package.json
in a custom key called (naturally)build
- All possible build steps are listed out in one place (
pipeline()
, lines 29-36); since the order of each step relative to others is always consistent, configuration can be used to toggle them on and off — which is made easy with gulp-if (aliased asgif()
above) - A few small utility functions (
build()
,chain()
) are used to map named build configurations (with optional overrides) to a flattened-out.pipe()
chain, and tighten up function-handling syntax (bind()
) - Though it's not really necessary for low-complexity scripts like this, I've gotten into the habit of declaring things as
const
whenever possible (even though JSconst
s aren't really real constants)
Each build task is then assigned a function with a named configuration, which can be merged with passed parameters (in the form of command line arguments in the case of 'dist'
).
The end result is that almost everything is expressed exactly once. Not only that, it allows for any configurable option to be overidden from the command line, for free. For example, if I wanted a minified production file with sourcemaps and AMD-formatted modules, I could run gulp dist --min --smaps --modules=amd
. This approach also has the benefit of programatically enforcing project structure conventions.
While the above is only appropriate for JavaScript library projects (it doesn't address CSS or other front-end tasks), the structure and patterns serve as a good basis for a generalized Gulpfile
, and project-specific tasks can always be added transparently by introducing a requireDir
-style pattern.
So that's my take. If you too are obsessive enough about build configuration to have a philosophy for managing it, I'd love to hear about it.