gulp-task-maker
⚠ Requires Node.js 4 or later.
Helps you write gulp tasks focused on building assets, so that you can:
- Get matching
build
and watch
tasks for free. - Separate task configuration and implementation.
- Configure multiple builds for each task (e.g. with different sources and configuration).
- Improve developer usability by logging what gets written to disk, use system notifications for errors, etc.
gulp-task-maker
also bundles useful gulp plugins such as gulp-sourcemaps
, gulp-if
and gulp-concat
, and provides a commonBuilder
helper function that takes care of the most common tasks, reducing boilerplate between tasks.
Table of contents:
- Example usage
- Configuring tasks
- Writing tasks
Example usage
At the root of your project, install as a dependency:
npm install gulp-task-maker --save-dev
Then in your gulpfile.js
, you could have:
const gtm = require('gulp-task-maker')
const jsBuild = {
src: ['src/foo/foo.js', 'src/bar/*.js'],
dest: 'public/main.js',
watch: true
}
gtm.load('gulp-tasks', {
mytask: jsBuild
})
This will instruct gulp-task-maker
to load ./gulp-tasks/mytask.js
, which could look like:
const path = require('path')
const gulp = require('gulp')
const somePlugin = require('gulp-something')
module.exports = function mytaskBuilder(config, tools) {
const file = path.basename(config.dest)
const dir = path.dirname(config.dest)
return gulp.src(config.src)
.pipe(tools.logErrors())
.pipe(tools.concat(file))
.pipe(somePlugin())
.pipe(tools.size(dir))
.pipe(gulp.dest(dir))
}
We could also simplify our task’s function further by using the tools.commonBuilder
helper:
const path = require('path')
const somePlugin = require('gulp-something')
module.exports = function mytaskBuilder(config, tools) {
return tools.commonBuilder(config, [
tools.concat(path.basename(config.dest)),
somePlugin()
])
}
Compared to a 100% DIY gulp workflow, we gained a few things:
- we separated the task’s logic (
gulp-tasks/mytask.js
) from the build’s config (input files’ paths, output path, and any other configuration you want), making it more portable; - we can provide more than one build config, as an array of objects;
- we will get a watch task automatically;
- we improved error handling and result reports.
Configuring tasks
Using the load and task methods
Let’s say you have one or several task scripts that work with gulp-task-maker
, living in some folder in your project. (If you don’t, see the next section, “Writing tasks”, and check out the example
directory in this repo for basic examples.)
There are two ways to call these task scripts and pass one or several config objects to them.
A. Use the load
method to load several task scripts at once:
const gtm = require('gulp-task-maker')
gtm.load(
'my/tasks/directory',
{
taskName: { src: 'x', dest: 'y' }
other_task: { … }
}
}
This will look for the following files:
my/tasks/directory/taskName.js
my/tasks/directory/other_task.js
B. Alternatively, or in addition to the load
method, use the task
method to load a single script:
const gtm = require('gulp-task-maker')
gtm.task(
'my/tasks/directory/coolstuff.js',
{ src: 'x', dest: 'y' }
}
You can use both methods in the same gulpfile.js
, which can be useful if your tasks live in different places:
const gtm = require('gulp-task-maker')
gtm.load('node_modules/our-default-task-package', {
foo: { … },
bar: { … }
})
gtm.task('other-tasks/baz.js', {
…
})
The task configuration object
The task configuration object can contain whatever you want, but three keys have special meaning:
src
, required: a string or array of glob patterns; gulp-task-maker
will notify you if one of those paths or patterns match zero files.dest
, required: a string defining where the compilation result will go. Could be a folder or file name.watch
, optional: if true, gulp-task-maker
will watch the src
patterns for file changes; if set as a string or array of strings, it will watch those.
For instance if you have a task that concatenates and minify JS code, you could have this config:
cont gtm = require('gulp-task-maker')
const jsLibs = [
'node_modules/jquery/dist/jquery.js',
'node_modules/other-lib/dist/other-lib.js'
]
const ourJS = [
'src/core/*.js',
'src/module1/*.js,
'src/module4
Note that in your task scripts, you would still have to explictely use the config’s src
, dest
, revisions
and minify
values and do something useful with them. Only the watch
property is handled 100% by gulp-task-maker
. See the Writing tasks section for more info.
Multiple builds for a task
Each task can be called with multiple config objects:
cont gtm = require('gulp-task-maker')
gtm.load('gulp-tasks', {
mytask: [
{
src: ['node_modules/abc/abc.js',
'node_modules/xyz/xyz.js'],
dest: 'public/vendor.js'
},
{
src: ['src/foo/foo.js', 'src/bar/*.js'],
dest: 'public/main.js',
watch: true
}
]
})
See what tasks are created
gulp-task-maker
will create one or two tasks for each valid script & config object pair:
- a build task (e.g.
build-js-0
); - and a matching watch task (e.g.
watch-js-0
) if the watch
option is set to true or to a string or array of files.
It can also be useful to use gulp’s task list, using the built-in --tasks
option:
$ ./node_modules/.bin/gulp --tasks
[13:37:00] Tasks for ~/gulp-task-maker/example/gulpfile.js
[13:37:00] ├── build-mincss
[13:37:00] ├── watch-mincss
[13:37:00] ├── build-minjs
[13:37:00] ├── watch-minjs
[13:37:00] ├─┬ watch
[13:37:00] │ ├── watch-mincss
[13:37:00] │ └── watch-minjs
[13:37:00] └─┬ build
[13:37:00] ├── build-mincss
[13:37:00] └── build-minjs
Notice the build
, and watch
tasks; they are global shortcut tasks that run all the build-*
or watch-*
tasks. (gulp-task-maker
will also configure a default
task as an alias for build
.)
Enabling system notifications
gulp-task-maker
uses system notifications, via the node-notifier
package, to signal configuration errors. (It can also use system notifications for errors occuring when processing source files, if tasks use the tools.logErrors
or tools.commonBuilder
.)
To enable system notifications, use the NODE_NOTIFIER
or NOTIFY
environment variables. You can do it globally in your ~/.bashrc
or similar, but I recommend using npm scripts for this.
For example, your package.json
could look like this:
{
"scripts": {
"build": "cross-env NOTIFY=1 gulp build",
"watch": "cross-env NOTIFY=1 gulp watch"
},
"devDependencies": {
"cross-env": "^4.0",
"gulp": "^3.9",
"gulp-task-maker": "^1.0"
}
}
(The cross-env
package is used to set a command variable that works on Windows as well as *nix systems. If you only use macOS and Linux, you could ditch it.)
Now you can run the main build
task with:
npm run build
Advanced config
Since gulpfile.js
is a Node.js script, you can use the full power of JavaScript to build your configuration, if necessary.
We’ve seen how we can declare a system variable with cross-env
. We could use that to make variations of our build, first passing different variables in package.json
:
{
"scripts": {
"build-prod": "cross-env NOTIFY=1 BUILD=prod gulp build",
"build-dev": "cross-env NOTIFY=1 BUILD=dev gulp build",
"watch": "cross-env NOTIFY=1 BUILD=dev gulp watch"
},
"devDependencies": {
"cross-env": "^4.0",
"gulp": "^3.9",
"gulp-task-maker": "^1.0"
}
}
And in your gulpfile.js
:
const gtm = require('gulp-task-maker')
const isDev = process.env.TARGET === 'dev'
gtm.task('gulp-tasks/minjs.js', {
src: ['node_modules/jquery/dist/jquery.js', 'src/*.js'],
watch: 'src/*.js',
dest: isDev?'dist/output.js':'dist/output.min.js',
minify: !isDev,
sourcemaps: isDev
})
This is just one possible approach. You could use two different config objects, instead of relying on npm scripts and environment variables:
const jsBuild = {
src: [
'node_modules/jquery/dist/jquery.js',
'node_modules/foo/foo.js',
'node_modules/bar/bar.js',
'src/*.js',
'other-source/js/*.js'
],
watch: ['src/*.js', 'other-source/js/*.js'],
dest: 'dist/output.js',
minify: true,
sourcemaps: true
}
gtm.task('gulp-tasks/minjs.js', [
Object.assign(jsBuild, {minify:false}),
Object.assign(jsBuild, {watch:false, sourcemaps:false})
])
Overriding global tasks
You can override the global tasks (build
, watch
and default
) with gulpTaskMaker.config
:
const gtm = require('gulp-task-maker')
gtm.config({
buildTask: 'my-build-name',
watchTask: 'my-watch-name',
defaultTask: false,
defaultTask: ['my-build-name', 'my-watch-name', 'some-other-task'],
defaultTask: function() {
}
})
console.log(gtm.config())
Writing tasks
Creating a task script
The minimal requirement for a task script is that it returns a function. This one is a valid gulp-task-maker
task script:
module.exports = function() {}
It’s not very useful though. Let’s add some actual logic.
Your function could look like any standard gulp task:
const gulp = require('gulp')
const concat = require('gulp-concat')
module.exports = function() {
return gulp.src('my/source/files/*.js')
.pipe(concat('build.js'))
.pipe(gulp.dest('public/js'))
}
Tip: always return the gulp stream — the part that looks like .pipe(gulp.dest(…))
in the end — if you want gulp to be able to log the task’s duration correctly. Or you might see things like "Finished 'build-mytask' after 50 μs"
, while your task actually took quite longer to complete.
Define dependencies
I recommend creating a mytask.json
file alongside the mytask.js
script, with a list of dependencies. For example:
{
"dependencies": {
"gulp": "^3.9.1",
"gulp-concat": "^2.6.0"
}
}
Why do that? Well, if you try to use a task which has missing dependencies, gulp-task-builder will read the dependencies info from this JSON file, and print the npm
command to install the missing ones.
This can be useful if you want to copy tasks from one project to another, or if you want to have a collection of a dozen ready-to-use task scripts in your project, but only install dependencies for the few you’re actually using!
Note that this feature will only print some information to the console; it won’t manage dependencies or resolve version conflicts for you! Still, it’s helpful to have it, if only as documentation for your task scripts when porting them from one project to another.
The config argument
This is the config object used with the gulpTaskMaker.load
and gulpTaskMaker.task
methods, with a few normalized properties:
src
and watch
, normalized as arrays of stringsdest
, normalized as a string- all other properties are kept as-is
Note that you can ignore the watch
property: it’s managed automatically by gulp-task-maker
.
Let’s use this object to make our task function actually configurable.
const gulp = require('gulp')
const path = require('path')
const concat = require('gulp-concat')
module.exports = function(config) {
let file = 'build.js'
let dir = config.dest
if (path.extname(config.dest) === '.js') {
file = path.basename(config.dest)
dir = path.dirname(config.dest)
}
return gulp.src(config.src)
.pipe(concat(file))
.pipe(gulp.dest(dir))
}
We could also make some actions optional. For instance, let’s add facultative UglifyJS support. For that, we’re going to use gulp-if
, which is already provided by gulp-task-maker
in the second argument to our function, along with gulp-concat
and other useful tools.
const gulp = require('gulp')
const path = require('path')
const uglify = require('gulp-uglify')
module.exports = function(config, tools) {
let file = 'build.js'
let dir = config.dest
if (path.extname(config.dest) === '.js') {
file = path.basename(config.dest)
dir = path.dirname(config.dest)
}
const minifyOrMaybeNot = tools.gulpif(
config.minify === true,
uglify()
)
return gulp.src(config.src)
.pipe(tools.concat(file))
.pipe(minifyOrMaybeNot)
.pipe(gulp.dest(dir))
}
Now if we add minify: true
to our task config, we will activate source minification.
A note about how gulp plugins work
If you’re unfamiliar with the way gulp and gulp plugins work: they’re using Node.js streams, which are representations of data (mostly files, when it comes to gulp).
Gulp plugins are Transform streams, which means they take some source data and transform it in some way (e.g. taking CSS and adding browser prefixes), and return a transformed stream which can be piped to the next Transform stream, or to a Writeable stream like gulp.dest
(which takes the transformed files and writes them to disk).
Anyway, that’s why with gulp we tend to have a logic that looks like this:
streamThatReadsFiles
.pipe( transformStream1 )
.pipe( transformStream2 )
.pipe( streamThatWritesToDisk )
One issue when piping streams is that it can be hard to have an optional stream. For example, this code would fail:
streamThatReadsFiles
.pipe( transformStream1 )
.pipe( someCondition ? transformStream2 : null )
.pipe( streamThatWritesToDisk )
If you pipe one stream into something that is not a stream (like null
), your script will crash. That’s what gulp-if
tries to fix. It’s a utility function that checks a condition and returns either the second argument, or a “neutral” stream that just passes the data along.
streamThatReadsFiles
.pipe( transformStream1 )
.pipe( gulpif(someCondition, transformStream2) )
.pipe( streamThatWritesToDisk )
See the gulp-if documentation for more options.
Note: you don’t have to use gulp-if
, of course; you can always store the initial stream in a variable and then conditionnaly update the stream:
let stream = gulp.src(…).pipe(…)
if (someCondition) {
stream = stream.pipe(transform)
}
return stream.pipe( gulp.dest(…) )
The tools argument
The second argument received by your task function is a collection of very common gulp plugins:
tools.concat
: gulp-concattools.gulpif
: gulp-iftools.rename
: gulp-renametools.sourcemaps
: gulp-sourcemapstools.plumber
: gulp-plumbertools.size
: gulp-size
In addition to that, there are a few tools using gulp-task-maker
’s logging-and-notification function:
tools.notify
: notification function, which takes a string or an object with the following properties: plugin
, message
(shown in system notifications and the console), details
(shown in the console only).tools.logErrors
: a function that sets up gulp-plumber with our custom notify
function.tools.logSize
: logs the path and size of output files.
We can use those tools and helpers to make our task a bit friendlier for ourselves and others. In particular, we can:
- Improve error management and logging (see this short article on why it can be useful with gulp).
- Add a log of files we’re writing to disk.
const gulp = require('gulp')
const path = require('path')
const uglify = require('gulp-uglify')
module.exports = function(config, tools) {
let file = 'build.js'
let dir = config.dest
if (path.extname(config.dest) === '.js') {
file = path.basename(config.dest)
dir = path.dirname(config.dest)
}
const minifyTransform = tools.gulpif(
config.minify === true,
uglify()
)
return gulp.src(config.src)
.pipe(tools.logErrors())
.pipe(tools.sourcemaps.init())
.pipe(tools.concat(file))
.pipe(minifyTransform)
.pipe(tools.logSize())
.pipe(tools.sourcemaps.write('.'))
.pipe(gulp.dest(dir))
}
If you find that you’re repeating most of this structure from one task function to the next, you might want to use the tools.commonBuilder
helper.
The commonBuilder helper
gulp-task-maker
provides an helper function that takes care of this boilerplate:
gulp.src(…)
.pipe(tools.logErrors())
.pipe(tools.sourcemaps.init())
…
.pipe(tools.logSize())
.pipe(tools.sourcemaps.write())
.pipe(gulp.dest(…))
… and lets you apply your own transforms in the middle. The tools.commonBuilder(config, transforms)
helper function:
- uses
config.src
as input, and config.dest
to figure out the output directory; - logs errors and written files;
- has off-by-default sourcemaps support (activate by setting
config.sourcemaps
to true
or to a relative path); - and will apply any transforms you want, passed as an array of transforms.
Usage in your task’s function might look like this:
module.exports = function(config, tools) {
config = Object.assign({
sourcemaps: '.'
}, config)
return tools.commonBuilder(config, [
transform1(),
transform2()
])
}
This helper also makes conditional transforms easier: if the transforms
array contains values other than Transform streams (which are returned by all gulp plugins), they will be discarded.
module.exports = function(config, tools) {
config = Object.assign({
sourcemaps: '.',
doTransform1: true,
doTransform2: false,
doTransform3: false
}, config)
return tools.commonBuilder(config, [
tools.gulpif(config.doTransform1, transform1()),
config.doTransform2 ? transform2() : null,
config.doTransform3 && transform3(),
])
}