bs_base-8.x-1.x-dev/bs_base.bs_base.install
bs_base.bs_base.install
<?php
use Drupal\bs_lib\ThemeTools;
use Drush\Drush;
use Psr\Log\LogLevel;
/**
* @file
* Update hooks for bs_base base themes.
*/
/**
* Set bs_base.maintenance_image configuration value.
*/
function bs_base_bs_update_8000($target_theme_name) {
echo "To set the default maintenance image, run drush vset $target_theme_name.settings bs_base.maintenance_image.path themes/contrib/bs_base/images/maintenance.svg\n";
}
/**
* Update node js packages, remove gulp-rucksack and gulp-sourcemaps.
*/
function bs_base_bs_update_8001($target_theme_name) {
$themes_info = _bs_base_drupal_theme_list_info();
if (isset($themes_info[$target_theme_name])) {
_bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/package.json', [
'"autoprefixer": ".*"' => '"autoprefixer": "^9.7.3"',
'"deepmerge": ".*"' => '"deepmerge": "^3.3.0"',
'"del": ".*"' => '"del": "^4.1.1"',
'"eslint": ".*"' => '"eslint": "^5.16.0"',
'"gulp": ".*"' => '"gulp": "^4.0.2"',
'"gulp-load-plugins": ".*"' => '"gulp-load-plugins": "^1.6.0"',
// Remove gulp-rucksack.
'^\s*"gulp-rucksack": ".*",\n' => '',
'"gulp-sass-glob": ".*"' => '"gulp-sass-glob": "^1.1.0"',
// Remove gulp-sourcemaps.
'^\s*"gulp-sourcemaps": ".*",\n' => '',
'"js-yaml": ".*"' => '"js-yaml": "^3.13.1"',
'"postcss-scss": ".*"' => '"postcss-scss": "^1.0.6"',
'"pump": ".*"' => '"pump": "^2.0.1"',
'"stylelint-config-standard": ".*"' => '"stylelint-config-standard": "^18.3.0"',
'"stylelint-order": ".*"' => '"stylelint-order": "^2.2.1"',
'"stylelint-scss": ".*"' => '"stylelint-scss": "^3.13.0"',
'"stylelint-selector-bem-pattern": ".*"' => '"stylelint-selector-bem-pattern": "^2.1.0"'
]);
}
}
/**
* Add gulp-debug and through2 packages.
*/
function bs_base_bs_update_8002($target_theme_name) {
$themes_info = _bs_base_drupal_theme_list_info();
if (isset($themes_info[$target_theme_name])) {
_bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/package.json', [
'"devDependencies": {' => "\"devDependencies\": {\n \"gulp-debug\": \"^4.0.0\",\n \"through2\": \"^3.0.1\",",
]);
}
}
/**
* Add build-css package.json script.
*/
function bs_base_bs_update_8003($target_theme_name) {
$themes_info = _bs_base_drupal_theme_list_info();
if (isset($themes_info[$target_theme_name])) {
_bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/package.json', [
// Add a script part.
'^(?=.*devDependencies)[^{]+{[^}]+}' => "$0," . "\n". ' "scripts": {'. "\n". ' "build-css": "((type pnpm && pnpm install) || npm install) && npx gulp clean:css && npx gulp sass"' . "\n" . ' }',
]);
}
}
/**
* Drupal 9 compatibility update.
*/
function bs_base_bs_update_8004($target_theme_name) {
$themes_info = _bs_base_drupal_theme_list_info();
if (isset($themes_info[$target_theme_name])) {
_bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/' . $target_theme_name . '.info.yml', [
'core: 8.x' => 'core_version_requirement: ^8.7.7 || ^9',
]);
}
}
/**
* Update node libs.
*/
function bs_base_bs_update_8005($target_theme_name) {
$themes_info = _bs_base_drupal_theme_list_info();
if (isset($themes_info[$target_theme_name])) {
_bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/.browserslistrc', [
'last 2 version' => 'defaults',
"Safari >= 8\n" => '',
"not bb < 10 # means Support only latest Blackberry 10\n" => '',
]);
_bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/gulpfile.js', [
'yaml\.safeLoad' => 'yaml.load',
]);
_bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/package.json', [
'"autoprefixer": ".*"' => '"autoprefixer": "^10.2.6"',
'"deepmerge": ".*"' => '"deepmerge": "^4.2.2"',
'"del": ".*"' => '"del": "^6.0.0"',
'"eslint": ".*"' => '"eslint": "^7.28.0"',
'"gulp-changed": ".*"' => '"gulp-changed": "^4.0.3"',
'"gulp-if": ".*"' => '"gulp-if": "^3.0.0"',
'"gulp-load-plugins": ".*"' => '"gulp-load-plugins": "^2.0.7"',
'"gulp-notify": ".*"' => '"gulp-notify": "^4.0.0"',
'"gulp-postcss": ".*"' => '"gulp-postcss": "^9.0.0"',
'"gulp-sass": ".*"' => '"gulp-sass": "^4.1.0"',
'"js-yaml": ".*"' => '"js-yaml": "^4.1.0"',
'"postcss-reporter"' => '"postcss": "^8.3.4",' . "\n" . ' "postcss-reporter"',
'"postcss-reporter": ".*"' => '"postcss-reporter": "^7.0.2"',
'"postcss-scss": ".*"' => '"postcss-scss": "^4.0.0"',
'"pump": ".*"' => '"pump": "^3.0.0"',
'"stylelint": ".*"' => '"stylelint": "^13.13.1"',
'"stylelint-config-standard": ".*"' => '"stylelint-config-standard": "^22.0.0"',
'"stylelint-order": ".*"' => '"stylelint-order": "^4.1.0"',
'"stylelint-scss": ".*"' => '"stylelint-scss": "^3.19.0"',
// Remove through2 - replaced with native Node.js Transform streams
'^\s*"through2": ".*",?\n' => ''
]);
}
}
/**
* Check and fix bad update of bs_bootstrap to version 4.6.0.
*/
function bs_base_bs_update_8006($target_theme_name) {
// Detect if target theme is updated to non-existent 'bs_bootstrap: 8005'
// while actual bootstrap version used is 4.3.1.
$themes_info = _bs_base_drupal_theme_list_info();
if (isset($themes_info[$target_theme_name])) {
$yml_content = file_get_contents($themes_info[$target_theme_name]->pathname);
if ($yml_content && (strpos($yml_content, 'bs_bootstrap: 8004') || strpos($yml_content, 'bs_bootstrap: 8005'))) {
$package_content = file_get_contents($themes_info[$target_theme_name]->subpath . '/package.json');
if ($package_content && strpos($package_content, '"bootstrap": "~4.3.1",')) {
// Fix update version info.
$yml_content = str_replace('bs_bootstrap: 8004', 'bs_bootstrap: 8003', $yml_content);
$yml_content = str_replace('bs_bootstrap: 8005', 'bs_bootstrap: 8003', $yml_content);
file_put_contents($themes_info[$target_theme_name]->pathname, $yml_content);
// Notify user that he needs to execute update one more time.
drush_log("Bad update is fixed, please run `drush bs-tu $target_theme_name` one more time to update theme correctly.", LogLevel::ALERT);
}
}
}
}
/**
* Remove node plugins that we do not use.
*/
function bs_base_bs_update_8007($target_theme_name) {
$themes_info = _bs_base_drupal_theme_list_info();
if (isset($themes_info[$target_theme_name])) {
_bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/package.json', [
"^.*\"eslint.*\n" => '',
"^.*\"gulp-cached.*\n" => '',
"^.*\"gulp-changed.*\n" => '',
"^.*\"gulp-if.*\n" => '',
"^.*\"gulp-notify.*\n" => '',
"^.*\"gulp-sass-glob.*\n" => '',
"^.*\"gulp-svgstore.*\n" => '',
"^.*\"gulp-uglify.*\n" => '',
"^.*\"node-sass-import-once.*\n" => '',
"^.*\"postcss-scss.*\n" => '',
"^.*\"pump.*\n" => '',
]);
}
}
/**
* Update node js packages.
*/
function bs_base_bs_update_8008($target_theme_name) {
$themes_info = _bs_base_drupal_theme_list_info();
if (isset($themes_info[$target_theme_name])) {
_bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/package.json', [
'"autoprefixer": ".*"' => '"autoprefixer": "^10.4.5"',
'"gulp-postcss": ".*"' => '"gulp-postcss": "^9.0.1"',
'"gulp-sass": ".*"' => '"gulp-sass": "^4.1.1"',
'"postcss": ".*"' => '"postcss": "^8.4.13"',
'"postcss-reporter": ".*"' => '"postcss-reporter": "^7.0.5"',
'"stylelint-scss": ".*"' => '"stylelint-scss": "^3.21.0"',
'"stylelint-selector-bem-pattern": ".*"' => '"stylelint-selector-bem-pattern": "^2.1.1"',
]);
}
}
/**
* Remove IE11 support.
*/
function bs_base_bs_update_8009($target_theme_name) {
$themes_info = _bs_base_drupal_theme_list_info();
if (isset($themes_info[$target_theme_name])) {
_bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/.browserslistrc', [
'^#?ie >= 11\n' => '',
]);
}
}
/**
* Update node js packages.
*/
function bs_base_bs_update_8010($target_theme_name) {
$themes_info = _bs_base_drupal_theme_list_info();
if (isset($themes_info[$target_theme_name])) {
// Dependency update.
$updates = [
'"autoprefixer": ".*"' => "\"@hail2u/css-mqpacker\": \"github:hail2u/node-css-mqpacker#v9.0.1\",\n \"autoprefixer\": \"^10.4.13\"",
// Remove css-mqpacker in favor on new @hail2u/css-mqpacker.
'^\s*"css-mqpacker": ".*",\n' => '',
'"bootstrap": ".*"' => '"bootstrap": "~4.6.2"',
'"gulp-load-plugins": ".*"' => '"gulp-load-plugins": "^2.0.8"',
'"gulp-sass": ".*"' => '"gulp-sass": "^5.1.0"',
'"postcss": ".*"' => "\"node-sass\": \"^8.0.0\",\n \"postcss\": \"^8.4.21\"",
'"stylelint": ".*"' => '"stylelint": "^14.16.1"',
'"stylelint-config-standard": ".*"' => '"stylelint-config-standard": "^29.0.0"',
'"stylelint-order": ".*"' => '"stylelint-order": "^6.0.1"',
'"stylelint-scss": ".*"' => '"stylelint-scss": "^4.3.0"',
];
// If node is already defined just update to version 18.
if (_bs_base_regexp_exist($themes_info[$target_theme_name]->subpath . '/package.json', '"node": ".*"')) {
$updates['"node": ".*"'] = '"node": "18"';
}
else {
$updates['"license": "GPL",'] = "\"license\": \"GPL\",\n \"engines\": {\n \"node\": \"18\"\n },";
}
_bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/package.json', $updates);
// gulp-sass 5 breaking change.
_bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/gulp-tasks.js', [
'\(gulp, plugins, options\)' => '(gulp, sass, plugins, options)',
]);
_bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/gulpfile.js', [
'merge = require\(\'deepmerge\'\);' => "merge = require('deepmerge');\nconst sass = require('gulp-sass')(require('node-sass'));",
'\(gulp, plugins, options\)' => '(gulp, sass, plugins, options)'
]);
// Copy new .npmrc and .nvmrc from bs_bootstrap theme.
$options = [
'parent_machine_name' => 'bs_bootstrap',
'parent_path' => _bs_base_drupal_get_theme_path('bs_bootstrap'),
'child_machine_name' => $target_theme_name,
'child_path' => _bs_base_drupal_get_theme_path($target_theme_name),
];
foreach (['.npmrc', '.nvmrc'] as $file) {
_bs_base_copy_file($file, $options);
}
// Remove node_modules so, we are sure we install/rebuild everything clean.
system('rm -rf ' . escapeshellarg($themes_info[$target_theme_name]->subpath . '/node_modules'));
}
}
/**
* Add caniuse-lite update into theme build process.
*/
function bs_base_bs_update_8011($target_theme_name) {
$themes_info = _bs_base_drupal_theme_list_info();
if (isset($themes_info[$target_theme_name])) {
_bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/package.json', [
'npx gulp clean:css' => "npx browserslist@latest --update-db && npx gulp clean:css",
]);
}
}
/**
* Remove gulp-plumber, styleint and gulp-sass-lint, update other node libs.
*/
function bs_base_bs_update_8012($target_theme_name) {
$themes_info = _bs_base_drupal_theme_list_info();
if (isset($themes_info[$target_theme_name])) {
// Dependency updates.
_bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/package.json', [
// Packages updates.
'"autoprefixer": ".*"' => '"autoprefixer": "^10.4.14"',
'"deepmerge": ".*"' => '"deepmerge": "^4.3.0"',
'"del": ".*"' => '"del": "^6.1.1"',
// Packages removal.
'^\s*"gulp-plumber": ".*",\n' => '',
'^\s*"gulp-sass-lint": ".*",\n' => '',
'^\s*"stylelint": ".*",\n' => '',
'^\s*"stylelint-config-standard": ".*",\n' => '',
'^\s*"stylelint-order": ".*",\n' => '',
'^\s*"stylelint-scss": ".*",\n' => '',
'^\s*"stylelint-selector-bem-pattern": ".*",\n' => '',
]);
// Remove stylelint configuration.
_bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/gulp-options.yml', [
'# Stylelint config.*\n.*\n.*\.stylelintrc\.yml\'' => '',
]);
// Update other node dependencies in a package-lock.json or pnpm-lock.yaml.
$target_path = _bs_base_drupal_get_theme_path($target_theme_name);
exec('cd ' . $target_path . ' && ((type pnpm && pnpm update) || npm update)');
}
}
/**
* Remove pnpm support.
*/
function bs_base_bs_update_8013($target_theme_name) {
$themes_info = _bs_base_drupal_theme_list_info();
if (isset($themes_info[$target_theme_name])) {
_bs_base_regexp_file($themes_info[$target_theme_name]->subpath . '/package.json', [
// Packages updates.
'\(\(type pnpm \&\& pnpm install\) \|\| npm install\)' => 'npm install',
]);
system('rm -f ' . escapeshellarg($themes_info[$target_theme_name]->subpath . '/pnpm-lock.yaml'));
// Remove node_modules so, we are sure we install/rebuild everything clean.
system('rm -rf ' . escapeshellarg($themes_info[$target_theme_name]->subpath . '/node_modules'));
}
}
/**
* Convert theme-options.yml vars to SASS vars and remove theme-options.yml.
*/
function bs_base_bs_update_200001($target_theme_name) {
$themes_info = ThemeTools::drupalThemeListInfo();
if (isset($themes_info[$target_theme_name])) {
// Convert theme-options.yml SASS vars to native SASS vars.
$theme_path = $themes_info[$target_theme_name]->subpath;
$theme_options_file = $theme_path . '/theme-options.yml';
if (file_exists($theme_options_file)) {
$sass_content = _bs_base_yaml_variables_to_sass($theme_options_file);
// Check if sass section exists with injectVariables
if (!empty($sass_content)) {
$file_content = "// Converted from theme-options.yml\n\n" . $sass_content;
// Create variables directory if it doesn't exist
$variables_dir = $theme_path . '/sass/variables';
if (!is_dir($variables_dir)) {
mkdir($variables_dir, 0755, TRUE);
}
// Write SASS variables to file.
$sass_file = $variables_dir . '/_' . $target_theme_name . '.scss';
if (file_exists($sass_file)) {
// If SASS vars file already exist join converted SASS code with old
// code.
$existing_content = file_get_contents($sass_file);
$file_content .= "\n\n\n// Original content.\n\n" . $existing_content;
}
file_put_contents($sass_file, $file_content);
Drush::logger()->info("Successfully converted YAML variables to SASS for $target_theme_name theme.");
}
else {
Drush::logger()->warning("No variables section found in theme-options.yml for $target_theme_name theme.");
}
// Delete theme-options.yml file after conversion.
unlink($theme_options_file);
}
else {
Drush::logger()->warning("No theme-options.yml file found for $target_theme_name theme.");
}
// Remove gulp-sass-inject package.
ThemeTools::regexpFile($themes_info[$target_theme_name]->subpath . '/package.json', [
'\s*"gulp-sass-inject": ".*",' => '',
]);
// Remove the theme-options.yml JS code block from gulpfile.js.
$theme_gulp_file = $theme_path . '/gulpfile.js';
$js = file_get_contents($theme_gulp_file);
$replace_count = 0;
$js = preg_replace(
'#\n// Load theme options if theme-options\.yml file exist and merge it with.*?\n}\n#s',
'',
$js,
-1,
$replace_count
);
if ($replace_count === 1) {
file_put_contents($theme_gulp_file, $js);
}
else {
Drush::logger()->warning("Failed to remove theme-options.yml JS code in $theme_gulp_file file.");
}
}
}
/**
* Converts a YAML file's `variables:` section into SASS variables.
*
* - Supports nested maps (up to 3 levels deep).
* - Preserves comments and blank lines.
* - Skips YAML lists (not supported).
*
* @param string $yamlPath
* Path to YAML file.
* @return string
* Converted YAML vars to SASS vars.
*/
function _bs_base_yaml_variables_to_sass(string $yamlPath): string {
$lines = file($yamlPath);
$inVariables = FALSE;
$variablesIndent = NULL;
$sass = [];
$stack = [];
foreach ($lines as $line) {
// Detect start of 'variables:' section.
if (!$inVariables && preg_match('/^\s*variables:\s*$/', $line)) {
$inVariables = TRUE;
$variablesIndent = strlen($line) - strlen(ltrim($line)) + 2;
continue;
}
if ($inVariables) {
$indent = strlen($line) - strlen(ltrim($line));
$trimmed = trim($line);
// Check empty lines and line comments.
if ($trimmed === '' || str_starts_with($trimmed, '#')) {
if (str_starts_with($trimmed, '#')) {
// Replace leading # with //
$sass[] = preg_replace('/^#/', '$1//', $trimmed);
}
else {
// Close maps if needed.
while (count($stack) > 0) {
array_pop($stack);
$sass[] = str_repeat(' ', count($stack)) . (count($stack) > 0 ? '),' : ');');
}
$sass[] = '';
}
continue;
}
// Check for a key without value (start of a map).
if (preg_match('/^(\s*)([^\s:]+):\s*$/', $line, $m)) {
$key = $m[2];
$depth = ($indent - $variablesIndent) / 2;
// Close map if needed.
while (count($stack) > $depth) {
array_pop($stack);
$sass[] = str_repeat(' ', count($stack)) . (count($stack) > 0 ? '),' : ');');
}
// Add new map opening.
if (count($stack) === 0) {
$sass[] = str_repeat(' ', $depth) . "\$$key: (";
}
else {
$sass[] = str_repeat(' ', $depth) . "$key: (";
}
$stack[] = $key;
continue;
}
// Handle key-value with optional inline comment.
if (preg_match('/^\s*([^\s:]+):\s+([\'"]?.+?[\'"]?)\s*(#.*)?$/', $line, $m)) {
$key = $m[1];
$val = trim($m[2], "'\""); // Remove YAML quotes
$comment = $m[3] ?? '';
$depth = ($indent - $variablesIndent) / 2;
$lineOut = str_repeat(' ', $depth);
if (empty($stack)) {
$lineOut .= "\$$key: $val;";
} else {
$lineOut .= "$key: $val,";
}
if ($comment) {
$comment = preg_replace('/^#/', '$1//', $comment);
$lineOut .= " $comment";
}
$sass[] = $lineOut;
continue;
}
// Unknown line structure, just keep it.
$sass[] = $line;
}
}
// Close any remaining open maps.
while (!empty($stack)) {
array_pop($stack);
$sass[] = str_repeat(' ', count($stack)) . (count($stack) > 0 ? '),' : ');');
}
return implode("\n", $sass);
}
/**
* Deprecate bs_bootstrap.
*/
function bs_base_bs_update_200002($target_theme_name) {
$themes_info = ThemeTools::drupalThemeListInfo();
if (isset($themes_info[$target_theme_name])) {
// Theme info file.
ThemeTools::regexpFile($themes_info[$target_theme_name]->subpath . '/' . $target_theme_name . '.info.yml', [
// Remove bs_bootstrap update version.
'\s*bs_bootstrap: 8009' => '',
// /themes/contrib/bs_base/themes/bs_bootstrap => /themes/contrib/bs_base
'/' . $themes_info['bs_bootstrap']->subpath . '/css' => '/' . $themes_info['bs_base']->subpath . '/css',
// All other bs_bootstrap to bs_base.
'bs_bootstrap' => 'bs_base',
]);
// README.md, gulpfile.js: bs_bootstrap => bs_base.
ThemeTools::regexpFile($themes_info[$target_theme_name]->subpath . '/README.md', [
'bs_bootstrap' => 'bs_base',
]);
ThemeTools::regexpFile($themes_info[$target_theme_name]->subpath . '/gulpfile.js', [
'bs_bootstrap' => 'bs_base',
]);
// Config install bs_bootstrap: => bs_base:.
ThemeTools::regexpFile($themes_info[$target_theme_name]->subpath . '/config/install/' . $target_theme_name . '.settings.yml', [
'bs_bootstrap:' => 'bs_base:',
]);
// Config scheme bs_bootstrap.settings => bs_base.settings.
ThemeTools::regexpFile($themes_info[$target_theme_name]->subpath . '/config/schema/' . $target_theme_name . '.schema.yml', [
'bs_bootstrap\.settings' => 'bs_base.settings',
]);
// Theme file bs_bootstrap => bs_base.
$theme_file = $themes_info[$target_theme_name]->subpath . '/' . $target_theme_name . '.theme';
if (file_exists($theme_file)) {
ThemeTools::regexpFile($theme_file, [
'bs_bootstrap' => 'bs_base',
]);
}
// All SASS files bs_bootstrap => bs_base.
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator(DRUPAL_ROOT . '/' . $themes_info[$target_theme_name]->subpath . '/sass')
);
foreach ($iterator as $file) {
if ($file->isFile() && $file->getExtension() === 'scss') {
ThemeTools::regexpFile($file->getPathname(), [
'bs_bootstrap' => 'bs_base',
]);
}
}
// gulp-options.yml remove bs_bootstrap section from parentTheme.
ThemeTools::regexpFile($themes_info[$target_theme_name]->subpath . '/gulp-options.yml', [
"\s*-\s*name: 'bs_bootstrap'(?:\n\s*#.*)?\n.*" => '',
]);
// Templates folder - extending bs_bootstrap and attaching bs_bootstrap libs.
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator(DRUPAL_ROOT . '/' . $themes_info[$target_theme_name]->subpath . '/templates')
);
foreach ($iterator as $file) {
if ($file->isFile()) {
ThemeTools::regexpFile($file->getPathname(), [
'bs_bootstrap' => 'bs_base',
]);
}
}
// We changed parent theme definition from bs_bootstrap to bs_base so we
// need to refresh theme info data.
ThemeTools::drupalThemeListInfo(TRUE);
Drush::logger()->warning('Make sure to clear Drupal cache after this update!');
}
}
/**
* Switch to icons font auto hashing.
*/
function bs_base_bs_update_200003($target_theme_name) {
$themes_info = ThemeTools::drupalThemeListInfo();
if (isset($themes_info[$target_theme_name])) {
// fantasticon/scss.hbs append SASS var $bs-icon-font-hash to the end.
$file_contents = file_get_contents($themes_info[$target_theme_name]->subpath . '/fantasticon/scss.hbs');
if ($file_contents !== FALSE) {
$file_contents .= "\n" . '$bs-icon-font-hash: "__ICON_FONT_HASH__" !default;' . "\n";
file_put_contents($themes_info[$target_theme_name]->subpath . '/fantasticon/scss.hbs', $file_contents);
}
// package.json update build-icons step.
ThemeTools::regexpFile($themes_info[$target_theme_name]->subpath . '/package.json', [
'"build-icons": "fantasticon"' => '"build-icons": "node build-icons.js"',
]);
// Copy build-icons.js from bs_base to target theme.
$options = [
'parent_machine_name' => 'bs_base',
'parent_path' => ThemeTools::drupalGetThemePath('bs_base'),
'child_machine_name' => $target_theme_name,
'child_path' => ThemeTools::drupalGetThemePath($target_theme_name),
];
ThemeTools::copyFile('build-icons.js', $options);
// Check if $bs-icon-font-hash was used in target theme and if yes remove
// it definition.
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator(DRUPAL_ROOT . '/' . $themes_info[$target_theme_name]->subpath . '/sass/variables')
);
foreach ($iterator as $file) {
if ($file->isFile() && $file->getExtension() === 'scss') {
ThemeTools::regexpFile($file->getPathname(), [
'\$bs-icon-font-hash:.*\n' => '',
]);
}
}
}
}
/**
* Do not automatically update caniuse-lite db anymore.
*/
function bs_base_bs_update_200004($target_theme_name) {
$themes_info = ThemeTools::drupalThemeListInfo();
if (isset($themes_info[$target_theme_name])) {
// package.json update steps changes.
ThemeTools::regexpFile($themes_info[$target_theme_name]->subpath . '/package.json', [
// Remove: "build-browsers": "npx browserslist@latest --update-db",
'^\s+"build-browsers": "npx browserslist@latest --update-db",\n' => '',
// Remove 'npm run build-browsers' from build step and add a new
// 'update-browserslist' step after it.
'"build": "npm run build-install & npm run build-icons &\s*npm run build-browsers & npm run build-css"' => '"build": "npm run build-install & npm run build-icons & npm run build-css",'. "\n" . ' "update-browserslist": "npx update-browserslist-db@latest"',
]);
// Show an example how to execute browserlist-db update in the future
// from update function when needed.
ThemeTools::updateBrowserList($target_theme_name);
}
}
/**
* Optimize CSS @font-face loading.
*/
function bs_base_bs_update_200005($target_theme_name) {
$themes_info = ThemeTools::drupalThemeListInfo();
if (isset($themes_info[$target_theme_name])) {
// Remove: `@import "bs_base/sass/theme/partials/fonts";` line with or
// without comment on the start of the line.
ThemeTools::regexpFile($themes_info[$target_theme_name]->subpath . '/sass/theme/theme.scss', [
'^(?:\/\/\s+)?@import\s+"bs_base\/sass\/theme\/partials\/fonts";\n' => '',
]);
}
}
/**
* Migrate to Bootstrap 5.3.8 and update dependencies.
*/
function bs_base_bs_update_200006($target_theme_name) {
$themes_info = ThemeTools::drupalThemeListInfo();
if (empty($themes_info[$target_theme_name])) {
return;
}
$theme_path = $themes_info[$target_theme_name]->subpath;
// Update Bootstrap and related dependencies.
ThemeTools::regexpFile($theme_path . '/package.json', [
// Update Node.js version requirement.
'"node": "18"' => '"node": "20"',
// Add Popper.js for Bootstrap 5, update to Bootstrap 5.3.8, update other
// dependencies and switch to exact versions to prevent accidental updates to
//insecure npm versions.
'"@twbs/fantasticon": ".*"' => '"@popperjs/core": "2.11.8",' . "\n" . ' "@twbs/fantasticon": "3.1.0"',
'"bootstrap": ".*"' => '"bootstrap": "5.3.8"',
'"autoprefixer": ".*"' => '"autoprefixer": "10.4.21"',
'"deepmerge": ".*"' => '"deepmerge": "4.3.1"',
'"del": ".*"' => '"del": "6.1.1"',
'"gulp": ".*"' => '"gulp": "4.0.2"',
'"gulp-load-plugins": ".*"' => '"gulp-load-plugins": "2.0.8"',
'"gulp-postcss": ".*"' => '"gulp-postcss": "10.0.0"',
'"gulp-sass": ".*"' => '"gulp-sass": "5.1.0"',
'"gulp-strip-css-comments": ".*"' => '"gulp-strip-css-comments": "2.0.0"',
'"js-yaml": ".*"' => '"js-yaml": "4.1.0"',
'"postcss": ".*"' => '"postcss": "8.5.6"',
'"postcss-reporter": ".*"' => '"postcss-reporter": "7.1.0"',
'"node-sass": ".*"' => '"node-sass": "9.0.0"',
// Remove gulp-debug - replaced with native Node.js Transform streams.
'^\s*"gulp-debug": ".*",?\n' => '',
// Remove deprecated/unused packages.
'^\s*"@hail2u/css-mqpacker": ".*",\n' => '',
'^\s*"font-awesome": ".*",\n' => '',
'^\s*"gulp-concat": ".*",\n' => '',
'^\s*"picocolors": ".*",\n' => '',
// Make sure through2 is removed no matter is it last or in the middle.
'^\s*"through2": ".*",\n' => '',
',\n^\s*"through2": ".*"\n' => '',
// Fix build script to use sequential execution.
'"build": "npm run build-install & npm run build-icons & npm run build-css"' => '"build": "npm run build-install && npm run build-icons && npm run build-css"',
]);
// Remove css-mqpacker from gulp-options.yml.
ThemeTools::regexpFile($theme_path . '/gulp-options.yml', [
'^\s*- "@hail2u/css-mqpacker"\n' => '',
]);
// Remove css-mqpacker from gulp-tasks.js if it exists.
$gulp_tasks_file = $theme_path . '/gulp-tasks.js';
if (file_exists($gulp_tasks_file)) {
ThemeTools::regexpFile($gulp_tasks_file, [
',\s*plugins\.hail2u\.cssMqpacker\(\{sort:\s*true\}\)' => '',
]);
}
// Rename bs_lib/messages to bs_lib/alert. for twig and yaml files.
// No need to do replacement in theme.info.yml file, ti will be done by
// generator update when it detects missing library and a new library in
// parent theme.
ThemeTools::regexpFiles($theme_path, '*.twig', [
'bs_lib/messages' => 'bs_lib/alert',
]);
ThemeTools::regexpFile($theme_path . '/' . $target_theme_name . '.libraries.yml', [
// navigation.css is removed and parts are mvoed to nav and navbar libs.
'\s*css/components/navigation.css: {}' => '',
// pagination.css is moved from global styling to pagination lib.
'\s*css/components/pagination.css: {}' => '',
]);
// Notify s that sass/components/navigation.scss is removed, and they
// needs to manually delete this file and move any custom SASS imports to
// either nav or navbar.scss.
Drush::logger()->warning("Notice that {$theme_path}/sass/components/navigation.scss is removed. Please remove this file and move any custom SASS imports to sass/components/nav.scss or sass/components/navbar.scss.");
// Update Bootstrap 5 data attributes in template files.
$template_files = glob($theme_path . '/{**/*,*}.twig');
foreach ($template_files as $file) {
$content = file_get_contents($file);
$original_content = $content;
// Update data attributes
$content = str_replace('data-toggle=', 'data-bs-toggle=', $content);
$content = str_replace('data-target=', 'data-bs-target=', $content);
$content = str_replace('data-dismiss=', 'data-bs-dismiss=', $content);
// Update screen reader classes
$content = str_replace('class="sr-only"', 'class="visually-hidden"', $content);
$content = str_replace("class='sr-only'", "class='visually-hidden'", $content);
// Update close button classes
$content = str_replace('class="close"', 'class="btn-close"', $content);
$content = str_replace("class='close'", "class='btn-close'", $content);
// Update dropdown positioning classes
$content = str_replace('dropdown-menu-right', 'dropdown-menu-end', $content);
$content = str_replace('dropdown-menu-left', 'dropdown-menu-start', $content);
// Remove deprecated input-group wrapper classes (simplified for safety)
$content = str_replace('input-group-prepend', '', $content);
$content = str_replace('input-group-append', '', $content);
if ($content !== $original_content) {
file_put_contents($file, $content);
}
}
// Update JavaScript files too with data attrib changes.
$js_files = glob($theme_path . '/js/*.js');
foreach ($js_files as $file) {
$content = file_get_contents($file);
$original_content = $content;
// Update data attributes in JS
$content = str_replace('data-dismiss', 'data-bs-dismiss', $content);
$content = str_replace('data-toggle', 'data-bs-toggle', $content);
$content = str_replace('data-target', 'data-bs-target', $content);
// Update close button class and remove inner span
$content = preg_replace('/class="close"/', 'class="btn-close"', $content);
$content = preg_replace('/<span aria-hidden="true">×<\/span>\s*<\/button>/', '</button>', $content);
// Update sr-only class
$content = str_replace('sr-only', 'visually-hidden', $content);
if ($content !== $original_content) {
file_put_contents($file, $content);
}
}
// Inject import functions into SASS init.
$sass_import_functions = ThemeTools::getThemeSASSImportFunctions($target_theme_name);
if (!empty($sass_import_functions)) {
// Inject on first empty line after first comments.
if (ThemeTools::regexpExist($theme_path . '/sass/_init.scss', '\A((?://[^\n]*\n)+)(\n)')) {
ThemeTools::regexpFile($theme_path . '/sass/_init.scss', [
'\A((?://[^\n]*\n)+)(\n)' => '$1' . "\n" . $sass_import_functions . "\n\n",
], 1);
}
// Or if this empty line does not exist inject on the start.
else {
ThemeTools::regexpFile($theme_path . '/sass/_init.scss', [
'\A' => $sass_import_functions . "\n\n",
], 1);
}
}
ThemeTools::regexpFile($theme_path . '/config/install/' . $target_theme_name . '.settings.yml', [
// Remove custom_forms setting.
'\s*custom_forms: .*' => '',
]);
// Bootstrap 5 SASS breaking changes - imports, functions, and variables
// changes.
// Update variable names, classes, etc in SASS.
ThemeTools::regexpFiles($theme_path . '/sass', '*.scss', [
// Remove Bootstrap imports that no longer exist in Bootstrap 5.
'^\s*@import\s*"bootstrap/scss/code";\s*\n' => '',
'^\s*@import\s*"bootstrap/scss/custom-forms";\s*\n' => '',
'^\s*@import\s*"bootstrap/scss/print";\s*\n' => '',
'^\s*@import\s*"bootstrap/scss/utilities/text";\s*\n' => '',
'^\s*@import\s*"bootstrap/scss/utilities/background";\s*\n' => '',
'^\s*@import\s*"bootstrap/scss/utilities/sizing";\s*\n' => '',
// Fix Bootstrap 5 import path changes
'@import\s*"bootstrap/scss/utilities/screenreaders(\.scss)?"' => '@import "bootstrap/scss/helpers/visually-hidden"',
'@import\s*"bootstrap/scss/utilities/stretched-link"' => '@import "bootstrap/scss/helpers/stretched-link"',
'@import\s*"bootstrap/scss/input-group"' => '@import "bootstrap/scss/forms/input-group"',
// Heading vars.
'\$font-size-h1' => '$h1-font-size',
'\$font-size-h2' => '$h2-font-size',
'\$font-size-h3' => '$h3-font-size',
'\$font-size-h4' => '$h4-font-size',
'\$font-size-h5' => '$h5-font-size',
'\$font-size-h6' => '$h6-font-size',
// Forms.
'\$form-group-margin-bottom' => '$form-item-margin-y',
'\$custom-control-indicator-bg' => '$form-check-input-bg',
'\$custom-control-label-disabled-color' => '$form-check-input-disabled-color',
'\$custom-select-bg' => '$form-select-bg',
'\$custom-select-border-radius' => '$form-select-border-radius',
'\$custom-select-disabled-color' => '$form-select-disabled-color',
'\$custom-select-focus-box-shadow' => '$form-select-focus-box-shadow',
'\$custom-select-indicator-color' => '$form-select-indicator-color',
'\$custom-select-bg-size' => '$form-select-bg-size',
'\$custom-select-indicator' => '$form-select-indicator',
'\$custom-select-indicator-padding' => '$form-select-indicator-padding',
'\.custom-control-label' => '.form-check-label',
'\.custom-control-input' => '.form-check-input',
'\.custom-select' => '.form-select',
'\.custom-file-control' => '.form-control',
'\.form-group' => '.form-item',
// $custom-input-button key border-hover-color is renamed.
'\'border-hover-color\'' => '\'hover-border-color\'',
// Spacing utilities.
'\.ml-' => '.ms-',
'\.mr-' => '.me-',
'\.pl-' => '.ps-',
'\.pr-' => '.pe-',
// Text alignment.
'\.text-left' => '.text-start',
'\.text-right' => '.text-end',
// Float utilities.
'\.float-left' => '.float-start',
'\.float-right' => '.float-end',
// Border utilities.
'\.border-left' => '.border-start',
'\.border-right' => '.border-end',
// Table utilities.
'\$table-cell-padding;' => '$table-cell-padding-x;',
// Text utilities.
'\.text-monospace' => '.font-monospace',
'\.font-weight-' => '.fw-',
'\.font-italic' => '.fst-italic',
// Button utilities.
'\.btn-block' => '.d-grid',
// Replace deprecated SASS mixins and function with pure CSS, Bootstrap 5 or
// more supported SASS versions.
'@include hover\(\)' => '&:hover',
'@include hover-focus\(\)' => '&:hover, &:focus',
'@include plain-hover-focus\(\)' => '&:hover, &:focus',
'@include hover-focus-active\(\)' => '&:hover, &:focus, &:active',
'@include hover {' => '&:hover {',
'@include hover-focus {' => '&:hover, &:focus {',
'color-yiq\(' => 'color-contrast(',
// darken(color, amt) → adjust-color(color, $lightness: -amt)
'(?<![\w-])darken\(\s*((?:[^(),]++|(?<par>\((?:[^()]++|(?&par))*\)))++)\s*,\s*((?:[^()]++|(?&par))++)\s*\)' => 'adjust-color($1, \$lightness: -$3)',
// lighten(color, amt) → adjust-color(color, $lightness: amt)
'(?<![\w-])lighten\(\s*((?:[^(),]++|(?<par>\((?:[^()]++|(?&par))*\)))++)\s*,\s*((?:[^()]++|(?&par))++)\s*\)' => 'adjust-color($1, \$lightness: $3)',
'@elseif' => '@else if',
'@include sr-only' => '@include visually-hidden',
'@include bs-icon-var\(bs-icon-content\(([\'"][^\'\"]+[\'"])\)\);' => '@include bs-icon-create-content($1);',
'@include fa-icon\(' => '@include bs-icon-font(',
// Replace theme-color-level function with shift-color
'theme-color-level\(' => 'shift-color(',
'\$alert-bg-level' => '$alert-bg-scale',
'border-left-radius\(' => 'border-start-radius(',
'border-right-radius\(' => 'border-end-radius(',
// Replace all division operator `/` with `divide` BS5 function.
// This pattern:
// 0. Skips lines starting with `//` or containing aspect-ratio/grid-row/grid-column
// 1. Uses negative lookbehind (?<![\/\w]) to avoid URLs
// 2. Uses negative lookahead (?![\/\w]) to avoid file paths
// 3. Matches full left-hand expressions (variables, numbers, parens, + - * ops)
// 4. Restricts right-hand side to a single primary (var/number/paren), preserving operator order
// 5. Handles Bootstrap’s shorthand cases like `0 2/3*$spacer-x`
//
// EXAMPLES IT HANDLES:
// - $spacer / 2 → divide($spacer, 2)
// - $grid-gutter-width / 2 → divide($grid-gutter-width, 2)
// - $font-size-base * 1.25 / 2 → divide($font-size-base * 1.25, 2)
// - ($spacer * 3) / 4 → divide(($spacer * 3), 4)
// - $spacer / ($ratio + 1) → divide($spacer, ($ratio + 1))
// - 0 2/3*$spacer-x → 0 divide(2, 3)*$spacer-x
//
// NOTE - this is not perfect, some more complex combinations will either
// not be converted or converted with error which will require manual
// intervention/fix.
'/(?m)^[ \t]*\/\/.*$(*SKIP)(*F)|^[ \t]*(?:aspect-ratio|grid-(?:column|row))\s*:[^\n]*$(*SKIP)(*F)|(?<![\/\w])((?:\$[\w-]+|\d+(?:\.\d+)?|\([^)]+\))(?:\s*[\+\-\*]\s*(?:\$[\w-]+|\d+(?:\.\d+)?|\([^)]+\)))*?)\s*\/\s*(-?\s*(?:\$[\w-]+|\d+(?:\.\d+)?|\([^)]+\)))(?![\/\w])/' => 'divide($1, $2)',
]);
// Check for deprecated classes that need manual removal.
$deprecated_classes = ['text-hide', 'rounded-sm', 'rounded-lg'];
$warnings = [];
foreach ($deprecated_classes as $class) {
$escaped_class = '\.' . $class;
// Check SASS files.
$matching_sass_files = ThemeTools::regexpExistFiles($theme_path, '*.scss', $escaped_class);
if (!empty($matching_sass_files)) {
$warnings[] = "SASS: Found deprecated class '.$class' - manual removal required";
}
// Check templates too.
$template_files = glob($theme_path . '/**/*.twig');
foreach ($template_files as $file) {
$content = file_get_contents($file);
if (preg_match('/([\'"]\s*[^\'"]*)' . preg_quote($class, '/') . '([\s\'"])/i', $content)) {
$relative_file = str_replace(DRUPAL_ROOT . '/', '', $file);
$warnings[] = "TEMPLATE: Found deprecated class '$class' in $relative_file - manual removal required";
break;
}
}
}
// Check for removed form variables that still need manual attention.
$removed_vars = ['$custom-control-', '$custom-select-', '$custom-file-'];
foreach ($removed_vars as $var) {
$escaped_var = str_replace('$', '\$', $var);
// Check SASS files.
$matching_sass_files = ThemeTools::regexpExistFiles($theme_path, '*.scss', $escaped_var);
if (!empty($matching_sass_files)) {
$warnings[] = "SASS: Found removed Bootstrap 4 variable starting with: $var - manual migration required";
}
}
if (!empty($warnings)) {
Drush::logger()->warning("$target_theme_name contains deprecated classes/variables requiring manual attention:");
foreach ($warnings as $warning) {
Drush::logger()->warning($warning);
}
Drush::logger()->warning('See @link for guidance on manual migration.', ['@link' => 'https://www.drupal.org/docs/8/themes/bs-base/misc/upgrading-from-1x-to-20x-version']);
}
}
