diff --git a/bs_lib.services.yml b/bs_lib.services.yml
index 7ce0085eb34e06f56a934d5b37aac13625905ab7..4fcfcabeb93f4cb8c51895b7f14cdfd69b0a2cf6 100644
--- a/bs_lib.services.yml
+++ b/bs_lib.services.yml
@@ -1,3 +1,5 @@
 services:
   bs_lib.svg_tools:
     class: Drupal\bs_lib\SvgTools
+  bs_lib.theme_tools:
+    class: Drupal\bs_lib\ThemeTools
diff --git a/src/Commands/BsLibCommands.php b/src/Commands/BsLibCommands.php
index 5ca4040db79c6d622b489503b4afeb0eb3d381b2..03f02343859b7217d2f4c946cdf0851882a838ff 100644
--- a/src/Commands/BsLibCommands.php
+++ b/src/Commands/BsLibCommands.php
@@ -2,9 +2,9 @@
 
 namespace Drupal\bs_lib\Commands;
 
+use Drupal\bs_lib\ThemeTools;
 use Drupal\Component\Serialization\Yaml;
-use Drupal\Core\Extension\ExtensionDiscovery;
-use Drupal\Core\Extension\ThemeHandlerInterface;
+use Symfony\Component\Routing\Generator\UrlGenerator;
 use Drush\Commands\DrushCommands;
 use Drush\Drush;
 use Psr\Log\LogLevel;
@@ -16,15 +16,6 @@ use stdClass;
  */
 class BsLibCommands extends DrushCommands {
 
-  /**
-   * BsLibCommands constructor.
-   */
-  public function __construct(ThemeHandlerInterface $theme_handler) {
-    // For easier maintenance we just wrap the methods from bs_base here.
-    $bs_base = $theme_handler->getTheme('bs_base');
-    include_once "{$bs_base->getPath()}/bs_base.drush.inc";
-  }
-
   /**
    * Create a new bs_base compatible child theme.
    *
@@ -41,7 +32,7 @@ class BsLibCommands extends DrushCommands {
    * @usage drush bs-tc bs_bootstrap custom_theme 'Custom theme' 'Custom theme description'
    *   Create a new bs_base compatible child theme.
    *
-   * @throws \Exception
+   * @throws Exception
    */
   public function themeCreate($parent_machine_name, $child_machine_name, $child_name, $child_description) {
     // Verify that the child machine name contains no disallowed characters.
@@ -51,14 +42,17 @@ class BsLibCommands extends DrushCommands {
 
     $this->output()->writeln("Starting $child_machine_name theme creation");
 
+    /** @var ThemeTools $theme_tools */
+    $theme_tools = \Drupal::service('bs_lib.theme_tools');
+
     // Parent theme should exist.
-    $parent_path = $this->drupalGetThemePath($parent_machine_name);
+    $parent_path = $theme_tools->drupalGetThemePath($parent_machine_name);
     if (empty($parent_path)) {
       throw new \Exception('Parent theme does not exist.');
     }
 
     // Child theme should not exist.
-    if (!empty($child_path = $this->drupalGetThemePath($child_machine_name))) {
+    if (!empty($child_path = $theme_tools->drupalGetThemePath($child_machine_name))) {
       throw new \Exception("Child theme already exist on $child_path file system.");
     }
 
@@ -74,7 +68,7 @@ class BsLibCommands extends DrushCommands {
       // For multi sites we will use multisite folder.
       $child_path = $site_path . '/themes/custom/' . $child_machine_name;
     }
-    drush_log("Creating {$child_path} folder.", LogLevel::INFO);
+    Drush::logger()->log(LogLevel::INFO, "Creating {$child_path} folder.");
     if (!mkdir($child_path, 0755, TRUE)) {
       throw new \Exception("Failed to create child theme directory on $child_path path.");
     }
@@ -115,7 +109,7 @@ class BsLibCommands extends DrushCommands {
     $this->generateFile('README.md', $options);
 
     // Rebuild themes static cache because new theme is created.
-    $this->drupalThemeListInfo(TRUE);
+    $theme_tools->drupalThemeListInfo(TRUE);
 
     // Make sure we are on latest parent theme versions.
     $update_functions = $this->GetUpdateHooks($child_machine_name);
@@ -128,7 +122,7 @@ class BsLibCommands extends DrushCommands {
         $bs_versions['bs_versions.' . $theme_name] = (int) $last_function;
       }
 
-      $all_themes = $this->drupalThemeListInfo();
+      $all_themes = $theme_tools->drupalThemeListInfo();
       $this->setYmlValue($all_themes[$child_machine_name]->pathname, $bs_versions, TRUE);
     }
 
@@ -150,12 +144,16 @@ class BsLibCommands extends DrushCommands {
    *
    * @usage drush bs-tu custom_theme
    *   Create a new bs_base compatible child theme.
-   * @throws \Exception
+   *
+   * @throws Exception
    */
   public function themeUpdate($target_machine_name) {
     $this->output()->writeln("Updating a $target_machine_name theme");
 
-    $target_path = $this->drupalGetThemePath($target_machine_name);
+    /** @var ThemeTools $theme_tools */
+    $theme_tools = \Drupal::service('bs_lib.theme_tools');
+
+    $target_path = $theme_tools->drupalGetThemePath($target_machine_name);
     if (empty($target_path)) {
       throw new \Exception('Target theme does not exist.');
     }
@@ -168,8 +166,8 @@ class BsLibCommands extends DrushCommands {
     // Run update hooks.
     $this->themeRunUpdateHooks($target_machine_name);
 
-    $all_themes = $this->drupalThemeListInfo();
-    $first_parent_machine_name = $this->drupalGetParentThemeName($target_machine_name);
+    $all_themes = $theme_tools->drupalThemeListInfo();
+    $first_parent_machine_name = $theme_tools->drupalGetParentThemeName($target_machine_name);
 
     $this->updateSassFiles($target_machine_name);
 
@@ -214,11 +212,16 @@ class BsLibCommands extends DrushCommands {
    *
    * @usage drush bs-tb custom_theme
    *   Download custom_theme build dependencies and build all assets.
+   *
+   * @throws Exception
    */
   public function themeBuild($theme_machine_name) {
     $this->output()->writeln("Building asset for a $theme_machine_name theme");
 
-    $target_path = $this->drupalGetThemePath($theme_machine_name);
+    /** @var ThemeTools $theme_tools */
+    $theme_tools = \Drupal::service('bs_lib.theme_tools');
+
+    $target_path = $theme_tools->drupalGetThemePath($theme_machine_name);
     if (empty($target_path)) {
       throw new \Exception("Target theme {$theme_machine_name} does not exist.");
     }
@@ -250,8 +253,50 @@ class BsLibCommands extends DrushCommands {
    * @return array
    *   Array of unique lines.
    */
-  protected function arrayUniqueLines(array $lines) {
-    return _bs_base_array_unique_lines($lines);
+  protected function arrayUniqueLines(array $lines): array {
+    $unique_lines = array();
+    $matches = [];
+
+    foreach ($lines as $line) {
+      // Empty line we just add and continue.
+      if ($line === "\n") {
+        $unique_lines[] = $line;
+        continue;
+      }
+
+      // Get import path parts.
+      $res = FALSE;
+      if (preg_match("#(//|/\*)?\s*@import\s*['\"]?(.*)/(.*)(\.scss)?['\"]#", $line, $matches)) {
+        // If import path is not in unique lines then lets add it. We check partial
+        // name with `_` and without `_` character on the start.
+        $pattern = "#@import\s*['\"]{$matches[2]}/_?{$matches[3]}(\.scss)?['\"]#";
+        $res = preg_grep($pattern, $unique_lines);
+      }
+
+      if (empty($res)) {
+        // Compare line comment variation.
+        // @todo - this comparator is weak and will only work for simple comment
+        // case. If more is needed we will need to use some kind of regular
+        // expression.
+        if (!str_starts_with($line, '//')) {
+          $comment_line = '//' . $line;
+          if (in_array($comment_line, $lines)) {
+            // If it exists we will save commented version.
+            $unique_lines[] = $comment_line;
+            continue;
+          }
+        }
+        $unique_lines[] = $line;
+      }
+      else {
+        // When we have a match we will override previous line because match can
+        // happen with comment and without comment, and new line is always the
+        // strongest.
+        $unique_lines[key($res)] = $line;
+      }
+    }
+
+    return $unique_lines;
   }
 
   /**
@@ -265,19 +310,51 @@ class BsLibCommands extends DrushCommands {
    *   - child_name
    *   - child_description.
    */
-  protected function copyThemeFiles(array $options) {
-    _bs_base_copy_theme_files($options);
+  protected function copyThemeFiles(array $options): void {
+    // Single files and folders which we will copy if they exist in parent theme.
+    $files = [
+      'config',
+      'config/install',
+      'config/install/' . $options['parent_machine_name'] . '.settings.yml',
+      'config/schema',
+      'fonts',
+      'templates',
+      'templates/README.md',
+      '.browserslistrc',
+      '.fantasticonrc.js',
+      '.gitignore',
+      '.npmrc',
+      '.nvmrc',
+      'favicon.ico',
+      'gulp-tasks.js',
+      'gulpfile.js',
+      'logo.svg',
+      'package.json',
+      'screenshot.png',
+    ];
+    foreach ($files as $file) {
+      $this->copyFile($file, $options);
+    }
+
+    // Recursive copy of folders from parent to child theme.
+    $recursive_copy = [
+      'fantasticon',
+      'images/font-icons',
+    ];
+    foreach ($recursive_copy as $folder) {
+      $this->recursiveCopy($folder, $options);
+    }
   }
 
   /**
-   * Copy file from parent theme to child theme.
+   * Copy folder and it content recursively from parent theme to child theme.
    *
    * If parent file is a directory then it will be created.
    *
-   * Target subdirectories of a file will be created automatically if they do
+   * Target subdirectories of a folder will be created automatically if they do
    * not exist.
    *
-   * @param string $file
+   * @param string $folder
    *   File name.
    * @param array $options
    *   Array of options having next keys:
@@ -287,100 +364,140 @@ class BsLibCommands extends DrushCommands {
    *   - child_path.
    *
    * @return bool
-   *   TRUE if the file was copied, FALSE otherwise.
+   *   TRUE if the folder was copied, FALSE otherwise.
    */
-  protected function copyFile($file, array $options) {
-    return _bs_base_copy_file($file, $options);
-  }
+  protected function recursiveCopy(string $folder, array $options): bool {
+    // If source file does not exist, log a warning and return.
+    if (!file_exists($options['parent_path'] . '/' . $folder)) {
+      Drush::logger()->log(LogLevel::WARNING, "Source folder $folder in {$options['parent_path']} does not exist, can not copy.");
+      return FALSE;
+    }
 
-  /**
-   * Finds all the base themes for the specified theme.
-   *
-   * @param array $themes
-   *   An array of available themes.
-   * @param string $theme
-   *   The name of the theme whose base we are looking for.
-   *
-   * @return array
-   *   Returns an array of all the theme's ancestors including specified theme.
-   */
-  protected function drupalGetBaseThemes(array $themes, $theme) {
-    return _bs_base_drupal_get_base_themes($themes, $theme);
-  }
+    // In the case when folder has directories in it make sure that all
+    // subdirectories exists in target before doing actual file copy.
+    if (!$this->ensureDirectory($folder, $options['child_path'])) {
+      return FALSE;
+    }
 
-  /**
-   * Returns the first parent theme of passed child theme.
-   *
-   * @param string $theme_name
-   *   The name of the child theme whose first parent theme we are looking for.
-   *
-   * @return string|NULL
-   *   Returns a theme machine name of first parent theme or NULL if parent does
-   *   not exist.
-   */
-  protected function drupalGetParentThemeName($theme_name) {
-    return _bs_base_drupal_get_parent_theme_name($theme_name);
-  }
+    if (!$this->copyFolder($options['parent_path'] . '/' . $folder, $options['child_path'] . '/' . $folder)) {
+      Drush::logger()->log(LogLevel::ERROR, "Failed to copy $folder folder from {$options['parent_path']} to {$options['child_path']}.");
+      return FALSE;
+    }
 
-  /**
-   * Returns the path to a Drupal theme.
-   *
-   * @param string $name
-   *   Theme machine name.
-   *
-   * @return string
-   *   The path to the requested theme or an empty string if the item is not
-   *   found.
-   */
-  protected function drupalGetThemePath($name) {
-    return _bs_base_drupal_get_theme_path($name);
+    return TRUE;
   }
 
   /**
-   * Discovers available extensions of a given type.
+   * Copy file from parent theme to child theme.
    *
-   * For an explanation of how this work see ExtensionDiscovery::scan().
+   * If parent file is a directory then it will be created.
+   *
+   * Target subdirectories of a file will be created automatically if they do not
+   * exist.
    *
-   * @param string $type
-   *   The extension type to search for. One of 'profile', 'module', 'theme', or
-   *   'theme_engine'.
-   * @param bool $reset
-   *   Reset internal cache.
+   * @param string $file
+   *   File name.
+   * @param array $options
+   *   Array of options having next keys:
+   *   - parent_machine_name
+   *   - parent_path
+   *   - child_machine_name
+   *   - child_path.
    *
-   * @return stdClass[]
-   *   An associative array of stdClass objects, keyed by extension name.
+   * @return bool
+   *   TRUE if the file was copied, FALSE otherwise.
    */
-  protected function drupalScan($type, $reset = FALSE) {
-    return _bs_base_drupal_scan($type, $reset);
+  protected function copyFile(string $file, array $options): bool {
+    // Do file rename if needed.
+    $new_file_name = str_replace($options['parent_machine_name'], $options['child_machine_name'], $file);
+
+    // If source file does not exist, log a warning and return.
+    if (!file_exists($options['parent_path'] . '/' . $file)) {
+      Drush::logger()->log(LogLevel::WARNING, "Source file $file in {$options['parent_path']} does not exist, can not copy.");
+      return FALSE;
+    }
+
+    // If target file exist just return.
+    if (file_exists($options['child_path'] . '/' . $new_file_name)) {
+      return FALSE;
+    }
+
+    // If parent file is directory then create directory.
+    if (is_dir($options['parent_path'] . '/' . $file)) {
+      return mkdir($options['child_path'] . '/' . $new_file_name, 0755);
+    }
+
+    // In the case when file name has directory in it make sure that all
+    // subdirectories exists in target before doing actual file copy.
+    if (!$this->ensureDirectory($new_file_name, $options['child_path'])) {
+      return FALSE;
+    }
+
+    // Copy file from parent theme folder to child theme folder.
+    if (!copy($options['parent_path'] . '/' . $file, $options['child_path'] . '/' . $new_file_name)) {
+      Drush::logger()->log(LogLevel::ERROR, "Failed to copy $file file from {$options['parent_path']} to {$options['child_path']}.");
+      return FALSE;
+    }
+
+    return TRUE;
   }
 
   /**
-   * Recursively scans a base directory for the extensions it contains.
+   * Recursive copy of source folder to the destination.
    *
-   * @see ExtensionDiscovery::scanDirectory()
-   *   For an explanation of how this works.
+   * The function checks if the source and destination directories exist, and
+   * creates the destination directory if it doesn't. Then it opens the source
+   * directory, iterates through its contents, and for each file it finds,
+   * it recursively calls itself if the file is a directory, or copies the file to
+   * the destination if it's a regular file.
    *
-   * @param string $dir
-   *   A relative base directory path to scan, without trailing slash.
+   * @param string $source
+   *  Path to the folder to be copied.
+   * @param string $destination
+   *  Path to the destination folder where the source folder should be copied to.
    *
-   * @return stdClass[]
-   *   An associative array of stdClass objects, keyed by extension name.
-   */
-  protected function drupalScanDirectory($dir) {
-    return _bs_base_drupal_scan_directory($dir);
-  }
+   * @return bool
+   *   TRUE if the folder was copied, FALSE otherwise.
+ */
+  protected function copyFolder(string $source, string $destination): bool {
+    // Check if source exists.
+    if (!is_dir($source)) {
+      return FALSE;
+    }
 
-  /**
-   * Get information's for all themes.
-   *
-   * @param bool $reset
-   *   Reset internal cache.
-   *
-   * @return array
-   *   Array holding themes information's.
-   */
-  protected function drupalThemeListInfo($reset = FALSE) {
-    return _bs_base_drupal_theme_list_info($reset);
+    // Check if destination exists. If not, create it.
+    if (!is_dir($destination)) {
+      mkdir($destination);
+    }
+
+    // Open the source directory and iterate through its contents.
+    $dir = opendir($source);
+    while (($file = readdir($dir)) !== FALSE) {
+      // Skip special files "." and ".."
+      if ($file == '.' || $file == '..') {
+        continue;
+      }
+
+      $sourcePath = $source . '/' . $file;
+      $destinationPath = $destination . '/' . $file;
+      // If the file is a directory, do a recursive call.
+      if (is_dir($sourcePath)) {
+        $this->copyFolder($sourcePath, $destinationPath);
+      }
+      // Skip existing files.
+      elseif (file_exists($destinationPath)) {
+        continue;
+      }
+      // If not exist copy the file.
+      else {
+        copy($sourcePath, $destinationPath);
+      }
+    }
+
+    // Close the source directory
+    closedir($dir);
+
+    return TRUE;
   }
 
   /**
@@ -392,8 +509,13 @@ class BsLibCommands extends DrushCommands {
    * @return array
    *   Returns an array of all the parent themes.
    */
-  protected function getParentThemes($theme_machine_name) {
-    return _bs_base_get_parent_themes($theme_machine_name);
+  protected function getParentThemes(string $theme_machine_name): array {
+    /** @var ThemeTools $bs_lib_theme */
+    $theme_tools = \Drupal::service('bs_lib.theme_tools');
+    $all_themes = $theme_tools->drupalThemeListInfo();
+    $parent_themes = $theme_tools->drupalGetBaseThemes($all_themes, $theme_machine_name);
+    array_pop($parent_themes);
+    return $parent_themes;
   }
 
   /**
@@ -405,8 +527,9 @@ class BsLibCommands extends DrushCommands {
    * @return mixed|null
    *   Theme info object or NULL if theme does not exist.
    */
-  protected function getThemeInfo($theme_machine_name) {
-    return _bs_base_get_theme_info($theme_machine_name);
+  protected function getThemeInfo(string $theme_machine_name): mixed {
+    $all_themes = \Drupal::service('bs_lib.theme_tools')->drupalThemeListInfo();
+    return $all_themes[$theme_machine_name] ?? NULL;
   }
 
   /**
@@ -421,8 +544,24 @@ class BsLibCommands extends DrushCommands {
    *   Array of all child themes machine names. Empty array if child themes does
    *   not exist.
    */
-  protected function findChildThemes($parent_theme) {
-    return _bs_base_find_child_themes($parent_theme);
+  protected function findChildThemes(string $parent_theme): array {
+    $child_themes = [];
+
+    /** @var ThemeTools $bs_lib_theme */
+    $bs_lib_theme = \Drupal::service('bs_lib.theme_tools');
+
+    $themes = $bs_lib_theme->drupalThemeListInfo();
+    foreach ($themes as $theme => $theme_info) {
+      if ($theme === $parent_theme) {
+        continue;
+      }
+      $parent_themes = $bs_lib_theme->drupalGetBaseThemes($themes, $theme);
+      if (isset($parent_themes[$parent_theme])) {
+        $child_themes[$theme] = $theme;
+      }
+    }
+
+    return $child_themes;
   }
 
   /**
@@ -436,8 +575,16 @@ class BsLibCommands extends DrushCommands {
    * @return bool
    *   TRUE if library is coming from parents, FALSE other way.
    */
-  protected function libraryKeyComingFromParents($library_key, $theme_machine_name) {
-    return _bs_base_library_key_coming_from_parents($library_key, $theme_machine_name);
+  protected function libraryKeyComingFromParents(string $library_key, string $theme_machine_name): bool {
+    // Add bs_lib module to the parents array.
+    $parent_themes = ['bs_lib' => 'bs_lib'] + $this->getParentThemes($theme_machine_name);
+    foreach ($parent_themes as $parent_theme_machine_name => $parent_theme_label) {
+      if (str_starts_with($library_key, $parent_theme_machine_name)) {
+        return TRUE;
+      }
+    }
+
+    return FALSE;
   }
 
   /**
@@ -451,10 +598,36 @@ class BsLibCommands extends DrushCommands {
    *   - child_name
    *   - child_description.
    *
-   * @throws \Exception
+   * @throws Exception
    */
-  protected function reconfigureThemeFiles(array $options) {
-    _bs_base_reconfigure_theme_files($options);
+  protected function reconfigureThemeFiles(array $options): void {
+    // Yaml value replaces.
+    $yaml_files = [
+      'config/install/' . $options['child_machine_name'] . '.settings.yml' => [
+        'logo.path' => $options['child_path'] . '/logo.svg',
+        'favicon.path' => $options['child_path'] . '/favicon.ico',
+      ],
+    ];
+    foreach ($yaml_files as $file => $value) {
+      $this->setYmlValue($options['child_path'] . '/' . $file, $value);
+    }
+
+    // Regexp string replacements.
+    $regexp_files = [
+      'gulpfile.js' => [
+        $options['parent_machine_name'] => $options['child_machine_name'],
+      ],
+      'package.json' => [
+        "\"name\":\s*\"{$options['parent_machine_name']}\"" => "\"name\": \"{$options['child_machine_name']}\"",
+        "\"description\":\s*\".*\"" => "\"description\": \"{$options['child_description']}\"",
+      ],
+    ];
+    foreach ($regexp_files as $file => $regexps) {
+      $file_name = $options['child_path'] . '/' . $file;
+      if (!\Drupal::service('bs_lib.theme_tools')->regexpFile($file_name, $regexps)) {
+        Drush::logger()->log(LogLevel::WARNING, "Can not process file $file_name for regexp search&replace.");
+      }
+    }
   }
 
   /**
@@ -466,8 +639,48 @@ class BsLibCommands extends DrushCommands {
    * @return array
    *   Array of update functions.
    */
-  protected function getUpdateHooks($target_machine_name) {
-    return _bs_base_get_update_hooks($target_machine_name);
+  protected function getUpdateHooks(string $target_machine_name): array {
+    $update_functions = [];
+    $all_themes = \Drupal::service('bs_lib.theme_tools')->drupalThemeListInfo();
+
+    // Get current parents version information for this theme. If not defined this
+    // means that we will run all existing update functions.
+    $bs_versions = $all_themes[$target_machine_name]->info['bs_versions'] ?? [];
+
+    // Get parent themes.
+    $parent_themes = $this->getParentThemes($target_machine_name);
+
+    // Cycle through all parent themes and find update functions.
+    foreach (array_keys($parent_themes) as $parent_theme) {
+      // If install file exist get all update functions from it.
+      $install_filepath = $all_themes[$parent_theme]->subpath . '/' . $parent_theme . '.bs_base.install';
+
+      if (file_exists($install_filepath)) {
+        $content = file_get_contents($install_filepath);
+
+        // Find update functions and first comment line.
+        if (preg_match_all('/\s\*\s(.*?)\n\s\*\/\nfunction\s' . $parent_theme . '_bs_update_(\d{4})/', $content, $matches)) {
+          $functions = array_combine($matches[2], $matches[1]);
+
+          // Filter update functions that were run in the past.
+          if (isset($bs_versions[$parent_theme])) {
+            $parent_theme_version = (int) $bs_versions[$parent_theme];
+            $functions = array_filter($functions, function ($version) use ($parent_theme_version) {
+              return (int) $version > $parent_theme_version;
+            }, ARRAY_FILTER_USE_KEY);
+          }
+
+          if (!empty($functions)) {
+            $update_functions[$parent_theme] = [
+              'file' => $install_filepath,
+              'functions' => $functions,
+            ];
+          }
+        }
+      }
+    }
+
+    return $update_functions;
   }
 
   /**
@@ -476,12 +689,12 @@ class BsLibCommands extends DrushCommands {
    * @param string $target_machine_name
    *   Target theme machine name.
    *
-   * @throws \Exception
+   * @throws Exception
    */
-  protected function themeRunUpdateHooks($target_machine_name) {
+  protected function themeRunUpdateHooks(string $target_machine_name): void {
     $update_functions = $this->getUpdateHooks($target_machine_name);
     if (empty($update_functions)) {
-      $this->output()->writeln("No theme updates required.");
+      $this->output()->writeln('No theme updates required.');
       return;
     }
 
@@ -519,7 +732,7 @@ class BsLibCommands extends DrushCommands {
     }
 
     // Update info file with the latest versions.
-    $all_themes = $this->drupalThemeListInfo();
+    $all_themes = \Drupal::service('bs_lib.theme_tools')->drupalThemeListInfo();
     $this->setYmlValue($all_themes[$target_machine_name]->pathname, $bs_versions, TRUE);
   }
 
@@ -529,8 +742,12 @@ class BsLibCommands extends DrushCommands {
    * @param string $path
    *   Path to theme folder.
    */
-  protected function drushBuild($path) {
-    _bs_base_drush_build($path);
+  protected function drushBuild(string $path): void {
+    // Install npm packages and execute gulp sass compilation.
+    Drush::logger()->log(LogLevel::INFO, "Installing any missing package and rebuilding assets");
+    // Run build-install first time, so we are sure that npm-run-all is installed
+    // which is needed for `npm-run-all`.
+    exec('cd ' . $path . ' && npm run build-install && npm run build');
   }
 
   /**
@@ -544,8 +761,17 @@ class BsLibCommands extends DrushCommands {
    * @return bool
    *   TRUE on success, FALSE on error.
    */
-  protected function ensureDirectory($filename, $path) {
-    return _bs_base_ensure_directory($filename, $path);
+  protected function ensureDirectory(string $filename, string $path): bool {
+    // In the case when file name has directory in it make sure that all
+    // subdirectories exists in target before doing actual file copy.
+    $dirname = dirname($filename);
+    if ($dirname && !is_dir($path . '/' . $dirname)) {
+      if (!mkdir($path . '/' . $dirname, 0755, TRUE)) {
+        Drush::logger()->log(LogLevel::ERROR, "Failed to create copy target directory $dirname.");
+        return FALSE;
+      }
+    }
+    return TRUE;
   }
 
   /**
@@ -571,16 +797,67 @@ class BsLibCommands extends DrushCommands {
    *     extension. Defaults to 'uri'.
    *   - 'min_depth': Minimum depth of directories to return files from. Defaults
    *     to 0.
-   * @param int $depth
-   *   The current depth of recursion. This parameter is only used internally and
-   *   should not be passed in.
    *
-   * @return
+   * @return array
    *   An associative array (keyed on the chosen key) of objects with 'uri',
    *   'filename', and 'name' properties corresponding to the matched files.
    */
-  protected function fileScanDirectory($dir, $mask, $options = [], $depth = 0) {
-    return _bs_base_file_scan_directory($dir, $mask, $options, $depth);
+  protected function fileScanDirectory(string $dir, string $mask, array $options = [], int $depth = 0): array {
+    // Merge in defaults.
+    $options += [
+      'callback' => 0,
+      'recurse' => TRUE,
+      'key' => 'uri',
+      'min_depth' => 0,
+    ];
+    // Normalize $dir only once.
+    if ($depth == 0) {
+      $dir_has_slash = (str_ends_with($dir, '/'));
+    }
+
+    $options['key'] = in_array($options['key'], ['uri', 'filename', 'name']) ? $options['key'] : 'uri';
+    $files = [];
+    // Avoid warnings when opendir does not have the permissions to open a
+    // directory.
+    if (is_dir($dir)) {
+      if ($handle = @opendir($dir)) {
+        while (FALSE !== ($filename = readdir($handle))) {
+          // Skip this file if it matches the nomask or starts with a dot.
+          if ($filename[0] != '.'
+            && !(isset($options['nomask']) && preg_match($options['nomask'], $filename))
+          ) {
+            if ($depth == 0 && $dir_has_slash) {
+              $uri = "$dir$filename";
+            }
+            else {
+              $uri = "$dir/$filename";
+            }
+            if ($options['recurse'] && is_dir($uri)) {
+              // Give priority to files in this folder by merging them in after
+              // any subdirectory files.
+              $files = array_merge($this->fileScanDirectory($uri, $mask, $options, $depth + 1), $files);
+            }
+            elseif ($depth >= $options['min_depth'] && preg_match($mask, $filename)) {
+              // Always use this match over anything already set in $files with
+              // the same $options['key'].
+              $file = new stdClass();
+              $file->uri = $uri;
+              $file->filename = $filename;
+              $file->name = pathinfo($filename, PATHINFO_FILENAME);
+              $key = $options['key'];
+              $files[$file->$key] = $file;
+              if ($options['callback']) {
+                $options['callback']($uri);
+              }
+            }
+          }
+        }
+
+        closedir($handle);
+      }
+    }
+
+    return $files;
   }
 
   /**
@@ -600,8 +877,175 @@ class BsLibCommands extends DrushCommands {
    * @return array
    *   Returns array of flattened SASS @import directives.
    */
-  protected function flattenSassFileImports($target_sass_file, $target_machine_name, array $current_themes, array $parent_themes_sass_files, $depth = 0) {
-    return _bs_base_flatten_sass_file_imports($target_sass_file, $target_machine_name, $current_themes, $parent_themes_sass_files, $depth);
+  protected function flattenSassFileImports(object $target_sass_file, string $target_machine_name, array $current_themes, array $parent_themes_sass_files, int $depth = 0): array {
+    $flatten_lines = [];
+
+    $lines = file($target_sass_file->uri);
+    if ($lines === FALSE) {
+      Drush::logger()->log(LogLevel::ERROR, "Failed to open {$target_sass_file->uri} file.");
+      return [];
+    }
+
+    // For the depth 0 even when we are already flattened (no direct imports to
+    // parent main SASS file) we still need to check parent theme SASS file in
+    // case that parent files have additional changes that are not in target
+    // files.
+    $check_parent = FALSE;
+    if ($depth === 0) {
+      $check_parent = TRUE;
+    }
+    else {
+      // For the depth greater than 0 we will check parent theme only if it has
+      // direct import.
+      $parent_imports = [];
+      foreach (array_keys($parent_themes_sass_files) as $parent_theme) {
+        $parent_imports = array_merge($parent_imports, preg_grep("#^@import\s*['\"]{$parent_theme}((?!/partials/).)*['\"]#", $lines));
+      }
+      if (!empty($parent_imports)) {
+        $check_parent = TRUE;
+      }
+    }
+    if ($check_parent) {
+      $additional_lines = [];
+      end($parent_themes_sass_files);
+      $first_parent_theme = key($parent_themes_sass_files);
+      foreach ($parent_themes_sass_files[$first_parent_theme] as $file_path => $file) {
+        if ($target_sass_file->filepath === $file->filepath) {
+          // Remove 'file-name.scss' part from a file path.
+          $sass_import_path = substr($file->filepath, 0, -strlen($file->filename));
+          // This patter will pick all possible import variation we need, with
+          // partial sign and without, with .scss extension and without, commented
+          // or not commented:
+          // For example all next cases are considered positive for us:
+          //   @import "parent_theme/sass/theme/print";
+          //   @import "parent_theme/sass/theme/print.scss";
+          //   @import "parent_theme/sass/theme/_print";
+          //   @import "parent_theme/sass/theme/_print.scss";
+          //   //@import "parent_theme/sass/theme/print";
+          //   // and all other // or /* combinations.
+          $pattern = "#(?://|/\*)?\s*@import\s*['\"]{$first_parent_theme}/{$sass_import_path}_?{$file->name}(\.scss)?['\"]#";
+
+          // Add additional parent imports only if they already don't exist in
+          // current lines.
+          if (empty(preg_grep($pattern, $lines))) {
+            $rest_of_parent_themes = $parent_themes_sass_files;
+            array_pop($rest_of_parent_themes);
+            $additional_flattened_files = $this->flattenSassFileImports($file, $first_parent_theme, $current_themes, $rest_of_parent_themes, $depth + 1);
+            $additional_lines = $this->arrayUniqueLines(array_merge($additional_lines, $additional_flattened_files));
+          }
+          break;
+        }
+      }
+      // If there are new additional lines from parent themes we need to check
+      // them also. This will produce duplicate lines, so we need to take that
+      // into consideration.
+      if (!empty($additional_lines)) {
+        // If first line is init the lets add parent imports after it.
+        if (str_starts_with($lines[0], '@import "init"')) {
+          // Eliminate duplicate lines and comment duplicate lines.
+          $lines = $this->arrayUniqueLines(array_merge([array_shift($lines)], $additional_lines, $lines));
+        }
+        else {
+          $lines = $this->arrayUniqueLines(array_merge($additional_lines, $lines));
+        }
+      }
+    }
+
+    foreach ($lines as $line) {
+      if ($depth === 0) {
+        // If it does not start with @import just add it and move on.
+        if (!str_starts_with($line, '@import')) {
+          $flatten_lines[] = $line;
+          continue;
+        }
+      }
+      // Skip empty lines, and commented lines from parent theme SASS files that
+      // are NOT @import directives.
+      elseif ($depth > 0 && (empty($line) || (str_starts_with($line, '//') && !str_contains($line, '@import')))) {
+        continue;
+      }
+
+      // Find and flatten @import directives.
+      if (preg_match("#^(//|/\*)?\s*(@import\s+['\"])([a-zA-Z0-9_@]+)(.*?)['\"]#", $line, $matches)) {
+        $sass_file_commented = !empty($matches[1]);
+        $sass_file_context = $matches[3];
+        $sass_file_path_part = $matches[4];
+
+        if (isset($parent_themes_sass_files[$sass_file_context]) && !empty($sass_file_path_part)) {
+          // Append scss extension.
+          if (!str_contains($sass_file_path_part, '.scss')) {
+            $sass_file_path_part .= '.scss';
+          }
+
+          $sass_file_path = DRUPAL_ROOT . '/' . $current_themes[$sass_file_context]->subpath . $sass_file_path_part;
+
+          if (isset($parent_themes_sass_files[$sass_file_context][$sass_file_path])) {
+            // If this @import partial directive has '_' in the path file name
+            // then this is a parent partial and in this case just c&p the line.
+            $sass_file_path_info = pathinfo($sass_file_path);
+            if (str_starts_with($sass_file_path_info['basename'], '_')) {
+              $flatten_lines[] = $line;
+            }
+            // If this is not a parent partial then lets import it.
+            else {
+              $new_flattened_files = $this->flattenSassFileImports($parent_themes_sass_files[$sass_file_context][$sass_file_path], $sass_file_context, $current_themes, $parent_themes_sass_files, $depth + 1);
+              $flatten_lines = $this->arrayUniqueLines(array_merge($flatten_lines, $new_flattened_files));
+            }
+          }
+          else {
+            // If file does not exist check that it is not maybe a partial.
+            $sass_partial_file_path_part = explode('/', $sass_file_path_part);
+            $sass_partial_file_path_part[count($sass_partial_file_path_part) - 1] = '_' . $sass_partial_file_path_part[count($sass_partial_file_path_part) - 1];
+            $sass_partial_file_path_part = implode('/', $sass_partial_file_path_part);
+            $sass_file_path = DRUPAL_ROOT . '/' . $current_themes[$sass_file_context]->subpath . $sass_partial_file_path_part;
+
+            if (isset($parent_themes_sass_files[$sass_file_context][$sass_file_path])) {
+              // @todo - Check for duplicates including commented duplicate for
+              // $depth > 0?
+              $flatten_lines[] = $line;
+            }
+          }
+        }
+        else {
+          if ($depth > 0) {
+            // Skip @import "init"; lines from parent themes.
+            if ($sass_file_context === 'init' && empty($sass_file_path)) {
+              continue;
+            }
+            // Expand the import for all partials from these themes.
+            elseif ($sass_file_context === 'partials') {
+              // Get the sub-folder sass part of the target file.
+              $subfolder = NULL;
+              $parts = explode($current_themes[$target_machine_name]->subpath, $target_sass_file->uri);
+              if (!empty($parts)) {
+                $parts = explode('/', $parts[1]);
+                array_pop($parts);
+                $subfolder = implode('/', $parts);
+              }
+
+              if (empty($subfolder)) {
+                Drush::logger()->log(LogLevel::ERROR, "Sub folder is empty and it is needed for $line.");
+                continue;
+              }
+              $line = ($sass_file_commented ? '//' : '') . "@import \"{$target_machine_name}{$subfolder}/{$sass_file_context}{$sass_file_path_part}\";\n";
+            }
+          }
+
+          // @todo - Check for duplicates including commented duplicate?
+          $flatten_lines[] = $line;
+        }
+      }
+      else {
+        // If we land here, and we are target theme ($depth == 0) then something
+        // is probably wrong lets report it.
+        // If we are in some parent theme ($depth > 0) then we ignore this line.
+        if ($depth === 0) {
+          Drush::logger()->log(LogLevel::ERROR, "Parsing of SASS file $target_sass_file->uri failed for some reason for line  $line\n");
+        }
+      }
+    }
+
+    return $flatten_lines;
   }
 
   /**
@@ -612,11 +1056,21 @@ class BsLibCommands extends DrushCommands {
    * @param array $options
    *   Array of parent/child options.
    *
-   * @return bool
-   *   TRUE on success, FALSE other way.
+   * @return string|bool
+   *   String of SASS import path on success, FALSE other way.
    */
-  protected function generateSassFile($parent_sass_file, array $options) {
-    return _bs_base_generate_sass_file($parent_sass_file, $options);
+  protected function generateSassFile(object $parent_sass_file, array $options): string|bool {
+    $filepath = $options['child_path'] . '/' . $parent_sass_file->filepath;
+    $this->ensureDirectory($parent_sass_file->filepath, $options['child_path']);
+
+    $sass_import_path = $options['parent_machine_name'] . '/' . $parent_sass_file->filepath;
+    $sass_import_path = substr($sass_import_path, 0, strpos($sass_import_path, '.scss'));
+
+    if (!file_put_contents($filepath, "@import \"init\";\n@import \"{$sass_import_path}\";\n")) {
+      Drush::logger()->log(LogLevel::ERROR, "Failed to generate default SASS file in $filepath.");
+      return FALSE;
+    }
+    return $sass_import_path;
   }
 
   /**
@@ -630,8 +1084,117 @@ class BsLibCommands extends DrushCommands {
    * @return bool
    *   TRUE on success, FALSE on failure.
    */
-  protected function generateFile($file_name, array $options) {
-    return _bs_base_generate_file($file_name, $options);
+  protected function generateFile(string $file_name, array $options): bool {
+    $content = '';
+    switch ($file_name) {
+      case $options['child_machine_name'] . '.info.yml':
+        /** @var ThemeTools $theme_tools */
+        $theme_tools = \Drupal::service('bs_lib.theme_tools');
+
+        // Lets start from parent info file.
+        $all_themes = $theme_tools->drupalThemeListInfo();
+        $yaml_array = $all_themes[$options['parent_machine_name']]->info;
+
+        // Change basic stuff.
+        $yaml_array['name'] = $options['child_name'];
+        $yaml_array['description'] = $options['child_description'];
+        $yaml_array['base theme'] = $options['parent_machine_name'];
+
+        // Override libraries.
+        $yaml_array['libraries'] = [$options['child_machine_name'] . '/global-styling'];
+
+        // Remove version, project and datestamp keys that are coming from
+        // Drupal.org packager.
+        unset($yaml_array['version']);
+        unset($yaml_array['project']);
+        unset($yaml_array['datestamp']);
+
+        // Remove libraries-extend and component-libraries.
+        unset($yaml_array['libraries-extend']);
+        unset($yaml_array['component-libraries']);
+
+        $yaml_array['libraries-override'] = $this->generateLibrariesOverride($options['parent_machine_name']);
+
+        // Finally lets add empty line separators for better visual code grouping.
+        $content .= $theme_tools->regexp(Yaml::encode($yaml_array), [
+          "^regions:" => "\nregions:",
+          "^libraries:" => "\nlibraries:",
+          "^libraries-override:" => "\nlibraries-override:",
+          "^bs_versions:" => "\nbs_versions:",
+        ]);
+        break;
+
+      case $options['child_machine_name'] . '.libraries.yml':
+        // Lets start from parent libraries file.
+        $file_content = file_get_contents($options['parent_path'] . '/' . $options['parent_machine_name'] . '.libraries.yml');
+        $yaml_array = Yaml::decode($file_content);
+        $yaml_array = ['global-styling' => $yaml_array['global-styling']];
+        $content .= Yaml::encode($yaml_array);
+        break;
+
+      case 'sass/variables/_' . $options['child_machine_name'] . '.scss':
+        $content .= "// Variable overrides and custom variable definitions.\n// Note that first all variables from theme-options.yml file will be loaded and\n// then variables from this file.\n";
+        break;
+
+      case 'sass/_init.scss':
+        $content .= <<<EOD
+// Main base file which is responsible of loading all variables in correct
+// order and all mixin files we are using.
+//
+// NOTE that this order is not fixed, feel free to change it you see it fit for
+// your custom theme.
+
+// Theme custom variables and overrides.
+@import "variables/{$options['child_machine_name']}";
+@import "variables/icons";
+
+// Load variables and other init code from all parent themes.
+@import "{$options['parent_machine_name']}/sass/init";
+
+EOD;
+        break;
+
+      case 'gulp-options.yml':
+        $content .= "# This file holds various gulp configurations that we need in our Gulp process.\n#\n# Note that options from this file will be merged with Gulp options from parent\n# theme.\n\n";
+
+        // Get parent themes info.
+        $content .= "parentTheme:\n  # Order is important and needs to goes from top most parent to bottom.\n  # Default path values will work for standard positions of contrib and custom\n  # themes.\n  # Theme path to parent theme folder can be relative or absolute.";
+
+        $all_themes = \Drupal::service('bs_lib.theme_tools')->drupalThemeListInfo();
+        $parent_themes = \Drupal::service('bs_lib.theme_tools')->drupalGetBaseThemes($all_themes, $options['parent_machine_name']);
+        foreach (array_keys(array_reverse($parent_themes)) as $parent_theme) {
+          $relative_path = '../' . UrlGenerator::getRelativePath($options['child_path'], $all_themes[$parent_theme]->subpath) . '/';
+          $content .= "\n  -\n    name: '$parent_theme'\n    path: '$relative_path'";
+        }
+
+        break;
+
+      case "config/schema/{$options['child_machine_name']}.schema.yml":
+        $content .= Yaml::encode([
+          $options['child_machine_name'] . '.settings' => [
+            'type' => $options['parent_machine_name'] . '.settings',
+            'label' => $options['child_name'] . ' settings',
+          ],
+        ]);
+        break;
+
+      case 'README.md':
+        $content .= "THEME DOCUMENTATION\n-------------------\n\nPut your custom theme documentation here.";
+        break;
+
+      default:
+        Drush::logger()->log(LogLevel::ERROR, "Generation of file $file_name not supported.");
+        return FALSE;
+    }
+
+    $this->ensureDirectory($file_name, $options['child_path']);
+
+    if (!file_put_contents($options['child_path'] . '/' . $file_name, $content)) {
+      Drush::logger()->log(LogLevel::ERROR, "Failed to generate file $file_name.");
+      return FALSE;
+    }
+
+    return TRUE;
   }
 
   /**
@@ -643,8 +1206,43 @@ class BsLibCommands extends DrushCommands {
    * @return array
    *   Libraries override array.
    */
-  protected function generateLibrariesOverride($parent_machine_name) {
-    return _bs_base_generate_libraries_override($parent_machine_name);
+  protected function generateLibrariesOverride(string $parent_machine_name): array {
+    $parent_theme_info = $this->getThemeInfo($parent_machine_name);
+
+    // Remove all libraries-override elements with false - we don't need them
+    // duplicated here.
+    $libraries_override = array_filter($parent_theme_info->info['libraries-override']);
+
+    // Change override keys until core improve this in
+    // https://www.drupal.org/node/2642122.
+    $parent_root_path = '/' . $parent_theme_info->subpath . '/';
+    foreach ($libraries_override as $key => &$library) {
+      if (isset($library['css'])) {
+        foreach ($library['css'] as $library_key => $library_value) {
+          $css = reset($library_value);
+          // If this is false and the only thing in the library then we should
+          // remove it in the same way as we are removing duplicates in
+          // previous array_filter.
+          if ($css === FALSE && count($library) === 1) {
+            unset($libraries_override[$key]);
+          }
+          else {
+            $library['css'][$library_key] = [$parent_root_path . $css => $css];
+          }
+        }
+      }
+    }
+
+    // Disable loading of global styles from parent theme.
+    $libraries_override[$parent_machine_name . '/global-styling'] = FALSE;
+
+    // Override libraries defined in first parent theme.
+    $parent_library_overrides = $this->getCssLibrariesForOverride($parent_machine_name);
+    foreach ($parent_library_overrides as $key => $parent_library_override) {
+      $libraries_override[$key] = $parent_library_override;
+    }
+
+    return $libraries_override;
   }
 
   /**
@@ -656,8 +1254,35 @@ class BsLibCommands extends DrushCommands {
    * @return array
    *   Arrays of CSS libraries in libraries override format.
    */
-  protected function getCssLibrariesForOverride($theme_machine_name) {
-    return _bs_base_get_css_libraries_for_override($theme_machine_name);
+  protected function getCssLibrariesForOverride(string $theme_machine_name): array {
+    $theme_info = $this->getThemeInfo($theme_machine_name);
+    $library_path = $theme_info->subpath . '/' . $theme_machine_name . '.libraries.yml';
+
+    if (!file_exists($library_path)) {
+      return [];
+    }
+
+    // Check and override any CSS library that is defined in parent theme.
+    // @todo - we should allow library override addition only in the case when
+    // css files does belong into the flatten SASS/CSS file structure.
+    $file_content = file_get_contents($library_path);
+    $parent_libraries = Yaml::decode($file_content);
+
+    $parent_library_overrides = [];
+    foreach ($parent_libraries as $key => $parent_library) {
+      if ($key == 'global-styling') {
+        continue;
+      }
+      if (isset($parent_library['css'])) {
+        foreach ($parent_library['css'] as $css_key => $css_library) {
+          $css_file = key($css_library);
+          $parent_library['css'][$css_key] = [$css_file => $css_file];
+        }
+        $parent_library_overrides[$theme_machine_name . '/' . $key] = ['css' => $parent_library['css']];
+      }
+    }
+
+    return $parent_library_overrides;
   }
 
   /**
@@ -669,42 +1294,19 @@ class BsLibCommands extends DrushCommands {
    * @return array
    *   Array of all SASS files in the given path.
    */
-  protected function getSassFiles($path) {
-    return _bs_base_get_sass_files($path);
-  }
+  protected function getSassFiles(string $path): array {
+    $files = $this->fileScanDirectory(DRUPAL_ROOT . '/' . $path . '/sass', '/.*\.scss$/');
 
-  /**
-   * Regular expression search and replace in the text.
-   *
-   * @param string $text
-   *   Text to search and replace.
-   * @param array $regexps
-   *   Array of regexps searches with it replace values.
-   * @param string $modifiers
-   *   PHP regular expression modifiers.
-   * @param string $delimiter
-   *   PHP regular expression delimiter.
-   *
-   * @return string
-   *   Replaced text.
-   */
-  protected function regexp($text, array $regexps, $modifiers = 'm', $delimiter = '%') {
-    return _bs_base_regexp($text,$regexps, $modifiers, $delimiter);
-  }
+    // Add some more info's that we need.
+    foreach ($files as &$file) {
+      // Add SASS partial flag.
+      $file->partial = str_starts_with($file->name, '_');
 
-  /**
-   * Regular expression search and replace in the file.
-   *
-   * @param string $file_name
-   *   File path.
-   * @param array $regexps
-   *   Array of regexps searches with it replace values.
-   *
-   * @return bool
-   *   TRUE on success, FALSE other way.
-   */
-  protected function regexpFile($file_name, array $regexps) {
-    return _bs_base_regexp_file($file_name, $regexps);
+      // Add theme relative file path.
+      $file->filepath = substr($file->uri, strpos($file->uri, '/sass/') + 1);
+    }
+
+    return $files;
   }
 
   /**
@@ -733,8 +1335,86 @@ class BsLibCommands extends DrushCommands {
    * @throws Exception
    *   Throws exception in the case that we try to set non-scalar value.
    */
-  protected function setYmlValue($path, array $values, $add = FALSE) {
-    _bs_base_set_yml_value($path, $values, $add);
+  protected function setYmlValue(string $path, array $values, bool $add = FALSE): void {
+    $write = FALSE;
+    $file_contents = file_get_contents($path);
+
+    foreach ($values as $yml_key => $value) {
+      if (!is_scalar($value)) {
+        throw new Exception("Can not set non scalar value for $yml_key in $path");
+      }
+
+      // Regular expression pattern that can locate and change value based on yaml
+      // array keys.
+      $pattern = "#" . implode(":\n(.|\n)*?(", explode('.', $yml_key)) . ":\s)(.*?)\n#";
+
+      $count = 0;
+      $res = preg_replace_callback($pattern, function ($matches) use ($value) {
+        return str_replace($matches[2] . $matches[3], $matches[2] . $value, $matches[0]);
+      }, $file_contents, 1, $count);
+
+      // If variable does not exist and $add flag is turn on we will add this
+      // value.
+      // @todo, @note - not sure how this will support two call like in the case
+      // when bs_versions key does not exist at all:
+      //
+      // $values = [
+      //   'bs_versions.bs_base' => 8000,
+      //   'bs_versions.bs_bootstrap' => 8000,
+      // ];
+      // _bs_base_set_yml_value($path, $values, TRUE);
+      //
+      // This call will fail in this case. If this happens we will need to
+      // fix/improve this code.
+      if ($count === 0 && $add) {
+        // Convert variable to yaml text format.
+        $keys = explode('.', $yml_key);
+        $yaml_value = [array_pop($keys) => $value];
+        while ($key = array_pop($keys)) {
+          $yaml_value = [$key => $yaml_value];
+        }
+
+        // Check does part of the yaml value already exist and if yes merge it
+        // with a new value.
+        // @note - this will use Yaml decode and encode and we will lose any
+        // comment that exist. Symfony it self will not implement comments
+        // support. @see https://github.com/symfony/symfony/issues/22516.
+        // @todo - however Acquia is using consolidation/comments library to
+        //   overcome this limitation of Symfony Yaml parser. We could implement
+        //   this approach in the future because it would simplify other parts
+        //   of drush script.
+        //   @see https://github.com/acquia/blt/pull/3629/files.
+        //   @see https://github.com/consolidation/comments
+        $root_key = key($yaml_value);
+        // Regexp to select all indented lines for a given root key.
+        // @see https://stackoverflow.com/a/48313919 for a bit more explanation on
+        // this pattern (multi line mode variation).
+        $root_key_pattern = "/^$root_key:\R(^ +.+\R)+/m";
+        $root_key_count = 0;
+        $res = preg_replace_callback($root_key_pattern, function ($matches) use ($yaml_value) {
+          $root_key_value = Yaml::decode($matches[0]);
+          $root_key_value = array_merge_recursive($root_key_value, $yaml_value);
+          return Yaml::encode($root_key_value);
+        }, $file_contents, 1, $root_key_count);
+        if ($root_key_count === 1) {
+          $file_contents = $res;
+        }
+        // If value does not exist let us simply add it to the end of the file.
+        else {
+          $file_contents .= "\n" . Yaml::encode($yaml_value);
+        }
+
+        $write = TRUE;
+      }
+      elseif (!empty($res) && $res !== $file_contents) {
+        $file_contents = $res;
+        $write = TRUE;
+      }
+    }
+
+    if ($write) {
+      file_put_contents($path, $file_contents);
+    }
   }
 
   /**
@@ -743,8 +1423,143 @@ class BsLibCommands extends DrushCommands {
    * @param string $theme_machine_name
    *   Theme machine name.
    */
-  protected function updateSassFiles($theme_machine_name) {
-    _bs_base_update_sass_files($theme_machine_name);
+  protected function updateSassFiles(string $theme_machine_name): bool {
+    /** @var ThemeTools $theme_tools */
+    $theme_tools = \Drupal::service('bs_lib.theme_tools');
+
+    $all_themes = $theme_tools->drupalThemeListInfo();
+    $parent_themes = $this->getParentThemes($theme_machine_name);
+    $target_path = $theme_tools->drupalGetThemePath($theme_machine_name);
+    $first_parent_machine_name = $all_themes[$theme_machine_name]->info['base theme'];
+
+    $options = [
+      'parent_machine_name' => $first_parent_machine_name,
+      'parent_path' => $all_themes[$first_parent_machine_name]->subpath,
+      'child_machine_name' => $theme_machine_name,
+      'child_path' => $target_path,
+      'child_name' => $all_themes[$theme_machine_name]->info['name'],
+      'child_description' => $all_themes[$theme_machine_name]->info['description'],
+    ];
+
+    // Build SASS info array of parent themes.
+    $parent_themes_sass_files = [];
+    /** @var \Drupal\Core\Extension\Extension $theme */
+    foreach (array_keys($parent_themes) as $parent_theme_machine_name) {
+      $parent_themes_sass_files[$parent_theme_machine_name] = $this->getSassFiles($all_themes[$parent_theme_machine_name]->subpath);
+    }
+
+    // Get target SASS files.
+    $target_theme_sass_files = $this->getSassFiles($all_themes[$theme_machine_name]->subpath);
+
+    // Add any missing main SASS files from first parent theme to a target theme.
+    $new_files = FALSE;
+    $new_sass_files = [];
+    foreach ($parent_themes_sass_files[$first_parent_machine_name] as $sass_file) {
+      $target_theme_sass_uri = DRUPAL_ROOT . '/' . $target_path . '/' . $sass_file->filepath;
+      if (!$sass_file->partial && empty($target_theme_sass_files[$target_theme_sass_uri])) {
+        $new_sass_files[] = $this->generateSassFile($sass_file, $options);
+        $new_files = TRUE;
+      }
+    }
+
+    // Make sure that _init and variables/_target_theme_machine_name.scss exists.
+    foreach (['sass/variables/_' . $theme_machine_name . '.scss', 'sass/_init.scss'] as $file) {
+      if (empty($target_theme_sass_files[DRUPAL_ROOT . '/' . $target_path . '/' . $file])) {
+        if (!$this->generateFile($file, $options)) {
+          Drush::logger()->log(LogLevel::ERROR, "Failed to generate default SASS file $file file.");
+        }
+        $new_files = TRUE;
+      }
+    }
+
+    // If we have new SASS files lets rescan target theme again.
+    if ($new_files) {
+      $target_theme_sass_files = $this->getSassFiles($target_path);
+    }
+
+    // Iterate over all *.scss files and for all non-partial files flatten SASS
+    // imports.
+    foreach ($target_theme_sass_files as $sass_file) {
+      // Do not process partials.
+      if ($sass_file->partial) {
+        continue;
+      }
+
+      $flattened_sass = $this->flattenSassFileImports($sass_file, $theme_machine_name, $all_themes, $parent_themes_sass_files);
+
+      // Check that parent files partials that are added to new files maybe exist
+      // in existing imports. If yes remove them. This is a case when some
+      // partials from parent themes are moved to new SASS file due to
+      // refactoring.
+      // Before removing duplicated imports check the content of target partials -
+      // if the partials holds only variables, mixins or functions (no CSS rules)
+      // then having multiple imports is fine and we should not remove it.
+      // @TODO - this is a very complex logic which does not need to be valid
+      // always. If this part of code is making more problems in future then
+      // consider to remove it and use update functions to handle refactor cases
+      // on per case base?
+      foreach ($new_sass_files as $new_file) {
+        if (empty($new_file)) {
+          continue;
+        }
+
+        $new_file_path = $all_themes[$first_parent_machine_name]->parentPath . '/' . $new_file . '.scss';
+        $new_sass_file = $parent_themes_sass_files[$first_parent_machine_name][$new_file_path];
+
+        // Skip newly added SASS file.
+        if ($new_sass_file->filepath === $sass_file->filepath) {
+          continue;
+        }
+
+        $new_sass_file_flattened = $this->flattenSassFileImports($new_sass_file, $theme_machine_name, $all_themes, $parent_themes_sass_files);
+        // Remove first @import "init";
+        array_shift($new_sass_file_flattened);
+
+        $remove_lines = array_intersect($flattened_sass, $new_sass_file_flattened);
+        if (!empty($remove_lines)) {
+          foreach ($remove_lines as $line_no => $remove_line) {
+            if (preg_match("#^(//|/\*)?\s*(@import\s+['\"])([a-zA-Z0-9_@]+)(.*?)['\"]#", $remove_line, $matches)) {
+              $remove_line_theme = $matches[3];
+              $remove_line_filepath_part = $matches[4];
+
+              if (isset($all_themes[$remove_line_theme]) && !empty($remove_line_filepath_part)) {
+                $parts = explode('/', $remove_line_filepath_part);
+                $last_element = array_key_last($parts);
+                if (!str_starts_with($parts[$last_element], '_')) {
+                  $parts[$last_element] = '_' . $parts[$last_element];
+                }
+
+                // Make partial file name.
+                $partial_filename = DRUPAL_ROOT . '/' . $all_themes[$remove_line_theme]->subpath . join('/', $parts) . '.scss';
+
+                if (isset($parent_themes_sass_files[$remove_line_theme][$partial_filename])) {
+                  $file_contents = file_get_contents($partial_filename);
+                  if ($file_contents === FALSE) {
+                    Drush::logger()->log(LogLevel::WARNING, "Can not open file $partial_filename for a check.");
+                    return FALSE;
+                  }
+
+                  // If there are no CSS rules in this partial we will consider it
+                  // as a variable/mixin/function partial that CAN be included in
+                  // multiple files and therefor we will not remove it.
+                  if (preg_match_all("#^[\.\#\[a-zA-Z0-9\*].+?\s+\{#m", $file_contents, $matches) === 0) {
+                    unset($remove_lines[$line_no]);
+                  }
+                }
+              }
+            }
+          }
+          $flattened_sass = array_diff($flattened_sass, $remove_lines);
+        }
+      }
+
+      // Save the file.
+      if (file_put_contents($sass_file->uri, implode('', $flattened_sass)) === FALSE) {
+        Drush::logger()->log(LogLevel::ERROR, "Failed to write {$sass_file->uri} file.");
+      }
+    }
+
+    return TRUE;
   }
 
 }
diff --git a/src/ThemeTools.php b/src/ThemeTools.php
new file mode 100644
index 0000000000000000000000000000000000000000..cafa8370156f72437b0820fe63b57277603e2cb6
--- /dev/null
+++ b/src/ThemeTools.php
@@ -0,0 +1,333 @@
+<?php
+
+namespace Drupal\bs_lib;
+
+use Drupal\Component\Serialization\Yaml;
+use Drupal\Core\DrupalKernel;
+use Drupal\Core\Extension\ExtensionDiscovery;
+use Drupal\Core\Extension\Discovery\RecursiveExtensionFilterIterator;
+use Drupal\Core\Site\Settings;
+use Exception;
+use stdClass;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Usefull functions for theme inspection.
+ */
+class ThemeTools {
+
+  /**
+   * Finds all the base themes for the specified theme.
+   *
+   * @param array $themes
+   *   An array of available themes.
+   * @param string $theme
+   *   The name of the theme whose base we are looking for.
+   *
+   * @return array
+   *   Returns an array of all the theme's ancestors including specified theme.
+   */
+  public function drupalGetBaseThemes(array $themes, string $theme): array {
+    $base_themes = [$theme => $themes[$theme]->info['name']];
+
+    if (!empty($themes[$theme]->info['base theme'])) {
+      return $this->drupalGetBaseThemes($themes, $themes[$theme]->info['base theme']) + $base_themes;
+    }
+
+    return $base_themes;
+  }
+
+  /**
+   * Returns the first parent theme of passed child theme.
+   *
+   * @param string $theme_name
+   *   The name of the child theme whose first parent theme we are looking for.
+   *
+   * @return string|NULL
+   *   Returns a theme machine name of first parent theme or NULL if parent does
+   *   not exist.
+   */
+  public function drupalGetParentThemeName($theme_name): ?string {
+    $themes_info = $this->drupalThemeListInfo();
+    $parent_themes = $this->drupalGetBaseThemes($themes_info, $theme_name);
+
+    end($parent_themes);
+    if (!prev($parent_themes)) {
+      return NULL;
+    }
+
+    return key($parent_themes);
+  }
+
+  /**
+   * Returns the path to a Drupal theme.
+   *
+   * @param string $name
+   *   Theme machine name.
+   *
+   * @return string
+   *   The path to the requested theme or an empty string if the item is not
+   *   found.
+   */
+  public function drupalGetThemePath($name): string {
+    $scan = $this->drupalScan('theme');
+
+    if (isset($scan[$name])) {
+      return $scan[$name]->subpath;
+    }
+
+    return '';
+  }
+
+  /**
+   * Get information's for all themes.
+   *
+   * @param bool $reset
+   *   Reset internal cache.
+   *
+   * @return array
+   *   Array holding themes information's.
+   */
+  public function drupalThemeListInfo(bool $reset = FALSE): array {
+    static $themes = [];
+
+    if (!$reset && !empty($themes)) {
+      return $themes;
+    }
+
+    $themes = $this->drupalScan('theme', $reset);
+    foreach ($themes as $theme_name => $theme) {
+      $themes[$theme_name]->info = Yaml::decode(file_get_contents($theme->pathname));
+    }
+
+    return $themes;
+  }
+
+  /**
+   * Discovers available extensions of a given type.
+   *
+   * For an explanation of how this work see ExtensionDiscovery::scan().
+   *
+   * @param string $type
+   *   The extension type to search for. One of 'profile', 'module', 'theme', or
+   *   'theme_engine'.
+   * @param bool $reset
+   *   Reset internal cache.
+   *
+   * @return array|null
+   *   An associative array of stdClass objects, keyed by extension name.
+   */
+  public function drupalScan(string $type, bool $reset = FALSE): ?array {
+    static $processed_files = NULL;
+
+    if (!$reset && !is_null($processed_files)) {
+      return $processed_files;
+    }
+
+    $search_dirs = [
+      ExtensionDiscovery::ORIGIN_SITES_ALL => 'sites/all',
+      ExtensionDiscovery::ORIGIN_ROOT => ''
+    ];
+    if (\Drupal::hasService('kernel')) {
+      $search_dirs[ExtensionDiscovery::ORIGIN_SITE] = \Drupal::getContainer()->getParameter('site.path');
+    }
+    else {
+      $search_dirs[ExtensionDiscovery::ORIGIN_SITE] = DrupalKernel::findSitePath(Request::createFromGlobals());
+    }
+
+    $files = [];
+    foreach ($search_dirs as $dir) {
+      $scan_res = $this->drupalScanDirectory($dir);
+      // Only return extensions of the requested type.
+      if (isset($scan_res[$type])) {
+        $files += $scan_res[$type];
+      }
+    }
+
+    // Duplicate files found in later search directories take precedence over
+    // earlier ones; they replace the extension in the existing $files array.
+    $processed_files = [];
+    foreach ($files as $file) {
+      $processed_files[basename($file->pathname, '.info.yml')] = $file;
+    }
+
+    return $processed_files;
+  }
+
+  /**
+   * Recursively scans a base directory for the extensions it contains.
+   *
+   * For an explanation of how this work @see
+   * ExtensionDiscovery::scanDirectory().
+   *
+   * @param string $dir
+   *   A relative base directory path to scan, without trailing slash.
+   *
+   * @return array
+   *   An associative array of stdClass objects, keyed by extension name.
+   */
+  public function drupalScanDirectory(string $dir): array {
+    $files = [];
+
+    $dir_prefix = ($dir == '' ? '' : "$dir/");
+    $absolute_dir = ($dir == '' ? DRUPAL_ROOT : DRUPAL_ROOT . "/$dir");
+
+    if (!is_dir($absolute_dir)) {
+      return $files;
+    }
+
+    $flags = \FilesystemIterator::UNIX_PATHS;
+    $flags |= \FilesystemIterator::SKIP_DOTS;
+    $flags |= \FilesystemIterator::FOLLOW_SYMLINKS;
+    $flags |= \FilesystemIterator::CURRENT_AS_SELF;
+    $directory_iterator = new \RecursiveDirectoryIterator($absolute_dir, $flags);
+
+    $ignore_directories = Settings::get('file_scan_ignore_directories', []);
+
+    $filter = new RecursiveExtensionFilterIterator($directory_iterator, $ignore_directories);
+
+    $iterator = new \RecursiveIteratorIterator($filter,
+      \RecursiveIteratorIterator::LEAVES_ONLY,
+      // Suppress filesystem errors in case a directory cannot be accessed.
+      \RecursiveIteratorIterator::CATCH_GET_CHILD
+    );
+
+    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('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $fileinfo->getBasename('.info.yml'))) {
+        continue;
+      }
+
+      // Determine extension type from info file.
+      $type = FALSE;
+      /** @var \SplFileObject $file */
+      $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)) {
+        continue;
+      }
+      $name = $fileinfo->getBasename('.info.yml');
+      $pathname = $dir_prefix . $fileinfo->getSubPathname();
+
+      // Determine whether the extension has a main extension file.
+      // For theme engines, the file extension is .engine.
+      if ($type == 'theme_engine') {
+        $filename = $name . '.engine';
+      }
+      // For profiles/modules/themes, it is the extension type.
+      else {
+        $filename = $name . '.' . $type;
+      }
+      if (!file_exists(DRUPAL_ROOT . '/' . dirname($pathname) . '/' . $filename)) {
+        $filename = NULL;
+      }
+
+      $extension = new stdClass();
+      $extension->type = $type;
+      $extension->pathname = $pathname;
+      $extension->filename = $filename;
+      // Add dir to subpath, so we can work with multisites also.
+      $extension->subpath = (!empty($dir) ? $dir . '/' : '') . $fileinfo->getSubPath();
+      // Extension parent folder path.
+      $extension->parentPath = substr($fileinfo->getPath(), 0, -(strlen($name) + 1));
+      $extension->origin = $dir;
+
+      $files[$type][$key] = $extension;
+    }
+
+    return $files;
+  }
+
+  /**
+   * Regular expression search and replace in the text.
+   *
+   * @param string $text
+   *   Text to search and replace.
+   * @param array $regexps
+   *   Array of regexps searches with it replace values.
+   * @param string $modifiers
+   *   PHP regular expression modifiers.
+   * @param string $delimiter
+   *   PHP regular expression delimiter.
+   *
+   * @return string
+   *   Replaced text.
+   */
+  public function regexp(string $text, array $regexps, string $modifiers = 'm', string $delimiter = '%'): string {
+    $new_content = $text;
+    foreach ($regexps as $pattern => $value) {
+      if ($replaced = preg_replace($this->getRegexp($pattern, $modifiers, $delimiter), $value, $new_content)) {
+        $new_content = $replaced;
+      }
+    }
+
+    return $new_content;
+  }
+
+  /**
+   * Regular expression search and replace in the file.
+   *
+   * @param string $file_name
+   *   File path.
+   * @param array $regexps
+   *   Array of regexps searches with it replace values.
+   *
+   * @return bool
+   *   TRUE on success, FALSE if file can not be open or saved.
+   */
+  public function regexpFile(string $file_name, array $regexps): bool {
+    $file_contents = file_get_contents($file_name);
+    if ($file_contents === FALSE) {
+      return FALSE;
+    }
+
+    return file_put_contents($file_name, $this->regexp($file_contents, $regexps));
+  }
+
+  /**
+   * Check does regular expression result exist in the file.
+   *
+   * @param string $file_name
+   *   File path.
+   * @param string $pattern
+   *   Regular expression pattern for search.
+   *
+   * @return bool
+   *   TRUE if it exists, FALSE other way.
+   *
+   * @throws Exception
+   */
+  public function regexpExist(string $file_name, string $pattern): bool {
+    $file_contents = file_get_contents($file_name);
+    if ($file_contents === FALSE) {
+      throw new Exception("Can not open file $file_name.");
+    }
+
+    $matches = [];
+    return preg_match($this->getRegexp($pattern), $file_contents, $matches) === 1;
+  }
+
+  /**
+   * Wraps a regexp pattern.
+   *
+   * @param string $pattern
+   *   Regexp pattern.
+   * @param string $modifiers
+   *   PHP regular expression modifiers.
+   * @param string $delimiter
+   *   PHP regular expression delimiter.
+   *
+   * @return string
+   *   Wrapped regexp pattern.
+   */
+  protected function getRegexp(string $pattern, string $modifiers = 'm', string $delimiter = '%'): string {
+    return $delimiter . $pattern . $delimiter . $modifiers;
+  }
+
+}