├── src ├── styles │ ├── index.css │ └── gridjs.css ├── components │ ├── AddToCartForm.js │ └── App.js └── index.js ├── view ├── base │ ├── web │ │ └── js │ │ │ ├── index_bundle.js │ │ │ ├── react-dom.js │ │ │ └── htm.module.js │ ├── templates │ │ ├── vue-component.phtml │ │ ├── component.phtml │ │ └── react-component.phtml │ └── requirejs-config.js ├── adminhtml │ ├── templates │ │ └── index │ │ │ └── index.phtml │ └── layout │ │ └── default.xml └── frontend │ ├── layout │ ├── catalog_product_view_type_configurable.xml │ ├── catalog_product_view.xml │ ├── catalog_category_view.xml │ ├── default_head_blocks.xml │ └── default.xml │ ├── templates │ ├── product │ │ ├── list │ │ │ └── addto │ │ │ │ └── compare.phtml │ │ ├── image_with_borders.phtml │ │ ├── breadcrumbs.phtml │ │ ├── compare │ │ │ └── sidebar.phtml │ │ ├── view │ │ │ ├── renderer.phtml │ │ │ └── gallery.phtml │ │ ├── list.phtml │ │ └── listing │ │ │ └── renderer.phtml │ ├── topmenu-account.phtml │ ├── html │ │ └── header │ │ │ └── logo.phtml │ ├── react-footer.phtml │ ├── react-header.phtml │ ├── sidebar.phtml │ ├── react-header-css.phtml │ └── react-footer-css.phtml │ ├── etc │ └── view.xml │ └── web │ └── js │ └── cash.js ├── React └── React │ ├── .gitignore │ ├── .babelrc │ └── composer.json ├── tests ├── .gitignore ├── phpunit.xml ├── composer.json ├── Pest.php ├── README.md └── bootstrap.php ├── KnockoutMagento2React.jpg ├── KnockoutMagento2React.png ├── registration.php ├── etc ├── adminhtml │ ├── routes.xml │ ├── menu.xml │ └── system.xml ├── frontend │ └── events.xml ├── module.xml ├── acl.xml ├── config.xml └── di.xml ├── Block ├── Adminhtml │ └── Index │ │ └── Index.php └── Product │ ├── Renderer │ └── Configurable.php │ └── ImageFactory.php ├── composer.json ├── Controller └── Adminhtml │ └── Index │ └── Index.php ├── LICENSE.md ├── DeferJS.php ├── .gitignore ├── package.json ├── webpack.config.js ├── RemoveMagentoInitScripts.php ├── purge.json ├── Template.php ├── Plugin └── PostDeployCopy.php ├── custom-purge.json.example ├── purge.json.example ├── CustomerData └── Cart.php ├── css-compile.js └── README.md /src/styles/index.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /view/base/web/js/index_bundle.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /React/React/.gitignore: -------------------------------------------------------------------------------- 1 | ./node_modules 2 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /composer.lock -------------------------------------------------------------------------------- /KnockoutMagento2React.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Genaker/reactmagento2/HEAD/KnockoutMagento2React.jpg -------------------------------------------------------------------------------- /KnockoutMagento2React.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Genaker/reactmagento2/HEAD/KnockoutMagento2React.png -------------------------------------------------------------------------------- /React/React/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": ["@babel/plugin-proposal-class-properties"] 4 | } 5 | -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 | 2 |

{{ message }} -> {{ name }} !

3 | 4 | -------------------------------------------------------------------------------- /view/base/requirejs-config.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | /* map: { 3 | '*': { 4 | 'react': 'React_React/js/react', 5 | 'react-dom': 'React_React/js/react-dom', 6 | 'react-app': 'React_React/js/index_bundle', 7 | } 8 | },*/ 9 | }; 10 | -------------------------------------------------------------------------------- /view/adminhtml/templates/index/index.phtml: -------------------------------------------------------------------------------- 1 |
2 | 3 | 15 | 16 | -------------------------------------------------------------------------------- /etc/adminhtml/routes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /etc/frontend/events.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /etc/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /view/adminhtml/layout/default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | Unit 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /view/base/templates/component.phtml: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
7 | 8 | 18 | -------------------------------------------------------------------------------- /view/base/templates/react-component.phtml: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
7 | 8 | 18 | -------------------------------------------------------------------------------- /etc/acl.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Block/Adminhtml/Index/Index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /view/frontend/layout/catalog_product_view_type_configurable.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React_React::product/view/renderer.phtml 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 0 7 | 8 | 9 | 0 10 | 11 | 12 | 0 13 | 14 | 15 | 1 16 | 17 | 18 | 1 19 | 1 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "genaker/react-luma", 3 | "description": "", 4 | "type": "magento2-module", 5 | "license": "proprietary", 6 | "authors": [ 7 | { 8 | "email": "egorshitikov@gmail.com", 9 | "name": "Yegor Shytikov" 10 | }, 11 | { 12 | "email": "sh.kiruh@gmail.com", 13 | "name": "Kirill Shytikov" 14 | } 15 | ], 16 | "minimum-stability": "stable", 17 | "require": {}, 18 | "autoload": { 19 | "files": [ 20 | "registration.php" 21 | ], 22 | "psr-4": { 23 | "React\\React\\": "" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /React/React/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react/react", 3 | "description": "", 4 | "type": "magento2-module", 5 | "license": "proprietary", 6 | "authors": [ 7 | { 8 | "email": "egorshitikov@gmail.com", 9 | "name": "Yegor Shytikov" 10 | }, 11 | { 12 | "email": "sh.kiruh@gmail.com", 13 | "name": "Kirill Shytikov" 14 | } 15 | ], 16 | "minimum-stability": "stable", 17 | "require": {}, 18 | "autoload": { 19 | "psr-4": { 20 | "React\\React\\": "React/React" 21 | }, 22 | "files": [ 23 | "registration.php" 24 | ] 25 | } 26 | } -------------------------------------------------------------------------------- /view/frontend/templates/product/list/addto/compare.phtml: -------------------------------------------------------------------------------- 1 | 9 | 17 | escapeHtml(__('Add to Compare')) ?> 18 | 19 | -------------------------------------------------------------------------------- /Block/Product/Renderer/Configurable.php: -------------------------------------------------------------------------------- 1 | isProductHasSwatchAttribute() ? 20 | self::SWATCH_RENDERER_TEMPLATE : self::CONFIGURABLE_RENDERER_TEMPLATE; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /view/frontend/etc/view.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 700 6 | 560 7 | 50 8 | 9 | 10 | 700 11 | 560 12 | 65 13 | 14 | 15 | 700 16 | 700 17 | 65 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/components/AddToCartForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | //import jQuery from 'jquery'; 3 | 4 | class AddToCartForm extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.addToCartUrl = this.addToCartUrl.bind(this); 8 | } 9 | 10 | 11 | addToCartUrl() 12 | { 13 | 14 | return 'url'; 15 | } 16 | 17 | 18 | render() { 19 | return ( 20 |
21 |
22 | 23 | 24 | 25 |
26 |
27 | ) 28 | } 29 | } 30 | 31 | export default AddToCartForm; 32 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import AddToCartForm from './AddToCartForm'; 4 | //import jQuery from 'jquery'; 5 | 6 | class App extends React.Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | //Add to cart Toggle flag 11 | document.afterAddToCart = false; 12 | document.warrantyPopUpWasOpen = false; 13 | 14 | this.state = { 15 | data: document.global_data_for_react_app, 16 | selected: false, 17 | selectedItem: null, 18 | }; 19 | 20 | this.togglePopup = this.togglePopup.bind(this); 21 | } 22 | 23 | togglePopup() { 24 | 25 | } 26 | 27 | 28 | componentDidMount() { 29 | } 30 | 31 | 32 | 33 | 34 | render() { 35 | return ( 36 |
37 |

React Component<\h1> 38 | 39 |

40 | ); 41 | } 42 | } 43 | 44 | export default App; 45 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Index/Index.php: -------------------------------------------------------------------------------- 1 | resultPageFactory = $resultPageFactory; 22 | parent::__construct($context); 23 | } 24 | 25 | /** 26 | * Execute view action 27 | * 28 | * @return \Magento\Framework\Controller\ResultInterface 29 | */ 30 | public function execute() 31 | { 32 | return $this->resultPageFactory->create(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './styles/index.css'; 4 | import App from './components/App'; 5 | 6 | const reactApp = function(){ 7 | return { 8 | init(element,parameter) { 9 | console.log('Rendering React Component Into:' + element); 10 | let live = ''; 11 | /// Magento site live reloading feature 12 | if (window.location.hostname == 'localhost' || window.location.hostname.includes('loc')){ 13 | live =''; 14 | var imported = document.createElement('script'); 15 | imported.src = 'http://localhost:35729/livereload.js'; 16 | document.head.appendChild(imported); 17 | } 18 | 19 | return ReactDOM.render(, document.getElementById(element)); 20 | } 21 | } 22 | } 23 | 24 | define(reactApp); -------------------------------------------------------------------------------- /view/frontend/layout/catalog_product_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | Magento\Catalog\ViewModel\Product\Breadcrumbs 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /view/frontend/templates/topmenu-account.phtml: -------------------------------------------------------------------------------- 1 |
2 | 3 | 19 |
20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2025 React-Luma 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /tests/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "genaker/react-luma", 3 | "description": "", 4 | "type": "magento2-module", 5 | "license": "proprietary", 6 | "authors": [ 7 | { 8 | "email": "egorshitikov@gmail.com", 9 | "name": "Yegor Shytikov" 10 | }, 11 | { 12 | "email": "sh.kiruh@gmail.com", 13 | "name": "Kirill Shytikov" 14 | } 15 | ], 16 | "minimum-stability": "stable", 17 | "require": {}, 18 | "require-dev": { 19 | "pestphp/pest": "^2.36", 20 | "pestphp/pest-plugin-mock": "^2.0" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "React\\React\\": "../" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "React\\React\\Tests\\": "./" 30 | } 31 | }, 32 | "scripts": { 33 | "test": "pest", 34 | "test:coverage": "pest --coverage" 35 | }, 36 | "config": { 37 | "allow-plugins": { 38 | "pestphp/pest-plugin": true 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /DeferJS.php: -------------------------------------------------------------------------------- 1 | config->getValue('react_vue_config/junk/remove')); 19 | 20 | if ($removeAdobeJSJunk) { 21 | return; 22 | } 23 | $response = $observer->getEvent()->getData('response'); 24 | if (!$response) { 25 | return; 26 | } 27 | $html = $response->getBody(); 28 | if ($html == '') { 29 | return; 30 | } 31 | $conditionalJsPattern = '@(?: 18 | getInlineJs('react-core.js'); 21 | } 22 | } else { 23 | ?> 24 | 25 | 42 | 45 | 46 | -------------------------------------------------------------------------------- /view/base/web/js/htm.module.js: -------------------------------------------------------------------------------- 1 | var n = function (t, s, r, e) {var u;s[0] = 0;for (var h = 1; h < s.length; h++) {var p = s[h++],a = s[h] ? (s[0] |= p ? 1 : 2, r[s[h++]]) : s[++h];3 === p ? e[0] = a : 4 === p ? e[1] = Object.assign(e[1] || {}, a) : 5 === p ? (e[1] = e[1] || {})[s[++h]] = a : 6 === p ? e[1][s[++h]] += a + "" : p ? (u = t.apply(a, n(t, a, r, ["", null])), e.push(u), a[0] ? s[0] |= 2 : (s[h - 2] = 0, s[h] = u)) : e.push(a);}return e;},t = new Map();export default function (s) {var r = t.get(this);return r || (r = new Map(), t.set(this, r)), (r = n(this, r.get(s) || (r.set(s, r = function (n) {for (var t, s, r = 1, e = "", u = "", h = [0], p = function (n) {1 === r && (n || (e = e.replace(/^\s*\n\s*|\s*\n\s*$/g, ""))) ? h.push(0, n, e) : 3 === r && (n || e) ? (h.push(3, n, e), r = 2) : 2 === r && "..." === e && n ? h.push(4, n, 0) : 2 === r && e && !n ? h.push(5, 0, !0, e) : r >= 5 && ((e || !n && 5 === r) && (h.push(r, 0, e, s), r = 6), n && (h.push(r, n, 0, s), r = 6)), e = "";}, a = 0; a < n.length; a++) {a && (1 === r && p(), p(a));for (var l = 0; l < n[a].length; l++) t = n[a][l], 1 === r ? "<" === t ? (p(), h = [h], r = 3) : e += t : 4 === r ? "--" === e && ">" === t ? (r = 1, e = "") : e = t + e[0] : u ? t === u ? u = "" : e += t : '"' === t || "'" === t ? u = t : ">" === t ? (p(), r = 1) : r && ("=" === t ? (r = 5, s = e, e = "") : "/" === t && (r < 5 || ">" === n[a][l + 1]) ? (p(), 3 === r && (h = h[0]), r = h, (h = h[0]).push(2, 0, r), r = 0) : " " === t || "\t" === t || "\n" === t || "\r" === t ? (p(), r = 2) : e += t), 3 === r && "!--" === e && (r = 4, h = h[0]);}return p(), h;}(s)), r), arguments, [])).length > 1 ? r : r[0];} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reacttable", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "description": "The React-Luma Module is a Faster and Free Open-Source Magento 2 Luma or another Theme Optimiser and Hyva Theme Alternative. Actually, you don't even need to change the Theme. It works as a composer module to improve your existing theme without re-platforming to Hyva, or it can be used to improve Hyva.", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "webpack --mode development --watch", 9 | "build": "webpack --mode production" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@babel/core": "^7.4.4", 15 | "@babel/plugin-proposal-class-properties": "^7.4.4", 16 | "@babel/preset-env": "^7.4.4", 17 | "@babel/preset-react": "^7.0.0", 18 | "babel-loader": "^8.0.5", 19 | "copy-webpack-plugin": "^5.0.3", 20 | "css-loader": "^2.1.1", 21 | "html-webpack-harddisk-plugin": "^1.0.1", 22 | "style-loader": "^0.23.1", 23 | "webpack": "^4.32.0", 24 | "webpack-cli": "^3.3.2", 25 | "webpack-livereload-plugin": "^2.2.0" 26 | }, 27 | "dependencies": { 28 | "@fullhuman/postcss-purgecss": "^5.0.0", 29 | "autoprefixer": "^10.4.21", 30 | "chokidar": "^3.5.3", 31 | "cssnano": "^7.1.0", 32 | "fs-extra": "^11.3.0", 33 | "glob": "^11.0.3", 34 | "html-react-parser": "^0.7.1", 35 | "js-cookie": "^2.2.0", 36 | "node-fetch": "^2.7.0", 37 | "postcss": "^8.5.6", 38 | "postcss-cli": "^11.0.1", 39 | "react": "^16.8.6", 40 | "react-dom": "^16.8.6", 41 | "sass": "^1.89.2" 42 | }, 43 | "keywords": [] 44 | } 45 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | group('unit')->in('Unit'); 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Expectations 21 | |-------------------------------------------------------------------------- 22 | | 23 | | When you're writing tests, you often need to check that values meet certain conditions. The 24 | | "expect()" function gives you access to a set of "expectations" methods that you can use 25 | | to assert different things. Of course, you may extend the Expectation API at any time. 26 | | 27 | */ 28 | 29 | expect()->extend('toBeOne', function () { 30 | return $this->toBe(1); 31 | }); 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Functions 36 | |-------------------------------------------------------------------------- 37 | | 38 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 39 | | project that you don't want to repeat in every file. Here you can also expose helpers as 40 | | global functions to help you to reduce the amount of code you need to write. 41 | | 42 | */ 43 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 3 | const LiveReloadPlugin = require('webpack-livereload-plugin'); 4 | 5 | const liveReloadOptions = { 6 | 7 | } 8 | 9 | console.log('Dirrectory for compiling:'); 10 | console.log(path.join(__dirname, "/view/base/web/js/")); 11 | 12 | module.exports = { 13 | entry: "./src/index.js", 14 | output: { 15 | path: path.join(__dirname, "view/base/web/js/"), 16 | filename: "index_bundle.js" 17 | }, 18 | /*externals: { 19 | 'React': 'React', 20 | 'ReactDOM': 'ReactDOM', 21 | 'ReactRouter': 'ReactRouter' 22 | },*/ 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.js$/, 27 | exclude: /node_modules/, 28 | use: { 29 | loader: "babel-loader" 30 | }, 31 | }, 32 | { 33 | test: /\.css$/, 34 | use: ["style-loader", "css-loader"] 35 | } 36 | ] 37 | }, 38 | ///var/www/html/magento 39 | ///var/www/html/magento/../../../../../../../../index_bundle.js 40 | 41 | 42 | //Deployment path needs to be adjusted 43 | plugins: [ 44 | new LiveReloadPlugin(), 45 | new CopyWebpackPlugin([ 46 | { 47 | from:path.join(__dirname, "/view/base/web/js/"), 48 | to:'../../../../../../../../pub/static/frontend/{ThemeNamae}/{theme}/en_US/React_React/js/', 49 | force: true 50 | }, 51 | { 52 | from:path.join(__dirname, "/view/base/web/js/"), 53 | to:'../../../../../../../../magento/pub/static/frontend/{ThemeName}/{theme}/en_US/React_React/js/', 54 | force: true 55 | } 56 | ]), 57 | ] 58 | }; 59 | -------------------------------------------------------------------------------- /view/frontend/templates/react-header.phtml: -------------------------------------------------------------------------------- 1 | registry->registry('current_product') ? true : false; 3 | $criticalCSSHTML = boolval($this->config->getValue('react_vue_config/css/critical')); 4 | 5 | if ($criticalCSSHTML || isset($_GET['css-html'])) { 6 | $criticalCSSHTML = true; 7 | } 8 | if ($isProductPage && $criticalCSSHTML) { 9 | $css = @file_get_contents(BP . '/pub/static/product-critical-m.css'); 10 | ?> 11 | 14 | 16 | 34 | 35 | 36 | removeAdobeJSJunk()) { 38 | ?> 39 | 49 | 52 | 53 | -------------------------------------------------------------------------------- /view/frontend/layout/default_head_blocks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 25 | 26 | 27 | 28 | 29 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /view/frontend/templates/product/image_with_borders.phtml: -------------------------------------------------------------------------------- 1 | 7 | getWidth(); 12 | $height = (int) $block->getHeight(); 13 | $paddingBottom = $block->getRatio() * 100; 14 | 15 | $globalBlock = $block->getLayout()->getBlock('header'); 16 | $preloadImages = 2; 17 | if ($globalBlock) { 18 | $catImage = $globalBlock->getData('cat_image_counter') ?? 0; 19 | $catImage++; 20 | $globalBlock->setData('cat_image_counter', $catImage); 21 | } else { 22 | $catImage = 10; 23 | } 24 | $isProductPage = ($block->getFullActionName() === 'catalog_product_view'); 25 | $isHomePage = ($block->getFullActionName() === 'cms_index_index'); 26 | ?> 27 | 28 | 29 | getCustomAttributes() as $name => $value): ?> 31 | escapeHtmlAttr($name)?>="escapeHtml($value)?>" 32 | 33 | src="escapeUrl($block->getImageUrl())?>" 34 | $preloadImages || $isProductPage || $isHomePage) { ?> 35 | loading="lazy" 36 | fetchpriority="low" 37 | 38 | loading="eager" 39 | fetchpriority="high" 40 | 41 | width="escapeHtmlAttr($block->getWidth())?>" 42 | height="escapeHtmlAttr($block->getHeight())?>" 43 | alt="escapeHtmlAttr($block->getLabel())?>"/> 44 | 45 | getProductId()} { 48 | width: {$width}px; 49 | height: auto; 50 | aspect-ratio: {$width} / {$height}; 51 | } 52 | .product-image-container-{$block->getProductId()} span.product-image-wrapper { 53 | height: 100%; 54 | width: 100%; 55 | } 56 | @supports not (aspect-ratio: auto) { 57 | .product-image-container-{$block->getProductId()} span.product-image-wrapper { 58 | padding-bottom: {$paddingBottom}%; 59 | } 60 | } 61 | STYLE; 62 | ?> 63 | renderTag('style', [], $styles, false)?> 64 | -------------------------------------------------------------------------------- /RemoveMagentoInitScripts.php: -------------------------------------------------------------------------------- 1 | get(\Magento\Framework\App\Request\Http::class); 34 | $config = $objectManager->get(Config::class); 35 | $removeAdobeJSJunk = boolval($config->getValue('react_vue_config/junk/remove')); 36 | if (isset($_GET['js-junk']) && $_GET['js-junk'] === "false") { 37 | $removeAdobeJSJunk = false; 38 | } 39 | if (isset($_GET['js-junk']) && $_GET['js-junk'] === "true") { 40 | $removeAdobeJSJunk = true; 41 | } 42 | 43 | if ($removeAdobeJSJunk) { 44 | $actionName = $request->getFullActionName(); 45 | $content = $result; 46 | 47 | if (!in_array($actionName, $this->actionFilter)) { 48 | return $result; 49 | } 50 | if ((!is_string($content) || empty($content) || $this->flag)) { 51 | return $result; 52 | } 53 | 54 | $startTime = microtime(true); 55 | // Remove all `@msU'; 72 | preg_match_all($conditionalJsPattern, $html, $_matches); 73 | $jsHtml = implode('', $_matches[0]); 74 | $html = preg_replace($conditionalJsPattern, '', $html); 75 | $html .= $jsHtml; 76 | 77 | $result = $html; 78 | 79 | return $result; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /view/frontend/layout/default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 16 | 17 | 18 | 22 | 25 | 26 | true 27 | 28 | 29 | 30 | 31 | 32 | 33 | Account 34 | 35 | 36 | 37 | 38 | 39 | 40 | React_React::html/header/logo.phtml 41 | images/logo.svg 42 | React-Luma 43 | 150 44 | 50 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /purge.json: -------------------------------------------------------------------------------- 1 | { 2 | "styles-m.min.css": { 3 | "content": { 4 | "urls": [ 5 | "https://react-luma.cnxt.link", 6 | "https://react-luma.cnxt.link/men/bottoms-men/pants-men.html" 7 | ], 8 | "paths": [ 9 | "content/react_luma_cnxt_link_1.html", 10 | "content/react_luma_cnxt_link_2.html" 11 | ] 12 | }, 13 | "options": { 14 | "safelist": [ 15 | "html", 16 | "body", 17 | "head", 18 | "script", 19 | "style" 20 | ], 21 | "ignore": [ 22 | ".debug-*", 23 | ".test-*", 24 | ".temp-*", 25 | ".unused-*" 26 | ], 27 | "blocklist": [ 28 | ".deprecated-*", 29 | ".old-*", 30 | ".legacy-*", 31 | ".vendor-prefix-*" 32 | ] 33 | } 34 | }, 35 | "styles-l.min.css": { 36 | "content": { 37 | "urls": [ 38 | "https://react-luma.cnxt.link" 39 | ], 40 | "paths": [ 41 | "content/react_luma_cnxt_link_1.html" 42 | ] 43 | }, 44 | "options": { 45 | "safelist": [ 46 | "html", 47 | "body", 48 | "head", 49 | "script", 50 | "style" 51 | ], 52 | "ignore": [ 53 | ".debug-*", 54 | ".test-*" 55 | ], 56 | "blocklist": [ 57 | ".deprecated-*", 58 | ".old-*" 59 | ] 60 | } 61 | }, 62 | "product-styles-m.min.css": { 63 | "content": { 64 | "urls": [ 65 | "https://react-luma.cnxt.link", 66 | "https://react-luma.cnxt.link/men/bottoms-men/pants-men.html" 67 | ], 68 | "paths": [ 69 | "content/react_luma_cnxt_link_1.html" 70 | ] 71 | }, 72 | "options": { 73 | "safelist": [ 74 | "html", 75 | "body", 76 | "head", 77 | "script", 78 | "style", 79 | "product-item", 80 | "product-image" 81 | ], 82 | "ignore": [ 83 | ".debug-*", 84 | ".test-*", 85 | ".temp-*", 86 | ".unused-*", 87 | ".product-debug-*" 88 | ], 89 | "blocklist": [ 90 | ".deprecated-*", 91 | ".old-*", 92 | ".legacy-*", 93 | ".vendor-prefix-*", 94 | ".product-old-*" 95 | ] 96 | } 97 | }, 98 | "category-styles-m.min.css": { 99 | "content": { 100 | "urls": [ 101 | "https://react-luma.cnxt.link/men/bottoms-men/pants-men.html" 102 | ], 103 | "paths": [ 104 | "content/react_luma_cnxt_link_2.html" 105 | ] 106 | }, 107 | "options": { 108 | "safelist": [ 109 | "html", 110 | "body", 111 | "head", 112 | "script", 113 | "style", 114 | "category-page", 115 | "product-grid" 116 | ], 117 | "ignore": [ 118 | ".debug-*", 119 | ".test-*", 120 | ".temp-*", 121 | ".unused-*", 122 | ".category-debug-*" 123 | ], 124 | "blocklist": [ 125 | ".deprecated-*", 126 | ".old-*", 127 | ".legacy-*", 128 | ".vendor-prefix-*", 129 | ".category-old-*" 130 | ] 131 | } 132 | } 133 | } -------------------------------------------------------------------------------- /Template.php: -------------------------------------------------------------------------------- 1 | om = $om; 25 | $this->registry = $registry; 26 | $this->config = $config; 27 | 28 | parent::__construct($context, $data); 29 | } 30 | 31 | // Function to encode an image as Base64 32 | public function imageToBase64($imagePath) 33 | { 34 | if (file_exists($imagePath)) { 35 | $imageData = file_get_contents($imagePath); 36 | $base64 = base64_encode($imageData); 37 | $mimeType = mime_content_type($imagePath); // Get MIME type 38 | return "data:$mimeType;base64,$base64"; 39 | } 40 | return ""; 41 | } 42 | 43 | public function removeAdobeJSJunk() 44 | { 45 | // Check cookie first 46 | if (isset($_COOKIE['js-junk'])) { 47 | return $_COOKIE['js-junk'] === "true"; 48 | } 49 | 50 | // Fall back to GET parameter 51 | if (isset($_GET['js-junk']) && $_GET['js-junk'] === "false") { 52 | return false; 53 | } 54 | if (isset($_GET['js-junk']) && $_GET['js-junk'] === "true") { 55 | return true; 56 | } 57 | 58 | // Fall back to config 59 | return boolval($this->config->getValue('react_vue_config/junk/remove')); 60 | } 61 | 62 | public function removeAdobeCSSJunk() 63 | { 64 | // Check cookie first 65 | if (isset($_COOKIE['css-react'])) { 66 | return $_COOKIE['css-react'] === "true"; 67 | } 68 | 69 | // Fall back to GET parameter 70 | if (!isset($_GET['css-react'])) { 71 | return boolval($this->config->getValue('react_vue_config/junk/remove')); 72 | } 73 | 74 | if (isset($_GET['css-react']) && $_GET['css-react'] === "false") { 75 | return false; 76 | } 77 | if (isset($_GET['css-react']) && $_GET['css-react'] === "true") { 78 | return true; 79 | } 80 | 81 | // Fall back to config (should not reach here, but safety fallback) 82 | return boolval($this->config->getValue('react_vue_config/junk/remove')); 83 | } 84 | 85 | public function getInlineJs($file) { 86 | $jsContent = file_get_contents(__DIR__ . '/view/frontend/web/js/' . $file); 87 | return ''; 88 | } 89 | 90 | 91 | /** 92 | * Check if minification is enabled 93 | * 94 | * @return bool|null 95 | */ 96 | public function isMinifyEnabled($flag = false) 97 | { 98 | return $this->getData('minify'); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /etc/adminhtml/system.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | react_vue 10 | React_React::config_react_react 11 | 12 | 13 | 14 | 15 | Magento\Config\Model\Config\Source\Yesno 16 | enable VueJS 17 | 18 | 19 | 20 | 21 | 22 | 23 | Magento\Config\Model\Config\Source\Yesno 24 | enable ReactJS 25 | 26 | 27 | 28 | 29 | 30 | 31 | Magento\Config\Model\Config\Source\Yesno 32 | enable GridJS (https://gridjs.io/) 33 | 34 | 35 | 36 | 37 | 38 | 39 | Magento\Config\Model\Config\Source\Yesno 40 | Remove Magento's default JS garbage (Require,Knokout,jQuery). You will need implement required functionality or use Magento Open Source ReactJS Luma Theme 41 | 42 | 43 | 44 | 45 | 46 | 47 | Magento\Config\Model\Config\Source\Yesno 48 | Remove Magento's default CSS garbage. You will need add optimized CSS to pub/static/styles-m.css and pub/static/styles-l.css. The files will be automaticaly included if they are exists 49 | 50 | 51 | 52 | Magento\Config\Model\Config\Source\Yesno 53 | Output Critical CSS to HTML 54 | 55 | 56 |
57 |
58 |
59 | -------------------------------------------------------------------------------- /Plugin/PostDeployCopy.php: -------------------------------------------------------------------------------- 1 | filesystem = $filesystem; 40 | $this->logger = $logger; 41 | $this->staticDirectory = $filesystem->getDirectoryWrite(DirectoryList::STATIC_VIEW); 42 | } 43 | 44 | /** 45 | * After plugin for deploy method 46 | * 47 | * @param DeployStaticContent $subject 48 | * @param void $result 49 | * @param array $options 50 | * @return void 51 | */ 52 | public function afterDeploy(DeployStaticContent $subject, $result, array $options) 53 | { 54 | try { 55 | $this->copyCustomStaticFiles(); 56 | $this->logger->info('Custom static files copied successfully after deployment'); 57 | } catch (\Exception $e) { 58 | $this->logger->error('Failed to copy custom static files: ' . $e->getMessage()); 59 | } 60 | } 61 | 62 | /** 63 | * Copy custom static files from module to main static directory 64 | */ 65 | private function copyCustomStaticFiles() 66 | { 67 | $sourcePath = dirname(__DIR__) . '/pub/static'; 68 | $targetPath = BP . '/pub/static'; 69 | if (!is_dir($sourcePath)) { 70 | $this->logger->warning('Source directory does not exist: ' . $sourcePath); 71 | return; 72 | } 73 | 74 | $this->copyDirectory($sourcePath, $targetPath); 75 | } 76 | 77 | /** 78 | * Recursively copy directory contents 79 | * 80 | * @param string $source 81 | * @param string $destination 82 | */ 83 | private function copyDirectory($source, $destination) 84 | { 85 | 86 | $dir = opendir($source); 87 | while (($file = readdir($dir)) !== false) { 88 | if ($file === '.' || $file === '..') { 89 | continue; 90 | } 91 | 92 | $sourcePath = $source . '/' . $file; 93 | $destPath = $destination . '/' . $file; 94 | 95 | if (is_dir($sourcePath)) { 96 | $this->copyDirectory($sourcePath, $destPath); 97 | } else { 98 | $this->copyFile($sourcePath, $destPath); 99 | } 100 | } 101 | closedir($dir); 102 | } 103 | 104 | /** 105 | * Copy a single file 106 | * 107 | * @param string $source 108 | * @param string $destination 109 | */ 110 | private function copyFile($source, $destination) 111 | { 112 | $destinationDir = dirname($destination); 113 | if (!is_dir($destinationDir)) { 114 | mkdir($destinationDir, 0755, true); 115 | } 116 | 117 | if (copy($source, $destination)) { 118 | $this->logger->debug('Copied file: ' . $source . ' -> ' . $destination); 119 | } else { 120 | $this->logger->error('Failed to copy file: ' . $source . ' -> ' . $destination); 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /view/frontend/templates/product/breadcrumbs.phtml: -------------------------------------------------------------------------------- 1 | getData('viewModel'); 13 | $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); 14 | $product = $objectManager->get('Magento\Framework\Registry')->registry('current_product'); 15 | $collection = $objectManager->get(Collection::class); 16 | $categoryHelper = $objectManager->get(\Magento\Catalog\Helper\Data::class); 17 | $categoryIds = $product->getCategoryIds(); 18 | 19 | $categoryPath = []; 20 | 21 | if (!empty($categoryIds)) { 22 | 23 | $categoryRepository = $objectManager->get(\Magento\Catalog\Model\CategoryRepository::class); 24 | 25 | // Get the category with the **deepest path** (most specific category) 26 | $maxLevel = 0; 27 | $selectedCategory = null; 28 | 29 | $categories = $collection->addAttributeToSelect('entity_id') 30 | ->addFieldToFilter('entity_id', ['in' => $categoryIds])->getItems(); 31 | 32 | // Stupid Adobe doesn't have getList for categories for prefetch... 33 | //$categoryList = $categoryRepository->getList($searchCriteria)->getItems(); 34 | 35 | foreach ($categories as $category) { 36 | try { 37 | if ($category->getLevel() > $maxLevel) { 38 | $maxLevel = $category->getLevel(); 39 | $selectedCategory = $category; 40 | } 41 | } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { 42 | continue; 43 | } 44 | } 45 | 46 | // If we have a selected category, build its breadcrumb path 47 | if ($selectedCategory) { 48 | $pathIds = explode('/', $selectedCategory->getPath()); // Category path 49 | 50 | foreach ($pathIds as $pathId) { 51 | if ($pathId > 1) { // Ignore root category (ID:1) 52 | try { 53 | $parentCategory = $categoryRepository->get($pathId); 54 | $categoryPath[] = [ 55 | 'label' => $parentCategory->getName(), 56 | 'link' => $parentCategory->getUrl(), 57 | ]; 58 | } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { 59 | continue; 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | $endTime = microtime(true); 67 | $time = $endTime - $startTime; 68 | 69 | // Regularly: 4-10ms. 70 | //header("Server-Timing: x-mag-bread;dur=" . number_format($time * 1000, 2), false); 71 | 72 | unset($categoryPath[0]); 73 | ?> 74 | 152 |
153 | 168 |
169 | 170 | helper(\Magento\Framework\Json\Helper\Data::class)->jsonDecode($viewModel->getJsonConfigurationHtmlEscaped()); 172 | //$widgetOptions = $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($widget['breadcrumbs']); 173 | //dd($widgetOptions); 174 | ?> -------------------------------------------------------------------------------- /view/frontend/templates/product/compare/sidebar.phtml: -------------------------------------------------------------------------------- 1 | 10 |
11 | 12 | 125 | -------------------------------------------------------------------------------- /Block/Product/ImageFactory.php: -------------------------------------------------------------------------------- 1 | objectManager = $objectManager; 70 | $this->presentationConfig = $presentationConfig; 71 | $this->viewAssetPlaceholderFactory = $viewAssetPlaceholderFactory; 72 | $this->viewAssetImageFactory = $viewAssetImageFactory; 73 | $this->imageParamsBuilder = $imageParamsBuilder; 74 | } 75 | 76 | /** 77 | * Remove class from custom attributes 78 | * 79 | * @param array $attributes 80 | * @return array 81 | */ 82 | private function filterCustomAttributes(array $attributes): array 83 | { 84 | if (isset($attributes['class'])) { 85 | unset($attributes['class']); 86 | } 87 | return $attributes; 88 | } 89 | 90 | /** 91 | * Retrieve image class for HTML element 92 | * 93 | * @param array $attributes 94 | * @return string 95 | */ 96 | private function getClass(array $attributes): string 97 | { 98 | return $attributes['class'] ?? 'product-image-photo'; 99 | } 100 | 101 | /** 102 | * Calculate image ratio 103 | * 104 | * @param int $width 105 | * @param int $height 106 | * @return float 107 | */ 108 | private function getRatio(int $width, int $height): float 109 | { 110 | if ($width && $height) { 111 | return $height / $width; 112 | } 113 | return 1.0; 114 | } 115 | 116 | /** 117 | * Get image label 118 | * 119 | * @param Product $product 120 | * @param string $imageType 121 | * @return string 122 | */ 123 | private function getLabel(Product $product, string $imageType): string 124 | { 125 | $label = $product->getData($imageType . '_' . 'label'); 126 | if (empty($label)) { 127 | $label = $product->getName(); 128 | } 129 | return (string) $label; 130 | } 131 | 132 | /** 133 | * Create image block from product 134 | * 135 | * @param Product $product 136 | * @param string $imageId 137 | * @param array|null $attributes 138 | * @return ImageBlock 139 | */ 140 | public function create(Product $product, string $imageId, array $attributes = null): ImageBlock 141 | { 142 | $viewImageConfig = $this->presentationConfig->getViewConfig()->getMediaAttributes( 143 | 'Magento_Catalog', 144 | ImageHelper::MEDIA_TYPE_CONFIG_NODE, 145 | $imageId 146 | ); 147 | 148 | $imageMiscParams = $this->imageParamsBuilder->build($viewImageConfig); 149 | $originalFilePath = $product->getData($imageMiscParams['image_type']); 150 | 151 | if ($originalFilePath === null || $originalFilePath === 'no_selection') { 152 | $imageAsset = $this->viewAssetPlaceholderFactory->create( 153 | [ 154 | 'type' => $imageMiscParams['image_type'], 155 | ] 156 | ); 157 | } else { 158 | $imageAsset = $this->viewAssetImageFactory->create( 159 | [ 160 | 'miscParams' => $imageMiscParams, 161 | 'filePath' => $originalFilePath, 162 | ] 163 | ); 164 | } 165 | 166 | $attributes = $attributes === null ? [] : $attributes; 167 | 168 | $data = [ 169 | 'data' => [ 170 | 'template' => self::TEMPLATE, 171 | 'image_url' => $imageAsset->getUrl(), 172 | 'width' => $imageMiscParams['image_width'], 173 | 'height' => $imageMiscParams['image_height'], 174 | 'label' => $this->getLabel($product, $imageMiscParams['image_type'] ?? ''), 175 | 'ratio' => $this->getRatio($imageMiscParams['image_width'] ?? 0, $imageMiscParams['image_height'] ?? 0), 176 | 'custom_attributes' => $this->filterCustomAttributes($attributes), 177 | 'class' => $this->getClass($attributes), 178 | 'product_id' => $product->getId(), 179 | ], 180 | ]; 181 | 182 | return $this->objectManager->create(ImageBlock::class, $data); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # ReactInjectPlugin Tests - Modern Pest Architecture 2 | 3 | This directory contains Pest PHP tests for the `ReactInjectPlugin` class, implementing the modern Pest testing architecture for Magento 2. 4 | 5 | ## Architecture 6 | 7 | Based on: https://yegorshytikov.medium.com/modern-pest-testing-architecture-for-magento-2-818d5ea2b406 8 | 9 | ### Hybrid Bootstrap Architecture 10 | 11 | ``` 12 | ┌─────────────────────────────────────────────────┐ 13 | │ HYBRID BOOTSTRAP ARCHITECTURE │ 14 | └─────────────────────────────────────────────────┘ 15 | 16 | ┌──────────────────────────────────────────┐ 17 | │ 1. Pest Autoloader (PHPUnit 10+) │ 18 | │ ✓ Modern testing framework │ 19 | │ ✓ Beautiful syntax │ 20 | │ ✓ Fast execution │ 21 | │ Location: tests/vendor/ │ 22 | └──────────────────────────────────────────┘ 23 | ↓ 24 | ┌──────────────────────────────────────────┐ 25 | │ 2. Custom Magento Autoloader │ 26 | │ ✓ Loads Magento classes │ 27 | │ ✓ Excludes PHPUnit classes │ 28 | │ ✓ Preserves Pest's PHPUnit │ 29 | │ Location: tests/bootstrap.php │ 30 | └──────────────────────────────────────────┘ 31 | ↓ 32 | ┌──────────────────────────────────────────┐ 33 | │ 3. Test Environment │ 34 | │ ✓ Pest tests ✓ │ 35 | │ ✓ Magento classes ✓ │ 36 | │ ✓ No conflicts ✓ │ 37 | └──────────────────────────────────────────┘ 38 | ``` 39 | 40 | ### Key Innovation: Selective Autoloader 41 | 42 | The core innovation is a **selective autoloader** that loads Magento classes while explicitly skipping PHPUnit: 43 | 44 | ```php 45 | spl_autoload_register(function ($class) { 46 | // SKIP PHPUnit classes - use Pest's version 47 | if (strpos($class, 'PHPUnit\\') === 0) { 48 | return false; 49 | } 50 | // Load Magento classes... 51 | }, true, false); 52 | ``` 53 | 54 | ## Current Implementation 55 | 56 | ✅ **Pest PHP installed** (v2.36.0 with PHPUnit 10+) 57 | ✅ **Test structure created** with proper bootstrap 58 | ✅ **Custom autoloader** filtering PHPUnit classes 59 | ✅ **Magento bootstrap** integration 60 | ✅ **All tests passing** (37 tests, 112 assertions) 61 | ✅ **Isolated vendor directory** in tests/ folder 62 | 63 | ## Directory Structure 64 | 65 | ``` 66 | tests/ 67 | ├── Pest.php # Pest configuration 68 | ├── bootstrap.php # Hybrid bootstrap (Pest + Magento) 69 | ├── composer.json # Test dependencies (Pest, etc.) 70 | ├── composer.lock # Lock file 71 | ├── README.md # This file 72 | ├── Unit/ 73 | │ └── ReactInjectPlugin.test.php # Main test file 74 | └── vendor/ # Test dependencies (isolated) 75 | └── ... 76 | ``` 77 | 78 | ## Running Tests 79 | 80 | ### From Module Root 81 | 82 | ```bash 83 | # Run all tests 84 | cd /var/www/html/react-luma/reactmagento2 85 | tests/vendor/bin/pest --configuration=tests/phpunit.xml 86 | 87 | # Run specific test file 88 | tests/vendor/bin/pest --configuration=tests/phpunit.xml tests/Unit/ReactInjectPlugin.test.php 89 | 90 | # Run with filter 91 | tests/vendor/bin/pest --configuration=tests/phpunit.xml tests/Unit/ReactInjectPlugin.test.php --filter="Action Filter" 92 | 93 | # Run with coverage 94 | tests/vendor/bin/pest --configuration=tests/phpunit.xml --coverage 95 | ``` 96 | 97 | ### From Tests Directory 98 | 99 | ```bash 100 | cd /var/www/html/react-luma/reactmagento2/tests 101 | composer test 102 | 103 | # Or directly 104 | vendor/bin/pest 105 | ``` 106 | 107 | ## Test Coverage 108 | 109 | The test suite covers: 110 | 111 | - ✅ **Basic functionality** - Instantiation, configuration 112 | - ✅ **Action filter** - Only allowed actions are optimized 113 | - ✅ **Page type detection** - Product, category, search pages 114 | - ✅ **Per-page CSS optimization** - Product/Category specific CSS 115 | - ✅ **Critical CSS** - Inline vs preload configuration 116 | - ✅ **JavaScript optimization** - RequireJS, React/Vue handling 117 | - ✅ **Configuration variants** - All config combinations tested 118 | 119 | ## Key Files 120 | 121 | - `tests/bootstrap.php` - Hybrid bootstrap implementing Pest + Magento architecture 122 | - `tests/Pest.php` - Pest configuration and test setup 123 | - `tests/Unit/ReactInjectPlugin.test.php` - Comprehensive unit tests 124 | - `tests/composer.json` - Test dependencies configuration 125 | - `tests/phpunit.xml` - PHPUnit configuration 126 | - `tests/vendor/bin/pest` - Pest executable (from vendor) 127 | 128 | ## How It Works 129 | 130 | 1. **Isolated Dependencies**: Test dependencies are in `tests/vendor/`, separate from Magento's vendor 131 | 2. **Selective Autoloading**: Custom autoloader loads Magento classes but skips PHPUnit 132 | 3. **Mock ObjectManager**: Uses mock ObjectManager for testing without full Magento bootstrap 133 | 4. **Hybrid Bootstrap**: Loads Pest's PHPUnit 10+ first, then Magento classes 134 | 135 | ## Troubleshooting 136 | 137 | ### Tests not running? 138 | 139 | ```bash 140 | # Ensure dependencies are installed 141 | cd tests 142 | composer install 143 | 144 | # Check Pest is accessible 145 | vendor/bin/pest --version 146 | ``` 147 | 148 | ### PHPUnit conflicts? 149 | 150 | The selective autoloader prevents PHPUnit conflicts by: 151 | - Loading Pest's PHPUnit 10+ first 152 | - Filtering out PHPUnit classes from Magento's autoloader 153 | - Using isolated vendor directory 154 | 155 | ## Development 156 | 157 | ### Adding New Tests 158 | 159 | 1. Create test file in `tests/Unit/` or `tests/Feature/` 160 | 2. Use Pest syntax: 161 | ```php 162 | test('description', function () { 163 | expect($value)->toBe($expected); 164 | }); 165 | ``` 166 | 167 | ### Running Specific Tests 168 | 169 | ```bash 170 | # Filter by name (from module root) 171 | tests/vendor/bin/pest --configuration=tests/phpunit.xml --filter="product page" 172 | 173 | # Filter by name (from tests directory - phpunit.xml found automatically) 174 | cd tests 175 | vendor/bin/pest --filter="product page" 176 | 177 | # Filter by group 178 | tests/vendor/bin/pest --configuration=tests/phpunit.xml --group=unit 179 | ``` 180 | 181 | ## References 182 | 183 | - [Modern Pest Testing Architecture for Magento 2](https://yegorshytikov.medium.com/modern-pest-testing-architecture-for-magento-2-818d5ea2b406) 184 | - [Pest PHP Documentation](https://pestphp.com/docs) 185 | -------------------------------------------------------------------------------- /custom-purge.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "main-styles.css": { 3 | "content": { 4 | "urls": [ 5 | "https://mywebsite.com", 6 | "https://mywebsite.com/about", 7 | "https://mywebsite.com/contact", 8 | "https://mywebsite.com/blog" 9 | ], 10 | "paths": [ 11 | "./src/templates/*.html", 12 | "./src/components/*.vue", 13 | "./src/pages/*.jsx", 14 | "./public/index.html" 15 | ] 16 | }, 17 | "options": { 18 | "safelist": [ 19 | "html", 20 | "body", 21 | "head", 22 | "script", 23 | "style", 24 | "app", 25 | "root", 26 | "main", 27 | "header", 28 | "footer", 29 | "nav", 30 | "sidebar" 31 | ], 32 | "ignore": [ 33 | ".debug-*", 34 | ".test-*", 35 | ".temp-*", 36 | ".unused-*", 37 | ".dev-*", 38 | ".staging-*", 39 | ".local-*" 40 | ], 41 | "blocklist": [ 42 | ".deprecated-*", 43 | ".old-*", 44 | ".legacy-*", 45 | ".vendor-prefix-*", 46 | ".ie-*", 47 | ".firefox-*", 48 | ".chrome-*", 49 | ".safari-*" 50 | ] 51 | } 52 | }, 53 | "component-styles.css": { 54 | "content": { 55 | "urls": [ 56 | "https://mywebsite.com/components" 57 | ], 58 | "paths": [ 59 | "./src/components/**/*.vue", 60 | "./src/components/**/*.jsx", 61 | "./src/components/**/*.tsx" 62 | ] 63 | }, 64 | "options": { 65 | "safelist": [ 66 | "html", 67 | "body", 68 | "head", 69 | "script", 70 | "style", 71 | "component-*", 72 | "btn-*", 73 | "card-*", 74 | "modal-*", 75 | "form-*" 76 | ], 77 | "ignore": [ 78 | ".debug-*", 79 | ".test-*", 80 | ".temp-*", 81 | ".unused-*" 82 | ], 83 | "blocklist": [ 84 | ".deprecated-*", 85 | ".old-*", 86 | ".legacy-*", 87 | ".vendor-prefix-*" 88 | ] 89 | } 90 | }, 91 | "admin-styles.css": { 92 | "content": { 93 | "urls": [ 94 | "https://mywebsite.com/admin", 95 | "https://mywebsite.com/admin/dashboard", 96 | "https://mywebsite.com/admin/users" 97 | ], 98 | "paths": [ 99 | "./src/admin/**/*.html", 100 | "./src/admin/**/*.vue", 101 | "./src/admin/**/*.jsx" 102 | ] 103 | }, 104 | "options": { 105 | "safelist": [ 106 | "html", 107 | "body", 108 | "head", 109 | "script", 110 | "style", 111 | "admin-*", 112 | "dashboard-*", 113 | "sidebar-*", 114 | "menu-*", 115 | "content-*", 116 | "table-*", 117 | "form-*" 118 | ], 119 | "ignore": [ 120 | ".debug-*", 121 | ".test-*", 122 | ".temp-*", 123 | ".unused-*", 124 | ".admin-debug-*" 125 | ], 126 | "blocklist": [ 127 | ".deprecated-*", 128 | ".old-*", 129 | ".legacy-*", 130 | ".vendor-prefix-*", 131 | ".admin-old-*" 132 | ] 133 | } 134 | }, 135 | "mobile-styles.css": { 136 | "content": { 137 | "urls": [ 138 | "https://mywebsite.com", 139 | "https://mywebsite.com/mobile" 140 | ], 141 | "paths": [ 142 | "./src/mobile/**/*.html", 143 | "./src/mobile/**/*.vue" 144 | ] 145 | }, 146 | "options": { 147 | "safelist": [ 148 | "html", 149 | "body", 150 | "head", 151 | "script", 152 | "style", 153 | "mobile-*", 154 | "touch-*", 155 | "swipe-*", 156 | "gesture-*" 157 | ], 158 | "ignore": [ 159 | ".debug-*", 160 | ".test-*", 161 | ".temp-*", 162 | ".unused-*" 163 | ], 164 | "blocklist": [ 165 | ".deprecated-*", 166 | ".old-*", 167 | ".legacy-*", 168 | ".vendor-prefix-*", 169 | ".desktop-*" 170 | ] 171 | } 172 | }, 173 | "print-styles.css": { 174 | "content": { 175 | "urls": [ 176 | "https://mywebsite.com/print" 177 | ], 178 | "paths": [ 179 | "./src/print/**/*.html" 180 | ] 181 | }, 182 | "options": { 183 | "safelist": [ 184 | "html", 185 | "body", 186 | "head", 187 | "script", 188 | "style", 189 | "print-*", 190 | "page-*", 191 | "media-print" 192 | ], 193 | "ignore": [ 194 | ".debug-*", 195 | ".test-*", 196 | ".temp-*", 197 | ".unused-*" 198 | ], 199 | "blocklist": [ 200 | ".deprecated-*", 201 | ".old-*", 202 | ".legacy-*", 203 | ".vendor-prefix-*", 204 | ".screen-*" 205 | ] 206 | } 207 | }, 208 | "dark-theme.css": { 209 | "content": { 210 | "urls": [ 211 | "https://mywebsite.com/dark", 212 | "https://mywebsite.com/settings" 213 | ], 214 | "paths": [ 215 | "./src/themes/dark/**/*.html", 216 | "./src/themes/dark/**/*.vue" 217 | ] 218 | }, 219 | "options": { 220 | "safelist": [ 221 | "html", 222 | "body", 223 | "head", 224 | "script", 225 | "style", 226 | "dark-*", 227 | "theme-*", 228 | "color-*", 229 | "bg-*" 230 | ], 231 | "ignore": [ 232 | ".debug-*", 233 | ".test-*", 234 | ".temp-*", 235 | ".unused-*" 236 | ], 237 | "blocklist": [ 238 | ".deprecated-*", 239 | ".old-*", 240 | ".legacy-*", 241 | ".vendor-prefix-*", 242 | ".light-*" 243 | ] 244 | } 245 | }, 246 | "animation-styles.css": { 247 | "content": { 248 | "urls": [ 249 | "https://mywebsite.com/animations" 250 | ], 251 | "paths": [ 252 | "./src/animations/**/*.html", 253 | "./src/animations/**/*.vue" 254 | ] 255 | }, 256 | "options": { 257 | "safelist": [ 258 | "html", 259 | "body", 260 | "head", 261 | "script", 262 | "style", 263 | "animate-*", 264 | "transition-*", 265 | "keyframe-*", 266 | "animation-*" 267 | ], 268 | "ignore": [ 269 | ".debug-*", 270 | ".test-*", 271 | ".temp-*", 272 | ".unused-*" 273 | ], 274 | "blocklist": [ 275 | ".deprecated-*", 276 | ".old-*", 277 | ".legacy-*", 278 | ".vendor-prefix-*" 279 | ] 280 | } 281 | } 282 | } -------------------------------------------------------------------------------- /purge.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "styles-m.min.css": { 3 | "content": { 4 | "urls": [ 5 | "https://react-luma.cnxt.link", 6 | "https://react-luma.cnxt.link/men/bottoms-men/pants-men.html", 7 | "https://react-luma.cnxt.link/women", 8 | "https://react-luma.cnxt.link/customer/account" 9 | ], 10 | "paths": [ 11 | "content/react_luma_cnxt_link_1.html", 12 | "content/react_luma_cnxt_link_2.html", 13 | "./templates/*.html", 14 | "./view/frontend/templates/**/*.phtml", 15 | "./components/*.vue", 16 | "./js/**/*.js" 17 | ] 18 | }, 19 | "options": { 20 | "safelist": [ 21 | "html", 22 | "body", 23 | "head", 24 | "script", 25 | "style", 26 | "page-wrapper", 27 | "page-main", 28 | "page-header", 29 | "page-footer", 30 | "navigation", 31 | "breadcrumbs" 32 | ], 33 | "ignore": [ 34 | ".debug-*", 35 | ".test-*", 36 | ".temp-*", 37 | ".unused-*", 38 | ".dev-*", 39 | ".staging-*" 40 | ], 41 | "blocklist": [ 42 | ".deprecated-*", 43 | ".old-*", 44 | ".legacy-*", 45 | ".vendor-prefix-*", 46 | ".ie-*", 47 | ".firefox-*", 48 | ".chrome-*" 49 | ] 50 | } 51 | }, 52 | "styles-l.min.css": { 53 | "content": { 54 | "urls": [ 55 | "https://react-luma.cnxt.link" 56 | ], 57 | "paths": [ 58 | "content/react_luma_cnxt_link_1.html", 59 | "./templates/*.html" 60 | ] 61 | }, 62 | "options": { 63 | "safelist": [ 64 | "html", 65 | "body", 66 | "head", 67 | "script", 68 | "style", 69 | "page-wrapper", 70 | "page-main" 71 | ], 72 | "ignore": [ 73 | ".debug-*", 74 | ".test-*" 75 | ], 76 | "blocklist": [ 77 | ".deprecated-*", 78 | ".old-*" 79 | ] 80 | } 81 | }, 82 | "product-styles-m.min.css": { 83 | "content": { 84 | "urls": [ 85 | "https://react-luma.cnxt.link", 86 | "https://react-luma.cnxt.link/men/bottoms-men/pants-men.html", 87 | "https://react-luma.cnxt.link/thorpe-track-pant.html" 88 | ], 89 | "paths": [ 90 | "content/react_luma_cnxt_link_1.html", 91 | "./view/frontend/templates/product/**/*.phtml", 92 | "./view/frontend/templates/catalog/product/**/*.phtml" 93 | ] 94 | }, 95 | "options": { 96 | "safelist": [ 97 | "html", 98 | "body", 99 | "head", 100 | "script", 101 | "style", 102 | "product-item", 103 | "product-image", 104 | "product-details", 105 | "product-actions", 106 | "product-options", 107 | "swatch-*", 108 | "gallery-*" 109 | ], 110 | "ignore": [ 111 | ".debug-*", 112 | ".test-*", 113 | ".temp-*", 114 | ".unused-*", 115 | ".product-debug-*" 116 | ], 117 | "blocklist": [ 118 | ".deprecated-*", 119 | ".old-*", 120 | ".legacy-*", 121 | ".vendor-prefix-*", 122 | ".product-old-*", 123 | ".product-legacy-*" 124 | ] 125 | } 126 | }, 127 | "category-styles-m.min.css": { 128 | "content": { 129 | "urls": [ 130 | "https://react-luma.cnxt.link/men/bottoms-men/pants-men.html", 131 | "https://react-luma.cnxt.link/women/tops-women" 132 | ], 133 | "paths": [ 134 | "content/react_luma_cnxt_link_2.html", 135 | "./view/frontend/templates/catalog/category/**/*.phtml", 136 | "./view/frontend/templates/catalog/product/list.phtml" 137 | ] 138 | }, 139 | "options": { 140 | "safelist": [ 141 | "html", 142 | "body", 143 | "head", 144 | "script", 145 | "style", 146 | "category-page", 147 | "product-grid", 148 | "toolbar", 149 | "sorter", 150 | "limiter", 151 | "pager" 152 | ], 153 | "ignore": [ 154 | ".debug-*", 155 | ".test-*", 156 | ".temp-*", 157 | ".unused-*", 158 | ".category-debug-*" 159 | ], 160 | "blocklist": [ 161 | ".deprecated-*", 162 | ".old-*", 163 | ".legacy-*", 164 | ".vendor-prefix-*", 165 | ".category-old-*" 166 | ] 167 | } 168 | }, 169 | "checkout-styles-m.min.css": { 170 | "content": { 171 | "urls": [ 172 | "https://react-luma.cnxt.link/checkout", 173 | "https://react-luma.cnxt.link/checkout/cart" 174 | ], 175 | "paths": [ 176 | "./view/frontend/templates/checkout/**/*.phtml", 177 | "./view/frontend/templates/cart/**/*.phtml" 178 | ] 179 | }, 180 | "options": { 181 | "safelist": [ 182 | "html", 183 | "body", 184 | "head", 185 | "script", 186 | "style", 187 | "checkout-*", 188 | "cart-*", 189 | "payment-*", 190 | "shipping-*", 191 | "billing-*" 192 | ], 193 | "ignore": [ 194 | ".debug-*", 195 | ".test-*", 196 | ".temp-*", 197 | ".unused-*" 198 | ], 199 | "blocklist": [ 200 | ".deprecated-*", 201 | ".old-*", 202 | ".legacy-*", 203 | ".vendor-prefix-*" 204 | ] 205 | } 206 | }, 207 | "customer-styles-m.min.css": { 208 | "content": { 209 | "urls": [ 210 | "https://react-luma.cnxt.link/customer/account", 211 | "https://react-luma.cnxt.link/customer/account/login" 212 | ], 213 | "paths": [ 214 | "./view/frontend/templates/customer/**/*.phtml", 215 | "./view/frontend/templates/customer/account/**/*.phtml" 216 | ] 217 | }, 218 | "options": { 219 | "safelist": [ 220 | "html", 221 | "body", 222 | "head", 223 | "script", 224 | "style", 225 | "customer-*", 226 | "account-*", 227 | "login-*", 228 | "register-*", 229 | "dashboard-*" 230 | ], 231 | "ignore": [ 232 | ".debug-*", 233 | ".test-*", 234 | ".temp-*", 235 | ".unused-*" 236 | ], 237 | "blocklist": [ 238 | ".deprecated-*", 239 | ".old-*", 240 | ".legacy-*", 241 | ".vendor-prefix-*" 242 | ] 243 | } 244 | }, 245 | "admin-styles-m.min.css": { 246 | "content": { 247 | "urls": [ 248 | "https://react-luma.cnxt.link/admin" 249 | ], 250 | "paths": [ 251 | "./view/adminhtml/templates/**/*.phtml", 252 | "./view/adminhtml/web/**/*.html" 253 | ] 254 | }, 255 | "options": { 256 | "safelist": [ 257 | "html", 258 | "body", 259 | "head", 260 | "script", 261 | "style", 262 | "admin-*", 263 | "dashboard-*", 264 | "menu-*", 265 | "sidebar-*", 266 | "content-*" 267 | ], 268 | "ignore": [ 269 | ".debug-*", 270 | ".test-*", 271 | ".temp-*", 272 | ".unused-*", 273 | ".admin-debug-*" 274 | ], 275 | "blocklist": [ 276 | ".deprecated-*", 277 | ".old-*", 278 | ".legacy-*", 279 | ".vendor-prefix-*", 280 | ".admin-old-*" 281 | ] 282 | } 283 | } 284 | } -------------------------------------------------------------------------------- /src/styles/gridjs.css: -------------------------------------------------------------------------------- 1 | .gridjs-footer button,.gridjs-head button{background-color:transparent;background-image:none;border:none;cursor:pointer;margin:0;outline:none;padding:0}.gridjs-temp{position:relative}.gridjs-head{margin-bottom:5px;padding:5px 1px;width:100%}.gridjs-head:after{clear:both;content:"";display:block}.gridjs-head:empty{border:none;padding:0}.gridjs-container{color:#000;display:inline-block;overflow:hidden;padding:2px;position:relative;z-index:0}.gridjs-footer{background-color:#fff;border-bottom-width:1px;border-color:#e5e7eb;border-radius:0 0 8px 8px;border-top:1px solid #e5e7eb;box-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px 0 rgba(0,0,0,.26);display:block;padding:12px 24px;position:relative;width:100%;z-index:5}.gridjs-footer:empty{border:none;padding:0}input.gridjs-input{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border:1px solid #d2d6dc;border-radius:5px;font-size:14px;line-height:1.45;outline:none;padding:10px 13px}input.gridjs-input:focus{border-color:#9bc2f7;box-shadow:0 0 0 3px rgba(149,189,243,.5)}.gridjs-pagination{color:#3d4044}.gridjs-pagination:after{clear:both;content:"";display:block}.gridjs-pagination .gridjs-summary{float:left;margin-top:5px}.gridjs-pagination .gridjs-pages{float:right}.gridjs-pagination .gridjs-pages button{background-color:#fff;border:1px solid #d2d6dc;border-right:none;outline:none;padding:5px 14px;-webkit-user-select:none;-moz-user-select:none;user-select:none}.gridjs-pagination .gridjs-pages button:focus{border-right:1px solid #d2d6dc;box-shadow:0 0 0 2px rgba(149,189,243,.5);margin-right:-1px;position:relative}.gridjs-pagination .gridjs-pages button:hover{background-color:#f7f7f7;color:#3c4257;outline:none}.gridjs-pagination .gridjs-pages button:disabled,.gridjs-pagination .gridjs-pages button:hover:disabled,.gridjs-pagination .gridjs-pages button[disabled]{background-color:#fff;color:#6b7280;cursor:default}.gridjs-pagination .gridjs-pages button.gridjs-spread{background-color:#fff;box-shadow:none;cursor:default}.gridjs-pagination .gridjs-pages button.gridjs-currentPage{background-color:#f7f7f7;font-weight:700}.gridjs-pagination .gridjs-pages button:last-child{border-bottom-right-radius:6px;border-right:1px solid #d2d6dc;border-top-right-radius:6px}.gridjs-pagination .gridjs-pages button:first-child{border-bottom-left-radius:6px;border-top-left-radius:6px}.gridjs-pagination .gridjs-pages button:last-child:focus{margin-right:0}button.gridjs-sort{background-color:transparent;background-position-x:center;background-repeat:no-repeat;background-size:contain;border:none;cursor:pointer;float:right;height:24px;margin:0;outline:none;padding:0;width:13px}button.gridjs-sort-neutral{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MDEuOTk4IiBoZWlnaHQ9IjQwMS45OTgiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDQwMS45OTggNDAxLjk5OCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHBhdGggZD0iTTczLjA5MiAxNjQuNDUyaDI1NS44MTNjNC45NDkgMCA5LjIzMy0xLjgwNyAxMi44NDgtNS40MjQgMy42MTMtMy42MTYgNS40MjctNy44OTggNS40MjctMTIuODQ3cy0xLjgxMy05LjIyOS01LjQyNy0xMi44NUwyMTMuODQ2IDUuNDI0QzIxMC4yMzIgMS44MTIgMjA1Ljk1MSAwIDIwMC45OTkgMHMtOS4yMzMgMS44MTItMTIuODUgNS40MjRMNjAuMjQyIDEzMy4zMzFjLTMuNjE3IDMuNjE3LTUuNDI0IDcuOTAxLTUuNDI0IDEyLjg1IDAgNC45NDggMS44MDcgOS4yMzEgNS40MjQgMTIuODQ3IDMuNjIxIDMuNjE3IDcuOTAyIDUuNDI0IDEyLjg1IDUuNDI0ek0zMjguOTA1IDIzNy41NDlINzMuMDkyYy00Ljk1MiAwLTkuMjMzIDEuODA4LTEyLjg1IDUuNDIxLTMuNjE3IDMuNjE3LTUuNDI0IDcuODk4LTUuNDI0IDEyLjg0N3MxLjgwNyA5LjIzMyA1LjQyNCAxMi44NDhMMTg4LjE0OSAzOTYuNTdjMy42MjEgMy42MTcgNy45MDIgNS40MjggMTIuODUgNS40MjhzOS4yMzMtMS44MTEgMTIuODQ3LTUuNDI4bDEyNy45MDctMTI3LjkwNmMzLjYxMy0zLjYxNCA1LjQyNy03Ljg5OCA1LjQyNy0xMi44NDggMC00Ljk0OC0xLjgxMy05LjIyOS01LjQyNy0xMi44NDctMy42MTQtMy42MTYtNy44OTktNS40Mi0xMi44NDgtNS40MnoiLz48L3N2Zz4=");background-position-y:center;opacity:.3}button.gridjs-sort-asc{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOTIuMzYyIiBoZWlnaHQ9IjI5Mi4zNjEiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDI5Mi4zNjIgMjkyLjM2MSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHBhdGggZD0iTTI4Ni45MzUgMTk3LjI4NyAxNTkuMDI4IDY5LjM4MWMtMy42MTMtMy42MTctNy44OTUtNS40MjQtMTIuODQ3LTUuNDI0cy05LjIzMyAxLjgwNy0xMi44NSA1LjQyNEw1LjQyNCAxOTcuMjg3QzEuODA3IDIwMC45MDQgMCAyMDUuMTg2IDAgMjEwLjEzNHMxLjgwNyA5LjIzMyA1LjQyNCAxMi44NDdjMy42MjEgMy42MTcgNy45MDIgNS40MjUgMTIuODUgNS40MjVoMjU1LjgxM2M0Ljk0OSAwIDkuMjMzLTEuODA4IDEyLjg0OC01LjQyNSAzLjYxMy0zLjYxMyA1LjQyNy03Ljg5OCA1LjQyNy0xMi44NDdzLTEuODE0LTkuMjMtNS40MjctMTIuODQ3eiIvPjwvc3ZnPg==");background-position-y:35%;background-size:10px}button.gridjs-sort-desc{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOTIuMzYyIiBoZWlnaHQ9IjI5Mi4zNjIiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDI5Mi4zNjIgMjkyLjM2MiIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHBhdGggZD0iTTI4Ni45MzUgNjkuMzc3Yy0zLjYxNC0zLjYxNy03Ljg5OC01LjQyNC0xMi44NDgtNS40MjRIMTguMjc0Yy00Ljk1MiAwLTkuMjMzIDEuODA3LTEyLjg1IDUuNDI0QzEuODA3IDcyLjk5OCAwIDc3LjI3OSAwIDgyLjIyOGMwIDQuOTQ4IDEuODA3IDkuMjI5IDUuNDI0IDEyLjg0N2wxMjcuOTA3IDEyNy45MDdjMy42MjEgMy42MTcgNy45MDIgNS40MjggMTIuODUgNS40MjhzOS4yMzMtMS44MTEgMTIuODQ3LTUuNDI4TDI4Ni45MzUgOTUuMDc0YzMuNjEzLTMuNjE3IDUuNDI3LTcuODk4IDUuNDI3LTEyLjg0NyAwLTQuOTQ4LTEuODE0LTkuMjI5LTUuNDI3LTEyLjg1eiIvPjwvc3ZnPg==");background-position-y:65%;background-size:10px}button.gridjs-sort:focus{outline:none}table.gridjs-table{border-collapse:collapse;display:table;margin:0;max-width:100%;overflow:auto;padding:0;table-layout:fixed;text-align:left;width:100%}.gridjs-tbody,td.gridjs-td{background-color:#fff}td.gridjs-td{border:1px solid #e5e7eb;box-sizing:content-box;padding:12px 24px}td.gridjs-td:first-child{border-left:none}td.gridjs-td:last-child{border-right:none}td.gridjs-message{text-align:center}th.gridjs-th{background-color:#f9fafb;border:1px solid #e5e7eb;border-top:none;box-sizing:border-box;color:#6b7280;outline:none;padding:14px 24px;position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;white-space:nowrap}th.gridjs-th .gridjs-th-content{float:left;overflow:hidden;text-overflow:ellipsis;width:100%}th.gridjs-th-sort{cursor:pointer}th.gridjs-th-sort .gridjs-th-content{width:calc(100% - 15px)}th.gridjs-th-sort:focus,th.gridjs-th-sort:hover{background-color:#e5e7eb}th.gridjs-th-fixed{box-shadow:0 1px 0 0 #e5e7eb;position:sticky}@supports (-moz-appearance:none){th.gridjs-th-fixed{box-shadow:0 0 0 1px #e5e7eb}}th.gridjs-th:first-child{border-left:none}th.gridjs-th:last-child{border-right:none}.gridjs-tr{border:none}.gridjs-tr-selected td{background-color:#ebf5ff}.gridjs-tr:last-child td{border-bottom:0}.gridjs *,.gridjs :after,.gridjs :before{box-sizing:border-box}.gridjs-wrapper{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;border-color:#e5e7eb;border-radius:8px 8px 0 0;border-top-width:1px;box-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px 0 rgba(0,0,0,.26);display:block;overflow:auto;position:relative;width:100%;z-index:1}.gridjs-wrapper:nth-last-of-type(2){border-bottom-width:1px;border-radius:8px}.gridjs-search{float:left}.gridjs-search-input{width:250px}.gridjs-loading-bar{background-color:#fff;opacity:.5;z-index:10}.gridjs-loading-bar,.gridjs-loading-bar:after{bottom:0;left:0;position:absolute;right:0;top:0}.gridjs-loading-bar:after{animation:shimmer 2s infinite;background-image:linear-gradient(90deg,hsla(0,0%,80%,0),hsla(0,0%,80%,.2) 20%,hsla(0,0%,80%,.5) 60%,hsla(0,0%,80%,0));content:"";transform:translateX(-100%)}@keyframes shimmer{to{transform:translateX(100%)}}.gridjs-td .gridjs-checkbox{cursor:pointer;display:block;margin:auto}.gridjs-resizable{bottom:0;position:absolute;right:0;top:0;width:5px}.gridjs-resizable:hover{background-color:#9bc2f7;cursor:ew-resize} 2 | -------------------------------------------------------------------------------- /view/frontend/web/js/cash.js: -------------------------------------------------------------------------------- 1 | //CAsh v1.0.0 2 | "use strict";!function(t,e){"function"==typeof define&&define.amd?define(e):"undefined"!=typeof exports?module.exports=e():t.cash=t.$=e()}(this,function(){function t(e,n){return new t.fn.init(e,n)}function e(t){var e=e||s.createDocumentFragment(),n=n||e.appendChild(s.createElement("div"));return n.innerHTML=t,n}function n(t,e){return parseInt(u.getComputedStyle(t[0],null)[e],10)}function i(){function t(t){var e=(Math.random().toString(16)+"000000000").substr(2,8);return t?"-"+e.substr(0,4)+"-"+e.substr(4,4):e}return t()+t(!0)+t(!0)+t()}function r(e,n,r){var s=t(e).data("cshid")||i();t(e).data("cshid",s),s in p||(p[s]={}),n in p[s]||(p[s][n]=[]),p[s][n].push(r)}var s=document,u=window,c=Array.prototype,a=c.slice,h=c.filter,o=/^#[\w-]*$/,f=/^\.[\w-]*$/,l=/^[\w-]*$/,d=t.fn=t.prototype={cash:!0,length:0};d.init=function(e,n){var i,r,u=[];if(!e)return this;if(this.length=1,"string"!=typeof e)return e.cash?e:(this[0]=e,this);if("<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3)u=t.parseHTML(e);else{if(i=o.test(e),r=e.slice(1),!n&&i)return this[0]=s.getElementById(r),this;n=t(n)[0]||s,u=a.call(l.test(r)?f.test(e)?s.getElementsByClassName(r):s.getElementsByTagName(e):n.querySelectorAll(e))}return this.length=0,t.merge(this,u),this},d.init.prototype=d,t.each=function(t,e){for(var n=t.length,i=0;n>i;i++)e.call(t[i],t[i],i,t)},t.extend=d.extend=function(t,e){var n;e||(e=t,t=this);for(n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t},t.matches=function(t,e){return(t.matches||t.matchesSelector||t.msMatchesSelector||t.mozMatchesSelector||t.webkitMatchesSelector||t.oMatchesSelector).call(t,e)},t.merge=function(t,e){for(var n=+e.length,i=t.length,r=0;n>r;i++,r++)t[i]=e[r];return t.length=i,t},t.parseHTML=function(t){var n=/^<(\w+)\s*\/?>(?:<\/\1>|)$/.exec(t);return n?[s.createElement(n[1])]:(n=e(t),a.call(n.childNodes))},t.unique=function(e){return t.merge(t(),a.call(e).filter(function(t,e,n){return n.indexOf(t)===e}))};var m=/\S+/g;d.extend({addClass:function(t){var e,n,i=t.match(m);return this.each(function(t){if(n=i.length,t.classList)for(;n--;)t.classList.add(i[n]);else for(;n--;)e=" "+t.className+" ",-1===e.indexOf(" "+i[n]+" ")&&(t.className+=" "+i[n])}),this},attr:function(t,e){return e?(this.each(function(n){return n.setAttribute(t,e)}),this):this[0].getAttribute(t)},hasClass:function(t){return this[0].classList?this[0].classList.contains(t):-1!==this[0].className.indexOf(t)},prop:function(t){return this[0][t]},removeAttr:function(t){return this.each(function(e){return e.removeAttribute(t)}),this},removeClass:function(t){var e,n,i=t.match(m);return this.each(function(t){if(e=i.length,t.classList)for(;e--;)t.classList.remove(i[e]);else{for(n=" "+t.className+" ";e--;)n=n.replace(" "+i[e]+" "," ");t.className=n.trim()}}),this}}),d.extend({add:function(){var e,n=a.call(this),i=0;for(e=arguments.length;e>i;i++)n=n.concat(a.call(t(arguments[i])));return t.unique(n)},each:function(e){t.each(this,e)},eq:function(e){return t(this[e])},filter:function(e){return"string"==typeof e?h.call(this,function(n){return t.matches(n,e)}):h.call(this,e)},first:function(){return t(this[0])},get:function(t){return this[t]},index:function(e){return e?a.call(t(e).children()).indexOf(this[0]):a.call(t(this[0]).parent().children()).indexOf(this[0])},last:function(){return t(this[this.length-1])}}),d.extend({css:function(t,e){return"object"!=typeof t?e?(this.each(function(n){return n.style[t]=e}),this):u.getComputedStyle(this[0],null)[t]:void this.each(function(e){for(var n in t)t.hasOwnProperty(n)&&(e.style[n]=t[n])})}}),d.extend({data:function(e,n){return n?(this.each(function(i){i.dataset?i.dataset[e]=n:t(i).attr("data-"+e,n)}),this):this[0].dataset?this[0].dataset[e]:t(this[0]).attr("data-"+e)},removeData:function(e){return this.each(function(n){n.dataset?delete n.dataset[e]:t(n).removeAttr("data-"+e)}),this}}),d.extend({height:function(){return this[0].getBoundingClientRect().height},innerWidth:function(){return this[0].clientWidth},innerHeight:function(){return this[0].clientHeight},outerWidth:function(t){return t===!0?this[0].offsetWidth+(n(this,"margin-left")||n(this,"marginLeft")||0)+(n(this,"margin-right")||n(this,"marginRight")||0):this[0].offsetWidth},outerHeight:function(t){return t===!0?this[0].offsetHeight+(n(this,"margin-top")||n(this,"marginTop")||0)+(n(this,"margin-bottom")||n(this,"marginBottom")||0):this[0].offsetHeight},width:function(){return this[0].getBoundingClientRect().width}});var p={};d.extend({off:function(e,n){return this.each(function(i){if(n)i.removeEventListener(e,n);else for(var r in p[t(i).data("cshid")][e])i.removeEventListener(e,p[t(i).data("cshid")][e][r])}),this},on:function(e,n,i){return"function"==typeof n?(i=n,this.each(function(n){r(t(n),e,i),n.addEventListener(e,i)}),this):(this.each(function(s){function u(e){var r=e.target;if(t.matches(r,n))i.call(r);else{for(;!t.matches(r,n);){if(r===s)return r=!1;r=r.parentNode}r&&i.call(r)}}r(t(s),e,u),s.addEventListener(e,u)}),this)},ready:function(t){this[0].addEventListener("DOMContentLoaded",t)},trigger:function(t){var e=s.createEvent("HTMLEvents");return e.initEvent(t,!0,!1),this.each(function(t){return t.dispatchEvent(e)}),this}});var g=encodeURIComponent;return d.extend({serialize:function(){var t,e,n,i=this[0],r="";for(e=i.elements.length-1;e>=0;e--)if(t=i.elements[e],t.name&&"file"!==t.type&&"reset"!==t.type)if("select-multiple"===t.type)for(n=i.elements[e].options.length-1;n>=0;n--)t.options[n].selected&&(r+="&"+t.name+"="+g(t.options[n].value).replace(/%20/g,"+"));else"submit"!==t.type&&"button"!==t.type&&(r+="&"+t.name+"="+g(t.value).replace(/%20/g,"+"));return r.substr(1)},val:function(t){return void 0===t?this[0].value:(this.each(function(e){return e.value=t}),this)}}),d.extend({append:function(e){return this[0].appendChild(t(e)[0]),this},appendTo:function(e){return t(e)[0].appendChild(this[0]),this},clone:function(){return t(this[0].cloneNode(!0))},empty:function(){return this.each(function(t){return t.innerHTML=""}),this},html:function(e){var n;return"undefined"===e?this[0].innerHTML:(n="object"==typeof e?t(e)[0].outerHTML:e,this.each(function(t){return t.innerHTML=""+n}),this)},insertAfter:function(e){return t(e)[0].insertAdjacentHTML("afterend",this[0].outerHTML),this},insertBefore:function(e){return t(e)[0].insertAdjacentHTML("beforebegin",this[0].outerHTML),this},prepend:function(e){return t(this)[0].insertAdjacentHTML("afterBegin",t(e)[0].outerHTML),this},prependTo:function(e){return t(e)[0].insertAdjacentHTML("afterBegin",this[0].outerHTML),this},remove:function(){this.each(function(t){return t.parentNode.removeChild(t)})},text:function(t){return t?(this.each(function(e){return e.textContent=t}),this):this[0].textContent}}),d.extend({children:function(e){return e?t(this[0].children).filter(function(n){return t.matches(n,e)}):t.fn.extend(this[0].children,t.fn)},closest:function(e){return!e||t.matches(this[0],e)?this:this.parent().closest(e)},is:function(e){return e?e.cash?this[0]===e[0]:"string"==typeof e?t.matches(this[0],e):!1:!1},find:function(e){return t.fn.extend(this[0].querySelectorAll(e),t.fn)},has:function(e){return h.call(this,function(n){return 0!==t(n).find(e).length})},next:function(){return t(this[0].nextElementSibling)},not:function(e){return h.call(this,function(n){return!t.matches(n,e)})},parent:function(){var e=c.map.call(this,function(t){return t.parentElement||s.body.parentNode});return t.unique(e)},parents:function(e){var n,i=[],r=0;return this.each(function(u){for(n=u;n!==s.body.parentNode;)n=n.parentElement,(!e||e&&t.matches(n,e))&&(i[r]=n,r++)}),t.unique(i)},prev:function(){return t(this[0].previousElementSibling)},siblings:function(){var t=this.parent().children(),e=this[0];return h.call(t,function(t){return t!==e})}}),t}); -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | vendor/magento/module-store/Model/ScopeInterface.php 88 | $parts = explode('\\', $class); 89 | if (count($parts) >= 2) { 90 | // Convert second part to module name: Store -> module-store 91 | $moduleName = 'module-' . strtolower(preg_replace('/([a-z])([A-Z])/', '$1-$2', $parts[1])); 92 | $subPath = implode('/', array_slice($parts, 2)); 93 | $file = BP . '/vendor/magento/' . $moduleName . '/' . $subPath . '.php'; 94 | if (file_exists($file)) { 95 | require_once $file; 96 | return true; 97 | } 98 | 99 | // Also try framework path: Magento\Framework\... -> vendor/magento/framework/... 100 | if ($parts[1] === 'Framework') { 101 | $subPath = implode('/', array_slice($parts, 2)); 102 | $file = BP . '/vendor/magento/framework/' . $subPath . '.php'; 103 | if (file_exists($file)) { 104 | require_once $file; 105 | return true; 106 | } 107 | } 108 | } 109 | } 110 | 111 | // Load PSR interfaces (needed for LoggerInterface, etc.) 112 | if (strpos($class, 'Psr\\') === 0) { 113 | $file = BP . '/vendor/' . strtolower(str_replace('\\', '/', $class)) . '.php'; 114 | if (file_exists($file)) { 115 | require_once $file; 116 | return true; 117 | } 118 | } 119 | 120 | return false; 121 | }, true, false); // Prepend = true, throw = false (don't throw on failure) 122 | 123 | echo "✓ Magento class autoloader registered (PHPUnit excluded)\n"; 124 | 125 | // STEP 4: Verify our class is available 126 | $classExists = class_exists('React\React\ReactInjectPlugin'); 127 | echo "ReactInjectPlugin class exists: " . ($classExists ? 'YES ✓' : 'NO ✗') . "\n"; 128 | 129 | // Check PHPUnit version 130 | $reflection = new ReflectionClass('PHPUnit\Framework\TestCase'); 131 | $phpunitFile = $reflection->getFileName(); 132 | echo "PHPUnit loaded from: " . (strpos($phpunitFile, 'reactmagento2/vendor') !== false ? 'PEST ✓' : 'MAGENTO ✗') . "\n"; 133 | echo "PHPUnit file: " . $phpunitFile . "\n"; 134 | 135 | echo "========== BOOTSTRAP COMPLETE ==========\n\n"; 136 | 137 | // Mock ObjectManager for tests (set before any class uses ObjectManager::getInstance()) 138 | if (class_exists('Magento\Framework\App\ObjectManager')) { 139 | // Create a mock ObjectManager that returns mock objects 140 | $mockObjectManager = new class implements \Magento\Framework\ObjectManagerInterface { 141 | public function get($type) { 142 | // Return mock Request when requested (without extending real class to avoid dependencies) 143 | if ($type === \Magento\Framework\App\Request\Http::class) { 144 | return new class { 145 | public function getFullActionName($delimiter = '_') { 146 | return 'catalog_product_view'; 147 | } 148 | }; 149 | } 150 | return null; 151 | } 152 | 153 | public function create($type, array $arguments = []) { 154 | try { 155 | return new $type(...$arguments); 156 | } catch (\Exception $e) { 157 | return null; 158 | } 159 | } 160 | 161 | public function configure(array $configuration) { 162 | return $this; 163 | } 164 | 165 | public function setSharedInstance(\Magento\Framework\ObjectManagerInterface $objectManager) {} 166 | 167 | public function getSharedInstance($type, $arguments = []) { 168 | return null; 169 | } 170 | 171 | public function reset() {} 172 | }; 173 | 174 | // Use reflection to set the private static property 175 | try { 176 | $reflection = new ReflectionClass('Magento\Framework\App\ObjectManager'); 177 | $property = $reflection->getProperty('_instance'); 178 | $property->setAccessible(true); 179 | $property->setValue(null, $mockObjectManager); 180 | echo "✓ Mock ObjectManager initialized\n"; 181 | } catch (\Exception $e) { 182 | echo "⚠ Could not set mock ObjectManager: " . $e->getMessage() . "\n"; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /view/frontend/templates/sidebar.phtml: -------------------------------------------------------------------------------- 1 | 10 | getData('wishlistDataViewModel'); 12 | ?> 13 | isAllow()): ?> 14 |
15 | 16 | 17 | 175 | -------------------------------------------------------------------------------- /view/frontend/templates/react-header-css.phtml: -------------------------------------------------------------------------------- 1 | 2 | 59 | 289 | 290 | -------------------------------------------------------------------------------- /CustomerData/Cart.php: -------------------------------------------------------------------------------- 1 | renderCartHtml($data); 30 | } else { 31 | $data['html_cart'] = null; // Render empty cart 32 | } 33 | 34 | return $data; 35 | } 36 | 37 | /** 38 | * Render cart HTML with enhanced React Luma features 39 | * 40 | * @param array $data Cart data 41 | * @return string 42 | */ 43 | public function renderCartHtml($data) 44 | { 45 | // Build items HTML 46 | $itemsHtml = ''; 47 | if (!empty($data['items'])) { 48 | // Debug: Show available item data 49 | $debugComment = '' . PHP_EOL . ''; 50 | 51 | $itemsHtml = $debugComment . ' 52 |
53 | ' . $data['summary_count'] . ''; 54 | if ($data['summary_count'] > 1) { 55 | $itemsHtml .= 'Items in Cart'; 56 | } else { 57 | $itemsHtml .= 'Item in Cart'; 58 | } 59 | $itemsHtml .= '
60 | 61 |
62 | Cart Subtotal 63 |
64 | 65 | ' . $data['subtotal'] . ' 66 | 67 |
68 |
69 | 70 |
71 |
72 | 73 |
74 |
75 | 76 | Recently added item(s) 77 |
78 |
    '; 79 | 80 | foreach ($data['items'] as $item) { 81 | $itemsHtml .= $this->renderCartItem($item); 82 | } 83 | 84 | $itemsHtml .= '
85 |
86 | 87 |
88 | 93 |
'; 94 | } else { 95 | $itemsHtml = '
96 |

Your cart is empty

97 |
'; 98 | } 99 | 100 | // Build complete cart HTML using heredoc 101 | $html = << 103 | 104 | My Cart 105 | {$data['summary_count']} 106 | 107 | 108 |
109 | 112 | {$itemsHtml} 113 |
114 | HTML; 115 | 116 | return $html; 117 | } 118 | 119 | /** 120 | * Render individual cart item HTML 121 | * 122 | * @param array $item Cart item data 123 | * @return string 124 | */ 125 | protected function renderCartItem($item) 126 | { 127 | // Get image source with fallbacks 128 | $imageSrc = ''; 129 | if (isset($item['product_image']['src'])) { 130 | $imageSrc = $item['product_image']['src']; 131 | } elseif (isset($item['product_image'])) { 132 | $imageSrc = $item['product_image']; 133 | } elseif (isset($item['thumbnail'])) { 134 | $imageSrc = $item['thumbnail']; 135 | } 136 | 137 | // Build product image HTML 138 | $imageHtml = ''; 139 | if (isset($item['product_url'])) { 140 | $imageUrl = $imageSrc ?: '/media/catalog/product/placeholder/default/image.jpg'; 141 | $imageHtml = ' 142 | 143 | 144 | ' . $item['product_name'] . ' 145 | 146 | 147 | '; 148 | } 149 | 150 | // Build product name HTML 151 | $nameHtml = ''; 152 | if (isset($item['product_url'])) { 153 | $nameHtml .= '' . $item['product_name'] . ''; 154 | } else { 155 | $nameHtml .= $item['product_name']; 156 | } 157 | $nameHtml .= ''; 158 | 159 | // Build product options HTML 160 | $optionsHtml = ''; 161 | if (isset($item['options']) && !empty($item['options'])) { 162 | $optionsHtml = '
163 |
'; 164 | foreach ($item['options'] as $option) { 165 | $optionsHtml .= '
' . $option['label'] . '
166 |
167 | ' . $option['value'] . ' 168 |
'; 169 | } 170 | $optionsHtml .= '
171 |
'; 172 | } 173 | 174 | // Build actions HTML 175 | $actionsHtml = '
'; 176 | if (isset($item['configure_url'])) { 177 | $actionsHtml .= '
178 | 179 | Edit 180 | 181 |
'; 182 | } 183 | $actionsHtml .= ' 188 |
'; 189 | 190 | // Build complete cart item HTML using heredoc 191 | $html = << 193 |
194 | {$imageHtml} 195 |
196 | {$nameHtml} 197 | {$optionsHtml} 198 |
199 |
200 | 201 | {$item['product_price']} 202 | 203 |
204 |
205 | 206 | 207 |
208 |
209 | {$actionsHtml} 210 |
211 |
212 | 213 | HTML; 214 | 215 | return $html; 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /view/frontend/templates/react-footer-css.phtml: -------------------------------------------------------------------------------- 1 | registry->registry('current_product'); 3 | ?> 4 | 5 | 6 | 7 | 8 | 9 | 309 | 310 | 336 | 337 | -------------------------------------------------------------------------------- /view/frontend/templates/product/view/renderer.phtml: -------------------------------------------------------------------------------- 1 | 7 | getConfigurableViewModel() 11 | ?> 12 | 13 | json_decode($block->getJsonConfig(), true), 17 | "jsonSwatchConfig" => json_decode($block->getJsonSwatchConfig(), true), 18 | "mediaCallback" => json_decode($block->escapeJs($block->escapeUrl($block->getMediaCallback())), true), 19 | "jsonSwatchImageSizeConfig" => json_decode($block->getJsonSwatchSizeConfig(), true), 20 | "showTooltip" => json_decode($block->escapeJs($configurableViewModel->getShowSwatchTooltip()), true), 21 | ]; 22 | 23 | $configurableImages = $swatchData["jsonConfig"]['images']; 24 | $swatchConfig = $swatchData["jsonConfig"]; 25 | $swatchAttributes = $swatchData["jsonSwatchConfig"]; 26 | 27 | $optionToImage = []; 28 | $imageIndex = $swatchData["jsonConfig"]['index']; 29 | foreach ($imageIndex as $imageId => $options) { 30 | foreach ($options as $option) { 31 | $optionToImage[$option] = $imageId; 32 | } 33 | } 34 | 35 | //dd($optionToImage); 36 | $configurableGallary = []; 37 | 38 | //TODO: move to the reusable method 39 | 40 | foreach ($configurableImages as $productId => $images) { 41 | $galleryHtml = ''; 69 | 70 | // Store the generated HTML in the array 71 | $configurableGallery[$productId] = $galleryHtml; 72 | } 73 | 74 | ?> 75 | 78 | 79 |
80 | 81 | 82 |
83 |
84 |
85 | 86 | 87 | $swatchData): ?> 88 |
91 | 92 | escapeHtml($swatchData['label'])?> 94 | 95 | 96 |
97 | $optionData): ?> 98 | 102 | 103 |
109 | escapeHtml($optionData['value']) : ''?> 110 |
111 | 112 |
113 | 114 |
115 | 116 | 117 |
118 |
119 |
120 | 121 | 212 | -------------------------------------------------------------------------------- /css-compile.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { readFileSync, writeFileSync, statSync } from 'fs'; 4 | import { dirname, resolve } from 'path'; 5 | import { fileURLToPath } from 'url'; 6 | import * as sass from 'sass'; 7 | import postcss from 'postcss'; 8 | import autoprefixer from 'autoprefixer'; 9 | import cssnano from 'cssnano'; 10 | import { glob } from 'glob'; 11 | import chokidar from 'chokidar'; 12 | 13 | const __filename = fileURLToPath(import.meta.url); 14 | const __dirname = dirname(__filename); 15 | 16 | // Color codes for console output 17 | const colors = { 18 | reset: '\x1b[0m', 19 | bright: '\x1b[1m', 20 | red: '\x1b[31m', 21 | green: '\x1b[32m', 22 | yellow: '\x1b[33m', 23 | blue: '\x1b[34m', 24 | magenta: '\x1b[35m', 25 | cyan: '\x1b[36m', 26 | white: '\x1b[37m' 27 | }; 28 | 29 | // Helper function for colored output 30 | function log(message, color = 'white') { 31 | console.log(`${colors[color]}${message}${colors.reset}`); 32 | } 33 | 34 | // Helper function to format file size 35 | function formatFileSize(bytes) { 36 | if (bytes === 0) return '0 Bytes'; 37 | const k = 1024; 38 | const sizes = ['Bytes', 'KB', 'MB', 'GB']; 39 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 40 | return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; 41 | } 42 | 43 | // Helper function to get file size 44 | function getFileSize(filePath) { 45 | try { 46 | const stats = statSync(filePath); 47 | return stats.size; 48 | } catch (error) { 49 | return 0; 50 | } 51 | } 52 | 53 | // Helper function to count CSS selectors 54 | function countSelectors(cssContent) { 55 | // Remove comments first 56 | const withoutComments = cssContent.replace(/\/\*[\s\S]*?\*\//g, ''); 57 | 58 | // Count selectors by looking for opening braces 59 | // This is a simplified approach - counts rule blocks 60 | const selectorMatches = withoutComments.match(/[^}]*{/g); 61 | const selectorCount = selectorMatches ? selectorMatches.length : 0; 62 | 63 | // Also count @media, @keyframes, @font-face, etc. 64 | const atRuleMatches = withoutComments.match(/@(media|keyframes|font-face|import|charset|namespace|supports|document|page|viewport|counter-style|font-feature-values|swash|ornaments|annotation|stylistic|styleset|character-variant|font-variant-alternates|font-feature-value)[^{]*{/g); 65 | const atRuleCount = atRuleMatches ? atRuleMatches.length : 0; 66 | 67 | return selectorCount + atRuleCount; 68 | } 69 | 70 | // Helper function to count SCSS selectors (simplified - just count all selectors) 71 | function countSCSSSelectors(scssContent) { 72 | // Remove comments 73 | const withoutComments = scssContent.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, ''); 74 | 75 | // Count all selectors by looking for patterns that end with { 76 | const selectorMatches = withoutComments.match(/[^}]*{/g); 77 | const selectorCount = selectorMatches ? selectorMatches.length : 0; 78 | 79 | // Count @media, @keyframes, etc. 80 | const atRuleMatches = withoutComments.match(/@(media|keyframes|font-face|import|charset|namespace|supports|document|page|viewport|counter-style|font-feature-values|swash|ornaments|annotation|stylistic|styleset|character-variant|font-variant-alternates|font-feature-value)[^{]*{/g); 81 | const atRuleCount = atRuleMatches ? atRuleMatches.length : 0; 82 | 83 | return selectorCount + atRuleCount; 84 | } 85 | 86 | async function compileSCSS(inputFile, outputFile, minify = true, isWatch = false) { 87 | try { 88 | if (isWatch) { 89 | log(`🔄 [WATCH] Compiling ${inputFile}...`, 'cyan'); 90 | } else { 91 | log(`🔄 Compiling ${inputFile}...`, 'cyan'); 92 | } 93 | 94 | // Get original file size 95 | const originalFileSize = getFileSize(inputFile); 96 | log(` 📏 Original file size: ${formatFileSize(originalFileSize)}`, 'white'); 97 | 98 | // Read SCSS content for selector counting 99 | const scssContent = readFileSync(inputFile, 'utf8'); 100 | const originalSelectors = countSCSSSelectors(scssContent); 101 | log(` 🎯 Original selectors: ${originalSelectors}`, 'white'); 102 | 103 | // 1. Compile SCSS to CSS 104 | const result = sass.compile(inputFile, { 105 | style: "expanded", 106 | sourceMap: true 107 | }); 108 | 109 | let css = result.css; 110 | log(` ✅ Sass compilation completed`, 'green'); 111 | 112 | // 2. Run PostCSS with autoprefixer and cssnano 113 | const plugins = [autoprefixer()]; 114 | 115 | if (minify) plugins.push(cssnano({ preset: 'default' })); 116 | 117 | const postCSSResult = await postcss(plugins).process(css, { from: inputFile }); 118 | css = postCSSResult.css; 119 | 120 | log(` ✅ PostCSS processing completed`, 'green'); 121 | 122 | // 3. Save output 123 | writeFileSync(outputFile, css); 124 | 125 | // Get final file size from filesystem 126 | const finalFileSize = getFileSize(outputFile); 127 | const reduction = originalFileSize > 0 ? ((originalFileSize - finalFileSize) / originalFileSize * 100).toFixed(1) : 0; 128 | 129 | // Count final selectors 130 | const finalSelectors = countSelectors(css); 131 | 132 | log(` 📁 Saved: ${outputFile}`, 'green'); 133 | log(` 📊 Final size: ${formatFileSize(finalFileSize)} (${reduction}% reduction)`, 'yellow'); 134 | log(` 🎯 Final selectors: ${finalSelectors}`, 'yellow'); 135 | 136 | if (!isWatch) { 137 | console.log(''); // Empty line for spacing 138 | } 139 | 140 | return { 141 | success: true, 142 | originalSize: originalFileSize, 143 | finalSize: finalFileSize, 144 | reduction, 145 | originalSelectors, 146 | finalSelectors 147 | }; 148 | 149 | } catch (error) { 150 | log(`❌ Error compiling ${inputFile}:`, 'red'); 151 | log(` ${error.message}`, 'red'); 152 | if (!isWatch) { 153 | console.log(''); // Empty line for spacing 154 | } 155 | return { success: false, error: error.message }; 156 | } 157 | } 158 | 159 | // Watch function for SCSS files 160 | function watchSCSS() { 161 | log('�� Starting SCSS file watcher...', 'magenta'); 162 | log('📂 Watching pub/static/*.scss files for changes...', 'blue'); 163 | console.log(''); 164 | 165 | chokidar.watch('pub/static/*.scss').on('change', async (path) => { 166 | const outputFile = path.replace('.scss', '.min.css'); 167 | await compileSCSS(path, outputFile, true, true); 168 | log(`✅ [WATCH] Recompiled: ${path}`, 'green'); 169 | console.log(''); 170 | }); 171 | 172 | log('🎯 Watcher is active. Press Ctrl+C to stop.', 'yellow'); 173 | } 174 | 175 | // Compile all SCSS files in ./src/scss 176 | async function compileAll() { 177 | log('🎨 Starting SCSS compilation...', 'magenta'); 178 | console.log(''); 179 | 180 | try { 181 | const files = await glob('pub/static/*.scss'); 182 | log(`📂 Found ${files.length} SCSS files to compile:`, 'blue'); 183 | 184 | // Show original file sizes and selector counts 185 | files.forEach(file => { 186 | const size = getFileSize(file); 187 | const scssContent = readFileSync(file, 'utf8'); 188 | const selectors = countSCSSSelectors(scssContent); 189 | log(` 📄 ${file} (${formatFileSize(size)}, ${selectors} selectors)`, 'white'); 190 | }); 191 | 192 | console.log(''); 193 | 194 | let successCount = 0; 195 | let errorCount = 0; 196 | let totalOriginalSize = 0; 197 | let totalFinalSize = 0; 198 | let totalOriginalSelectors = 0; 199 | let totalFinalSelectors = 0; 200 | 201 | for (const file of files) { 202 | const outputFile = file.replace('.scss', '.min.css'); 203 | const result = await compileSCSS(file, outputFile, true, false); 204 | 205 | if (result.success) { 206 | successCount++; 207 | totalOriginalSize += result.originalSize; 208 | totalFinalSize += result.finalSize; 209 | totalOriginalSelectors += result.originalSelectors; 210 | totalFinalSelectors += result.finalSelectors; 211 | } else { 212 | errorCount++; 213 | } 214 | } 215 | 216 | console.log(''); 217 | log('📋 Compilation Summary:', 'magenta'); 218 | log(` ✅ Successfully compiled: ${successCount} files`, 'green'); 219 | if (errorCount > 0) { 220 | log(` ❌ Errors: ${errorCount} files`, 'red'); 221 | } 222 | log(` 🎯 Total files processed: ${files.length}`, 'blue'); 223 | 224 | // Show total size reduction 225 | if (totalOriginalSize > 0) { 226 | const totalReduction = ((totalOriginalSize - totalFinalSize) / totalOriginalSize * 100).toFixed(1); 227 | log(` 📏 Total original size: ${formatFileSize(totalOriginalSize)}`, 'white'); 228 | log(` 📏 Total final size: ${formatFileSize(totalFinalSize)}`, 'white'); 229 | log(` 📊 Total reduction: ${formatFileSize(totalOriginalSize - totalFinalSize)} (${totalReduction}%)`, 'yellow'); 230 | } 231 | 232 | // Show total selector information 233 | if (totalOriginalSelectors > 0) { 234 | log(` 🎯 Total original selectors: ${totalOriginalSelectors}`, 'white'); 235 | log(` 🎯 Total final selectors: ${totalFinalSelectors}`, 'white'); 236 | const selectorReduction = totalOriginalSelectors > 0 ? ((totalOriginalSelectors - totalFinalSelectors) / totalOriginalSelectors * 100).toFixed(1) : 0; 237 | log(` 📊 Selector reduction: ${totalOriginalSelectors - totalFinalSelectors} (${selectorReduction}%)`, 'yellow'); 238 | } 239 | 240 | } catch (error) { 241 | log('❌ Error finding SCSS files:', 'red'); 242 | log(` ${error.message}`, 'red'); 243 | } 244 | } 245 | 246 | // Check if watch mode is enabled 247 | if (process.argv.includes('--watch')) { 248 | watchSCSS(); 249 | } else { 250 | compileAll(); 251 | } -------------------------------------------------------------------------------- /view/frontend/templates/product/view/gallery.phtml: -------------------------------------------------------------------------------- 1 | 14 | 15 | getProduct(); 17 | $images = $block->getGalleryImages()->getItems(); 18 | $mainImage = current(array_filter($images, function ($img) use ($block) { 19 | return $block->isMainImage($img); 20 | })); 21 | 22 | if (!empty($images) && empty($mainImage)) { 23 | $mainImage = $block->getGalleryImages()->getFirstItem(); 24 | } 25 | 26 | // Function to encode an image as Base64 27 | function imageToBase64($imagePath) 28 | { 29 | if (file_exists($imagePath)) { 30 | $imageData = file_get_contents($imagePath); 31 | $base64 = base64_encode($imageData); 32 | $mimeType = mime_content_type($imagePath); // Get MIME type 33 | return "data:$mimeType;base64,$base64"; 34 | } 35 | return ""; 36 | } 37 | 38 | 39 | $helper = $block->getData('imageHelper'); 40 | $mainImageData = $mainImage ? 41 | $mainImage->getData('medium_image_url') : 42 | $helper->getDefaultPlaceholderUrl('image'); 43 | $imageWidth = $block->getImageAttribute('product_page_image_medium', 'width'); 44 | $imageHeight = $block->getImageAttribute('product_page_image_medium', 'height'); 45 | $mobileImage = $helper->init($product, 'product_page_image_small')->resize(365, 260)->keepAspectRatio(true)->setQuality(90); 46 | $mobileImageUrl = $mobileImage->getUrl(); 47 | $base64Image = true; //wins 0.1-0.2s to LCP 48 | if ($base64Image) { 49 | $mobileImagePath = BP . '/pub/' . parse_url($mobileImageUrl, PHP_URL_PATH); 50 | $mobileImageUrl = imageToBase64($mobileImagePath); 51 | } 52 | 53 | $gallaryData = $block->getGalleryImagesJson(); 54 | 55 | $images = json_decode($gallaryData, true); 56 | 57 | ?> 58 | 59 | 60 | 85 | 86 | 87 | 91 | 92 | 163 | 164 | 237 | 264 | 265 | 306 | -------------------------------------------------------------------------------- /view/frontend/templates/product/list.phtml: -------------------------------------------------------------------------------- 1 | 9 | 18 | getLoadedProductCollection(); 20 | /** @var \Magento\Catalog\Helper\Output $_helper */ 21 | $_helper = $block->getData('outputHelper'); 22 | ?> 23 | count()): ?> 24 |
25 |
escapeHtml(__('We can\'t find products matching the selection.')) ?>
26 |
27 | 28 | getToolbarHtml() ?> 29 | getAdditionalHtml() ?> 30 | getMode() === 'grid') { 32 | $viewMode = 'grid'; 33 | $imageDisplayArea = 'category_page_grid'; 34 | $showDescription = false; 35 | $templateType = \Magento\Catalog\Block\Product\ReviewRendererInterface::SHORT_VIEW; 36 | } else { 37 | $viewMode = 'list'; 38 | $imageDisplayArea = 'category_page_list'; 39 | $showDescription = true; 40 | $templateType = \Magento\Catalog\Block\Product\ReviewRendererInterface::FULL_VIEW; 41 | } 42 | /** 43 | * Position for actions regarding image size changing in vde if needed 44 | */ 45 | $pos = $block->getPositioned(); 46 | ?> 47 |
49 |
    50 | 51 | 52 | getProductUrl(); 55 | if (empty($productUrl) || $productUrl === '#') { 56 | $productUrl = $block->getUrl('catalog/product/view', ['id' => $_product->getId()]); 57 | } 58 | ?> 59 |
  1. 60 |
    63 | getImage($_product, $imageDisplayArea); 65 | if ($pos != null) { 66 | $position = 'left:' . $productImage->getWidth() . 'px;' 67 | . 'top:' . $productImage->getHeight() . 'px;'; 68 | } 69 | ?> 70 | 71 | 74 | toHtml() ?> 75 | 76 |
    77 | stripTags($_product->getName(), null, true); ?> 78 | 79 | 81 | productAttribute($_product, $_product->getName(), 'name')?> 82 | 83 | 84 | getReviewsSummaryHtml($_product, $templateType) ?> 85 | getProductPrice($_product) ?> 86 | 87 | getProductDetailsHtml($_product) ?> 88 | 89 |
    90 |
    91 |
    92 | isSaleable()):?> 93 | getAddToCartPostParams($_product); ?> 94 |
    99 | getData('viewModel')->getOptionsData($_product); ?> 100 | 101 | 104 | 105 | 108 | 113 | getBlockHtml('formkey') ?> 114 | 120 |
    121 | 122 | isAvailable()):?> 123 |
    124 | escapeHtml(__('In stock')) ?>
    125 | 126 |
    127 | escapeHtml(__('Out of stock')) ?>
    128 | 129 | 130 |
    131 | renderStyleAsTag( 133 | $position, 134 | 'product-item-info_' . $_product->getId() . ' div.actions-primary' 135 | ) : '' ?> 136 |
    137 | getChildBlock('addto')): ?> 138 | setProduct($_product)->getChildHtml() ?> 139 | 140 |
    141 | renderStyleAsTag( 143 | $position, 144 | 'product-item-info_' . $_product->getId() . ' div.actions-secondary' 145 | ) : '' ?> 146 |
    147 | 148 |
    149 | productAttribute( 150 | $_product, 151 | $_product->getShortDescription(), 152 | 'short_description' 153 | ) ?> 154 | escapeHtml(__('Learn More')) ?> 157 |
    158 | 159 |
    160 |
    161 |
    162 | renderStyleAsTag( 164 | $position, 165 | 'product-item-info_' . $_product->getId() . ' div.product-item-actions' 166 | ) : '' ?> 167 |
  2. 168 | 169 |
170 |
171 | getChildBlock('toolbar')->setIsBottom(true)->toHtml() ?> 172 | 173 | 174 | -------------------------------------------------------------------------------- /view/frontend/templates/product/listing/renderer.phtml: -------------------------------------------------------------------------------- 1 | getProduct() 14 | ?> 15 | isAvailable()): ?> 16 | getId() ?> 17 | 18 | getConfigurableViewModel() ?> 19 |
22 | ".product-item-details", 24 | "onlySwatches" => true, 25 | "enableControlLabel" => false, 26 | "numberToShow" => json_decode($block->escapeJs($block->getNumberSwatchesPerProduct()), true), 27 | "jsonConfig"=> json_decode($block->getJsonConfig(), true), 28 | "jsonSwatchConfig"=> json_decode($block->getJsonSwatchConfig(), true), 29 | "mediaCallback" => $block->getMediaCallback(), 30 | "jsonSwatchImageSizeConfig" => json_decode($block->getJsonSwatchSizeConfig(), true), 31 | "showTooltip"=> json_decode($block->escapeJs($configurableViewModel->getShowSwatchTooltip()), true)]; 32 | 33 | $configurableImages = $swatchData["jsonConfig"]; 34 | $swatchConfig = $swatchData["jsonConfig"]; 35 | $swatchAttributes = $swatchData["jsonSwatchConfig"]; 36 | $mediaUrl = $swatchData["mediaCallback"]; 37 | ?> 38 | 40 | { 41 | "[data-role=priceBox][data-price-box=product-id-escapeJs($productId) ?>]": { 42 | "priceBox": { 43 | "priceConfig": { 44 | "priceFormat": getPriceFormatJson() ?>, 45 | "prices": getPricesJson() ?> 46 | } 47 | } 48 | } 49 | } 50 | 51 | */ 52 | ?> 53 | 54 | 55 | $options) { 62 | foreach ($options as $option) { 63 | $optionToImage[$option] = $imageId; 64 | } 65 | } 66 | 67 | 68 | // Get category image from Magento request object 69 | $request = $block->getRequest(); 70 | $catImage = $request->getParam('cat_image') ?? ''; 71 | 72 | // Store configurable images data for JavaScript 73 | $configurableImagesData = []; 74 | foreach ($configurableImages as $productId => $images) { 75 | $configurableImagesData[$productId] = $images; 76 | } 77 | $lockBlock = $block->getLayout()->getBlock('header'); 78 | $lock = $lockBlock->getData('print') ?? false; 79 | if (!$lock) { 80 | ?> 81 | 142 | 143 |
144 | 145 | 146 |
147 |
148 |
149 | 150 | 151 | $swatchData): ?> 152 |
155 | 156 | escapeHtml($swatchData['label'])?> 158 | 159 | 160 |
161 | $optionData): ?> 162 | 167 | getProductUrl(); 170 | if (empty($productUrl) || $productUrl === '#') { 171 | $productUrl = $block->getUrl('catalog/product/view', ['id' => $product->getId()]); 172 | } 173 | ?> 174 | 175 | 176 | 177 |
183 | 199 | onclick="loadSwatchImages()" 200 | 201 | style="escapeHtmlAttr($optionData['value']) . ' no-repeat center;' : ''?>"> 202 | escapeHtml($optionData['value']) : ''?> 203 |
204 | 205 |
206 | 207 | 208 |
209 | 210 |
211 | 212 | 213 |
214 |
215 |
216 | 217 | 218 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The React-Luma Module is a Faster and Free Open-Source Magento 2 Luma or another Theme Optimiser and Hyva Theme Alternative. Actually, you don’t even need to change the Theme. It works as a composer module to improve your existing theme without re-platforming to Hyva, or it can be used to improve Hyva. 2 | 3 | React-Luma is 20% faster than any M2 front-end, including Hyva, today! 4 | 5 | 100% Vanilla.JS(no framework was used to improve performance) and default magento CSS without LESS compilation. However, it can be easily extended by any JS (ReactJS, VueJS) or CSS(Tilewind) library of your choice. 6 | 7 | image 8 | 9 | ## CSS Deployment Guide 10 | 11 | **Known issue**: The CSS files are not loading correctly on the frontend. The layout is broken due to missing or non-deployed static CSS assets. 12 | It is because not deployed React-Luma optimised CSS. 13 | If specific CSS files from the extension are missing, manually copy them: 14 | ‘’’ 15 | cp -R {path to extension}/pub/static/* pub/static/ 16 | ‘’’ 17 | 18 | ## SCSS to CSS Compilation Manual 19 | To compile SCSS files in React-Luma, navigate to the module directory (vendor/genaker/magento-reactjs) and run node css-compile.js. This script automatically finds all .scss files in pub/static/, compiles them to minified CSS using Sass and PostCSS with autoprefixer and cssnano, and outputs detailed statistics including file sizes and selector counts. The compiled .min.css files are saved in the same directory, ready for production use. Ensure Node.js is installed and run npm install first to install dependencies (sass, postcss, autoprefixer, cssnano, glob). 20 | 21 | ## CSS Purge Tool 22 | The project includes a powerful CSS purging tool that removes unused CSS selectors to optimize file sizes and improve performance. The tool uses PurgeCSS with support for URL fetching, local content scanning, and advanced configuration options including ignore patterns and blocklists. To use the CSS purge tool, navigate to `vendor/genaker/magento-reactjs/` and run `node css-purge.js --css path/to/your/file.css`. The tool supports file-specific configurations via `purge.json`, can fetch content from URLs using `--url` parameters, scan local files with `--path` parameters, and apply custom configurations with `--config`. It automatically removes development classes (`.debug-*`, `.test-*`) and deprecated code (`.deprecated-*`, `.old-*`) while preserving critical selectors through safelists. For detailed usage instructions and examples, see the comprehensive documentation in `vendor/genaker/magento-reactjs/PURGE_README.md`. 23 | 24 | # Video overview of the React-Luma 25 | 26 | Video is here -> https://www.youtube.com/watch?v=TJPNAgDCkWk 27 | 28 | # Have you questions about integrating ReactJS or VueJS with Magento 2 frontend? This Magento module will help you 29 | React Magento 2 implementation. This module explains how to add and use ReactJS or any other framework micro-frontend UI Components with Magento 2 and forget about Knockout/JQuery Magento 2 UI without migration to a new theme(Works with existing theme and designs). Checkout, admin, customer account, and any other part of your store can work using legacy Magento 2 JS implementation 30 | 31 | # No Magento CSS and Magento JS 32 | Enable only CSS Junk settings and 33 | ``` 34 | php bin/magento config:set dev/js/move_script_to_bottom 1 35 | ``` 36 | 37 | ![React + Magento 2](https://github.com/Genaker/reactmagento2/blob/master/KnockoutMagento2React.png) 38 | 39 | # Updates: 40 | - VueJS implementation 41 | - Magento Configuration enable React, VueJS 42 | - Remove Magento's default JS Junk (Require, Knockout, jQuery) configuration. You will need to implement the required functionality or use Magento Open Source ReactJS Luma Theme 43 | - CSS optimizer feature added. Add optimized CSS files to pub/static/styles-(l|m).css and replace the default Magento one 44 | - Magento ReactJS Luma theme released 45 | - React-Luma module. Just install the module and optimize the theme 46 | 47 | # Magento Blazing-Fast React Luma Theme Implementation: 48 | 49 | magento react theme 50 |
51 | Repo is here: https://github.com/Genaker/Luma-React-PWA-Magento-Theme 52 | 53 | # About the Magento 2 React implementation module 54 | You can add any modern library to extend the magento feature. React-Luma has basic magento features implemented using native JS without dependencie.s 55 | It is not a PWA or headless implementation, which is impossible to use with an existing website. Also, Single Page Application (SPA) PWA Magento 2 implementations have issues with Magento 2 API performance - too slow. This implementation is High-Performance integration with magento2 (with Magento 1 also easy to use) it uses inline JSON directly from the page. The same approach is used in Magento 2 backend and frontend checkout, color swatches by default. Also can use Ajax HTTP call to fetch data (not the best solution Magento API is slow and will increase the load on your backend server). You can also use my future project, "Microservices Magento," to fetch data. 56 | 57 | 58 | # CSS improvements 59 | 60 | You can also replace Magento CSS with your custom CSS files if you put them compiled into pub/static 61 | ``` 62 | $optimisedCSSFilePath = BP . '/pub/static/styles-m.css'; 63 | ``` 64 | 65 | ## Store specific styles 66 | 67 | The module supports store-specific CSS files for multi-store setups. By default, optimized CSS files are located in `/pub/static/` for the default store. For other stores, CSS files should be placed in `/pub/static/{store_code}/` directory. For example: 68 | - Default store: `/pub/static/product-styles-m.css` 69 | - French store (`fr`): `/pub/static/fr/product-styles-m.css` 70 | - German store (`de`): `/pub/static/de/product-styles-m.css` 71 | 72 | The module automatically detects the current store code and loads the appropriate CSS files. If store-specific files don't exist, it falls back to the default store files. This allows you to customize styles per store while maintaining a fallback mechanism. 73 | 74 | 75 | # VueJS support 76 | 77 | ![Logo-Vuejs](https://user-images.githubusercontent.com/9213670/150036919-3486e016-3d37-4ffd-b4ee-a3a3bbc961e9.png) 78 | 79 | VueJS is a progressive framework for building user interfaces. Unlike Magento 2 UI monolithic component, Vue is designed from the ground up to be incrementally adoptable. The core library is focused on the view layer only, and is easy to pick up and integrate with other libraries or existing projects. 80 | 81 | ## JS libraries footprint: 82 | * ReactJS 17 - 11KB 83 | * ReactDOM - 120KB 84 | * Preact - 12KB 85 | * HTML for Preact - 1KB 86 | * RequireJS - 17KB 87 | * VueJS - 94KB 88 | * KnockoutJS - 67KB 89 | * jQuery - 89Kb 90 | * AlpineJS - 34KB 91 | 92 | ## My thougts About VUE.js PWA and Magento 2 93 | 94 | Read this article: https://blog.vuestorefront.io/yehor-shytikov-pwa-is-a-real-revolution-not-only-in-ecommerce/ 95 | 96 | Magento 2 has a better framework optimization from a development perspective. It allows developers to use the dependency injection, plugin system (**which is considered harmful AOP software development principle** read: https://yegorshytikov.medium.com/magento-2-plug-ins-aod-architecture-are-harmful-dc23c4edb534), and XML notations for layout. I personally like the folder structure in which one directory is one module. Magento 1 was messy since one module contains several folders. 97 | 98 | Magento 2 uses a modern Symphony approach but it still has a lot of legacy code. Even though it introduces a new way of embracing front-end development, however from a nowadays perspective it is not enough(legacy). No wonder as Magento 2 was released before Vue.js and React took the world popularity. These JS frameworks add more features for developers and - in general - provide more possibilities. 99 | 100 | # Install Magento ReactJS/VueJS extension via composer: 101 | ``` 102 | composer require genaker/react-luma 103 | ``` 104 | # Deploy static 105 | React Luma doesn't use native magento Less generated CSS. Instead you need copy pre-optimised style from module *pub/static* content(CSS) to the root *pub/static* 106 | 107 | # Disable dafault Adobe's broken Magento 2 KnockoutJS UI Components from the frontend 108 | 109 | ![image](https://user-images.githubusercontent.com/9213670/154380709-8a0eaf05-266a-4aa6-9c1c-d79653dba38d.png) 110 | 111 | 112 | 113 | # Magento 2 Admin module built with ReactJS instead of the legacy default JS: 114 | 115 | (https://github.com/Genaker/Magento2OPcacheGUI) 116 | 117 | 118 | # How to use WebPack with Magento 2 119 | 120 | Install Node.JS (https://github.com/nodesource/distributions/blob/master/README.md) From the extension root (React/React) folder run: 121 | 122 | ``` 123 | npm install 124 | npm start 125 | ``` 126 | 127 | Web Puck compiles everything automatically into React/React/view/base/web/js/index_bundle.js and deploys to pub/static without running static:deploy ssh command. LiveReload Plugin will reload Magento 2 pages automatically (not recommended solution by React Community. F5 more reliable solution). What you need just disable the Cache of your browser during development. Also, You can disable caching for single react bundle files via Nginx config. 128 | 129 | # Easy deployment 130 | 131 | Usage of the web-pack sometimes is too difficult for Magento developers. I have created another way to use React with Magento without any compilation/complication. 132 | Simply add these files to the Magento installation: 133 | ``` 134 | // React JS itself 135 | 136 | // Babel to avoid compilation 137 | 138 | 139 | //Add to the page to render React component 140 |
141 | 142 | //Write you scripts using babel 143 | 159 | ``` 160 | 161 | # Magento 2 live reload 162 | 163 | This project aims to solve the case where you want assets served by your Magento app server, but still want reloads triggered from web packs build pipeline. 164 | 165 | Add a script tag to your page pointed at the livereload server 166 | 167 | ``` 168 | 169 | ``` 170 | For development purposes, better disable browser caching (https://www.technipages.com/google-chrome-how-to-completely-disable-cache) 171 | 172 | Disable react bundle caching for development purposes using Nginx (not tested yet): 173 | 174 | ``` 175 | location ~ index_bundle\.js { 176 | add_header Cache-Control no-cache; 177 | expires 0; 178 | } 179 | ``` 180 | --------------------------------------------------------------------------------