diff --git a/.ddev/homeadditions/bin/generate-composer-json b/.ddev/homeadditions/bin/generate-composer-json index 554d18dc45e5f96de1f7dcc0a5f19ceb646be2c8..0b9a99ce3de48a399d53a15f48b6ad4ab24e3a8b 100755 --- a/.ddev/homeadditions/bin/generate-composer-json +++ b/.ddev/homeadditions/bin/generate-composer-json @@ -10,10 +10,27 @@ $read_json = function (string $file): array { return json_decode($data, TRUE, flags: JSON_THROW_ON_ERROR); }; -$data = array_merge_recursive( - $read_json('project_template/composer.json'), - $read_json('dev.composer.json'), -); +// From \Drupal\Component\Utility\NestedArray::mergeDeep(). +$merge_deep = function (array ...$arrays) use (&$merge_deep): array { + $result = []; + foreach ($arrays as $array) { + foreach ($array as $key => $value) { + // Recurse when both values are arrays. + if (isset($result[$key]) && is_array($result[$key]) && is_array($value)) { + $result[$key] = $merge_deep($result[$key], $value); + } + // Otherwise, use the latter value, overriding any previous value. + else { + $result[$key] = $value; + } + } + } + return $result; +}; + +$base = $read_json('project_template/composer.json'); +$dev = $read_json('dev.composer.json'); +$data = $merge_deep($base, $dev); // If in a CI environment, make all path repository URLs absolute. if (getenv('CI') || getenv('TUGBOAT_PREVIEW')) { diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f2fd78c9e35a86197d41113996c7c4b8f460607f..29a3a8282f227923a70b039316d4e9fb5ea91e6a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -111,9 +111,6 @@ build test project: - *create-project # Generate `composer.json` by merging our dev requirements into the project template. - .ddev/homeadditions/bin/generate-composer-json > $BUILD_DIR/composer.json - # @todo Remove this line when XB is one of our production dependencies and - # the demo page has been moved into the `drupal_cms_starter` recipe. - - mv xb_page.yml $BUILD_DIR # Install dependencies. - composer install --working-dir=$BUILD_DIR # Remove all `.git` directories in the built project. diff --git a/.tugboat/config.yml b/.tugboat/config.yml index ddef083f35bc3cd01ef03b8d5327ed38ec868579..e91abf7b7234579927c2a5382bb2b77805f0bb90 100644 --- a/.tugboat/config.yml +++ b/.tugboat/config.yml @@ -17,9 +17,6 @@ services: - composer create-project drupal/cms $TUGBOAT_ROOT/project --stability=dev --no-install # Generate `composer.json` by merging our dev requirements into the project template. - .ddev/homeadditions/bin/generate-composer-json > $TUGBOAT_ROOT/project/composer.json - # TODO: remove this when https://www.drupal.org/project/drupal_cms/issues/3497781 lands - # Temporary workaround to bypass scaffolding the XB demo - - composer config --working-dir=$TUGBOAT_ROOT/project --unset extra.drupal-scaffold.file-mapping # Run composer install with the new composer.json - composer install --working-dir=$TUGBOAT_ROOT/project --optimize-autoloader # Symlink the Drupal root to the Apache web root. diff --git a/dev.composer.json b/dev.composer.json index 811994147f8f76422bba3afd1e40434aa51f598e..35513c1c44025137923912380107bdc6ecb2576f 100644 --- a/dev.composer.json +++ b/dev.composer.json @@ -99,12 +99,15 @@ "starter": { "type": "path", "url": "recipes/drupal_cms_starter" + }, + "xb_demo": { + "type": "path", + "url": "project_template/recipes/drupal_cms_xb_demo" } }, "require-dev": { "drupal/core-dev": "^11.1.1", - "drupal/default_content": "^2", - "drupal/experience_builder": "0.x-dev" + "drupal/default_content": "^2" }, "autoload-dev": { "files": [ @@ -123,11 +126,6 @@ } }, "extra": { - "drupal-scaffold": { - "file-mapping": { - "[web-root]/modules/contrib/experience_builder/content/xb_page/xb_page.yml": "xb_page.yml" - } - }, "patches": { "drupal/core": { "#3481164: Announcements Feed breaks `test-site.php install`": "https://www.drupal.org/files/issues/2024-10-16/3481164.patch" diff --git a/project_template/composer.json b/project_template/composer.json index 71b2d84f6d941856d5261a5094863c1516f36ae3..8509bcc47c6678c3f0ad1aea6b5b1892167d3745 100644 --- a/project_template/composer.json +++ b/project_template/composer.json @@ -12,6 +12,10 @@ "drupal": { "type": "composer", "url": "https://packages.drupal.org/8" + }, + "xb_demo": { + "type": "path", + "url": "recipes/drupal_cms_xb_demo" } }, "require": { @@ -31,6 +35,7 @@ "drupal/drupal_cms_person": "*", "drupal/drupal_cms_project": "*", "drupal/drupal_cms_seo_tools": "*", + "drupal/drupal_cms_xb_demo": "@dev", "drush/drush": "^13" }, "conflict": { @@ -43,6 +48,7 @@ "composer/installers": true, "drupal/core-composer-scaffold": true, "drupal/core-project-message": true, + "drupal/drupal_cms_xb_demo": true, "php-http/discovery": true }, "sort-packages": true, diff --git a/project_template/recipes/drupal_cms_xb_demo/composer.json b/project_template/recipes/drupal_cms_xb_demo/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..b0198a3649069f6228dae64b851b2f0db1210189 --- /dev/null +++ b/project_template/recipes/drupal_cms_xb_demo/composer.json @@ -0,0 +1,18 @@ +{ + "name": "drupal/drupal_cms_xb_demo", + "type": "composer-plugin", + "description": "A read-only demonstration of the next-generation Experience Builder for Drupal.", + "require": { + "composer-plugin-api": "^2", + "drupal/experience_builder": "0.x-dev", + "drush/drush": "^13" + }, + "autoload": { + "classmap": ["src/"] + }, + "extra": { + "class": "Drupal\\XbDemo\\Plugin", + "plugin-optional": true + }, + "version": "dev-main" +} diff --git a/xb_page.yml b/project_template/recipes/drupal_cms_xb_demo/content/xb_page.yml similarity index 100% rename from xb_page.yml rename to project_template/recipes/drupal_cms_xb_demo/content/xb_page.yml diff --git a/project_template/recipes/drupal_cms_xb_demo/recipe.yml b/project_template/recipes/drupal_cms_xb_demo/recipe.yml new file mode 100644 index 0000000000000000000000000000000000000000..d920bc42a4905a5748ffa946633cf089106430c6 --- /dev/null +++ b/project_template/recipes/drupal_cms_xb_demo/recipe.yml @@ -0,0 +1,13 @@ +name: Experience Builder +type: Drupal CMS +description: A read-only demonstration of the next-generation Experience Builder for Drupal. +install: + - experience_builder +config: + import: + experience_builder: + - image.style.xb_avatar + actions: + experience_builder.settings: + simpleConfigUpdate: + demo_mode: true diff --git a/project_template/recipes/drupal_cms_xb_demo/src/Plugin.php b/project_template/recipes/drupal_cms_xb_demo/src/Plugin.php new file mode 100644 index 0000000000000000000000000000000000000000..72b9841d5f21e1aacd9c63e2c9d54e087f1c84bc --- /dev/null +++ b/project_template/recipes/drupal_cms_xb_demo/src/Plugin.php @@ -0,0 +1,118 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\XbDemo; + +use Composer\Composer; +use Composer\DependencyResolver\Operation\UpdateOperation; +use Composer\EventDispatcher\EventSubscriberInterface; +use Composer\InstalledVersions; +use Composer\Installer\PackageEvent; +use Composer\Installer\PackageEvents; +use Composer\IO\IOInterface; +use Composer\Plugin\PluginInterface; +use Composer\Semver\VersionParser; +use Symfony\Component\Process\Process; + +/** + * Uninstalls Experience Builder whenever Composer attempts to update it. + * + * Experience Builder currently has no update path, which means anyone who has + * it installed in demo mode could theoretically, suddenly break their site if + * Experience Builder ships a breaking change. To prevent that scenario, this + * plugin will uninstall Experience Builder and delete all of its data before + * the Composer package is updated. + */ +final class Plugin implements PluginInterface, EventSubscriberInterface { + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents(): array { + return [ + PackageEvents::PRE_PACKAGE_UPDATE => 'onPackageUpdate', + ]; + } + + /** + * Uninstalls Experience Builder before it is updated. + * + * @param \Composer\Installer\PackageEvent $event + * The event object. + */ + public function onPackageUpdate(PackageEvent $event): void { + $operation = $event->getOperation(); + + // We only need to uninstall XB if we're updating it from any version less + // than 1.0.0-beta1. + if ($operation instanceof UpdateOperation) { + $from = $operation->getInitialPackage(); + + if ($from->getName() === 'drupal/experience_builder' && version_compare($from->getVersion(), '1.0.0-beta1', '<')) { + $drush = $event->getComposer()->getConfig()->get('bin-dir') . '/drush'; + assert(is_executable($drush)); + + $io = $event->getIO(); + $io->write('Uninstalling Experience Builder because it is being updated from an unstable version.'); + $this->uninstallXb(function (array $command) use ($drush): Process { + $process = new Process([$drush, ...$command]); + return $process->mustRun(); + }); + + $io->write('Successfully uninstalled Experience Builder. You can reinstall the demo by running the following command:'); + $io->write("$drush recipe " . InstalledVersions::getInstallPath('drupal/drupal_cms_xb_demo')); + } + } + } + + /** + * Deletes all of Experience Builder's data and uninstalls it. + * + * @param callable $drush + * A callable that runs Drush. Accepts an array of command-line arguments + * and options, and returns a Process object that has run successfully. + */ + private function uninstallXb(callable $drush): void { + // Ensure that Drupal is installed and has a database connection; otherwise, + // there's nothing to do. + $output = $drush(['core:status', '--field=db-status'])->getOutput(); + $output = trim($output); + if (empty($output)) { + return; + } + + // Ask Drush if Experience Builder is installed. + $output = $drush([ + 'pm:list', + '--type=module', + '--field=status', + '--filter=experience_builder', + ])->getOutput(); + + if (trim($output) === 'Enabled') { + // Delete all xb_page entities and uninstall XB. + $drush(['entity:delete', 'xb_page']); + $drush(['pm:uninstall', 'experience_builder', '--yes']); + } + } + + /** + * {@inheritdoc} + */ + public function activate(Composer $composer, IOInterface $io): void { + } + + /** + * {@inheritdoc} + */ + public function deactivate(Composer $composer, IOInterface $io): void { + } + + /** + * {@inheritdoc} + */ + public function uninstall(Composer $composer, IOInterface $io): void { + } + +} diff --git a/project_template/web/profiles/drupal_cms_installer/drupal_cms_installer.profile b/project_template/web/profiles/drupal_cms_installer/drupal_cms_installer.profile index d514205d985d9481b6418f6f3300f4293dc90a0f..4973967bdc4669a834d56b6e6c706aa03a7e8b6b 100644 --- a/project_template/web/profiles/drupal_cms_installer/drupal_cms_installer.profile +++ b/project_template/web/profiles/drupal_cms_installer/drupal_cms_installer.profile @@ -255,14 +255,11 @@ function drupal_cms_installer_apply_recipes(array &$install_state): array { $batch = install_profile_modules($install_state); $batch['title'] = t('Setting up your site'); - ['install_path' => $cookbook_path] = InstalledVersions::getRootPackage(); - $cookbook_path .= '/recipes'; - $recipe_operations = []; - foreach ($recipes_to_apply as $recipe) { + foreach ($recipes_to_apply as $name) { $recipe = RecipeLoader::load( - $cookbook_path . '/' . $recipe, + InstalledVersions::getInstallPath('drupal/' . $name), // Only save a cached copy of the recipe if this environment variable is // set. This allows us to ship a pre-primed cache of recipes to improve // installer performance for first-time users.