Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Add WP_Build_Polyfills::predict_registration() to expose the force-replacement decision per polyfill for a given WordPress and Gutenberg version.
144 changes: 97 additions & 47 deletions projects/packages/wp-build-polyfills/src/class-wp-build-polyfills.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,46 +185,9 @@ private static function register_scripts( $scripts, $build_dir, $base_file, $wp_
// Gutenberg cannot be trusted to provide a compatible implementation.
$gutenberg_version = defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : null;

$polyfills = array(
'wp-notices' => array(
'path' => 'notices',
'force_threshold' => '7.0',
// Only force-replace on older WP without Gutenberg: older Core
// versions ship notices without SnackbarNotices and InlineNotices
// component exports that @wordpress/boot depends on.
),
'wp-private-apis' => array(
'path' => 'private-apis',
'force_threshold' => '7.1',
'gutenberg_min_version' => self::GUTENBERG_PRIVATE_APIS_MIN_VERSION,
// WP 7.0 and older versions ship private-apis with an incomplete
// allowlist that rejects @wordpress/theme, @wordpress/route, and
// newer dashboard packages. Active Gutenberg is only a safe
// substitute once its private-apis allowlist includes those
// dashboard packages too.
),
'wp-rich-text' => array(
'path' => 'rich-text',
'force_threshold' => '7.1',
'gutenberg_min_version' => self::GUTENBERG_RICH_TEXT_MIN_VERSION,
// WP 7.0 and older ship a rich-text whose `privateApis` current
// dashboard dependencies cannot use (e.g. @wordpress/dataviews
// >= 17.2 dataform controls, which unlock it at module scope).
// WP 6.9 exports no `privateApis` at all, which throws "Cannot
// unlock an undefined object"; WP 7.0 exports one locked with
// only `useRichText`, so destructuring the other keys yields
// undefined. Either way the page blanks. Older Gutenberg is not
// a safe substitute either — see the constant's doc.
),
'wp-theme' => array(
'path' => 'theme',
),
'wp-views' => array(
'path' => 'views',
),
);
$wp_version = $GLOBALS['wp_version'] ?? '0';

foreach ( $polyfills as $handle => $data ) {
foreach ( self::script_policies() as $handle => $data ) {
if ( ! isset( self::$requested[ $handle ] ) ) {
continue;
}
Expand All @@ -235,14 +198,7 @@ private static function register_scripts( $scripts, $build_dir, $base_file, $wp_
continue;
}

$force_threshold = $data['force_threshold'] ?? null;
if ( null !== $force_threshold && version_compare( $wp_version_threshold, $force_threshold, '>' ) ) {
$force_threshold = $wp_version_threshold;
}

$force = null !== $force_threshold
&& ! self::is_gutenberg_version_safe( $data['gutenberg_min_version'] ?? null, $gutenberg_version )
&& version_compare( $GLOBALS['wp_version'] ?? '0', $force_threshold, '<' );
$force = self::should_force( $data, $wp_version, $gutenberg_version, $wp_version_threshold );

if ( ! $force && $scripts->query( $handle, 'registered' ) ) {
continue;
Expand Down Expand Up @@ -282,6 +238,100 @@ private static function register_scripts( $scripts, $build_dir, $base_file, $wp_
}
}

/**
* Predicts how each polyfill registers on a given WordPress and Gutenberg version.
*
* `force` replaces the copy Core or Gutenberg registered under the handle;
* `fallback` only registers when nobody else did. Script modules are always
* `fallback`: `wp_register_script_module()` keeps the first registration.
*
* @since $$next-version$$
*
* @param string $wp_version WordPress version to evaluate.
* @param string|null $gutenberg_version Gutenberg plugin version, or null when it is inactive.
* @param string $wp_version_threshold WordPress version below which force-replacements apply. Defaults to '7.0'.
* @return array<string, string> Script handle or module ID => 'force' | 'fallback'.
*/
public static function predict_registration( $wp_version, $gutenberg_version = null, $wp_version_threshold = '7.0' ) {
$modes = array();
foreach ( self::script_policies() as $handle => $policy ) {
$modes[ $handle ] = self::should_force( $policy, $wp_version, $gutenberg_version, $wp_version_threshold ) ? 'force' : 'fallback';
}
foreach ( self::MODULE_IDS as $module_id ) {
$modes[ $module_id ] = 'fallback';
}

return $modes;
}

/**
* Registration policy per polyfilled classic script.
*
* @return array<string, array{path: string, force_threshold?: string, gutenberg_min_version?: string}>
*/
private static function script_policies() {
return array(
'wp-notices' => array(
'path' => 'notices',
'force_threshold' => '7.0',
// Only force-replace on older WP without Gutenberg: older Core
// versions ship notices without SnackbarNotices and InlineNotices
// component exports that @wordpress/boot depends on.
),
'wp-private-apis' => array(
'path' => 'private-apis',
'force_threshold' => '7.1',
'gutenberg_min_version' => self::GUTENBERG_PRIVATE_APIS_MIN_VERSION,
// WP 7.0 and older versions ship private-apis with an incomplete
// allowlist that rejects @wordpress/theme, @wordpress/route, and
// newer dashboard packages. Active Gutenberg is only a safe
// substitute once its private-apis allowlist includes those
// dashboard packages too.
),
'wp-rich-text' => array(
'path' => 'rich-text',
'force_threshold' => '7.1',
'gutenberg_min_version' => self::GUTENBERG_RICH_TEXT_MIN_VERSION,
// WP 7.0 and older ship a rich-text whose `privateApis` current
// dashboard dependencies cannot use (e.g. @wordpress/dataviews
// >= 17.2 dataform controls, which unlock it at module scope).
// WP 6.9 exports no `privateApis` at all, which throws "Cannot
// unlock an undefined object"; WP 7.0 exports one locked with
// only `useRichText`, so destructuring the other keys yields
// undefined. Either way the page blanks. Older Gutenberg is not
// a safe substitute either — see the constant's doc.
),
'wp-theme' => array(
'path' => 'theme',
),
'wp-views' => array(
'path' => 'views',
),
);
}

/**
* Whether a polyfill must replace an existing registration.
*
* @param array $policy Entry from script_policies().
* @param string $wp_version WordPress version to evaluate.
* @param string|null $gutenberg_version Gutenberg plugin version, or null when it is inactive.
* @param string $wp_version_threshold WordPress version below which force-replacements apply.
* @return bool
*/
private static function should_force( $policy, $wp_version, $gutenberg_version, $wp_version_threshold ) {
$force_threshold = $policy['force_threshold'] ?? null;
if ( null === $force_threshold ) {
return false;
}
if ( version_compare( $wp_version_threshold, $force_threshold, '>' ) ) {
$force_threshold = $wp_version_threshold;
}

return ! self::is_gutenberg_version_safe( $policy['gutenberg_min_version'] ?? null, $gutenberg_version )
&& version_compare( $wp_version, $force_threshold, '<' );
}

/**
* Check whether the active Gutenberg plugin can satisfy a forced script.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -990,4 +990,97 @@ public function test_register_scripts_only_registers_requested_handles() {
$this->assertFalse( $scripts->query( 'wp-private-apis', 'registered' ) );
$this->assertFalse( $scripts->query( 'wp-theme', 'registered' ) );
}

/**
* Data provider for test_predict_registration.
*
* @return array[]
*/
public static function provide_predict_registration() {
return array(
'WP 6.9, no Gutenberg' => array(
'6.9.1',
null,
'7.0',
array(
'wp-notices' => 'force',
'wp-private-apis' => 'force',
'wp-rich-text' => 'force',
),
),
'WP 7.0.4, no Gutenberg' => array(
'7.0.4',
null,
'7.0',
array(
'wp-notices' => 'fallback',
'wp-private-apis' => 'force',
'wp-rich-text' => 'force',
),
),
'WP 7.1, no Gutenberg' => array(
'7.1',
null,
'7.0',
array(
'wp-notices' => 'fallback',
'wp-private-apis' => 'fallback',
'wp-rich-text' => 'fallback',
),
),
'WP 7.0.4, Gutenberg 23.4.0' => array(
'7.0.4',
'23.4.0',
'7.0',
array(
'wp-private-apis' => 'force',
'wp-rich-text' => 'force',
),
),
'WP 7.0.4, Gutenberg 23.5.0' => array(
'7.0.4',
'23.5.0',
'7.0',
array(
'wp-private-apis' => 'fallback',
'wp-rich-text' => 'force',
),
),
'WP 7.0.4, Gutenberg 23.6.0' => array(
'7.0.4',
'23.6.0',
'7.0',
array(
'wp-private-apis' => 'fallback',
'wp-rich-text' => 'fallback',
),
),
'WP 6.9, Gutenberg 23.8.0' => array( '6.9.1', '23.8.0', '7.0', array( 'wp-notices' => 'fallback' ) ),
'consumer threshold 7.1' => array( '7.0.4', null, '7.1', array( 'wp-notices' => 'force' ) ),
);
}

/**
* Test predict_registration() against the force rules.
*
* @dataProvider provide_predict_registration
*
* @param string $wp_version WordPress version.
* @param string|null $gutenberg_version Gutenberg version or null.
* @param string $threshold Consumer threshold.
* @param array $expected Expected modes for a subset of handles.
*/
#[DataProvider( 'provide_predict_registration' )]
public function test_predict_registration( $wp_version, $gutenberg_version, $threshold, $expected ) {
$modes = WP_Build_Polyfills::predict_registration( $wp_version, $gutenberg_version, $threshold );

foreach ( $expected as $handle => $mode ) {
$this->assertSame( $mode, $modes[ $handle ], $handle );
}
$this->assertSame( 'fallback', $modes['wp-theme'] );
$this->assertSame( 'fallback', $modes['wp-views'] );
foreach ( WP_Build_Polyfills::MODULE_IDS as $module_id ) {
$this->assertSame( 'fallback', $modes[ $module_id ], $module_id );
}
}
}
1 change: 1 addition & 0 deletions projects/plugins/debug-helper/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/vendor/
/.phpunit.cache/
21 changes: 21 additions & 0 deletions projects/plugins/debug-helper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,24 @@ There are two mockers implemented:
#### Adding a Custom Mocker
1. Create a class implementing `Automattic\Jetpack\Debug_Helper\Mocker\Runner_Interface` that mocks the data.
2. Add it into the list in `Automattic\Jetpack\Debug_Helper\Mocker::$runners`.

### Package Provenance

Shows which runtime serves each WordPress package on the current admin screen: WordPress core, the Gutenberg plugin, Jetpack's wp-build-polyfills, or another plugin. A badge in the admin bar (`WP x · GB y`) opens a panel listing every registered `wp-*` script and script module with its provider, the plugin tree the file ships from, and its version.

With the module active, a WP-CLI command predicts the same table for WordPress and Gutenberg versions the site is not running, and checks whether the private-apis copy that would win accepts the module names the site's bundles opt in with:

```
wp jetpack-debug provenance predict --wp=7.0.4,7.1 --gutenberg=off,23.8.0
wp jetpack-debug provenance predict --wp=7.1 --gutenberg=/tmp/gutenberg.zip --format=json
```

`--wp` takes release versions or `trunk` (read from the WordPress/WordPress mirror on GitHub; the running version is read from disk). `--gutenberg` takes `off`, `active`, a release version (downloaded from GitHub Releases), or a path to a plugin zip or directory. Downloads are cached in the system temp directory; `--refresh` bypasses the cache. The command exits with status 1 when any cell rejects an opt-in, so it can gate a script.

To evaluate another checkout (a branch that bumps the bundled `@wordpress/*` packages, say) without switching sites, point `--polyfills` at that checkout's `projects/packages/wp-build-polyfills` and `--plugins` at its built plugins. In the monorepo Docker environment, mount the other worktree through `tools/docker/jetpack-docker-config.yml`:

```
wp jetpack-debug provenance predict --wp=7.0.4,7.1 --gutenberg=off,23.8.0 \
--polyfills=/usr/local/src/other-worktree/projects/packages/wp-build-polyfills \
--plugins=/usr/local/src/other-worktree/projects/plugins/jetpack,/usr/local/src/other-worktree/projects/plugins/premium-analytics
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Package Provenance: Add a WP-CLI command that predicts which runtime serves each polyfilled package on a given WordPress and Gutenberg version, and whether the site's bundles would be rejected by the winning private-apis allowlist.
16 changes: 16 additions & 0 deletions projects/plugins/debug-helper/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@
"type": "wordpress-plugin",
"license": "GPL-2.0-or-later",
"require": {},
"require-dev": {
"yoast/phpunit-polyfills": "^4.0.0",
"automattic/phpunit-select-config": "@dev",
"automattic/jetpack-wp-build-polyfills": "@dev"
},
"scripts": {
"phpunit": [
"phpunit-select-config phpunit.#.xml.dist --colors=always"
],
"test-php": [
"@composer phpunit"
],
"test-php-coverage": [
"php -dpcov.directory=. ./vendor/bin/phpunit-select-config phpunit.#.xml.dist --coverage-php \"$COVERAGE_DIR/php.cov\""
]
},
"repositories": [
{
"type": "path",
Expand Down
Loading
Loading