Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
feddb35
delete cache clearing command during generation flow
itaymesh Nov 4, 2022
14a3cfa
skip Magneto Critical CSS flag during CSS file generation flow
itaymesh Nov 4, 2022
ab37186
M2CRIT-1: Skip Magento native Critical CSS flow
d10nZ Nov 4, 2022
97ec6a6
restore providers
itaymesh Nov 6, 2022
b0ba43c
deprecate caching flushing during generate command
itaymesh Nov 6, 2022
43c3211
move asyncCssLoad to frontend/di.xml
itaymesh Nov 6, 2022
317fd36
add ingore-rule option to critical cli to ignore font-face
itaymesh Nov 6, 2022
b993057
M2CRIT-2: add random query string param
d10nZ Nov 7, 2022
0da31e4
add penthouse force include CSS selectors option
itaymesh Nov 7, 2022
dfd7805
Merge remote-tracking branch 'origin/feature/skip-critical-css-flag-o…
itaymesh Nov 7, 2022
44713fb
Merge pull request #1 from studioraz/feature/skip-critical-css-flag-o…
itaymesh Nov 7, 2022
0afa7b0
CLI command info output rephrasing
itaymesh Nov 8, 2022
90ba3f5
deprecate DefaultProvider
itaymesh Nov 8, 2022
8688a29
Merge remote-tracking branch 'origin/master'
itaymesh Nov 8, 2022
11edaa7
Merge pull request #2 from studioraz/feature/skip-critical-css-flag-o…
itaymesh Nov 8, 2022
2fa822a
M2CRIT-6: Skip Critical CSS Magento native flow
d10nZ Nov 8, 2022
7e8bb7a
M2CRIT-8: add store-id InputOption for cli command
d10nZ Nov 8, 2022
8673ecc
Merge branch 'feature/skip-critical-css-flag-on-generartion'
d10nZ Nov 8, 2022
0b02dce
inject only generated css code
itaymesh Nov 9, 2022
2289c53
Update composer.json
itaymesh Nov 9, 2022
1f60846
Update composer.json
itaymesh Nov 9, 2022
3652bd0
Update composer.json
itaymesh Nov 9, 2022
fcab705
added php 8.x compatibility
MaxMage Jul 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
{
"name": "m2-boilerplate/module-critical-css",
"name": "studioraz/magento2-critical-css",
"description": "Magento 2 module to automatically generate critical css with the addyosmani/critical npm package",
"type": "magento2-module",
"prefer-stable": true,
"version": "2.1.0",
"keywords": [
],
"require": {
"magento/framework": "^102.0|^103.0",
"php": ">=7.1.0"
},
"type": "magento2-module",
"license": [
"MIT"
],
Expand Down
13 changes: 12 additions & 1 deletion src/Config/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Config
const CONFIG_PATH_USERNAME = 'dev/css/critical_css_username';
const CONFIG_PATH_PASSWORD = 'dev/css/critical_css_password';
const CONFIG_PATH_DIMENSIONS = 'dev/css/critical_css_dimensions';
const CONFIG_PATH_FORCE_INCLUDE_CSS_SELECTORS = 'dev/css/critical_css_force_include_css_selectors';

/**
* @var ScopeConfigInterface
Expand All @@ -38,6 +39,16 @@ public function isEnabled(): bool
return (bool) $this->scopeConfig->isSetFlag(self::CONFIG_PATH_ENABLED);
}

public function getForceIncludeCssSelectors(): array
{
$cssSelectors = $this->scopeConfig->getValue(self::CONFIG_PATH_FORCE_INCLUDE_CSS_SELECTORS);
$cssSelectors = explode(',', $cssSelectors);
$cssSelectors = array_map('trim', $cssSelectors);
$cssSelectors = array_filter($cssSelectors);

return $cssSelectors;
}

public function getDimensions(): array
{
$dimensions = $this->scopeConfig->getValue(self::CONFIG_PATH_DIMENSIONS);
Expand Down Expand Up @@ -78,4 +89,4 @@ public function getCriticalBinary(): string
{
return $this->scopeConfig->getValue(self::CONFIG_PATH_CRITICAL_BINARY);
}
}
}
69 changes: 62 additions & 7 deletions src/Console/Command/GenerateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@
use M2Boilerplate\CriticalCss\Service\CriticalCss;
use M2Boilerplate\CriticalCss\Service\ProcessManager;
use M2Boilerplate\CriticalCss\Service\ProcessManagerFactory;
use Magento\Framework\App\Cache\Manager;
use Magento\Framework\App\Config\Storage\WriterInterface;
use Magento\Framework\ObjectManagerInterface;
use Magento\Framework\App\State;
use Magento\Framework\FlagManager;
use Magento\Framework\ObjectManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Magento\Framework\FlagManager;
use Magento\Framework\App\Cache\Manager;

class GenerateCommand extends Command
{
public const INPUT_OPTION_KEY_STORE_IDS = 'store-id';

/**
* @var ProcessManagerFactory
*/
Expand Down Expand Up @@ -88,6 +91,7 @@ public function __construct(
protected function configure()
{
$this->setName('m2bp:critical-css:generate');
$this->getDefinition()->addOptions($this->getOptionsList());
parent::configure();
}

Expand All @@ -96,25 +100,76 @@ protected function execute(InputInterface $input, OutputInterface $output)
try {
$this->state->setAreaCode(\Magento\Framework\App\Area::AREA_ADMINHTML);

$this->cacheManager->flush($this->cacheManager->getAvailableTypes());
// TODO: decide whether cache flushing is really required. temporally commented.
//$this->cacheManager->flush($this->cacheManager->getAvailableTypes());
Comment on lines +103 to +104
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure we only need to flush the full page cache. It would be much better


$this->criticalCssService->test($this->config->getCriticalBinary());

$consoleHandler = $this->consoleHandlerFactory->create(['output' => $output]);

$logger = $this->objectManager->create('M2Boilerplate\CriticalCss\Logger\Console', ['handlers' => ['console' => $consoleHandler]]);
$output->writeln('<info>Generating Critical CSS</info>');

/** @var ProcessManager $processManager */
$processManager = $this->processManagerFactory->create(['logger' => $logger]);


$output->writeln('<info>\'Use CSS critical path\' config is ' . ($this->config->isEnabled() ? 'Enabled' : 'Disabled') . '</info>');
$output->writeln("<info>-----------------------------------------</info>");
$output->writeln('<info>Critical Command Configured Options</info>');
$output->writeln("<info>-----------------------------------------</info>");
$output->writeln('<comment>Screen Dimensions: ' . implode('', $this->config->getDimensions()) . '</comment>');
$output->writeln('<comment>Force Include Css Selectors: ' . implode('', $this->config->getForceIncludeCssSelectors()) . '</comment>');

$output->writeln('<comment>HTTP Auth Username: ' . $this->config->getUsername() . '</comment>');
$output->writeln('<comment>HTTP Auth Password: ' . $this->config->getPassword() . '</comment>');

$output->writeln("<info>-----------------------------------------</info>");
$output->writeln('<info>Gathering URLs...</info>');
$processes = $processManager->createProcesses();
$output->writeln("<info>-----------------------------------------</info>");

$processes = $processManager->createProcesses(
$this->getStoreIds($input) ?: null
);

$output->writeln("<info>-----------------------------------------</info>");
$output->writeln('<info>Generating Critical CSS for ' . count($processes) . ' URLs...</info>');
$output->writeln("<info>-----------------------------------------</info>");
$processManager->executeProcesses($processes, true);

$this->cacheManager->flush($this->cacheManager->getAvailableTypes());
// TODO: decide whether cache flushing is really required. temporally commented.
// $this->cacheManager->flush($this->cacheManager->getAvailableTypes());

} catch (\Throwable $e) {
throw $e;
}
return 0;
}

/**
* Returns list of options and arguments for the command
*
* @return mixed
*/
public function getOptionsList()
{
return [
new InputOption(
self::INPUT_OPTION_KEY_STORE_IDS,
null,
InputOption::VALUE_REQUIRED,
'Coma-separated list of Magento Store IDs or single value to process specific Store.'
),
];
}

/**
* @param InputInterface $input
* @return int[]
*/
private function getStoreIds(InputInterface $input): array
{
$ids = $input->getOption(self::INPUT_OPTION_KEY_STORE_IDS) ?: '';
$ids = explode(',', $ids);
return array_map('intval', array_filter($ids));
}
}
125 changes: 125 additions & 0 deletions src/Plugin/AsyncCssPlugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

namespace M2Boilerplate\CriticalCss\Plugin;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\ResponseInterface;
use Magento\Framework\HTTP\Header;
use Magento\Framework\View\Result\Layout;
use Magento\Store\Model\ScopeInterface;

class AsyncCssPlugin extends \Magento\Theme\Controller\Result\AsyncCssPlugin
{
private ScopeConfigInterface $scopeConfig;
private Header $httpHeader;

/**
* @param ScopeConfigInterface $scopeConfig
* @param Header $httpHeader
*/
public function __construct(
ScopeConfigInterface $scopeConfig,
Header $httpHeader
) {
parent::__construct($scopeConfig);
$this->scopeConfig = $scopeConfig;
$this->httpHeader = $httpHeader;
}

/**
* @inheritDoc
*/
public function afterRenderResult(Layout $subject, Layout $result, ResponseInterface $httpResponse)
{
if ($this->canBeProcessed($httpResponse)) {
return parent::afterRenderResult($subject, $result, $httpResponse);
}

return $result;
}

/**
* @return bool
*/
private function canBeProcessed(ResponseInterface $httpResponse): bool
{
// NOTE: validate Critical Css activity Flag
if (!$this->isCssCriticalEnabled()) {
return false;
}

// NOTE: validate Request, it MUST NOT be initiated by NPM CRITICAL-CSS
// check on user-agent value
if ($this->httpHeader->getHttpUserAgent() === 'got (https://github.com/sindresorhus/got)') {
return false;
}

// NOTE: validate if CriticalCss node includes not-empty content
$content = (string)$httpResponse->getContent();
if ($this->isCriticalCssNodeEmpty($content)) {
return false;
}

return true;
}

/**
* NOTE:
* @see \Magento\Theme\Controller\Result\AsyncCssPlugin::isCssCriticalEnabled
*
* Returns information whether css critical path is enabled
*
* @return bool
*/
private function isCssCriticalEnabled(): bool
{
return $this->scopeConfig->isSetFlag(
'dev/css/use_css_critical_path',
ScopeInterface::SCOPE_STORE
);
}

/**
* Validates if STYLE CriticalCss-node exists and is NOT empty
*
* @param string $content
* @return bool
*/
private function isCriticalCssNodeEmpty(string $content): bool
{
$styles = '';
$styleOpen = '<style';
$styleClose = '/style>';
$styleOpenPos = strpos($content, $styleOpen);

$headClosePos = strpos($content, '</head>');

while ($styleOpenPos !== false) {
// NOTE: no need to proceed in case lines on HEAD-node have been processed
if ($styleOpenPos >= $headClosePos) {
break;
}

$styleClosePos = strpos($content, $styleClose, $styleOpenPos);
$style = substr($content, $styleOpenPos, $styleClosePos - $styleOpenPos + strlen($styleClose));

// NOTE: validation of STYLE-node string (tags exactly and node's inner content).
// in case match - fetch the styles and filter the string
if (preg_match('@<style.+data-type=["\']criticalCss["\'](.+)?>(.+)</style>@s', $style, $matches)) {
$styles = str_replace(
["\n","\r\n","\r"],
'',
trim((string)($matches[2] ?? null)));
break;
}

// NOTE: remove processed style-node from HTML.
$content = str_replace($style, '', $content);

// NOTE: style-node was cut out, search for the next one at its former position.
$styleOpenPos = strpos($content, $styleOpen, $styleOpenPos);
}

return empty($styles);
}
}
15 changes: 12 additions & 3 deletions src/Plugin/CriticalCss.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,18 @@ public function __construct(
$this->storeManager = $storeManager;
}

/**
* @param \Magento\Theme\Block\Html\Header\CriticalCss $subject
* @param $result generated CSS code to be inline injected to page head
* @return string|null
* @throws \Magento\Framework\Exception\FileSystemException
*/
public function afterGetCriticalCssData(\Magento\Theme\Block\Html\Header\CriticalCss $subject, $result)
{
$result = '';

$providers = $this->container->getProviders();

try {
$store = $this->storeManager->getStore();
} catch (NoSuchEntityException $e) {
Expand All @@ -77,9 +86,9 @@ public function afterGetCriticalCssData(\Magento\Theme\Block\Html\Header\Critica
foreach ($providers as $provider) {
if ($identifier = $provider->getCssIdentifierForRequest($this->request, $this->layout)) {
$identifier = $this->identifier->generateIdentifier($provider, $store, $identifier);
$css = $this->storage->getCriticalCss($identifier);
if ($css) {
return $css;
$result = $this->storage->getCriticalCss($identifier);
if ($result) {
break;
}
}
}
Expand Down
15 changes: 13 additions & 2 deletions src/Service/CriticalCss.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public function __construct(ProcessFactory $processFactory)
public function createCriticalCssProcess(
string $url,
array $dimensions,
array $forceIncludeCssSelectors,
string $criticalBinary = 'critical',
?string $username = null,
?string $password = null
Expand All @@ -30,6 +31,12 @@ public function createCriticalCssProcess(
$criticalBinary,
$url
];

foreach ($forceIncludeCssSelectors as $selector) {
$command[] = '--penthouse-forceInclude';
$command[] = $selector;
}

foreach ($dimensions as $dimension) {
$command[] = '--dimensions';
$command[] = $dimension;
Expand All @@ -44,8 +51,12 @@ public function createCriticalCssProcess(

$command[] = '--strict';
$command[] = '--no-request-https.rejectUnauthorized';
$command[] = '--ignore-rule';
$command[] = '[data-role=main-css-loader]';

$command[] = '--ignore-atrule';
$command[] = '@font-face';

//$command[] = '--penthouse-blockJSRequests';
//$command[] = 'true';

/** @var Process $process */
$process = $this->processFactory->create(['command' => $command, 'commandline' => $command]);
Expand Down
Loading