Current File : //var/www/vinorea/modules/psxdesign/src/Compiler/ThemeStylesheetCompiler.php |
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to https://devdocs.prestashop.com/ for more information.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
declare(strict_types=1);
namespace PrestaShop\Module\PsxDesign\Compiler;
if (!defined('_PS_VERSION_')) {
exit;
}
use InvalidArgumentException;
use PrestaShop\Module\PsxDesign\Config\PsxDesignConfig;
use PrestaShop\Module\PsxDesign\Exception\PsxDesignCompilerException;
use PrestaShop\Module\PsxDesign\Provider\ThemeConfiguration\ThemeConfigurationProvider;
use PrestaShop\Module\PsxDesign\Utility\DirectoryUtility;
use PrestaShop\Module\PsxDesign\Vendor\ScssPhp\ScssPhp\CompilationResult;
use PrestaShop\Module\PsxDesign\Vendor\ScssPhp\ScssPhp\Compiler;
use PrestaShop\Module\PsxDesign\Vendor\ScssPhp\ScssPhp\Exception\CompilerException;
use PrestaShop\Module\PsxDesign\Vendor\ScssPhp\ScssPhp\OutputStyle;
use RuntimeException;
use Shop;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
use Tools;
class ThemeStylesheetCompiler
{
private const MANIFEST_FILE_NAME = 'manifest.json';
/**
* @var string
*/
private $modulePath;
/**
* @var string
*/
private $themesPath;
/**
* @var Filesystem
*/
private $fileSystem;
/**
* @var ThemeConfigurationProvider
*/
private $configurationProvider;
/**
* @var string
*/
private $themeName;
public function __construct(
string $modulePath,
string $themesPath,
Filesystem $fileSystem,
ThemeConfigurationProvider $configurationProvider,
int $shopId
) {
$this->modulePath = $modulePath;
$this->themesPath = $themesPath;
$this->fileSystem = $fileSystem;
$this->configurationProvider = $configurationProvider;
$this->themeName = (new Shop($shopId))->theme_name;
}
/**
* Process the replacement of the theme stylesheet.
*
* @return void
*
* @throws PsxDesignCompilerException
* @throws CompilerException
*/
public function processThemeStylesheetReplacement(): void
{
// Compile SCSS, update asset paths, replace CSS, and clean up
$compiledScss = $this->compile();
$compiledScss = $this->updateAssetsPath($compiledScss);
$this->replaceStylesheet($compiledScss);
$this->deleteTmpFolder();
}
/**
* Compile theme SCSS files located in the module root directory,
* generate CSS, and replace the current theme's CSS file with the new one.
*
* @return CompilationResult Compiled CSS
*
* @throws PsxDesignCompilerException
* @throws CompilerException
*/
// TODO : geré le cas du RTL (a voir)
public function compile(): CompilationResult
{
try {
$originalThemePath = $this->themesPath . $this->themeName;
$tmpThemePath = $this->modulePath . PsxDesignConfig::TMP_DIR_NAME . DIRECTORY_SEPARATOR . $this->themeName;
try {
DirectoryUtility::copyDirectory($originalThemePath . DIRECTORY_SEPARATOR, $tmpThemePath);
} catch (RuntimeException|InvalidArgumentException $e) {
$this->deleteTmpFolder();
throw new PsxDesignCompilerException($e->getMessage(), PsxDesignCompilerException::FAILED_TO_OVERWRITE_VARIABLES_SCSS);
}
$scssBaseFolder = $this->configurationProvider->global->getScssBaseFolder();
if (is_dir($tmpThemePath . DIRECTORY_SEPARATOR . 'node_modules')) {
DirectoryUtility::copyDirectory($tmpThemePath . '/node_modules', $tmpThemePath . $scssBaseFolder);
$this->manageImports($tmpThemePath . $scssBaseFolder, $tmpThemePath . $scssBaseFolder);
}
$content = '';
foreach ($this->configurationProvider->global->getScssFiles() as $file) {
$content .= Tools::file_get_contents($tmpThemePath . $scssBaseFolder . DIRECTORY_SEPARATOR . $file);
}
$compiler = new Compiler();
$compiler->setOutputStyle(OutputStyle::COMPRESSED);
$compiler->setImportPaths($tmpThemePath . $scssBaseFolder . DIRECTORY_SEPARATOR);
$result = $compiler->compileString($content);
} catch (CompilerException $e) {
$this->deleteTmpFolder();
throw new PsxDesignCompilerException('Unable to compile scss files' . $e->getMessage(), PsxDesignCompilerException::FAILED_COMPILING);
}
return $result;
}
/**
* Replace the current theme's compiled stylesheet with the newly generated CSS.
*
* @param string $result Compiled CSS content
*
* @return void
*
* @throws PsxDesignCompilerException
*/
public function replaceStylesheet(string $result): void
{
try {
$oldStylesheetPath = PsxDesignConfig::getHashedStylesheetPathByFileName(PsxDesignConfig::COMPILED_THEME_FILE_NAME, $this->modulePath);
if ($oldStylesheetPath && $this->fileSystem->exists($oldStylesheetPath)) {
$this->fileSystem->remove($oldStylesheetPath);
}
$newStylesheetPath = $this->modulePath . PsxDesignConfig::CUSTOM_STYLESHEETS_PATH . PsxDesignConfig::generateStylesheetHashedPathByFileName(PsxDesignConfig::COMPILED_THEME_FILE_NAME, $result);
$this->fileSystem->dumpFile($newStylesheetPath, $result);
} catch (IOException $e) {
throw new PsxDesignCompilerException('Failed to overwrite theme compilated file.' . $e->getMessage(), PsxDesignCompilerException::FAILED_TO_OVERWRITE);
}
}
/**
* Recursively manage imports in SCSS files within the specified directory.
*
* @param string $currentDirectory
* @param string $baseDirectory
*
* @return void
*/
private function manageImports(string $currentDirectory, string $baseDirectory): void
{
$dir = opendir($currentDirectory);
while ($file = readdir($dir)) {
if (($file != '.') && ($file != '..')) {
if (is_dir($currentDirectory . DIRECTORY_SEPARATOR . $file)) {
$this->manageImports($currentDirectory . DIRECTORY_SEPARATOR . $file, $baseDirectory);
} else {
$contents = Tools::file_get_contents($currentDirectory . DIRECTORY_SEPARATOR . $file);
$contents = $this->manageCssImport($contents, $currentDirectory, $baseDirectory);
$contents = $this->manageNodeModulesImport($contents);
file_put_contents($currentDirectory . '/' . $file, $contents);
}
}
}
}
/**
* Manage Node.js module imports in CSS content.
*
* @param string $cssContent
*
* @return string
*/
private function manageNodeModulesImport(string $cssContent): string
{
return preg_replace('/(@import\s*["\']?)~/', '$1', $cssContent);
}
/**
* Manage CSS imports by resolving relative paths and replacing them with the corresponding content.
*
* @param string $cssContent
* @param string $currentDirectory
* @param string $baseDirectory
*
* @return string
*/
private function manageCssImport(string $cssContent, string $currentDirectory, string $baseDirectory): string
{
$cssImportPattern = '/@import\s+[\'"]([^\'"]+\.css)[\'"]/';
return preg_replace_callback($cssImportPattern, function ($matches) use ($currentDirectory, $baseDirectory) {
$importedFile = $matches[1];
$fullPath = $importedFile;
if (strpos($importedFile, '~') !== false) {
$fullPath = str_replace('~', '', $fullPath);
$fullPath = $baseDirectory . DIRECTORY_SEPARATOR . $fullPath;
} else {
$fullPath = $currentDirectory . DIRECTORY_SEPARATOR . $fullPath;
}
if (file_exists($fullPath)) {
return Tools::file_get_contents($fullPath);
}
return $matches[0];
}, $cssContent);
}
/**
* Update asset paths in CSS content using a manifest file.
*
* @param CompilationResult $compiledScss
*
* @return string
*/
private function updateAssetsPath(CompilationResult $compiledScss): string
{
$cssStylesheet = $compiledScss->getCss();
$manifestPath = $this->modulePath . PsxDesignConfig::TMP_DIR_NAME . DIRECTORY_SEPARATOR . $this->themeName . DIRECTORY_SEPARATOR . 'assets' . DIRECTORY_SEPARATOR . self::MANIFEST_FILE_NAME;
if (!file_exists($manifestPath)) {
return $cssStylesheet;
}
$manifestJson = Tools::file_get_contents($manifestPath);
$manifestData = json_decode($manifestJson, true);
$simpleManifestData = [];
foreach ($manifestData as $oldKey => $value) {
$keyExploded = explode('/', $oldKey);
$fileNameKey = end($keyExploded);
$simpleManifestData[$fileNameKey] = $value;
}
$relativeUrlPattern = '/url\(\s*[\'"]?([^\'"\s]+)[\'"]?\s*\)/';
return preg_replace_callback($relativeUrlPattern, function ($matches) use ($manifestData, $simpleManifestData) {
$relativePath = $matches[1];
$pathSegments = explode('/', $relativePath);
while (!empty($pathSegments)) {
$shortenedPath = implode('/', $pathSegments);
if (isset($manifestData[$shortenedPath])) {
return 'url("' . DIRECTORY_SEPARATOR . 'themes' . DIRECTORY_SEPARATOR . $this->themeName . DIRECTORY_SEPARATOR . 'assets' . DIRECTORY_SEPARATOR . str_replace('../', '', $manifestData[$shortenedPath]) . '")';
} elseif (isset($simpleManifestData[$shortenedPath])) {
return 'url("' . DIRECTORY_SEPARATOR . 'themes' . DIRECTORY_SEPARATOR . $this->themeName . DIRECTORY_SEPARATOR . 'assets' . DIRECTORY_SEPARATOR . str_replace('../', '', $simpleManifestData[$shortenedPath]) . '")';
}
array_shift($pathSegments);
}
return $matches[0];
}, $cssStylesheet);
}
/**
* Delete the temporary folder created during the compilation process.
*
* @return void
*
* @throws PsxDesignCompilerException
*/
private function deleteTmpFolder(): void
{
try {
DirectoryUtility::deleteDirectory($this->modulePath . PsxDesignConfig::TMP_DIR_NAME . DIRECTORY_SEPARATOR . $this->themeName);
} catch (RuntimeException|InvalidArgumentException $e) {
throw new PsxDesignCompilerException($e->getMessage(), PsxDesignCompilerException::FAILED_TO_OVERWRITE_VARIABLES_SCSS);
}
}
}