How webpack and WordPress packages interact

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.

Browser console displaying the contents of the wp global.

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?


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;
			plugin_dir_url( __FILE__ ) . 'build/index.js',

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 many examples available in the Gutenberg 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.


return (
   <p { ...useBlockProps() }>
       { __(
           'My First Block – hello from the editor!',
       ) }

Vanilla JavaScript

return el(
   __( '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 = {
   entry: {
       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.

6 responses to “How webpack and WordPress packages interact”

  1. Juan Pablo Muñoz Upegui Avatar
    Juan Pablo Muñoz Upegui

    Hi, thanks for post. I just have a question. How can I add to “npm run build” a folder with assets?


    1. Ryan Welcher Avatar

      I’d need more information on what you are trying to do here but currently you can refer to assets (such as fonts and images) in your code and the wordpress/scripts will build them for you.

      See the Advanced Usage section here –

  2. It's me Avatar
    It’s me

    Great article. I have a question. What rule must be in the webpack.config.js file so that I can compile any separate custom.scss file? For example, the custom.scss file is located in /plugin-path/_src/sass/custom.scss and I want it to be compiled to /plugin-path/assets/css/custom.css

  3. John Avatar

    Great article. I have a question. What rule must be in the webpack.config.js file so that I can compile any separate custom.scss file? For example, the custom.scss file is located in /plugin-path/_src/sass/custom.scss and I want it to be compiled to /plugin-path/assets/css/custom.css

  4. Dovid Levine Avatar

    Great article!

    If the scripts are just proxy’s for WordPress’s built in bundles, how can I make sure the script versions I’m using are compatible with a range of WP versions?

    Is there a way to target a minimum WP version (like WPCompatibility does for phpcs?)

Leave a Reply

Your email address will not be published. Required fields are marked *