diff --git a/core/modules/ckeditor5/src/Annotation/DrupalAspectsOfCKEditor5Plugin.php b/core/modules/ckeditor5/src/Annotation/DrupalAspectsOfCKEditor5Plugin.php
index a8feeb33a22058729055450df8f780f79df0a7ed..fff4a59aa89fec303464b536fddae3e6acdf86a2 100644
--- a/core/modules/ckeditor5/src/Annotation/DrupalAspectsOfCKEditor5Plugin.php
+++ b/core/modules/ckeditor5/src/Annotation/DrupalAspectsOfCKEditor5Plugin.php
@@ -42,6 +42,15 @@ class DrupalAspectsOfCKEditor5Plugin extends Plugin {
    */
   public $class = CKEditor5PluginDefault::class;
 
+  /**
+   * The CKEditor 5 plugin deriver class.
+   *
+   * This property is optional and it does not need to be declared.
+   *
+   * @var string|null
+   */
+  public $deriver = NULL;
+
   /**
    * The library this plugin requires.
    *
diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginDefinition.php b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginDefinition.php
index 6fcae7bdd8645d90fdeaee142b8a9c5e7eed02ff..371a53aeaaf8de85766c1671e7dc7f4318326d90 100644
--- a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginDefinition.php
+++ b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginDefinition.php
@@ -6,6 +6,7 @@
 
 use Drupal\ckeditor5\HTMLRestrictions;
 use Drupal\Component\Assertion\Inspector;
+use Drupal\Component\Plugin\Definition\DerivablePluginDefinitionInterface;
 use Drupal\Component\Plugin\Definition\PluginDefinition;
 use Drupal\Component\Plugin\Definition\PluginDefinitionInterface;
 use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
@@ -16,7 +17,7 @@
 /**
  * Provides an implementation of a CKEditor 5 plugin definition.
  */
-final class CKEditor5PluginDefinition extends PluginDefinition implements PluginDefinitionInterface {
+final class CKEditor5PluginDefinition extends PluginDefinition implements PluginDefinitionInterface, DerivablePluginDefinitionInterface {
 
   use SchemaCheckTrait;
 
@@ -40,22 +41,17 @@ final class CKEditor5PluginDefinition extends PluginDefinition implements Plugin
    * @param array $definition
    *   An array of values from the annotation/YAML.
    *
-   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+   * @throws \InvalidArgumentException
    */
   public function __construct(array $definition) {
-    $this->id = $id = $definition['id'];
-
-    $expected_prefix = sprintf("%s_", $definition['provider']);
-    if (strpos($id, $expected_prefix) !== 0) {
-      throw new InvalidPluginDefinitionException($id, sprintf('The "%s" CKEditor 5 plugin definition must have a plugin ID that starts with "%s".', $id, $expected_prefix));
+    foreach ($definition as $property => $value) {
+      if (property_exists($this, $property)) {
+        $this->{$property} = $value;
+      }
+      else {
+        throw new \InvalidArgumentException(sprintf('Property %s with value %s does not exist on %s.', $property, $value, __CLASS__));
+      }
     }
-    $this->provider = $definition['provider'];
-
-    static::validateCKEditor5Aspects($id, $definition);
-    $this->ckeditor5 = $definition['ckeditor5'];
-
-    $this->validateDrupalAspects($id, $definition);
-    $this->drupal = $definition['drupal'];
   }
 
   /**
@@ -81,8 +77,11 @@ public function toArray(): array {
    *   The plugin definition to validate.
    *
    * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+   *
+   * @internal
+   * @see \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::processDefinition()
    */
-  private static function validateCKEditor5Aspects(string $id, array $definition): void {
+  public static function validateCKEditor5Aspects(string $id, array $definition): void {
     if (!isset($definition['ckeditor5'])) {
       throw new InvalidPluginDefinitionException($id, sprintf('The "%s" CKEditor 5 plugin definition must contain a "ckeditor5" key.', $id));
     }
@@ -122,8 +121,11 @@ private static function validateCKEditor5Aspects(string $id, array $definition):
    *   The plugin definition to validate.
    *
    * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+   *
+   * @internal
+   * @see \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::processDefinition()
    */
-  private function validateDrupalAspects(string $id, array $definition): void {
+  public function validateDrupalAspects(string $id, array $definition): void {
     if (!isset($definition['drupal'])) {
       throw new InvalidPluginDefinitionException($id, sprintf('The "%s" CKEditor 5 plugin definition must contain a "drupal" key.', $id));
     }
@@ -304,6 +306,31 @@ public function setClass($class) {
     return $this;
   }
 
+  /**
+   * {@inheritdoc}
+   *
+   * @see \Drupal\ckeditor5\Annotation\DrupalAspectsOfCKEditor5Plugin::$deriver
+   */
+  public function getDeriver() {
+    // TRICKY: this is the only key that is allowed to not be set, because it is
+    // possible that this plugin definition is a partial/incomplete one, and the
+    // default from the annotation is only applied automatically for class
+    // annotation CKEditor 5 plugin definitions (because they create an instance
+    // of the DrupalAspectsOfCKEditor5Plugin annotation level), not for CKEditor
+    // 5 plugin definitions in YAML.
+    // @see \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::processDefinition()
+    // @see \Drupal\ckeditor5\Annotation\CKEditor5Plugin::__construct()
+    return $this->drupal['deriver'] ?? NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setDeriver($deriver) {
+    $this->drupal['deriver'] = $deriver;
+    return $this;
+  }
+
   /**
    * Whether this plugin is configurable by the user.
    *
diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php
index 69e8eb51be0a232aea026a0bcacaf42ed3517be3..3558ab407bc0afc7f8c3cf063fe0a0bde340d838 100644
--- a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php
+++ b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php
@@ -8,11 +8,13 @@
 use Drupal\ckeditor5\HTMLRestrictions;
 use Drupal\Component\Annotation\Plugin\Discovery\AnnotationBridgeDecorator;
 use Drupal\Component\Assertion\Inspector;
+use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Plugin\DefaultPluginManager;
 use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
+use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
 use Drupal\Core\Plugin\Discovery\YamlDiscoveryDecorator;
 use Drupal\editor\EditorInterface;
 use Drupal\filter\FilterPluginCollection;
@@ -61,11 +63,55 @@ protected function getDiscovery() {
       // supports top-level properties.
       // @see \Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition::label()
       $discovery = new AnnotationBridgeDecorator($discovery, $this->pluginDefinitionAnnotationName);
+      $discovery = new ContainerDerivativeDiscoveryDecorator($discovery);
       $this->discovery = $discovery;
     }
     return $this->discovery;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function processDefinition(&$definition, $plugin_id) {
+    if (!$definition instanceof CKEditor5PluginDefinition) {
+      throw new InvalidPluginDefinitionException($plugin_id, sprintf('The "%s" CKEditor 5 plugin definition must extend %s', $plugin_id, CKEditor5PluginDefinition::class));
+    }
+
+    // A derived plugin will still have the ID of the derivative, rather than
+    // that of the derived plugin ID (`<base plugin ID>:<derivative ID>`).
+    // Generate an updated CKEditor5PluginDefinition.
+    // @see \Drupal\Component\Plugin\Discovery\DerivativeDiscoveryDecorator::encodePluginId()
+    // @todo Remove this in https://www.drupal.org/project/drupal/issues/2458769.
+    $is_derived = $definition->id() !== $plugin_id;
+    if ($is_derived) {
+      $definition = new CKEditor5PluginDefinition(['id' => $plugin_id] + $definition->toArray());
+    }
+
+    $expected_prefix = sprintf("%s_", $definition->getProvider());
+    $id = $definition->id();
+    if (strpos($id, $expected_prefix) !== 0) {
+      throw new InvalidPluginDefinitionException($id, sprintf('The "%s" CKEditor 5 plugin definition must have a plugin ID that starts with "%s".', $id, $expected_prefix));
+    }
+
+    try {
+      $definition->validateCKEditor5Aspects($id, $definition->toArray());
+      $definition->validateDrupalAspects($id, $definition->toArray());
+    }
+    catch (InvalidPluginDefinitionException $e) {
+      // If this exception is thrown for a derived CKEditor 5 plugin definition,
+      // it means the deriver did not generate a valid plugin definition.
+      // Re-throw the exception, but tweak the language for DX: clarify it is
+      // for a derived plugin definition.
+      if ($is_derived) {
+        throw new InvalidPluginDefinitionException($e->getPluginId(), str_replace('plugin definition', 'derived plugin definition', $e->getMessage()));
+      }
+      // Otherwise, the exception was appropriate: re-throw it.
+      throw $e;
+    }
+
+    parent::processDefinition($definition, $plugin_id);
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php b/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php
index aa5d2661668a1ca03d75be766a3cd37e86c5f18f..64e18af00e5f1091612a34b833a1f2a7e328726c 100644
--- a/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php
+++ b/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php
@@ -3,8 +3,11 @@
 namespace Drupal\Tests\ckeditor5\Kernel;
 
 use Composer\Autoload\ClassLoader;
+use Drupal\ckeditor5\Annotation\CKEditor5AspectsOfCKEditor5Plugin;
+use Drupal\ckeditor5\Annotation\DrupalAspectsOfCKEditor5Plugin;
 use Drupal\ckeditor5\HTMLRestrictions;
 use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Heading;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
 use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\editor\Entity\Editor;
@@ -12,6 +15,7 @@
 use Drupal\filter\Entity\FilterFormat;
 use Drupal\Tests\SchemaCheckTestTrait;
 use org\bovigo\vfs\vfsStream;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
 use Symfony\Component\Yaml\Yaml;
 
@@ -78,38 +82,44 @@ protected function setUp(): void {
   }
 
   /**
-   * @covers \Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition::__construct()
-   * @dataProvider providerTestInvalidPluginDefinitions
+   * Mocks a module providing a CKEditor 5 plugin in VFS.
+   *
+   * @param string $module_name
+   *   The name of the module.
+   * @param string $yaml
+   *   The YAML to be stored in the *.ckeditor5.yml file.
+   * @param array $additional_files
+   *   The additional files to create.
+   *
+   * @return \Symfony\Component\DependencyInjection\ContainerInterface
+   *   The container that has the VFS-mocked CKEditor 5 plugin-providing module
+   *   installed in it; this container must be used to simulate this module
+   *   being installed.
    */
-  public function testInvalidPluginDefinitions(string $yaml, ?string $expected_message, array $additional_files = []): void {
-    if ($expected_message) {
-      $this->expectException(InvalidPluginDefinitionException::class);
-      $this->expectExceptionMessage($expected_message);
-    }
-
+  private function mockModuleInVfs(string $module_name, string $yaml, array $additional_files = []): ContainerInterface {
     $site_directory = ltrim(parse_url($this->siteDirectory)['path'], '/');
     vfsStream::create([
       'modules' => [
-        'ckeditor5_invalid_plugin' => [
-          'ckeditor5_invalid_plugin.info.yml' => <<<YAML
-name: CKEditor 5 Invalid Plugin Definition Test
+        $module_name => [
+          "$module_name.info.yml" => <<<YAML
+name: CKEditor 5 Test $module_name
 type: module
 core_version_requirement: ^9
 YAML,
-          'ckeditor5_invalid_plugin.ckeditor5.yml' => $yaml,
+          "$module_name.ckeditor5.yml" => $yaml,
         ] + $additional_files,
       ],
     ], $this->vfsRoot->getChild($site_directory));
 
     if (!empty($additional_files)) {
       $additional_class_loader = new ClassLoader();
-      $additional_class_loader->addPsr4("Drupal\\ckeditor5_invalid_plugin\\Plugin\\CKEditor5Plugin\\", vfsStream::url("root/$site_directory/modules/ckeditor5_invalid_plugin/src/Plugin/CKEditor5Plugin"));
+      $additional_class_loader->addPsr4("Drupal\\$module_name\\Plugin\\CKEditor5Plugin\\", vfsStream::url("root/$site_directory/modules/$module_name/src/Plugin/CKEditor5Plugin"));
       $additional_class_loader->register(TRUE);
     }
 
     $config_sync = \Drupal::service('config.storage');
     $config_data = $this->config('core.extension')->get();
-    $config_data['module']['ckeditor5_invalid_plugin'] = 1;
+    $config_data['module'][$module_name] = 1;
     $config_sync->write('core.extension', $config_data);
 
     // Construct a new container for testing a plugin definition in isolation,
@@ -123,12 +133,15 @@ public function testInvalidPluginDefinitions(string $yaml, ?string $expected_mes
     $container = new ContainerBuilder(new FrozenParameterBag([
       'app.root' => $root,
       'container.modules' => [
-        'ckeditor5_invalid_plugin' => [
+        $module_name => [
           'type' => 'module',
-          'pathname' => 'modules/ckeditor5_invalid_plugin/ckeditor5_invalid_plugin.info.yml',
+          'pathname' => "modules/$module_name/$module_name.info.yml",
           'filename' => NULL,
         ] + $this->container->getParameter('container.modules'),
       ],
+      'container.namespaces' => [
+        "Drupal\\$module_name" => vfsStream::url("root/$site_directory/modules/$module_name/src"),
+      ] + $this->container->getParameter('container.namespaces'),
     ] + $this->container->getParameterBag()->all()));
     $container->setDefinitions($this->container->getDefinitions());
 
@@ -146,14 +159,28 @@ public function testInvalidPluginDefinitions(string $yaml, ?string $expected_mes
     // only work-around possible is to manipulate the config schema definition
     // cache.
     // @todo Remove this in https://www.drupal.org/project/drupal/issues/2961541.
-    if (isset($additional_files['config']['schema']['ckeditor5_invalid_plugin.schema.yml'])) {
-      $cache = \Drupal::service('cache.discovery')->get('typed_config_definitions');
+    if (isset($additional_files['config']['schema']["$module_name.schema.yml"])) {
+      $cache = \Drupal::service('cache.discovery')
+        ->get('typed_config_definitions');
       $typed_config_definitions = $cache->data;
-      $typed_config_definitions += Yaml::parse($additional_files['config']['schema']['ckeditor5_invalid_plugin.schema.yml']);
+      $typed_config_definitions += Yaml::parse($additional_files['config']['schema']["$module_name.schema.yml"]);
       \Drupal::service('config.typed')->clearCachedDefinitions();
       \Drupal::service('cache.discovery')->set('typed_config_definitions', $typed_config_definitions, $cache->expire, $cache->tags);
     }
 
+    return $container;
+  }
+
+  /**
+   * @covers \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::processDefinition()
+   * @dataProvider providerTestInvalidPluginDefinitions
+   */
+  public function testInvalidPluginDefinitions(string $yaml, ?string $expected_message, array $additional_files = []): void {
+    if ($expected_message) {
+      $this->expectException(InvalidPluginDefinitionException::class);
+      $this->expectExceptionMessage($expected_message);
+    }
+    $container = $this->mockModuleInVfs('ckeditor5_invalid_plugin', $yaml, $additional_files);
     $container->get('plugin.manager.ckeditor5.plugin')->getDefinitions();
   }
 
@@ -1519,4 +1546,361 @@ public function testExternalLinkAutomaticLinkDecoratorDisallowed(): void {
     $this->manager->getDefinitions();
   }
 
+  /**
+   * @covers ::getDiscovery
+   * @dataProvider providerTestDerivedPluginDefinitions
+   */
+  public function testDerivedPluginDefinitions(string $yaml, ?string $expected_message, array $additional_files = [], ?array $expected_derived_plugin_definitions = NULL): void {
+    if ($expected_message) {
+      $this->expectException(InvalidPluginDefinitionException::class);
+      $this->expectExceptionMessage($expected_message);
+    }
+    $container = $this->mockModuleInVfs('ckeditor5_derived_plugin', $yaml, $additional_files);
+
+    $actual_definitions = $container->get('plugin.manager.ckeditor5.plugin')->getDefinitions();
+    $this->assertEquals($expected_derived_plugin_definitions, $actual_definitions);
+  }
+
+  /**
+   * Data provider.
+   *
+   * @return \Generator
+   *   Test scenarios.
+   */
+  public function providerTestDerivedPluginDefinitions(): \Generator {
+    // Defaults inherited from CKEditor5AspectsOfCKEditor5Plugin.
+    $ckeditor5_aspects_defaults = get_class_vars(CKEditor5AspectsOfCKEditor5Plugin::class);
+    // Defaults inherited from DrupalAspectsOfCKEditor5Plugin.
+    $drupal_aspects_defaults = get_class_vars(DrupalAspectsOfCKEditor5Plugin::class);
+
+    $simple_deriver_additional_files = [
+      'src' => [
+        'Plugin' => [
+          'CKEditor5Plugin' => [
+            'SimpleDeriver.php' => <<<'PHP'
+<?php
+namespace Drupal\ckeditor5_derived_plugin\Plugin\CKEditor5Plugin;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+class SimpleDeriver extends DeriverBase {
+  public function getDerivativeDefinitions($base_plugin_definition) {
+    assert($base_plugin_definition instanceof CKEditor5PluginDefinition);
+    foreach (['bar', 'baz'] as $id) {
+      $definition = $base_plugin_definition->toArray();
+      $definition['id'] = $id;
+      $definition['drupal']['label'] = sprintf("Foo %s", $id);
+      $this->derivatives[$id] = new CKEditor5PluginDefinition($definition);
+    }
+    return $this->derivatives;
+  }
+}
+PHP,
+          ],
+        ],
+      ],
+    ];
+
+    yield 'INVALID: simple deriver but without `drupal.elements` in the base definition and it not getting set by the deriver' => [
+      <<<YAML
+ckeditor5_derived_plugin_foo:
+  ckeditor5:
+    plugins: {}
+  drupal:
+    deriver: Drupal\ckeditor5_derived_plugin\Plugin\CKEditor5Plugin\SimpleDeriver
+YAML,
+      'The "ckeditor5_derived_plugin_foo:bar" CKEditor 5 derived plugin definition must contain a "drupal.elements" key.',
+      $simple_deriver_additional_files,
+    ];
+
+    yield 'INVALID: simple deriver but without `ckeditor5.plugins` in the base definition and it not getting set by the deriver' => [
+      <<<YAML
+ckeditor5_derived_plugin_foo:
+  ckeditor5: {}
+  drupal:
+    elements: false
+    deriver: Drupal\ckeditor5_derived_plugin\Plugin\CKEditor5Plugin\SimpleDeriver
+YAML,
+      'The "ckeditor5_derived_plugin_foo:bar" CKEditor 5 derived plugin definition must contain a "ckeditor5.plugins" key.',
+      $simple_deriver_additional_files,
+    ];
+
+    yield 'INVALID: simple deriver but without `ckeditor5` in the base definition and it not getting set by the deriver' => [
+      <<<YAML
+ckeditor5_derived_plugin_foo:
+  drupal:
+    elements: false
+    deriver: Drupal\ckeditor5_derived_plugin\Plugin\CKEditor5Plugin\SimpleDeriver
+YAML,
+      'The "ckeditor5_derived_plugin_foo:bar" CKEditor 5 derived plugin definition must contain a "ckeditor5" key.',
+      $simple_deriver_additional_files,
+    ];
+
+    yield 'INVALID: simple deriver which returns arrays instead of CKEditor5PluginDefinition instances' => [
+      <<<YAML
+ckeditor5_derived_plugin_foo:
+  ckeditor5:
+    plugins: {}
+  drupal:
+    elements: false
+    deriver: Drupal\ckeditor5_derived_plugin\Plugin\CKEditor5Plugin\SimpleDeriver
+YAML,
+      'The "ckeditor5_derived_plugin_foo:bar" CKEditor 5 plugin definition must extend Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition',
+      [
+        'src' => [
+          'Plugin' => [
+            'CKEditor5Plugin' => [
+              'SimpleDeriver.php' => <<<'PHP'
+<?php
+namespace Drupal\ckeditor5_derived_plugin\Plugin\CKEditor5Plugin;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+class SimpleDeriver extends DeriverBase {
+  public function getDerivativeDefinitions($base_plugin_definition) {
+    assert($base_plugin_definition instanceof CKEditor5PluginDefinition);
+    foreach (['bar', 'baz'] as $id) {
+      $definition = $base_plugin_definition->toArray();
+      $definition['id'] = $id;
+      $definition['drupal']['label'] = sprintf("Foo %s", $id);
+      $this->derivatives[$id] = $definition;
+    }
+    return $this->derivatives;
+  }
+}
+PHP,
+            ],
+          ],
+        ],
+      ],
+    ];
+
+    yield 'VALID: simple deriver, base definition in YAML' => [
+      <<<YAML
+ckeditor5_derived_plugin_foo:
+  ckeditor5:
+    plugins: {}
+  drupal:
+    elements: false
+    deriver: Drupal\ckeditor5_derived_plugin\Plugin\CKEditor5Plugin\SimpleDeriver
+YAML,
+      NULL,
+      $simple_deriver_additional_files,
+      [
+        'ckeditor5_derived_plugin_foo:bar' => new CKEditor5PluginDefinition([
+          'provider' => 'ckeditor5_derived_plugin',
+          'id' => 'ckeditor5_derived_plugin_foo:bar',
+          'ckeditor5' => ['plugins' => []] + $ckeditor5_aspects_defaults,
+          'drupal' => [
+            'label' => 'Foo bar',
+            'elements' => FALSE,
+            'deriver' => 'Drupal\ckeditor5_derived_plugin\Plugin\CKEditor5Plugin\SimpleDeriver',
+          ] + $drupal_aspects_defaults,
+        ]),
+        'ckeditor5_derived_plugin_foo:baz' => new CKEditor5PluginDefinition([
+          'provider' => 'ckeditor5_derived_plugin',
+          'id' => 'ckeditor5_derived_plugin_foo:baz',
+          'ckeditor5' => ['plugins' => []] + $ckeditor5_aspects_defaults,
+          'drupal' => [
+            'label' => 'Foo baz',
+            'elements' => FALSE,
+            'deriver' => 'Drupal\ckeditor5_derived_plugin\Plugin\CKEditor5Plugin\SimpleDeriver',
+          ] + $drupal_aspects_defaults,
+        ]),
+      ],
+    ];
+
+    yield 'VALID: simple deriver, base definition in PHP' => [
+      '',
+      NULL,
+      [
+        'src' => [
+          'Plugin' => [
+            'CKEditor5Plugin' => [
+              'Foo.php' => <<<'PHP'
+<?php
+declare(strict_types = 1);
+namespace Drupal\ckeditor5_derived_plugin\Plugin\CKEditor5Plugin;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginDefault;
+/**
+ * @CKEditor5Plugin(
+ *   id = "ckeditor5_derived_plugin_foo",
+ *   ckeditor5 = @CKEditor5AspectsOfCKEditor5Plugin(
+ *     plugins = {},
+ *   ),
+ *   drupal = @DrupalAspectsOfCKEditor5Plugin(
+ *     elements = false,
+ *     deriver = "Drupal\ckeditor5_derived_plugin\Plugin\CKEditor5Plugin\SimpleDeriver",
+ *   )
+ * )
+ */
+class Foo extends CKEditor5PluginDefault {
+}
+PHP,
+              'SimpleDeriver.php' => $simple_deriver_additional_files['src']['Plugin']['CKEditor5Plugin']['SimpleDeriver.php'],
+            ],
+          ],
+        ],
+      ],
+      [
+        'ckeditor5_derived_plugin_foo:bar' => new CKEditor5PluginDefinition([
+          'provider' => 'ckeditor5_derived_plugin',
+          'id' => 'ckeditor5_derived_plugin_foo:bar',
+          'ckeditor5' => ['plugins' => []] + $ckeditor5_aspects_defaults,
+          'drupal' => [
+            'class' => 'Drupal\ckeditor5_derived_plugin\Plugin\CKEditor5Plugin\Foo',
+            'label' => 'Foo bar',
+            'elements' => FALSE,
+            'deriver' => 'Drupal\ckeditor5_derived_plugin\Plugin\CKEditor5Plugin\SimpleDeriver',
+          ] + $drupal_aspects_defaults,
+        ]),
+        'ckeditor5_derived_plugin_foo:baz' => new CKEditor5PluginDefinition([
+          'provider' => 'ckeditor5_derived_plugin',
+          'id' => 'ckeditor5_derived_plugin_foo:baz',
+          'ckeditor5' => ['plugins' => []] + $ckeditor5_aspects_defaults,
+          'drupal' => [
+            'class' => 'Drupal\ckeditor5_derived_plugin\Plugin\CKEditor5Plugin\Foo',
+            'label' => 'Foo baz',
+            'elements' => FALSE,
+            'deriver' => 'Drupal\ckeditor5_derived_plugin\Plugin\CKEditor5Plugin\SimpleDeriver',
+          ] + $drupal_aspects_defaults,
+        ]),
+      ],
+    ];
+
+    yield 'VALID: minimal base plugin definition, maximal deriver' => [
+      <<<YAML
+# Minimal annotation key-value pairs set in the YAML, most set in the deriver.
+ckeditor5_derived_plugin_foo:
+  drupal:
+    deriver: Drupal\ckeditor5_derived_plugin\Plugin\CKEditor5Plugin\MaximalDeriver
+YAML,
+      NULL,
+      [
+        'src' => [
+          'Plugin' => [
+            'CKEditor5Plugin' => [
+              'MaximalDeriver.php' => <<<'PHP'
+<?php
+namespace Drupal\ckeditor5_derived_plugin\Plugin\CKEditor5Plugin;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+class MaximalDeriver extends DeriverBase {
+  public function getDerivativeDefinitions($base_plugin_definition) {
+    assert($base_plugin_definition instanceof CKEditor5PluginDefinition);
+    foreach (['A', 'B'] as $id) {
+      $definition = $base_plugin_definition->toArray();
+      $definition['id'] = $id;
+      $definition['drupal']['label'] = sprintf("Foo %s", $id);
+      $definition['drupal']['elements'] = FALSE;
+      $definition['ckeditor5']['plugins'] = [];
+      $this->derivatives[$id] = new CKEditor5PluginDefinition($definition);
+    }
+    return $this->derivatives;
+  }
+}
+PHP,
+            ],
+          ],
+        ],
+      ],
+      [
+        'ckeditor5_derived_plugin_foo:A' => new CKEditor5PluginDefinition([
+          'provider' => 'ckeditor5_derived_plugin',
+          'id' => 'ckeditor5_derived_plugin_foo:A',
+          'ckeditor5' => ['plugins' => []],
+          'drupal' => [
+            'label' => 'Foo A',
+            'elements' => FALSE,
+            'deriver' => 'Drupal\ckeditor5_derived_plugin\Plugin\CKEditor5Plugin\MaximalDeriver',
+          ] + $drupal_aspects_defaults,
+        ]),
+        'ckeditor5_derived_plugin_foo:B' => new CKEditor5PluginDefinition([
+          'provider' => 'ckeditor5_derived_plugin',
+          'id' => 'ckeditor5_derived_plugin_foo:B',
+          'ckeditor5' => ['plugins' => []],
+          'drupal' => [
+            'label' => 'Foo B',
+            'elements' => FALSE,
+            'deriver' => 'Drupal\ckeditor5_derived_plugin\Plugin\CKEditor5Plugin\MaximalDeriver',
+          ] + $drupal_aspects_defaults,
+        ]),
+      ],
+    ];
+
+    yield 'VALID: container-dependent deriver' => [
+      <<<YAML
+ckeditor5_derived_plugin_foo:
+  ckeditor5:
+    plugins: {}
+  drupal:
+    elements: false
+    deriver: Drupal\ckeditor5_derived_plugin\Plugin\CKEditor5Plugin\ContainerDependentDeriver
+YAML,
+      NULL,
+      [
+        'config' => [
+          'schema' => [
+            'ckeditor5_derived_plugin.schema.yml' => <<<YAML
+ckeditor5.plugin.ckeditor5_derived_plugin:
+  type: mapping
+  label: 'Foo'
+  mapping:
+    foo:
+      type: boolean
+      label: 'Foo'
+YAML,
+          ],
+        ],
+        'src' => [
+          'Plugin' => [
+            'CKEditor5Plugin' => [
+              'ContainerDependentDeriver.php' => <<<'PHP'
+<?php
+namespace Drupal\ckeditor5_derived_plugin\Plugin\CKEditor5Plugin;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+use Drupal\Core\Authentication\AuthenticationCollectorInterface;
+use Drupal\Core\Entity\EntityTypeRepositoryInterface;
+use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+class ContainerDependentDeriver extends DeriverBase implements ContainerDeriverInterface {
+  protected $authenticationCollector;
+  public function __construct(AuthenticationCollectorInterface $authentication_collector) {
+    $this->authenticationCollector = $authentication_collector;
+  }
+  public static function create(ContainerInterface $container, $base_plugin_id) {
+    assert($base_plugin_id === 'ckeditor5_derived_plugin_foo');
+    return new static($container->get('authentication_collector'));
+  }
+  public function getDerivativeDefinitions($base_plugin_definition) {
+    assert($base_plugin_definition instanceof CKEditor5PluginDefinition);
+    $authentication_providers = array_keys($this->authenticationCollector->getSortedProviders());
+    foreach ($authentication_providers as $id) {
+      $definition = $base_plugin_definition->toArray();
+      $definition['id'] = $id;
+      $definition['drupal']['label'] = sprintf("Foo %s", $id);
+      $this->derivatives[$definition['id']] = new CKEditor5PluginDefinition($definition);
+    }
+    return $this->derivatives;
+  }
+}
+PHP,
+            ],
+          ],
+        ],
+      ],
+      [
+        'ckeditor5_derived_plugin_foo:cookie' => new CKEditor5PluginDefinition([
+          'provider' => 'ckeditor5_derived_plugin',
+          'id' => 'ckeditor5_derived_plugin_foo:cookie',
+          'ckeditor5' => ['plugins' => []] + $ckeditor5_aspects_defaults,
+          'drupal' => [
+            'label' => 'Foo cookie',
+            'elements' => FALSE,
+            'deriver' => 'Drupal\ckeditor5_derived_plugin\Plugin\CKEditor5Plugin\ContainerDependentDeriver',
+          ] + $drupal_aspects_defaults,
+        ]),
+      ],
+    ];
+  }
+
 }