Skip to content

Bridge @wordpress/build to upstream identity + discovery (#78822, #77465)#48089

Open
retrofox wants to merge 11 commits into
trunkfrom
update/wp-build-package-sources
Open

Bridge @wordpress/build to upstream identity + discovery (#78822, #77465)#48089
retrofox wants to merge 11 commits into
trunkfrom
update/wp-build-package-sources

Conversation

@retrofox
Copy link
Copy Markdown
Contributor

@retrofox retrofox commented Apr 14, 2026

What?

Replaces the pnpm patch on @wordpress/build with one that mirrors two upstream Gutenberg PRs applied to @wordpress/build@0.13.0:

  • #78822 — reads each script module's ID from package.json#name, externalizes internal-package imports by exact name, and discovers script-module packages outside ./packages/ via convention (any dependencies entry whose package.json declares wpScriptModuleExports).
  • #77465 — scopes the generated wp_deregister_script_module() calls to @wordpress/* IDs, so shared non-core packages get Core's idempotent first-wins instead of last-plugin-wins.

Plus one consumer-side change: @automattic/number-formatters exposes ./package.json in its exports so the convention discovery can read its manifest.

Supersedes the earlier wpPlugin.packageSources config approach (closed upstream as #77226 after slippery-slope feedback). No new wp-build config; the new behavior is convention-driven.

Closes WOOA7S-1341 WOOA7S-1342

Why?

Stock @wordpress/build only externalizes @<packageNamespace>/* and only scans ./packages/. A shared package like @automattic/number-formatters matches no plugin's namespace and lives in js-packages/, so it gets inlined into every consumer's bundle: duplicated code, no script-module deduplication. And because the script-module ID is derived from wpPlugin.packageNamespace, a package's npm name and its runtime specifier are forced to diverge, which needs a tsconfig paths alias and trips dependency-confusion scanners.

The two upstream PRs fix both: identity comes from the package's own name, and shared packages are discovered and registered once. This PR is the bridge that validates that bundle end-to-end inside Jetpack until Core ships it.

How?

The patch is a verified faithful mirror of core

The patch is exactly #78822 + #77465 applied to @wordpress/build@0.13.0. The only addition is a one-line createNodeRequire import that 0.13.0 predates (the PR branch sits on newer trunk where it already exists). Verified by a two-tree diff: pristine 0.13.0 + the two PR .diffs is byte-identical to pristine + this patch. The patch header documents the rebase recipe so it can be regenerated against a future @wordpress/build without a Gutenberg checkout.

premium-analytics migrated to name-based identity

With identity coming from name, the local init package is renamed to @automattic/jetpack-premium-analytics-init, and wpPlugin.pages[].init is updated to match. Result: npm name === import specifier === script-module ID, with no tsconfig alias and no unregistered-scope scanner exposure.

number-formatters as the shared module

@automattic/number-formatters declares wpScriptModuleExports and exposes ./package.json. The premium-analytics dashboard route imports it both statically (formatNumber) and dynamically (formatNumberCompact). Both resolve to a single shared @automattic/number-formatters script module, externalized out of the route bundle.

This is a monorepo-wide win, not premium-analytics only

The patch is applied to every @wordpress/build consumer through pnpm-workspace.yaml, so the deduplication happens automatically wherever a wp-build plugin already imports a shared wpScriptModuleExports package. No per-consumer change is needed.

@automattic/jetpack-forms (a shipping package, not experimental) is the clearest example: number-formatters is imported across 13 source files in its routes and dashboard. Its wp-build run logs ✔ Bundled @automattic/number-formatters and emits a single shared build/modules/@automattic/number-formatters/index.min.js (14.6 KB), byte-identical to the one premium-analytics produces. Without the patch, that formatter code is inlined into each wp-build route bundle that imports it; with the patch they share one module, registered once in WordPress.

This is the real point of the change: it improves all of Jetpack's wp-build integration, not a single plugin.

Testing

Build premium-analytics and confirm the shared module is deduplicated:

nvm use   # Node 24, the monorepo engine
pnpm install
pnpm --filter @automattic/jetpack-premium-analytics run build
  • build/modules/registry.php lists @automattic/number-formatters and @automattic/jetpack-premium-analytics-init.
  • build/routes/dashboard/content.min.js is ~1.45 KB and does not inline number-formatters. Contrast: ~16 KB with it inlined. A separate build/modules/@automattic/number-formatters/index.min.js holds the single shared copy.
  • build/modules.php deregisters only @wordpress/* IDs (if ( str_starts_with( $module['id'], '@wordpress/' ) )).

Because the patch is global, the same deduplication applies to every wp-build consumer. @automattic/jetpack-forms, for instance, emits the identical shared build/modules/@automattic/number-formatters/index.min.js from its own build, with no forms-specific change.

To verify the faithful-mirror claim, follow the recipe in the patch header: it rebuilds the patched tree from the two upstream .diffs and diffs it against this patch (identical).

(Optional) Live in WordPress: activate the Premium Analytics plugin alongside the Gutenberg plugin and open the dashboard. The page loads the renamed init module (which sets the menu icon) and number-formatters as a shared module, in both static and dynamic form.

Trade-offs

Shared script modules across plugins use the same trust model externalNamespaces already applies to @wordpress/*: one registration in WordPress's global registry. With #77465 a shared non-core ID falls through to Core's idempotent first-wins (deterministic) instead of last-plugin-wins. Within the monorepo the shared lockfile pins one version; cross-release version skew is the same exposure wp_register_script() has always had.

Follow-ups

@retrofox retrofox requested a review from a team as a code owner April 14, 2026 14:15
@retrofox retrofox self-assigned this Apr 14, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 14, 2026

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack or WordPress.com Site Helper), and enable the update/wp-build-package-sources branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack update/wp-build-package-sources
bin/jetpack-downloader test jetpack-mu-wpcom-plugin update/wp-build-package-sources

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 14, 2026

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • 🔴 Add testing instructions.
  • 🔴 Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


🔴 Action required: Please include detailed testing steps, explaining how to test your change, like so:

## Testing instructions:

* Go to '..'
*

🔴 Action required: We would recommend that you add a section to the PR description to specify whether this PR includes any changes to data or privacy, like so:

## Does this pull request change what data or activity we track or use?

My PR adds *x* and *y*.

Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!


Premium Analytics plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.

@github-actions github-actions Bot added the [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. label Apr 14, 2026
@jp-launch-control
Copy link
Copy Markdown

jp-launch-control Bot commented Apr 14, 2026

Code Coverage Summary

This PR did not change code coverage!

That could be good or bad, depending on the situation. Everything covered before, and still is? Great! Nothing was covered before? Not so great. 🤷

Full summary · PHP report · JS report

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move to the existing .pnpm-patches/ directory.

Also, I'm not terribly fond of the "apply a large patch I hope to get merged upstream soon" approach here; such hopes seem frequently dashed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's fair. We need to try to move these changes upstream asap.

Comment thread patches/README.md Outdated
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unneeded. Any comments about a patch can be included at the top of the patch file itself, as shown by the existing patches.

Comment thread projects/packages/analytics/.phan/baseline.php
@retrofox retrofox marked this pull request as draft April 14, 2026 15:22
@retrofox
Copy link
Copy Markdown
Contributor Author

@anomiex PR is not ready for review. Sorry about it. We need to land the package and plugin PRs first

@anomiex
Copy link
Copy Markdown
Contributor

anomiex commented Apr 14, 2026

Will this idea cause problems if multiple different wp-build-using plugins each include incompatible versions of @automattic/number-formatters?

It looks like, unless all plugins are careful to only load their build/build.php when needed and no two ever do it on the same request, the last plugin will "win" even if it has an older version of @automattic/number-formatters.

@retrofox retrofox force-pushed the update/premium-analytics-plugin branch from 6f55a70 to ae38a48 Compare April 14, 2026 17:58
@retrofox retrofox force-pushed the update/wp-build-package-sources branch from a53fd17 to 7ccaa1a Compare April 14, 2026 18:14
@retrofox
Copy link
Copy Markdown
Contributor Author

Will this idea cause problems if multiple different wp-build-using plugins each include incompatible versions of @automattic/number-formatters?

It looks like, unless all plugins are careful to only load their build/build.php when needed and no two ever do it on the same request, the last plugin will "win" even if it has an older version of @automattic/number-formatters.

Great point. As far as I understand, this is inherent to WordPress's global script module registry, not specific to packageSources.

The same trade-off already exists with externalNamespaces: when a plugin declares externalNamespaces: { wordpress: { ... } }, it trusts that the @wordpress/* modules in the environment are compatible. packageSources extends that same trust model to specific named packages.

Looking at the generated modules.php, wp-build calls wp_deregister_script_module() before wp_register_script_module(), so the last plugin to load wins.

Within the Jetpack monorepo, this is mitigated by the shared lockfile: all plugins built from the same commit get the same version of @automattic/number-formatters. The real risk is cross-release version skew, which is the same problem wp_register_script() has always had with shared dependencies.

Without packageSources, each consumer bundles its own copy inline. No collision, but no deduplication either. packageSources trades isolation for deduplication, the same trade-off externalNamespaces already makes.

This is a crucial point to keep in mind as more plugins adopt this pattern.

Does it make sense?

@anomiex
Copy link
Copy Markdown
Contributor

anomiex commented Apr 14, 2026

Something to keep in mind is that it doesn't seem like the people working on wp-build are considering these sorts of edge cases; they seem pretty focused on Gutenberg and Core. You should probably copy that information to your WordPress/gutenberg#77226 PR.

@simison
Copy link
Copy Markdown
Member

simison commented Apr 15, 2026

Note to also keep testing Forms builds and works well with this, as it's the only other thing using WP Build: jetpack build packages/forms

@retrofox retrofox force-pushed the update/premium-analytics-plugin branch from 2eafa61 to ecf8b90 Compare April 15, 2026 13:49
@retrofox retrofox force-pushed the update/wp-build-package-sources branch from 7ccaa1a to e9b7a96 Compare April 15, 2026 16:54
@retrofox retrofox marked this pull request as ready for review April 15, 2026 16:57
@retrofox
Copy link
Copy Markdown
Contributor Author

Addressed all three:

@retrofox
Copy link
Copy Markdown
Contributor Author

Something to keep in mind is that it doesn't seem like the people working on wp-build are considering these sorts of edge cases; they seem pretty focused on Gutenberg and Core. You should probably copy that information to your WordPress/gutenberg#77226 PR.

Good idea. I'll add the script module collision analysis to the Gutenberg PR #77226 so upstream maintainers are aware of the deduplication trade-off.

@retrofox
Copy link
Copy Markdown
Contributor Author

Note to also keep testing Forms builds and works well with this, as it's the only other thing using WP Build: jetpack build packages/forms

Confirmed. pnpm --filter @automattic/jetpack-forms run build passes with the patch applied. All build steps complete successfully (blocks, contact-form, dashboard, wp-build, form-editor, modules).

@retrofox retrofox force-pushed the update/wp-build-package-sources branch from dd7ccc7 to 8610eb4 Compare April 16, 2026 09:07
patches @wordpress/build@0.11.0 to add packageSources config for
cross-directory package discovery. mirrors upstream Gutenberg PR
WordPress/gutenberg#77226 1:1.

flags @automattic/number-formatters as a script-module exporter
via wpScriptModuleExports so consumers can externalize it.
configures @automattic/number-formatters as a named packageSource
and demonstrates both import modes in the dashboard route:
static import for formatNumber, dynamic import for formatNumberCompact
via React.lazy and Suspense.
@retrofox retrofox force-pushed the update/wp-build-package-sources branch from 25c7409 to 1b3fb87 Compare April 17, 2026 16:00
@retrofox retrofox requested a review from anomiex April 17, 2026 16:10
regenerate patch against 0.12.0 sources using upstream PR #77226 diff.
header embeds the version-agnostic rebase procedure.
# Conflicts:
#	pnpm-lock.yaml
#	projects/packages/premium-analytics/package.json
Replaces the @wordpress/build@0.12.0 packageSources patch with a 0.13.0
patch that mirrors the new upstream proposal: identity from
package.json#name, discovery via convention (dependencies that declare
wpScriptModuleExports), plus a node_modules walk-up fallback in
getPackageInfo for packages whose exports field hides ./package.json.

No more wpPlugin.packageSources config needed on the consumer side.
mirror upstream PR #77465 so shared non-core modules use Core first-wins
rename init to @automattic/jetpack-premium-analytics-init; align pages[].init so name, specifier, and module ID match
regenerate from #78822 + #77465 diffs: drop the node_modules walk-up (number-formatters now exposes ./package.json) and adopt #77465's exact template comments
lets wp-build convention discovery read the manifest without a resolver workaround
@retrofox retrofox changed the title Add wpPlugin.packageSources via pnpm patch on @wordpress/build Bridge @wordpress/build to upstream identity + discovery (#78822, #77465) Jun 2, 2026
trunk bumped @wordpress/build to 0.14.0 monorepo-wide; patch regenerated from #78822 + #77465 against 0.14.0
@retrofox
Copy link
Copy Markdown
Contributor Author

retrofox commented Jun 2, 2026

Re-validated end-to-end on the current branch (@wordpress/build@0.14.0, Node 24):

  • build/modules/registry.php registers @automattic/jetpack-premium-analytics-init and @automattic/number-formatters — the npm names verbatim, no derived @jetpack-premium-analytics/* shape.
  • build/routes/dashboard/content.min.js is 1450 B and externalizes number-formatters in both static (import { formatNumber }) and dynamic (import( '@automattic/number-formatters' )) form. The implementation lives once in build/modules/@automattic/number-formatters/index.min.js (~14.9 KB); without the patch that code is inlined into the route (~16 KB).
  • build/modules.php deregisters only @wordpress/* IDs.

On the "last plugin wins on version skew" concern. That's now handled by the companion #77465.

Stock wp-build deregisters every module ID before registering it, so the last plugin to run clobbers a shared non-core module. #77465 scopes that deregister to @wordpress/* (the only namespace Core registers by default), so a shared ID like @automattic/number-formatters is never deregistered and falls through to Core's own semantics in WP_Script_Modules::register():

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

i.e. first registration wins, deterministically. The same model wp_register_script() has always used for shared handles.

Within the monorepo, the shared lockfile pins one version, so the copies are identical anyway; cross-release skew falls back to first-wins instead of an arbitrary last-wins clobber.

Note the approach also pivoted away from the wpPlugin.packageSources config (closed upstream as #77226): #78822 replaces it with convention-driven discovery + name-based identity, no new wp-build config.

@anomiex
Copy link
Copy Markdown
Contributor

anomiex commented Jun 2, 2026

On the "last plugin wins on version skew" concern. That's now handled by the companion #77465.

I note that just changes it to "first plugin wins"; the concern itself about multiple plugins with incompatible versions of the module still exists.

Within the monorepo, the shared lockfile pins one version, so the copies are identical anyway; cross-release skew falls back to first-wins instead of an arbitrary last-wins clobber.

That assumes that everyone has matching versions of multiple plugins, and no external plugin starts using the package. Neither seem like things to rely on.

@retrofox
Copy link
Copy Markdown
Contributor Author

retrofox commented Jun 3, 2026

On the "last plugin wins on version skew" concern. That's now handled by the companion #77465.

I note that just changes it to "first plugin wins"; the concern itself about multiple plugins with incompatible versions of the module still exists.

Within the monorepo, the shared lockfile pins one version, so the copies are identical anyway; cross-release skew falls back to first-wins instead of an arbitrary last-wins clobber.

That assumes that everyone has matching versions of multiple plugins, and no external plugin starts using the package. Neither seem like things to rely on.

You're right, and I won't pretend otherwise: #77465 turns last-plugin-wins into first-plugin-wins, but it doesn't make version skew go away.

Being upfront: this is the same concern @youknowriad raised on the upstream PR (#77226), "separate plugins shouldn't register the same module names; it creates conflicts if they require different versions."

This patch is three independent pieces:

  1. Name-based identity (#78822 part 1): the script-module ID comes from package.json#name. This is what removes the tsconfig paths alias and the dependency-confusion exposure. By itself, it doesn't make two plugins register the same ID.
  2. Deregister scoped to @wordpress/* (#77465): purely defensive. It stops a blanket deregister from clobbering non-core modules.
  3. Convention discovery (#78822 part 2): this is the only piece that makes two different plugins register the same ID (e.g. @automattic/number-formatters). The skew you're describing lives here and nowhere else.

So there are two honest framings:

  • Today, inside the monorepo: the shared lockfile pins a single version, so every plugin built from the same commit gets identical copies. The dedup is safe here and now.
  • The general case (mixed plugin versions on one site, or an external plugin adopting the package): first-wins isn't enough.
    The real fix is for the script-module ID to encode compatibility: incompatible versions get different IDs and never collide, while compatible ones still deduplicate. That's a wp-build design decision, so it belongs upstream on #78822.
    I'll open that discussion there.

My proposal: treat this patch as a bridge scoped to the monorepo, where it's safe today, and drive the compatibility-aware ID upstream before this becomes something other plugins (or external ones) rely on.

If you'd rather not carry the convention-discovery piece until that lands, I can split it out and keep only name-based identity plus the deregister scope: that still gives us the dependency-confusion fix with none of the shared-ID risk.

WTDT?

@simison
Copy link
Copy Markdown
Member

simison commented Jun 3, 2026

FYI it'll be important to test with Gutenberg, with WP 7.0 and with WP 6.9, as we've had version compatibility issues in the past with WP Build (hence polyfills).

@anomiex
Copy link
Copy Markdown
Contributor

anomiex commented Jun 3, 2026

As I said earlier, I'm wary of an "apply a large patch I hope to get merged upstream soon" approach, as such hopes seem frequently dashed. Whichever piece it is.

Copy link
Copy Markdown
Contributor Author

retrofox commented Jun 3, 2026

I agree. I'm still working on it and would like to share it today or tomorrow. Clearly, it's not my area, if I have one. Thanks, and sorry for the noise.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants