Skip to content

Adopt WordPress Core's New Package-Based Externalization Approach #474

@fabiankaegy

Description

@fabiankaegy

Note

This issue proposes adopting patterns from WordPress core's evolving build tooling. Sections marked with 🔮 are proposals for 10up-toolkit, not existing features.

Summary

WordPress core is evolving its build tooling from @wordpress/scripts (Webpack-based) to the new @wordpress/build package (esbuild-based). As part of this transition, they are deprecating the DependencyExtractionWebpackPlugin in favor of a more flexible, package.json metadata-driven externalization system.

This new approach offers significant advantages that 10up-toolkit should consider adopting, particularly the ability for individual packages to define their own externalization behavior through declarative configuration.

Background

The Current Approach (DependencyExtractionWebpackPlugin)

Currently, @wordpress/scripts and 10up-toolkit rely on @wordpress/dependency-extraction-webpack-plugin to:

  • Automatically externalize @wordpress/* packages to wp.* globals
  • Generate .asset.php files with dependency arrays
  • Handle vendor externalization (React, lodash, jQuery, etc.)

While functional, this approach has limitations:

  • Tightly coupled to Webpack
  • Hardcoded externalization rules are inflexible
  • Third-party packages cannot easily declare their own externalization behavior
  • No straightforward way for plugins to consume other plugins' packages as externals

The New Approach (@wordpress/build)

WordPress core's new @wordpress/build package introduces a declarative, metadata-driven externalization system using package.json fields.

Package-Level Metadata

Individual packages can declare their build behavior via package.json:

{
  "name": "@wordpress/data",
  "wpScript": true,
  "wpScriptModuleExports": "./build-module/index.js"
}

Key fields:

Field Purpose
wpScript When true, package is bundled and exposed via configured global (e.g., window.wp.data)
wpScriptModuleExports Defines entry points for ES module script exports (string or object)

Root-Level Plugin Configuration

Projects configure externalization behavior in their root package.json via a wpPlugin object:

{
  "wpPlugin": {
    "scriptGlobal": "wp",
    "packageNamespace": "wordpress",
    "handlePrefix": "wp",
    "externalNamespaces": {
      "woo": {
        "global": "woo",
        "handlePrefix": "woocommerce"
      }
    }
  }
}

Configuration options:

Option Purpose
scriptGlobal Global variable namespace (e.g., "wp", "myPlugin"). Set to false to disable
packageNamespace Package scope to match for global exposure (without @ prefix)
handlePrefix Prefix for WordPress script handles in .asset.php files
externalNamespaces Third-party namespaces to consume as externals

The Key Innovation: externalNamespaces

The externalNamespaces configuration is particularly powerful. It allows projects to declare that imports from specific namespaces should be externalized without bundling:

{
  "wpPlugin": {
    "externalNamespaces": {
      "woo": {
        "global": "woo",
        "handlePrefix": "woocommerce"
      }
    }
  }
}

With this configuration:

  • import { Cart } from '@woo/cart' → resolves to window.woo.cart
  • The dependency woocommerce-cart is tracked in .asset.php
  • No bundling of WooCommerce packages occurs

This enables a powerful ecosystem where:

  1. WooCommerce can expose @woo/* packages via window.woo.*
  2. Any plugin can expose its own packages via a configured global
  3. Consumers can add those namespaces to externalNamespaces and use them as externals without bundling

🔮 Proposal for 10up-toolkit

1. Modernize WordPress Externals Handling

The current approach relies on DependencyExtractionWebpackPlugin which is tightly coupled to Webpack and will be deprecated. We should update how 10up-toolkit handles WordPress externals to align with core's new direction:

  • Read the wpScript metadata from @wordpress/* packages to determine if they're bundled in core
  • Only externalize packages that are actually available as WordPress scripts
  • Generate accurate .asset.php dependency arrays

2. Implement externalNamespaces Configuration

Adopt the externalNamespaces pattern to give projects flexibility in defining their own externalization rules:

// 10up-toolkit.config.js
module.exports = {
  externalNamespaces: {
    // WordPress packages (built-in default)
    wordpress: {
      global: 'wp',
      handlePrefix: 'wp'
    },
    // Add WooCommerce support
    woo: {
      global: 'woo',
      handlePrefix: 'woocommerce'
    },
    // Add support for any other ecosystem
    acme: {
      global: 'acme',
      handlePrefix: 'acme-plugin'
    }
  }
};

This allows projects to:

  • import { Cart } from '@woo/cart' → resolves to window.woo.cart with woocommerce-cart as a dependency
  • import { Button } from '@acme/ui' → resolves to window.acme.ui with acme-plugin-ui as a dependency
  • Easily add/remove external namespaces without modifying toolkit internals

3. Pluggable Externalization Handlers (Optional Enhancement)

For even more flexibility, allow registering custom handler functions:

// 10up-toolkit.config.js
module.exports = {
  externalHandlers: [
    // Custom handler with full control
    {
      namespace: 'mycompany',
      global: 'mycompany',
      handlePrefix: 'mycompany',

      // Optional: custom logic for edge cases
      shouldExternalize: (packageName) => {
        // Don't externalize internal-only packages
        if (packageName.includes('/internal/')) return false;
        return packageName.startsWith('@mycompany/');
      }
    }
  ]
};

🔮 Benefits of Adoption

  1. Future-proofing: Aligns with WordPress core's direction as they deprecate the Webpack plugin
  2. Flexibility: Projects can easily add/remove externalization targets via configuration
  3. Ecosystem interop: Seamlessly consume packages from WooCommerce, other plugins, or internal libraries
  4. Build tool agnostic: The configuration approach works regardless of underlying bundler (Webpack, esbuild, etc.)
  5. Accurate dependencies: Reading package metadata ensures we only externalize what's actually available in WordPress

Tradeoffs

Package Installation Requirements

A key difference with this approach is that every @wordpress/* package you import must be installed in your project in order to read its metadata and determine externalization behavior.

Downsides:

  • Slower installs: More packages to download and resolve, especially on fresh installs or CI
  • NPM dependency headaches: More packages means more potential for version conflicts, peer dependency warnings, and resolution issues
  • Larger node_modules: Even though the packages aren't bundled, they still need to be present locally
  • Version management: Need to keep WordPress package versions in sync with the WordPress version you're targeting

Upsides:

  • TypeScript support: Having packages installed means full type definitions are available out of the box
  • Better IDE integration: Autocomplete, go-to-definition, and inline documentation work correctly
  • Explicit dependencies: Your package.json accurately reflects what your code depends on
  • Version awareness: You can intentionally pin to specific versions rather than relying on whatever WordPress ships

This is a philosophical shift from "trust that WordPress has these globals" to "explicitly declare and type-check your dependencies." The tradeoff is worth considering based on project needs.

Questions to Consider

  1. Should externalNamespaces be configured via 10up-toolkit.config.js, package.json, or both?
  2. What should the default configuration include? (WordPress only, or also common ecosystems like WooCommerce?)
  3. How do we handle the transition period while DependencyExtractionWebpackPlugin is still supported but deprecated?
  4. Should handlers be able to read package metadata (like wpScript) to make smarter externalization decisions?

References

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions