How to bundle blocks with Webpack and @wordpress/scripts in WordPress

Contents

Introduction

This tutorial explains, in exhaustive detail, how to bundle Gutenberg blocks for WordPress using Webpack together with the official @wordpress/scripts package. It covers project layout, installation, single-block and multi-block workflows, handling styles and assets, how to leverage block.json, automatic asset manifest usage, customizing Webpack when you need control, code splitting, internationalization, debugging tips, production optimizations, and common pitfalls.

Why use @wordpress/scripts?

@wordpress/scripts is a zero-configuration build tool created by the WordPress project. It wraps Babel, Webpack, ESLint, PostCSS, and other tools with sensible defaults for building blocks and other editor scripts. Use it to avoid handcrafting a full Webpack config unless you need custom behavior. When your needs outgrow the defaults, you can extend or override the default Webpack configuration.

Prerequisites

  • Node.js (LTS recommended) and npm (or Yarn) installed.
  • WordPress development install where you can add a plugin for testing.
  • Familiarity with ESNext (import/ export), React-style JSX (Gutenberg uses it), and PHP basics for registering the block server-side.

Project layout (recommended)

A recommended plugin layout for one or more blocks is:

my-block-plugin/
├─ package.json
├─ plugin.php
├─ src/
│  ├─ blocks/
│  │  ├─ hello-world/
│  │  │  ├─ index.js
│  │  │  ├─ editor.scss
│  │  │  ├─ style.scss
│  │  │  └─ block.json
│  │  └─ another-block/
│  │     ├─ index.js
│  │     ├─ editor.scss
│  │     ├─ style.scss
│  │     └─ block.json
└─ build/   (generated)

Install dependencies

Install @wordpress/scripts and typical build tools (sass for scss compilation if you use SCSS):

npm init -y
npm install --save-dev @wordpress/scripts sass

Alternative with Yarn:

yarn init -y
yarn add -D @wordpress/scripts sass

package.json scripts

Add convenient scripts. The two most important commands are start (development watch server) and build (production build).

{
  name: my-block-plugin,
  version: 1.0.0,
  scripts: {
    start: wp-scripts start,
    build: wp-scripts build,
    lint:js: wp-scripts lint-js,
    format:js: wp-scripts format-js
  },
  devDependencies: {
    @wordpress/scripts: ^25.0.0,
    sass: ^1.64.0
  }
}

Single-block example (simple)

Create a minimal block in src/blocks/hello-world. Key parts:

block.json (metadata)

Use block.json to describe metadata for block registration. This enables registering from the server via register_block_type with the metadata file. The special file:./index.js and file:./style.css notation tells WordPress that the build results should be used (when you register metadata from the build directory).

{
  apiVersion: 2,
  name: my-plugin/hello-world,
  title: Hello World,
  category: widgets,
  icon: smiley,
  description: A simple hello world block.,
  supports: {
    html: false
  },
  textdomain: my-plugin,
  editorScript: file:./index.js,
  editorStyle: file:./editor.css,
  style: file:./style.css
}

src/blocks/hello-world/index.js (editor entry)

import { registerBlockType } from @wordpress/blocks
import { __ } from @wordpress/i18n
import { RichText } from @wordpress/block-editor
import ./editor.scss
import ./style.scss

registerBlockType( my-plugin/hello-world, {
  apiVersion: 2,
  edit: ( { attributes, setAttributes } ) => {
    const content = attributes.content  Hello world
    return (
      
setAttributes( { content } ) } />
) }, save: ( { attributes } ) => { return (
) }, } )

Styles (SCSS) imports

Import your editor and front-end styles directly from JS. @wordpress/scripts will extract and emit CSS files for the corresponding JS entry.

/ src/blocks/hello-world/style.scss /
.my-plugin-hello {
  padding: 10px
  background: #f0f0f0
}
/ src/blocks/hello-world/editor.scss /
.my-plugin-hello {
  border: 1px dashed #ccc
}

Build and register

Run a build and then register the block on the PHP side. After running npm run build, a build folder will be generated with compiled JS and CSS and small asset PHP files that contain dependencies and version info.

npm run build

PHP registration (recommended: use block.json metadata)

Two common approaches: (1) register script and style manually using the generated asset file, or (2) use register_block_type reading the block.json file from the build folder. Both examples are shown below.

// plugin.php (manual registration using assets generated by wp-scripts)
function myplugin_register_hello_block() {
    dir = plugin_dir_path( __FILE__ ) . build/hello-world
    // Each built entry will have an asset.php with dependencies and version
    asset_file = include( dir . /index.asset.php )

    wp_register_script(
        myplugin-hello-editor,
        plugins_url( build/hello-world/index.js, __FILE__ ),
        asset_file[dependencies],
        asset_file[version]
    )

    // If CSS was emitted:
    wp_register_style(
        myplugin-hello-style,
        plugins_url( build/hello-world/style.css, __FILE__ ),
        array(),
        filemtime( dir . /style.css )
    )

    register_block_type( dir . /block.json, array(
        editor_script => myplugin-hello-editor,
        style => myplugin-hello-style
    ) )
}
add_action( init, myplugin_register_hello_block )

PHP registration (automatic via metadata)

If you place the built block.json in a folder in your plugin and keep the file:./index.js notation, you can call register_block_type with the metadata file path – WordPress will lookup and enqueue the referenced files. This is simpler when your build output keeps the block.json next to the built assets.

// plugin.php (metadata-based registration)
function myplugin_register_blocks_from_build() {
    register_block_type( __DIR__ . /build/hello-world/block.json )
}
add_action( init, myplugin_register_blocks_from_build )

Working with multiple blocks in one plugin

Two main strategies:

  1. Keep each block as its own folder under src/blocks/ and run wp-scripts build with a small custom step that copies built block folders to plugin build/ location. Use block.json for each block. This aligns with block metadata and recommended registration.
  2. Use a custom Webpack entry map so that a single build produces multiple named entries (one output JS and CSS file per block). To do that you will extend the default Webpack config instructions follow in the Customize Webpack section.

How @wordpress/scripts treats WordPress packages

By default, @wordpress/scripts externalizes WordPress provided packages (packages under @wordpress/) so they are not bundled. They are expected to be available globally on the WordPress page via the wp global (e.g., wp.blocks, wp.element, wp.i18n). This keeps bundles small and avoids duplicating Gutenberg code. If you need to bundle a third-party dependency that is not present in WordPress, just import it it will be bundled unless you explicitly mark it external.

Asset files produced by build

For each entry, wp-scripts produces:

  • Compiled JavaScript file (e.g., index.js)
  • Compiled CSS files for editor and front-end (if you imported .scss/.css)
  • An asset manifest PHP file (e.g., index.asset.php) containing an array with dependencies and version — use this to register/enqueue your script with correct dependencies and cache-busting version.
  • When using block.json in the build folder, those files will be discovered when you call register_block_type on the metadata file.

Customizing Webpack when you need it

If you need to add a loader, change entry names, or implement special output behavior (multiple entry points), extend the default @wordpress/scripts Webpack configuration rather than replacing it. The default config is exported by @wordpress/scripts and can be required and modified.

Example: add image handling and multiple entries

Create a top-level webpack.config.js that imports the default factory and returns a modified config. The default is a function that accepts (env, argv) — call it and mutate the result.

// webpack.config.js
const path = require( path )
const defaultConfigFactory = require( @wordpress/scripts/config/webpack.config )

module.exports = ( env, argv ) => {
  const config = defaultConfigFactory( env, argv )

  // Example: multiple entries for two blocks
  config.entry = {
    hello-world: path.resolve( __dirname, src/blocks/hello-world/index.js ),
    another-block: path.resolve( __dirname, src/blocks/another-block/index.js )
  }

  // Example: asset/resource rule for images (emits to build/)
  config.module.rules.push( {
    test: /.(pngjpe?ggifsvg)/,
    type: asset/resource,
    generator: {
      filename: images/[name][hash][ext]
    }
  } )

  // Optionally modify output filename pattern (be careful with block.json references)
  config.output = Object.assign( {}, config.output, {
    filename: [name].js
  } )

  return config
}

Notes:

  • If you change filenames or folder structure, ensure your PHP registration points to the right files or your block.json references the filenames correctly.
  • When adding new loaders, keep the default rules and just append new ones where needed.

How to use the generated index.asset.php file

The generated PHP asset file contains the JS dependencies and a version hash. Use it in your PHP registration so that WordPress enqueues your script with correct dependencies.

// Example using generated asset file for a build entry hello-world
function myplugin_register_hello_block_with_asset() {
    base = plugin_dir_path( __FILE__ ) . build/hello-world/
    asset = include base . index.asset.php

    wp_register_script(
        myplugin-hello-editor,
        plugins_url( build/hello-world/index.js, __FILE__ ),
        asset[dependencies],
        asset[version],
        true
    )

    register_block_type( base . block.json )
}
add_action( init, myplugin_register_hello_block_with_asset )

Development: using wp-scripts start

Run npm run start in your project root. This spins up a Webpack dev server in watch mode and rebuilds on change. It uses the same configuration as build but with watch friendly options. For many block projects this is sufficient: edit files, and refresh the Gutenberg editor in the browser to see updates. You can also enable hot module replacement if you set up the dev server for your theme/plugin, but that requires additional Webpack customization.

i18n (translations) in JavaScript code

Use @wordpress/i18n functions (__, _n, etc.) in your JavaScript. During build, @wordpress/scripts will transform translation function calls if you additionally run a workflow to extract strings (wp i18n make-pot or other tooling). A common process:

  1. Write code using __() from @wordpress/i18n.
  2. Extract strings to a POT file (e.g., using WP-CLI make-pot or other extractor).
  3. Create .po/.mo files for the plugin domain used in PHP’s load_plugin_textdomain or via the plugin headers.

Code splitting and dynamic imports

Use dynamic imports to split heavy dependencies into separate chunks that load only when needed. Example:

// Lazy load a component for performance
const LazyComponent = React.lazy( () => import( ./heavy-editor-component ) )

function Edit() {
  return (
    Loading…
}> ) }

Webpack will produce additional chunk files. If you use code splitting, confirm your PHP registration and WordPress environment allow those chunks to be fetched (they will be emitted into the build folder and referenced by the main bundles runtime).

Testing and debugging

Production optimizations

Common pitfalls and troubleshooting

Advanced: full custom Webpack replacement (when necessary)

If your project requires a custom toolchain, you can replace @wordpress/scripts entirely with your own Webpack config. Only do this if you need control beyond what extension offers. When replacing, remember to:

Helpful commands summary

npm run start Development mode: watch/rebuild. Good for iterative block development.
npm run build Produce production-ready bundles and asset files under build/.
wp-scripts lint-js Lint JavaScript using WordPress ESLint rules.

References and further reading

Official resources that are helpful:

Appendix — useful example files

Complete package.json (example)

{
  name: my-block-plugin,
  version: 1.0.0,
  private: true,
  scripts: {
    start: wp-scripts start,
    build: wp-scripts build,
    lint:js: wp-scripts lint-js
  },
  devDependencies: {
    @wordpress/scripts: ^25.0.0,
    sass: ^1.64.0
  }
}

Minimal plugin bootstrap (plugin.php)


Final notes

Using @wordpress/scripts gives a fast, supported path to build Gutenberg blocks. Start with the zero-config mode and block.json metadata. When you need customization, extend the exported Webpack config rather than rewriting the whole configuration. Always confirm built assets are copied or available in the plugins build folder that WordPress expects at runtime. Thoroughly test registration, asset URLs, and externalized dependencies to avoid conflicts and 404s.



Acepto donaciones de BAT's mediante el navegador Brave :)



Leave a Reply

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