Gulp and Webpack – Better Together

I’ve used gulp as a workflow engine prior, but I’d pretty much given up on using it because webpack did so much of what I needed. However, I was flying back from my vacation and I was reminded why I still need it. Not everything in my workflow is actually involved in creating bundles. In particular, some of my stuff is loaded from CDN – things like core-js and some icon fonts. When I am developing without the Internet (like on an airplane), I’d like to still use them. I need to copy libraries that I normally grab from the CDN and place them into the local public area. That requires something other than webpack.

This begs the question – how can one convert the build I had been doing with webpack into something that gulp runs. Well, it turns out that there is a recipe for that. Here is my new Gulpfile.js:

 var eslint = require('gulp-eslint'),
    gulp = require('gulp'),
    gutil = require('gulp-util'),
    webpack = require('webpack'),
    webpackConfig = require('./webpack.config.js');

var files = {
    client: [ 'client/**/*.js', 'client/**/*.jsx' ],
    server: [ 'server/**/*.js' ]

gulp.task('build', [

gulp.task('lint', [

gulp.task('server:lint', function () {
    return gulp.src(files.server)

gulp.task('webpack:lint', function () {
    return gulp.src(files.client)

gulp.task('webpack:build', function (callback) {
    webpack(webpackConfig, function (err, stats) {
        if (err)
            throw new gutil.PluginError('webpack:build', err);
        gutil.log('[webpack:build] Completed\n' + stats.toString({
            assets: true,
            chunks: false,
            chunkModules: false,
            colors: true,
            hash: false,
            timings: false,
            version: false

The task you want to look at is the webpack:build task. This simply calls the webpack() API. Normally, the stats.toString() call will contain a whole host of information many hundreds of lines long – I only want the summary, so I’ve turned off the things I don’t want to see.

I’ve also added two tasks for checking the files with a eslint. I tend to run linters separately as well as together with the client. My webpack configuration still specifies that the linting is done as part of the build. This allows me to continue to use the development server. However, now I can run linting separately as well.

Now that I have this in place, I can rig my server to do a development build. Here are all the pieces:

Step 1: Install the libraries

I use font-awesome, material design icons and core-js in my project:

npm install --save font-awesome mdi core-js

Step 2: Create a task that copies the right files into the public area

Here is the code snippet for copying the files to the right place:

var eslint = require('gulp-eslint'),
    gulp = require('gulp'),
    gutil = require('gulp-util'),
    webpack = require('webpack'),
    webpackConfig = require('./webpack.config.js');

var files = {
    client: [ 'client/**/*.js', 'client/**/*.jsx' ],
    server: [ 'server/**/*.js' ],
    libraries: [
var destination = './public';

gulp.task('libraries:copy', function () {
    return gulp.src(files.libraries, { base: './node_modules' })

Note that I’m not interested in copying all the files from the packages into my web area. In general, the package contains much more than you need. For example, font-awesome contains less and sass files – not really needed in my project. Take a look at what comes along with the package and only copy what you need. You can find out about the syntax of the filename glob by reading the Glob primer in node-glob.

Step 3: Update your configuration to specify the locations of the libraries.

I added the following to the config/default.json:

    "port": 3000,
    "env": "development",
    "base": "/",
    "library": {
        "core-js": "//",
        "mdi": "//",
        "font-awesome": "//"

Lines 5-9 specify the normal locations of the libraries. In this case, they are all out on the Internet on a CDN somewhere. In my config/development.json file, I specify their new locations:

    "env": "development",
    "base": "",
    "library": {
        "core-js": "core-js/client/core.min.js",
        "mdi": "mdi/css/materialdesignicons.min.css",
        "font-awesome": "font-awesome/css/font-awesome.min.css"

When I import the config, I can read the library location with config.get('library.core-js'); (or whatever the library is).

Step 4: Update the home page configuration

In server/static/index.js, I have a nice function for loading a HTML file. I want to replace the libraries as I do the env and base configuration:

function loadHtmlFile(filename) {
    var contents = '', file = path.join(__dirname, filename);
    if (!Object.hasOwnProperty(fileContents, filename)) {
        contents = fs.readFileSync(file, 'utf8'); // eslint-disable-line no-sync
        fileContents[filename] = contents
            .replace(/\$\{config.base\}/g, config.get('base'))
            .replace(/\$\{config.env\}/g, config.get('env'))
            .replace(/\$\{config.library.font-awesome}/g, config.get('library.font-awesome'))
            .replace(/\$\{config.library.mdi}/g, config.get('library.mdi'))
            .replace(/\$\{config.library.core-js}/g, config.get('library.core-js'))
    return fileContents[filename];

I’ve got a relatively small number of libraries, so the overhead of a templating engine is not worth it right now. However, if I grew the number of libraries more, I’d probably switch this over to a template engine like EJS. I also need to update my index.html file to match:

<!DOCTYPE html>

    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Grumpy Wizards</title>
    <link rel="stylesheet" href=",400,500,700"/>
    <link rel="stylesheet" href="${config.library.mdi}"/>
    <link rel="stylesheet" href="${config.library.font-awesome}">

    <div id="pageview"></div>

        window.GRUMPYWIZARDS = {
            env: '${config.env}',
            base: '${config.base}'
    <script src="${config.library.core-js}"></script>
    <script src="vendor.bundle.js"></script>
    <script src="grumpywizards.js"></script>

Step 5: Update package.json to copy the libraries to the right place before running nodemon

I added a new script to my package.json:

  "scripts": {
    "build": "gulp build",
    "prenodemon": "gulp libraries:copy",
    "nodemon": "nodemon --watch server ./server.js",
    "start": "node ./server.js"

With all this done, I now have two modes:

  • In development mode, run by npm run nodemon, I copy the libraries to the right place and then serve those libraries locally
  • In production mode, run by NODE_ENV=production npm start, I serve the libraries from a CDN, saving my bandwidth

If I change the libraries that are copied into the public area, I will have to stop and restart the server. That is a relatively rare thing (I only have three libraries), so I’m willing to make that a part of my workflow when it happens.

As always, grab the latest source from my GitHub Repository.

2 thoughts on “Gulp and Webpack – Better Together

  1. Hey Adrian! Interesting article!

    However, I’m wondering that, since you’re already using WebPack, if you could have simply used the css/style/file/url loaders to include font-awesome and related libraries right in your bundle. Then, in your entrypoint, simply:


    Without any need to copy it anywhere.

    While this will embed your css files into your bundles, you can use the ExtractTextPlugin to extract it into CSS files for production. Or, for production, you can leave in the link tags for the CDN URLs, and have Webpack exclude those libraries from being included in the bundle.


    • I could certainly have done that, and I’ve blogged about that in the past. I felt that I wanted certain things to be separate – JS libraries being bundled into a vendor bundle is a good idea, but webpack does introduce complexity – I wanted to keep the fonts separate and available on a CDN. Just because you can webpack everything doesn’t mean you should. My feeling is that I don’t leverage CDNs enough.


Comments are closed.