Learning Gutenberg: Setting up a Custom webpack Config

Avatar of Andy Bell
Andy Bell on (Updated on )

📣 Freelancers, Developers, and Part-Time Agency Owners: Kickstart Your Own Digital Agency with UACADEMY Launch by UGURUS 📣

Gutenberg introduces the modern JavaScript stack into the WordPress ecosystem, which means some new tooling should be learned. Although tools like create-guten-block are incredibly useful, it’s also handy to know what’s going on under the hood.

Article Series:

  1. Series Introduction
  2. What is Gutenberg, Anyway?
  3. A Primer with create-guten-block
  4. Modern JavaScript Syntax
  5. React 101
  6. Setting up a Custom webpack (This Post)
  7. A Custom “Card” Block
The files we will be configuring here should be familiar from what we covered in the Part 2 Primer with create-guten-block. If you’re like me (before reading Andy’s tutorial, that is!) and would rather not dive into the configuration part just yet, the scaffold created by create-guten-block matches what we are about to create here, so you can certainly use that as well.

Let’s jump in!

Getting started

Webpack takes the small, modular aspects of your front-end codebase and smooshes them down into one efficient file. It’s highly extendable and configurable and works as the beating heart of some of the most popular products and projects on the web. It’s very much a JavaScript tool, although it can be used for pretty much whatever you want. For this tutorial, it’s sole focus is JavaScript though.

What we’re going to get webpack doing is watch for our changes on some custom block files and compile them with Babel to generate JavaScript files that can be read by most browsers. It’ll also merge any dependencies that we import.

But first, we need a place to store our actual webpack setup and front-end files. In Part 2, when we poked around the files generated by create-guten-block, we saw that it created an infrastructure for a WordPress plugin that enqueued our front-end files in a WordPress way, and enabled us to activate the plugin through WordPress. I’m going to take this next section to walk us through setting up the infrastructure for a WordPress plugin for our custom block.

Setting up a plugin

Hopefully you still have a local WordPress instance running from our primer in Part 2, but if not, you’ll need to have one installed to continue with what we’re about to do. In that install, navigate to wp-content/plugins and create a fresh directory called card-block (spoiler alert: we’re going to make a card block… who doesn’t like cards?).

Then, inside card-block, create a file called card-block.php. This will be the equivalent to plugin.php from create-guten-block. Next, drop in this chunk of comments to tell WordPress to acknowledge this directory as a plugin and display it in the Plugins page of the Dashboard:

<?php
   /*
   Plugin Name: Card Block
   */

Don’t forget the opening PHP tag, but you can leave the closing one off since we’ll be adding more to this file soon enough.

WordPress looks for these comments to register a plugin in the same way it looks for comments at the top of style.css in a theme. This is an abbreviated version of what you’ll find at the top of other plugins’ main files. If you were planning to release it on the WordPress plugin repository, you’d want to add a description and version number as well as license and author information.

Go ahead and activate the plugin through the WordPress Dashboard, and I’ll take the steering wheel back to take us through setting up our Webpack config!

Getting started with webpack

The first thing we’re going to do is initialize npm. Run the following at the root of your plugin folder (wp-content/plugins/card-block):

npm init

This will ask you a few questions about your project and ultimately generate you a package.json file, which lists dependencies and stores core information about your project.

Next, let’s install webpack:

npm install webpack --save-dev

You might have noticed that we’re installing webpack locally to our project. This is a good practice to get into with crucial packages that are prone to large, breaking changes. It also means you and your team are all singing off the same song sheet.

Then run this:

npm install extract-text-webpack-plugin@next --save-dev

Then these Sass and CSS dependencies:

npm install node-sass sass-loader css-loader --save-dev

# J Parenti wrote in to say that in Babel 7, it's different. You may need to:
npm uninstall --save-dev sass-loader
npm install --save-dev [email protected]

Now, NPX to allow us to use our local dependencies instead of any global ones:

npm install npx -g

Lastly, run this:

npm install webpack-cli --save-dev

That will install the webpack CLI for you.

Now that we have this installed, we should create our config file. Still in the root of your plugin, create a file called webpack.config.js and open that file so we can get coding.

With this webpack file, we’re going to be working with traditional ES5 code for maximum compatibility, because it runs with Node JS. You can use ES6 and Babel, but we’ll keep things as simple as possible for this tutorial.

Alight, let’s add some constants and imports. Add the following, right at the top of your webpack.config.js file:

var ExtractText = require('extract-text-webpack-plugin');
var debug = process.env.NODE_ENV !== 'production';
var webpack = require('webpack');

The debug var is declaring whether or not we’re in debug mode. This is our default mode, but it can be overridden by prepending our webpack commands with NODE_ENV=production. The debug boolean flag will determine whether webpack generates sourcemaps and minifies the code.

As you can see, we’re requiring some dependencies. We know that webpack is needed, so we’ll skip that. Let’s instead focus our attention on ExtractText. Essentially, ExtractText enables us to pull in files other than JavaScript into the mix. We’re going to need this for our Sass files. By default, webpack assumes everything is JavaScript, so ExtractText kind of *translates* the other types of files.

Let’s add some config now. Add the following after the webpack definition:

var extractEditorSCSS = new ExtractText({
  filename: './blocks.editor.build.css'
});

var extractBlockSCSS = new ExtractText({
  filename: './blocks.style.build.css'
});

What we’ve done there is instantiate two instances of ExtractText by passing a config object. All that we’ve set is the output of our two block stylesheets. We’ll come to these in the next series, but right now all you need to know is that this is the first step in getting our Sass compiling.

OK, after that last bit of code, add the following:

var plugins = [ extractEditorSCSS, extractBlockSCSS ];

Here we’ve got two arrays of plugins. Our ExtractText instances live in the core plugins set and we’ve got a couple of optimization plugins that are only smooshed into the core plugins set if we’re not in debug mode. We’re running that logic right at the end.

Next up, add this SCSS config object:

var scssConfig = {
  use: [
    {
      loader: 'css-loader'
    },
    {
      loader: 'sass-loader',
      options: {
        outputStyle: 'compressed'
      }
    }
  ]
};

This object is going to tell our webpack instance how to behave when it comes across scss files. We’ve got it in a config object to keep things as DRY as possible.

Last up, the meat and taters of the config:

module.exports = {
  context: __dirname,
  devtool: debug ? 'inline-sourcemap' : null,
  mode: debug ? 'development' : 'production',
  entry: './blocks/src/blocks.js',
  output: {
    path: __dirname + '/blocks/dist/',
    filename: 'blocks.build.js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader'
          }
        ]
      },
      {
        test: /editor\.scss$/,
        exclude: /node_modules/,
        use: extractEditorSCSS.extract(scssConfig)
      },
      {
        test: /style\.scss$/,
        exclude: /node_modules/,
        use: extractBlockSCSS.extract(scssConfig)
      }
    ]
  },
  plugins: plugins
};

That’s our entire config, so let’s break it down.

The script starts with module.exports which is essentially saying, “when you import or require me, this is what you’re getting.” We can determine what we expose to whatever imports our code, which means we could run code above module.exports that do some heavy calculations, for example.

Next, let’s look at some of these following properties:

  • context is our base where paths will resolve from. We’ve passed __dirname, which is the current working directory.
  • devtool is where we define what sort of sourcemap we may or may not want. If we’re not in debug mode, we pass null with a ternary operator.
  • entry is where we tell webpack to start its journey of pack-ery. In our instance, this is the path to our blocks.js file.
  • output is what it says on the tin. We’re passing an object that defines the output path and the filename that we want to call it.

Next is module, which we’ll get into a touch more detail. The module section can contain multiple rules. In our instance, the only rule we have is looking for JavaScript and SCSS files. It’s doing this by searching with a regular expression that’s defined by the test property. The end-goal of the rule is to find the right sort of files and pass them into a loader, which is babel-loader in our case. As we learned in a previous tutorial in this series, Babel is what converts our modern ES6 code into more supported ES5 code.

Lastly is our plugins section. Here, we pass our array of plugin instances. In our project, we have plugins that minify code, remove duplicate code and one that reduces the length of commonly used IDs. This is all to make sure our production code is optimized.

For reference, this is how your full config file should look:

var ExtractText = require('extract-text-webpack-plugin');
var debug = process.env.NODE_ENV !== 'production';
var webpack = require('webpack');

var extractEditorSCSS = new ExtractText({
  filename: './blocks.editor.build.css'
});

var extractBlockSCSS = new ExtractText({
  filename: './blocks.style.build.css'
});

var plugins = [extractEditorSCSS, extractBlockSCSS];

var scssConfig = {
  use: [
    {
      loader: 'css-loader'
    },
    {
      loader: 'sass-loader',
      options: {
        outputStyle: 'compressed'
      }
    }
  ]
};

module.exports = {
  context: __dirname,
  devtool: debug ? 'inline-sourcemap' : null,
  mode: debug ? 'development' : 'production',
  entry: './blocks/src/blocks.js',
  output: {
    path: __dirname + '/blocks/dist/',
    filename: 'blocks.build.js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader'
          }
        ]
      },
      {
        test: /editor\.scss$/,
        exclude: /node_modules/,
        use: extractEditorSCSS.extract(scssConfig)
      },
      {
        test: /style\.scss$/,
        exclude: /node_modules/,
        use: extractBlockSCSS.extract(scssConfig)
      }
    ]
  },
  plugins: plugins
};
That’s webpack configured. I’ll take the mic back to show us how to officially register our block with WordPress. This should feel pretty familiar to those of you who are used to wrangling with actions and filters in WordPress.

Registering our block

Back in card-block.php, our main task now is to enqueue the JavaScript and CSS files we will be building with webpack. In a theme, we would do this with call wp_enqueue_script and wp_enqueue_style inside an action added to wp_enqueue_scripts. We essentially do the same thing here, except instead we enqueue the scripts and styles with a function specific to blocks.

Drop this code below the opening comment in card-block.php:

function my_register_gutenberg_card_block() {

  // Register our block script with WordPress
  wp_register_script(
    'gutenberg-card-block',
    plugins_url('/blocks/dist/blocks.build.js', __FILE__),
    array('wp-blocks', 'wp-element', 'wp-editor')
  );

  // Register our block's base CSS  
  wp_register_style(
    'gutenberg-card-block-style',
    plugins_url( '/blocks/dist/blocks.style.build.css', __FILE__ ),
    array( 'wp-blocks' )
  );
  
  // Register our block's editor-specific CSS
  if( is_admin() ) :
     wp_register_style(
      'gutenberg-card-block-edit-style',
      plugins_url('/blocks/dist/blocks.editor.build.css', __FILE__),
      array( 'wp-edit-blocks' )
    );
  endif;
  
  // Enqueue the script in the editor
  register_block_type('card-block/main', array(
    'editor_script' => 'gutenberg-card-block',
    'editor_style' => 'gutenberg-card-block-edit-style',
    'style' => 'gutenberg-card-block-edit-style'
  ));
}

add_action('init', 'my_register_gutenberg_card_block');

As the above comments indicate, we first register our script with WordPress using the handle gutenberg-card-block with two dependencies: wp-blocks and wp-elements. This function only registers a script, it does not enqueue it. We do something similar for out edit and main stylesheets.

Our final function, register_block_type, does the enqueuing for us. It also gives the block a name, card-block/main, which identifies this block as the main block within the namespace card-block, then identifies the script and styles we just registered as the main editor script, editor stylesheet, and primary stylesheet for the block.

If you are familiar with theme development, you’ve probably used get_template_directory() to handle file paths in hooks like the ones above. For plugin development, we use the function plugins_url() which does pretty much the same thing, except instead of concatenating a path like this: get_template_directory() . '/script.js', plugins_url() accepts a string path as a parameter and does the concatenation for us. The second parameter, _ FILE _, is one of PHP’s magic constants that equates to the full file path of the current file.

Finally, if you want to add more blocks to this plugin, you’d need a version of this function for each block. You could work out what’s variable and generate some sort of loop to keep it nice and DRY, further down the line. Now, I’ll walk us through getting Babel up and running.

Getting Babel running

Babel turns our ES6 code into better-supported ES5 code, so we need to install some dependencies. In the root of your plugin (wp-content/plugins/card-block), run the following:

npm install babel-core babel-loader babel-plugin-add-module-exports babel-plugin-transform-react-jsx babel-preset-env --save-dev

Update April 2019: We’ve had a lot of updates to this over the last many months because of the big change in Babel from 6 to 7. Thanks to Nina Regli, Rick Hughs, and Bryan Chong.<.p>

Nina had luck with:

npm install --save-dev @babel/core @babel/preset-env

Rick had luck with:

npm install @babel/core babel-loader babel-plugin-add-module-exports babel-plugin-transform-react-jsx @babel/preset-env --save-dev

Bryan had luck with:

npm install --save-dev @babel/preset-react babel-preset-minify @babel/core @babel/cli @babel/preset-en

Bryan also renamed `.babelrc` to `babel.config.js` and configured it like this:

module.exports = function (api) {
  return {
    presets: [
      [
        "@babel/preset-react",
        {
          "pragma": "wp.element.createElement"
        }
      ],
      "minify",
      "@babel/env"
    ]
  };
}

That big ol’ npm install adds all the Babel dependencies. Now we can add our .babelrc file which stores some settings for us. It prevents us from having to repeat them over and over in the command line.

While still in your theme folder, add the following file: .babelrc.

Now open it up and paste the following:

{
  "presets": ["env"],
  "plugins": [
    ["transform-react-jsx", {
      "pragma": "wp.element.createElement"
    }]
  ]
}

Update April 2019: With Babel 7, you probably need "presets": ["@babel/preset-env"],

So, what we’ve got there are two things:

"presets": ["env"] is basically magic. It automatically determines which ES features to use to generate your ES5 code. We used to have to add different presets for all the different ES versions (e.g. ES2015), but it’s been simplified.

In the plugins, you’ll notice that there’s a React JSX transformer. That’s sorting out our JSX and turning it into proper JavaScript, but what we’re doing is telling it to generate WordPress elements, rather than React elements, which JSX is more commonly associated with.

Generate stub files

The last thing we’re going to do is generate some stub files and test that our webpack and WordPress setup is all good.

Go into your plugin directory and create a folder called blocks and, within that, create two folders: one called src and one called dist.

Inside the src folder, create the following files. We’ve added the paths, too, so you put them in the right place:

  • blocks.js
  • common.scss
  • block/block.js
  • block/editor.scss
  • block/style.scss

Now that you’ve created those files, open up blocks.js and add this one-liner and save:

import './block/block';

OK, so now we’ve generated the minimum amount of things, let’s run webpack. Open up your terminal and move into your current plugin folder—then we can run the following, which will fire-up webpack:

npx webpack

Pretty dang straightforward, huh? If you go ahead and look in your dist folder, you should see some compiled goodies in there!

Wrapping up

A lot of setup has been done, but all of our ducks are in a row. We’ve set up webpack, Babel and WordPress to all work as a team to build out or custom Gutenberg block (and future blocks). Hopefully now you feel more comfortable working with webpack and feel like you could dive in and make customizations to fit your projects.


Article Series:

  1. Series Introduction
  2. What is Gutenberg, Anyway?
  3. A Primer with create-guten-block
  4. Modern JavaScript Syntax
  5. React 101
  6. Setting up a Custom webpack (This Post)
  7. A Custom “Card” Block