diff --git a/.gitignore b/.gitignore index fc2e0f7d09..9c1d86dc0f 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ nbproject/ build .wp-env.override.json *.min.js +*.min.css *.asset.php ############ diff --git a/package-lock.json b/package-lock.json index d3f187bf11..012c513631 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@wordpress/scripts": "^30.5.1", "commander": "12.1.0", "copy-webpack-plugin": "^12.0.2", + "css-minimizer-webpack-plugin": "^7.0.0", "fast-glob": "^3.3.2", "fs-extra": "^11.2.0", "husky": "^9.1.7", @@ -7294,9 +7295,9 @@ "dev": true }, "node_modules/css-declaration-sorter": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.1.1.tgz", - "integrity": "sha512-dZ3bVTEEc1vxr3Bek9vGwfB5Z6ESPULhcRvO472mfjVnj8jRcTnKO8/JTczlvxM10Myb+wBM++1MtdO76eWcaQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz", + "integrity": "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==", "dev": true, "engines": { "node": "^14 || ^16 || >=18" @@ -7373,6 +7374,636 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/css-minimizer-webpack-plugin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-7.0.0.tgz", + "integrity": "sha512-niy66jxsQHqO+EYbhPuIhqRQ1mNcNVUHrMnkzzir9kFOERJUaQDDRhh7dKDz33kBpkWMF9M8Vx0QlDbc5AHOsw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "cssnano": "^7.0.1", + "jest-worker": "^29.7.0", + "postcss": "^8.4.38", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@parcel/css": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "lightningcss": { + "optional": true + } + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/cssnano": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.0.6.tgz", + "integrity": "sha512-54woqx8SCbp8HwvNZYn68ZFAepuouZW4lTwiMVnBErM3VkO7/Sd4oTOt3Zz3bPx3kxQ36aISppyXj2Md4lg8bw==", + "dev": true, + "dependencies": { + "cssnano-preset-default": "^7.0.6", + "lilconfig": "^3.1.2" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/cssnano-preset-default": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.6.tgz", + "integrity": "sha512-ZzrgYupYxEvdGGuqL+JKOY70s7+saoNlHSCK/OGn1vB2pQK8KSET8jvenzItcY+kA7NoWvfbb/YhlzuzNKjOhQ==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^5.0.0", + "postcss-calc": "^10.0.2", + "postcss-colormin": "^7.0.2", + "postcss-convert-values": "^7.0.4", + "postcss-discard-comments": "^7.0.3", + "postcss-discard-duplicates": "^7.0.1", + "postcss-discard-empty": "^7.0.0", + "postcss-discard-overridden": "^7.0.0", + "postcss-merge-longhand": "^7.0.4", + "postcss-merge-rules": "^7.0.4", + "postcss-minify-font-values": "^7.0.0", + "postcss-minify-gradients": "^7.0.0", + "postcss-minify-params": "^7.0.2", + "postcss-minify-selectors": "^7.0.4", + "postcss-normalize-charset": "^7.0.0", + "postcss-normalize-display-values": "^7.0.0", + "postcss-normalize-positions": "^7.0.0", + "postcss-normalize-repeat-style": "^7.0.0", + "postcss-normalize-string": "^7.0.0", + "postcss-normalize-timing-functions": "^7.0.0", + "postcss-normalize-unicode": "^7.0.2", + "postcss-normalize-url": "^7.0.0", + "postcss-normalize-whitespace": "^7.0.0", + "postcss-ordered-values": "^7.0.1", + "postcss-reduce-initial": "^7.0.2", + "postcss-reduce-transforms": "^7.0.0", + "postcss-svgo": "^7.0.1", + "postcss-unique-selectors": "^7.0.3" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/cssnano-utils": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.0.tgz", + "integrity": "sha512-Uij0Xdxc24L6SirFr25MlwC2rCFX6scyUmuKpzI+JQ7cyqDEwD42fJ0xfB3yLfOnRDU5LKGgjQ9FA6LYh76GWQ==", + "dev": true, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-calc": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.0.2.tgz", + "integrity": "sha512-DT/Wwm6fCKgpYVI7ZEWuPJ4az8hiEHtCUeYjZXqU7Ou4QqYh1Df2yCQ7Ca6N7xqKPFkxN3fhf+u9KSoOCJNAjg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.1.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12 || ^20.9 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.38" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-colormin": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.2.tgz", + "integrity": "sha512-YntRXNngcvEvDbEjTdRWGU606eZvB5prmHG4BF0yLmVpamXbpsRJzevyy6MZVyuecgzI2AWAlvFi8DAeCqwpvA==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-api": "^3.0.0", + "colord": "^2.9.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-convert-values": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.4.tgz", + "integrity": "sha512-e2LSXPqEHVW6aoGbjV9RsSSNDO3A0rZLCBxN24zvxF25WknMPpX8Dm9UxxThyEbaytzggRuZxaGXqaOhxQ514Q==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-discard-comments": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.3.tgz", + "integrity": "sha512-q6fjd4WU4afNhWOA2WltHgCbkRhZPgQe7cXF74fuVB/ge4QbM9HEaOIzGSiMvM+g/cOsNAUGdf2JDzqA2F8iLA==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.1.2" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-discard-duplicates": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.1.tgz", + "integrity": "sha512-oZA+v8Jkpu1ct/xbbrntHRsfLGuzoP+cpt0nJe5ED2FQF8n8bJtn7Bo28jSmBYwqgqnqkuSXJfSUEE7if4nClQ==", + "dev": true, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-discard-empty": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.0.tgz", + "integrity": "sha512-e+QzoReTZ8IAwhnSdp/++7gBZ/F+nBq9y6PomfwORfP7q9nBpK5AMP64kOt0bA+lShBFbBDcgpJ3X4etHg4lzA==", + "dev": true, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-discard-overridden": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.0.tgz", + "integrity": "sha512-GmNAzx88u3k2+sBTZrJSDauR0ccpE24omTQCVmaTTZFz1du6AasspjaUPMJ2ud4RslZpoFKyf+6MSPETLojc6w==", + "dev": true, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-merge-longhand": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.4.tgz", + "integrity": "sha512-zer1KoZA54Q8RVHKOY5vMke0cCdNxMP3KBfDerjH/BYHh4nCIh+1Yy0t1pAEQF18ac/4z3OFclO+ZVH8azjR4A==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^7.0.4" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-merge-rules": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.4.tgz", + "integrity": "sha512-ZsaamiMVu7uBYsIdGtKJ64PkcQt6Pcpep/uO90EpLS3dxJi6OXamIobTYcImyXGoW0Wpugh7DSD3XzxZS9JCPg==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^5.0.0", + "postcss-selector-parser": "^6.1.2" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-minify-font-values": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.0.tgz", + "integrity": "sha512-2ckkZtgT0zG8SMc5aoNwtm5234eUx1GGFJKf2b1bSp8UflqaeFzR50lid4PfqVI9NtGqJ2J4Y7fwvnP/u1cQog==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-minify-gradients": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.0.tgz", + "integrity": "sha512-pdUIIdj/C93ryCHew0UgBnL2DtUS3hfFa5XtERrs4x+hmpMYGhbzo6l/Ir5de41O0GaKVpK1ZbDNXSY6GkXvtg==", + "dev": true, + "dependencies": { + "colord": "^2.9.3", + "cssnano-utils": "^5.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-minify-params": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.2.tgz", + "integrity": "sha512-nyqVLu4MFl9df32zTsdcLqCFfE/z2+f8GE1KHPxWOAmegSo6lpV2GNy5XQvrzwbLmiU7d+fYay4cwto1oNdAaQ==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3", + "cssnano-utils": "^5.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-minify-selectors": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.0.4.tgz", + "integrity": "sha512-JG55VADcNb4xFCf75hXkzc1rNeURhlo7ugf6JjiiKRfMsKlDzN9CXHZDyiG6x/zGchpjQS+UAgb1d4nqXqOpmA==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "postcss-selector-parser": "^6.1.2" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-charset": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.0.tgz", + "integrity": "sha512-ABisNUXMeZeDNzCQxPxBCkXexvBrUHV+p7/BXOY+ulxkcjUZO0cp8ekGBwvIh2LbCwnWbyMPNJVtBSdyhM2zYQ==", + "dev": true, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-display-values": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.0.tgz", + "integrity": "sha512-lnFZzNPeDf5uGMPYgGOw7v0BfB45+irSRz9gHQStdkkhiM0gTfvWkWB5BMxpn0OqgOQuZG/mRlZyJxp0EImr2Q==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-positions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.0.tgz", + "integrity": "sha512-I0yt8wX529UKIGs2y/9Ybs2CelSvItfmvg/DBIjTnoUSrPxSV7Z0yZ8ShSVtKNaV/wAY+m7bgtyVQLhB00A1NQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-repeat-style": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.0.tgz", + "integrity": "sha512-o3uSGYH+2q30ieM3ppu9GTjSXIzOrRdCUn8UOMGNw7Af61bmurHTWI87hRybrP6xDHvOe5WlAj3XzN6vEO8jLw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-string": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.0.tgz", + "integrity": "sha512-w/qzL212DFVOpMy3UGyxrND+Kb0fvCiBBujiaONIihq7VvtC7bswjWgKQU/w4VcRyDD8gpfqUiBQ4DUOwEJ6Qg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-timing-functions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.0.tgz", + "integrity": "sha512-tNgw3YV0LYoRwg43N3lTe3AEWZ66W7Dh7lVEpJbHoKOuHc1sLrzMLMFjP8SNULHaykzsonUEDbKedv8C+7ej6g==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-unicode": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.2.tgz", + "integrity": "sha512-ztisabK5C/+ZWBdYC+Y9JCkp3M9qBv/XFvDtSw0d/XwfT3UaKeW/YTm/MD/QrPNxuecia46vkfEhewjwcYFjkg==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-url": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.0.tgz", + "integrity": "sha512-+d7+PpE+jyPX1hDQZYG+NaFD+Nd2ris6r8fPTBAjE8z/U41n/bib3vze8x7rKs5H1uEw5ppe9IojewouHk0klQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-whitespace": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.0.tgz", + "integrity": "sha512-37/toN4wwZErqohedXYqWgvcHUGlT8O/m2jVkAfAe9Bd4MzRqlBmXrJRePH0e9Wgnz2X7KymTgTOaaFizQe3AQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-ordered-values": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.1.tgz", + "integrity": "sha512-irWScWRL6nRzYmBOXReIKch75RRhNS86UPUAxXdmW/l0FcAsg0lvAXQCby/1lymxn/o0gVa6Rv/0f03eJOwHxw==", + "dev": true, + "dependencies": { + "cssnano-utils": "^5.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-reduce-initial": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.2.tgz", + "integrity": "sha512-pOnu9zqQww7dEKf62Nuju6JgsW2V0KRNBHxeKohU+JkHd/GAH5uvoObqFLqkeB2n20mr6yrlWDvo5UBU5GnkfA==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-reduce-transforms": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.0.tgz", + "integrity": "sha512-pnt1HKKZ07/idH8cpATX/ujMbtOGhUfE+m8gbqwJE05aTaNw8gbo34a2e3if0xc0dlu75sUOiqvwCGY3fzOHew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-svgo": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.0.1.tgz", + "integrity": "sha512-0WBUlSL4lhD9rA5k1e5D8EN5wCEyZD6HJk0jIvRxl+FDVOMlJ7DePHYWGGVc5QRqrJ3/06FTXM0bxjmJpmTPSA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^3.3.2" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >= 18" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-unique-selectors": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.3.tgz", + "integrity": "sha512-J+58u5Ic5T1QjP/LDV9g3Cx4CNOgB5vz+kM6+OxHHhFACdcDeKhBXjQmB7fnIZM12YSTvsL0Opwco83DmacW2g==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.1.2" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/stylehacks": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.4.tgz", + "integrity": "sha512-i4zfNrGMt9SB4xRK9L83rlsFCgdGANfeDAYacO1pkqcE7cRHPdWHwnKZVz7WY17Veq/FvyYsRAU++Ga+qDFIww==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3", + "postcss-selector-parser": "^6.1.2" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -19038,9 +19669,9 @@ "dev": true }, "node_modules/svgo": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.2.0.tgz", - "integrity": "sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", "dev": true, "dependencies": { "@trysound/sax": "0.2.0", diff --git a/package.json b/package.json index d60cc8ea9a..78dac054d4 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@wordpress/scripts": "^30.5.1", "commander": "12.1.0", "copy-webpack-plugin": "^12.0.2", + "css-minimizer-webpack-plugin": "^7.0.0", "fast-glob": "^3.3.2", "fs-extra": "^11.2.0", "husky": "^9.1.7", diff --git a/plugins/embed-optimizer/load.php b/plugins/embed-optimizer/load.php index 58aa08cb7d..7dc81d13d7 100644 --- a/plugins/embed-optimizer/load.php +++ b/plugins/embed-optimizer/load.php @@ -76,24 +76,6 @@ static function ( string $version ): void { return; } - if ( - ( is_admin() || ( defined( 'WP_CLI' ) && WP_CLI ) ) && - ! file_exists( __DIR__ . '/detect.min.js' ) - ) { - // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error - trigger_error( - esc_html( - sprintf( - /* translators: 1: File path. 2: CLI command. */ - '[Embed Optimizer] ' . __( 'Unable to load %1$s. Please make sure you have run %2$s.', 'embed-optimizer' ), - 'detect.min.js', - '`npm install && npm run build:plugin:embed-optimizer`' - ) - ), - E_USER_ERROR - ); - } - define( 'EMBED_OPTIMIZER_VERSION', $version ); // Load in the Embed Optimizer plugin hooks. diff --git a/plugins/image-prioritizer/class-image-prioritizer-background-image-styled-tag-visitor.php b/plugins/image-prioritizer/class-image-prioritizer-background-image-styled-tag-visitor.php index 798d42f1f6..a778f867dc 100644 --- a/plugins/image-prioritizer/class-image-prioritizer-background-image-styled-tag-visitor.php +++ b/plugins/image-prioritizer/class-image-prioritizer-background-image-styled-tag-visitor.php @@ -19,6 +19,22 @@ */ final class Image_Prioritizer_Background_Image_Styled_Tag_Visitor extends Image_Prioritizer_Tag_Visitor { + /** + * Class name used to indicate a background image which is lazy-loaded. + * + * @since n.e.x.t + * @var string + */ + const LAZY_BG_IMAGE_CLASS_NAME = 'od-lazy-bg-image'; + + /** + * Whether the lazy-loading script and stylesheet have been added. + * + * @since n.e.x.t + * @var bool + */ + private $added_lazy_assets = false; + /** * Visits a tag. * @@ -71,6 +87,43 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { ); } + $this->lazy_load_bg_images( $context ); + return true; } + + /** + * Optimizes an element with a background image based on whether it is displayed in any initial viewport. + * + * @since n.e.x.t + * + * @param OD_Tag_Visitor_Context $context Tag visitor context, with the cursor currently at block with a background image. + */ + private function lazy_load_bg_images( OD_Tag_Visitor_Context $context ): void { + $processor = $context->processor; + + // Lazy-loading can only be done once there are URL Metrics collected for both mobile and desktop. + if ( + $context->url_metric_group_collection->get_first_group()->count() === 0 + || + $context->url_metric_group_collection->get_last_group()->count() === 0 + ) { + return; + } + + $xpath = $processor->get_xpath(); + + // If the element is in the initial viewport, do not lazy load its background image. + if ( false !== $context->url_metric_group_collection->is_element_positioned_in_any_initial_viewport( $xpath ) ) { + return; + } + + $processor->add_class( self::LAZY_BG_IMAGE_CLASS_NAME ); + + if ( ! $this->added_lazy_assets ) { + $processor->append_head_html( sprintf( "\n", image_prioritizer_get_lazy_load_bg_image_stylesheet() ) ); + $processor->append_body_html( wp_get_inline_script_tag( image_prioritizer_get_lazy_load_bg_image_script(), array( 'type' => 'module' ) ) ); + $this->added_lazy_assets = true; + } + } } diff --git a/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php b/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php index 120b9a8af8..1e940fab01 100644 --- a/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php +++ b/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php @@ -241,7 +241,7 @@ private function lazy_load_videos( ?string $poster, OD_Tag_Visitor_Context $cont } if ( ! $this->added_lazy_script ) { - $processor->append_body_html( wp_get_inline_script_tag( image_prioritizer_get_lazy_load_script(), array( 'type' => 'module' ) ) ); + $processor->append_body_html( wp_get_inline_script_tag( image_prioritizer_get_video_lazy_load_script(), array( 'type' => 'module' ) ) ); $this->added_lazy_script = true; } } diff --git a/plugins/image-prioritizer/helper.php b/plugins/image-prioritizer/helper.php index 025e07d4c6..a62677c542 100644 --- a/plugins/image-prioritizer/helper.php +++ b/plugins/image-prioritizer/helper.php @@ -112,3 +112,45 @@ function image_prioritizer_get_asset_path( string $src_path, ?string $min_path = return $min_path; } + +/** + * Gets the script to lazy-load videos. + * + * Load a video and its poster image when it approaches the viewport using an IntersectionObserver. + * + * Handles 'autoplay' and 'preload' attributes accordingly. + * + * @since 0.2.0 + * + * @return string Lazy load script. + */ +function image_prioritizer_get_video_lazy_load_script(): string { + $path = image_prioritizer_get_asset_path( 'lazy-load-video.js' ); + return (string) file_get_contents( __DIR__ . '/' . $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- It's a local filesystem path not a remote request. +} + +/** + * Gets the script to lazy-load background images. + * + * Load the background image when it approaches the viewport using an IntersectionObserver. + * + * @since n.e.x.t + * + * @return string Lazy load script. + */ +function image_prioritizer_get_lazy_load_bg_image_script(): string { + $path = image_prioritizer_get_asset_path( 'lazy-load-bg-image.js' ); + return (string) file_get_contents( __DIR__ . '/' . $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- It's a local filesystem path not a remote request. +} + +/** + * Gets the stylesheet to lazy-load background images. + * + * @since n.e.x.t + * + * @return string Lazy load stylesheet. + */ +function image_prioritizer_get_lazy_load_bg_image_stylesheet(): string { + $path = image_prioritizer_get_asset_path( 'lazy-load-bg-image.css' ); + return (string) file_get_contents( __DIR__ . '/' . $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- It's a local filesystem path not a remote request. +} diff --git a/plugins/image-prioritizer/hooks.php b/plugins/image-prioritizer/hooks.php index 3e16f3e9ae..62d2fd3158 100644 --- a/plugins/image-prioritizer/hooks.php +++ b/plugins/image-prioritizer/hooks.php @@ -11,23 +11,3 @@ } add_action( 'od_init', 'image_prioritizer_init' ); - -/** - * Gets the script to lazy-load videos. - * - * Load a video and its poster image when it approaches the viewport using an IntersectionObserver. - * - * Handles 'autoplay' and 'preload' attributes accordingly. - * - * @since 0.2.0 - */ -function image_prioritizer_get_lazy_load_script(): string { - $path = image_prioritizer_get_asset_path( 'lazy-load.js' ); - $script = file_get_contents( __DIR__ . '/' . $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- It's a local filesystem path not a remote request. - - if ( false === $script ) { - return ''; - } - - return $script; -} diff --git a/plugins/image-prioritizer/lazy-load-bg-image.css b/plugins/image-prioritizer/lazy-load-bg-image.css new file mode 100644 index 0000000000..7f1a66574d --- /dev/null +++ b/plugins/image-prioritizer/lazy-load-bg-image.css @@ -0,0 +1,5 @@ +@media (scripting: enabled) { + .od-lazy-bg-image { + background-image: none !important; + } +} diff --git a/plugins/image-prioritizer/lazy-load-bg-image.js b/plugins/image-prioritizer/lazy-load-bg-image.js new file mode 100644 index 0000000000..d79531f715 --- /dev/null +++ b/plugins/image-prioritizer/lazy-load-bg-image.js @@ -0,0 +1,22 @@ +const lazyBgImageObserver = new IntersectionObserver( + ( entries ) => { + for ( const entry of entries ) { + if ( entry.isIntersecting ) { + const bgImageElement = /** @type {HTMLElement} */ entry.target; + + bgImageElement.classList.remove( 'od-lazy-bg-image' ); + + lazyBgImageObserver.unobserve( bgImageElement ); + } + } + }, + { + rootMargin: '100% 0% 100% 0%', + threshold: 0, + } +); + +const bgImageElements = document.querySelectorAll( '.od-lazy-bg-image' ); +for ( const bgImageElement of bgImageElements ) { + lazyBgImageObserver.observe( bgImageElement ); +} diff --git a/plugins/image-prioritizer/lazy-load.js b/plugins/image-prioritizer/lazy-load-video.js similarity index 100% rename from plugins/image-prioritizer/lazy-load.js rename to plugins/image-prioritizer/lazy-load-video.js diff --git a/plugins/image-prioritizer/load.php b/plugins/image-prioritizer/load.php index 0b386f27c2..8e4ca92334 100644 --- a/plugins/image-prioritizer/load.php +++ b/plugins/image-prioritizer/load.php @@ -77,24 +77,6 @@ static function ( string $version ): void { return; } - if ( - ( is_admin() || ( defined( 'WP_CLI' ) && WP_CLI ) ) && - ! file_exists( __DIR__ . '/lazy-load.min.js' ) - ) { - // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error - trigger_error( - esc_html( - sprintf( - /* translators: 1: File path. 2: CLI command. */ - '[Image Prioritizer] ' . __( 'Unable to load %1$s. Please make sure you have run %2$s.', 'image-prioritizer' ), - 'lazy-load.min.js', - '`npm install && npm run build:plugin:image-prioritizer`' - ) - ), - E_USER_ERROR - ); - } - define( 'IMAGE_PRIORITIZER_VERSION', $version ); require_once __DIR__ . '/helper.php'; diff --git a/plugins/image-prioritizer/readme.txt b/plugins/image-prioritizer/readme.txt index a7925be3ab..37ea1fb918 100644 --- a/plugins/image-prioritizer/readme.txt +++ b/plugins/image-prioritizer/readme.txt @@ -15,12 +15,13 @@ This plugin optimizes the loading of images (and videos) with prioritization, la The current optimizations include: -1. Ensuring `fetchpriority=high` is only added to an `IMG` when it is the Largest Contentful Paint (LCP) element across all responsive breakpoints. -2. Adding breakpoint-specific `fetchpriority=high` preload links for the LCP elements which are `IMG` elements or elements with a CSS `background-image` inline style. -3. Applying lazy-loading to `IMG` tags based on whether they appear in any breakpoint’s initial viewport. (Additionally, [`sizes=auto`](https://make.wordpress.org/core/2024/10/18/auto-sizes-for-lazy-loaded-images-in-wordpress-6-7/) is then also correctly applied.) -4. Adding `fetchpriority=low` to `IMG` tags which appear in the initial viewport but are not visible, such as when they are subsequent carousel slides. -5. Reducing the size of the `poster` image of a `VIDEO` from full size to the size appropriate for the maximum width of the video (on desktop). -6. Lazy-loading `VIDEO` tags by setting the appropriate attributes based on whether they appear in the initial viewport. If a `VIDEO` is the LCP element, it gets `preload=auto`; if it is in an initial viewport, the `preload=metadata` default is left; if it is not in an initial viewport, it gets `preload=none`. Lazy-loaded videos also get initial `preload`, `autoplay`, and `poster` attributes restored when the `VIDEO` is going to enter the viewport. +1. Ensure `fetchpriority=high` is only added to an `IMG` when it is the Largest Contentful Paint (LCP) element across all responsive breakpoints. +2. Add breakpoint-specific `fetchpriority=high` preload links for the LCP elements which are `IMG` elements or elements with a CSS `background-image` inline style. +3. Apply lazy-loading to `IMG` tags based on whether they appear in any breakpoint’s initial viewport. (Additionally, [`sizes=auto`](https://make.wordpress.org/core/2024/10/18/auto-sizes-for-lazy-loaded-images-in-wordpress-6-7/) is then also correctly applied.) +4. Implement lazy-loading of CSS background images added via inline `style` attributes. +5. Add `fetchpriority=low` to `IMG` tags which appear in the initial viewport but are not visible, such as when they are subsequent carousel slides. +6. Reduce the size of the `poster` image of a `VIDEO` from full size to the size appropriate for the maximum width of the video (on desktop). +7. Lazy-load `VIDEO` tags by setting the appropriate attributes based on whether they appear in the initial viewport. If a `VIDEO` is the LCP element, it gets `preload=auto`; if it is in an initial viewport, the `preload=metadata` default is left; if it is not in an initial viewport, it gets `preload=none`. Lazy-loaded videos also get initial `preload`, `autoplay`, and `poster` attributes restored when the `VIDEO` is going to enter the viewport. **This plugin requires the [Optimization Detective](https://wordpress.org/plugins/optimization-detective/) plugin as a dependency.** Please refer to that plugin for additional background on how this plugin works as well as additional developer options. diff --git a/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-on-all-breakpoints-but-not-desktop-with-fully-populated-sample-data.php b/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-on-all-breakpoints-but-not-desktop-with-fully-populated-sample-data.php new file mode 100644 index 0000000000..194d40499f --- /dev/null +++ b/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-on-all-breakpoints-but-not-desktop-with-fully-populated-sample-data.php @@ -0,0 +1,85 @@ + static function ( Test_Image_Prioritizer_Helper $test_case ): void { + $breakpoint_max_widths = array( 480, 600, 782 ); + $sample_size = od_get_url_metrics_breakpoint_sample_size(); + + add_filter( + 'od_breakpoint_max_widths', + static function () use ( $breakpoint_max_widths ) { + return $breakpoint_max_widths; + } + ); + + $outside_viewport_rect = array_merge( + $test_case->get_sample_dom_rect(), + array( + 'top' => 100000, + ) + ); + + foreach ( $breakpoint_max_widths as $non_desktop_viewport_width ) { + for ( $i = 0; $i < $sample_size; $i++ ) { + OD_URL_Metrics_Post_Type::store_url_metric( + od_get_url_metrics_slug( od_get_normalized_query_vars() ), + $test_case->get_sample_url_metric( + array( + 'viewport_width' => $non_desktop_viewport_width, + 'elements' => array( + array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::DIV]', + 'isLCP' => false, + 'intersectionRatio' => 0.0, + 'intersectionRect' => $outside_viewport_rect, + 'boundingClientRect' => $outside_viewport_rect, + ), + ), + ) + ) + ); + } + } + + for ( $i = 0; $i < $sample_size; $i++ ) { + OD_URL_Metrics_Post_Type::store_url_metric( + od_get_url_metrics_slug( od_get_normalized_query_vars() ), + $test_case->get_sample_url_metric( + array( + 'viewport_width' => 1000, + 'elements' => array( + array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::DIV]', + 'isLCP' => false, + 'intersectionRatio' => 0.3, + ), + ), + ) + ) + ); + } + }, + 'buffer' => ' + + + + ... + + +

Pretend this is a super long paragraph that pushes the next div out of the initial viewport except on desktop.

+
This is so background!
+ + + ', + 'expected' => ' + + + + ... + + +

Pretend this is a super long paragraph that pushes the next div out of the initial viewport except on desktop.

+
This is so background!
+ + + ', +); diff --git a/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-with-desktop-metrics-missing.php b/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-with-desktop-metrics-missing.php new file mode 100644 index 0000000000..a167b86ba2 --- /dev/null +++ b/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-with-desktop-metrics-missing.php @@ -0,0 +1,65 @@ + static function ( Test_Image_Prioritizer_Helper $test_case ): void { + $breakpoint_max_widths = array( 480, 600, 782 ); + + add_filter( + 'od_breakpoint_max_widths', + static function () use ( $breakpoint_max_widths ) { + return $breakpoint_max_widths; + } + ); + + $outside_viewport_rect = array_merge( + $test_case->get_sample_dom_rect(), + array( + 'top' => 100000, + ) + ); + + foreach ( $breakpoint_max_widths as $non_desktop_viewport_width ) { + OD_URL_Metrics_Post_Type::store_url_metric( + od_get_url_metrics_slug( od_get_normalized_query_vars() ), + $test_case->get_sample_url_metric( + array( + 'viewport_width' => $non_desktop_viewport_width, + 'elements' => array( + array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::DIV]', + 'isLCP' => false, + 'intersectionRatio' => 0.0, + 'intersectionRect' => $outside_viewport_rect, + 'boundingClientRect' => $outside_viewport_rect, + ), + ), + ) + ) + ); + } + }, + 'buffer' => ' + + + + ... + + +

Pretend this is a super long paragraph that pushes the next div out of the initial viewport.

+
This is so background!
+ + + ', + 'expected' => ' + + + + ... + + +

Pretend this is a super long paragraph that pushes the next div out of the initial viewport.

+
This is so background!
+ + + + ', +); diff --git a/plugins/image-prioritizer/tests/test-cases/common-lcp-background-image-and-lazy-loaded-background-image-outside-viewport-with-fully-populated-sample-data.php b/plugins/image-prioritizer/tests/test-cases/common-lcp-background-image-and-lazy-loaded-background-image-outside-viewport-with-fully-populated-sample-data.php new file mode 100644 index 0000000000..6e2f2f9b7b --- /dev/null +++ b/plugins/image-prioritizer/tests/test-cases/common-lcp-background-image-and-lazy-loaded-background-image-outside-viewport-with-fully-populated-sample-data.php @@ -0,0 +1,67 @@ + static function ( Test_Image_Prioritizer_Helper $test_case ): void { + $outside_viewport_rect = array_merge( + $test_case->get_sample_dom_rect(), + array( + 'top' => 100000, + ) + ); + + $test_case->populate_url_metrics( + array( + array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::DIV]', + 'isLCP' => true, + ), + array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[3][self::DIV]', + 'isLCP' => false, + 'intersectionRatio' => 0.0, + 'intersectionRect' => $outside_viewport_rect, + 'boundingClientRect' => $outside_viewport_rect, + ), + array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[4][self::DIV]', + 'isLCP' => false, + 'intersectionRatio' => 0.0, + 'intersectionRect' => $outside_viewport_rect, + 'boundingClientRect' => $outside_viewport_rect, + ), + ) + ); + }, + 'buffer' => ' + + + + ... + + +
This is so background!
+

Pretend this is a super long paragraph that pushes the next div out of the initial viewport.

+
This is so background!
+
This is so background!
+ + + ', + 'expected' => ' + + + + ... + + + + +
This is so background!
+

Pretend this is a super long paragraph that pushes the next div out of the initial viewport.

+
This is so background!
+
This is so background!
+ + + + ', +); diff --git a/plugins/image-prioritizer/tests/test-cases/common-lcp-background-image-with-fully-populated-sample-data.php b/plugins/image-prioritizer/tests/test-cases/common-lcp-background-image-with-fully-populated-sample-data.php index b18b42520f..5ecd8e7de7 100644 --- a/plugins/image-prioritizer/tests/test-cases/common-lcp-background-image-with-fully-populated-sample-data.php +++ b/plugins/image-prioritizer/tests/test-cases/common-lcp-background-image-with-fully-populated-sample-data.php @@ -21,6 +21,7 @@ ', + // Note: There should be no lazy-loading script or styles added here as the only background image is in the initial viewport. 'expected' => ' diff --git a/plugins/image-prioritizer/tests/test-cases/images-located-above-or-along-initial-viewport.php b/plugins/image-prioritizer/tests/test-cases/images-located-above-or-along-initial-viewport.php index 2cfc863452..be5ceae574 100644 --- a/plugins/image-prioritizer/tests/test-cases/images-located-above-or-along-initial-viewport.php +++ b/plugins/image-prioritizer/tests/test-cases/images-located-above-or-along-initial-viewport.php @@ -3,12 +3,6 @@ 'set_up' => static function ( Test_Image_Prioritizer_Helper $test_case ): void { $slug = od_get_url_metrics_slug( od_get_normalized_query_vars() ); $sample_size = od_get_url_metrics_breakpoint_sample_size(); - $outside_viewport_rect = array_merge( - $test_case->get_sample_dom_rect(), - array( - 'top' => 100000, - ) - ); $get_dom_rect = static function ( $left, $top, $width, $height ) { $dom_rect = array( diff --git a/plugins/image-prioritizer/tests/test-cases/no-lcp-image-with-populated-url-metrics.php b/plugins/image-prioritizer/tests/test-cases/no-lcp-image-or-background-image-outside-viewport-with-populated-url-metrics.php similarity index 100% rename from plugins/image-prioritizer/tests/test-cases/no-lcp-image-with-populated-url-metrics.php rename to plugins/image-prioritizer/tests/test-cases/no-lcp-image-or-background-image-outside-viewport-with-populated-url-metrics.php diff --git a/plugins/image-prioritizer/tests/test-cases/no-url-metrics-with-non-background-image-style.php b/plugins/image-prioritizer/tests/test-cases/no-url-metrics-with-non-background-image-style.php index eaee686ee0..259f010fdc 100644 --- a/plugins/image-prioritizer/tests/test-cases/no-url-metrics-with-non-background-image-style.php +++ b/plugins/image-prioritizer/tests/test-cases/no-url-metrics-with-non-background-image-style.php @@ -12,7 +12,7 @@ ', - // There should be no data-od-xpath added to the DIV because it is using a data: URL for the background-image. + // There should be no data-od-xpath added to the DIV because it is using a background-color style. 'expected' => ' diff --git a/plugins/image-prioritizer/tests/test-cases/no-url-metrics.php b/plugins/image-prioritizer/tests/test-cases/no-url-metrics.php index 422ec38ba7..cd386295b3 100644 --- a/plugins/image-prioritizer/tests/test-cases/no-url-metrics.php +++ b/plugins/image-prioritizer/tests/test-cases/no-url-metrics.php @@ -9,6 +9,8 @@ Foo +

Pretend this is a super long paragraph that pushes the next div out of the initial viewport.

+
This is so background!
', @@ -20,6 +22,8 @@ Foo +

Pretend this is a super long paragraph that pushes the next div out of the initial viewport.

+
This is so background!
diff --git a/plugins/image-prioritizer/tests/test-cases/only-mobile-and-desktop-groups-are-populated.php b/plugins/image-prioritizer/tests/test-cases/only-mobile-and-desktop-groups-are-populated.php index ce2358b94c..76164a562d 100644 --- a/plugins/image-prioritizer/tests/test-cases/only-mobile-and-desktop-groups-are-populated.php +++ b/plugins/image-prioritizer/tests/test-cases/only-mobile-and-desktop-groups-are-populated.php @@ -11,6 +11,13 @@ static function () { $slug = od_get_url_metrics_slug( od_get_normalized_query_vars() ); $sample_size = od_get_url_metrics_breakpoint_sample_size(); + $outside_viewport_rect = array_merge( + $test_case->get_sample_dom_rect(), + array( + 'top' => 100000, + ) + ); + // Populate the mobile and desktop viewport groups only. foreach ( array( 400, 800 ) as $viewport_width ) { for ( $i = 0; $i < $sample_size; $i++ ) { @@ -19,10 +26,19 @@ static function () { $test_case->get_sample_url_metric( array( 'viewport_width' => $viewport_width, - 'element' => array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::MAIN]/*[2][self::ARTICLE]/*[2][self::FIGURE]/*[1][self::IMG]', - 'isLCP' => $viewport_width > 600, - 'intersectionRatio' => $viewport_width > 600 ? 1.0 : 0.1, + 'elements' => array( + array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::MAIN]/*[2][self::ARTICLE]/*[2][self::FIGURE]/*[1][self::IMG]', + 'isLCP' => $viewport_width > 600, + 'intersectionRatio' => $viewport_width > 600 ? 1.0 : 0.1, + ), + array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::MAIN]/*[4][self::DIV]', + 'isLCP' => false, + 'intersectionRatio' => 0.0, + 'intersectionRect' => $outside_viewport_rect, + 'boundingClientRect' => $outside_viewport_rect, + ), ), ) ) @@ -57,6 +73,8 @@ static function () {

This post does have a featured image, and the server-side heuristics in WordPress cause it to get fetchpriority=high, but it should not have this since it is out of the viewport on mobile.

+

Pretend this is a super long paragraph that pushes the next div out of the initial viewport.

+
This is so background!
@@ -66,6 +84,9 @@ static function () { ... + @@ -89,7 +110,10 @@ static function () {

This post does have a featured image, and the server-side heuristics in WordPress cause it to get fetchpriority=high, but it should not have this since it is out of the viewport on mobile.

+

Pretend this is a super long paragraph that pushes the next div out of the initial viewport.

+
This is so background!
+ diff --git a/plugins/image-prioritizer/tests/test-helper.php b/plugins/image-prioritizer/tests/test-helper.php index ac67ac867e..bd646ab98a 100644 --- a/plugins/image-prioritizer/tests/test-helper.php +++ b/plugins/image-prioritizer/tests/test-helper.php @@ -97,6 +97,8 @@ static function ( $matches ) { $matches[1] = '/* import detect ... */'; } elseif ( false !== strpos( $matches[1], 'const lazyVideoObserver' ) ) { $matches[1] = '/* const lazyVideoObserver ... */'; + } elseif ( false !== strpos( $matches[1], 'const lazyBgImageObserver' ) ) { + $matches[1] = '/* const lazyBgImageObserver ... */'; } return implode( '', $matches ); }, @@ -220,4 +222,31 @@ public function test_auto_sizes( array $element_metrics, string $buffer, string "Buffer snapshot:\n$buffer" ); } + + /** + * Test image_prioritizer_get_video_lazy_load_script. + * + * @covers ::image_prioritizer_get_video_lazy_load_script + */ + public function test_image_prioritizer_get_video_lazy_load_script(): void { + $this->assertGreaterThan( 0, strlen( image_prioritizer_get_video_lazy_load_script() ) ); + } + + /** + * Test image_prioritizer_get_lazy_load_bg_image_script. + * + * @covers ::image_prioritizer_get_lazy_load_bg_image_script + */ + public function test_image_prioritizer_get_lazy_load_bg_image_script(): void { + $this->assertGreaterThan( 0, strlen( image_prioritizer_get_lazy_load_bg_image_script() ) ); + } + + /** + * Test image_prioritizer_get_lazy_load_bg_image_stylesheet. + * + * @covers ::image_prioritizer_get_lazy_load_bg_image_stylesheet + */ + public function test_image_prioritizer_get_lazy_load_bg_image_stylesheet(): void { + $this->assertGreaterThan( 0, strlen( image_prioritizer_get_lazy_load_bg_image_stylesheet() ) ); + } } diff --git a/tools/webpack/utils.js b/tools/webpack/utils.js index 6d596e1e0f..1e85f10276 100644 --- a/tools/webpack/utils.js +++ b/tools/webpack/utils.js @@ -2,6 +2,7 @@ const fs = require( 'fs' ); const path = require( 'path' ); const { chdir } = require( 'process' ); const { spawnSync } = require( 'child_process' ); +const CssMinimizerPlugin = require( 'css-minimizer-webpack-plugin' ); /** * Return plugin root path. @@ -99,6 +100,37 @@ const assetDataTransformer = ( content, absoluteFrom ) => { return ` array(), 'version' => '${ version }');`; }; +/** + * Transformer to minify CSS content. + * + * @param {Buffer} content The content as a Buffer of the file being transformed. + * @param {string} absoluteFrom The absolute path to the file being transformed. + * + * @return {Promise} A promise that resolves to the transformed (minified) content. + */ +const cssMinifyTransformer = ( content, absoluteFrom ) => { + const cssContent = content.toString(); + + return Promise.resolve( + CssMinimizerPlugin.cssnanoMinify( + { [ absoluteFrom ]: cssContent }, + undefined, + { + preset: [ + 'default', + { + discardComments: { + removeAll: true, + }, + }, + ], + } + ) + ).then( ( result ) => { + return result.code; + } ); +}; + /** * Create plugins zip file using `zip` command. * @@ -129,5 +161,6 @@ module.exports = { getPluginVersion, generateBuildManifest, assetDataTransformer, + cssMinifyTransformer, createPluginZip, }; diff --git a/webpack.config.js b/webpack.config.js index 2ed13745ca..47744964dc 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -12,6 +12,7 @@ const { plugins: standalonePlugins } = require( './plugins.json' ); const { createPluginZip, assetDataTransformer, + cssMinifyTransformer, deleteFileOrDirectory, generateBuildManifest, } = require( './tools/webpack/utils' ); @@ -132,8 +133,20 @@ const imagePrioritizer = ( env ) => { new CopyWebpackPlugin( { patterns: [ { - from: `${ pluginDir }/lazy-load.js`, - to: `${ pluginDir }/lazy-load.min.js`, + from: `${ pluginDir }/lazy-load-video.js`, + to: `${ pluginDir }/lazy-load-video.min.js`, + }, + { + from: `${ pluginDir }/lazy-load-bg-image.js`, + to: `${ pluginDir }/lazy-load-bg-image.min.js`, + }, + { + from: `${ pluginDir }/lazy-load-bg-image.css`, + to: `${ pluginDir }/lazy-load-bg-image.min.css`, + transform: { + transformer: cssMinifyTransformer, + cache: false, + }, }, ], } ),