Skip to content

Build Tools: Scope wp-build's deregister-before-register to @wordpress/* modules#77465

Open
retrofox wants to merge 2 commits into
trunkfrom
fix/wp-build-scope-deregister
Open

Build Tools: Scope wp-build's deregister-before-register to @wordpress/* modules#77465
retrofox wants to merge 2 commits into
trunkfrom
fix/wp-build-scope-deregister

Conversation

@retrofox
Copy link
Copy Markdown
Contributor

@retrofox retrofox commented Apr 17, 2026

What?

Scopes wp-build's deregister-before-register pattern to IDs in the @wordpress/* namespace, leaving every other module ID to rely on Core's idempotent wp_register_script_module() first-wins semantics.

Affects the two generated PHP templates:

  • packages/wp-build/templates/module-registration.php.template
  • packages/wp-build/templates/routes-registration.php.template

Why?

The deregister-before-register pattern was introduced in #75909 for a specific scenario: Gutenberg-as-plugin needs to override Core's bundled @wordpress/* module versions, and WP_Script_Modules::register() is idempotent with no override API. Deregister-then-register is the only Core-sanctioned path.

The fix worked, but was applied universally to every generated registration — including plugin-local modules and shared packages that were never overriding anything Core registers. For an ID Core doesn't own there's no upside: nothing to override, and each deregister also dequeues the module (see #76170, which added a static guard to work around that).

This becomes load-bearing once more than one wp-build plugin can register the same shared, non-@wordpress/* module ID. The universal deregister then turns into a silent last-plugin-wins race decided by plugin load order; scoping it keeps those IDs on Core's deterministic first-wins instead.

How?

Wrap each wp_deregister_script_module() call in a str_starts_with( $id, '@wordpress/' ) check. @wordpress/* is the only namespace Core registers by default (see wp-includes/assets/script-modules-packages.php), so the Gutenberg-as-plugin override scenario is preserved. All other IDs fall through to Core's idempotent first-wins semantics.

str_starts_with is safe here — WordPress Core polyfills it since 5.9 and Gutenberg already uses it unconditionally in lib/client-assets.php.

Testing

  • No automated tests exist for wp-build templates.
  • Manual: build a consumer plugin, verify generated build/modules.php and route registration contain the conditional.
  • Functional: load two plugins registering the same non-@wordpress/* ID → first-plugin-wins, no cross-plugin dequeue. Load Gutenberg-as-plugin → @wordpress/* overrides still take precedence over Core.

Related

The deregister-before-register pattern in wp-build templates was
introduced in #75909 so that Gutenberg-as-plugin can override Core's
bundled @wordpress/* module versions. Core's wp_register_script_module()
is idempotent (first-wins) with no override API, so deregister-then-
register is the only path.

Applied universally, the pattern creates silent last-plugin-wins
collisions for any non-@wordpress/* module shared across plugins
(plugin-local IDs, vendor packages) — with no upside, since these
entries are never overriding Core defaults.

Scope the pattern to @wordpress/* IDs — the only namespace Core
registers by default. Plugin-local and shared-package IDs now rely
on Core's idempotent first-wins semantics, matching the behavior
the Script Modules API was designed for.
@retrofox retrofox self-assigned this Apr 17, 2026
@github-actions
Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: retrofox <retrofox@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@retrofox
Copy link
Copy Markdown
Contributor Author

retrofox commented Jun 2, 2026

Why this matters beyond Core's own modules:

The blanket wp_deregister_script_module( $id ) before every wp_register_script_module( $id ) was introduced in #75909, so Gutenberg-as-plugin could override Core's bundled @wordpress/* module versions.

Core's register() is first-wins and exposes no override API, so deregister-then-register is the only sanctioned path there. That was correct for the @wordpress/* case, but it is/was applied universally.

For any shared non-core ID, the same deregister just means the last plugin to register it clobbers whatever was there (last-registration-wins), with nothing from Core being overridden.

WP_Script_Modules::register() is already idempotent first-wins:

if ( ! isset( $this->registered[ $id ] ) ) {
	// ...register...
}

Scoping the generated deregister to @wordpress/* (the only namespace Core registers by default) lets every non-core shared module fall through to that first-wins behavior; deterministic, and consistent with how wp_register_script() / wp_register_style() have always treated shared handles, while preserving the override for Core's own modules.

This pairs with #78822: once packages outside ./packages/ can be discovered and registered under their own name, more than one plugin can register the same shared ID, and a blanket deregister would clobber them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] wp-build /packages/wp-build [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant