Automatically Deactivate Merged Packages (#41956)

Once a feature plugin has been merged it is no longer necessary for it
to be activated. In some cases, having it activated can actually
lead to problems. This will automatically disable any packages
that we have marked as "merged".
This commit is contained in:
Christopher Allford 2023-12-08 10:38:47 -08:00 committed by GitHub
parent e4f67f9f10
commit 58b692c3a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 2823 additions and 39 deletions

View File

@ -0,0 +1,24 @@
{
"name": "woocommerce/monorepo-plugin",
"type": "composer-plugin",
"require": {
"composer-plugin-api": "^2.0"
},
"require-dev": {
"composer/composer": "^2.0",
"wp-coding-standards/wpcs": "^3.0"
},
"autoload": {
"psr-4": {
"Automattic\\WooCommerce\\Monorepo\\Composer\\": "src/"
}
},
"extra": {
"class": "Automattic\\WooCommerce\\Monorepo\\Composer\\Plugin"
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}

2454
packages/php/monorepo-plugin/composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,21 @@
<?xml version="1.0"?>
<ruleset name="WordPress Coding Standards">
<file>src/</file>
<!-- Show progress, show the error codes for each message (source). -->
<arg value="ps" />
<!-- Check up to 8 files simultaneously. -->
<arg name="parallel" value="8" />
<!-- Rules -->
<rule ref="WordPress-Core" />
<!-- Support PSR-4 -->
<rule ref="WordPress.Files.FileName.NotHyphenatedLowercase">
<exclude-pattern>src/</exclude-pattern>
</rule>
<rule ref="WordPress.Files.FileName.InvalidClassFileName">
<exclude-pattern>src/</exclude-pattern>
</rule>
</ruleset>

View File

@ -0,0 +1,107 @@
<?php
/**
* A class for handling the overwriting of version strings in the Jetpack Autoloader for merged feature plugins.
*
* @package woocommerce/monorepo-plugin
*/
namespace Automattic\WooCommerce\Monorepo\Composer;
use Composer\Composer;
use Composer\IO\IOInterface;
use Composer\Package\RootPackageInterface;
use Composer\Util\Filesystem;
use RuntimeException;
/**
* Class JetpackManifestEditor.
*
* @package woocommerce/monorepo-plugin
*/
class JetpackManifestEditor {
/**
* The Composer instance to use.
*
* @var Composer
*/
private $composer;
/**
* The IO interface to use.
*
* @var IOInterface
*/
private $io;
/**
* The Filesystem instance to use.
*
* @var Filesystem
*/
private $filesystem;
/**
* Constructor.
*
* @param IOInterface $io The IO interface to use.
*/
public function __construct( Composer $composer, IOInterface $io ) {
$this->composer = $composer;
$this->io = $io;
$this->filesystem = new Filesystem();
}
/**
* Updates the autoload manifests for the given package.
*
* @param RootPackageInterface $root_package The package to change the manifests for.
*/
public function update_autoload_manifests( RootPackageInterface $root_package ) {
// All of the manifest files are in the vendor/composer directory.
$manifest_dir = $this->composer->getConfig()->get( 'vendor-dir' ) . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR;
// Add an offset to the version string so that we can still tell
// the difference between subsequent versions of an autoload.
$new_version = $root_package->getVersion();
if ( preg_match( '/^([\\d.]+)?(.*)$/', $new_version, $version_parts ) ) {
$new_version = '10' . $version_parts[1] . $version_parts[2];
}
if ( $root_package->getVersion() === $new_version ) {
return;
}
$this->io->write( "<info>Updating Merged Feature Plugin Autoloads ($manifest_dir)</info>" );
// Update all of the manifest files.
$this->update_manifest_versions( $manifest_dir, 'jetpack_autoload_classmap.php', $new_version );
$this->update_manifest_versions( $manifest_dir, 'jetpack_autoload_psr4.php', $new_version );
$this->update_manifest_versions( $manifest_dir, 'jetpack_autoload_filemap.php', $new_version );
}
/**
* Updates the versions for local autoloads in the manifest file.
*
* @param string $manifest_dir The directory that holds the manifest files.
* @param string $file The manifest file to update.
* @param string $new_version The new version to set into the manifest file.
*/
private function update_manifest_versions( $manifest_dir, $file, $new_version ) {
$manifest_content = @file_get_contents( $manifest_dir . $file );
if ( ! $manifest_content ) {
return;
}
$this->io->write( "<info>Updating: $file</info>" );
// We should replace anything mapped from the base directory since that's a path we're mapping an autoload for.
$manifest_content = preg_replace(
"/('version'\s+=>\s+')[^']+(',\s+'path'\s+=>\s+(?:array\( )?\\\$baseDir)/",
'${1}' . $new_version . '${2}',
$manifest_content
);
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
@file_put_contents( $manifest_dir . $file, $manifest_content );
}
}

View File

@ -0,0 +1,88 @@
<?php
/**
* Custom Composer plugin to override any standard behavior required within the monorepo.
*
* @package woocommerce/monorepo-plugin
*/
namespace Automattic\WooCommerce\Monorepo\Composer;
use Composer\Composer;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\IO\IOInterface;
use Composer\Plugin\PluginInterface;
use Composer\Script\Event;
use Composer\Script\ScriptEvents;
/**
* Class Plugin.
*
* @package woocommerce/monorepo-plugin
*/
class Plugin implements PluginInterface, EventSubscriberInterface {
/**
* Composer object.
*
* @var Composer Composer object.
*/
private $composer;
/**
* The IO interface to use.
*
* @var IOInterface
*/
private $io;
/**
* Runs when the plugin activates.
*
* @param Composer $composer The composer instance.
* @param IOInterface $io The IO interface.
*/
public function activate( Composer $composer, IOInterface $io ) {
$this->composer = $composer;
$this->io = $io;
}
/**
* Runs when the plugin deactivates.
*
* @param Composer $composer The composer instance.
* @param IOInterface $io The IO interface.
*/
public function deactivate( Composer $composer, IOInterface $io ) { }
/**
* Runs when the plugin is uninstalled.
*
* @param Composer $composer The composer instance.
* @param IOInterface $io The IO interface.
*/
public function uninstall( Composer $composer, IOInterface $io ) { }
/**
* Subscribe to the events that we're interested in.
*/
public static function getSubscribedEvents() {
return array(
// Make sure this event will run after the Jetpack Autoloader plugin.
ScriptEvents::POST_AUTOLOAD_DUMP => array( 'onPostAutoloadDump', -100000 ),
);
}
/**
* An event handler that runs after the autoload dump event.
*
* @param Event $event The event to handle.
*/
public function onPostAutoloadDump( Event $event ) {
$root_package = $this->composer->getPackage();
// Update the Jetpack autoload manifests with a custom string version so that we can ensure they will always be loaded
// preferentially over other packages that may provide the same autoloads.
$manifest_editor = new JetpackManifestEditor( $this->composer, $this->io );
$manifest_editor->update_autoload_manifests( $root_package );
}
}

View File

@ -315,12 +315,12 @@
"version": "3.7.2",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
"url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
"reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879",
"url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879",
"reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879",
"shasum": ""
},
@ -365,6 +365,20 @@
"source": "https://github.com/squizlabs/PHP_CodeSniffer",
"wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
},
"funding": [
{
"url": "https://github.com/PHPCSStandards",
"type": "github"
},
{
"url": "https://github.com/jrfnl",
"type": "github"
},
{
"url": "https://opencollective.com/php_codesniffer",
"type": "open_collective"
}
],
"time": "2023-02-22T23:07:41+00:00"
},
{

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Automatically disable the feature plugins that have been merged into WooCommerce Core.

View File

@ -11,6 +11,10 @@
{
"type": "path",
"url": "lib"
},
{
"type": "path",
"url": "../../packages/php/monorepo-plugin"
}
],
"require": {
@ -31,7 +35,8 @@
"dms/phpunit-arraysubset-asserts": "^0.4.0",
"phpunit/phpunit": "^9.0",
"sebastian/comparator": "^4.0",
"yoast/phpunit-polyfills": "^2.0"
"yoast/phpunit-polyfills": "^2.0",
"woocommerce/monorepo-plugin": "*"
},
"config": {
"optimize-autoloader": true,
@ -47,7 +52,8 @@
"allow-plugins": {
"automattic/jetpack-autoloader": true,
"composer/installers": true,
"bamarni/composer-bin-plugin": true
"bamarni/composer-bin-plugin": true,
"woocommerce/monorepo-plugin": true
}
},
"autoload": {

View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "b6acb0cb84f1203222c10e9a337f2963",
"content-hash": "3dbbf6613e191c5f595184e03bff2d91",
"packages": [
{
"name": "automattic/jetpack-a8c-mc-stats",
@ -3876,6 +3876,34 @@
},
"time": "2021-02-27T15:53:37+00:00"
},
{
"name": "woocommerce/monorepo-plugin",
"version": "dev-add/feature-plugin-disable",
"dist": {
"type": "path",
"url": "../../packages/php/monorepo-plugin",
"reference": "77919ca36196034975a1dac1a670f73731cdf4ac"
},
"require": {
"composer-plugin-api": "^2.0"
},
"require-dev": {
"composer/composer": "^2.0",
"wp-coding-standards/wpcs": "^3.0"
},
"type": "composer-plugin",
"extra": {
"class": "Automattic\\WooCommerce\\Monorepo\\Composer\\Plugin"
},
"autoload": {
"psr-4": {
"Automattic\\WooCommerce\\Monorepo\\Composer\\": "src/"
}
},
"transport-options": {
"relative": true
}
},
{
"name": "yoast/phpunit-polyfills",
"version": "2.0.0",

View File

@ -92,36 +92,9 @@ class Loader {
*/
remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
add_action( 'admin_init', array( __CLASS__, 'deactivate_wc_admin_plugin' ) );
add_action( 'load-themes.php', array( __CLASS__, 'add_appearance_theme_view_tracks_event' ) );
}
/**
* If WooCommerce Admin is installed and activated, it will attempt to deactivate and show a notice.
*/
public static function deactivate_wc_admin_plugin() {
$plugin_path = PluginsHelper::get_plugin_path_from_slug( 'woocommerce-admin' );
if ( is_plugin_active( $plugin_path ) ) {
$path = PluginsHelper::get_plugin_path_from_slug( 'woocommerce-admin' );
deactivate_plugins( $path );
$notice_action = is_network_admin() ? 'network_admin_notices' : 'admin_notices';
add_action(
$notice_action,
function() {
echo '<div class="error"><p>';
printf(
/* translators: %s: is referring to the plugin's name. */
esc_html__( 'The %1$s plugin has been deactivated as the latest improvements are now included with the %2$s plugin.', 'woocommerce' ),
'<code>WooCommerce Admin</code>',
'<code>WooCommerce</code>'
);
echo '</p></div>';
}
);
}
}
/**
* Returns breadcrumbs for the current page.
*/

View File

@ -5,6 +5,8 @@
namespace Automattic\WooCommerce;
use Automattic\Jetpack\Constants;
defined( 'ABSPATH' ) || exit;
/**
@ -20,12 +22,29 @@ class Packages {
private function __construct() {}
/**
* Array of package names and their main package classes.
* Array of package names and their main package classes. Once a package has been merged into WooCommerce
* directly it should be removed from here and added to the merged packages array.
*
* @var array Key is the package name/directory, value is the main package class which handles init.
*/
protected static $packages = array(
'woocommerce-blocks' => '\\Automattic\\WooCommerce\\Blocks\\Package'
protected static $packages = array();
/**
* Array of package names and their main package classes.
*
* One a package has been merged into WooCommerce Core it should be moved fron the package list and placed in
* this list. This will ensure that the feature plugin is disabled as well as provide the class to handle
* initialization for the now-merged feature plugin.
*
* Once a package has been merged into WooCommerce Core it should have its slug added here. This will ensure
* that we deactivate the feature plugin automaticatlly to prevent any problems caused by conflicts between
* the two versions caused by them both being active.
*
* @var array Key is the package name/directory, value is the main package class which handles init.
*/
protected static $merged_packages = array(
'woocommerce-admin' => '\\Automattic\\WooCommerce\\Admin\\Composer\\Package',
'woocommerce-gutenberg-products-block' => '\\Automattic\\WooCommerce\\Blocks\\Package',
);
/**
@ -41,7 +60,8 @@ class Packages {
* Callback for WordPress init hook.
*/
public static function on_init() {
self::load_packages();
self::deactivate_merged_packages();
self::initialize_packages();
}
/**
@ -54,14 +74,59 @@ class Packages {
return file_exists( dirname( __DIR__ ) . '/packages/' . $package );
}
/**
* Deactivates merged feature plugins.
*
* Once a feature plugin is merged into WooCommerce Core it should be deactivated. This method will
* ensure that a plugin gets deactivated. Note that for the first request it will still be active,
* and as such, there may be some odd behavior. This is unlikely to cause any issues though
* because it will be deactivated on the request that updates or activates WooCommerce.
*/
protected static function deactivate_merged_packages() {
// Developers may need to be able to run merged feature plugins alongside merged packages for testing purposes.
if ( Constants::is_true( 'WC_ALLOW_MERGED_FEATURE_PLUGINS' ) ) {
return;
}
// Scroll through all of the active plugins and disable them if they're merged packages.
$active_plugins = get_option( 'active_plugins', array() );
// Deactivate the plugin if possible so that there are no conflicts.
foreach ( $active_plugins as $active_plugin_path ) {
$plugin_file = basename( plugin_basename( $active_plugin_path ), '.php' );
if ( ! isset( self::$merged_packages[ $plugin_file ] ) ) {
continue;
}
require_once ABSPATH . 'wp-admin/includes/plugin.php';
// Make sure to display a message informing the user that the plugin has been deactivated.
$plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $active_plugin_path );
deactivate_plugins( $active_plugin_path );
add_action(
'admin_notices',
function() use ( $plugin_data ) {
echo '<div class="error"><p>';
printf(
/* translators: %s: is referring to the plugin's name. */
esc_html__( 'The %1$s plugin has been deactivated as the latest improvements are now included with the %2$s plugin.', 'woocommerce' ),
'<code>' . esc_html( $plugin_data['Name'] ) . '</code>',
'<code>WooCommerce</code>'
);
echo '</p></div>';
}
);
}
}
/**
* Loads packages after plugins_loaded hook.
*
* Each package should include an init file which loads the package so it can be used by core.
*/
protected static function load_packages() {
// Initialize WooCommerce Admin.
\Automattic\WooCommerce\Admin\Composer\Package::init();
protected static function initialize_packages() {
foreach ( self::$merged_packages as $package_name => $package_class ) {
call_user_func( array( $package_class, 'init' ) );
}
foreach ( self::$packages as $package_name => $package_class ) {
if ( ! self::package_exists( $package_name ) ) {