From 48b0d8c49443efa108578c5eab7c3dec319665fa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Mon, 6 Jan 2025 22:29:16 -0500
Subject: [PATCH 1/8] Add an XB recipe that is part of the project template

---
 .gitlab-ci.yml                                               | 3 ---
 dev.composer.json                                            | 5 -----
 .../recipes/drupal_cms_xb/content/xb_page.yml                | 0
 project_template/recipes/drupal_cms_xb/recipe.yml            | 5 +++++
 recipes/drupal_cms_xb                                        | 1 +
 5 files changed, 6 insertions(+), 8 deletions(-)
 rename xb_page.yml => project_template/recipes/drupal_cms_xb/content/xb_page.yml (100%)
 create mode 100644 project_template/recipes/drupal_cms_xb/recipe.yml
 create mode 120000 recipes/drupal_cms_xb

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index f2fd78c9e..29a3a8282 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/dev.composer.json b/dev.composer.json
index f6e69e99c..a79be0d9b 100644
--- a/dev.composer.json
+++ b/dev.composer.json
@@ -123,11 +123,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/xb_page.yml b/project_template/recipes/drupal_cms_xb/content/xb_page.yml
similarity index 100%
rename from xb_page.yml
rename to project_template/recipes/drupal_cms_xb/content/xb_page.yml
diff --git a/project_template/recipes/drupal_cms_xb/recipe.yml b/project_template/recipes/drupal_cms_xb/recipe.yml
new file mode 100644
index 000000000..7ba892fcd
--- /dev/null
+++ b/project_template/recipes/drupal_cms_xb/recipe.yml
@@ -0,0 +1,5 @@
+name: Experience Builder
+type: Drupal CMS
+description: A read-only demonstration of the next-generation Experience Builder for Drupal.
+install:
+  - experience_builder
diff --git a/recipes/drupal_cms_xb b/recipes/drupal_cms_xb
new file mode 120000
index 000000000..23929511c
--- /dev/null
+++ b/recipes/drupal_cms_xb
@@ -0,0 +1 @@
+../project_template/recipes/drupal_cms_xb
\ No newline at end of file
-- 
GitLab


From 833323c31414e836e7e77e4f59eb43ef1ea2738f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Tue, 7 Jan 2025 11:24:27 -0500
Subject: [PATCH 2/8] Make the recipe a Composer plugin that uninstalls XB
 before it gets updated.

---
 .../homeadditions/bin/generate-composer-json  |  22 +++-
 dev.composer.json                             |   7 +-
 project_template/composer.json                |   6 +
 .../recipes/drupal_cms_xb_demo/composer.json  |  16 +++
 .../content/xb_page.yml                       |   0
 .../recipe.yml                                |   0
 .../recipes/drupal_cms_xb_demo/src/Plugin.php | 109 ++++++++++++++++++
 .../drupal_cms_installer.profile              |   7 +-
 recipes/drupal_cms_xb                         |   1 -
 9 files changed, 156 insertions(+), 12 deletions(-)
 create mode 100644 project_template/recipes/drupal_cms_xb_demo/composer.json
 rename project_template/recipes/{drupal_cms_xb => drupal_cms_xb_demo}/content/xb_page.yml (100%)
 rename project_template/recipes/{drupal_cms_xb => drupal_cms_xb_demo}/recipe.yml (100%)
 create mode 100644 project_template/recipes/drupal_cms_xb_demo/src/Plugin.php
 delete mode 120000 recipes/drupal_cms_xb

diff --git a/.ddev/homeadditions/bin/generate-composer-json b/.ddev/homeadditions/bin/generate-composer-json
index 8762732db..c4f64f609 100755
--- a/.ddev/homeadditions/bin/generate-composer-json
+++ b/.ddev/homeadditions/bin/generate-composer-json
@@ -10,10 +10,24 @@ $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 https://www.php.net/manual/en/function.array-merge-recursive.php#92195
+function array_merge_recursive_distinct(array &$array1, array &$array2): array {
+  $merged = $array1;
+
+  foreach ($array2 as $key => &$value) {
+    if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
+      $merged[$key] = array_merge_recursive_distinct($merged[$key], $value);
+    }
+    else {
+      $merged[$key] = $value;
+    }
+  }
+  return $merged;
+}
+
+$base = $read_json('project_template/composer.json');
+$dev = $read_json('dev.composer.json');
+$data = array_merge_recursive_distinct($base, $dev);
 
 // If in a CI environment, make all path repository URLs absolute.
 if (getenv('CI')) {
diff --git a/dev.composer.json b/dev.composer.json
index ea4bb35de..35513c1c4 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": [
diff --git a/project_template/composer.json b/project_template/composer.json
index 71b2d84f6..0d8a26ad4 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": "*",
         "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 000000000..e07fab53b
--- /dev/null
+++ b/project_template/recipes/drupal_cms_xb_demo/composer.json
@@ -0,0 +1,16 @@
+{
+  "name": "drupal/drupal_cms_xb_demo",
+  "type": "composer-plugin",
+  "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
+  }
+}
diff --git a/project_template/recipes/drupal_cms_xb/content/xb_page.yml b/project_template/recipes/drupal_cms_xb_demo/content/xb_page.yml
similarity index 100%
rename from project_template/recipes/drupal_cms_xb/content/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/recipe.yml b/project_template/recipes/drupal_cms_xb_demo/recipe.yml
similarity index 100%
rename from project_template/recipes/drupal_cms_xb/recipe.yml
rename to project_template/recipes/drupal_cms_xb_demo/recipe.yml
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 000000000..ab6f885d0
--- /dev/null
+++ b/project_template/recipes/drupal_cms_xb_demo/src/Plugin.php
@@ -0,0 +1,109 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\XbDemo;
+
+use Composer\Composer;
+use Composer\DependencyResolver\Operation\UpdateOperation;
+use Composer\EventDispatcher\EventSubscriberInterface;
+use Composer\Installer\PackageEvent;
+use Composer\Installer\PackageEvents;
+use Composer\IO\IOInterface;
+use Composer\Plugin\PluginInterface;
+use Symfony\Component\Process\Process;
+
+final class Plugin implements PluginInterface, EventSubscriberInterface {
+
+  private readonly string $drushPath;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents(): array {
+    return [
+      PackageEvents::PRE_PACKAGE_UPDATE => 'onPackageUpdate',
+    ];
+  }
+
+  /**
+   * Uninstalls Experience Builder before it is updated.
+   */
+  public function onPackageUpdate(PackageEvent $event): void {
+    $package_name = 'drupal/experience_builder';
+
+    $operation = $event->getOperation();
+    if ($operation instanceof UpdateOperation && $operation->getInitialPackage()->getName() === $package_name && $operation->getTargetPackage()->getName() === $package_name) {
+      $this->uninstallXb();
+    }
+  }
+
+  private function uninstallXb(): void {
+    // Ensure that Drupal is installed and has a database connection; otherwise,
+    // there's nothing to do.
+    $process = new Process([$this->drushPath, 'core:status', '--field=db-status']);
+    $output = trim($process->mustRun()->getOutput());
+    if (empty($output)) {
+      return;
+    }
+
+    // Ask Drush if Experience Builder is installed. If it's not, there's
+    // nothing else to do.
+    $process = new Process([
+      $this->drushPath,
+      'pm:list',
+      '--type=module',
+      '--field=status',
+      '--filter=experience_builder',
+    ]);
+    $status = trim($process->mustRun()->getOutput());
+    if ($status === 'Disabled') {
+      return;
+    }
+
+    // Delete all xb_page entities.
+    $process = new Process([
+      $this->drushPath,
+      'entity:delete',
+      'xb_page',
+    ]);
+    $process->mustRun();
+
+    // Delete the demo field storage.
+    $process = new Process([
+      $this->drushPath,
+      'php:eval',
+      'Drupal\field\Entity\FieldStorageConfig::loadByName("node", "field_xb_demo")?->delete();',
+    ]);
+    $process->mustRun();
+
+    // Uninstall Experience Builder.
+    $process = new Process([
+      $this->drushPath,
+      'pm:uninstall',
+      'experience_builder',
+      '--yes',
+    ]);
+    $process->mustRun();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function activate(Composer $composer, IOInterface $io): void {
+    $this->drushPath = $composer->getConfig()->get('bin-dir') . '/drush';
+  }
+
+  /**
+   * {@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 d514205d9..4973967bd 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.
diff --git a/recipes/drupal_cms_xb b/recipes/drupal_cms_xb
deleted file mode 120000
index 23929511c..000000000
--- a/recipes/drupal_cms_xb
+++ /dev/null
@@ -1 +0,0 @@
-../project_template/recipes/drupal_cms_xb
\ No newline at end of file
-- 
GitLab


From e58410854a1afaf222c5be285488c706b8a64ef1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Tue, 7 Jan 2025 11:42:43 -0500
Subject: [PATCH 3/8] Set demo mode

---
 project_template/recipes/drupal_cms_xb_demo/recipe.yml | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/project_template/recipes/drupal_cms_xb_demo/recipe.yml b/project_template/recipes/drupal_cms_xb_demo/recipe.yml
index 7ba892fcd..e1dbe85de 100644
--- a/project_template/recipes/drupal_cms_xb_demo/recipe.yml
+++ b/project_template/recipes/drupal_cms_xb_demo/recipe.yml
@@ -3,3 +3,8 @@ type: Drupal CMS
 description: A read-only demonstration of the next-generation Experience Builder for Drupal.
 install:
   - experience_builder
+config:
+  actions:
+    experience_builder.settings:
+      simpleConfigUpdate:
+        demo_mode: true
-- 
GitLab


From cb36a7e374bc7b61fba87a11dc602ee92f0c174f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Tue, 7 Jan 2025 12:15:42 -0500
Subject: [PATCH 4/8] Ah, okay, just delete xb_pages

---
 .../recipes/drupal_cms_xb_demo/composer.json  |  1 +
 .../recipes/drupal_cms_xb_demo/recipe.yml     |  3 +++
 .../recipes/drupal_cms_xb_demo/src/Plugin.php | 23 ++++++++++++-------
 3 files changed, 19 insertions(+), 8 deletions(-)

diff --git a/project_template/recipes/drupal_cms_xb_demo/composer.json b/project_template/recipes/drupal_cms_xb_demo/composer.json
index e07fab53b..9809cfd66 100644
--- a/project_template/recipes/drupal_cms_xb_demo/composer.json
+++ b/project_template/recipes/drupal_cms_xb_demo/composer.json
@@ -1,6 +1,7 @@
 {
   "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",
diff --git a/project_template/recipes/drupal_cms_xb_demo/recipe.yml b/project_template/recipes/drupal_cms_xb_demo/recipe.yml
index e1dbe85de..d920bc42a 100644
--- a/project_template/recipes/drupal_cms_xb_demo/recipe.yml
+++ b/project_template/recipes/drupal_cms_xb_demo/recipe.yml
@@ -4,6 +4,9 @@ description: A read-only demonstration of the next-generation Experience Builder
 install:
   - experience_builder
 config:
+  import:
+    experience_builder:
+      - image.style.xb_avatar
   actions:
     experience_builder.settings:
       simpleConfigUpdate:
diff --git a/project_template/recipes/drupal_cms_xb_demo/src/Plugin.php b/project_template/recipes/drupal_cms_xb_demo/src/Plugin.php
index ab6f885d0..691e6d10f 100644
--- a/project_template/recipes/drupal_cms_xb_demo/src/Plugin.php
+++ b/project_template/recipes/drupal_cms_xb_demo/src/Plugin.php
@@ -13,6 +13,15 @@ use Composer\IO\IOInterface;
 use Composer\Plugin\PluginInterface;
 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 {
 
   private readonly string $drushPath;
@@ -28,6 +37,9 @@ final class Plugin implements PluginInterface, EventSubscriberInterface {
 
   /**
    * Uninstalls Experience Builder before it is updated.
+   *
+   * @param \Composer\Installer\PackageEvent $event
+   *   The event object.
    */
   public function onPackageUpdate(PackageEvent $event): void {
     $package_name = 'drupal/experience_builder';
@@ -38,6 +50,9 @@ final class Plugin implements PluginInterface, EventSubscriberInterface {
     }
   }
 
+  /**
+   * Deletes all of Experience Builder's data and uninstalls it.
+   */
   private function uninstallXb(): void {
     // Ensure that Drupal is installed and has a database connection; otherwise,
     // there's nothing to do.
@@ -69,14 +84,6 @@ final class Plugin implements PluginInterface, EventSubscriberInterface {
     ]);
     $process->mustRun();
 
-    // Delete the demo field storage.
-    $process = new Process([
-      $this->drushPath,
-      'php:eval',
-      'Drupal\field\Entity\FieldStorageConfig::loadByName("node", "field_xb_demo")?->delete();',
-    ]);
-    $process->mustRun();
-
     // Uninstall Experience Builder.
     $process = new Process([
       $this->drushPath,
-- 
GitLab


From 6e6cbdab7572d555d18582d84c14dfc74616efe7 Mon Sep 17 00:00:00 2001
From: Adam G-H <32250-phenaproxima@users.noreply.drupalcode.org>
Date: Tue, 7 Jan 2025 17:16:26 +0000
Subject: [PATCH 5/8] Apply 1 suggestion(s) to 1 file(s)

Co-authored-by: Tim Plunkett <41843-tim.plunkett@users.noreply.drupalcode.org>
---
 .../homeadditions/bin/generate-composer-json  | 29 ++++++++++---------
 1 file changed, 16 insertions(+), 13 deletions(-)

diff --git a/.ddev/homeadditions/bin/generate-composer-json b/.ddev/homeadditions/bin/generate-composer-json
index c4f64f609..109378e0b 100755
--- a/.ddev/homeadditions/bin/generate-composer-json
+++ b/.ddev/homeadditions/bin/generate-composer-json
@@ -10,24 +10,27 @@ $read_json = function (string $file): array {
   return json_decode($data, TRUE, flags: JSON_THROW_ON_ERROR);
 };
 
-// From https://www.php.net/manual/en/function.array-merge-recursive.php#92195
-function array_merge_recursive_distinct(array &$array1, array &$array2): array {
-  $merged = $array1;
-
-  foreach ($array2 as $key => &$value) {
-    if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
-      $merged[$key] = array_merge_recursive_distinct($merged[$key], $value);
-    }
-    else {
-      $merged[$key] = $value;
+// 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 $merged;
-}
+  return $result;
+};
 
 $base = $read_json('project_template/composer.json');
 $dev = $read_json('dev.composer.json');
-$data = array_merge_recursive_distinct($base, $dev);
+$data = $merge_deep($base, $dev);
 
 // If in a CI environment, make all path repository URLs absolute.
 if (getenv('CI')) {
-- 
GitLab


From 59e4027189d0ca3609bc41b11552124412c72f1f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Tue, 7 Jan 2025 12:33:54 -0500
Subject: [PATCH 6/8] Refactoring

---
 .../recipes/drupal_cms_xb_demo/src/Plugin.php | 54 ++++++++-----------
 1 file changed, 22 insertions(+), 32 deletions(-)

diff --git a/project_template/recipes/drupal_cms_xb_demo/src/Plugin.php b/project_template/recipes/drupal_cms_xb_demo/src/Plugin.php
index 691e6d10f..ddbaa4fa4 100644
--- a/project_template/recipes/drupal_cms_xb_demo/src/Plugin.php
+++ b/project_template/recipes/drupal_cms_xb_demo/src/Plugin.php
@@ -24,8 +24,6 @@ use Symfony\Component\Process\Process;
  */
 final class Plugin implements PluginInterface, EventSubscriberInterface {
 
-  private readonly string $drushPath;
-
   /**
    * {@inheritdoc}
    */
@@ -46,59 +44,51 @@ final class Plugin implements PluginInterface, EventSubscriberInterface {
 
     $operation = $event->getOperation();
     if ($operation instanceof UpdateOperation && $operation->getInitialPackage()->getName() === $package_name && $operation->getTargetPackage()->getName() === $package_name) {
-      $this->uninstallXb();
+      $drush = $event->getComposer()->getConfig()->get('bin-dir') . '/drush';
+      assert(is_executable($drush));
+
+      $this->uninstallXb(function (array $command) use ($drush): Process {
+        $process = new Process([$drush, ...$command]);
+        return $process->mustRun();
+      });
     }
   }
 
   /**
    * 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(): void {
+  private function uninstallXb(callable $drush): void {
     // Ensure that Drupal is installed and has a database connection; otherwise,
     // there's nothing to do.
-    $process = new Process([$this->drushPath, 'core:status', '--field=db-status']);
-    $output = trim($process->mustRun()->getOutput());
+    $output = $drush(['core:status', '--field=db-status'])->getOutput();
+    $output = trim($output);
     if (empty($output)) {
       return;
     }
 
-    // Ask Drush if Experience Builder is installed. If it's not, there's
-    // nothing else to do.
-    $process = new Process([
-      $this->drushPath,
+    // Ask Drush if Experience Builder is installed.
+    $output = $drush([
       'pm:list',
       '--type=module',
       '--field=status',
       '--filter=experience_builder',
-    ]);
-    $status = trim($process->mustRun()->getOutput());
-    if ($status === 'Disabled') {
-      return;
-    }
-
-    // Delete all xb_page entities.
-    $process = new Process([
-      $this->drushPath,
-      'entity:delete',
-      'xb_page',
-    ]);
-    $process->mustRun();
+    ])->getOutput();
 
-    // Uninstall Experience Builder.
-    $process = new Process([
-      $this->drushPath,
-      'pm:uninstall',
-      'experience_builder',
-      '--yes',
-    ]);
-    $process->mustRun();
+    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 {
-    $this->drushPath = $composer->getConfig()->get('bin-dir') . '/drush';
   }
 
   /**
-- 
GitLab


From 15f0ad36d77d426649e3bb6c26d5ddba7e3e2373 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Tue, 7 Jan 2025 16:16:05 -0500
Subject: [PATCH 7/8] Make it do the thing only if updating from a prerelease
 version

---
 .../recipes/drupal_cms_xb_demo/src/Plugin.php | 32 +++++++++++++------
 1 file changed, 22 insertions(+), 10 deletions(-)

diff --git a/project_template/recipes/drupal_cms_xb_demo/src/Plugin.php b/project_template/recipes/drupal_cms_xb_demo/src/Plugin.php
index ddbaa4fa4..693f5f643 100644
--- a/project_template/recipes/drupal_cms_xb_demo/src/Plugin.php
+++ b/project_template/recipes/drupal_cms_xb_demo/src/Plugin.php
@@ -7,10 +7,12 @@ 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;
 
 /**
@@ -40,17 +42,27 @@ final class Plugin implements PluginInterface, EventSubscriberInterface {
    *   The event object.
    */
   public function onPackageUpdate(PackageEvent $event): void {
-    $package_name = 'drupal/experience_builder';
-
     $operation = $event->getOperation();
-    if ($operation instanceof UpdateOperation && $operation->getInitialPackage()->getName() === $package_name && $operation->getTargetPackage()->getName() === $package_name) {
-      $drush = $event->getComposer()->getConfig()->get('bin-dir') . '/drush';
-      assert(is_executable($drush));
-
-      $this->uninstallXb(function (array $command) use ($drush): Process {
-        $process = new Process([$drush, ...$command]);
-        return $process->mustRun();
-      });
+
+    // We only need to uninstall XB if we're updating it from an alpha or dev
+    // version.
+    if ($operation instanceof UpdateOperation) {
+      $from = $operation->getInitialPackage();
+
+      if ($from->getName() === 'drupal/experience_builder' && in_array($from->getStability(), ['alpha', 'dev'], TRUE)) {
+        $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'));
+      }
     }
   }
 
-- 
GitLab


From 245c0828f4fc461b11e24d946e856a71345507ce Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Tue, 7 Jan 2025 19:52:08 -0500
Subject: [PATCH 8/8] Anticipate the change made by the ddev tag script

---
 project_template/composer.json                            | 2 +-
 project_template/recipes/drupal_cms_xb_demo/composer.json | 3 ++-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/project_template/composer.json b/project_template/composer.json
index 0d8a26ad4..8509bcc47 100644
--- a/project_template/composer.json
+++ b/project_template/composer.json
@@ -35,7 +35,7 @@
         "drupal/drupal_cms_person": "*",
         "drupal/drupal_cms_project": "*",
         "drupal/drupal_cms_seo_tools": "*",
-        "drupal/drupal_cms_xb_demo": "*",
+        "drupal/drupal_cms_xb_demo": "@dev",
         "drush/drush": "^13"
     },
     "conflict": {
diff --git a/project_template/recipes/drupal_cms_xb_demo/composer.json b/project_template/recipes/drupal_cms_xb_demo/composer.json
index 9809cfd66..b0198a364 100644
--- a/project_template/recipes/drupal_cms_xb_demo/composer.json
+++ b/project_template/recipes/drupal_cms_xb_demo/composer.json
@@ -13,5 +13,6 @@
   "extra": {
     "class": "Drupal\\XbDemo\\Plugin",
     "plugin-optional": true
-  }
+  },
+  "version": "dev-main"
 }
-- 
GitLab