From Grunt to Webpack

For the purposes of this article I’ll assume that you’re already familiar with build automation tools and know what a task runner and module bundler are.  I’ll also assume that you’re familiar with node, node package manager and at least somewhat familiar with Grunt, since you’re reading this article about upgrading from Grunt to Webpack.  I’ll be describing the process of installing and configuring Webpack v.1 using npm so be sure to have node and npm installed.

Grunt is a task runner built on node.js and Webpack is a module bundler built on javascript.  Both have huge ecosystems with plenty of plugins/modules available. But why should you choose Webpack over Grunt?  Since Grunt emphasizes configuration over code, configuring Grunt is a somewhat simple process.  However, because of this, Grunt configuration files tend to become large and bloated.  While the Webpack configuration may be a little more complex, Webpack does offer some benefits over Grunt.  For instance, with Webpack support ES6, simple loaders can be piped together to create more complex transformations and Webpack allows your codebase to be split into chunks that get loaded on demand, thereby reducing loading times.

Getting started with Webpack

While Webpack isn’t a task runner, per se, in most cases, it can serve as an adequate substitute for a task runner like Grunt.  While Grunt uses “tasks” to handle the front-end build process, Webpack uses “loaders”.  You can think of loaders kind of like tasks in other build tools like Grunt.  Loaders allow you to preprocess files as they are loaded.  Installing and configuring Webpack is a pretty straightforward process.

Step 1: Install Webpack

In your project, use npm to install and save Webpack.

$ npm install webpack --save-dev

 

 

Step 2: Create webpack.config.js

At the root of your project, or wherever you’d normally add your Gruntfile.js file, create a new file and save it as “webpack.config.js”.  Add the following to the top:

var webpack = require("webpack");

var path = require('path');

module.exports = {

    entry: {

        main: '[PATH TO INPUT]'

    },

    output: {

        path: '[PATH TO JS OUTPUT]',

        filename: 'scripts.bundle.js',

    },

    devtool: "source-map",

    module: {},

    plugins: [],

    watch: true, // Set to false to keep the grunt process alive

    watchOptions: {

        aggregateTimeout: 500,

        // poll: true // Use this when you need to fallback to poll based watching (webpack 1.9.1+ only)

    },

    keepalive: true, // defaults to true for watch and dev-server otherwise false

};

 

The first 2 variables at the top are requiring Webpack and setting the path variable.  The entry option defines our main javascript file.  It’s possible to specify multiple entry points but a single entry point is sufficient in most cases.  You can read more about Webpack entry points here.   You can specify the name and path of your output file in the output object.

The devtool option controls if and how source maps are generated.  We’ll set this to “source-map” to generate a full sourcemap as a separate file.  You can read more about the devtool option here.  The module object and plugins array are where we will define the loaders, preloaders, postloaders and plugins that we want to use in our project.  You can read more about Webpack loaders here.

The watch option turns the watch task on/off and the watchOptions object configures the options for the watch task.  The keepalive option will keep the watch task running once it’s complete if set to true.  NOTE: It’s possible to use a Grunt task to run Webpack which I’ll touch on a bit later in this article.  If you want to use a Grunt watch task, then be sure to set the ‘watch’ and ‘keepalive’ options to ‘false’ so that the Webpack watch task won’t interfere with the Grunt watch task.

You can now use the ‘require();’ statement in your js.  Run this to build the bundle:

$ webpack
 

 

Step 3: Modules Part 1 - Add preloaders

Next, we’ll add a preloader to our config.  Like loaders, preloaders are preprocessing tasks that run before all other loaders.  Alternatively, postloaders run after all other loaders.  We won’t be adding any postloaders and the only preloader we’ll using is the jshint-loader to lint our js. To install jshint-loader run:

$ npm install jshint-loader --save-dev

 

Once installed you’ll need to update the preloaders array in the webpack config.
 

var webpack = require("webpack");

var path = require('path');

module.exports = {

    entry: {

        main: '[PATH TO INPUT]'

    },

    output: {

        path: '[PATH TO JS OUTPUT]',

        filename: 'scripts.bundle.js',

    },

    devtool: "source-map",

    module: {

        preLoaders: [

            {

                test: /.*\.js$/,

                exclude: [

                    /(node_modules|vendor|wp-admin|wp-includes|plugins|twentyfifteen|twentysixteen|twentyseventeen|libs|vendor|bundle|public)/

                ],

                loader: 'jshint-loader'

            }

        ]

    },

    plugins: [],

    watch: false, // You need to keep the grunt process alive

    watchOptions: {

        aggregateTimeout: 500,

        // poll: true // Use this when you need to fallback to poll based watching (webpack 1.9.1+ only)

    },

    keepalive: false, // defaults to true for watch and dev-server otherwise false

};

 

We’ve added a preloader object to the preloaders array with 3 option, test, exclude and loader.  The loader option simply specifies which loader we are configuring.  Set the value of that to “jshint-loader” to use the preloader that we just installed.  The test option is a regular expression that matches the pattern of the file names/extensions that you want the preloader/loader/postloader to affect.  The exclude option is an array of file and directory names that you want the preloader/loader/postloader to ignore.

Step 4: Modules Part 2 - Install loaders

There are a ton of loaders that you can use to add functionality to your projects.  Here I’ll touch on some of the more common ones.

babel-loader
Allows transpiling javascript files using Babel and Webpack.

$ npm install babel-loader babel-core babel-preset-es2015 --save-dev

 

uglify-loader
A parser for javascript minification and beautification.

$ npm install uglify-loader --save-dev

 

file-loader
Resolves import or require() on a file into a URL and emits the file into the output directory.

$ npm install file-loader --save-dev

 

image-webpack-loader
Loads image files into the js bundle.  File loader comes bundled with this loader and will be installed automatically when installing image-webpack-loader.

$ npm install image-webpack --save-dev

 

ruby-sass-loader
A SASS compiler for webpack.

$ npm install ruby-sass-loader --save-dev

 

postcss-loader
Runs a variety of PostCSS javascript tools for transforming CSS.  

$ npm install postcss-loader --save-dev

 

css-loader
Loads compiled CSS into the js bundle.

$ npm install css-loader --save-dev

 

style-loader
Loads styles into the DOM or external stylesheet using the ExtractTextPlugin

npm install style-loader --save-dev

 

 

Again, once you’ve installed all of the loaders that you would like to use in your project, you’ll need to update the Webpack config to use them.

var webpack = require("webpack");

var path = require('path');

module.exports = {

    entry: {

        main: '[PATH TO INPUT]'

    },

    output: {

        path: '[PATH TO JS OUTPUT]',

        filename: 'scripts.bundle.js',

    },

    devtool: "source-map",

    module: {

        preLoaders: [

            {

                test: /.*\.js$/,

                exclude: [

                    /(node_modules|vendor|wp-admin|wp-includes|plugins|twentyfifteen|twentysixteen|twentyseventeen|libs|vendor|bundle|public)/

                ],

                loader: 'jshint-loader'

            }

        ],

        loaders: [

            {

                test: /.*\.js$/,

                exclude: [

                    /(node_modules|vendor|wp-admin|wp-includes|plugins|twentyfifteen|twentysixteen|twentyseventeen|libs|vendor|bundle|public)/

                ],

                loader: 'babel-loader',

               query: {

                    presets: ['es2015']

                }

            },

            {

                test: /.*\.js$/,

                exclude: [

                    /(node_modules|vendor|wp-admin|wp-includes|plugins|twentyfifteen|twentysixteen|twentyseventeen|libs|vendor|bundle|public)/

                ],

                loader: 'uglify'

            },

            {

                test: /\.(jpe?g|gif|png)$/i,

                loaders: [

                    'file?hash=sha512&digest=hex&name=[name].[ext]',

                    'image-webpack?bypassOnDebug&optimizationLevel=7&interlaced=false&name=/[name].[ext]'

                ]

            },

            {

                test: /\.(ttf|eot|svg|woff(2)?)(\?|#[#?a-z]+)?(\?|#[#?0-9]+)?(#[a-z]+)?(#[0-9]+)?/,

                loader: 'file-loader?name=/[name].[ext]'

            },

            {

                test: /.*\.scss$/,

                loader: ExtractTextPlugin.extract('style-loader', 'css-loader?sourceMap!postcss-loader?sourceMap!ruby-sass?sourceMap'),

                fallbackLoader: 'style-loader!css-loader?sourceMap!postcss-loader?sourceMap!ruby-sass?sourceMap'

            }

        ]

    },

    postcss: function() {

        return [

            require('pixrem'),

            require('autoprefixer')

        ];

    },

    plugins: [],

    devtool: "source-map",

    watch: false, // You need to keep the grunt process alive

    watchOptions: {

        aggregateTimeout: 500,

        // poll: true // Use this when you need to fallback to poll based watching (webpack 1.9.1+ only)

    },

    keepalive: false, // defaults to true for watch and dev-server otherwise false

};

 

 

For the most part, adding loaders is no different than adding preloaders.  Notice that some loaders, such as the Babel loader, also include a query option.  This is just an object which defines the options for the loader.  In this specific case we’re telling the Babel loader to use the ES6 preset to transpile our ES6 code into javascript code that current browsers can actually read.  Also notice that we’re using the ExtractTextPlugin on the style-loader to extract the CSS into a separate file.  I’ll touch on this more in step 7

Step 5: Modules Part 3 - Install Postloaders

We aren’t using any postloaders in this project but do add them if you need to.  The process for installing postloaders is exactly the same as the process for installing preloaders and loaders.  Be sure to reference the documentation for the specific preloader that you want to use for specific installation and usage information.

Step 6: Install Plugins

Webpack has a rich plugin interface which makes it extremely flexible.  Here I’ll touch on 3 of the more common plugins, ExtractTextPlugin, CommonsChunkPlugin and webpackUglifyJsPlugin.  You can read more about the available Webpack plugins here.

ExtractTextPlugin
Outputs CSS to a separate file.

$ npm install extract-text-webpack-plugin --save-dev

 

CommonsChunkPlugin
Outputs common javascript in different files to a separate file.  Comes bundled with webpack.

webpackUglifyJsPlugin
Minifies the files that webpack outputs.

$ npm install webpack-uglify-js-plugin --save-dev

 

Per the usual, you’ll need to update the wordpress config file once installed.  Obviously, you’ll need to add the plugins to the “plugins” array.

var webpack = require("webpack");

var path = require('path');

var ExtractTextPlugin = require("extract-text-webpack-plugin");

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");

var webpackUglifyJsPlugin = require('webpack-uglify-js-plugin');

module.exports = {

    entry: {

        main: '[PATH TO INPUT]'

    },

    output: {

        path: '[PATH TO JS OUTPUT]',

        filename: 'scripts.bundle.js',

    },

    devtool: "source-map",

    module: {

        preLoaders: [

            {

                test: /.*\.js$/,

                exclude: [

                    /(node_modules|vendor|wp-admin|wp-includes|plugins|twentyfifteen|twentysixteen|twentyseventeen|libs|vendor|bundle|public)/

                ],

                loader: 'jshint-loader'

            }

        ],

        loaders: [

            {

                test: /.*\.js$/,

                exclude: [

                    /(node_modules|vendor|wp-admin|wp-includes|plugins|twentyfifteen|twentysixteen|twentyseventeen|libs|vendor|bundle|public)/

                ],

                loader: 'babel-loader',

               query: {

                    presets: ['es2015']

                }

            },

            {

                test: /.*\.js$/,

                exclude: [

                    /(node_modules|vendor|wp-admin|wp-includes|plugins|twentyfifteen|twentysixteen|twentyseventeen|libs|vendor|bundle|public)/

                ],

                loader: 'uglify'

            },

            {

                test: /\.(jpe?g|gif|png)$/i,

                loaders: [

                    'file?hash=sha512&digest=hex&name=[name].[ext]',

                    'image-webpack?bypassOnDebug&optimizationLevel=7&interlaced=false&name=/[name].[ext]'

                ]

            },

            {

                test: /\.(ttf|eot|svg|woff(2)?)(\?|#[#?a-z]+)?(\?|#[#?0-9]+)?(#[a-z]+)?(#[0-9]+)?/,

                loader: 'file-loader?name=/[name].[ext]'

            },

            {

                test: /.*\.scss$/,

                loader: ExtractTextPlugin.extract('style-loader', 'css-loader?sourceMap!postcss-loader?sourceMap!ruby-sass?sourceMap'),

                fallbackLoader: 'style-loader!css-loader?sourceMap!postcss-loader?sourceMap!ruby-sass?sourceMap'

            }

        ]

    },

    postcss: function() {

        return [

            require('pixrem'),

            require('autoprefixer')

        ];

    },

    plugins: [

        new ExtractTextPlugin('styles.bundle.css'),

        new CommonsChunkPlugin('commons.chunk.js'),

        new webpackUglifyJsPlugin({

            cacheFolder: path.resolve(__dirname, 'public/cached_uglify/'),

            debug: true,

            minimize: true,

            sourceMap: true,

            output: {

                comments: false

            },

            compressor: {

                warnings: false

            }

        })

    ],

    devtool: "source-map",

    watch: false, // You need to keep the grunt process alive

    watchOptions: {

        aggregateTimeout: 500,

        // poll: true // Use this when you need to fallback to poll based watching (webpack 1.9.1+ only)

    },

    keepalive: false, // defaults to true for watch and dev-server otherwise false

};

 

You’ll need to refer to the specific plugin documentation for usage instructions but generally, the process consists of creating a new object and passing some options in.

Webpack Dev Server

The Webpack dev server can be used to quickly develop you application and comes included with webpack v. < 2.0.  I won’t go into details on this here but you can refer to the github repo for usage instructions.

"webpack-dev-server": {

  options: {

    webpack: {

      // configuration for all builds

    },

    // server and middleware options for all builds

  },

  start: {

    webpack: {

      // configuration for this build

    },

    // server and middleware options for this build

  }

},

 

Using Webpack with Grunt

You can use the available grunt-webpack task to run webpack from Grunt.

$ npm install grunt-webpack --save-dev

 

Once installed you’ll need to update the Gruntfile.js file to include the task to run the webpack command.
 

var webpackConfig = require("[PATH TO webpack.config.js]");

webpack: {

  options: webpackConfig,

  dist: {

    // configuration for this build

  }

},

 

Grunt and Webpack are both great tools that ease some of the pain of the development process but there are pros and cons to both. You should use whatever makes sense for your particular project. Using Grunt and Webpack together could allow for one to compensate for the other where it's lacking.


Are you smart, motivated, and eager to solve problems in a collaborative environment? 

If so, we want you! Join our team!

See Our Current Career Opportunities