diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 03230a0519..0248ad8051 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -1594,6 +1594,9 @@ function install_profile_modules(&$install_state) {
   }
   $modules = array_unique($modules);
   foreach ($modules as $module) {
+    if ($module === 'core') {
+      continue;
+    }
     if (!empty($files[$module]->info['required'])) {
       $required[$module] = $files[$module]->sort;
     }
@@ -1606,6 +1609,9 @@ function install_profile_modules(&$install_state) {
 
   $operations = [];
   foreach ($required + $non_required as $module => $weight) {
+    if ($module === 'core') {
+      continue;
+    }
     $operations[] = ['_install_module_batch', [$module, $files[$module]->info['name']]];
   }
   $batch = [
diff --git a/core/includes/install.inc b/core/includes/install.inc
index bdf4cfcd07..57481dfcc3 100644
--- a/core/includes/install.inc
+++ b/core/includes/install.inc
@@ -1120,15 +1120,33 @@ function install_profile_info($profile, $langcode = 'en') {
       'config_install_path' => NULL,
     ];
     $profile_path = drupal_get_path('profile', $profile);
-    $info = \Drupal::service('info_parser')->parse("$profile_path/$profile.info.yml");
+    $info_path = "$profile_path/$profile.info.yml";
+    $info_parser = \Drupal::service('info_parser');
+    if (file_exists($info_path)) {
+      // Favour the legacy .info.yml file.
+      $info = $info_parser->parse($info_path);
+    }
+    else {
+      $info = $info_parser->parse("$profile_path/composer.json");
+    }
     $info += $defaults;
 
+    // Modern composer.json file.
     $dependency_name_function = function ($dependency) {
       return Dependency::createFromString($dependency)->getName();
     };
-    // Convert dependencies in [project:module] format.
-    $info['dependencies'] = array_map($dependency_name_function, $info['dependencies']);
-
+    if (!empty($info['require'])) {
+      // Remove the entry for core, it is not a module.
+      unset($info['require']['core']);
+      $info['dependencies'] = array_keys($info['require']);
+    }
+    else {
+      $dependency_name_function = function ($dependency) {
+        return Dependency::createFromString($dependency)->getName();
+      };
+      // Convert dependencies in [project:module] format.
+      $info['dependencies'] = array_map($dependency_name_function, $info['dependencies']);
+    }
     // Convert install key in [project:module] format.
     $info['install'] = array_map($dependency_name_function, $info['install']);
 
diff --git a/core/lib/Drupal/Core/Command/UpgradeInfoFilesCommand.php b/core/lib/Drupal/Core/Command/UpgradeInfoFilesCommand.php
new file mode 100644
index 0000000000..4870992d7d
--- /dev/null
+++ b/core/lib/Drupal/Core/Command/UpgradeInfoFilesCommand.php
@@ -0,0 +1,181 @@
+<?php
+
+namespace Drupal\Core\Command;
+
+use Drupal\Component\Version\Constraint;
+use Drupal\Core\DrupalKernel;
+use Drupal\Core\Extension\Dependency;
+use Drupal\Core\Extension\Discovery\RecursiveExtensionFilterIterator;
+use Drupal\Core\Serialization\Yaml;
+use Drupal\Core\Site\Settings;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+/**
+ * Upgrades .info.yml files to composer.json files for a given directory.
+ */
+final class UpgradeInfoFilesCommand extends Command {
+
+  const ERROR_INVALID_DIRECTORY = 1;
+  const ERROR_INVALID_KEY = 2;
+
+  /**
+   * The class loader.
+   *
+   * @var object
+   */
+  protected $classLoader;
+
+  /**
+   * Constructs a new UpgradeInfoFilesCommand command.
+   *
+   * @param object $class_loader
+   *   The class loader.
+   */
+  public function __construct($class_loader) {
+    parent::__construct('upgrade-info-files');
+    $this->classLoader = $class_loader;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function configure() {
+    $this->setDescription('Upgrades .info.yml files to composer.json files for a given directory.')
+      ->addArgument('directory', InputArgument::REQUIRED, 'Path to the folder to find and upgrate .info.yml files in.')
+      ->addOption('delete', 'd', InputOption::VALUE_NONE, 'Pass -d to delete the original info files.')
+      ->addUsage('modules/contrib/token -d')
+      ->addUsage('modules/contrib/entity_hierarchy');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function execute(InputInterface $input, OutputInterface $output) {
+    $io = new SymfonyStyle($input, $output);
+    $kernel = new DrupalKernel('prod', $this->classLoader, FALSE);
+    $kernel::bootEnvironment();
+    $kernel->setSitePath($this->getSitePath());
+    Settings::initialize($kernel->getAppRoot(), $kernel->getSitePath(), $this->classLoader);
+    $dir = $input->getArgument('directory');
+    $absolute_dir = ($dir == '' ? $kernel->getAppRoot() : $kernel->getAppRoot() . "/$dir");
+
+    if (!is_dir($absolute_dir)) {
+      $io->error(sprintf('%s is not a directory', $absolute_dir));
+      return static::ERROR_INVALID_DIRECTORY;
+    }
+    // Use Unix paths regardless of platform, skip dot directories, follow
+    // symlinks (to allow extensions to be linked from elsewhere), and return
+    // the RecursiveDirectoryIterator instance to have access to getSubPath(),
+    // since SplFileInfo does not support relative paths.
+    $flags = \FilesystemIterator::UNIX_PATHS;
+    $flags |= \FilesystemIterator::SKIP_DOTS;
+    $flags |= \FilesystemIterator::FOLLOW_SYMLINKS;
+    $flags |= \FilesystemIterator::CURRENT_AS_SELF;
+    $directory_iterator = new \RecursiveDirectoryIterator($absolute_dir, $flags);
+
+    // Allow directories specified in settings.php to be ignored. You can use
+    // this to not check for files in common special-purpose directories. For
+    // example, node_modules and bower_components. Ignoring irrelevant
+    // directories is a performance boost.
+    $ignore_directories = Settings::get('file_scan_ignore_directories', []);
+
+    // Filter the recursive scan to discover extensions only.
+    // Important: Without a RecursiveFilterIterator, RecursiveDirectoryIterator
+    // would recurse into the entire filesystem directory tree without any kind
+    // of limitations.
+    $filter = new RecursiveExtensionFilterIterator($directory_iterator, $ignore_directories);
+    $filter->acceptTests(TRUE);
+
+    // The actual recursive filesystem scan is only invoked by instantiating the
+    // RecursiveIteratorIterator.
+    $iterator = new \RecursiveIteratorIterator($filter,
+      \RecursiveIteratorIterator::LEAVES_ONLY,
+      // Suppress filesystem errors in case a directory cannot be accessed.
+      \RecursiveIteratorIterator::CATCH_GET_CHILD
+    );
+
+    $return = 0;
+    $progress = $io->createProgressBar();
+    $progress->setFormat("%current%/%max% [%bar%]\n%message%\n");
+    $progress->setMessage('Processing info files');
+    $progress->start(iterator_count($iterator));
+    foreach ($iterator as $key => $fileinfo) {
+      if ($fileinfo->getBasename('.json') === 'composer') {
+        // RecursiveExtensionFilterIterator also matches composer.json files.
+        $progress->advance();
+        continue;
+      }
+      $file = $fileinfo->openFile('r');
+      $details = Yaml::decode(($file->fread($file->getSize())));
+      $folder = $fileinfo->getPath();
+      $out = [];
+      $composer_path = $folder . '/composer.json';
+      if (file_exists($composer_path)) {
+        $out = json_decode(file_get_contents($composer_path), TRUE);
+      }
+      $out += [
+        'type' => 'drupal-' . $details['type'],
+        'name' => 'drupal/' . $fileinfo->getBasename('.info.yml'),
+        'extra' => [],
+      ];
+      $out['extra'] += ['drupal' => []];
+      foreach ($details as $key => $value) {
+        switch ($key) {
+          case 'type':
+            break;
+
+          case 'description':
+          case 'version':
+            $out[$key] = $value;
+            break;
+
+          case 'core':
+            list ($core,) = explode('.', $value);
+            $out['require']['drupal/core'] = "~" . $core;
+            break;
+
+          case 'dependencies':
+            foreach ($value as $item) {
+              $dependency = Dependency::createFromString($item);
+              $out['require']['drupal/' . $dependency->getName()] = (new Constraint($dependency->getConstraintString(), $details['core']))->getComposerConstraint();
+            }
+            break;
+
+          default:
+            $out['extra']['drupal'][$key] = $value;
+            break;
+
+        }
+      }
+      file_put_contents($composer_path, json_encode($out, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
+      $progress->setMessage(sprintf('Processed %s', $fileinfo->getPathname()));
+      if ($input->getOption('delete')) {
+        $file = NULL;
+        unlink($fileinfo->getPathname());
+        $progress->setMessage(sprintf('Deleted %s', $fileinfo->getPathname()));
+      }
+      $progress->advance();
+    }
+    return $return;
+
+  }
+
+  /**
+   * Gets the site path.
+   *
+   * Defaults to 'sites/default'. For testing purposes this can be overridden
+   * using the DRUPAL_DEV_SITE_PATH environment variable.
+   *
+   * @return string
+   *   The site path to use.
+   */
+  protected function getSitePath() {
+    return getenv('DRUPAL_DEV_SITE_PATH') ?: 'sites/default';
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Extension/ComposerExtension.php b/core/lib/Drupal/Core/Extension/ComposerExtension.php
new file mode 100644
index 0000000000..c10f5dce38
--- /dev/null
+++ b/core/lib/Drupal/Core/Extension/ComposerExtension.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Drupal\Core\Extension;
+
+/**
+ * Defines a class for an extension discovered using composer.
+ */
+class ComposerExtension extends Extension {
+
+  /**
+   * The extension name.
+   *
+   * @var string
+   */
+  protected $name;
+
+  /**
+   * Constructs a new ComposerExtension object.
+   *
+   * @param string $root
+   *   The app root.
+   * @param string $type
+   *   The type of the extension; e.g., 'module'.
+   * @param string $pathname
+   *   The relative path and filename of the extension's composer file; e.g.,
+   *   'core/modules/node/composer.json'.
+   * @param string $name
+   *   The extension name.
+   * @param string $filename
+   *   (optional) The filename of the main extension file; e.g., 'node.module'.
+   */
+  public function __construct($root, $type, $pathname, $name, $filename = NULL) {
+    parent::__construct($root, $type, $pathname, $filename);
+    $this->name = $name;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getName() {
+    return $this->name;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Extension/Dependency.php b/core/lib/Drupal/Core/Extension/Dependency.php
index c9e386805a..a8b59d7c80 100644
--- a/core/lib/Drupal/Core/Extension/Dependency.php
+++ b/core/lib/Drupal/Core/Extension/Dependency.php
@@ -3,6 +3,7 @@
 namespace Drupal\Core\Extension;
 
 use Drupal\Component\Version\Constraint;
+use Drupal\Core\Extension\Dependency\DependencyInterface;
 
 /**
  * A value object representing dependency information.
@@ -11,8 +12,11 @@
  * for Drupal 8.x. This will be removed before Drupal 9.x.
  *
  * @see https://www.drupal.org/node/2756875
+ * @deprecated \Drupal\Core\Extension\Dependency is deprecated in drupal:8.8.0.
+ *   Use \Drupal\Core\Extension\Dependency\Composer instead. See
+ *   https://www.drupal.org/node/3047893
  */
-class Dependency implements \ArrayAccess {
+class Dependency implements \ArrayAccess, DependencyInterface {
 
   /**
    * The name of the dependency.
@@ -56,13 +60,11 @@ public function __construct($name, $project, $constraint) {
     $this->name = $name;
     $this->project = $project;
     $this->constraintString = $constraint;
+    @trigger_error('\Drupal\Core\Extension\Dependency is deprecated in drupal:8.8.0. Use \Drupal\Core\Extension\Dependency\Composer instead. See https://www.drupal.org/node/3047893', E_USER_DEPRECATED);
   }
 
   /**
-   * Gets the dependency's name.
-   *
-   * @return string
-   *   The dependency's name.
+   * {@inheritdoc}
    */
   public function getName() {
     return $this->name;
@@ -102,13 +104,7 @@ protected function getConstraint() {
   }
 
   /**
-   * Determines if the provided version is compatible with this dependency.
-   *
-   * @param string $version
-   *   The version to check, for example '4.2'.
-   *
-   * @return bool
-   *   TRUE if compatible with the provided version, FALSE if not.
+   * {@inheritdoc}
    */
   public function isCompatible($version) {
     return $this->getConstraint()->isCompatible($version);
diff --git a/core/lib/Drupal/Core/Extension/Dependency/Composer.php b/core/lib/Drupal/Core/Extension/Dependency/Composer.php
new file mode 100644
index 0000000000..054d2f3bfc
--- /dev/null
+++ b/core/lib/Drupal/Core/Extension/Dependency/Composer.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Drupal\Core\Extension\Dependency;
+
+use Composer\Semver\Semver;
+
+/**
+ * Defines a dependency from a composer require definition.
+ */
+class Composer implements DependencyInterface {
+
+  /**
+   * Constraint string.
+   *
+   * @var string
+   */
+  protected $constraint;
+
+  /**
+   * Dependency name.
+   *
+   * @var string
+   */
+  protected $name;
+
+  /**
+   * Constructs a new Composer.
+   *
+   * @param string $constraint
+   *   String constraint.
+   * @param $name
+   */
+  public function __construct($constraint, $name) {
+    $this->constraint = $constraint;
+    $this->name = $name;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getName() {
+    return $this->name;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isCompatible($version) {
+    return Semver::satisfies($version, $this->constraint);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConstraintString() {
+    return $this->constraint;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Extension/Dependency/DependencyInterface.php b/core/lib/Drupal/Core/Extension/Dependency/DependencyInterface.php
new file mode 100644
index 0000000000..0686906c96
--- /dev/null
+++ b/core/lib/Drupal/Core/Extension/Dependency/DependencyInterface.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\Core\Extension\Dependency;
+
+/**
+ * Defines an interface for a project dependency.
+ */
+interface DependencyInterface {
+
+  /**
+   * Determines if the provided version is compatible with this dependency.
+   *
+   * @param string $version
+   *   The version to check, for example '4.2'.
+   *
+   * @return bool
+   *   TRUE if compatible with the provided version, FALSE if not.
+   */
+  public function isCompatible($version);
+
+  /**
+   * Gets dependency name.
+   *
+   * @return string
+   *   Dependency name.
+   */
+  public function getName();
+
+  /**
+   * Gets constraint string from the dependency.
+   *
+   * @return string
+   *   The constraint string.
+   */
+  public function getConstraintString();
+
+}
diff --git a/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php b/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php
index ac69f01a0c..8c2f0f80c9 100644
--- a/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php
+++ b/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php
@@ -157,8 +157,8 @@ public function accept() {
       return !in_array($name, $this->blacklist, TRUE);
     }
     else {
-      // Only accept extension info files.
-      return substr($name, -9) == '.info.yml';
+      // Only accept extension info or composer.json files.
+      return $name === 'composer.json'|| substr($name, -9) == '.info.yml';
     }
   }
 
diff --git a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
index 17c0e5b4a3..101026ae9e 100644
--- a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
+++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
@@ -441,31 +441,64 @@ protected function scanDirectory($dir, $include_tests) {
       \RecursiveIteratorIterator::CATCH_GET_CHILD
     );
 
+    $name_map = [];
     foreach ($iterator as $key => $fileinfo) {
-      // All extension names in Drupal have to be valid PHP function names due
-      // to the module hook architecture.
-      if (!preg_match(static::PHP_FUNCTION_PATTERN, $fileinfo->getBasename('.info.yml'))) {
-        continue;
-      }
-
       if ($this->fileCache && $cached_extension = $this->fileCache->get($fileinfo->getPathName())) {
         $files[$cached_extension->getType()][$key] = $cached_extension;
         continue;
       }
-
-      // Determine extension type from info file.
-      $type = FALSE;
-      $file = $fileinfo->openFile('r');
-      while (!$type && !$file->eof()) {
-        preg_match('@^type:\s*(\'|")?(\w+)\1?\s*$@', $file->fgets(), $matches);
-        if (isset($matches[2])) {
-          $type = $matches[2];
+      $composer = FALSE;
+      if ($fileinfo->getBasename('.json') === 'composer') {
+        $composer = TRUE;
+        $file = $fileinfo->openFile('r');
+        $details = json_decode($file->fread($file->getSize()), TRUE);
+        if (!isset($details['type']) || !isset($details['name'])) {
+          continue;
+        }
+        list($namespace, $name) = explode('/', $details['name']);
+        // All extension names in Drupal have to be valid PHP function names due
+        // to the module hook architecture.
+        if ($namespace !== 'drupal' || !preg_match(static::PHP_FUNCTION_PATTERN, $name)) {
+          continue;
+        }
+        // Type should be in format drupal-module, drupal-theme etc.
+        if (strpos($details['type'], '-') === FALSE) {
+          // Invalid type specification.
+          continue;
+        }
+        list($project, $type) = explode('-', $details['type']);
+        if (isset($name_map[$type][$name])) {
+          // We already have a legacy info.yml file for this.
+          continue;
+        }
+        if ($project !== 'drupal' || !in_array($type, ['module', 'theme', 'theme_engine', 'profile'])) {
+          continue;
         }
       }
-      if (empty($type)) {
-        continue;
+      else {
+        @trigger_error('Using an info.yml file for defining modules, themes, theme engines and profiles is deprecated in drupal:8.8.0. It will be removed from drupal:9.0. Use the upgrade command to convert the info file to composer.json. See https://www.drupal.org/node/3047893', E_USER_DEPRECATED);
+        // All extension names in Drupal have to be valid PHP function names due
+        // to the module hook architecture.
+        $name = $fileinfo->getBasename('.info.yml');
+        if (!preg_match(static::PHP_FUNCTION_PATTERN, $name)) {
+          continue;
+        }
+
+        // Determine extension type from info file.
+        $type = FALSE;
+        $file = $fileinfo->openFile('r');
+        while (!$type && !$file->eof()) {
+          preg_match('@^type:\s*(\'|")?(\w+)\1?\s*$@', $file->fgets(), $matches);
+          if (isset($matches[2])) {
+            $type = $matches[2];
+          }
+        }
+        if (empty($type) || isset($name_map[$type][$name])) {
+          // No type is given or we've already found a composer.json for this
+          // module.
+          continue;
+        }
       }
-      $name = $fileinfo->getBasename('.info.yml');
       $pathname = $dir_prefix . $fileinfo->getSubPathname();
 
       // Determine whether the extension has a main extension file.
@@ -481,7 +514,22 @@ protected function scanDirectory($dir, $include_tests) {
         $filename = NULL;
       }
 
-      $extension = new Extension($this->root, $type, $pathname, $filename);
+      if ($composer) {
+        $extension = new ComposerExtension($this->root, $type, $pathname, $name, $filename);
+      }
+      else {
+        $extension = new Extension($this->root, $type, $pathname, $filename);
+        if (isset($name_map[$type][$name])) {
+          // We found both a composer and an info file, give precedence to the
+          // info file under the assumption the module/theme has its own
+          // composer dependencies and is yet to remove the .info.yml file.
+          if ($this->fileCache) {
+            $this->fileCache->delete($name_map[$type][$name]);
+          }
+          unset($files[$type][$name_map[$type][$name]]);
+        }
+      }
+      $name_map[$type][$name] = $fileinfo->getPathname();
 
       // Track the originating directory for sorting purposes.
       $extension->subpath = $fileinfo->getSubPath();
diff --git a/core/lib/Drupal/Core/Extension/InfoParserDynamic.php b/core/lib/Drupal/Core/Extension/InfoParserDynamic.php
index 9eb0a56e24..324fc33984 100644
--- a/core/lib/Drupal/Core/Extension/InfoParserDynamic.php
+++ b/core/lib/Drupal/Core/Extension/InfoParserDynamic.php
@@ -15,42 +15,13 @@ class InfoParserDynamic implements InfoParserInterface {
    */
   public function parse($filename) {
     if (!file_exists($filename)) {
-      $parsed_info = [];
+      return [];
     }
-    else {
-      try {
-        $parsed_info = Yaml::decode(file_get_contents($filename));
-      }
-      catch (InvalidDataTypeException $e) {
-        throw new InfoParserException("Unable to parse $filename " . $e->getMessage());
-      }
-      $missing_keys = array_diff($this->getRequiredKeys(), array_keys($parsed_info));
-      if (!empty($missing_keys)) {
-        throw new InfoParserException('Missing required keys (' . implode(', ', $missing_keys) . ') in ' . $filename);
-      }
-      if (isset($parsed_info['version']) && $parsed_info['version'] === 'VERSION') {
-        $parsed_info['version'] = \Drupal::VERSION;
-      }
-      // Special backwards compatible handling profiles and their 'dependencies'
-      // key.
-      if ($parsed_info['type'] === 'profile' && isset($parsed_info['dependencies']) && !array_key_exists('install', $parsed_info)) {
-        // Only trigger the deprecation message if we are actually using the
-        // profile with the missing 'install' key. This avoids triggering the
-        // deprecation when scanning all the available install profiles.
-        global $install_state;
-        if (isset($install_state['parameters']['profile'])) {
-          $pattern = '@' . preg_quote(DIRECTORY_SEPARATOR . $install_state['parameters']['profile'] . '.info.yml') . '$@';
-          if (preg_match($pattern, $filename)) {
-            @trigger_error("The install profile $filename only implements a 'dependencies' key. As of Drupal 8.6.0 profile's support a new 'install' key for modules that should be installed but not depended on. See https://www.drupal.org/node/2952947.", E_USER_DEPRECATED);
-          }
-        }
-        // Move dependencies to install so that if a profile has both
-        // dependencies and install then dependencies are real.
-        $parsed_info['install'] = $parsed_info['dependencies'];
-        $parsed_info['dependencies'] = [];
-      }
+    $basename = basename($filename);
+    if ($basename === 'composer.json') {
+      return $this->doParseComposerFile($filename);
     }
-    return $parsed_info;
+    return $this->doParseInfoYmlFile($filename);
   }
 
   /**
@@ -63,4 +34,92 @@ protected function getRequiredKeys() {
     return ['type', 'core', 'name'];
   }
 
+  /**
+   * Parses an info.yml file.
+   *
+   * @param string $filename
+   *   Filename.
+   *
+   * @return array
+   *   Parsed info file.
+   */
+  protected function doParseInfoYmlFile($filename) {
+    try {
+      $parsed_info = Yaml::decode(file_get_contents($filename));
+    }
+    catch (InvalidDataTypeException $e) {
+      throw new InfoParserException("Unable to parse $filename " . $e->getMessage());
+    }
+    $missing_keys = array_diff($this->getRequiredKeys(), array_keys($parsed_info));
+    if (!empty($missing_keys)) {
+      throw new InfoParserException('Missing required keys (' . implode(', ', $missing_keys) . ') in ' . $filename);
+    }
+    if (isset($parsed_info['version']) && $parsed_info['version'] === 'VERSION') {
+      $parsed_info['version'] = \Drupal::VERSION;
+    }
+    // Special backwards compatible handling profiles and their 'dependencies'
+    // key.
+    if ($parsed_info['type'] === 'profile' && isset($parsed_info['dependencies']) && !array_key_exists('install', $parsed_info)) {
+      // Only trigger the deprecation message if we are actually using the
+      // profile with the missing 'install' key. This avoids triggering the
+      // deprecation when scanning all the available install profiles.
+      global $install_state;
+      if (isset($install_state['parameters']['profile'])) {
+        $pattern = '@' . preg_quote(DIRECTORY_SEPARATOR . $install_state['parameters']['profile'] . '.info.yml') . '$@';
+        if (preg_match($pattern, $filename)) {
+          @trigger_error("The install profile $filename only implements a 'dependencies' key. As of Drupal 8.6.0 profile's support a new 'install' key for modules that should be installed but not depended on. See https://www.drupal.org/node/2952947.", E_USER_DEPRECATED);
+        }
+      }
+      // Move dependencies to install so that if a profile has both
+      // dependencies and install then dependencies are real.
+      $parsed_info['install'] = $parsed_info['dependencies'];
+      $parsed_info['dependencies'] = [];
+    }
+    return $parsed_info;
+  }
+
+  /**
+   * Parses a composer.json file.
+   *
+   * @param string $filename
+   *   Filename.
+   *
+   * @return array
+   *   Parsed info file.
+   */
+  protected function doParseComposerFile($filename) {
+    if (!$parsed_info = json_decode(file_get_contents($filename), TRUE)) {
+      throw new InfoParserException("Unable to parse $filename " . json_last_error_msg());
+    }
+    $missing_keys = array_diff(['type', 'name'], array_keys($parsed_info));
+    if (!empty($missing_keys)) {
+      throw new InfoParserException('Missing required keys (' . implode(', ', $missing_keys) . ') in ' . $filename);
+    }
+    // Get a fallback-name from the project name, e.g. drupal/system.
+    $parsed_info += ['require' => [], 'extra' => []];
+    $name = $parsed_info['name'];
+    list(, $machine_name) = explode('/', $name);
+    // Build defaults for the extra key.
+    $parsed_info['extra'] += ['drupal' => []];
+    $parsed_info['extra']['drupal'] += ['name' => $machine_name];
+    // Copy values from extra into the parsed info.
+    $parsed_info = $parsed_info['extra']['drupal'] + $parsed_info;
+    if (isset($parsed_info['version']) && $parsed_info['version'] === 'VERSION') {
+      $parsed_info['version'] = \Drupal::VERSION;
+    }
+    $require = $parsed_info['require'];
+    $parsed_info['require'] = [];
+    $parsed_info['dependencies'] = [];
+    foreach ($require as $project => $constraint) {
+      list($namespace, $name) = explode('/', $project);
+      if ($namespace !== 'drupal') {
+        continue;
+      }
+      $parsed_info['dependencies'][] = sprintf('%s (%s)', $name, $constraint);
+      $parsed_info['require'][$name] = $constraint;
+    }
+    $parsed_info['type'] = str_replace('drupal-', '', $parsed_info['type']);
+    return $parsed_info;
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Extension/ModuleExtensionList.php b/core/lib/Drupal/Core/Extension/ModuleExtensionList.php
index c8f492bd4f..6026efeada 100644
--- a/core/lib/Drupal/Core/Extension/ModuleExtensionList.php
+++ b/core/lib/Drupal/Core/Extension/ModuleExtensionList.php
@@ -219,8 +219,20 @@ protected function getInstalledExtensionNames() {
    */
   protected function ensureRequiredDependencies(Extension $module, array $modules = []) {
     if (!empty($module->info['required'])) {
-      foreach ($module->info['dependencies'] as $dependency) {
-        $dependency_name = Dependency::createFromString($dependency)->getName();
+      // Modern composer.json file.
+      if (!empty($module->info['require'])) {
+        $dependencies = array_keys($module->info['require']);
+      }
+      else {
+        // Legacy .info.yml files.
+        $dependencies = array_map(function ($dependency) {
+          return Dependency::createFromString($dependency)->getName();
+        }, $module->info['dependencies']);
+      }
+      foreach ($dependencies as $dependency_name) {
+        if ($dependency_name === 'core') {
+          continue;
+        }
         if (!isset($modules[$dependency_name]->info['required'])) {
           $modules[$dependency_name]->info['required'] = TRUE;
           $modules[$dependency_name]->info['explanation'] = $this->t('Dependency of required module @module', ['@module' => $module->info['name']]);
diff --git a/core/lib/Drupal/Core/Extension/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php
index 3b60d528bf..4a5f6a0627 100644
--- a/core/lib/Drupal/Core/Extension/ModuleHandler.php
+++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php
@@ -5,6 +5,7 @@
 use Drupal\Component\Graph\Graph;
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\Dependency\Composer;
 use Drupal\Core\Extension\Exception\UnknownExtensionException;
 
 /**
@@ -223,7 +224,12 @@ protected function add($type, $name, $path) {
   public function buildModuleDependencies(array $modules) {
     foreach ($modules as $module) {
       $graph[$module->getName()]['edges'] = [];
-      if (isset($module->info['dependencies']) && is_array($module->info['dependencies'])) {
+      if (isset($module->info['require']) && is_array($module->info['require'])) {
+        foreach ($module->info['require'] as $name => $constraint) {
+          $graph[$module->getName()]['edges'][$name] = new Composer($constraint, $name);
+        }
+      }
+      elseif (isset($module->info['dependencies']) && is_array($module->info['dependencies'])) {
         foreach ($module->info['dependencies'] as $dependency) {
           $dependency_data = Dependency::createFromString($dependency);
           $graph[$module->getName()]['edges'][$dependency_data->getName()] = $dependency_data;
diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
index 86cc9bc44d..3e46051fef 100644
--- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php
+++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
@@ -101,6 +101,9 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
       // the foreach loop continues.
       foreach ($module_list as $module => $value) {
         foreach (array_keys($module_data[$module]->requires) as $dependency) {
+          if ($dependency === 'core') {
+            continue;
+          }
           if (!isset($module_data[$dependency])) {
             // The dependency does not exist.
             throw new MissingDependencyException("Unable to install modules: module '$module' is missing its dependency module $dependency.");
diff --git a/core/lib/Drupal/Core/Extension/ThemeExtensionList.php b/core/lib/Drupal/Core/Extension/ThemeExtensionList.php
index 07a2f3f8f6..23e794fad7 100644
--- a/core/lib/Drupal/Core/Extension/ThemeExtensionList.php
+++ b/core/lib/Drupal/Core/Extension/ThemeExtensionList.php
@@ -256,6 +256,7 @@ protected function createExtensionInfo(Extension $extension) {
     if (!empty($info['base theme'])) {
       // Add the base theme as a proper dependency.
       $info['dependencies'][] = $info['base theme'];
+      $info['require'][$info['base theme']] = '';
     }
 
     // Prefix screenshot with theme path.
diff --git a/core/lib/Drupal/Core/Extension/ThemeInstaller.php b/core/lib/Drupal/Core/Extension/ThemeInstaller.php
index e1149e7b66..96126ccd9e 100644
--- a/core/lib/Drupal/Core/Extension/ThemeInstaller.php
+++ b/core/lib/Drupal/Core/Extension/ThemeInstaller.php
@@ -126,6 +126,9 @@ public function install(array $theme_list, $install_dependencies = TRUE) {
         // Add dependencies to the list. The new themes will be processed as
         // the parent foreach loop continues.
         foreach (array_keys($theme_data[$theme]->requires) as $dependency) {
+          if ($dependency === 'core') {
+            continue;
+          }
           if (!isset($theme_data[$dependency])) {
             // The dependency does not exist.
             return FALSE;
diff --git a/core/modules/system/src/Form/ModulesListForm.php b/core/modules/system/src/Form/ModulesListForm.php
index c2ade11b7a..ffc6523b66 100644
--- a/core/modules/system/src/Form/ModulesListForm.php
+++ b/core/modules/system/src/Form/ModulesListForm.php
@@ -307,7 +307,7 @@ protected function buildRow(array $modules, Extension $module, $distribution) {
     }
 
     // If this module requires other modules, add them to the array.
-    /** @var \Drupal\Core\Extension\Dependency $dependency_object */
+    /** @var \Drupal\Core\Extension\Dependency\DependencyInterface $dependency_object */
     foreach ($module->requires as $dependency => $dependency_object) {
       if (!isset($modules[$dependency])) {
         $row['#requires'][$dependency] = $this->t('@module (<span class="admin-missing">missing</span>)', ['@module' => Unicode::ucfirst($dependency)]);
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index 2b626c5901..bbbcd03854 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -811,7 +811,7 @@ function system_requirements($phase) {
         $requirements['php']['severity'] = REQUIREMENT_ERROR;
       }
       // Check the module's required modules.
-      /** @var \Drupal\Core\Extension\Dependency $requirement */
+      /** @var \Drupal\Core\Extension\Dependency\DependencyInterface $requirement */
       foreach ($file->requires as $requirement) {
         $required_module = $requirement->getName();
         // Check if the module exists.
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 014153958e..476283bc3d 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -998,8 +998,20 @@ function system_get_info($type, $name = NULL) {
 function _system_rebuild_module_data_ensure_required($module, &$modules) {
   @trigger_error("_system_rebuild_module_data_ensure_required() is deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. This function is no longer used in Drupal core. See https://www.drupal.org/node/2709919", E_USER_DEPRECATED);
   if (!empty($module->info['required'])) {
-    foreach ($module->info['dependencies'] as $dependency) {
-      $dependency_name = Dependency::createFromString($dependency)->getName();
+    // Modern composer.json file.
+    if (!empty($module->info['require'])) {
+      $dependencies = array_keys($module->info['require']);
+    }
+    else {
+      // Legacy .info.yml files.
+      $dependencies = array_map(function ($dependency) {
+        return Dependency::createFromString($dependency)->getName();
+      }, $module->info['dependencies']);
+    }
+    foreach ($dependencies as $dependency_name) {
+      if ($dependency_name === 'core') {
+        continue;
+      }
       if (!isset($modules[$dependency_name]->info['required'])) {
         $modules[$dependency_name]->info['required'] = TRUE;
         $modules[$dependency_name]->info['explanation'] = t('Dependency of required module @module', ['@module' => $module->info['name']]);
diff --git a/core/scripts/drupal b/core/scripts/drupal
old mode 100644
new mode 100755
index 7f71228b03..262a0e94aa
--- a/core/scripts/drupal
+++ b/core/scripts/drupal
@@ -9,6 +9,7 @@
 use Drupal\Core\Command\QuickStartCommand;
 use Drupal\Core\Command\InstallCommand;
 use Drupal\Core\Command\ServerCommand;
+use Drupal\Core\Command\UpgradeInfoFilesCommand;
 use Symfony\Component\Console\Application;
 
 if (PHP_SAPI !== 'cli') {
@@ -22,5 +23,6 @@ $application = new Application('drupal', \Drupal::VERSION);
 $application->add(new QuickStartCommand());
 $application->add(new InstallCommand($classloader));
 $application->add(new ServerCommand($classloader));
+$application->add(new UpgradeInfoFilesCommand($classloader));
 
 $application->run();
diff --git a/core/tests/Drupal/Tests/Core/Extension/DependencyTest.php b/core/tests/Drupal/Tests/Core/Extension/DependencyTest.php
index d1d403f04d..8dcc6dbfb8 100644
--- a/core/tests/Drupal/Tests/Core/Extension/DependencyTest.php
+++ b/core/tests/Drupal/Tests/Core/Extension/DependencyTest.php
@@ -9,6 +9,7 @@
 /**
  * @coversDefaultClass \Drupal\Core\Extension\Dependency
  * @group Extension
+ * @group legacy
  */
 class DependencyTest extends UnitTestCase {
 
diff --git a/core/tests/Drupal/Tests/Core/Extension/ExtensionDiscoveryDeprecationTest.php b/core/tests/Drupal/Tests/Core/Extension/ExtensionDiscoveryDeprecationTest.php
new file mode 100644
index 0000000000..d17d50b3bf
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Extension/ExtensionDiscoveryDeprecationTest.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Drupal\Tests\Core\Extension;
+
+use Drupal\Core\Extension\ExtensionDiscovery;
+use Drupal\Core\Serialization\Yaml;
+use Drupal\Tests\UnitTestCase;
+use org\bovigo\vfs\vfsStream;
+
+/**
+ * Defines a class for testing .info.yml files trigger deprecation warnings.
+ *
+ * @group Extension
+ * @group legacy
+ */
+class ExtensionDiscoveryDeprecationTest extends UnitTestCase {
+
+  /**
+   * @expectedDeprecation Using an info.yml file for defining modules, themes, theme engines and profiles is deprecated in drupal:8.8.0. It will be removed from drupal:9.0. Use the upgrade command to convert the info file to composer.json. See https://www.drupal.org/node/3047893
+   */
+  public function testInfoFileTriggersDeprecation() {
+    $vfs = vfsStream::setup('root', NULL, [
+      'core' => [
+        'modules' => [
+          'system' => [
+            'system.info.yml' => Yaml::encode([
+              'type' => 'module',
+              'name' => "System module",
+              'core' => '8.x',
+            ]),
+          ],
+        ],
+        'profiles' => [
+          'myprofile' => Yaml::encode([
+            'type' => 'profile',
+            'name' => "My profile",
+            'core' => '8.x',
+          ]),
+        ],
+      ],
+    ]);
+    $root = $vfs->url();
+
+    $this->assertFileExists($root . '/core/modules/system/system.info.yml');
+    // Create an ExtensionDiscovery with $root.
+    $extension_discovery = new ExtensionDiscovery($root, FALSE, NULL, 'sites/default');
+    $extension_discovery->setProfileDirectories(['myprofile' => 'profiles/myprofile']);
+    $extension_discovery->scan('module', FALSE);
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Extension/ExtensionDiscoveryTest.php b/core/tests/Drupal/Tests/Core/Extension/ExtensionDiscoveryTest.php
index 3ec2e9630b..242a9146a0 100644
--- a/core/tests/Drupal/Tests/Core/Extension/ExtensionDiscoveryTest.php
+++ b/core/tests/Drupal/Tests/Core/Extension/ExtensionDiscoveryTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\Core\Extension;
 
+use Drupal\Component\Serialization\Json;
 use Drupal\Core\Extension\Extension;
 use Drupal\Core\Extension\ExtensionDiscovery;
 use Drupal\Tests\UnitTestCase;
@@ -13,6 +14,7 @@
  *
  * @coversDefaultClass \Drupal\Core\Extension\ExtensionDiscovery
  * @group Extension
+ * @group legacy
  */
 class ExtensionDiscoveryTest extends UnitTestCase {
 
@@ -32,6 +34,13 @@ public function testExtensionDiscoveryVfs() {
 
     $this->assertFileExists($root . '/core/modules/system/system.module');
     $this->assertFileExists($root . '/core/modules/system/system.info.yml');
+    $this->assertFileExists($root . '/core/modules/gridlog/composer.json');
+    $this->assertFileExists($root . '/core/modules/library/composer.json');
+    $this->assertFileExists($root . '/core/theme/wrongproject/composer.json');
+    $this->assertFileExists($root . '/core/theme/wrongtype/composer.json');
+    $this->assertFileExists($root . '/core/theme/messed_up/composer.json');
+    $this->assertFileExists($root . '/core/modules/both/composer.json');
+    $this->assertFileExists($root . '/core/modules/both/both.info.yml');
 
     // Create an ExtensionDiscovery with $root.
     $extension_discovery = new ExtensionDiscovery($root, FALSE, NULL, 'sites/default');
@@ -92,6 +101,7 @@ protected function populateFilesystemStructure(array &$filesystem_structure) {
         'type' => 'profile',
       ],
       'core/modules/user/user.info.yml' => [],
+      'core/modules/both/both.info.yml' => [],
       'profiles/otherprofile/modules/otherprofile_nested_module/otherprofile_nested_module.info.yml' => [],
       'core/modules/system/system.info.yml' => [],
       'core/themes/seven/seven.info.yml' => [
@@ -127,6 +137,48 @@ protected function populateFilesystemStructure(array &$filesystem_structure) {
     $content_by_file['core/modules/system/system.module'] = '<?php';
     $content_by_file['core/themes/engines/twig/twig.engine'] = '<?php';
 
+    // Two files for composer-based discovery.
+    list($major, $minor) = explode('.', \Drupal::VERSION, 3);
+    $core_version = sprintf('~%s.%s', $major, $minor);
+    $content_by_file['core/modules/gridlog/composer.json'] = Json::encode([
+      'name' => 'drupal/gridlog',
+      'description' => 'Everything logs to my grid',
+      'type' => 'drupal-module',
+      'require' => ['drupal/system' => $core_version],
+    ]);
+    $content_by_file['core/modules/library/composer.json'] = Json::encode([
+      'name' => 'library/not-a-module',
+      'description' => 'Books n stuff',
+      'type' => 'library',
+      'require' => ['drupal/system' => $core_version],
+    ]);
+    $content_by_file['core/theme/wrongproject/composer.json'] = Json::encode([
+      'name' => 'drupal/wrongtype',
+      'description' => 'A composer file with an invalid type',
+      'type' => 'smurfcore-plugin',
+      'require' => ['drupal/system' => $core_version],
+    ]);
+    $content_by_file['core/theme/wrongtype/composer.json'] = Json::encode([
+      'name' => 'drupal/wrongtype',
+      'description' => 'A composer file with an invalid type',
+      'type' => 'drupal-widget',
+      'require' => ['drupal/system' => $core_version],
+    ]);
+    $content_by_file['core/theme/dudtype/composer.json'] = Json::encode([
+      'name' => 'drupal/dudtype',
+      'description' => 'A composer file with an malformed type',
+      'type' => 'powersauce',
+      'require' => ['drupal/system' => $core_version],
+    ]);
+    $content_by_file['core/modules/both/composer.json'] = Json::encode([
+      'name' => 'drupal/both',
+      'description' => 'A module that has both a composer.json and an info file',
+      'type' => 'drupal-module',
+      'require' => ['drupal/system' => $core_version],
+    ]);
+    $content_by_file['core/theme/messed_up/composer.json'] = 'this is not json';
+    $files_by_type_and_name_expected['module']['gridlog'] = 'core/modules/gridlog/composer.json';
+
     foreach ($content_by_file as $file => $content) {
       $pieces = explode('/', $file);
       $this->addFileToFilesystemStructure($filesystem_structure, $pieces, $content);
diff --git a/core/tests/Drupal/Tests/Core/Extension/InfoParserUnitTest.php b/core/tests/Drupal/Tests/Core/Extension/InfoParserUnitTest.php
index 796a916db9..0f7073e2d9 100644
--- a/core/tests/Drupal/Tests/Core/Extension/InfoParserUnitTest.php
+++ b/core/tests/Drupal/Tests/Core/Extension/InfoParserUnitTest.php
@@ -156,4 +156,90 @@ public function testInfoParserCommonInfo() {
     $this->assertEquals($info_values['double_colon'], 'dummyClassName::method', 'Value containing double-colon was parsed correctly.');
   }
 
+  /**
+   * Tests composer json file.
+   *
+   * @covers ::parse
+   */
+  public function testInfoParserComposerJson() {
+    $common = <<<COMMONTEST
+{
+  "name": "drupal/common_test",
+  "description": "testing info file parsing",
+  "type": "drupal-module",
+  "license": "GPL-2.0-or-later",
+  "require": {
+    "drupal/field": "~8.0",
+    "smurfcore/log": "~1.0"
+  },
+  "extra": {
+    "drupal": {
+      "configure": "common_test.admin",
+      "package": "Core",
+      "name": "Common test",
+      "simple_string": "A simple string"
+    }
+  },
+  "version": "VERSION"
+}
+COMMONTEST;
+
+    vfsStream::setup('modules');
+    vfsStream::create([
+      'fixtures' => [
+        'composer.json' => $common,
+      ],
+    ]);
+    $info_values = $this->infoParser->parse(vfsStream::url('modules/fixtures/composer.json'));
+    $this->assertEquals('A simple string', $info_values['simple_string']);
+    $this->assertEquals(\Drupal::VERSION, $info_values['version']);
+    $this->assertEquals('Common test', $info_values['name']);
+    $this->assertEquals('common_test.admin', $info_values['configure']);
+    $this->assertEquals('Core', $info_values['package']);
+    $this->assertEquals(['field (~8.0)'], $info_values['dependencies']);
+    $this->assertEquals(['field' => '~8.0'], $info_values['require']);
+    $this->assertEquals('module', $info_values['type']);
+  }
+
+  /**
+   * Tests composer json file.
+   *
+   * @covers ::parse
+   */
+  public function testInfoParserComposerJsonNoDependencies() {
+    $common = <<<COMMONTEST
+{
+  "name": "drupal/common_test",
+  "description": "testing info file parsing",
+  "type": "drupal-module",
+  "license": "GPL-2.0-or-later",
+  "extra": {
+    "drupal": {
+      "configure": "common_test.admin",
+      "package": "Core",
+      "name": "Common test",
+      "simple_string": "A simple string"
+    }
+  },
+  "version": "VERSION"
+}
+COMMONTEST;
+
+    vfsStream::setup('modules');
+    vfsStream::create([
+      'fixtures2' => [
+        'composer.json' => $common,
+      ],
+    ]);
+    $info_values = $this->infoParser->parse(vfsStream::url('modules/fixtures2/composer.json'));
+    $this->assertEquals('A simple string', $info_values['simple_string']);
+    $this->assertEquals(\Drupal::VERSION, $info_values['version']);
+    $this->assertEquals('Common test', $info_values['name']);
+    $this->assertEquals('common_test.admin', $info_values['configure']);
+    $this->assertEquals('Core', $info_values['package']);
+    $this->assertEquals([], $info_values['dependencies']);
+    $this->assertEquals([], $info_values['require']);
+    $this->assertEquals('module', $info_values['type']);
+  }
+
 }
diff --git a/core/tests/bootstrap.php b/core/tests/bootstrap.php
index 82a1830547..aa9d4899ab 100644
--- a/core/tests/bootstrap.php
+++ b/core/tests/bootstrap.php
@@ -31,6 +31,22 @@ function drupal_phpunit_find_extension_directories($scan_directory) {
       $extensions[substr($dir->getFilename(), 0, -9)] = $dir->getPathInfo()
         ->getRealPath();
     }
+    if ($dir->getBasename('.json') === 'composer') {
+      $file = $dir->openFile('r');
+      if (!$file->getSize()) {
+        continue;
+      }
+      $details = json_decode($file->fread($file->getSize()), TRUE);
+      if (!isset($details['name'])) {
+        continue;
+      }
+      list($namespace, $name) = explode('/', $details['name']);
+      if ($namespace !== 'drupal') {
+        continue;
+      }
+      $extensions[$name] = $dir->getPathInfo()
+        ->getRealPath();
+    }
   }
   return $extensions;
 }
