@wordpress/wp-build

Build tool for WordPress plugins.

Description

@wordpress/build is an opinionated build system designed for WordPress plugins. It provides:

  • Transpilation: Converts TypeScript/JSX source code to both CommonJS (build/) and ESM (build-module/) formats using esbuild
  • Style Compilation: Processes SCSS files and CSS modules, generating LTR and RTL versions
  • Bundling: Creates browser-ready bundles for WordPress scripts and modules
  • PHP Generation: Automatically generates PHP registration files for scripts, modules, and styles
  • Watch Mode: Incremental rebuilds during development

Installation

npm install @wordpress/build --save-dev

Usage

Production Build

wp-build

or via npm script:

{
    "scripts": {
        "build": "wp-build"
    }
}

Development Mode (Watch)

wp-build --watch

or via npm script:

{
    "scripts": {
        "dev": "wp-build --watch"
    }
}

Package Configuration

Configure your package.json with the following optional fields:

wpScript

Set to true to bundle the package as a WordPress script/module:

{
    "wpScript": true
}

wpScriptModuleExports

Define script module entry points:

{
    "wpScriptModuleExports": {
        "./interactivity": "./build-module/interactivity/index.js"
    }
}

wpScriptDefaultExport

Handle default export wrapping:

{
    "wpScriptDefaultExport": true
}

wpScriptExtraDependencies

Additional script dependencies:

{
    "wpScriptExtraDependencies": ["wp-polyfill"]
}

wpStyleEntryPoints

Custom SCSS entry point patterns:

{
    "wpStyleEntryPoints": {
        "style": "src/style.scss"
    }
}

wpCopyFiles

Files to copy with optional PHP transformations:

{
    "wpCopyFiles": [
        {
            "from": "src/index.php",
            "to": "build/index.php",
            "transform": "php"
        }
    ]
}

Root Configuration

Configure your root package.json with a wpPlugin object to control global namespace and externalization behavior:

wpPlugin.scriptGlobal

The global variable name for your packages (e.g., "wp", "myPlugin"). Set to false to disable global exposure:

{
    "wpPlugin": {
        "scriptGlobal": "myPlugin"
    }
}

wpPlugin.packageNamespace

The package scope to match for global exposure (without @ prefix). Only packages matching @{packageNamespace}/* will expose globals:

{
    "wpPlugin": {
        "scriptGlobal": "myPlugin",
        "packageNamespace": "my-plugin"
    }
}

wpPlugin.handlePrefix

The prefix used for WordPress script handles in .asset.php files (e.g., wp-data, my-plugin-editor). Defaults to packageNamespace:

{
    "wpPlugin": {
        "scriptGlobal": "myPlugin",
        "packageNamespace": "my-plugin",
        "handlePrefix": "mp"
    }
}

With this configuration:
@my-plugin/editor window.myPlugin.editor with handle mp-editor
@my-plugin/data window.myPlugin.data with handle mp-data

wpPlugin.externalNamespaces

Additional package namespaces to externalize (consume as externals, not expose). Each namespace must be an object with global and optional handlePrefix:

{
    "wpPlugin": {
        "externalNamespaces": {
            "woo": {
                "global": "woo",
                "handlePrefix": "woocommerce"
            },
            "acme": {
                "global": "acme",
                "handlePrefix": "acme-plugin"
            }
        }
    }
}

This allows your packages to consume third-party dependencies as externals:
import { Cart } from '@woo/cart' window.woo.cart with handle woocommerce-cart
import { Button } from '@acme/ui' window.acme.ui with handle acme-plugin-ui
– Dependencies are tracked in .asset.php files

If handlePrefix is omitted, it defaults to the namespace key (e.g., "woo" woo-cart).

wpPlugin.pages (Experimental)

Define admin pages that support routes. Each page gets generated PHP functions for route registration and can be extended by other plugins.

Pages can be defined as simple strings or as objects with initialization modules:

{
    "wpPlugin": {
        "pages": [
            "my-admin-page",
            {
                "id": "my-other-page",
                "init": ["@my-plugin/my-page-init"]
            }
        ]
    }
}

Page Configuration:
String format: "my-admin-page" – Simple page with no init modules
Object format: { "id": "page-slug", "init": ["@scope/package"] } – Page with init modules

Generated Files:

This generates two page modes:
build/pages/my-admin-page/page.php – Full-page mode (takes over entire admin screen with custom sidebar)
build/pages/my-admin-page/page-wp-admin.php – WP-Admin mode (integrates within standard wp-admin interface)
build/pages.php – Loader for all pages

Each mode provides route/menu registration functions and a render callback. Routes are automatically registered for both modes.

Registering a menu item for WP-Admin mode:

// Build URL with initial route via 'p' query parameter
$url = admin_url( 'admin.php?page=my-admin-page-wp-admin&p=' . urlencode( '/my/route' ) );
add_menu_page( 'Title', 'Menu', 'capability', $url, '', 'icon', 20 );

Registering a menu item for full-page mode:

add_menu_page( 'Title', 'Menu', 'capability', 'my-admin-page', 'my_admin_page_render_page', 'icon', 20 );

Init Modules:
Init modules are JavaScript packages that execute during page initialization, before routes are registered and the app renders. They’re ideal for:
– Adding icons to menu items (icons can’t be passed from PHP)
– Registering command palette entries

Creating an Init Module:

In packages/my-page-init/package.json:

{
    "name": "@my-plugin/my-page-init",
    "wpScriptModuleExports": "./build-module/index.js",
    "dependencies": {
        "@wordpress/boot": "file:../boot",
        "@wordpress/data": "file:../data",
        "@wordpress/icons": "file:../icons"
    }
}

In packages/my-page-init/src/index.ts:

import { home, styles } from '@wordpress/icons';
import { dispatch } from '@wordpress/data';
import { store as bootStore } from '@wordpress/boot';

/**
 * Initialize page - this function is mandatory.
 * All init modules must export an 'init' function.
 */
export async function init() {
    // Add icons to menu items
    dispatch( bootStore ).updateMenuItem( 'home', { icon: home } );
    dispatch( bootStore ).updateMenuItem( 'styles', { icon: styles } );
}

The init() function is mandatory – all init modules must export this named function. Init modules are loaded as static dependencies and executed sequentially before the boot system registers menu items and routes.

Example: WordPress Core (Gutenberg)

{
    "wpPlugin": {
        "scriptGlobal": "wp",
        "packageNamespace": "wordpress"
    }
}

This configuration:
– Packages like @wordpress/data expose window.wp.data
– Packages like @wordpress/block-editor expose window.wp.blockEditor
– All packages can consume @wordpress/* as externals

Example: Third-Party Plugin

{
    "wpPlugin": {
        "scriptGlobal": "acme",
        "packageNamespace": "acme"
    }
}

This configuration:
– Packages like @acme/editor expose window.acme.editor
– Packages like @acme/data expose window.acme.data
– All packages can still consume @wordpress/* window.wp.*
– All packages can still consume vendors (react, lodash) window.React, window.lodash

Behavior

  • Packages with wpScript: true matching the namespace: Bundled with global exposure
  • Packages with wpScript: true not matching the namespace: Bundled without global exposure
  • Dependencies: @wordpress/* packages are always externalized to wp.* globals
  • Vendors: React, lodash, jQuery, moment are always externalized to their standard globals
  • Asset files: .asset.php files are always generated for WordPress dependency management

Output Structure

The built tool generates several files in the build/ directory, but the primary output is the PHP registration file.

Make sure to include the generated PHP file in your plugin file.

require_once plugin_dir_path( __FILE__ ) . 'build/index.php';

Routes (Experimental)

Routes provide a file-based routing system for WordPress admin pages. Each route must be associated with a page defined in wpPlugin.pages (see above). Create a routes/ directory at your repository root with subdirectories for each route.

Structure

routes/
  home/
    package.json    # Route configuration
    stage.tsx       # Main content component
    inspector.tsx   # Optional sidebar component
    canvas.tsx      # Optional custom canvas component
    route.tsx       # Optional lifecycle hooks (beforeLoad, loader, canvas)

Route Configuration

In routes/{route-name}/package.json:

{
    "route": {
        "path": "/",
        "page": "my-admin-page"
    }
}

The page field must match one of the pages defined in wpPlugin.pages in your root package.json. This tells the build system which page this route belongs to. It can also map to an existing page registered by another plugin.

Components

stage.tsx – Main content (required):

export const stage = () => <div>Content</div>;

inspector.tsx – Sidebar content (optional):

export const inspector = () => <div>Inspector</div>;

canvas.tsx – Custom canvas component (optional):

export const canvas = () => <div>Custom Canvas</div>;

The canvas is a full-screen area typically used for editor previews. You can provide a custom canvas component that will be conditionally rendered based on the canvas() function’s return value in route.tsx.

route.tsx – Lifecycle hooks (optional):

export const route = {
    beforeLoad: ({ params, search }) => {
        // Pre-navigation validation, auth checks
    },
    loader: ({ params, search }) => {
        // Data preloading
    },
    canvas: ({ params, search }) => {
        // Return CanvasData to use default canvas (editor)
        return {
            postType: 'post',
            postId: '123',
            isPreview: true
        };

        // Return null to use custom canvas.tsx component
        // return null;

        // Return undefined to show no canvas
        // return undefined;
    }
};

The canvas() function controls which canvas is rendered:
– Returns CanvasData object ({ postType, postId, isPreview? }) Renders the default WordPress editor canvas
– Returns null Renders the custom canvas component from canvas.tsx (if provided)
– Returns undefined or is omitted No canvas is rendered

Build Output

The build system generates:
build/routes/{route-name}/content.js – Bundled stage/inspector/canvas components
build/routes/{route-name}/route.js – Bundled lifecycle hooks (if present)
build/routes/index.php – Route registry data
build/routes.php – Route registration logic

The boot package in Gutenberg will automatically use these routes and make them available.

Contributing to this package

This is an individual package that’s part of the Gutenberg project. The project is organized as a monorepo. It’s made up of multiple self-contained software packages, each with a specific purpose.

The packages in this monorepo are published to npm and used by WordPress as well as other software projects.

To find out more about contributing to this package or Gutenberg as a whole, please read the project’s main contributor guide.

License

GPL-2.0-or-later © The WordPress Contributors