├── Block └── Adminhtml │ └── System │ ├── ExcludeControllers.php │ ├── ExcludePaths.php │ └── ExcludeUrlPattern.php ├── Helper └── Config.php ├── LICENSE ├── Model ├── Config │ └── Backend │ │ └── Serialized │ │ └── ArraySerialized.php ├── Minify │ ├── MinifyJs.php │ └── MinifyJsInterface.php ├── MoveJsToFooter.php ├── MoveJsToFooterInterface.php └── PassesValidator │ ├── EntityList.php │ ├── ValidateSkipper.php │ ├── ValidatorInterface.php │ └── Validators │ ├── SkipGoogleTagManager.php │ ├── SkipScriptByAttribute.php │ ├── SkipScriptsByController.php │ ├── SkipScriptsByPath.php │ └── SkipScriptsByURLPattern.php ├── Plugin └── MoveJsToFooter.php ├── README.md ├── Test └── Unit │ └── Model │ ├── MoveJsToFooterTest.php │ └── PassesValidator │ └── Validators │ ├── SkipGoogleTagManagerTest.php │ ├── SkipScriptByAttributeTest.php │ ├── SkipScriptsByControllerTest.php │ ├── SkipScriptsByPathTest.php │ └── SkipScriptsByURLPatternTest.php ├── composer.json ├── etc ├── acl.xml ├── adminhtml │ └── system.xml ├── config.xml ├── di.xml ├── frontend │ └── di.xml └── module.xml └── registration.php /Block/Adminhtml/System/ExcludeControllers.php: -------------------------------------------------------------------------------- 1 | 5 | * @github: 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace Hryvinskyi\DeferJs\Block\Adminhtml\System; 11 | 12 | use Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray; 13 | 14 | /** 15 | * Class ExcludeControllers 16 | */ 17 | class ExcludeControllers extends AbstractFieldArray 18 | { 19 | /** 20 | * @return void 21 | */ 22 | protected function _construct() 23 | { 24 | $this->addColumn('controller', [ 25 | 'label' => __('Expression'), 26 | ]); 27 | 28 | $this->_addAfter = false; 29 | $this->_addButtonLabel = __('Add'); 30 | 31 | parent::_construct(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Block/Adminhtml/System/ExcludePaths.php: -------------------------------------------------------------------------------- 1 | 5 | * @github: 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace Hryvinskyi\DeferJs\Block\Adminhtml\System; 11 | 12 | use Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray; 13 | 14 | /** 15 | * Class ExcludeControllers 16 | */ 17 | class ExcludePaths extends AbstractFieldArray 18 | { 19 | /** 20 | * @return void 21 | */ 22 | protected function _construct() 23 | { 24 | $this->addColumn('path', [ 25 | 'label' => __('Expression'), 26 | ]); 27 | 28 | $this->_addAfter = false; 29 | $this->_addButtonLabel = __('Add'); 30 | 31 | parent::_construct(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Block/Adminhtml/System/ExcludeUrlPattern.php: -------------------------------------------------------------------------------- 1 | 5 | * @github: 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace Hryvinskyi\DeferJs\Block\Adminhtml\System; 11 | 12 | use Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray; 13 | 14 | /** 15 | * Class ExcludeUrlPattern 16 | */ 17 | class ExcludeUrlPattern extends AbstractFieldArray 18 | { 19 | /** 20 | * @return void 21 | */ 22 | protected function _construct() 23 | { 24 | $this->addColumn('pattern', [ 25 | 'label' => __('Expression') 26 | ]); 27 | 28 | $this->_addAfter = false; 29 | $this->_addButtonLabel = __('Add'); 30 | 31 | parent::_construct(); 32 | } 33 | } -------------------------------------------------------------------------------- /Helper/Config.php: -------------------------------------------------------------------------------- 1 | 5 | * @github: 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace Hryvinskyi\DeferJs\Helper; 11 | 12 | use Magento\Framework\App\Helper\AbstractHelper; 13 | use Magento\Store\Model\ScopeInterface; 14 | 15 | /** 16 | * Class Config 17 | */ 18 | class Config extends AbstractHelper 19 | { 20 | /** 21 | * Configuration paths 22 | */ 23 | const XML_HRYVINSKYI_DEFER_JS_GENERAL_ENABLED = 'hryvinskyi_defer_js/general/enabled'; 24 | const XML_HRYVINSKYI_DEFER_JS_DISABLE_ATTRIBUTE = 'hryvinskyi_defer_js/general/disable_attribute'; 25 | const XML_HRYVINSKYI_DEFER_JS_MINIFY_BODY_SCRIPTS = 'hryvinskyi_defer_js/general/minify_body_scripts'; 26 | const XML_HRYVINSKYI_DEFER_JS_OPTIMIZE_X_MAGENTO_INIT_SCRIPTS 27 | = 'hryvinskyi_defer_js/general/optimize_x_magento_init_scripts'; 28 | const XML_HRYVINSKYI_DEFER_JS_EXCLUDE_CONTROLLERS = 'hryvinskyi_defer_js/general/exclude_controllers'; 29 | const XML_HRYVINSKYI_DEFER_JS_EXCLUDE_PATHS = 'hryvinskyi_defer_js/general/exclude_paths'; 30 | const XML_HRYVINSKYI_DEFER_JS_EXCLUDE_URL_PATTERN = 'hryvinskyi_defer_js/general/exclude_url_pattern'; 31 | 32 | /** 33 | * @param string $scopeType 34 | * @param null|string $scopeCode 35 | * 36 | * @return bool 37 | */ 38 | public function isEnabled( 39 | string $scopeType = ScopeInterface::SCOPE_STORE, 40 | $scopeCode = null 41 | ): bool { 42 | return $this->scopeConfig->isSetFlag( 43 | self::XML_HRYVINSKYI_DEFER_JS_GENERAL_ENABLED, 44 | $scopeType, 45 | $scopeCode 46 | ); 47 | } 48 | 49 | /** 50 | * @param string $scopeType 51 | * @param null|string $scopeCode 52 | * 53 | * @return string 54 | */ 55 | public function getDisableAttribute( 56 | string $scopeType = ScopeInterface::SCOPE_STORE, 57 | $scopeCode = null 58 | ): string { 59 | return (string)$this->scopeConfig->getValue( 60 | self::XML_HRYVINSKYI_DEFER_JS_DISABLE_ATTRIBUTE, 61 | $scopeType, 62 | $scopeCode 63 | ); 64 | } 65 | 66 | /** 67 | * @param string $scopeType 68 | * @param null|string $scopeCode 69 | * 70 | * @return bool 71 | */ 72 | public function isMinifyBodyScript( 73 | string $scopeType = ScopeInterface::SCOPE_STORE, 74 | $scopeCode = null 75 | ): bool { 76 | return $this->scopeConfig->isSetFlag( 77 | self::XML_HRYVINSKYI_DEFER_JS_MINIFY_BODY_SCRIPTS, 78 | $scopeType, 79 | $scopeCode 80 | ); 81 | } 82 | 83 | /** 84 | * @param string $scopeType 85 | * @param null|string $scopeCode 86 | * 87 | * @return bool 88 | */ 89 | public function isOptimizeXMagentoInitScripts( 90 | string $scopeType = ScopeInterface::SCOPE_STORE, 91 | $scopeCode = null 92 | ): bool { 93 | return $this->scopeConfig->isSetFlag( 94 | self::XML_HRYVINSKYI_DEFER_JS_OPTIMIZE_X_MAGENTO_INIT_SCRIPTS, 95 | $scopeType, 96 | $scopeCode 97 | ); 98 | } 99 | 100 | /** 101 | * Return Excluded Controllers 102 | * 103 | * @param string $scopeType 104 | * @param null|string $scopeCode 105 | * 106 | * @return string 107 | */ 108 | public function getExcludeControllers( 109 | string $scopeType = ScopeInterface::SCOPE_STORE, 110 | $scopeCode = null 111 | ): string { 112 | return (string)$this->scopeConfig->getValue( 113 | self::XML_HRYVINSKYI_DEFER_JS_EXCLUDE_CONTROLLERS, 114 | $scopeType, 115 | $scopeCode 116 | ); 117 | } 118 | 119 | /** 120 | * Return Excluded Paths 121 | * 122 | * @param string $scopeType 123 | * @param null|string $scopeCode 124 | * 125 | * @return string 126 | */ 127 | public function getExcludePaths( 128 | string $scopeType = ScopeInterface::SCOPE_STORE, 129 | $scopeCode = null 130 | ): string { 131 | return (string)$this->scopeConfig->getValue( 132 | self::XML_HRYVINSKYI_DEFER_JS_EXCLUDE_PATHS, 133 | $scopeType, 134 | $scopeCode 135 | ); 136 | } 137 | 138 | /** 139 | * Return Excluded URL pattern 140 | * 141 | * @param string $scopeType 142 | * @param null|string $scopeCode 143 | * 144 | * @return string 145 | */ 146 | public function getExcludeUrlPattern( 147 | string $scopeType = ScopeInterface::SCOPE_STORE, 148 | $scopeCode = null 149 | ): string { 150 | return (string)$this->scopeConfig->getValue( 151 | self::XML_HRYVINSKYI_DEFER_JS_EXCLUDE_URL_PATTERN, 152 | $scopeType, 153 | $scopeCode 154 | ); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Model/Config/Backend/Serialized/ArraySerialized.php: -------------------------------------------------------------------------------- 1 | 5 | * @github: 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace Hryvinskyi\DeferJs\Model\Config\Backend\Serialized; 11 | 12 | use Magento\Config\Model\Config\Backend\Serialized\ArraySerialized as BaseArraySerialized; 13 | 14 | /** 15 | * Class ArraySerialized 16 | */ 17 | class ArraySerialized extends BaseArraySerialized 18 | { 19 | 20 | } 21 | -------------------------------------------------------------------------------- /Model/Minify/MinifyJs.php: -------------------------------------------------------------------------------- 1 | 5 | * @github: 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace Hryvinskyi\DeferJs\Model\Minify; 11 | 12 | use JShrink\Minifier; 13 | 14 | /** 15 | * Class MinifyJs 16 | */ 17 | class MinifyJs implements MinifyJsInterface 18 | { 19 | /** 20 | * @param array $scripts 21 | * 22 | * @return array 23 | * @throws \Exception 24 | */ 25 | public function execute(array $scripts): array 26 | { 27 | foreach ($scripts as &$script) { 28 | try { 29 | $script = Minifier::minify($script); 30 | } catch (\Exception $exception) { 31 | // Remove comments 32 | $script = preg_replace("/[^:']\/\/.*/", '', $script); 33 | } 34 | 35 | $script = str_replace('src=', ' src=', $script); 36 | $script = str_replace('type=', ' type=', $script); 37 | $script = preg_replace('!\s+!', ' ', $script); 38 | } 39 | 40 | return $scripts; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Model/Minify/MinifyJsInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * @github: 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace Hryvinskyi\DeferJs\Model\Minify; 11 | 12 | /** 13 | * Class MinifyJsInterface 14 | */ 15 | interface MinifyJsInterface 16 | { 17 | /** 18 | * @param array $scripts 19 | * 20 | * @return array 21 | */ 22 | public function execute(array $scripts): array; 23 | } 24 | -------------------------------------------------------------------------------- /Model/MoveJsToFooter.php: -------------------------------------------------------------------------------- 1 | 5 | * GitHub: https://github.com/hryvinskyi 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace Hryvinskyi\DeferJs\Model; 11 | 12 | use Hryvinskyi\Base\Helper\ArrayHelper; 13 | use Hryvinskyi\Base\Helper\Json; 14 | use Hryvinskyi\DeferJs\Helper\Config; 15 | use Hryvinskyi\DeferJs\Model\Minify\MinifyJsInterface; 16 | use Hryvinskyi\DeferJs\Model\PassesValidator\ValidateSkipper; 17 | use Magento\Framework\App\Request\Http as RequestHttp; 18 | use Magento\Framework\App\RequestInterface; 19 | use Magento\Framework\App\Response\Http; 20 | 21 | class MoveJsToFooter implements MoveJsToFooterInterface 22 | { 23 | private Config $config; 24 | private MinifyJsInterface $minifyJs; 25 | private ValidateSkipper $validateSkipper; 26 | private RequestInterface $request; 27 | 28 | public function __construct( 29 | Config $config, 30 | MinifyJsInterface $minifyJs, 31 | ValidateSkipper $validateSkipper, 32 | RequestInterface $request 33 | ) { 34 | $this->config = $config; 35 | $this->minifyJs = $minifyJs; 36 | $this->validateSkipper = $validateSkipper; 37 | $this->request = $request; 38 | } 39 | 40 | /** 41 | * Executes the JS to footer movement process on the HTTP response 42 | * 43 | * @param Http $http The HTTP response object to process 44 | * @return void 45 | */ 46 | public function execute(Http $http): void 47 | { 48 | if (!$this->request instanceof RequestHttp || $this->request->isAjax()) { 49 | return; 50 | } 51 | 52 | $html = $http->getBody(); 53 | 54 | // Process all scripts in a single pass 55 | $processedHtml = $this->processScripts($html, $http); 56 | 57 | if ($processedHtml !== $html) { 58 | $http->setBody($processedHtml); 59 | } 60 | } 61 | 62 | /** 63 | * Processes script tags in the HTML to move them to footer 64 | * 65 | * @param string $html The original HTML content 66 | * @param Http $http The HTTP response object 67 | * @return string The processed HTML with scripts moved to footer 68 | */ 69 | private function processScripts(string $html, Http $http): string 70 | { 71 | // Find all script tags 72 | preg_match_all('//is', $html, $matches, PREG_OFFSET_CAPTURE); 73 | 74 | if (empty($matches[0])) { 75 | return $html; 76 | } 77 | 78 | // Prepare collections 79 | $scriptsToMove = []; 80 | $jsonScripts = []; 81 | $positions = []; 82 | 83 | // Process matches in reverse order to avoid offset issues 84 | foreach (array_reverse($matches[0]) as [$script, $offset]) { 85 | if ($this->shouldExtractJson($script)) { 86 | $jsonScripts[] = strip_tags($script); 87 | $positions[] = [$offset, strlen($script)]; 88 | continue; 89 | } 90 | 91 | if (!$this->validateSkipper->execute($script, $http)) { 92 | $scriptsToMove[] = $script; 93 | $positions[] = [$offset, strlen($script)]; 94 | } 95 | } 96 | 97 | // If nothing to move, return original 98 | if (empty($scriptsToMove) && empty($jsonScripts)) { 99 | return $html; 100 | } 101 | 102 | // Prepare footer content 103 | $footerContent = ''; 104 | 105 | // Add merged JSON scripts if any 106 | if (!empty($jsonScripts)) { 107 | $mergedJson = $this->mergeJsons($jsonScripts); 108 | if (!empty($mergedJson)) { 109 | $footerContent .= $mergedJson; 110 | } 111 | } 112 | 113 | // Add scripts if any 114 | if (!empty($scriptsToMove)) { 115 | if ($this->config->isMinifyBodyScript()) { 116 | $footerContent .= implode(PHP_EOL, $this->minifyJs->execute(array_reverse($scriptsToMove))); 117 | } else { 118 | $footerContent .= implode(PHP_EOL, array_reverse($scriptsToMove)); 119 | } 120 | } 121 | 122 | // Remove scripts from original positions 123 | foreach ($positions as [$pos, $len]) { 124 | $html = substr_replace($html, '', $pos, $len); 125 | } 126 | 127 | // Insert scripts before closing body tag 128 | return $this->insertBeforeBodyEnd($html, $footerContent); 129 | } 130 | 131 | /** 132 | * Determines if a script should be extracted as JSON 133 | * 134 | * @param string $script The script tag to check 135 | * @return bool True if the script should be extracted as JSON 136 | */ 137 | private function shouldExtractJson(string $script): bool 138 | { 139 | return $this->config->isOptimizeXMagentoInitScripts() && 140 | preg_match('/]*type=["\']text\/x-magento-init["\'][^>]*>/i', $script); 141 | } 142 | 143 | /** 144 | * Merges multiple JSON scripts into a single script 145 | * 146 | * @param array $jsons Array of JSON strings to merge 147 | * @return string The merged JSON in a single script tag or empty string 148 | */ 149 | private function mergeJsons(array $jsons): string 150 | { 151 | $merged = []; 152 | foreach ($jsons as $json) { 153 | $data = Json::decode($json); 154 | if (is_array($data)) { 155 | $merged = ArrayHelper::merge($merged, $data); 156 | } 157 | } 158 | 159 | return !empty($merged) 160 | ? '' 161 | : ''; 162 | } 163 | 164 | /** 165 | * Inserts content before the closing body tag 166 | * 167 | * @param string $html The HTML to modify 168 | * @param string $content The content to insert before the body end tag 169 | * @return string The modified HTML 170 | */ 171 | private function insertBeforeBodyEnd(string $html, string $content): string 172 | { 173 | $bodyEndPos = stripos($html, ''); 174 | return $bodyEndPos !== false 175 | ? substr_replace($html, $content, $bodyEndPos, 0) 176 | : $html . $content; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /Model/MoveJsToFooterInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * @github: 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace Hryvinskyi\DeferJs\Model; 11 | 12 | interface MoveJsToFooterInterface 13 | { 14 | /** 15 | * @param \Magento\Framework\App\Response\Http $http 16 | * 17 | * @return void 18 | */ 19 | public function execute(\Magento\Framework\App\Response\Http $http); 20 | } -------------------------------------------------------------------------------- /Model/PassesValidator/EntityList.php: -------------------------------------------------------------------------------- 1 | 5 | * @github: 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace Hryvinskyi\DeferJs\Model\PassesValidator; 11 | 12 | /** 13 | * Class EntityList 14 | */ 15 | class EntityList 16 | { 17 | /** 18 | * @var ValidatorInterface[] 19 | */ 20 | private $entityTypes = []; 21 | 22 | /** 23 | * EntityList constructor. 24 | * 25 | * @param ValidatorInterface[] $entityTypes 26 | */ 27 | public function __construct( 28 | $entityTypes = [] 29 | ) { 30 | $this->entityTypes = $entityTypes; 31 | } 32 | 33 | /** 34 | * Retrieve list of entities 35 | * 36 | * @return ValidatorInterface[] 37 | */ 38 | public function getList(): array 39 | { 40 | return $this->entityTypes; 41 | } 42 | 43 | /** 44 | * @param string $code 45 | * 46 | * @return ValidatorInterface 47 | */ 48 | public function getEntityByCode(string $code): ValidatorInterface 49 | { 50 | return $this->entityTypes[$code]; 51 | } 52 | } -------------------------------------------------------------------------------- /Model/PassesValidator/ValidateSkipper.php: -------------------------------------------------------------------------------- 1 | 5 | * @github: 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace Hryvinskyi\DeferJs\Model\PassesValidator; 11 | 12 | use Magento\Framework\App\Response\Http; 13 | 14 | /** 15 | * Class Validate 16 | */ 17 | class ValidateSkipper 18 | { 19 | /** 20 | * @var EntityList 21 | */ 22 | private $deferJsPassesValidators; 23 | 24 | /** 25 | * Validate constructor. 26 | * 27 | * @param EntityList $deferJsPassesValidators 28 | */ 29 | public function __construct( 30 | EntityList $deferJsPassesValidators 31 | ) { 32 | $this->deferJsPassesValidators = $deferJsPassesValidators; 33 | } 34 | 35 | /** 36 | * @param string $script 37 | * @param Http $http 38 | * 39 | * @return bool 40 | */ 41 | public function execute(string $script, Http $http): bool 42 | { 43 | foreach ($this->deferJsPassesValidators->getList() as $deferJsPassesValidator) { 44 | if ($deferJsPassesValidator->validate($script, $http)) { 45 | return true; 46 | } 47 | } 48 | 49 | return false; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Model/PassesValidator/ValidatorInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * @github: 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace Hryvinskyi\DeferJs\Model\PassesValidator; 11 | 12 | use Magento\Framework\App\Response\Http; 13 | 14 | interface ValidatorInterface 15 | { 16 | /** 17 | * Validator function, handle javascript or not 18 | * 19 | * @param string $script 20 | * @param Http $http 21 | * 22 | * @return bool 23 | */ 24 | public function validate(string $script, Http $http): bool; 25 | } -------------------------------------------------------------------------------- /Model/PassesValidator/Validators/SkipGoogleTagManager.php: -------------------------------------------------------------------------------- 1 | 5 | * @github: 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | 11 | namespace Hryvinskyi\DeferJs\Model\PassesValidator\Validators; 12 | 13 | use Hryvinskyi\DeferJs\Model\PassesValidator\ValidatorInterface; 14 | use Magento\Framework\App\Response\Http; 15 | 16 | /** 17 | * Class SkipGoogleTagManager 18 | */ 19 | class SkipGoogleTagManager implements ValidatorInterface 20 | { 21 | /** 22 | * Validator function, handle javascript or not 23 | * 24 | * @param string $script 25 | * @param Http $http 26 | * 27 | * @return bool 28 | */ 29 | public function validate(string $script, Http $http): bool 30 | { 31 | return !!preg_match("/.*?googletagmanager\.com.*?/s", $script); 32 | } 33 | } -------------------------------------------------------------------------------- /Model/PassesValidator/Validators/SkipScriptByAttribute.php: -------------------------------------------------------------------------------- 1 | 5 | * @github: 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace Hryvinskyi\DeferJs\Model\PassesValidator\Validators; 11 | 12 | use Hryvinskyi\DeferJs\Helper\Config; 13 | use Hryvinskyi\DeferJs\Model\PassesValidator\ValidatorInterface; 14 | use Magento\Framework\App\Response\Http; 15 | 16 | /** 17 | * Class SkipScriptByAttribute 18 | */ 19 | class SkipScriptByAttribute implements ValidatorInterface 20 | { 21 | /** 22 | * @var Config 23 | */ 24 | private $config; 25 | 26 | /** 27 | * SkipScriptByAttribute constructor. 28 | * 29 | * @param Config $config 30 | */ 31 | public function __construct(Config $config) 32 | { 33 | $this->config = $config; 34 | } 35 | 36 | /** 37 | * Validator function, handle javascript or not 38 | * 39 | * @param string $script 40 | * @param Http $http 41 | * 42 | * @return bool 43 | */ 44 | public function validate(string $script, Http $http): bool 45 | { 46 | $return = false; 47 | 48 | if (stripos($script, $this->config->getDisableAttribute()) !== false) { 49 | $return = true; 50 | } 51 | 52 | return $return; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Model/PassesValidator/Validators/SkipScriptsByController.php: -------------------------------------------------------------------------------- 1 | 5 | * @github: 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace Hryvinskyi\DeferJs\Model\PassesValidator\Validators; 11 | 12 | use Hryvinskyi\Base\Helper\ArrayHelper; 13 | use Hryvinskyi\Base\Helper\Json; 14 | use Hryvinskyi\DeferJs\Helper\Config; 15 | use Hryvinskyi\DeferJs\Model\PassesValidator\ValidatorInterface; 16 | use Magento\Framework\App\Request\Http as RequestHttp; 17 | use Magento\Framework\App\Response\Http; 18 | 19 | /** 20 | * Class SkipScriptsByController 21 | */ 22 | class SkipScriptsByController implements ValidatorInterface 23 | { 24 | /** 25 | * @var Config 26 | */ 27 | private $config; 28 | 29 | /** 30 | * @var RequestHttp 31 | */ 32 | private $request; 33 | 34 | /** 35 | * SkipScriptsByController constructor. 36 | * 37 | * @param Config $config 38 | * @param RequestHttp $request 39 | */ 40 | public function __construct( 41 | Config $config, 42 | RequestHttp $request 43 | ) { 44 | $this->config = $config; 45 | $this->request = $request; 46 | } 47 | 48 | /** 49 | * Validator function, handle javascript or not 50 | * 51 | * @param string $script 52 | * @param Http $http 53 | * 54 | * @return bool 55 | */ 56 | public function validate(string $script, Http $http): bool 57 | { 58 | $moduleName = $this->request->getModuleName(); 59 | $controller = $this->request->getControllerName(); 60 | $action = $this->request->getActionName(); 61 | 62 | $controllerFull = $moduleName . '_' . $controller . '_' . $action; 63 | $controllersConfig = Json::decode($this->config->getExcludeControllers()); 64 | $controllersConfig = ArrayHelper::getColumn($controllersConfig, 'controller', false); 65 | 66 | $return = false; 67 | 68 | if (in_array($controllerFull, $controllersConfig)) { 69 | $return = true; 70 | } 71 | 72 | return $return; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Model/PassesValidator/Validators/SkipScriptsByPath.php: -------------------------------------------------------------------------------- 1 | 5 | * @github: 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace Hryvinskyi\DeferJs\Model\PassesValidator\Validators; 11 | 12 | use Hryvinskyi\Base\Helper\ArrayHelper; 13 | use Hryvinskyi\Base\Helper\Json; 14 | use Hryvinskyi\DeferJs\Helper\Config; 15 | use Hryvinskyi\DeferJs\Model\PassesValidator\ValidatorInterface; 16 | use Magento\Framework\App\Request\Http as RequestHttp; 17 | use Magento\Framework\App\Response\Http; 18 | 19 | /** 20 | * Class SkipScriptsByPath 21 | */ 22 | class SkipScriptsByPath implements ValidatorInterface 23 | { 24 | /** 25 | * @var Config 26 | */ 27 | private $config; 28 | 29 | /** 30 | * @var RequestHttp 31 | */ 32 | private $request; 33 | 34 | /** 35 | * SkipScriptsByController constructor. 36 | * 37 | * @param Config $config 38 | * @param RequestHttp $request 39 | */ 40 | public function __construct( 41 | Config $config, 42 | RequestHttp $request 43 | ) { 44 | $this->config = $config; 45 | $this->request = $request; 46 | } 47 | 48 | /** 49 | * Validator function, handle javascript or not 50 | * 51 | * @param string $script 52 | * @param Http $http 53 | * 54 | * @return bool 55 | */ 56 | public function validate(string $script, Http $http): bool 57 | { 58 | $paths = Json::decode($this->config->getExcludePaths()); 59 | $paths = ArrayHelper::getColumn($paths, 'path', false); 60 | 61 | $return = false; 62 | 63 | if (in_array($this->request->getRequestUri(), $paths)) { 64 | $return = true; 65 | } 66 | 67 | return $return; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Model/PassesValidator/Validators/SkipScriptsByURLPattern.php: -------------------------------------------------------------------------------- 1 | 5 | * GitHub: https://github.com/hryvinskyi 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace Hryvinskyi\DeferJs\Model\PassesValidator\Validators; 11 | 12 | use Hryvinskyi\Base\Helper\ArrayHelper; 13 | use Hryvinskyi\Base\Helper\Json; 14 | use Hryvinskyi\DeferJs\Helper\Config; 15 | use Hryvinskyi\DeferJs\Model\PassesValidator\ValidatorInterface; 16 | use Magento\Framework\App\Request\Http as RequestHttp; 17 | use Magento\Framework\App\Response\Http; 18 | 19 | /** 20 | * Class SkipScriptsByURLPattern 21 | */ 22 | class SkipScriptsByURLPattern implements ValidatorInterface 23 | { 24 | /** 25 | * @var Config 26 | */ 27 | private $config; 28 | 29 | /** 30 | * @var RequestHttp 31 | */ 32 | private $request; 33 | 34 | /** 35 | * SkipScriptsByController constructor. 36 | * 37 | * @param Config $config 38 | * @param RequestHttp $request 39 | */ 40 | public function __construct( 41 | Config $config, 42 | RequestHttp $request 43 | ) { 44 | $this->config = $config; 45 | $this->request = $request; 46 | } 47 | 48 | /** 49 | * Validator function, handle javascript or not 50 | * 51 | * @param string $script 52 | * @param Http $http 53 | * 54 | * @return bool 55 | */ 56 | public function validate(string $script, Http $http): bool 57 | { 58 | $executeUrlPattern = Json::decode($this->config->getExcludeUrlPattern()); 59 | $executeUrlPattern = ArrayHelper::getColumn($executeUrlPattern, 'pattern', false); 60 | 61 | $return = false; 62 | 63 | foreach ($executeUrlPattern as $pattern) { 64 | if ($this->checkPattern($this->request->getRequestUri(), $pattern)) { 65 | $return = true; 66 | break; 67 | } 68 | } 69 | 70 | return $return; 71 | } 72 | 73 | /** 74 | * Check if a string matches a wildcard pattern 75 | * 76 | * @param string $string The string to check 77 | * @param string $pattern The wildcard pattern (using * as wildcard) 78 | * @return bool True if the string matches the pattern 79 | */ 80 | public function checkPattern(string $string, string $pattern): bool 81 | { 82 | // Convert wildcard pattern to regex 83 | $regex = preg_quote($pattern, '/'); 84 | $regex = str_replace('\*', '.*', $regex); 85 | $regex = '/^' . $regex . '$/'; 86 | 87 | return (bool)preg_match($regex, $string); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Plugin/MoveJsToFooter.php: -------------------------------------------------------------------------------- 1 | 5 | * @github: 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace Hryvinskyi\DeferJs\Plugin; 11 | 12 | use Closure; 13 | use Hryvinskyi\DeferJs\Helper\Config; 14 | use Hryvinskyi\DeferJs\Model\MoveJsToFooterInterface; 15 | use Magento\Framework\App\Request\Http as RequestHttp; 16 | use Magento\Framework\App\Response\Http; 17 | use Magento\Framework\Controller\ResultInterface; 18 | 19 | /** 20 | * Class MoveJsToFooter 21 | */ 22 | class MoveJsToFooter 23 | { 24 | /** 25 | * Configuration Module 26 | * 27 | * @var Config 28 | */ 29 | private $config; 30 | 31 | /** 32 | * Request HTTP 33 | * 34 | * @var RequestHttp 35 | */ 36 | private $request; 37 | 38 | /** 39 | * Mover Js 40 | * 41 | * @var MoveJsToFooterInterface 42 | */ 43 | private $moveJsToFooter; 44 | 45 | /** 46 | * MoveJsToFooter constructor. 47 | * 48 | * @param Config $config 49 | * @param RequestHttp $request 50 | * @param MoveJsToFooterInterface $moveJsToFooter 51 | */ 52 | public function __construct( 53 | Config $config, 54 | RequestHttp $request, 55 | MoveJsToFooterInterface $moveJsToFooter 56 | ) { 57 | $this->config = $config; 58 | $this->request = $request; 59 | $this->moveJsToFooter = $moveJsToFooter; 60 | } 61 | 62 | /** 63 | * @param ResultInterface $subject 64 | * @param Closure $proceed 65 | * @param Http $response 66 | * 67 | * @return string 68 | */ 69 | public function aroundRenderResult( 70 | ResultInterface $subject, 71 | Closure $proceed, 72 | Http $response 73 | ) { 74 | $result = $proceed($response); 75 | 76 | if (!$this->config->isEnabled() || PHP_SAPI === 'cli' || $this->request->isXmlHttpRequest()) { 77 | return $result; 78 | } 79 | 80 | $this->moveJsToFooter->execute($response); 81 | 82 | return $result; 83 | } 84 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Magento 2 Defer JavaScripts 2 | 3 | The module moves javascripts to the bottom of the page. 4 | 5 | [![Latest Stable Version](https://poser.pugx.org/hryvinskyi/magento2-deferjs/v/stable)](https://packagist.org/packages/hryvinskyi/magento2-deferjs) 6 | [![Total Downloads](https://poser.pugx.org/hryvinskyi/magento2-deferjs/downloads)](https://packagist.org/packages/hryvinskyi/magento2-deferjs) 7 | [![PayPal donate button](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=legionerblack%40yandex%2eru&lc=UA&item_name=Magento%202%20Defer%20Javascript¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted "Donate once-off to this project using Paypal") 8 | [![Latest Unstable Version](https://poser.pugx.org/hryvinskyi/magento2-deferjs/v/unstable)](https://packagist.org/packages/oakcms/oakcms) 9 | [![License](https://poser.pugx.org/hryvinskyi/magento2-deferjs/license)](https://packagist.org/packages/hryvinskyi/magento2-deferjs) 10 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/hryvinskyi/magento2-deferjs/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/hryvinskyi/magento2-deferjs/?branch=master) 11 | [![Build Status](https://scrutinizer-ci.com/g/hryvinskyi/magento2-deferjs/badges/build.png?b=master)](https://scrutinizer-ci.com/g/hryvinskyi/magento2-deferjs/build-status/master) 12 | 13 | 14 | # Installation Guide 15 | ### Install by composer 16 | ```` 17 | composer require hryvinskyi/magento2-deferjs 18 | bin/magento module:enable Hryvinskyi_Base 19 | bin/magento module:enable Hryvinskyi_DeferJs 20 | bin/magento setup:upgrade 21 | ```` 22 | ### Install download package 23 | 1. Download module https://github.com/hryvinskyi/magento2-base "Clone or download -> Download Zip" 24 | 2. Download this module "Clone or download -> Download Zip" 25 | 3. Unzip two modules in the folder app\code\Hryvinskyi\Base and app\code\Hryvinskyi\DeferJs 26 | 4. Run commands: 27 | 28 | ``` 29 | bin/magento module:enable Hryvinskyi_Base 30 | bin/magento module:enable Hryvinskyi_DeferJs 31 | bin/magento setup:upgrade 32 | ``` 33 | 5. Configure module in admin panel 34 | 35 | # General Settings 36 | To get the access to the 'Defer JavaScripts' settings please go to 37 | Stores -> Configuration -> Hryvinskyi Extensions -> Defer JavaScripts and expand the General Settings section. 38 | 39 | ***Enabled deferred javascript:*** Enable or disable the extension from here. 40 | ***Disable move attribute:*** Enter the attribute that will block the move of the script to the bottom. 41 | ***Enabled minify body scripts:*** Enable script minification. 42 | 43 | # Features 44 | 45 | - Ability to skip javascripts with a special tag that can be set in the admin panel 46 | - Built-in skipping move Google tag manager (If you use a third-party module and can not add a special tag) 47 | - Exclude by controllers from defer parsing of JavaScript. 48 | 49 | *Controller will be unaffected by defer js. Use like: [module]_[controller]_[action] (Example: cms_index_index)* 50 | 51 | - Exclude by store paths from defer parsing of JavaScript. 52 | 53 | *Paths will be unaffected by defer js. Use like: /women/tops-women/jackets-women.html* 54 | 55 | 56 | - Exclude by url pattern from defer parsing of JavaScript. 57 | 58 | *URL pattern can be a full action name or a request path. Wildcards are allowed. Like:* 59 | 60 | ``` 61 | *cell-phones* 62 | *cell-phones/nokia-2610-phone.html 63 | customer_account_* 64 | /customer/account/* 65 | *?mode=list 66 | ``` 67 | 68 | - Minification of moved javascripts 69 | - Optimize text/x-magento-init scripts 70 | - Ability to extend the module with your validator 71 | - Increased rendering time improves the Google Page Speed score. 72 | 73 | # Add custom validator 74 | To add your validator: 75 | 76 | 1. Create Validator file `app/code/Vendor/Module/Model/PassesValidator/Validators/YourValidator.php`: 77 | 78 | ```php 79 | 112 | 113 | 115 | 116 | 117 | 118 | 119 | Vendor\Module\Model\PassesValidator\Validators\YourValidator 121 | 122 | 123 | 124 | 125 | ``` 126 | 127 | 3. Run command: 128 | ``` 129 | php bin/magento setup:di:compile 130 | ``` 131 | -------------------------------------------------------------------------------- /Test/Unit/Model/MoveJsToFooterTest.php: -------------------------------------------------------------------------------- 1 | 5 | * @github: 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace Hryvinskyi\DeferJs\Test\Unit\Model; 11 | 12 | use Hryvinskyi\DeferJs\Helper\Config; 13 | use Hryvinskyi\DeferJs\Model\Minify\MinifyJsInterface; 14 | use Hryvinskyi\DeferJs\Model\MoveJsToFooter; 15 | use Hryvinskyi\DeferJs\Model\PassesValidator\ValidateSkipper; 16 | use Magento\Framework\App\Response\Http; 17 | use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; 18 | use PHPUnit\Framework\TestCase; 19 | 20 | class MoveJsToFooterTest extends TestCase 21 | { 22 | 23 | /** 24 | * @var Config|\PHPUnit_Framework_MockObject_MockObject 25 | */ 26 | private $config; 27 | 28 | /** 29 | * @var MinifyJsInterface|\PHPUnit_Framework_MockObject_MockObject 30 | */ 31 | private $minifyJs; 32 | 33 | /** 34 | * @var ValidateSkipper|\PHPUnit_Framework_MockObject_MockObject 35 | */ 36 | private $validateSkipper; 37 | 38 | /** 39 | * @var Http 40 | */ 41 | private $http; 42 | 43 | /** 44 | * @var MoveJsToFooter 45 | */ 46 | private $model; 47 | 48 | /** 49 | * Sets up the fixture 50 | * 51 | * @return void 52 | */ 53 | protected function setUp() 54 | { 55 | $this->config = $this->createPartialMock( 56 | Config::class, 57 | ['isMinifyBodyScript'] 58 | ); 59 | 60 | $this->minifyJs = $this->createPartialMock( 61 | MinifyJsInterface::class, 62 | ['execute'] 63 | ); 64 | 65 | $this->validateSkipper = $this->createPartialMock( 66 | ValidateSkipper::class, 67 | ['execute'] 68 | ); 69 | 70 | $this->http = (new ObjectManager($this))->getObject(Http::class); 71 | $this->model = (new ObjectManager($this))->getObject(MoveJsToFooter::class, [ 72 | 'config' => $this->config, 73 | 'minifyJs' => $this->minifyJs, 74 | 'validateSkipper' => $this->validateSkipper, 75 | ]); 76 | } 77 | 78 | public function testExecute(): void 79 | { 80 | $beforeBody = '' . 81 | '' . 82 | '' . 83 | '' . 84 | '' . 85 | '' . 86 | '

My First Heading

' . 87 | '

My first paragraph.

' . 88 | '' . 89 | ''; 90 | 91 | $afterBody = '' . 92 | '' . 93 | '' . 94 | '' . 95 | '

My First Heading

' . 96 | '

My first paragraph.

' . 97 | '' . 98 | '' . 99 | ''; 100 | $this->http->setBody($beforeBody); 101 | $this->model->execute($this->http); 102 | 103 | 104 | $this->assertEquals($this->http->getBody(), $afterBody); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Test/Unit/Model/PassesValidator/Validators/SkipGoogleTagManagerTest.php: -------------------------------------------------------------------------------- 1 | 5 | * @github: 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace Hryvinskyi\DeferJs\Test\Unit\Model\PassesValidator\Validators; 11 | 12 | use Hryvinskyi\DeferJs\Model\PassesValidator\Validators\SkipGoogleTagManager; 13 | use Magento\Framework\App\Response\Http; 14 | use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; 15 | use PHPUnit\Framework\TestCase; 16 | 17 | class SkipGoogleTagManagerTest extends TestCase 18 | { 19 | /** 20 | * @var Http 21 | */ 22 | private $http; 23 | 24 | /** 25 | * @var SkipGoogleTagManager 26 | */ 27 | private $model; 28 | 29 | /** 30 | * Sets up the fixture 31 | */ 32 | protected function setUp() 33 | { 34 | $this->http = (new ObjectManager($this))->getObject(Http::class); 35 | $this->model = (new ObjectManager($this))->getObject(SkipGoogleTagManager::class); 36 | } 37 | 38 | /** 39 | * @return string 40 | */ 41 | private function getScriptSkipped(): string 42 | { 43 | return ''; 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function getScriptNoSkipped(): string 54 | { 55 | return ''; 56 | } 57 | 58 | /** 59 | * 60 | */ 61 | public function testSkipScript(): void 62 | { 63 | $this->assertEquals(true, $this->model->validate($this->getScriptSkipped(), $this->http)); 64 | } 65 | 66 | /** 67 | * 68 | */ 69 | public function testNoSkipScript(): void 70 | { 71 | $this->assertEquals(false, $this->model->validate($this->getScriptNoSkipped(), $this->http)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Test/Unit/Model/PassesValidator/Validators/SkipScriptByAttributeTest.php: -------------------------------------------------------------------------------- 1 | 5 | * @github: 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace Hryvinskyi\DeferJs\Test\Unit\Model\PassesValidator\Validators; 11 | 12 | use Hryvinskyi\DeferJs\Helper\Config; 13 | use Hryvinskyi\DeferJs\Model\PassesValidator\Validators\SkipScriptByAttribute; 14 | use Magento\Framework\App\Response\Http; 15 | use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | class SkipScriptByAttributeTest extends TestCase 19 | { 20 | const DEFAULT_SKIP_TAG = 'data-deferjs="false"'; 21 | 22 | /** 23 | * @var Config|\PHPUnit_Framework_MockObject_MockObject 24 | */ 25 | private $config; 26 | 27 | /** 28 | * @var Http 29 | */ 30 | private $http; 31 | 32 | /** 33 | * @var SkipScriptByAttribute 34 | */ 35 | private $model; 36 | 37 | /** 38 | * Sets up the fixture 39 | */ 40 | protected function setUp() 41 | { 42 | $this->config = $this->createPartialMock( 43 | Config::class, 44 | ['getDisableAttribute'] 45 | ); 46 | $this->config->expects($this->any())->method('getDisableAttribute')->willReturn(self::DEFAULT_SKIP_TAG); 47 | $this->http = (new ObjectManager($this))->getObject(Http::class); 48 | $this->model = (new ObjectManager($this))->getObject(SkipScriptByAttribute::class, [ 49 | 'config' => $this->config 50 | ]); 51 | } 52 | 53 | /** 54 | * @return string 55 | */ 56 | private function getBodySkipped(): string 57 | { 58 | return '' . 59 | '' . 60 | '' . 61 | '' . 62 | '' . 63 | '' . 64 | '

My First Heading

' . 65 | '

My first paragraph.

' . 66 | '' . 67 | ''; 68 | } 69 | 70 | /** 71 | * @return string 72 | */ 73 | public function getBodyNoSkipped(): string 74 | { 75 | return '' . 76 | '' . 77 | '' . 78 | '' . '' . 79 | '' . 80 | '

My First Heading

' . 81 | '

My first paragraph.

' . 82 | '' . 83 | ''; 84 | } 85 | 86 | /** 87 | * 88 | */ 89 | public function testSkipScript(): void 90 | { 91 | $this->assertEquals(true, $this->model->validate($this->getBodySkipped(), $this->http)); 92 | } 93 | 94 | /** 95 | * 96 | */ 97 | public function testNoSkipScript(): void 98 | { 99 | $this->assertEquals(false, $this->model->validate($this->getBodyNoSkipped(), $this->http)); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Test/Unit/Model/PassesValidator/Validators/SkipScriptsByControllerTest.php: -------------------------------------------------------------------------------- 1 | 5 | * @github: 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace Hryvinskyi\DeferJs\Test\Unit\Model\PassesValidator\Validators; 11 | 12 | use Hryvinskyi\DeferJs\Helper\Config; 13 | use Hryvinskyi\DeferJs\Model\PassesValidator\Validators\SkipScriptByAttribute; 14 | use Hryvinskyi\DeferJs\Model\PassesValidator\Validators\SkipScriptsByController; 15 | use Magento\Framework\App\Request\Http as RequestHttp; 16 | use Magento\Framework\App\Response\Http; 17 | use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; 18 | use PHPUnit\Framework\TestCase; 19 | use PHPUnit_Framework_MockObject_MockObject; 20 | 21 | class SkipScriptByControllerTest extends TestCase 22 | { 23 | /** 24 | * @var Config|PHPUnit_Framework_MockObject_MockObject 25 | */ 26 | private $config; 27 | 28 | /** 29 | * @var RequestHttp|PHPUnit_Framework_MockObject_MockObject 30 | */ 31 | private $request; 32 | 33 | /** 34 | * @var Http 35 | */ 36 | private $http; 37 | 38 | /** 39 | * @var SkipScriptByAttribute 40 | */ 41 | private $model; 42 | 43 | /** 44 | * Sets up the fixture 45 | */ 46 | protected function setUp() 47 | { 48 | $this->config = $this->createPartialMock( 49 | Config::class, 50 | ['getExcludeControllers'] 51 | ); 52 | 53 | $this->request = $this->createPartialMock( 54 | RequestHttp::class, 55 | ['getModuleName', 'getControllerName', 'getActionName'] 56 | ); 57 | 58 | $this->config->expects($this->any())->method('getExcludeControllers') 59 | ->willReturn('{"_1554036929376_376":{"controller":"cms_index_index"}}'); 60 | $this->request->expects($this->any())->method('getControllerName')->willReturn('index'); 61 | $this->request->expects($this->any())->method('getActionName')->willReturn('index'); 62 | 63 | 64 | $this->http = (new ObjectManager($this))->getObject(Http::class); 65 | $this->model = (new ObjectManager($this))->getObject(SkipScriptsByController::class, [ 66 | 'config' => $this->config, 67 | 'request' => $this->request, 68 | ]); 69 | } 70 | 71 | /** 72 | * 73 | */ 74 | public function testSkipScript(): void 75 | { 76 | $this->request->expects($this->any())->method('getModuleName')->willReturn('cms'); 77 | $this->assertEquals(true, $this->model->validate('', $this->http)); 78 | } 79 | 80 | /** 81 | * 82 | */ 83 | public function testNoSkipScript(): void 84 | { 85 | $this->request->expects($this->any())->method('getModuleName')->willReturn('category'); 86 | $this->assertEquals(false, $this->model->validate('', $this->http)); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Test/Unit/Model/PassesValidator/Validators/SkipScriptsByPathTest.php: -------------------------------------------------------------------------------- 1 | 5 | * @github: 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace Hryvinskyi\DeferJs\Test\Unit\Model\PassesValidator\Validators; 11 | 12 | use Hryvinskyi\DeferJs\Helper\Config; 13 | use Hryvinskyi\DeferJs\Model\PassesValidator\Validators\SkipScriptByAttribute; 14 | use Hryvinskyi\DeferJs\Model\PassesValidator\Validators\SkipScriptsByPath; 15 | use Magento\Framework\App\Request\Http as RequestHttp; 16 | use Magento\Framework\App\Response\Http; 17 | use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; 18 | use PHPUnit\Framework\TestCase; 19 | use PHPUnit_Framework_MockObject_MockObject; 20 | 21 | class SkipScriptsByPathTest extends TestCase 22 | { 23 | /** 24 | * @var Config|PHPUnit_Framework_MockObject_MockObject 25 | */ 26 | private $config; 27 | 28 | /** 29 | * @var RequestHttp|PHPUnit_Framework_MockObject_MockObject 30 | */ 31 | private $request; 32 | 33 | /** 34 | * @var Http 35 | */ 36 | private $http; 37 | 38 | /** 39 | * @var SkipScriptByAttribute 40 | */ 41 | private $model; 42 | 43 | /** 44 | * Sets up the fixture 45 | */ 46 | protected function setUp() 47 | { 48 | $this->config = $this->createPartialMock( 49 | Config::class, 50 | ['getExcludePaths'] 51 | ); 52 | 53 | $this->request = $this->createPartialMock( 54 | RequestHttp::class, 55 | ['getRequestUri'] 56 | ); 57 | 58 | $this->config->expects($this->any())->method('getExcludePaths') 59 | ->willReturn('{"_1554036934542_542":{"path":"\/"}}'); 60 | 61 | 62 | $this->http = (new ObjectManager($this))->getObject(Http::class); 63 | $this->model = (new ObjectManager($this))->getObject(SkipScriptsByPath::class, [ 64 | 'config' => $this->config, 65 | 'request' => $this->request, 66 | ]); 67 | } 68 | 69 | /** 70 | * 71 | */ 72 | public function testSkipScript(): void 73 | { 74 | $this->request->expects($this->any())->method('getRequestUri')->willReturn('/'); 75 | $this->assertEquals(true, $this->model->validate('', $this->http)); 76 | } 77 | 78 | /** 79 | * 80 | */ 81 | public function testNoSkipScript(): void 82 | { 83 | $this->request->expects($this->any())->method('getRequestUri')->willReturn('/someUrl.html'); 84 | $this->assertEquals(false, $this->model->validate('', $this->http)); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Test/Unit/Model/PassesValidator/Validators/SkipScriptsByURLPatternTest.php: -------------------------------------------------------------------------------- 1 | 5 | * @github: 6 | */ 7 | 8 | declare(strict_types=1); 9 | 10 | namespace Hryvinskyi\DeferJs\Test\Unit\Model\PassesValidator\Validators; 11 | 12 | use Hryvinskyi\DeferJs\Helper\Config; 13 | use Hryvinskyi\DeferJs\Model\PassesValidator\Validators\SkipScriptsByURLPattern; 14 | use Magento\Framework\App\Request\Http as RequestHttp; 15 | use Magento\Framework\App\Response\Http; 16 | use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; 17 | use PHPUnit\Framework\TestCase; 18 | use PHPUnit_Framework_MockObject_MockObject; 19 | 20 | class SkipScriptsByURLPatternTest extends TestCase 21 | { 22 | /** 23 | * @var Config|PHPUnit_Framework_MockObject_MockObject 24 | */ 25 | private $config; 26 | 27 | /** 28 | * @var RequestHttp|PHPUnit_Framework_MockObject_MockObject 29 | */ 30 | private $request; 31 | 32 | /** 33 | * @var Http 34 | */ 35 | private $http; 36 | 37 | /** 38 | * @var SkipScriptsByURLPattern 39 | */ 40 | private $model; 41 | 42 | /** 43 | * Sets up the fixture 44 | */ 45 | protected function setUp() 46 | { 47 | $this->config = $this->createPartialMock( 48 | Config::class, 49 | ['getExcludeUrlPattern'] 50 | ); 51 | 52 | $this->request = $this->createPartialMock( 53 | RequestHttp::class, 54 | ['getRequestUri'] 55 | ); 56 | 57 | $this->config->expects($this->any())->method('getExcludeUrlPattern') 58 | ->willReturn('{"_1589359003110_110":{"pattern":"*argus-all-weather*"}}'); 59 | 60 | 61 | $this->http = (new ObjectManager($this))->getObject(Http::class); 62 | $this->model = (new ObjectManager($this))->getObject(SkipScriptsByURLPattern::class, [ 63 | 'config' => $this->config, 64 | 'request' => $this->request, 65 | ]); 66 | } 67 | 68 | /** 69 | * 70 | */ 71 | public function testSkipScript(): void 72 | { 73 | $this->request->expects($this->any())->method('getRequestUri')->willReturn('/argus-all-weather-tank.html'); 74 | $this->assertEquals(true, $this->model->validate('', $this->http)); 75 | } 76 | 77 | /** 78 | * 79 | */ 80 | public function testNoSkipScript(): void 81 | { 82 | $this->request->expects($this->any())->method('getRequestUri')->willReturn('/someUrl.html'); 83 | $this->assertEquals(false, $this->model->validate('', $this->http)); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hryvinskyi/magento2-deferjs", 3 | "description": "N/A", 4 | "require": { 5 | "php": "~7.4.0||~8.0||~8.1||~8.2||~8.3||~8.4", 6 | "magento/framework": "*", 7 | "magento/module-store": "*", 8 | "hryvinskyi/magento2-base": "~2.1.0" 9 | }, 10 | "require-dev": { 11 | "phpunit/phpunit": "~6.5.0" 12 | }, 13 | "type": "magento2-module", 14 | "version": "1.3.3", 15 | "license": "MIT", 16 | "autoload": { 17 | "files": [ 18 | "registration.php" 19 | ], 20 | "psr-4": { 21 | "Hryvinskyi\\DeferJs\\": "" 22 | } 23 | }, 24 | "config": { 25 | "secure-http":false 26 | }, 27 | "repositories": [ 28 | { 29 | "type": "composer", 30 | "url": "http://repo.magento.com/" 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /etc/acl.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /etc/adminhtml/system.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 10 | 11 |
13 | 14 | hryvinskyi 15 | Hryvinskyi_DeferJs::defer_js 16 | 17 | 18 | 19 | 20 | Magento\Config\Model\Config\Source\Yesno 21 | 22 | 24 | 25 | 26 | 1 27 | 28 | 29 | 31 | 32 | Only JavaScript in the body of the page 33 | Magento\Config\Model\Config\Source\Yesno 34 | 35 | 1 36 | 37 | 38 | 40 | 41 | Magento\Config\Model\Config\Source\Yesno 42 | 43 | 1 44 | 45 | 46 | 48 | 49 | \Hryvinskyi\DeferJs\Block\Adminhtml\System\ExcludeControllers 50 | \Hryvinskyi\DeferJs\Model\Config\Backend\Serialized\ArraySerialized 51 | Controller will be unaffected by defer js. Use like: [module]_[controller]_[action] (Example: cms_index_index) 52 | 53 | 55 | 56 | \Hryvinskyi\DeferJs\Block\Adminhtml\System\ExcludePaths 57 | \Hryvinskyi\DeferJs\Model\Config\Backend\Serialized\ArraySerialized 58 | Paths will be unaffected by defer js. Use like: /women/tops-women/jackets-women.html 59 | 60 | 62 | 63 | \Hryvinskyi\DeferJs\Block\Adminhtml\System\ExcludeUrlPattern 64 | \Hryvinskyi\DeferJs\Model\Config\Backend\Serialized\ArraySerialized 65 | 66 | *cell-phones* 68 | *cell-phones/nokia-2610-phone.html 69 | customer_account_* 70 | /customer/account/* 71 | *?mode=list]]> 72 | 73 | 74 | 75 |
76 |
77 |
78 | -------------------------------------------------------------------------------- /etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 10 | 11 | 12 | 13 | 0 14 | 15 | 1 16 | 0 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /etc/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 10 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /etc/frontend/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | Hryvinskyi\DeferJs\Model\PassesValidator\Validators\SkipGoogleTagManager 20 | Hryvinskyi\DeferJs\Model\PassesValidator\Validators\SkipScriptByAttribute 22 | Hryvinskyi\DeferJs\Model\PassesValidator\Validators\SkipScriptsByController 24 | Hryvinskyi\DeferJs\Model\PassesValidator\Validators\SkipScriptsByPath 26 | Hryvinskyi\DeferJs\Model\PassesValidator\Validators\SkipScriptsByURLPattern 28 | 29 | 30 | 31 | 32 | 33 | 34 | deferJsPassesValidators 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /etc/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 | 5 | * @github: 6 | */ 7 | 8 | \Magento\Framework\Component\ComponentRegistrar::register( 9 | \Magento\Framework\Component\ComponentRegistrar::MODULE, 10 | 'Hryvinskyi_DeferJs', 11 | __DIR__ 12 | ); 13 | --------------------------------------------------------------------------------