Less.php 7.18 KB
<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Magento\Deploy\Package\Processor\PreProcessor;

use Magento\Deploy\Console\DeployStaticOptions;
use Magento\Deploy\Package\Package;
use Magento\Deploy\Package\PackageFile;
use Magento\Deploy\Package\Processor\ProcessorInterface;
use Magento\Deploy\Service\DeployStaticFile;
use Magento\Framework\View\Asset\PreProcessor\FileNameResolver;
use Magento\Framework\View\Asset\Minification;
use Magento\Framework\Css\PreProcessor\Instruction\Import;
use Magento\Framework\View\Asset\NotationResolver;
use Magento\Framework\View\Asset\Repository;

/**
 * Pre-processor for speeding up deployment of Less files
 *
 * LESS-to-CSS compilation should happen only when it is really needed. If in some package there is no LESS files
 * overridden which used for generating some CSS file, then such CSS file can be copied from ancestor package as is
 */
class Less implements ProcessorInterface
{
    /**
     * Resolver allows to distinguish "root" LESS files from "imported" LESS files
     *
     * @var FileNameResolver
     */
    private $fileNameResolver;

    /**
     * @var \Magento\Framework\View\Asset\NotationResolver\Module
     */
    private $notationResolver;

    /**
     * @var DeployStaticFile
     */
    private $deployStaticFile;

    /**
     * @var Minification
     */
    private $minification;

    /**
     * Deployment procedure options
     *
     * @var array
     */
    private $options = [];

    /**
     * @var array
     */
    private $map = [];

    /**
     * Less constructor
     *
     * @param FileNameResolver $fileNameResolver
     * @param NotationResolver\Module $notationResolver
     * @param DeployStaticFile $deployStaticFile
     * @param Minification $minification
     */
    public function __construct(
        FileNameResolver $fileNameResolver,
        NotationResolver\Module $notationResolver,
        DeployStaticFile $deployStaticFile,
        Minification $minification
    ) {
        $this->fileNameResolver = $fileNameResolver;
        $this->notationResolver = $notationResolver;
        $this->deployStaticFile = $deployStaticFile;
        $this->minification = $minification;
    }

    /**
     * @inheritdoc
     */
    public function process(Package $package, array $options)
    {
        $this->options = $options;
        if ($this->options[DeployStaticOptions::NO_CSS] === true) {
            return false;
        }
        if ($package->getArea() !== Package::BASE_AREA && $package->getTheme() !== Package::BASE_THEME) {
            $files = $package->getParentFiles('less');
            foreach ($files as $file) {
                $packageFile = $package->getFile($file->getFileId());
                if ($packageFile && $packageFile->getOrigPackage() === $package) {
                    continue;
                }
                $deployFileName = $this->fileNameResolver->resolve($file->getFileName());
                if ($deployFileName !== $file->getFileName()) {
                    if ($this->hasOverrides($file, $package)) {
                        $file = clone $file;
                        $file->setArea($package->getArea());
                        $file->setTheme($package->getTheme());
                        $file->setLocale($package->getLocale());

                        $file->setPackage($package);
                        $package->addFileToMap($file);
                    }
                }
            }
        }
        return true;
    }

    /**
     * Checks if there are LESS files in current package which used for generating given CSS file from parent package
     *
     * If true then such CSS file must be re-compiled for current package to use overridden LESS files
     *
     * @param PackageFile $parentFile
     * @param Package $package
     * @return bool
     */
    private function hasOverrides(PackageFile $parentFile, Package $package)
    {
        $map = $this->buildMap(
            $parentFile->getFilePath(),
            $parentFile->getPackage()->getPath(),
            $parentFile->getExtension()
        );
        /** @var PackageFile[] $currentPackageFiles */
        $currentPackageFiles = array_merge($package->getFilesByType('less'), $package->getFilesByType('css'));

        foreach ($currentPackageFiles as $file) {
            if ($this->inParentFiles($file->getDeployedFileName(), $parentFile->getFileName(), $map)) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param string  $fileName
     * @param string  $parentFile
     * @param array $map
     * @return bool
     */
    private function inParentFiles($fileName, $parentFile, $map)
    {
        if (isset($map[$parentFile])) {
            if (in_array($fileName, $map[$parentFile])) {
                return true;
            } else {
                foreach ($map[$parentFile] as $pFile) {
                    return $this->inParentFiles($fileName, $pFile, $map);
                }
            }
        }
        return false;
    }

    /**
     * Build map of imported files
     *
     * @param string $filePath
     * @param string $packagePath
     * @param string $contentType
     * @return array
     */
    private function buildMap($filePath, $packagePath, $contentType)
    {
        $content = $this->deployStaticFile->readTmpFile($filePath, $packagePath);
        $replaceCallback = function ($matchedContent) use ($filePath, $packagePath, $contentType) {
            $matchedFileId = $matchedContent['path'];
            if (!pathinfo($matchedContent['path'], PATHINFO_EXTENSION)) {
                $matchedFileId .= '.' . $contentType;
            }
            if (strpos($matchedFileId, Repository::FILE_ID_SEPARATOR) !== false) {
                $basePath = $packagePath;
            } else {
                $basePath = pathinfo($filePath, PATHINFO_DIRNAME);
            }
            $resolvedPath = str_replace(Repository::FILE_ID_SEPARATOR, '/', $matchedFileId);
            if (strpos($resolvedPath, '@{baseUrl}') === 0) {
                $resolvedMapPath = str_replace('@{baseUrl}', '', $resolvedPath);
            } else {
                $resolvedMapPath = $this->normalizePath($basePath . '/' . $resolvedPath);
            }
            if (!isset($this->map[$filePath])) {
                $this->map[$filePath] = [];
            }
            $this->map[$filePath][] = $resolvedMapPath;
            $this->buildMap($resolvedMapPath, $packagePath, $contentType);
        };
        if ($content) {
            preg_replace_callback(Import::REPLACE_PATTERN, $replaceCallback, $content);
        }
        return $this->map;
    }

    /**
     * Return normalized path
     *
     * @param string $path
     * @return string
     */
    private function normalizePath($path)
    {
        if (strpos($path, '/../') === false) {
            return $path;
        }
        $pathParts = explode('/', $path);
        $realPath = [];
        foreach ($pathParts as $pathPart) {
            if ($pathPart == '.') {
                continue;
            }
            if ($pathPart == '..') {
                array_pop($realPath);
                continue;
            }
            $realPath[] = $pathPart;
        }
        return implode('/', $realPath);
    }
}