 components.module                                  |  22 ++
 components.services.yml                            |   4 +
 .../Discovery/DirectoryWithYamlDiscovery.php       |  89 +++++++
 .../Discovery/DirectoryWithYamlDiscovery.php       |  31 +++
 src/Theme/ComponentDiscovery.php                   | 288 +++++++++++++++++++++
 5 files changed, 434 insertions(+)

diff --git a/components.module b/components.module
new file mode 100644
index 0000000..374a9be
--- /dev/null
+++ b/components.module
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * Implements hook_library_info_alter().
+ */
+function components_library_info_alter(&$libraries, $extension) {
+  // Add components' asset libraries.
+  $components = \Drupal::service('theme_component_discovery')->getComponents();
+
+  $extension_components_with_libraries = array_filter($components, function ($component) use ($extension) {
+    // If this component has an asset library for the current extension, then
+    // it is guaranteed to be named '<extension>/<component ID>'.
+    return isset($component['_asset_libraries'][$extension . '/' . $component['id']]);
+  });
+  if (empty($extension_components_with_libraries)) {
+    return;
+  }
+
+  foreach ($extension_components_with_libraries as $component_id => $component_definition) {
+    $libraries[$component_id] = $component_definition['_asset_libraries'][$extension . '/' . $component_id];
+  }
+}
diff --git a/components.services.yml b/components.services.yml
index ddb25d5..c794f97 100644
--- a/components.services.yml
+++ b/components.services.yml
@@ -1,4 +1,8 @@
 services:
+  theme_component_discovery:
+    class: Drupal\components\Theme\ComponentDiscovery
+    arguments: ['@app.root', '@module_handler', '@theme_handler', '@theme.manager']
+
   twig.loader.componentlibrary:
     class: Drupal\components\Template\Loader\ComponentLibraryLoader
     arguments: ['@app.root', '@module_handler', '@theme_handler']
diff --git a/src/Component/Discovery/DirectoryWithYamlDiscovery.php b/src/Component/Discovery/DirectoryWithYamlDiscovery.php
new file mode 100644
index 0000000..2b9e5d1
--- /dev/null
+++ b/src/Component/Discovery/DirectoryWithYamlDiscovery.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Drupal\components\Component\Discovery;
+
+use Drupal\Component\Discovery\YamlDirectoryDiscovery;
+
+/**
+ * Discovers directories each with one YAML file in a set of directories.
+ *
+ * @todo Move into Drupal\Component\Discovery namespace in core.
+ */
+class DirectoryWithYamlDiscovery extends YamlDirectoryDiscovery  {
+
+  /**
+   * The subdirectory to scan.
+   *
+   * @var string
+   */
+  protected $subdirectory;
+
+  /**
+   * Constructs a DirectoryWithYamlDiscovery object.
+   *
+   * @param array $directories
+   *   An array of directories to scan, keyed by the provider. The value can
+   *   either be a string or an array of strings. The string values should be
+   *   the path of a directory to scan.
+   * @param string $subdirectory
+   *   The subdirectory to scan in each of the passed $directories.
+   */
+  public function __construct(array $directories, $subdirectory) {
+    parent::__construct($directories, 'directory_with_yaml:' . $subdirectory);
+    $this->subdirectory = $subdirectory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getIdentifier($file, array $data) {
+    return basename($data[static::FILE_KEY], '.yml');
+  }
+
+  /**
+   * Returns an array of providers keyed by file path.
+   *
+   * @return array
+   *   An array of providers keyed by file path.
+   */
+  protected function findFiles() {
+    $file_list = [];
+    foreach ($this->directories as $provider => $directories) {
+      $directories = (array) $directories;
+      foreach ($directories as $directory) {
+        // Check if there is a subdirectory with the specified name.
+        if (is_dir($directory) && is_dir($directory . '/' . $this->subdirectory)) {
+          // Now iterate over all subdirectories below the specifically named
+          // subdirectory, and check if a .yml file exists with the same name.
+          // For example:
+          // - Assuming $this->subdirectory === 'fancy'
+          // - Then this checks for 'fancy/foo/foo.yml', 'fancy/bar/bar.yml'
+          $iterator = new \FilesystemIterator($directory . '/' . $this->subdirectory);
+          /** @var \SplFileInfo $file_info */
+          foreach ($iterator as $file_info) {
+            if ($file_info->isDir()) {
+              $this->findFile($file_info, $provider, $file_list);
+
+              // Allow for two levels.
+              $nested_iterator = new \FilesystemIterator($directory . '/' . $this->subdirectory . '/' . $file_info->getBasename());
+              foreach ($nested_iterator as $nested_file_info) {
+                if ($nested_file_info->isDir()) {
+                  $this->findFile($nested_file_info, $provider, $file_list);
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+    return $file_list;
+  }
+
+  protected function findFile($file_info, $provider, array &$file_list) {
+    $yml_file_in_directory = $file_info->getPath() . '/' . $file_info->getBasename() . '/' . $file_info->getBasename() . '.yml';
+    if (is_file($yml_file_in_directory)) {
+      $file_list[$yml_file_in_directory] = $provider;
+    }
+  }
+
+}
\ No newline at end of file
diff --git a/src/Plugin/Discovery/DirectoryWithYamlDiscovery.php b/src/Plugin/Discovery/DirectoryWithYamlDiscovery.php
new file mode 100644
index 0000000..cd9a432
--- /dev/null
+++ b/src/Plugin/Discovery/DirectoryWithYamlDiscovery.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\components\Plugin\Discovery;
+
+use Drupal\Core\Plugin\Discovery\YamlDiscovery;
+use Drupal\components\Component\Discovery\DirectoryWithYamlDiscovery as ComponentDirectoryWithYamlDiscovery;
+
+/**
+ * Discovers directories each with one YAML file in a set of directories.
+ *
+ * @todo Move into Drupal\Core\Plugin\Discovery namespace in core.
+ */
+class DirectoryWithYamlDiscovery extends YamlDiscovery {
+
+  /**
+   * Constructs a DirectoryWithYamlDiscovery object.
+   *
+   * @param array $directories
+   *   An array of directories to scan, keyed by the provider. The value can
+   *   either be a string or an array of strings. The string values should be
+   *   the path of a directory to scan.
+   * @param string $subdirectory
+   *   The subdirectory to scan in each of the passed $directories.
+   */
+  public function __construct(array $directories, $subdirectory) {
+    // Intentionally does not call parent constructor as this class uses a
+    // different YAML discovery.
+    $this->discovery = new ComponentDirectoryWithYamlDiscovery($directories, $subdirectory);
+  }
+
+}
\ No newline at end of file
diff --git a/src/Theme/ComponentDiscovery.php b/src/Theme/ComponentDiscovery.php
new file mode 100644
index 0000000..6d1e0d9
--- /dev/null
+++ b/src/Theme/ComponentDiscovery.php
@@ -0,0 +1,288 @@
+<?php
+
+namespace Drupal\components\Theme;
+
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Extension\ThemeHandlerInterface;
+use Drupal\components\Plugin\Discovery\DirectoryWithYamlDiscovery;
+use Drupal\Core\Theme\ActiveTheme;
+
+/**
+ * Discovers components as defined by modules and themes.
+ *
+ * 1. Components
+ *
+ * - are uniquely identified by their name/ID across all themes and modules
+ * - themes can extend components: add CSS/JS, add more variables, change the
+ *   Twig template
+ * - can be defined by both modules and themes (but theme-defined components
+ *   cannot be used by modules)
+ *
+ * Implementation details:
+ * - The component definition as specified by the module or base theme providing
+ *   the original component is called the "base component".
+ * - Components with associated CSS/JS automatically have asset libraries
+ *   generated, one per component definition. Dependencies are added
+ *   automatically, so you end up with for example a "label" component in Classy
+ *   which has a "classy/label" asset library that depends on the "system/label
+ * - Module-defined components with associated CSS automatically get the SMACSS
+ *   category 'component'.
+ * - Theme-defined components with associated CSS automatically get the SMACSS
+ *   category 'theme'.
+ * - The 'extends' YAML key defaults to FALSE in modules and defaults to TRUE in
+ *   themes. (Because themes usually extend module-defined components.)
+ *
+ *
+ * 2. Directory structure and theme-based extending
+ *
+ * The component name/ID is seen in its directory name, and the corresponding
+ * YAML and Twig template. In other words, this is the layout:
+ *
+ * <extension> (module or theme)
+ *  |- components
+ *     |- <component name>
+ *        |- <component name>.yml
+ *        |- <component name>.twig
+ *
+ * Concrete example:
+ *
+ * core/modules/system
+ *  |- components
+ *     |- label
+ *        |- label.yml
+ *        |- label.html.twig
+ * cores/themes/classy
+ *  |- components
+ *     |- label
+ *        |- label.yml
+ *        |- label.html.twig
+ * themes/fancy
+ *  |- components
+ *     |- label
+ *        |- label.yml
+ *        |- label.html.twig
+ *
+ * In this example, the custom 'fancy' theme builds on the 'classy' base theme.
+ *
+ * The component extension (inheritance) tree:
+ *  1. module (e.g. 'mymodule')
+ *  2. ancestor base theme (e.g. 'stable')
+ *  3. parent base theme (e.g. 'classy')
+ *  4. theme (e.g. 'fancy')
+ *
+ * Both the 'classy' and 'fancy' themes:
+ * - extend the 'label' component defined by Drupal core's 'system'
+ * - add additional CSS in the YAML file
+ * - add an additional variable in the YAML file
+ * - add more classes in the Twig file
+ *
+ * The end result is a single 'label' component:
+ * - with three asset libraries: 'fancy/label', which depends on 'classy/label',
+ *   which depends on 'system/label'
+ * - with the variables defined in the system module component definition, plus
+ *   the one in 'classy' appended, plus the one in 'fancy' appended
+ *
+ * @see https://www.drupal.org/node/2702061
+ * @todo Move into Drupal\Core\Theme namespace in core.
+ */
+class ComponentDiscovery {
+
+  /**
+   * The app root.
+   *
+   * @var string
+   */
+  protected $root;
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  public function __construct($root, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) {
+    $this->root = $root;
+    $this->moduleHandler = $module_handler;
+    $this->themeHandler = \Drupal::service('theme_handler');
+    $this->themeManager = \Drupal::service('theme.manager');
+  }
+
+  // Note that the active theme can change mid-request! This is how the pattern
+  // library is able to work: it temporarily sets the active theme to 'stark'.
+  protected function getActiveTheme() {
+    return $this->themeManager->getActiveTheme();
+  }
+
+  /**
+   * Components where "extends: false" (or equivalent: no "extends" key at all),
+   * without extensions applied.
+   */
+  public function getBaseComponents() {
+    $base_components = array_filter($this->getModuleComponents(), 'static::isModuleBaseComponent');
+    foreach ($this->getThemeComponents() as $theme => $theme_components) {
+      $theme_base_components = array_filter($theme_components, 'static::isThemeBaseComponent');
+
+      // Extension-level validation.
+      $intersection = array_intersect_key($base_components, $theme_base_components);
+      if (empty($intersection)) {
+        $base_components += $theme_base_components;
+      }
+      else {
+        throw new \LogicException('Theme ' . $theme . ' is redefining components ' . implode(', ', array_keys($intersection)) . '.');
+      }
+    }
+    return $base_components;
+  }
+
+  /**
+   * All components, with extensions applied.
+   */
+  public function getComponents() {
+    $components = $this->getModuleComponents();
+
+    foreach ($this->getThemeComponents() as $theme => $theme_components) {
+      foreach ($theme_components as $id => $theme_component) {
+        // Defining new component.
+        if ($this->isThemeBaseComponent($theme_component)) {
+          if (isset($components[$id])) {
+            throw new \LogicException('Theme ' . $theme . ' is redefining component ' . $id . '.');
+          }
+          $components[$id] = $theme_component;
+        }
+        // Extending existing component.
+        else {
+          $component = &$components[$id];
+
+
+          // Adding more variables is allowed.
+          $component['variables'] = array_merge($component['variables'], $theme_component['variables']);
+
+
+          // Adding more assets is allowed.
+          if (count($theme_component['_asset_libraries'])) {
+            // Update the theme component's asset library to depend on the
+            // asset
+            if (count($component['_asset_libraries'])) {
+              $library = &$theme_component['_asset_libraries'][$theme_component['provider'] . '/' . $id];
+              $library['dependencies'] = array_merge($library['dependencies'], array_keys($component['_asset_libraries']));
+            }
+            $component['_asset_libraries'] = array_merge($component['_asset_libraries'], $theme_component['_asset_libraries']);
+          }
+
+
+          // Anything else is disallowed.
+          $disallowed_keys = ['label', 'documentation'];
+          foreach ($disallowed_keys as $disallowed_key) {
+            if (isset($theme_component[$disallowed_key])) {
+              throw new \LogicException('Theme ' . $theme . ' is trying to override the key ' . $disallowed_key . ' for the component ' . $id . ', this is not allowed.');
+            }
+          }
+
+          // Update provider, and track the tree of providers extending
+          // component.
+          $component['_provider tree'][$theme_component['provider']] = $theme_component;
+        }
+      }
+    }
+    return $components;
+  }
+
+  public function getAssetLibraryForComponent($component_id) {
+    $component = $this->getComponents()[$component_id];
+    $asset_libraries = array_keys($component['_asset_libraries']);
+    return end($asset_libraries);
+  }
+
+  // Components keyed by module. Horizontal extension.
+  protected function getModuleComponents() {
+    return $this->normalizeComponents($this->getModulesDiscovery()->getDefinitions(), 'module');
+  }
+
+  // Components keyed by theme. Vertical extension.
+  protected function getThemeComponents() {
+    $components_by_theme = [];
+    foreach ($this->getThemesDiscoveries() as $theme => $discovery) {
+      $components_by_theme[$theme] = $this->normalizeComponents($discovery->getDefinitions(), 'theme');
+    }
+    return $components_by_theme;
+  }
+
+  // sets defaults and generates asset libraries.
+  protected function normalizeComponents(array $components, $provider_type) {
+    foreach (array_keys($components) as $id) {
+      $component = &$components[$id];
+
+      $this->validateComponent($component);
+
+      // Set defaults.
+      if (!isset($component['variables'])) {
+        $component['variables'] = [];
+      }
+
+      // Generate initial provider tree.
+      if (!isset($component['_provider tree'])) {
+        $component['_provider tree'] = [$component['provider'] => $component];
+      }
+
+      // Generate asset library.
+      $component['_asset_libraries'] = [];
+      if (isset($component['assets']) && (isset($component['assets']['css']) || isset($component['assets']['js']) || isset($component['assets']['dependencies']))) {
+        $library_name = $component['provider'] . '/' . $id;
+
+
+        $library = [
+          'version' => 'VERSION',
+        ];
+        if (isset($component['assets']['css'])) {
+          $smacss_category = ($provider_type === 'module') ? 'component' : 'theme';
+          $library['css'][$smacss_category] = $component['assets']['css'];
+        }
+        $library['js'] = isset($component['assets']['js']) ? $component['assets']['js'] : [];
+        $library['dependencies'] = isset($component['assets']['dependencies']) ? $component['assets']['dependencies'] : [];
+
+
+        $component['_asset_libraries'][$library_name] = $library;
+      }
+
+    }
+    return $components;
+  }
+
+  protected function validateComponent(array $component_definition) {
+    // @todo throw exceptions for anything that is invalid in this component's
+    //       definition — in other words: YAML validation, config schema-style.
+    //       Also verify that any additional variables added by component
+    //       extensions have default values specified, otherwise they can break
+    //       existing code.
+  }
+
+  // In modules, 'extends' defaults to false.
+  protected static function isModuleBaseComponent(array $component_definition) {
+    return !isset($component_definition['extends']) || $component_definition['extends'] === FALSE;
+  }
+
+  // In themes, 'extends' defaults to true.
+  protected static function isThemeBaseComponent(array $component_definition) {
+    return isset($component_definition['extends']) && $component_definition['extends'] === FALSE;
+  }
+
+  // all installed modules
+  protected function getModulesDiscovery() {
+    return new DirectoryWithYamlDiscovery($this->moduleHandler->getModuleDirectories(), 'components');
+  }
+
+  // List of themes, from least to most specific.
+  protected function getOrderedThemes() {
+    return array_merge(array_reverse($this->getActiveTheme()->getBaseThemes()), [$this->getActiveTheme()->getName() => $this->getActiveTheme()]);
+  }
+
+  protected function getThemeDiscovery(ActiveTheme $theme) {
+    return new DirectoryWithYamlDiscovery([$theme->getName() => $this->root . '/' . $theme->getPath()], 'components');
+  }
+
+  protected function getThemesDiscoveries() {
+    return array_map([$this, 'getThemeDiscovery'], $this->getOrderedThemes());
+  }
+
+}
\ No newline at end of file
