Monday, 22 February 2021
Barnaby Turner
10 minute read
Gulp is an open source tool that allows you to automate slow and repetitive tasks and integrate them into your build pipelines. This is a fantastic tool to have as a web developer. It lets you create tasks such as CSS minification or JavaScript obfuscation, which can be run by any developer working on the project, as well as within your build pipelines for a consistent output with minimum friction.
In this post, I am going to talk about the process of refactoring a gulpfile (a JavaScript file that is read by Gulp, which contains instructions on how to complete tasks) to keep tasks DRY (Don’t Repeat Yourself) and reusable, while producing multiple bundles for different use cases.
I was recently tasked with creating a new microsite to live inside an already existing website and repository. There were already gulp tasks setup for compiling source SASS, merging and obfuscating our JavaScript, and producing revisions (among other tasks). These tasks, however, have all their configuration and paths hard coded within them, making it difficult to expand on while remaining DRY and reusable.
Here is a simplified example:
gulpfile.js:
// https://github.com/sindresorhus/gulp-autoprefixer
const autoprefixer = require('gulp-autoprefixer');
// https://github.com/sindresorhus/del
const del = require('del');
// https://github.com/gulpjs/gulp
const gulp = require('gulp');
// https://github.com/hparra/gulp-rename
const rename = require('gulp-rename');
// https://github.com/dlmanning/gulp-sass
const sass = require('gulp-sass');
// https://github.com/ubirak/gulp-uglifycss
const uglifycss = require('gulp-uglifycss');
const cssSourcePath = [
'./src/sass/**/*.scss',
'./node_modules/external/project.scss',
'!./src/sass/excludeMe.scss'
];
const cssExportPath = './public/css/';
// https://github.com/sass/node-sass#options
const sassOptions = {
errLogToConsole: true,
outputStyle: 'expanded'
};
// https://github.com/fmarcia/UglifyCSS
const uglifyCssOptions = {
"maxLineLen": 312,
"uglyComments": true
};
function css() {
del([cssExportPath + '**/*'], { force: true });
return gulp.src(cssSourcePath)
.pipe(sass(sassOptions).on('error', sass.logError))
.pipe(autoprefixer())
.pipe(uglifycss(uglifyCssOptions))
.pipe(rename({ suffix: '.min' }))
.pipe(gulp.dest(cssExportPath));
}
exports.build = css;
This gulpfile:
There is nothing wrong with this gulpfile and it is a common approach when working with a single project. As you can see, the tasks are dependent on predefined variables that contain paths and plugin options. If you needed the CSS task to produce a bundle with a different set of source files for example, you would need to duplicate the task. If you wanted to use this gulpfile inside another project, you would need to edit it with updated paths and options.
The gulpfile should only be responsible for processing tasks and should have no knowledge of source locations, intended output directories, or plugin configuration options.
We can accomplish this in a few steps:
Here is an example of how we would change the previous gulpfile to use an external configuration file:
gulp.config.js:
module.exports = {
app: { name: ‘example’ },
css: {
sourcePaths: [
‘./src/sass/**/*.scss’,
‘./node_modules/external/project.scss’,
‘!./src/sass/excludeMe.scss’
],
exportPath: `./public/css/`
},
thirdParty: {
// https://github.com/sass/node-sass#options
sassOptions: {
errLogToConsole: true,
outputStyle: 'expanded'
},
// https://github.com/fmarcia/UglifyCSS
uglifyCssOptions: {
'maxLineLen': 312,
'uglyComments': true
}
}
};
gulpfile.js:
// requires
const config = require(‘./gulp.config’);
function css() {
del(config.css.sourcePaths + '**/*', { force: true });
return gulp.src(config.css.sourcePaths)
.pipe(sass(config.thirdParty.sassOptions).on('error', sass.logError))
.pipe(autoprefixer())
.pipe(uglifycss(config.thirdParty.uglifyCssOptions))
.pipe(rename({ suffix: '.min' }))
.pipe(gulp.dest(config.css.exportPath));
}
exports.build = css;
We have now decoupled our tasks from any project specific configuration. This is a great solution if you plan to use the same gulpfile across multiple projects and is the perfect basis for producing multiple bundles from the same tasks while remaining DRY and maintainable.
As a project grows you might find the need to generate multiple bundles. In my use case, I wanted to generate bundles with their own source locations, output locations and plugin configurations for use in different microsites.
To achieve this, we can create multiple gulp configuration files and adjust our gulpfile to iterate over them. In this example we will be generating bundles for two microsites: foo and bar.
gulp.config.foo.js:
const baseName = 'foo';
module.exports = {
app: { name: baseName },
css: {
sourcePaths: [
`./src/${baseName}/sass/**/*.scss`,
'./node_modules/external/project.scss',
`!./src/${baseName}/sass/excludeMe.scss`
],
exportPath: `./public/css/${baseName}/`
},
thirdParty: {
// https://github.com/sass/node-sass#options
sassOptions: {
errLogToConsole: true,
outputStyle: 'expanded'
},
// https://github.com/fmarcia/UglifyCSS
uglifyCssOptions: {
'maxLineLen': 312,
'uglyComments': true
}
}
};
gulp.config.bar.js:
const baseName = 'bar';
module.exports = {
app: { name: baseName },
css: {
sourcePaths: [
`./src/${baseName}/sass/**/*.scss`,
'./node_modules/external/project.scss',
`!./src/${baseName}/sass/excludeMe.scss`
],
exportPath: `./public/css/${baseName}/`
},
thirdParty: {
// https://github.com/sass/node-sass#options
sassOptions: {
errLogToConsole: false,
outputStyle: 'expanded'
},
// https://github.com/fmarcia/UglifyCSS
uglifyCssOptions: {
'maxLineLen': 312,
'uglyComments': false
}
}
};
Now we have created our two configuration files, we need to modify our gulpfile to recognise the config files. In our gulpfile, we need to import each configuration file we want to use:
const CONFIGS = [
require('./gulp.config.foo'),
require('./gulp.config.bar')
];
Gulp manages files in memory using streams. This allows us to pipe tasks together efficiently without having to wait for Gulp to write anything to the disk. We can use these streams to help manage our multiple configurations. We can iterate over each configuration set and treat it as a stream. Then, when each configuration has been run against the task, we can merge the streams together and pass it back to gulp to continue processing.
To do this, we can use the merge-stream package.
gulpfile.js:
// requires…
const merge = require('merge-stream');
const CONFIGS = [
require('./gulp.config.foo'),
require('./gulp.config.bar')
];
function css() {
let tasks = CONFIGS.map(config => {
return gulp.src(config.css.sourcePaths)
.pipe(sass(config.thirdParty.sassOptions).on('error', sass.logError))
.pipe(autoprefixer())
.pipe(uglifycss(config.thirdParty.uglifyCssOptions))
.pipe(rename({ suffix: '.min' }))
.pipe(gulp.dest(config.css.exportPath))
});
return merge(tasks);
}
exports.build = css;
The gulpfile is now setup to accept and process any number of configuration files.
This Gulp technique can be used to create bundles for many different purposes.
For example, you might have an application that has two distinct sections: a public facing area and a protected dashboard area. The protected area could contain some important JavaScript code for communicating with an API, along with its own distinct stylesheets. For security and performance reasons, you might want to create and serve individual bundles for each area.
Or perhaps you have a large application that needs to be accessible over slower mobile connections. To reduce your load times, you could break out each section’s CSS/JavaScript into its own bundle to serve when needed, or to slowly preload in the background.
To see a working example of this, you can check out our example repository on GitHub.
Last updated: Tuesday, 20 June 2023
Software Developer
Barnaby is a Software Developer at Rock Solid Knowledge.
We're proud to be a Certified B Corporation, meeting the highest standards of social and environmental impact.
+44 333 939 8119