For your users, working with blocks makes everything look easy on the front end. But behind the scenes, your life will get a lot easier—for real—the better you understand and use the build process that turns your React and JSX into plain JavaScript that any browser can read.
In front-end development, the concept of a transpiler is not new. (That’s a program that translates a source code from one language to another.) You’ve probably heard of SASS and CoffeeScript; their output has to go through a transpiler before the browser can present it.
We can can use a transpiler to lighten our load of tedious, repetitive tasks, like browser prefixing. Plugins like Babel and PostCSS let us concentrate on our code, and what it does, and the plugins add the prefixes and whatever else our code needs to work in every browser.
Beyond transpilers, there are also bundlers—and lots of them!—to manage build processes. Tools like Grunt and Bower were among the first and are now almost ten years old. For the most part, the web has moved on to more modern tools, like webpack, Vite, and more.
WordPress uses webpack
For its build process, WordPress uses webpack. You’ll find it in the @wordpress/scripts
package, which gets output alongside the blocks that come from the @wordpress/create-block
package.
A fun fact: that’s the very package the Gutenberg project uses to build the Gutenberg plugin!
The intention of that package is to configure webpack for you, so you can hit the ground running with your project. Because configuring webpack yourself can be notoriously difficult.
Imports that “don’t exist”
If you come from a JavaScript background, you have likely worked with packages before. You start by installing the packages with npm install
, then you import items from those packages to use in your project.
You will also be familiar with how importing items from packages affects the size of your bundle (the JavaScript file that webpack outputs). Typically, the more you import, the bigger the file gets, and you address the issue with techniques like code splitting and tree shaking.
When you’re working with WordPress packages, you may have noticed two things:
- You never install the packages using
npm install
. - No matter how many items you import from
@wordpress/*
packages, the bundle size doesn’t change much, if at all.
Has WordPress found a way to work with JavaScript that requires no imports and doesn’t increase bundle size no matter how many things you import?
The short answer is: Yes. But only for packages that ship with WordPress. Because WordPress automatically enqueues and adds these packages to a global wp
object.
Because these packages are already available, it makes much more sense to use those instead of rebundling them with your custom scripts—and that’s exactly what is happening behind the scenes.
For example, this code snippet generates a very simple Button component, imported from the @wordpress/components
package.
import { Button } from '@wordpress/components';
const MyComponent = () => {
return <Button>{__('Click me!')}</Button>;
}
export default MyComponent;
If you take a look at the file webpack transpiles this into, you will see some lines that look like this. This is webpack exporting some paths to packages of items we’re using on the wp
global
/***/ "@wordpress/components":
/*!************************************!*\
!*** external ["wp","components"] ***!
\************************************/
/***/ ((module) => {
module.exports = window["wp"]["components"];
/***/ }),
/***/ "@wordpress/element":
/*!*********************************!*\
!*** external ["wp","element"] ***!
\*********************************/
/***/ ((module) => {
module.exports = window["wp"]["element"];
Then, later in the file, we have this. It is pretty terse, but this is webpack creating variables that refer to the exports it defined above. Those variables contain all of the contents of that package.
var _wordpress_element__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @wordpress/element */ "@wordpress/element");
var _wordpress_element__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_wordpress_element__WEBPACK_IMPORTED_MODULE_0__);
var _wordpress_components__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @wordpress/components */ "@wordpress/components");
var _wordpress_components__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_wordpress_components__WEBPACK_IMPORTED_MODULE_1__);
Finally, the actual component references those variables to retrieve the items you need.
const MyComponent = () => {
return (0,_wordpress_element__WEBPACK_IMPORTED_MODULE_0__.createElement)(_wordpress_components__WEBPACK_IMPORTED_MODULE_1__.Button, null, "'Click me!");
};
const __WEBPACK_DEFAULT_EXPORT__ = (MyComponent);
There is a lot going on here, so if you’re overwhelmed at this point, don’t worry. You don’t actually need to read (or understand) this file.
If you simplify the code to replace the variables with the actual paths, it looks something like this. And it gets a lot easier to see what is going on!
const MyComponent = () => {
return (0,wp.element.createElement)(wp.components.Button, null, "'Click me!");
};
const __WEBPACK_DEFAULT_EXPORT__ = (MyComponent);
As you can see, your code has changed: it looks at the wp
global instead of the packages being imported.
You may have noticed that in the example above wp.element.createElement
is used, but in the myComponent
example, there is no reference to it at all. This function is being added by the build process.
It is part of React, and it creates React components without using JSX. See the official documentation for more details.
So the big question is: how does Webpack do this?
DependencyExtractionWebPackPlugin
As part of the webpack configuration you get in the @wordpress/scripts
package, there is a custom webpack plugin called the DependencyExtractionWebPackPlugin, and it does two very important things to make all of this work:
- Convert the Imports
- Automate dependency management
Job One: convert the imports
Its first job is to detect any import statements that start with wordpress (and some others) and make them access the wp
global instead.
Essentially, it turns this code
import { Button } from '@wordpress/components';
Into this:
const { Button } = wp.components;
Any third-party packages that are installed in your project are not converted and will still increase the bundle size.
Job Two: Automate the dependency management
When you enqueue a script in WordPress, typically you need to define an array of dependencies that will load before your script.
But when you’re using the webpack workflow in this post, that’s Job Two of the DependencyExtractionWebPackPlugin
.
It will generate a PHP file called index.asset.php
that lists the dependencies it just converted from the import statements. Then it will version each one, based on the last time the file was built.
<?php return array('dependencies' => array('wp-components', 'wp-element'), 'version' => '1d75a9e186898f1d6300');
index.asset.php
is the default name but it is derived from the name of the entry point name as defined in webpack. If we defined the entry point name as test
, the emitted file would be called test.asset.php
. This is only important if you are extending the webpack configuration.
If you’re building blocks, the block registration process automatically loads this file. But if you wanted to use it in your code, you could do something like the following
add_action( 'enqueue_block_editor_assets', 'enqueue_my_file' );
function enqueue_my_file() {
// Find the path.
$dependencies_file_path = plugin_dir_path( __FILE__ ) . 'build/index.asset.php';
// If the file exists, enqueue it.
if ( file_exists( $dependencies_file_path ) ) {
$dependencies = require $dependencies_file_path;
wp_enqueue_script(
'my-script',
plugin_dir_url( __FILE__ ) . 'build/index.js',
$dependencies['dependencies'],
$dependencies['version']
);
}
}
This file is extremely handy. Once you reference it, you never have to worry about updating your dependencies again!
Bypassing the build process
You may be wondering, why would I need this? All it does is convert the code to something I can just as easily write myself.
Well, the simple answer is, you don’t. You can bypass the build process altogether and write standard JavaScript; it will work fine.
For example:
const MyComponent = function () {
return wp.element.createElement( wp.components.Button, null, "'Click me!" );
}
This code is perfectly valid, and there are some examples available in the Block Development Examples repository that don’t use the build process at all.
Benefits of using a build process
Again, you don’t really have to use the build process. It’s entirely optional.
But look what you get from the build process:
- Automatic block detection
- Automated dependency management
- Use of JSX
- Import syntax
- Static code analysis
- Available commands
All at no extra charge! (Just kidding … ) But take a look at why you might want these benefits.
Automatic block detection
If you are building custom blocks, the build process can automatically detect and build any blocks that get added to your project.
Automated dependency management
We’ve already discussed automatic dependency management, but here’s the thing: if you don’t use the build process for a custom block, you still need to create the index.asset.php
file. It’s required for block registration. And then you get to update the dependencies manually.
So as they say on those cheesy police shows: you can do it the hard way, or you can do it the easy way.
You can use JSX
When you use the build process, you can use JSX syntax. That’s a syntax extension for JavaScript a lot of developers use with React.js, which is the framework Gutenberg is built on. Its syntax looks a lot like HTML, which makes it easier to read (and easier to maintain!) than vanilla JS.
JSX
return (
<p { ...useBlockProps() }>
{ __(
'My First Block – hello from the editor!',
'my-first-block'
) }
</p>
);
Vanilla JavaScript
return el(
'p',
useBlockProps(),
__( 'My First Block – hello from the editor!', 'my-first-block' )
);
Import syntax
Leveraging the build process lets you use JavaScript modules to import and export files and components. Which makes your code easier to organize and much easier to reuse.
Static code analysis
The build process gives you lots of tools that check your code’s formatting and fix linting issues. Automatically.
And more
And the benefits above are just the beginning. There are a lot more commands and other tools that can make your life easier and your projects better. Check it all out in the official documentation for the @wordpress/scripts
package
{
"scripts": {
"build": "wp-scripts build",
"check-engines": "wp-scripts check-engines",
"check-licenses": "wp-scripts check-licenses",
"format": "wp-scripts format",
"lint:css": "wp-scripts lint-style",
"lint:js": "wp-scripts lint-js",
"lint:md:docs": "wp-scripts lint-md-docs",
"lint:md:js": "wp-scripts lint-md-js",
"lint:pkg-json": "wp-scripts lint-pkg-json",
"packages-update": "wp-scripts packages-update",
"plugin-zip": "wp-scripts plugin-zip",
"start": "wp-scripts start",
"test:e2e": "wp-scripts test-e2e",
"test:unit": "wp-scripts test-unit-js"
}
}
Customizing the Webpack configuration
As the official documentation points out, you can customize webpack to your needs. How? Add a webpack.config.js
, then extend the default configuration. We could spend the next week going into all the ways you can extend the configuration, but we’d be way beyond the scope of this article. Suffice it to say there’s a lot to unpack in the webpack documentation.
But there is one common case that’s worth covering here.
What if you want to create custom blocks in your project, but you also need to generate a separate file for another use case? For example, what if you want to generate SlotFills?
To get started, you need to add a new entry point.
// Import the original config from the @wordpress/scripts package.
const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );
// Import the helper to find and generate the entry points in the src directory
const { getWebpackEntryPoints } = require( '@wordpress/scripts/utils/config' );
// Add a new entry point by extending the Webpack config.
module.exports = {
...defaultConfig,
entry: {
...getWebpackEntryPoints(),
custom: './path/to/index.js',
},
};
The caveat here is that you also want to maintain the functionality that detects and builds blocks dynamically—normally handled with the getWebpackEntryPoints function.
Thank you to @bph, @greenshady, and @marybaum for reviewing this post.
Leave a Reply