diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 69fb21298bc3..2e3ff0d7f2ac 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2558,6 +2558,9 @@ importers:
'@automattic/jetpack-webpack-config':
specifier: workspace:*
version: link:../../js-packages/webpack-config
+ '@automattic/jetpack-wp-build-polyfills':
+ specifier: workspace:*
+ version: link:../wp-build-polyfills
'@automattic/remove-asset-webpack-plugin':
specifier: workspace:*
version: link:../../js-packages/remove-asset-webpack-plugin
@@ -2598,8 +2601,8 @@ importers:
specifier: 6.41.0
version: 6.41.0
'@wordpress/build':
- specifier: 0.8.0
- version: 0.8.0(@babel/core@7.29.0)(@wordpress/boot@0.8.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1))(@wordpress/route@0.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.8.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1))(browserslist@4.28.1)
+ specifier: 0.10.1-next.v.202603102151.0
+ version: 0.10.1-next.v.202603102151.0(@babel/core@7.29.0)(@wordpress/boot@0.8.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1))(@wordpress/route@0.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.8.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1))(browserslist@4.28.1)
'@wordpress/date':
specifier: 5.41.0
version: 5.41.0
@@ -4166,6 +4169,52 @@ importers:
specifier: 6.0.1
version: 6.0.1(webpack@5.105.2)
+ projects/packages/wp-build-polyfills:
+ devDependencies:
+ '@automattic/jetpack-webpack-config':
+ specifier: workspace:*
+ version: link:../../js-packages/webpack-config
+ '@wordpress/a11y':
+ specifier: ^4.40.0
+ version: 4.41.0
+ '@wordpress/admin-ui':
+ specifier: ^1.9.0
+ version: 1.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@wordpress/boot':
+ specifier: ^0.7.1
+ version: 0.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1)
+ '@wordpress/icons':
+ specifier: ^11.7.0
+ version: 11.8.0(react@18.3.1)
+ '@wordpress/lazy-editor':
+ specifier: ^1.6.1
+ version: 1.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1)
+ '@wordpress/notices':
+ specifier: ^5.40.0
+ version: 5.41.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@wordpress/private-apis':
+ specifier: ^1.40.0
+ version: 1.41.0
+ '@wordpress/route':
+ specifier: ^0.6.0
+ version: 0.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@wordpress/theme':
+ specifier: ^0.7.0
+ version: 0.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1)
+ webpack:
+ specifier: ^5.104.1
+ version: 5.105.2(webpack-cli@6.0.1)
+ webpack-cli:
+ specifier: ^6.0.1
+ version: 6.0.1(webpack@5.105.2)
+ optionalDependencies:
+ react:
+ specifier: 18.3.1
+ version: 18.3.1
+ react-dom:
+ specifier: 18.3.1
+ version: 18.3.1(react@18.3.1)
+
projects/packages/wp-js-data-sync: {}
projects/packages/yoast-promo:
@@ -10483,6 +10532,13 @@ packages:
peerDependencies:
react: ^18.0.0
+ '@wordpress/boot@0.7.1':
+ resolution: {integrity: sha512-rfoPJiDYCt4odZBmc3CL2C8wL+uH6L9Jfg+udTBk6sLx81MSgRJlhAmcH6T8LO1vemrdsChk8Z84VBO428glAA==}
+ engines: {node: '>=18.12.0', npm: '>=8.19.2'}
+ peerDependencies:
+ react: ^18.0.0
+ react-dom: ^18.0.0
+
'@wordpress/boot@0.8.0':
resolution: {integrity: sha512-vyjmPZXWC0FgoU27JO/8KVZVNLwELKgiSB6FO/DATy22zQBmOuqIBbGKhsf2MazPYpkwe+l5ozBp2hGgIX/ofg==}
engines: {node: '>=18.12.0', npm: '>=8.19.2'}
@@ -10494,8 +10550,8 @@ packages:
resolution: {integrity: sha512-Cp9JcjdL6ZYVNUGgXR/XOO9Fueb3i0dzpvdpC1pB/iJldiQWYRPZ7LMCaJrwprG92/AfuU68BaR5CwTwrSN5tA==}
engines: {node: '>=18.12.0', npm: '>=8.19.2'}
- '@wordpress/build@0.8.0':
- resolution: {integrity: sha512-703tl7VoGLNtAAEdVzLVWAVkid6jMh6tGZgD1NNH/2mWy+FXmOv/m5NxeoD+FD6Je3d93ladA+ln83ZcPH7Mzw==}
+ '@wordpress/build@0.10.1-next.v.202603102151.0':
+ resolution: {integrity: sha512-uk7LIvouTnrMTgFnvdGvS/BhVjBkkL5fwnr8JCyRud4AObAzsJRixu+ZG//T6SNLdJuiLdF5zihMBKCEqVvN8g==}
engines: {node: '>=20.10.0', npm: '>=10.2.3'}
hasBin: true
peerDependencies:
@@ -10837,6 +10893,12 @@ packages:
peerDependencies:
react: ^18.0.0
+ '@wordpress/route@0.6.0':
+ resolution: {integrity: sha512-NL0JZx/KoV0rHQhua8Py+rj/Z/KfyQusRw72yCk0Ei9sna1cYywAqzZNJfKxRr+Ua2YwJSy7ybueL0/OPZ0izw==}
+ engines: {node: '>=18.12.0', npm: '>=8.19.2'}
+ peerDependencies:
+ react: ^18.0.0
+
'@wordpress/route@0.7.0':
resolution: {integrity: sha512-6gv0hXb+bhGNK7bLAdcbMoDOdrXFRfsJfSN1P8hm429myZESmhYrh0mEV4at4y44C06zGPTqyzSKH0sR0eCMkg==}
engines: {node: '>=18.12.0', npm: '>=8.19.2'}
@@ -10886,6 +10948,17 @@ packages:
stylelint:
optional: true
+ '@wordpress/theme@0.7.0':
+ resolution: {integrity: sha512-ULwLCSKYraIsv83bVH+Hm5pGFen6/0/8xOXQwxMdxeU+8kSm0cTKlpQPNvJGCmAeQb2OgFcowB/8wrUdyqW8UQ==}
+ engines: {node: '>=18.12.0', npm: '>=8.19.2'}
+ peerDependencies:
+ react: ^18.0.0
+ react-dom: ^18.0.0
+ stylelint: '>=16.8.2'
+ peerDependenciesMeta:
+ stylelint:
+ optional: true
+
'@wordpress/theme@0.8.0':
resolution: {integrity: sha512-xXGjWNFHICBuMNfjCjTui5ChkiKmmPTJtsF5tPXnUBXJaw43xxGlL0y7lpCNPJQxz+NPMJ01KlGfxhRsHXjKrQ==}
engines: {node: '>=18.12.0', npm: '>=8.19.2'}
@@ -24024,6 +24097,40 @@ snapshots:
simple-html-tokenizer: 0.5.11
uuid: 9.0.1(patch_hash=87a713b75995ed86c0ecd0cadfd1b9f85092ca16fdff7132ff98b62fc3cf2db0)
+ '@wordpress/boot@0.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1)':
+ dependencies:
+ '@wordpress/a11y': 4.41.0
+ '@wordpress/admin-ui': 1.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@wordpress/base-styles': 6.17.0
+ '@wordpress/commands': 1.41.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@wordpress/components': 32.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@wordpress/compose': 7.41.0(react@18.3.1)
+ '@wordpress/core-data': 7.41.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1)
+ '@wordpress/data': 10.41.0(react@18.3.1)
+ '@wordpress/editor': 14.41.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1)
+ '@wordpress/element': 6.41.0
+ '@wordpress/html-entities': 4.41.0
+ '@wordpress/i18n': 6.14.0
+ '@wordpress/icons': 11.8.0(react@18.3.1)
+ '@wordpress/keyboard-shortcuts': 5.41.0(react@18.3.1)
+ '@wordpress/keycodes': 4.41.0
+ '@wordpress/lazy-editor': 1.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1)
+ '@wordpress/notices': 5.41.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@wordpress/primitives': 4.41.0(react@18.3.1)
+ '@wordpress/private-apis': 1.41.0
+ '@wordpress/route': 0.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@wordpress/theme': 0.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1)
+ '@wordpress/url': 4.41.0
+ clsx: 2.1.1
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ transitivePeerDependencies:
+ - '@emotion/is-prop-valid'
+ - '@types/react'
+ - '@types/react-dom'
+ - stylelint
+ - supports-color
+
'@wordpress/boot@0.8.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1)':
dependencies:
'@wordpress/a11y': 4.41.0
@@ -24060,7 +24167,7 @@ snapshots:
'@wordpress/browserslist-config@6.41.0': {}
- '@wordpress/build@0.8.0(@babel/core@7.29.0)(@wordpress/boot@0.8.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1))(@wordpress/route@0.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.8.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1))(browserslist@4.28.1)':
+ '@wordpress/build@0.10.1-next.v.202603102151.0(@babel/core@7.29.0)(@wordpress/boot@0.8.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1))(@wordpress/route@0.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.8.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1))(browserslist@4.28.1)':
dependencies:
'@emotion/babel-plugin': 11.13.5
autoprefixer: 10.4.27(postcss@8.5.6)
@@ -25322,6 +25429,29 @@ snapshots:
- stylelint
- supports-color
+ '@wordpress/lazy-editor@1.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1)':
+ dependencies:
+ '@wordpress/asset-loader': 1.7.0
+ '@wordpress/base-styles': 6.17.0
+ '@wordpress/block-editor': 15.14.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1)
+ '@wordpress/blocks': 15.14.0(react@18.3.1)
+ '@wordpress/components': 32.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@wordpress/core-data': 7.41.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1)
+ '@wordpress/data': 10.41.0(react@18.3.1)
+ '@wordpress/editor': 14.41.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1)
+ '@wordpress/element': 6.41.0
+ '@wordpress/global-styles-engine': 1.8.0(react@18.3.1)
+ '@wordpress/i18n': 6.14.0
+ '@wordpress/private-apis': 1.41.0
+ transitivePeerDependencies:
+ - '@emotion/is-prop-valid'
+ - '@types/react'
+ - '@types/react-dom'
+ - react
+ - react-dom
+ - stylelint
+ - supports-color
+
'@wordpress/media-editor@0.4.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1)':
dependencies:
'@babel/runtime': 7.28.6
@@ -25742,6 +25872,15 @@ snapshots:
memize: 2.1.1
react: 18.3.1
+ '@wordpress/route@0.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@tanstack/history': 1.161.4
+ '@tanstack/react-router': 1.166.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@wordpress/private-apis': 1.41.0
+ react: 18.3.1
+ transitivePeerDependencies:
+ - react-dom
+
'@wordpress/route@0.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@tanstack/history': 1.161.4
@@ -25822,6 +25961,17 @@ snapshots:
optionalDependencies:
stylelint: 16.26.1
+ '@wordpress/theme@0.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1)':
+ dependencies:
+ '@wordpress/element': 6.41.0
+ '@wordpress/private-apis': 1.41.0
+ colorjs.io: 0.6.1
+ memize: 2.1.1
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ stylelint: 16.26.1
+
'@wordpress/theme@0.8.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1(typescript@5.9.3))':
dependencies:
'@wordpress/element': 6.41.0
diff --git a/projects/packages/forms/changelog/fix-polyfill-wp-build-dependencies b/projects/packages/forms/changelog/fix-polyfill-wp-build-dependencies
new file mode 100644
index 000000000000..47f642d23ac9
--- /dev/null
+++ b/projects/packages/forms/changelog/fix-polyfill-wp-build-dependencies
@@ -0,0 +1,4 @@
+Significance: minor
+Type: fixed
+
+Add polyfills for wp-build unbundled dependencies
diff --git a/projects/packages/forms/composer.json b/projects/packages/forms/composer.json
index 65f86152c7fc..8520b2b828e9 100644
--- a/projects/packages/forms/composer.json
+++ b/projects/packages/forms/composer.json
@@ -7,6 +7,7 @@
"php": ">=7.2",
"automattic/jetpack-blocks": "@dev",
"automattic/jetpack-assets": "@dev",
+ "automattic/jetpack-wp-build-polyfills": "@dev",
"automattic/jetpack-connection": "@dev",
"automattic/jetpack-device-detection": "@dev",
"automattic/jetpack-external-connections": "@dev",
diff --git a/projects/packages/forms/package.json b/projects/packages/forms/package.json
index c562b692d7ec..954c0bc20956 100644
--- a/projects/packages/forms/package.json
+++ b/projects/packages/forms/package.json
@@ -16,8 +16,9 @@
"author": "Automattic",
"type": "module",
"scripts": {
- "build": "pnpm run clean && pnpm run build:blocks && pnpm run build:contact-form && pnpm run build:dashboard && pnpm run build:wp-build && pnpm run build:form-editor && pnpm run module:build",
+ "build": "pnpm run clean && pnpm run build:blocks && pnpm run build:contact-form && pnpm run build:dashboard && pnpm run build:wp-build && pnpm run build:boot-asset && pnpm run build:form-editor && pnpm run module:build",
"build:blocks": "webpack --config ./tools/webpack.config.blocks.js",
+ "build:boot-asset": "provide-boot-asset-file",
"build:contact-form": "webpack --config ./tools/webpack.config.contact-form.js",
"build:dashboard": "webpack --config ./tools/webpack.config.dashboard.js",
"build:form-editor": "webpack --config ./tools/webpack.config.form-editor.js",
@@ -99,6 +100,7 @@
"@automattic/color-studio": "4.1.0",
"@automattic/jetpack-base-styles": "workspace:*",
"@automattic/jetpack-webpack-config": "workspace:*",
+ "@automattic/jetpack-wp-build-polyfills": "workspace:*",
"@automattic/remove-asset-webpack-plugin": "workspace:*",
"@babel/core": "7.29.0",
"@babel/runtime": "7.28.6",
@@ -112,7 +114,7 @@
"@typescript/native-preview": "7.0.0-dev.20260225.1",
"@wordpress/api-fetch": "7.41.0",
"@wordpress/browserslist-config": "6.41.0",
- "@wordpress/build": "0.8.0",
+ "@wordpress/build": "0.10.1-next.v.202603102151.0",
"@wordpress/date": "5.41.0",
"autoprefixer": "10.4.20",
"browserslist": "^4.24.0",
diff --git a/projects/packages/forms/src/dashboard/class-dashboard.php b/projects/packages/forms/src/dashboard/class-dashboard.php
index 1200ca620141..f4e0129a2fed 100644
--- a/projects/packages/forms/src/dashboard/class-dashboard.php
+++ b/projects/packages/forms/src/dashboard/class-dashboard.php
@@ -12,6 +12,7 @@
use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
use Automattic\Jetpack\Forms\ContactForm\Contact_Form_Plugin;
use Automattic\Jetpack\Tracking;
+use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
@@ -36,9 +37,19 @@ public static function load_wp_build() {
: 'inbox';
wp_safe_redirect( self::get_forms_admin_url( $default_tab ) );
+
exit;
}
+ // Register polyfills for WP < 7.0 (must run before build.php).
+ WP_Build_Polyfills::register(
+ 'jetpack-forms',
+ array_merge(
+ WP_Build_Polyfills::SCRIPT_HANDLES,
+ WP_Build_Polyfills::MODULE_IDS
+ )
+ );
+
$wp_build_index = dirname( __DIR__, 2 ) . '/build/build.php';
if ( file_exists( $wp_build_index ) ) {
diff --git a/projects/packages/forms/tests/php/dashboard/Dashboard_Test.php b/projects/packages/forms/tests/php/dashboard/Dashboard_Test.php
index d1d740927164..b95dfdcbdb5e 100644
--- a/projects/packages/forms/tests/php/dashboard/Dashboard_Test.php
+++ b/projects/packages/forms/tests/php/dashboard/Dashboard_Test.php
@@ -7,6 +7,7 @@
namespace Automattic\Jetpack\Forms\Dashboard;
+use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills;
use PHPUnit\Framework\Attributes\CoversClass;
use WorDBless\BaseTestCase;
@@ -18,6 +19,15 @@
#[CoversClass( Dashboard::class )]
class Dashboard_Test extends BaseTestCase {
+ /**
+ * Clean up after each test.
+ */
+ public function tear_down() {
+ $this->reset_wp_build_polyfills();
+ unset( $_GET['page'], $_GET['p'] );
+ parent::tear_down();
+ }
+
/**
* Test get_forms_admin_url without tab parameter
*/
@@ -122,6 +132,91 @@ public function test_get_forms_admin_url_wp_build_with_tab() {
remove_filter( 'jetpack_forms_alpha', '__return_true' );
}
+ /**
+ * Reset WP_Build_Polyfills static state between tests.
+ */
+ private function reset_wp_build_polyfills() {
+ $ref = new \ReflectionClass( WP_Build_Polyfills::class );
+
+ $requested = $ref->getProperty( 'requested' );
+ if ( PHP_VERSION_ID < 80100 ) {
+ $requested->setAccessible( true );
+ }
+ $requested->setValue( null, array() );
+
+ $hooked = $ref->getProperty( 'hooked' );
+ if ( PHP_VERSION_ID < 80100 ) {
+ $hooked->setAccessible( true );
+ }
+ $hooked->setValue( null, false );
+
+ $threshold = $ref->getProperty( 'wp_version_threshold' );
+ if ( PHP_VERSION_ID < 80100 ) {
+ $threshold->setAccessible( true );
+ }
+ $threshold->setValue( null, '7.0' );
+ }
+
+ /**
+ * Test load_wp_build registers polyfills when on the wp-build admin page.
+ */
+ public function test_load_wp_build_registers_polyfills_on_wpbuild_page() {
+ $_GET['page'] = Dashboard::FORMS_WPBUILD_ADMIN_SLUG;
+ $_GET['p'] = '/responses/inbox';
+
+ Dashboard::load_wp_build();
+
+ $ref = new \ReflectionClass( WP_Build_Polyfills::class );
+ $requested = $ref->getProperty( 'requested' );
+ if ( PHP_VERSION_ID < 80100 ) {
+ $requested->setAccessible( true );
+ }
+ $value = $requested->getValue();
+
+ $expected_handles = array_merge( WP_Build_Polyfills::SCRIPT_HANDLES, WP_Build_Polyfills::MODULE_IDS );
+
+ foreach ( $expected_handles as $handle ) {
+ $this->assertArrayHasKey( $handle, $value, "Polyfill handle '$handle' should be registered." );
+ $this->assertContains( 'jetpack-forms', $value[ $handle ], "Consumer 'jetpack-forms' should be registered for '$handle'." );
+ }
+ }
+
+ /**
+ * Test load_wp_build does not register polyfills when on a different admin page.
+ */
+ public function test_load_wp_build_does_not_register_polyfills_on_other_page() {
+ $_GET['page'] = 'some-other-page';
+
+ Dashboard::load_wp_build();
+
+ $ref = new \ReflectionClass( WP_Build_Polyfills::class );
+ $requested = $ref->getProperty( 'requested' );
+ if ( PHP_VERSION_ID < 80100 ) {
+ $requested->setAccessible( true );
+ }
+ $value = $requested->getValue();
+
+ $this->assertEmpty( $value, 'No polyfills should be registered when on a different page.' );
+ }
+
+ /**
+ * Test load_wp_build does not register polyfills when no page is set.
+ */
+ public function test_load_wp_build_does_not_register_polyfills_without_page() {
+ unset( $_GET['page'] );
+
+ Dashboard::load_wp_build();
+
+ $ref = new \ReflectionClass( WP_Build_Polyfills::class );
+ $requested = $ref->getProperty( 'requested' );
+ if ( PHP_VERSION_ID < 80100 ) {
+ $requested->setAccessible( true );
+ }
+ $value = $requested->getValue();
+
+ $this->assertEmpty( $value, 'No polyfills should be registered when no page is set.' );
+ }
+
/**
* Test is_jetpack_forms_admin_page when get_current_screen is not available
*/
diff --git a/projects/packages/wp-build-polyfills/.gitattributes b/projects/packages/wp-build-polyfills/.gitattributes
new file mode 100644
index 000000000000..a8a09c7553b9
--- /dev/null
+++ b/projects/packages/wp-build-polyfills/.gitattributes
@@ -0,0 +1,21 @@
+# Files not needed to be distributed in the package.
+.gitattributes export-ignore
+.github/ export-ignore
+package.json export-ignore
+
+# Files to include in the mirror repo, but excluded via gitignore
+# Remember to end all directories with `/**` to properly tag every file.
+# /src/js/example.min.js production-include
+build/** production-include
+
+# Files to exclude from the mirror repo, but included in the monorepo.
+# Remember to end all directories with `/**` to properly tag every file.
+.gitignore production-exclude
+bin/** production-exclude
+webpack.config.js production-exclude
+changelog/** production-exclude
+phpunit.*.xml.dist production-exclude
+.phpcs.dir.xml production-exclude
+tests/** production-exclude
+.phpcsignore production-exclude
+preserve-dynamic-imports-loader.js production-exclude
diff --git a/projects/packages/wp-build-polyfills/.gitignore b/projects/packages/wp-build-polyfills/.gitignore
new file mode 100644
index 000000000000..83a5ab2f4c63
--- /dev/null
+++ b/projects/packages/wp-build-polyfills/.gitignore
@@ -0,0 +1,3 @@
+/node_modules
+/build
+/.cache
diff --git a/projects/packages/wp-build-polyfills/.phan/config.php b/projects/packages/wp-build-polyfills/.phan/config.php
new file mode 100644
index 000000000000..a0a9c626191e
--- /dev/null
+++ b/projects/packages/wp-build-polyfills/.phan/config.php
@@ -0,0 +1,13 @@
+=7.2"
+ },
+ "require-dev": {
+ "automattic/jetpack-changelogger": "@dev",
+ "automattic/jetpack-test-environment": "@dev",
+ "automattic/phpunit-select-config": "@dev",
+ "yoast/phpunit-polyfills": "^4.0.0"
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "repositories": [
+ {
+ "type": "path",
+ "url": "../../packages/*",
+ "options": {
+ "monorepo": true
+ }
+ }
+ ],
+ "scripts": {
+ "build-production": [
+ "pnpm run build-production"
+ ],
+ "build-development": [
+ "pnpm run build"
+ ],
+ "phpunit": [
+ "phpunit-select-config phpunit.#.xml.dist --colors=always"
+ ],
+ "test-coverage": [
+ "php -dpcov.directory=. ./vendor/bin/phpunit-select-config phpunit.#.xml.dist --coverage-php \"$COVERAGE_DIR/php.cov\""
+ ],
+ "test-php": [
+ "@composer phpunit"
+ ]
+ },
+ "config": {
+ "allow-plugins": {
+ "roots/wordpress-core-installer": true
+ }
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "extra": {
+ "autotagger": true,
+ "mirror-repo": "Automattic/jetpack-wp-build-polyfills",
+ "textdomain": "jetpack-wp-build-polyfills",
+ "changelogger": {
+ "link-template": "https://github.com/Automattic/jetpack-wp-build-polyfills/compare/v${old}...v${new}"
+ },
+ "branch-alias": {
+ "dev-trunk": "0.1.x-dev"
+ }
+ }
+}
diff --git a/projects/packages/wp-build-polyfills/package.json b/projects/packages/wp-build-polyfills/package.json
new file mode 100644
index 000000000000..cadcb3bdacf3
--- /dev/null
+++ b/projects/packages/wp-build-polyfills/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "@automattic/jetpack-wp-build-polyfills",
+ "version": "0.1.0-alpha",
+ "private": true,
+ "description": "Polyfills for WordPress Core packages not available in WP < 7.0",
+ "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/packages/wp-build-polyfills/#readme",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/Automattic/jetpack.git",
+ "directory": "projects/packages/wp-build-polyfills"
+ },
+ "license": "GPL-2.0-or-later",
+ "author": "Automattic",
+ "bin": {
+ "provide-boot-asset-file": "bin/provide-boot-asset-file.js"
+ },
+ "scripts": {
+ "build": "pnpm run clean && webpack",
+ "build-production": "NODE_ENV=production BABEL_ENV=production pnpm run build",
+ "clean": "rm -rf build/"
+ },
+ "devDependencies": {
+ "@automattic/jetpack-webpack-config": "workspace:*",
+ "@wordpress/a11y": "^4.40.0",
+ "@wordpress/admin-ui": "^1.9.0",
+ "@wordpress/boot": "^0.7.1",
+ "@wordpress/icons": "^11.7.0",
+ "@wordpress/lazy-editor": "^1.6.1",
+ "@wordpress/notices": "^5.40.0",
+ "@wordpress/private-apis": "^1.40.0",
+ "@wordpress/route": "^0.6.0",
+ "@wordpress/theme": "^0.7.0",
+ "webpack": "^5.104.1",
+ "webpack-cli": "^6.0.1"
+ },
+ "optionalDependencies": {
+ "react": "18.3.1",
+ "react-dom": "18.3.1"
+ }
+}
diff --git a/projects/packages/wp-build-polyfills/phpunit.11.xml.dist b/projects/packages/wp-build-polyfills/phpunit.11.xml.dist
new file mode 100644
index 000000000000..f7418373829b
--- /dev/null
+++ b/projects/packages/wp-build-polyfills/phpunit.11.xml.dist
@@ -0,0 +1,36 @@
+
+
+
+
+ tests/php
+
+
+
+
+
+
+
+ src
+
+
+
+
+
diff --git a/projects/packages/wp-build-polyfills/phpunit.12.xml.dist b/projects/packages/wp-build-polyfills/phpunit.12.xml.dist
new file mode 120000
index 000000000000..9fdb7a2c745c
--- /dev/null
+++ b/projects/packages/wp-build-polyfills/phpunit.12.xml.dist
@@ -0,0 +1 @@
+phpunit.11.xml.dist
\ No newline at end of file
diff --git a/projects/packages/wp-build-polyfills/phpunit.8.xml.dist b/projects/packages/wp-build-polyfills/phpunit.8.xml.dist
new file mode 120000
index 000000000000..707bde67863c
--- /dev/null
+++ b/projects/packages/wp-build-polyfills/phpunit.8.xml.dist
@@ -0,0 +1 @@
+phpunit.9.xml.dist
\ No newline at end of file
diff --git a/projects/packages/wp-build-polyfills/phpunit.9.xml.dist b/projects/packages/wp-build-polyfills/phpunit.9.xml.dist
new file mode 100644
index 000000000000..3965963c485e
--- /dev/null
+++ b/projects/packages/wp-build-polyfills/phpunit.9.xml.dist
@@ -0,0 +1,17 @@
+
+
+
+
+ tests/php
+
+
+
diff --git a/projects/packages/wp-build-polyfills/preserve-dynamic-imports-loader.js b/projects/packages/wp-build-polyfills/preserve-dynamic-imports-loader.js
new file mode 100644
index 000000000000..16094da8281a
--- /dev/null
+++ b/projects/packages/wp-build-polyfills/preserve-dynamic-imports-loader.js
@@ -0,0 +1,47 @@
+/**
+ * Webpack loader that preserves dynamic import() calls as native browser imports.
+ *
+ * When webpack encounters `import(variable)` (a dynamic import with a non-string
+ * argument), it creates a "context module" stub that always throws
+ * "Cannot find module". This is because webpack can't statically determine what
+ * module will be loaded at runtime.
+ *
+ * For packages like `@wordpress/boot` that rely on the browser's import map to
+ * resolve module IDs at runtime, these dynamic imports must be preserved as
+ * native `import()` calls. This loader adds `webpackIgnore: true` magic comments
+ * to such imports, telling webpack to leave them as-is.
+ *
+ * Only import() calls with variable arguments are affected. String-literal
+ * imports like `import("@wordpress/a11y")` are left untouched so that webpack's
+ * externals plugin can handle them normally. Dynamic imports with leading
+ * comments (e.g. `import(/* webpackChunkName: ... *\/ variable)`) are also
+ * handled correctly.
+ * @param {string} source - The source code to process.
+ * @return {string} - The processed source code.
+ */
+module.exports = function preserveDynamicImports( source ) {
+ return source.replace( /\bimport\(/g, ( match, offset ) => {
+ // Scan past whitespace and comments to find the first meaningful character.
+ let i = offset + match.length;
+ while ( i < source.length ) {
+ if ( source[ i ] === '/' && source[ i + 1 ] === '*' ) {
+ const end = source.indexOf( '*/', i + 2 );
+ i = end === -1 ? source.length : end + 2;
+ } else if ( source[ i ] === '/' && source[ i + 1 ] === '/' ) {
+ const end = source.indexOf( '\n', i + 2 );
+ i = end === -1 ? source.length : end + 1;
+ } else if ( /\s/.test( source[ i ] ) ) {
+ i++;
+ } else {
+ break;
+ }
+ }
+
+ // String-literal imports are left for webpack's externals plugin.
+ if ( i < source.length && /['"`]/.test( source[ i ] ) ) {
+ return match;
+ }
+
+ return 'import(/* webpackIgnore: true */ ';
+ } );
+};
diff --git a/projects/packages/wp-build-polyfills/src/class-wp-build-polyfills.php b/projects/packages/wp-build-polyfills/src/class-wp-build-polyfills.php
new file mode 100644
index 000000000000..f8ac3e88a1c4
--- /dev/null
+++ b/projects/packages/wp-build-polyfills/src/class-wp-build-polyfills.php
@@ -0,0 +1,218 @@
+
+ */
+ private static $requested = array();
+
+ /**
+ * Whether the wp_default_scripts hook has already been added.
+ *
+ * @var bool
+ */
+ private static $hooked = false;
+
+ /**
+ * The WordPress version below which force-replacements are applied.
+ * When multiple consumers call register() with different thresholds,
+ * the highest threshold wins (most conservative approach).
+ *
+ * @var string
+ */
+ private static $wp_version_threshold = '7.0';
+
+ /**
+ * Register polyfill scripts and modules.
+ *
+ * Call this early (e.g. during plugin load) — it hooks into wp_default_scripts
+ * at priority 20 so Core (default) and Gutenberg (priority 10) register first.
+ *
+ * When multiple consumers call this method with different thresholds, the
+ * highest threshold wins (most conservative — polyfills active on more versions).
+ *
+ * @param string $consumer A unique identifier for the consumer (e.g. plugin slug).
+ * @param string[] $polyfills List of polyfill handles/module IDs to register.
+ * Use class constants SCRIPT_HANDLES and MODULE_IDS for reference.
+ * @param string $wp_version_threshold The WordPress version below which force-replacements
+ * are applied. Defaults to '7.0'.
+ */
+ public static function register( $consumer, $polyfills, $wp_version_threshold = '7.0' ) {
+ foreach ( $polyfills as $handle ) {
+ if ( ! in_array( $handle, self::SCRIPT_HANDLES, true ) && ! in_array( $handle, self::MODULE_IDS, true ) ) {
+ continue;
+ }
+ if ( ! isset( self::$requested[ $handle ] ) ) {
+ self::$requested[ $handle ] = array();
+ }
+ if ( ! in_array( $consumer, self::$requested[ $handle ], true ) ) {
+ self::$requested[ $handle ][] = $consumer;
+ }
+ }
+
+ if ( version_compare( $wp_version_threshold, self::$wp_version_threshold, '>' ) ) {
+ self::$wp_version_threshold = $wp_version_threshold;
+ }
+
+ if ( self::$hooked ) {
+ return;
+ }
+ self::$hooked = true;
+
+ $package_root = dirname( __DIR__ );
+ $build_dir = $package_root . '/build';
+ $base_file = $package_root . '/composer.json';
+
+ add_action(
+ 'wp_default_scripts',
+ function ( $scripts ) use ( $build_dir, $base_file ) {
+ self::register_scripts( $scripts, $build_dir, $base_file, self::$wp_version_threshold );
+ self::register_modules( $build_dir, $base_file );
+ },
+ 20
+ );
+ }
+
+ /**
+ * Get the map of requested polyfills and their consumers.
+ *
+ * @return array Keys are polyfill handles/module IDs, values are consumer names.
+ */
+ public static function get_consumers() {
+ return self::$requested;
+ }
+
+ /**
+ * Register polyfill classic scripts.
+ *
+ * @param \WP_Scripts $scripts The WP_Scripts instance.
+ * @param string $build_dir Absolute path to the build directory.
+ * @param string $base_file File path for plugins_url() computation.
+ * @param string $wp_version_threshold WP version below which force-replacements apply.
+ */
+ private static function register_scripts( $scripts, $build_dir, $base_file, $wp_version_threshold ) {
+ $force_replace = version_compare( $GLOBALS['wp_version'] ?? '0', $wp_version_threshold, '<' );
+
+ $polyfills = array(
+ 'wp-notices' => array(
+ 'path' => 'notices',
+ // Only force-replace on older WP: older Core versions ship
+ // notices without SnackbarNotices and InlineNotices component
+ // exports that @wordpress/boot depends on.
+ 'force' => $force_replace,
+ ),
+ 'wp-private-apis' => array(
+ 'path' => 'private-apis',
+ // Only force-replace on older WP: older Core versions ship
+ // private-apis with an incomplete allowlist that rejects
+ // @wordpress/theme and @wordpress/route.
+ // Our version is a strict superset (same API, larger allowlist).
+ 'force' => $force_replace,
+ ),
+ 'wp-theme' => array(
+ 'path' => 'theme',
+ ),
+ );
+
+ foreach ( $polyfills as $handle => $data ) {
+ if ( ! isset( self::$requested[ $handle ] ) ) {
+ continue;
+ }
+
+ $asset_file = $build_dir . '/scripts/' . $data['path'] . '/index.asset.php';
+
+ if ( ! file_exists( $asset_file ) ) {
+ continue;
+ }
+
+ $force = ! empty( $data['force'] );
+
+ if ( ! $force && $scripts->query( $handle, 'registered' ) ) {
+ continue;
+ }
+
+ // Deregister first when forcing replacement of an existing registration.
+ if ( $force && $scripts->query( $handle, 'registered' ) ) {
+ $scripts->remove( $handle );
+ }
+
+ $asset = require $asset_file;
+
+ $scripts->add(
+ $handle,
+ plugins_url( 'build/scripts/' . $data['path'] . '/index.js', $base_file ),
+ $asset['dependencies'],
+ $asset['version']
+ );
+ }
+ }
+
+ /**
+ * Register polyfill script modules.
+ *
+ * Call to wp_register_script_module() silently ignores duplicate registrations (first wins),
+ * so no explicit is_registered check is needed.
+ *
+ * @param string $build_dir Absolute path to the build directory.
+ * @param string $base_file File path for plugins_url() computation.
+ */
+ private static function register_modules( $build_dir, $base_file ) {
+ if ( ! function_exists( 'wp_register_script_module' ) ) {
+ return;
+ }
+
+ $modules = array( 'boot', 'route', 'a11y' );
+
+ foreach ( $modules as $name ) {
+ $module_id = '@wordpress/' . $name;
+
+ if ( ! isset( self::$requested[ $module_id ] ) ) {
+ continue;
+ }
+
+ $asset_file = $build_dir . '/modules/' . $name . '/index.asset.php';
+
+ if ( ! file_exists( $asset_file ) ) {
+ continue;
+ }
+
+ $asset = require $asset_file;
+
+ wp_register_script_module(
+ $module_id,
+ plugins_url( 'build/modules/' . $name . '/index.js', $base_file ),
+ $asset['module_dependencies'] ?? array(),
+ $asset['version']
+ );
+ }
+ }
+}
diff --git a/projects/packages/wp-build-polyfills/tests/.phpcs.dir.xml b/projects/packages/wp-build-polyfills/tests/.phpcs.dir.xml
new file mode 100644
index 000000000000..46951fe77b37
--- /dev/null
+++ b/projects/packages/wp-build-polyfills/tests/.phpcs.dir.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/projects/packages/wp-build-polyfills/tests/php/WP_Build_Polyfills_Test.php b/projects/packages/wp-build-polyfills/tests/php/WP_Build_Polyfills_Test.php
new file mode 100644
index 000000000000..1d71d6902da1
--- /dev/null
+++ b/projects/packages/wp-build-polyfills/tests/php/WP_Build_Polyfills_Test.php
@@ -0,0 +1,521 @@
+build_dir = null;
+ $base = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'wp-build-polyfills-test-';
+ for ( $i = 0; $i < 1000; $i++ ) {
+ $tmpdir = $base . uniqid();
+ // Atomic mkdir prevents symlink race (TOCTOU).
+ if ( @mkdir( $tmpdir, 0700 ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
+ $this->build_dir = $tmpdir;
+ break;
+ }
+ }
+ if ( null === $this->build_dir ) {
+ throw new \RuntimeException( 'Failed to create temporary directory' );
+ }
+
+ mkdir( $this->build_dir . '/scripts/notices', 0755, true );
+ mkdir( $this->build_dir . '/scripts/private-apis', 0755, true );
+ mkdir( $this->build_dir . '/scripts/theme', 0755, true );
+ mkdir( $this->build_dir . '/modules/boot', 0755, true );
+ mkdir( $this->build_dir . '/modules/route', 0755, true );
+ mkdir( $this->build_dir . '/modules/a11y', 0755, true );
+
+ $this->original_wp_version = $GLOBALS['wp_version'];
+ $this->original_wp_script_modules = $GLOBALS['wp_script_modules'] ?? null;
+ }
+
+ /**
+ * Tear down test fixtures.
+ *
+ * @after
+ */
+ #[After]
+ public function tear_down() {
+ $GLOBALS['wp_version'] = $this->original_wp_version;
+ if ( null === $this->original_wp_script_modules ) {
+ unset( $GLOBALS['wp_script_modules'] );
+ } else {
+ $GLOBALS['wp_script_modules'] = $this->original_wp_script_modules; }
+
+ // Reset static state.
+ $requested = new \ReflectionProperty( WP_Build_Polyfills::class, 'requested' );
+ if ( PHP_VERSION_ID < 80100 ) {
+ $requested->setAccessible( true );
+ }
+ $requested->setValue( null, array() );
+
+ $hooked = new \ReflectionProperty( WP_Build_Polyfills::class, 'hooked' );
+ if ( PHP_VERSION_ID < 80100 ) {
+ $hooked->setAccessible( true );
+ }
+ $hooked->setValue( null, false );
+
+ $threshold = new \ReflectionProperty( WP_Build_Polyfills::class, 'wp_version_threshold' );
+ if ( PHP_VERSION_ID < 80100 ) {
+ $threshold->setAccessible( true );
+ }
+ $threshold->setValue( null, '7.0' );
+
+ $this->recursive_rmdir( $this->build_dir );
+
+ parent::tear_down();
+ }
+
+ /**
+ * Create a fake asset file.
+ *
+ * @param string $path Relative path within the build dir (e.g. "scripts/notices/index.asset.php").
+ * @param array $deps Dependencies array.
+ * @param string $version Version string.
+ * @param array $extra Extra keys to merge into the asset array.
+ */
+ private function create_asset_file( $path, $deps = array(), $version = '1.0.0', $extra = array() ) {
+ $data = array_merge(
+ array(
+ 'dependencies' => $deps,
+ 'version' => $version,
+ ),
+ $extra
+ );
+ $contents = 'build_dir . '/' . $path, $contents ); }
+
+ /**
+ * Create a WP_Scripts instance with polyfill handles removed.
+ *
+ * WP_Scripts::__construct() fires wp_default_scripts which registers core
+ * scripts. We remove the three handles under test so tests start clean.
+ *
+ * @return \WP_Scripts
+ */
+ private function create_clean_scripts() {
+ $scripts = new \WP_Scripts();
+ $scripts->remove( 'wp-notices' );
+ $scripts->remove( 'wp-private-apis' );
+ $scripts->remove( 'wp-theme' );
+ return $scripts;
+ }
+
+ /**
+ * Request all available polyfills for a test consumer.
+ *
+ * Populates the static $requested property via reflection so
+ * register_scripts/register_modules will process all handles,
+ * without triggering the wp_default_scripts hook.
+ *
+ * @param string[] $polyfills Optional specific polyfills to request. Defaults to all.
+ */
+ private function request_polyfills( $polyfills = null ) {
+ if ( null === $polyfills ) {
+ $polyfills = array_merge( WP_Build_Polyfills::SCRIPT_HANDLES, WP_Build_Polyfills::MODULE_IDS );
+ }
+
+ // Directly set the $requested static property via reflection.
+ $requested = new \ReflectionProperty( WP_Build_Polyfills::class, 'requested' );
+ if ( PHP_VERSION_ID < 80100 ) {
+ $requested->setAccessible( true );
+ }
+ $current = $requested->getValue();
+ foreach ( $polyfills as $handle ) {
+ if ( ! isset( $current[ $handle ] ) ) {
+ $current[ $handle ] = array();
+ }
+ $current[ $handle ][] = 'test';
+ }
+ $requested->setValue( null, $current );
+ }
+
+ /**
+ * Invoke the private register_scripts method.
+ *
+ * @param \WP_Scripts $scripts WP_Scripts instance.
+ */
+ private function invoke_register_scripts( $scripts ) {
+ $this->request_polyfills( WP_Build_Polyfills::SCRIPT_HANDLES );
+
+ $method = new \ReflectionMethod( WP_Build_Polyfills::class, 'register_scripts' );
+ if ( PHP_VERSION_ID < 80100 ) {
+ $method->setAccessible( true );
+ }
+ $method->invoke( null, $scripts, $this->build_dir, __FILE__, '7.0' );
+ }
+
+ /**
+ * Invoke the private register_modules method.
+ */
+ private function invoke_register_modules() {
+ $this->request_polyfills( WP_Build_Polyfills::MODULE_IDS );
+
+ $method = new \ReflectionMethod( WP_Build_Polyfills::class, 'register_modules' );
+ if ( PHP_VERSION_ID < 80100 ) {
+ $method->setAccessible( true );
+ }
+ $method->invoke( null, $this->build_dir, __FILE__ );
+ }
+
+ /**
+ * Check if a script module is registered.
+ *
+ * @param string $id Module ID.
+ * @return bool
+ */
+ private function is_module_registered( $id ) {
+ $registered = $this->get_registered_modules();
+ return isset( $registered[ $id ] );
+ }
+
+ /**
+ * Get data for a registered module.
+ *
+ * @param string $id Module ID.
+ * @return array|null
+ */
+ private function get_module_data( $id ) {
+ $registered = $this->get_registered_modules();
+ return $registered[ $id ] ?? null;
+ }
+
+ /**
+ * Get the private $registered property from WP_Script_Modules.
+ *
+ * @return array
+ */
+ private function get_registered_modules() {
+ $instance = wp_script_modules();
+ $prop = new \ReflectionProperty( $instance, 'registered' );
+ if ( PHP_VERSION_ID < 80100 ) {
+ $prop->setAccessible( true );
+ }
+ return $prop->getValue( $instance );
+ }
+
+ /**
+ * Recursively remove a directory.
+ *
+ * @param string $dir Directory path.
+ */
+ private function recursive_rmdir( $dir ) {
+ if ( ! is_dir( $dir ) ) {
+ return;
+ }
+ $items = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator( $dir, \RecursiveDirectoryIterator::SKIP_DOTS ),
+ \RecursiveIteratorIterator::CHILD_FIRST
+ );
+ foreach ( $items as $item ) {
+ if ( $item->isDir() ) {
+ rmdir( $item->getRealPath() ); } else {
+ unlink( $item->getRealPath() ); }
+ }
+ rmdir( $dir ); }
+
+ /**
+ * Test that all scripts are registered when all asset files exist.
+ */
+ public function test_register_scripts_registers_all_when_asset_files_exist() {
+ $this->create_asset_file( 'scripts/notices/index.asset.php' );
+ $this->create_asset_file( 'scripts/private-apis/index.asset.php' );
+ $this->create_asset_file( 'scripts/theme/index.asset.php' );
+
+ $scripts = $this->create_clean_scripts();
+ $this->invoke_register_scripts( $scripts );
+
+ $this->assertNotFalse( $scripts->query( 'wp-notices', 'registered' ) );
+ $this->assertNotFalse( $scripts->query( 'wp-private-apis', 'registered' ) );
+ $this->assertNotFalse( $scripts->query( 'wp-theme', 'registered' ) );
+ }
+
+ /**
+ * Test that no scripts are registered when asset files are missing.
+ */
+ public function test_register_scripts_skips_when_asset_files_missing() {
+ $scripts = $this->create_clean_scripts();
+ $this->invoke_register_scripts( $scripts );
+
+ $this->assertFalse( $scripts->query( 'wp-notices', 'registered' ) );
+ $this->assertFalse( $scripts->query( 'wp-private-apis', 'registered' ) );
+ $this->assertFalse( $scripts->query( 'wp-theme', 'registered' ) );
+ }
+
+ /**
+ * Test that only scripts with asset files are registered.
+ */
+ public function test_register_scripts_registers_only_scripts_with_asset_files() {
+ $this->create_asset_file( 'scripts/notices/index.asset.php' );
+ // No asset file for private-apis or theme.
+
+ $scripts = $this->create_clean_scripts();
+ $this->invoke_register_scripts( $scripts );
+
+ $this->assertNotFalse( $scripts->query( 'wp-notices', 'registered' ) );
+ $this->assertFalse( $scripts->query( 'wp-private-apis', 'registered' ) );
+ $this->assertFalse( $scripts->query( 'wp-theme', 'registered' ) );
+ }
+
+ /**
+ * Test that wp-theme (non-force) keeps existing registration.
+ */
+ public function test_register_scripts_skips_wp_theme_when_already_registered() {
+ $this->create_asset_file( 'scripts/theme/index.asset.php', array(), '2.0.0' );
+
+ $scripts = $this->create_clean_scripts();
+ $scripts->add( 'wp-theme', 'https://example.com/original-theme.js', array(), '1.0.0-original' );
+
+ $this->invoke_register_scripts( $scripts );
+
+ $registered = $scripts->query( 'wp-theme', 'registered' );
+ $this->assertNotFalse( $registered );
+ $this->assertSame( '1.0.0-original', $registered->ver );
+ }
+
+ /**
+ * Test that wp-notices is force-replaced on WP < 7.0.
+ */
+ public function test_register_scripts_force_replaces_wp_notices_on_old_wp() {
+ $GLOBALS['wp_version'] = '6.8';
+ $this->create_asset_file( 'scripts/notices/index.asset.php', array(), '9.9.9' );
+
+ $scripts = $this->create_clean_scripts();
+ $scripts->add( 'wp-notices', 'https://example.com/old-notices.js', array(), '1.0.0-old' );
+
+ $this->invoke_register_scripts( $scripts );
+
+ $registered = $scripts->query( 'wp-notices', 'registered' );
+ $this->assertNotFalse( $registered );
+ $this->assertSame( '9.9.9', $registered->ver );
+ }
+
+ /**
+ * Test that wp-private-apis is force-replaced on WP < 7.0.
+ */
+ public function test_register_scripts_force_replaces_wp_private_apis_on_old_wp() {
+ $GLOBALS['wp_version'] = '6.8';
+ $this->create_asset_file( 'scripts/private-apis/index.asset.php', array(), '9.9.9' );
+
+ $scripts = $this->create_clean_scripts();
+ $scripts->add( 'wp-private-apis', 'https://example.com/old-private-apis.js', array(), '1.0.0-old' );
+
+ $this->invoke_register_scripts( $scripts );
+
+ $registered = $scripts->query( 'wp-private-apis', 'registered' );
+ $this->assertNotFalse( $registered );
+ $this->assertSame( '9.9.9', $registered->ver );
+ }
+
+ /**
+ * Test that neither wp-notices nor wp-private-apis is force-replaced on WP >= 7.0.
+ */
+ public function test_register_scripts_does_not_force_replace_on_wp_7() {
+ $GLOBALS['wp_version'] = '7.0';
+ $this->create_asset_file( 'scripts/notices/index.asset.php', array(), '9.9.9' );
+ $this->create_asset_file( 'scripts/private-apis/index.asset.php', array(), '9.9.9' );
+
+ $scripts = $this->create_clean_scripts();
+ $scripts->add( 'wp-notices', 'https://example.com/core-notices.js', array(), '1.0.0-core' );
+ $scripts->add( 'wp-private-apis', 'https://example.com/core-private-apis.js', array(), '1.0.0-core' );
+
+ $this->invoke_register_scripts( $scripts );
+
+ $notices = $scripts->query( 'wp-notices', 'registered' );
+ $this->assertSame( '1.0.0-core', $notices->ver );
+
+ $private_apis = $scripts->query( 'wp-private-apis', 'registered' );
+ $this->assertSame( '1.0.0-core', $private_apis->ver );
+ }
+
+ /**
+ * Test that force scripts register fine even when not pre-existing.
+ */
+ public function test_register_scripts_force_registers_fresh_on_old_wp() {
+ $GLOBALS['wp_version'] = '6.8';
+ $this->create_asset_file( 'scripts/notices/index.asset.php', array(), '9.9.9' );
+ $this->create_asset_file( 'scripts/private-apis/index.asset.php', array(), '8.8.8' );
+
+ $scripts = $this->create_clean_scripts();
+ $this->invoke_register_scripts( $scripts );
+
+ $notices = $scripts->query( 'wp-notices', 'registered' );
+ $this->assertNotFalse( $notices );
+ $this->assertSame( '9.9.9', $notices->ver );
+
+ $private_apis = $scripts->query( 'wp-private-apis', 'registered' );
+ $this->assertNotFalse( $private_apis );
+ $this->assertSame( '8.8.8', $private_apis->ver );
+ }
+
+ /**
+ * Test that dependencies from asset files are passed through correctly.
+ */
+ public function test_register_scripts_has_correct_dependencies() {
+ $this->create_asset_file( 'scripts/notices/index.asset.php', array( 'wp-element', 'wp-data' ) );
+
+ $scripts = $this->create_clean_scripts();
+ $this->invoke_register_scripts( $scripts );
+
+ $registered = $scripts->query( 'wp-notices', 'registered' );
+ $this->assertNotFalse( $registered );
+ $this->assertSame( array( 'wp-element', 'wp-data' ), $registered->deps );
+ }
+
+ /**
+ * Test that all modules are registered when asset files exist.
+ */
+ public function test_register_modules_registers_all_when_asset_files_exist() {
+ // Reset the script modules global so we start fresh.
+ $GLOBALS['wp_script_modules'] = new \WP_Script_Modules();
+ $this->create_asset_file(
+ 'modules/boot/index.asset.php',
+ array(),
+ '1.0.0',
+ array( 'module_dependencies' => array() )
+ );
+ $this->create_asset_file(
+ 'modules/route/index.asset.php',
+ array(),
+ '1.0.0',
+ array( 'module_dependencies' => array() )
+ );
+ $this->create_asset_file(
+ 'modules/a11y/index.asset.php',
+ array(),
+ '1.0.0',
+ array( 'module_dependencies' => array() )
+ );
+
+ $this->invoke_register_modules();
+
+ $this->assertTrue( $this->is_module_registered( '@wordpress/boot' ) );
+ $this->assertTrue( $this->is_module_registered( '@wordpress/route' ) );
+ $this->assertTrue( $this->is_module_registered( '@wordpress/a11y' ) );
+ }
+
+ /**
+ * Test that no modules are registered when asset files are missing.
+ */
+ public function test_register_modules_skips_when_asset_files_missing() {
+ $GLOBALS['wp_script_modules'] = new \WP_Script_Modules();
+ $this->invoke_register_modules();
+
+ $this->assertFalse( $this->is_module_registered( '@wordpress/boot' ) );
+ $this->assertFalse( $this->is_module_registered( '@wordpress/route' ) );
+ $this->assertFalse( $this->is_module_registered( '@wordpress/a11y' ) );
+ }
+
+ /**
+ * Test that pre-registered modules are not replaced (first-wins semantics).
+ */
+ public function test_register_modules_does_not_replace_existing() {
+ $GLOBALS['wp_script_modules'] = new \WP_Script_Modules();
+ // Pre-register @wordpress/boot.
+ wp_register_script_module( '@wordpress/boot', 'https://example.com/core-boot.js', array(), '1.0.0-core' );
+
+ $this->create_asset_file(
+ 'modules/boot/index.asset.php',
+ array(),
+ '9.9.9',
+ array( 'module_dependencies' => array() )
+ );
+
+ $this->invoke_register_modules();
+
+ $module = $this->get_module_data( '@wordpress/boot' );
+ $this->assertNotNull( $module );
+ $this->assertSame( '1.0.0-core', $module['version'] );
+ }
+
+ /**
+ * Test that register() hooks into wp_default_scripts at priority 20.
+ */
+ public function test_register_hooks_into_wp_default_scripts() {
+ // Remove any existing hooks so we can verify the exact priority.
+ remove_all_filters( 'wp_default_scripts' );
+
+ WP_Build_Polyfills::register( 'test-plugin', array( 'wp-notices' ) );
+
+ global $wp_filter;
+ $this->assertArrayHasKey( 'wp_default_scripts', $wp_filter );
+ $this->assertArrayHasKey( 20, $wp_filter['wp_default_scripts']->callbacks );
+ }
+
+ /**
+ * Test that get_consumers returns the correct consumer map.
+ */
+ public function test_get_consumers_tracks_polyfill_consumers() {
+ WP_Build_Polyfills::register( 'plugin-a', array( 'wp-notices', '@wordpress/boot' ) );
+ WP_Build_Polyfills::register( 'plugin-b', array( 'wp-notices', 'wp-theme' ) );
+
+ $consumers = WP_Build_Polyfills::get_consumers();
+
+ $this->assertSame( array( 'plugin-a', 'plugin-b' ), $consumers['wp-notices'] );
+ $this->assertSame( array( 'plugin-a' ), $consumers['@wordpress/boot'] );
+ $this->assertSame( array( 'plugin-b' ), $consumers['wp-theme'] );
+ $this->assertArrayNotHasKey( 'wp-private-apis', $consumers );
+ }
+
+ /**
+ * Test that only requested polyfills are registered.
+ */
+ public function test_register_scripts_only_registers_requested_handles() {
+ $this->create_asset_file( 'scripts/notices/index.asset.php' );
+ $this->create_asset_file( 'scripts/private-apis/index.asset.php' );
+ $this->create_asset_file( 'scripts/theme/index.asset.php' );
+
+ // Only request wp-notices.
+ $this->request_polyfills( array( 'wp-notices' ) );
+
+ $scripts = $this->create_clean_scripts();
+
+ $method = new \ReflectionMethod( WP_Build_Polyfills::class, 'register_scripts' );
+ if ( PHP_VERSION_ID < 80100 ) {
+ $method->setAccessible( true );
+ }
+ $method->invoke( null, $scripts, $this->build_dir, __FILE__, '7.0' );
+
+ $this->assertNotFalse( $scripts->query( 'wp-notices', 'registered' ) );
+ $this->assertFalse( $scripts->query( 'wp-private-apis', 'registered' ) );
+ $this->assertFalse( $scripts->query( 'wp-theme', 'registered' ) );
+ }
+}
diff --git a/projects/packages/wp-build-polyfills/tests/php/bootstrap.php b/projects/packages/wp-build-polyfills/tests/php/bootstrap.php
new file mode 100644
index 000000000000..f87e4fe524e4
--- /dev/null
+++ b/projects/packages/wp-build-polyfills/tests/php/bootstrap.php
@@ -0,0 +1,12 @@
+ c.toUpperCase() );
+}
+
+const pkgJsonCache = new Map();
+
+/**
+ * Read and cache a package's package.json.
+ *
+ * @param {string} packageName - npm package name.
+ * @return {object|null} Parsed package.json or null.
+ */
+function readPackageJson( packageName ) {
+ if ( pkgJsonCache.has( packageName ) ) {
+ return pkgJsonCache.get( packageName );
+ }
+ try {
+ const pkgPath = localRequire.resolve( `${ packageName }/package.json` );
+ const pkg = JSON.parse( readFileSync( pkgPath, 'utf8' ) );
+ pkgJsonCache.set( packageName, pkg );
+ return pkg;
+ } catch {
+ pkgJsonCache.set( packageName, null );
+ return null;
+ }
+}
+
+/**
+ * Check if a package exports a WordPress script module (default export).
+ *
+ * @param {object} pkg - Parsed package.json.
+ * @return {boolean} True if the package has wpScriptModuleExports for '.'.
+ */
+function hasScriptModuleExport( pkg ) {
+ if ( ! pkg?.wpScriptModuleExports ) {
+ return false;
+ }
+ const exports = pkg.wpScriptModuleExports;
+ return typeof exports === 'string' || ( typeof exports === 'object' && exports[ '.' ] );
+}
+
+/**
+ * Resolve the entry point for a package.
+ *
+ * Some packages (e.g. `@wordpress/boot`) only export ESM, so CJS
+ * require.resolve() fails. We resolve via package.json instead and
+ * read the `module` or `main` field.
+ *
+ * @param {string} packageName - npm package name.
+ * @param {string|null} subEntry - Optional sub-entry relative to package root.
+ * @return {string} Absolute path to the entry file.
+ */
+function resolveEntry( packageName, subEntry = null ) {
+ const pkgPath = localRequire.resolve( `${ packageName }/package.json` );
+ const pkgDir = path.dirname( pkgPath );
+ if ( subEntry ) {
+ return path.join( pkgDir, subEntry );
+ }
+ const pkg = JSON.parse( readFileSync( pkgPath, 'utf8' ) );
+ return path.join( pkgDir, pkg.module || pkg.main );
+}
+
+// ── Shared config ───────────────────────────────────────────────────────────
+
+const sharedConfig = {
+ mode: jetpackWebpackConfig.mode,
+ devtool: jetpackWebpackConfig.devtool,
+ optimization: {
+ ...jetpackWebpackConfig.optimization,
+ },
+ resolve: {
+ ...jetpackWebpackConfig.resolve,
+ modules: [ path.join( packageRoot, 'node_modules' ), 'node_modules' ],
+ },
+ module: {
+ rules: [
+ // Transpile @wordpress/* packages from node_modules.
+ jetpackWebpackConfig.TranspileRule( {
+ includeNodeModules: [ '@wordpress/' ],
+ } ),
+ ],
+ },
+};
+
+// Plugins disabled for all polyfill builds: no CSS is bundled, and the i18n
+// loader/checker are unnecessary since Core doesn't provide translations for
+// these packages (they aren't shipped with Core in the first place).
+const disabledPlugins = {
+ MiniCssExtractPlugin: false,
+ MiniCssWithRtlPlugin: false,
+ WebpackRtlPlugin: false,
+ I18nLoaderPlugin: false,
+ I18nCheckPlugin: false,
+};
+
+// ── Polyfill definitions ────────────────────────────────────────────────────
+
+const classicPolyfills = [
+ {
+ name: 'notices',
+ packageName: '@wordpress/notices',
+ library: [ 'wp', 'notices' ],
+ },
+ {
+ name: 'private-apis',
+ packageName: '@wordpress/private-apis',
+ library: [ 'wp', 'privateApis' ],
+ },
+ {
+ name: 'theme',
+ packageName: '@wordpress/theme',
+ library: [ 'wp', 'theme' ],
+ },
+];
+
+const modulePolyfills = [
+ { name: 'boot', packageName: '@wordpress/boot' },
+ { name: 'route', packageName: '@wordpress/route' },
+ {
+ name: 'a11y',
+ packageName: '@wordpress/a11y',
+ // a11y's wpScriptModuleExports points to a separate module entry.
+ subEntry: 'build-module/module/index.mjs',
+ },
+];
+
+// ── IIFE configs (classic scripts) ──────────────────────────────────────────
+//
+// Uses @wordpress/dependency-extraction-webpack-plugin (via StandardPlugins)
+// for externals handling and .asset.php generation. The requestMap prevents
+// externalization of the package being polyfilled so webpack bundles it.
+
+const iifeConfigs = classicPolyfills.map( polyfill => ( {
+ name: `script-${ polyfill.name }`,
+ ...sharedConfig,
+ entry: {
+ index: resolveEntry( polyfill.packageName ),
+ },
+ output: {
+ ...jetpackWebpackConfig.output,
+ path: path.join( outputBase, 'scripts', polyfill.name ),
+ library: {
+ name: polyfill.library,
+ type: 'window',
+ },
+ },
+ plugins: [
+ ...jetpackWebpackConfig.StandardPlugins( {
+ DependencyExtractionPlugin: {
+ requestMap: {
+ [ polyfill.packageName ]: { external: false },
+ },
+ },
+ ...disabledPlugins,
+ } ),
+ ],
+} ) );
+
+// ── PolyfillModulePlugin ────────────────────────────────────────────────────
+//
+// Custom webpack plugin for ESM polyfill builds. Handles:
+// 1. Externals via webpack.ExternalsPlugin — script module deps become
+// `import @wordpress/xxx`, classic-only deps become `var wp.xxx`.
+// 2. Dependency tracking — separates classic script handles (dependencies)
+// from module IDs (module_dependencies).
+// 3. Asset generation — writes .asset.php in the same format as
+// @wordpress/dependency-extraction-webpack-plugin.
+
+// Vendor externals (same mapping as @wordpress/dependency-extraction-webpack-plugin).
+const VENDOR_EXTERNALS = {
+ react: { global: 'React', handle: 'react' },
+ 'react-dom': { global: 'ReactDOM', handle: 'react-dom' },
+ 'react/jsx-runtime': { global: 'ReactJSXRuntime', handle: 'react-jsx-runtime' },
+ 'react/jsx-dev-runtime': { global: 'ReactJSXRuntime', handle: 'react-jsx-runtime' },
+ moment: { global: 'moment', handle: 'moment' },
+ lodash: { global: 'lodash', handle: 'lodash' },
+ 'lodash-es': { global: 'lodash', handle: 'lodash' },
+ jquery: { global: 'jQuery', handle: 'jquery' },
+};
+
+class PolyfillModulePlugin {
+ constructor( { skipPackage } ) {
+ this.skipPackage = skipPackage;
+ }
+
+ apply( compiler ) {
+ const { webpack } = compiler;
+ const { RawSource } = webpack.sources;
+
+ const scriptDeps = new Set();
+ const moduleDeps = new Map();
+
+ // Track dynamic import() requests via the parser so we can
+ // distinguish them from static imports in the externals callback
+ // (webpack 5's externals API reports 'esm' for both).
+ const dynamicImportRequests = new Set();
+ compiler.hooks.compilation.tap(
+ 'PolyfillModulePlugin',
+ ( compilation, { normalModuleFactory } ) => {
+ // Clear stale deps from previous compilations (watch mode).
+ scriptDeps.clear();
+ moduleDeps.clear();
+ dynamicImportRequests.clear();
+
+ const handler = parser => {
+ parser.hooks.importCall.tap( 'PolyfillModulePlugin', expr => {
+ // expr.source is the argument to import(). For string
+ // literals, extract the value.
+ if ( expr.source && expr.source.type === 'Literal' ) {
+ dynamicImportRequests.add( expr.source.value );
+ }
+ } );
+ };
+ normalModuleFactory.hooks.parser
+ .for( 'javascript/auto' )
+ .tap( 'PolyfillModulePlugin', handler );
+ normalModuleFactory.hooks.parser
+ .for( 'javascript/esm' )
+ .tap( 'PolyfillModulePlugin', handler );
+ }
+ );
+
+ // Register externals.
+ new webpack.ExternalsPlugin( 'import', ( { request }, callback ) => {
+ // Don't externalize the package being polyfilled.
+ if ( request === this.skipPackage || request.startsWith( this.skipPackage + '/' ) ) {
+ return callback();
+ }
+
+ // @wordpress/* packages.
+ if ( request.startsWith( '@wordpress/' ) ) {
+ const pkgName = request.split( '/' ).slice( 0, 2 ).join( '/' );
+ const shortName = request.split( '/' )[ 1 ];
+ const pkg = readPackageJson( pkgName );
+
+ // Prefer script module for ESM builds.
+ if ( pkg && hasScriptModuleExport( pkg ) ) {
+ const kind = dynamicImportRequests.has( request ) ? 'dynamic' : 'static';
+ if ( kind === 'static' || ! moduleDeps.has( request ) ) {
+ moduleDeps.set( request, kind );
+ }
+ return callback( null, `import ${ request }` );
+ }
+
+ // If package is resolved and has neither wpScriptModuleExports
+ // nor wpScript, let webpack bundle it (e.g. @wordpress/admin-ui,
+ // @wordpress/icons).
+ if ( pkg && ! pkg.wpScript ) {
+ return callback();
+ }
+
+ // Classic script (includes unresolvable packages, which default
+ // to classic since most @wordpress/* packages are classic scripts).
+ scriptDeps.add( `wp-${ shortName }` );
+ return callback( null, `var wp.${ camelCaseDash( shortName ) }` );
+ }
+
+ // Vendor externals.
+ const vendor = VENDOR_EXTERNALS[ request ];
+ if ( vendor ) {
+ scriptDeps.add( vendor.handle );
+ return callback( null, `var ${ vendor.global }` );
+ }
+
+ // Unknown — let webpack bundle it.
+ return callback();
+ } ).apply( compiler );
+
+ // Generate .asset.php files.
+ compiler.hooks.thisCompilation.tap( 'PolyfillModulePlugin', compilation => {
+ compilation.hooks.processAssets.tap(
+ {
+ name: 'PolyfillModulePlugin',
+ stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ANALYSE,
+ additionalAssets: true,
+ },
+ () => {
+ for ( const [ , entrypoint ] of compilation.entrypoints ) {
+ for ( const chunk of entrypoint.chunks ) {
+ const jsFile = Array.from( chunk.files ).find( f => /\.m?js$/i.test( f ) );
+ if ( ! jsFile ) {
+ continue;
+ }
+
+ const content = compilation.getAsset( jsFile ).source.buffer();
+ const hash = webpack.util
+ .createHash( 'xxhash64' )
+ .update( content )
+ .digest( 'hex' )
+ .slice( 0, 16 );
+
+ const depsPhp = Array.from( scriptDeps )
+ .sort()
+ .map( d => `'${ d }'` )
+ .join( ', ' );
+ const modDepsPhp = Array.from( moduleDeps.entries() )
+ .sort( ( [ a ], [ b ] ) => a.localeCompare( b ) )
+ .map( ( [ id, imp ] ) => `array('id' => '${ id }', 'import' => '${ imp }')` )
+ .join( ', ' );
+
+ const parts = [ `'dependencies' => array(${ depsPhp })` ];
+ if ( moduleDeps.size > 0 ) {
+ parts.push( `'module_dependencies' => array(${ modDepsPhp })` );
+ }
+ parts.push( `'version' => '${ hash }'` );
+
+ const assetContent = ` ( {
+ name: `module-${ polyfill.name }`,
+ ...sharedConfig,
+ module: {
+ rules: [
+ ...sharedConfig.module.rules,
+ // Preserve native dynamic imports in @wordpress/boot so the
+ // browser can resolve route module IDs via the import map.
+ // Without this, webpack replaces `import(variable)` calls with
+ // context-module stubs that always throw "Cannot find module".
+ {
+ test: /[\\/]@wordpress[\\/]boot[\\/]/,
+ enforce: 'post',
+ loader: path.resolve( packageRoot, 'preserve-dynamic-imports-loader.js' ),
+ },
+ ],
+ },
+ entry: {
+ index: resolveEntry( polyfill.packageName, polyfill.subEntry ),
+ },
+ output: {
+ ...jetpackWebpackConfig.output,
+ path: path.join( outputBase, 'modules', polyfill.name ),
+ module: true,
+ chunkFormat: 'module',
+ environment: { module: true },
+ library: { type: 'module' },
+ },
+ experiments: {
+ outputModule: true,
+ },
+ plugins: [
+ ...jetpackWebpackConfig.StandardPlugins( {
+ DependencyExtractionPlugin: false,
+ ...disabledPlugins,
+ } ),
+ new PolyfillModulePlugin( { skipPackage: polyfill.packageName } ),
+ ],
+} ) );
+
+module.exports = [ ...iifeConfigs, ...esmConfigs ];
diff --git a/projects/plugins/jetpack/changelog/fix-polyfill-wp-build-dependencies b/projects/plugins/jetpack/changelog/fix-polyfill-wp-build-dependencies
new file mode 100644
index 000000000000..e93330c1de2e
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/fix-polyfill-wp-build-dependencies
@@ -0,0 +1,3 @@
+Significance: patch
+Type: other
+Comment: Update composer.lock.
diff --git a/projects/plugins/jetpack/composer.lock b/projects/plugins/jetpack/composer.lock
index 3c7b6c365888..b13b6b550ac0 100644
--- a/projects/plugins/jetpack/composer.lock
+++ b/projects/plugins/jetpack/composer.lock
@@ -1493,7 +1493,7 @@
"dist": {
"type": "path",
"url": "../../packages/forms",
- "reference": "50f87f787265e2f589b8ecd51407ffe4013e607f"
+ "reference": "1dd808dbc752ca6926a692969c704fb12fc233bc"
},
"require": {
"automattic/jetpack-admin-ui": "@dev",
@@ -1507,6 +1507,7 @@
"automattic/jetpack-plans": "@dev",
"automattic/jetpack-status": "@dev",
"automattic/jetpack-sync": "@dev",
+ "automattic/jetpack-wp-build-polyfills": "@dev",
"php": ">=7.2"
},
"require-dev": {
@@ -3580,6 +3581,65 @@
"relative": true
}
},
+ {
+ "name": "automattic/jetpack-wp-build-polyfills",
+ "version": "dev-trunk",
+ "dist": {
+ "type": "path",
+ "url": "../../packages/wp-build-polyfills",
+ "reference": "e8095b10b4cb4eac8b26dce82529f158aa5346e0"
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "require-dev": {
+ "automattic/jetpack-changelogger": "@dev",
+ "automattic/jetpack-test-environment": "@dev",
+ "automattic/phpunit-select-config": "@dev",
+ "yoast/phpunit-polyfills": "^4.0.0"
+ },
+ "type": "jetpack-library",
+ "extra": {
+ "autotagger": true,
+ "mirror-repo": "Automattic/jetpack-wp-build-polyfills",
+ "textdomain": "jetpack-wp-build-polyfills",
+ "changelogger": {
+ "link-template": "https://github.com/Automattic/jetpack-wp-build-polyfills/compare/v${old}...v${new}"
+ },
+ "branch-alias": {
+ "dev-trunk": "0.1.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "scripts": {
+ "build-production": [
+ "pnpm run build-production"
+ ],
+ "build-development": [
+ "pnpm run build"
+ ],
+ "phpunit": [
+ "phpunit-select-config phpunit.#.xml.dist --colors=always"
+ ],
+ "test-coverage": [
+ "php -dpcov.directory=. ./vendor/bin/phpunit-select-config phpunit.#.xml.dist --coverage-php \"$COVERAGE_DIR/php.cov\""
+ ],
+ "test-php": [
+ "@composer phpunit"
+ ]
+ },
+ "license": [
+ "GPL-2.0-or-later"
+ ],
+ "description": "Polyfills for WordPress Core packages not available in WP < 7.0",
+ "transport-options": {
+ "relative": true
+ }
+ },
{
"name": "automattic/woocommerce-analytics",
"version": "dev-trunk",
diff --git a/projects/plugins/jetpack/tests/e2e/package.json b/projects/plugins/jetpack/tests/e2e/package.json
index fcef5bb28b3c..78e9b02e90d2 100644
--- a/projects/plugins/jetpack/tests/e2e/package.json
+++ b/projects/plugins/jetpack/tests/e2e/package.json
@@ -3,7 +3,7 @@
"type": "module",
"scripts": {
"allure-report": "allure generate --clean --output ./output/allure-report ./output/allure-results && allure open ./output/allure-report",
- "build": "pnpm jetpack build packages/my-jetpack js-packages/social-logos js-packages/boost-score-api js-packages/components js-packages/number-formatters packages/forms packages/assets packages/connection packages/publicize packages/blaze plugins/jetpack -v --no-pnpm-install --production",
+ "build": "pnpm jetpack build packages/my-jetpack js-packages/social-logos js-packages/boost-score-api js-packages/components js-packages/number-formatters packages/wp-build-polyfills packages/forms packages/assets packages/connection packages/publicize packages/blaze plugins/jetpack -v --no-pnpm-install --production",
"clean": "rm -rf output",
"config:decrypt": "pnpm test-decrypt-default-config && pnpm test-decrypt-config",
"distclean": "rm -rf node_modules",