diff --git a/.eslintrc.js b/.eslintrc.js index 1810027392..00e45787e2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -25,6 +25,18 @@ const config = { '/dist', '/**/*.min.js', ], + overrides: [ + ...( wpConfig?.overrides || [] ), + { + files: [ 'plugins/view-transitions/js/**/*.js' ], + rules: { + 'jsdoc/no-undefined-types': [ + 'error', + { definedTypes: [ 'PageSwapEvent', 'PageRevealEvent' ] }, + ], + }, + }, + ], }; module.exports = config; diff --git a/composer.lock b/composer.lock index 0fb65c4661..e3fa53409f 100644 --- a/composer.lock +++ b/composer.lock @@ -2132,16 +2132,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.12.2", + "version": "3.13.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "6d4cf6032d4b718f168c90a96e36c7d0eaacb2aa" + "reference": "65ff2489553b83b4597e89c3b8b721487011d186" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/6d4cf6032d4b718f168c90a96e36c7d0eaacb2aa", - "reference": "6d4cf6032d4b718f168c90a96e36c7d0eaacb2aa", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/65ff2489553b83b4597e89c3b8b721487011d186", + "reference": "65ff2489553b83b4597e89c3b8b721487011d186", "shasum": "" }, "require": { @@ -2212,7 +2212,7 @@ "type": "thanks_dev" } ], - "time": "2025-04-13T04:10:18+00:00" + "time": "2025-05-11T03:36:00+00:00" }, { "name": "symfony/polyfill-php73", diff --git a/package-lock.json b/package-lock.json index 0c22b7e9b6..72358abf89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,9 @@ "@octokit/rest": "^21.1.1", "@playwright/test": "^1.51.1", "@wordpress/e2e-test-utils-playwright": "^1.21.0", - "@wordpress/env": "^10.22.0", - "@wordpress/prettier-config": "^4.20.0", - "@wordpress/scripts": "^30.15.0", + "@wordpress/env": "^10.23.0", + "@wordpress/prettier-config": "^4.23.0", + "@wordpress/scripts": "^30.16.0", "commander": "13.1.0", "copy-webpack-plugin": "^13.0.0", "css-minimizer-webpack-plugin": "^7.0.2", @@ -2099,9 +2099,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", - "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, "license": "MIT", "dependencies": { @@ -3006,9 +3006,9 @@ } }, "node_modules/@jest/reporters/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -3753,12 +3753,13 @@ } }, "node_modules/@paulirish/trace_engine": { - "version": "0.0.50", - "resolved": "https://registry.npmjs.org/@paulirish/trace_engine/-/trace_engine-0.0.50.tgz", - "integrity": "sha512-ktkbISnr0T9dkOxtnEadjYsbArMcvX2Wp8zwgyIP6KW0eOk2Oe2s49BY4v0qdE3uQdVv/GDdQ6MnoIFuYNJ9pg==", + "version": "0.0.52", + "resolved": "https://registry.npmjs.org/@paulirish/trace_engine/-/trace_engine-0.0.52.tgz", + "integrity": "sha512-RSIDdpvYRJIaXUSiJfTYxVRtjq3FPjU8FPT5BkpYXS4H7ofExEb4tZBXcqlRoriA8ykVTClgbqabmoI32n5zRQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { + "legacy-javascript": "latest", "third-party-web": "latest" } }, @@ -3786,13 +3787,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.51.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.51.1.tgz", - "integrity": "sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", + "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.51.1" + "playwright": "1.52.0" }, "bin": { "playwright": "cli.js" @@ -4979,9 +4980,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -5136,9 +5137,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -5175,9 +5176,9 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -5416,9 +5417,9 @@ } }, "node_modules/@wordpress/babel-preset-default": { - "version": "8.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/babel-preset-default/-/babel-preset-default-8.22.0.tgz", - "integrity": "sha512-iBPcAtfT6Qo745RBtiKyy6OwKB6qlLusLGE/+2W160oX4oaPlbrJbf37tb3LaYMR/+ncEakiio3eNUlR9wQE9A==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@wordpress/babel-preset-default/-/babel-preset-default-8.23.0.tgz", + "integrity": "sha512-dHUQJIXWsgIIhZHXjpHN53S1fzTy4ZJLk0Wpr29hRQYGrWAx9NSMHZRbaF2Qn5Ma5awe9ID5CIzBic4/pGxKqw==", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -5428,8 +5429,8 @@ "@babel/preset-env": "7.25.7", "@babel/preset-typescript": "7.25.7", "@babel/runtime": "7.25.7", - "@wordpress/browserslist-config": "^6.22.0", - "@wordpress/warning": "^3.22.0", + "@wordpress/browserslist-config": "^6.23.0", + "@wordpress/warning": "^3.23.0", "browserslist": "^4.21.10", "core-js": "^3.31.0", "react": "^18.3.0" @@ -5440,9 +5441,9 @@ } }, "node_modules/@wordpress/base-styles": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/base-styles/-/base-styles-5.22.0.tgz", - "integrity": "sha512-bsuVyCfdmDCIIMq1NdoeFGJzKMkd0qFlqNPeW1Hiz9yKtU+dJmccwrACCUVFydDS9zSVpN1VdI0NerFQMyr5wA==", + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@wordpress/base-styles/-/base-styles-5.23.0.tgz", + "integrity": "sha512-1mtX3jA9el2ZDkAJp7YEN1bX+DzfX0h496uxpRk+evmQJLZxBMPeu5datJFtwkWbVitOsR88WCDvUoNoKJMSuw==", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -5451,9 +5452,9 @@ } }, "node_modules/@wordpress/browserslist-config": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-6.22.0.tgz", - "integrity": "sha512-IrvIkmBSO/DsTQCSWqeds95JN31nl138RvspB93Y5+f9gF5RuZ281HhRas5HbSgPa/SMHRXz6uT4wzNamP2Piw==", + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-6.23.0.tgz", + "integrity": "sha512-AwQLVZ11tCpIWVMaQ3YrjVxDhYCZZjHpLCmEE7CcWWDGl8fFI5r6J696Aw8ImsVYslvCPdIzqXSWDsjxh/gPoA==", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -5462,9 +5463,9 @@ } }, "node_modules/@wordpress/dependency-extraction-webpack-plugin": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/dependency-extraction-webpack-plugin/-/dependency-extraction-webpack-plugin-6.22.0.tgz", - "integrity": "sha512-U1pPk2uxEfyB5x7T5lA9BPh3MSxEJ3lihOU1ULaUMUYC0ef2I+HP0k2CoR9G6SzR4lpJsfFZGxbOctBfFNH9gg==", + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/@wordpress/dependency-extraction-webpack-plugin/-/dependency-extraction-webpack-plugin-6.23.0.tgz", + "integrity": "sha512-D9AMOKwFkEg839uC3u9hybv48j4bRjjY5JCaHcKurOLD7wwQCpubF0Y3XmUf+TMWzFbZzJTzBP0xilerys9DsQ==", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -5486,9 +5487,9 @@ "license": "BSD" }, "node_modules/@wordpress/e2e-test-utils-playwright": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils-playwright/-/e2e-test-utils-playwright-1.22.0.tgz", - "integrity": "sha512-LJp+8+T3/Jk4dKbpLAYTxDvwn4yHhpzImezWOWsaoGMc92SvHjJfdexMB7vnzuE0IOEZUst7bIabui3tYkiUtQ==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils-playwright/-/e2e-test-utils-playwright-1.23.0.tgz", + "integrity": "sha512-ity7Xpyuo4s0US8jo70u6WAluI/VLPo/hxfhOaLoUo+jujQUgM3pt0RzkiXKaQ9Eca2+lG/qEwoRA0HQmenI2A==", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -5508,9 +5509,9 @@ } }, "node_modules/@wordpress/env": { - "version": "10.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/env/-/env-10.22.0.tgz", - "integrity": "sha512-w/OGGVI5PCWawAwUD6wFWWdb6etHJ8MHmf7DfW0xX/i7bXNqE8qvW/HimQx/ssILvHWC3CsB8x+CsNoG7ZTEIA==", + "version": "10.23.0", + "resolved": "https://registry.npmjs.org/@wordpress/env/-/env-10.23.0.tgz", + "integrity": "sha512-4OIydQJWIjkjR6o0YIC4zI853NrhaiaNkU47BpIh+OGEwMbDG4OZgith4l3ZlHP1ZGSg5qBt3gZSmXK/e0aKmQ==", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -5595,17 +5596,17 @@ } }, "node_modules/@wordpress/eslint-plugin": { - "version": "22.8.0", - "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-22.8.0.tgz", - "integrity": "sha512-VH39xtdnKqLag8PUhS+y4n0Ted4lPtUQ1vIr66DiFvGWMZ4+GfFl8IFOIWi41+6Obw8kgKuOUJhd0qSl+8tg1w==", + "version": "22.9.0", + "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-22.9.0.tgz", + "integrity": "sha512-reiIp2GpXpxWucTZgrswPVTl1YqoZOSW8DRS4U5tGw6eTNN5YDIcDH+yyje2O2kFNh0otCvF4Y4gzQiYDOulAw==", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { "@babel/eslint-parser": "7.25.7", "@typescript-eslint/eslint-plugin": "^6.4.1", "@typescript-eslint/parser": "^6.4.1", - "@wordpress/babel-preset-default": "^8.22.0", - "@wordpress/prettier-config": "^4.22.0", + "@wordpress/babel-preset-default": "^8.23.0", + "@wordpress/prettier-config": "^4.23.0", "cosmiconfig": "^7.0.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-import": "^2.25.2", @@ -5668,9 +5669,9 @@ } }, "node_modules/@wordpress/jest-console": { - "version": "8.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-8.22.0.tgz", - "integrity": "sha512-kVqZy98s5ROR3FXvkdde6YpPOthIu7JZJ1/DOv21xINo9VGEN+yx8h3/xwiBTsbEs4bLa+ttQnvVE/lKNj+cvg==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-8.23.0.tgz", + "integrity": "sha512-2uL5VzRf63Uyl6bym7sdYJhke4ziEj8kPlPn3ObCV210140TB17SOxJ8SOE3wxBlEh1VDPSotXto8GW/lu+iBg==", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -5686,13 +5687,13 @@ } }, "node_modules/@wordpress/jest-preset-default": { - "version": "12.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/jest-preset-default/-/jest-preset-default-12.22.0.tgz", - "integrity": "sha512-pC6H6RenGCza2uhoR/CN65Gt7izZVIo0Sf+QrkAGYuxBqubOn70EWg3UedY0Jwl53fGrbf5KqHBCbDHf8W397Q==", + "version": "12.23.0", + "resolved": "https://registry.npmjs.org/@wordpress/jest-preset-default/-/jest-preset-default-12.23.0.tgz", + "integrity": "sha512-vfdTifeiIDi5FlCYlKiKvmY9o1gymXajc8SBAM66y/qDJCbDi/kFsINDM4aejveIIx8qTO7t3lr8hNkOjxsCPw==", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/jest-console": "^8.22.0", + "@wordpress/jest-console": "^8.23.0", "babel-jest": "29.7.0" }, "engines": { @@ -5705,9 +5706,9 @@ } }, "node_modules/@wordpress/npm-package-json-lint-config": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/npm-package-json-lint-config/-/npm-package-json-lint-config-5.22.0.tgz", - "integrity": "sha512-3ZU5lhM9d5ePgI8Sw1oUDttWbj8Bxkh89IzJQGeCSB0HLo7n2sGADgfLx2+apuDiPGiRK4pIySxLaiFer+Tx/A==", + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@wordpress/npm-package-json-lint-config/-/npm-package-json-lint-config-5.23.0.tgz", + "integrity": "sha512-2YsGOcXVDGdi4+buE7X+uhguvWF2yEZSeR/H2+RNYHtmB0SnFIAdu5uODHEkMvNgQzf3/lHHsHsHndptXzXCfA==", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -5719,13 +5720,13 @@ } }, "node_modules/@wordpress/postcss-plugins-preset": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/postcss-plugins-preset/-/postcss-plugins-preset-5.22.0.tgz", - "integrity": "sha512-Bdj9S/9hMj3DxKreMyO8iAX5yI5BKrQOQCR5cU0M89oTuJp9/Y5UZG7NJrpj2ojYI3/nzR9Z+GdGIP69h2VoUA==", + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@wordpress/postcss-plugins-preset/-/postcss-plugins-preset-5.23.0.tgz", + "integrity": "sha512-XD7TRGUCxPuoOysT7AUXS2OifegqZt6MdScPifYudVYTxxTi+PEuyPL4tEuVpu9ZYWJuv3ShCNRppWiPsRTJrg==", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/base-styles": "^5.22.0", + "@wordpress/base-styles": "^5.23.0", "autoprefixer": "^10.4.20" }, "engines": { @@ -5737,9 +5738,9 @@ } }, "node_modules/@wordpress/prettier-config": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-4.22.0.tgz", - "integrity": "sha512-+XsgTyVSrPd7m+s4G/fNBuyzvkE/Dgx3syUn5G5KLhnb5atRb4r1hWrLBg/oC8vsU5kGEyO+p6LEDRjcZtl0nQ==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-4.23.0.tgz", + "integrity": "sha512-IxJcV/SxPzT8bRuYbhBZwAa8Q8n16con4uD/8X8OF3/vAP2StX5/kAo3tVvy7e1La7H/1QkmdyayeHkyiwNRsQ==", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -5751,25 +5752,25 @@ } }, "node_modules/@wordpress/scripts": { - "version": "30.15.0", - "resolved": "https://registry.npmjs.org/@wordpress/scripts/-/scripts-30.15.0.tgz", - "integrity": "sha512-mjV5jwTOopa2zLq75b+KfY0AYksLhiUcn13Ft5RjPZwYaofs7rflh0RVa5VK0j7cMzdYzSS7dJhQM68XTJqtBQ==", + "version": "30.16.0", + "resolved": "https://registry.npmjs.org/@wordpress/scripts/-/scripts-30.16.0.tgz", + "integrity": "sha512-1lOChs1DzI8YYsjZFBNFy0km5o5ts08rIanQQ6k/tdNiVLnb4h5/aUike60Y3phGjV3On7xO/K4ZJ7AyoI0oRA==", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { "@babel/core": "7.25.7", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", "@svgr/webpack": "^8.0.1", - "@wordpress/babel-preset-default": "^8.22.0", - "@wordpress/browserslist-config": "^6.22.0", - "@wordpress/dependency-extraction-webpack-plugin": "^6.22.0", - "@wordpress/e2e-test-utils-playwright": "^1.22.0", - "@wordpress/eslint-plugin": "^22.8.0", - "@wordpress/jest-preset-default": "^12.22.0", - "@wordpress/npm-package-json-lint-config": "^5.22.0", - "@wordpress/postcss-plugins-preset": "^5.22.0", - "@wordpress/prettier-config": "^4.22.0", - "@wordpress/stylelint-config": "^23.14.0", + "@wordpress/babel-preset-default": "^8.23.0", + "@wordpress/browserslist-config": "^6.23.0", + "@wordpress/dependency-extraction-webpack-plugin": "^6.23.0", + "@wordpress/e2e-test-utils-playwright": "^1.23.0", + "@wordpress/eslint-plugin": "^22.9.0", + "@wordpress/jest-preset-default": "^12.23.0", + "@wordpress/npm-package-json-lint-config": "^5.23.0", + "@wordpress/postcss-plugins-preset": "^5.23.0", + "@wordpress/prettier-config": "^4.23.0", + "@wordpress/stylelint-config": "^23.15.0", "adm-zip": "^0.5.9", "babel-jest": "29.7.0", "babel-loader": "9.2.1", @@ -5953,9 +5954,9 @@ } }, "node_modules/@wordpress/stylelint-config": { - "version": "23.14.0", - "resolved": "https://registry.npmjs.org/@wordpress/stylelint-config/-/stylelint-config-23.14.0.tgz", - "integrity": "sha512-SxrPIiR7LE8DMQblsPkiE81VY/JQAaU5SGmphDG+Bc2DnxfOdkt1oMsSUfsSEVwHuRlgh4ZD42CLlIV+Y0AexQ==", + "version": "23.15.0", + "resolved": "https://registry.npmjs.org/@wordpress/stylelint-config/-/stylelint-config-23.15.0.tgz", + "integrity": "sha512-DVxCs7uojx7YJ/jJvN3EZg4wHXgvaMCqiVCtBGOQphgaBetgH5GyEELoDA6MOFd7DCO6edVjx5fhT5dc40zmAQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5973,9 +5974,9 @@ } }, "node_modules/@wordpress/warning": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-3.22.0.tgz", - "integrity": "sha512-GvL9XdnRyDfFbwtZ6X0hoRDlQr6G5kHLWhq5gnE6uJ9xqiuPR9CQgM24a8LE3Oje1ipdWvPA7Cyr6tv4y8TuMQ==", + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-3.23.0.tgz", + "integrity": "sha512-tPJ8T5BBNRqNTdF8gOy97h+sD+bs0QwoRIzE4y2erQ9E+LqQXIgl8+UhT0F+5q7QcFzd0hGRF8sotzILMjJZHw==", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -7125,14 +7126,14 @@ } }, "node_modules/cacheable": { - "version": "1.8.10", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.8.10.tgz", - "integrity": "sha512-0ZnbicB/N2R6uziva8l6O6BieBklArWyiGx4GkwAhLKhSHyQtRfM9T1nx7HHuHDKkYB/efJQhz3QJ6x/YqoZzA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.9.0.tgz", + "integrity": "sha512-8D5htMCxPDUULux9gFzv30f04Xo3wCnik0oOxKoRTPIBoqA7HtOcJ87uBhQTs3jCfZZTrUBGsYIZOgE0ZRgMAg==", "dev": true, "license": "MIT", "dependencies": { - "hookified": "^1.8.1", - "keyv": "^5.3.2" + "hookified": "^1.8.2", + "keyv": "^5.3.3" } }, "node_modules/cacheable-lookup": { @@ -7163,9 +7164,9 @@ } }, "node_modules/cacheable/node_modules/keyv": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.2.tgz", - "integrity": "sha512-Lji2XRxqqa5Wg+CHLVfFKBImfJZ4pCSccu9eVWK6w4c2SDFLd8JAn1zqTuSFnsxb7ope6rMsnIHfp+eBbRBRZQ==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.3.tgz", + "integrity": "sha512-Rwu4+nXI9fqcxiEHtbkvoes2X+QfkTRo1TMkPfwzipGsJlJO/z69vqB4FNl9xJ3xCpAcbkvmEabZfPzrwN3+gQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7462,9 +7463,9 @@ } }, "node_modules/chrome-launcher": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.1.2.tgz", - "integrity": "sha512-YclTJey34KUm5jB1aEJCq807bSievi7Nb/TU4Gu504fUYi3jw3KCIaH6L7nFWQhdEgH3V+wCh+kKD1P5cXnfxw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.2.0.tgz", + "integrity": "sha512-JbuGuBNss258bvGil7FT4HKdC3SC2K7UAEUqiPy3ACS3Yxo3hAW6bvFpCu2HsIJLgTqxgEX6BkujvzZfLpUD0Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -7474,7 +7475,7 @@ "lighthouse-logger": "^2.0.1" }, "bin": { - "print-chrome-path": "bin/print-chrome-path.js" + "print-chrome-path": "bin/print-chrome-path.cjs" }, "engines": { "node": ">=12.13.0" @@ -8069,9 +8070,9 @@ } }, "node_modules/core-js": { - "version": "3.41.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.41.0.tgz", - "integrity": "sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA==", + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.42.0.tgz", + "integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -9301,9 +9302,9 @@ } }, "node_modules/dedent": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", - "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -9556,9 +9557,9 @@ "dev": true }, "node_modules/devtools-protocol": { - "version": "0.0.1436416", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1436416.tgz", - "integrity": "sha512-iGLhz2WOrlBLcTcoVsFy5dPPUqILG6cc8MITYd5lV6i38gWG14bMXRH/d8G5KITrWHBnbsOnWHfc9Qs4/jej9Q==", + "version": "0.0.1445099", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1445099.tgz", + "integrity": "sha512-GEuIbCLU2Iu6Sg05GeWS7ksijhOUZIDJD2YBUNRanK7SLKjeci1uxUUomu2VNvygQRuoq/vtnTYrgPZBEiYNMA==", "dev": true, "license": "BSD-3-Clause" }, @@ -10455,9 +10456,9 @@ } }, "node_modules/eslint-plugin-jest/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -10492,9 +10493,9 @@ } }, "node_modules/eslint-plugin-jsdoc/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -10551,9 +10552,9 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.6.tgz", - "integrity": "sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.0.tgz", + "integrity": "sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA==", "dev": true, "license": "MIT", "dependencies": { @@ -12192,9 +12193,9 @@ } }, "node_modules/hookified": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.8.2.tgz", - "integrity": "sha512-5nZbBNP44sFCDjSoB//0N7m508APCgbQ4mGGo1KJGBYyCKNHfry1Pvd0JVHZIxjdnqn8nFRBAN/eFB6Rk/4w5w==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.9.0.tgz", + "integrity": "sha512-2yEEGqphImtKIe1NXWEhu6yD3hlFR4Mxk4Mtp3XEyScpSt4pQ4ymmXA1zzxZpj99QkFK+nN0nzjeb2+RUi/6CQ==", "dev": true, "license": "MIT" }, @@ -13990,9 +13991,9 @@ } }, "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -14332,9 +14333,9 @@ } }, "node_modules/known-css-properties": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz", - "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==", + "version": "0.36.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.36.0.tgz", + "integrity": "sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA==", "dev": true, "license": "MIT" }, @@ -14377,6 +14378,13 @@ "node": ">=0.10.0" } }, + "node_modules/legacy-javascript": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/legacy-javascript/-/legacy-javascript-0.0.1.tgz", + "integrity": "sha512-lPyntS4/aS7jpuvOlitZDFifBCb4W8L/3QU0PLbUTUj+zYah8rfVjYic88yG7ZKTxhS5h9iz7duT8oUXKszLhg==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -14412,19 +14420,19 @@ } }, "node_modules/lighthouse": { - "version": "12.5.1", - "resolved": "https://registry.npmjs.org/lighthouse/-/lighthouse-12.5.1.tgz", - "integrity": "sha512-ooOIqtBxOEnuX3yKtc8WiMPI/fPqHtXHaXU4ey87icRcY5I2B9+imk8i6U7duIO+yrU0WwbIwhmCs8s/FFNRgA==", + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/lighthouse/-/lighthouse-12.6.0.tgz", + "integrity": "sha512-ufYw6dBR0PDEpO4pj45zRStatdTvBSi/LTXgs6ULmadSLTNXklP3XGGGuL7SA9pE/NltGbs5zQOA/ICQao1ywA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@paulirish/trace_engine": "0.0.50", + "@paulirish/trace_engine": "0.0.52", "@sentry/node": "^7.0.0", "axe-core": "^4.10.3", "chrome-launcher": "^1.1.2", "configstore": "^5.0.1", "csp_evaluator": "1.1.5", - "devtools-protocol": "0.0.1436416", + "devtools-protocol": "0.0.1445099", "enquirer": "^2.3.6", "http-link-header": "^1.1.1", "intl-messageformat": "^10.5.3", @@ -14437,11 +14445,11 @@ "metaviewport-parser": "0.3.0", "open": "^8.4.0", "parse-cache-control": "1.0.1", - "puppeteer-core": "^24.4.0", + "puppeteer-core": "^24.6.1", "robots-parser": "^3.0.1", "semver": "^5.3.0", "speedline-core": "^1.4.3", - "third-party-web": "^0.26.5", + "third-party-web": "^0.26.6", "tldts-icann": "^6.1.16", "ws": "^7.0.0", "yargs": "^17.3.1", @@ -14453,7 +14461,7 @@ "smokehouse": "cli/test/smokehouse/frontends/smokehouse-bin.js" }, "engines": { - "node": ">=18.16" + "node": ">=18.20" } }, "node_modules/lighthouse-logger": { @@ -14492,9 +14500,9 @@ "license": "Apache-2.0" }, "node_modules/lighthouse/node_modules/@puppeteer/browsers": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.0.tgz", - "integrity": "sha512-HdHF4rny4JCvIcm7V1dpvpctIGqM3/Me255CB44vW7hDG1zYMmcBMjpNqZEDxdCfXGLkx5kP0+Jz5DUS+ukqtA==", + "version": "2.10.4", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.4.tgz", + "integrity": "sha512-9DxbZx+XGMNdjBynIs4BRSz+M3iRDeB7qRcAr6UORFLphCIM2x3DXgOucvADiifcqCE4XePFUKcnaAMyGbrDlQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -14514,9 +14522,9 @@ } }, "node_modules/lighthouse/node_modules/@puppeteer/browsers/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -14548,27 +14556,27 @@ } }, "node_modules/lighthouse/node_modules/puppeteer-core": { - "version": "24.6.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.6.1.tgz", - "integrity": "sha512-sMCxsY+OPWO2fecBrhIeCeJbWWXJ6UaN997sTid6whY0YT9XM0RnxEwLeUibluIS5/fRmuxe1efjb5RMBsky7g==", + "version": "24.8.2", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.8.2.tgz", + "integrity": "sha512-wNw5cRZOHiFibWc0vdYCYO92QuKTbJ8frXiUfOq/UGJWMqhPoBThTKkV+dJ99YyWfzJ2CfQQ4T1nhhR0h8FlVw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.10.0", - "chromium-bidi": "3.0.0", + "@puppeteer/browsers": "2.10.4", + "chromium-bidi": "5.1.0", "debug": "^4.4.0", - "devtools-protocol": "0.0.1425554", + "devtools-protocol": "0.0.1439962", "typed-query-selector": "^2.12.0", - "ws": "^8.18.1" + "ws": "^8.18.2" }, "engines": { "node": ">=18" } }, "node_modules/lighthouse/node_modules/puppeteer-core/node_modules/chromium-bidi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-3.0.0.tgz", - "integrity": "sha512-ZOGRDAhBMX1uxL2Cm2TDuhImbrsEz5A/tTcVU6RpXEWaTNUNwsHW6njUXizh51Ir6iqHbKAfhA2XK33uBcLo5A==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-5.1.0.tgz", + "integrity": "sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -14580,16 +14588,16 @@ } }, "node_modules/lighthouse/node_modules/puppeteer-core/node_modules/devtools-protocol": { - "version": "0.0.1425554", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1425554.tgz", - "integrity": "sha512-uRfxR6Nlzdzt0ihVIkV+sLztKgs7rgquY/Mhcv1YNCWDh5IZgl5mnn2aeEnW5stYTE0wwiF4RYVz8eMEpV1SEw==", + "version": "0.0.1439962", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1439962.tgz", + "integrity": "sha512-jJF48UdryzKiWhJ1bLKr7BFWUQCEIT5uCNbDLqkQJBtkFxYzILJH44WN0PDKMIlGDN7Utb8vyUY85C3w4R/t2g==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/lighthouse/node_modules/puppeteer-core/node_modules/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", "dev": true, "license": "MIT", "engines": { @@ -14641,9 +14649,9 @@ } }, "node_modules/lighthouse/node_modules/zod": { - "version": "3.24.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", - "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", + "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", "dev": true, "license": "MIT", "funding": { @@ -15387,9 +15395,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -16131,9 +16139,9 @@ } }, "node_modules/normalize-package-data/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -16277,9 +16285,9 @@ "license": "MIT" }, "node_modules/npm-package-json-lint/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -17382,13 +17390,13 @@ } }, "node_modules/playwright": { - "version": "1.51.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.1.tgz", - "integrity": "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", + "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.51.1" + "playwright-core": "1.52.0" }, "bin": { "playwright": "cli.js" @@ -17401,9 +17409,9 @@ } }, "node_modules/playwright-core": { - "version": "1.51.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.1.tgz", - "integrity": "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", + "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -20394,9 +20402,9 @@ } }, "node_modules/stylelint": { - "version": "16.18.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.18.0.tgz", - "integrity": "sha512-OXb68qzesv7J70BSbFwfK3yTVLEVXiQ/ro6wUE4UrSbKCMjLLA02S8Qq3LC01DxKyVjk7z8xh35aB4JzO3/sNA==", + "version": "16.19.1", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.19.1.tgz", + "integrity": "sha512-C1SlPZNMKl+d/C867ZdCRthrS+6KuZ3AoGW113RZCOL0M8xOGpgx7G70wq7lFvqvm4dcfdGFVLB/mNaLFChRKw==", "dev": true, "funding": [ { @@ -20423,7 +20431,7 @@ "debug": "^4.3.7", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", - "file-entry-cache": "^10.0.7", + "file-entry-cache": "^10.0.8", "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", @@ -20431,7 +20439,7 @@ "ignore": "^7.0.3", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", - "known-css-properties": "^0.35.0", + "known-css-properties": "^0.36.0", "mathml-tag-names": "^2.1.3", "meow": "^13.2.0", "micromatch": "^4.0.8", @@ -20504,16 +20512,16 @@ } }, "node_modules/stylelint-scss": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-6.11.1.tgz", - "integrity": "sha512-e4rYo0UY+BIMtGeGanghrvHTjcryxgZbyFxUedp8dLFqC4P70aawNdYjRrQxbnKhu3BNr4+lt5e/53tcKXiwFA==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-6.12.0.tgz", + "integrity": "sha512-U7CKhi1YNkM1pXUXl/GMUXi8xKdhl4Ayxdyceie1nZ1XNIdaUgMV6OArpooWcDzEggwgYD0HP/xIgVJo9a655w==", "dev": true, "license": "MIT", "dependencies": { "css-tree": "^3.0.1", "is-plain-object": "^5.0.0", - "known-css-properties": "^0.35.0", - "mdn-data": "^2.15.0", + "known-css-properties": "^0.36.0", + "mdn-data": "^2.21.0", "postcss-media-query-parser": "^0.2.3", "postcss-resolve-nested-selector": "^0.1.6", "postcss-selector-parser": "^7.1.0", @@ -20671,25 +20679,25 @@ } }, "node_modules/stylelint/node_modules/file-entry-cache": { - "version": "10.0.8", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.0.8.tgz", - "integrity": "sha512-FGXHpfmI4XyzbLd3HQ8cbUcsFGohJpZtmQRHr8z8FxxtCe2PcpgIlVLwIgunqjvRmXypBETvwhV4ptJizA+Y1Q==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.1.0.tgz", + "integrity": "sha512-Et/ex6smi3wOOB+n5mek+Grf7P2AxZR5ueqRUvAAn4qkyatXi3cUC1cuQXVkX0VlzBVsN4BkWJFmY/fYiRTdww==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^6.1.8" + "flat-cache": "^6.1.9" } }, "node_modules/stylelint/node_modules/flat-cache": { - "version": "6.1.8", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.8.tgz", - "integrity": "sha512-R6MaD3nrJAtO7C3QOuS79ficm2pEAy++TgEUD8ii1LVlbcgZ9DtASLkt9B+RZSFCzm7QHDMlXPsqqB6W2Pfr1Q==", + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.9.tgz", + "integrity": "sha512-DUqiKkTlAfhtl7g78IuwqYM+YqvT+as0mY+EVk6mfimy19U79pJCzDZQsnqk3Ou/T6hFXWLGbwbADzD/c8Tydg==", "dev": true, "license": "MIT", "dependencies": { - "cacheable": "^1.8.9", + "cacheable": "^1.9.0", "flatted": "^3.3.3", - "hookified": "^1.8.1" + "hookified": "^1.8.2" } }, "node_modules/stylelint/node_modules/global-modules": { @@ -20721,9 +20729,9 @@ } }, "node_modules/stylelint/node_modules/ignore": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz", - "integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 9f586f0f2d..374bb9ee67 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,9 @@ "@octokit/rest": "^21.1.1", "@playwright/test": "^1.51.1", "@wordpress/e2e-test-utils-playwright": "^1.21.0", - "@wordpress/env": "^10.22.0", - "@wordpress/prettier-config": "^4.20.0", - "@wordpress/scripts": "^30.15.0", + "@wordpress/env": "^10.23.0", + "@wordpress/prettier-config": "^4.23.0", + "@wordpress/scripts": "^30.16.0", "commander": "13.1.0", "copy-webpack-plugin": "^13.0.0", "css-minimizer-webpack-plugin": "^7.0.2", diff --git a/plugins/optimization-detective/class-od-strict-url-metric.php b/plugins/optimization-detective/class-od-strict-url-metric.php index 09a014607a..d2644dfc7a 100644 --- a/plugins/optimization-detective/class-od-strict-url-metric.php +++ b/plugins/optimization-detective/class-od-strict-url-metric.php @@ -16,7 +16,7 @@ * Representation of the measurements taken from a single client's visit to a specific URL without additionalProperties allowed. * * This is used exclusively in the REST API endpoint for capturing new URL Metrics to prevent invalid additional data from being - * submitted in the request. For URL Metrics which have been stored the looser OD_URL_Metric class is used instead. + * submitted in the request. For URL Metrics which have been stored, the looser OD_URL_Metric class is used instead. * * @phpstan-import-type JSONSchema from OD_URL_Metric * diff --git a/plugins/optimization-detective/class-od-tag-visitor-context.php b/plugins/optimization-detective/class-od-tag-visitor-context.php index e88f4a1518..d2c5a6d8ff 100644 --- a/plugins/optimization-detective/class-od-tag-visitor-context.php +++ b/plugins/optimization-detective/class-od-tag-visitor-context.php @@ -124,7 +124,7 @@ public function __get( string $name ) { throw new Error( esc_html( sprintf( - /* translators: %s is class member variable name */ + /* translators: %s is the class member variable name */ __( 'Unknown property %s.', 'optimization-detective' ), __CLASS__ . '::$' . $name ) diff --git a/plugins/optimization-detective/class-od-template-optimization-context.php b/plugins/optimization-detective/class-od-template-optimization-context.php index e682f61322..26110d49d8 100644 --- a/plugins/optimization-detective/class-od-template-optimization-context.php +++ b/plugins/optimization-detective/class-od-template-optimization-context.php @@ -114,7 +114,7 @@ public function __get( string $name ) { throw new Error( esc_html( sprintf( - /* translators: %s is class member variable name */ + /* translators: %s is the class member variable name */ __( 'Unknown property %s.', 'optimization-detective' ), __CLASS__ . '::$' . $name ) diff --git a/plugins/optimization-detective/class-od-url-metric-group-collection.php b/plugins/optimization-detective/class-od-url-metric-group-collection.php index 202dba7faf..86b52517e9 100644 --- a/plugins/optimization-detective/class-od-url-metric-group-collection.php +++ b/plugins/optimization-detective/class-od-url-metric-group-collection.php @@ -51,7 +51,7 @@ final class OD_URL_Metric_Group_Collection implements Countable, IteratorAggrega * value of 1, and the breakpoints are used as the maximum viewport widths for the viewport groups, with the addition of * a final viewport group which has a maximum viewport width of infinity. * - * This array may be empty in which case there are no responsive breakpoints and all URL Metrics are collected in a + * This array may be empty, in which case there are no responsive breakpoints, and all URL Metrics are collected in a * single group. * * @since 0.1.0 @@ -153,7 +153,7 @@ public function __construct( array $url_metrics, string $current_etag, array $br */ $this->breakpoints = $breakpoints; - // Set sample size. + // Set the sample size. if ( $sample_size <= 0 ) { throw new InvalidArgumentException( esc_html( @@ -211,7 +211,7 @@ public function get_sample_size(): int { } /** - * Gets the freshness age (TTL) for a given URL Metric.. + * Gets the freshness age (TTL) for a given URL Metric. * * @since 1.0.0 * @@ -224,7 +224,7 @@ public function get_freshness_ttl(): int { /** * Gets the first URL Metric group (with the lowest minimum viewport width, e.g. for mobile). * - * This group normally represents viewports for mobile devices. This group always has a minimum viewport width of 0 + * This group normally represents viewports for mobile devices. This group always has a minimum viewport width of 0, * and the maximum viewport width corresponds to the smallest defined breakpoint returned by * {@see od_get_breakpoint_max_widths()}. * @@ -241,7 +241,7 @@ public function get_first_group(): OD_URL_Metric_Group { * * This group normally represents viewports for desktop devices. This group always has a minimum viewport width * defined as one greater than the largest breakpoint returned by {@see od_get_breakpoint_max_widths()}. - * The maximum viewport width of this group is always `null`, or in other words it is unbounded. + * The maximum viewport width of this group is always `null`, or in other words, it is unbounded. * * @since 0.7.0 * @@ -404,7 +404,7 @@ public function is_every_group_populated(): bool { /** * Checks whether every group is complete (full sample of non-stale URL Metrics). * - * Completeness means the full sample size of URL Metrics has been collected, + * Completeness means the full sample size of URL Metrics has been collected; * none of the collected URL Metrics are stale (with a mismatching ETag or a * timestamp older than the freshness TTL). * @@ -520,9 +520,9 @@ public function get_common_lcp_element(): ?OD_Element { /** * Gets all elements from all URL Metrics from all groups keyed by the elements' XPaths. * - * This is an O(n^3) function so its results must be cached. This being said, the number of groups should be 4 (one - * more than the default number of breakpoints) and the number of URL Metrics for each group should be 3 - * (the default sample size). Therefore, given the number (n) of visited elements on the page this will only + * This is an O(n^3) function, so its results must be cached. This being said, the number of groups should be 4 (one + * more than the default number of breakpoints), and the number of URL Metrics for each group should be 3 + * (the default sample size). Therefore, given the number (n) of visited elements on the page, this will only * end up running n*4*3 times. * * @since 0.7.0 @@ -584,8 +584,8 @@ public function get_all_element_max_intersection_ratios(): array { * * An element is positioned in the initial viewport if its `boundingClientRect.top` is less than the * `viewport.height` for any of its recorded URL Metrics. Note that even though the element may be positioned in the - * initial viewport, it may not actually be visible. It could be occluded as a latter slide in a carousel in which - * case it will have intersectionRatio of 0. Or the element may not be visible due to it or an ancestor having the + * initial viewport, it may not actually be visible. It could be occluded as a latter slide in a carousel, in which + * case it will have an intersectionRatio of 0. Or the element may not be visible due to it or an ancestor having the * `visibility:hidden` style, such as in the case of a dropdown navigation menu. When, for example, an IMG element * is positioned in any initial viewport, it should not get `loading=lazy` but rather `fetchpriority=low`. * Furthermore, the element may be positioned _above_ the initial viewport or to the left or right of the viewport, @@ -624,7 +624,7 @@ public function get_all_elements_positioned_in_any_initial_viewport(): array { * @since 0.3.0 * * @param string $xpath XPath for the element. - * @return float|null Max intersection ratio of null if tag is unknown (not captured). + * @return float|null Max intersection ratio or null if the tag is unknown (not captured). */ public function get_element_max_intersection_ratio( string $xpath ): ?float { return $this->get_all_element_max_intersection_ratios()[ $xpath ] ?? null; @@ -636,7 +636,7 @@ public function get_element_max_intersection_ratio( string $xpath ): ?float { * @since 0.7.0 * * @param string $xpath XPath for the element. - * @return bool|null Whether element is positioned in any initial viewport of null if unknown. + * @return bool|null Whether an element is positioned in any initial viewport or null if unknown. */ public function is_element_positioned_in_any_initial_viewport( string $xpath ): ?bool { return $this->get_all_elements_positioned_in_any_initial_viewport()[ $xpath ] ?? null; @@ -651,8 +651,8 @@ public function is_element_positioned_in_any_initial_viewport( string $xpath ): */ public function get_flattened_url_metrics(): array { // The duplication of iterator_to_array is not a mistake. This collection is an - // iterator and the collection contains iterator instances. So to flatten the - // two levels of iterators we need to nest calls to iterator_to_array(). + // iterator, and the collection contains iterator instances. So to flatten the + // two levels of iterators, we need to nest calls to iterator_to_array(). return array_merge( ...array_map( 'iterator_to_array', diff --git a/plugins/optimization-detective/class-od-url-metric-group.php b/plugins/optimization-detective/class-od-url-metric-group.php index e1225f7657..56c4b030c2 100644 --- a/plugins/optimization-detective/class-od-url-metric-group.php +++ b/plugins/optimization-detective/class-od-url-metric-group.php @@ -268,7 +268,7 @@ static function ( OD_URL_Metric $a, OD_URL_Metric $b ): int { /** * Determines whether the URL Metric group is complete. * - * A group is complete if it has the full sample size of URL Metrics + * A group is complete if it has the full sample size of URL Metrics, * and all of these URL Metrics are fresh (with a current ETag and a * timestamp that is not older than the freshness TTL). * @@ -322,7 +322,7 @@ public function get_lcp_element(): ?OD_Element { $result = ( function () { - // No metrics have been gathered for this group so there is no LCP element. + // No metrics have been gathered for this group, so there is no LCP element. if ( count( $this->url_metrics ) === 0 ) { return null; } @@ -330,7 +330,7 @@ public function get_lcp_element(): ?OD_Element { // The following arrays all share array indices. /** - * Seen breadcrumbs counts. + * Seen breadcrumb counts. * * @var array $seen_breadcrumbs */ @@ -350,7 +350,7 @@ public function get_lcp_element(): ?OD_Element { */ $breadcrumb_element = array(); - // Prefer to use URL Metrics which have a current ETag. + // Prefer to use URL Metrics, which have a current ETag. $url_metrics = array_filter( $this->url_metrics, function ( OD_URL_Metric $url_metric ): bool { @@ -459,7 +459,7 @@ public function get_all_element_max_intersection_ratios(): array { * @since 0.9.0 * * @param string $xpath XPath for the element. - * @return float|null Max intersection ratio of null if tag is unknown (not captured). + * @return float|null Max intersection ratio or null if the tag is unknown (not captured). */ public function get_element_max_intersection_ratio( string $xpath ): ?float { return $this->get_all_element_max_intersection_ratios()[ $xpath ] ?? null; diff --git a/plugins/optimization-detective/class-od-url-metric.php b/plugins/optimization-detective/class-od-url-metric.php index b8c4374af8..6dc4e9bac6 100644 --- a/plugins/optimization-detective/class-od-url-metric.php +++ b/plugins/optimization-detective/class-od-url-metric.php @@ -208,7 +208,7 @@ public static function get_json_schema(): array { ) ); - // The spec allows these to be negative but this doesn't make sense in the context of intersectionRect and boundingClientRect. + // The spec allows these to be negative, but this doesn't make sense in the context of intersectionRect and boundingClientRect. $dom_rect_properties['width']['minimum'] = 0.0; $dom_rect_properties['height']['minimum'] = 0.0; @@ -312,9 +312,9 @@ public static function get_json_schema(): array { ), ), // Additional root properties may be added to the schema via the od_url_metric_schema_root_additional_properties filter. - // Therefore, additionalProperties is set to true so that additional properties defined in the extended schema may persist + // Therefore, `additionalProperties` is set to true so that additional properties defined in the extended schema may persist // in a stored URL Metric even when the extension is deactivated. For REST API requests, the OD_Strict_URL_Metric - // which sets this to false so that newly-submitted URL Metrics only ever include the known properties. + // which sets this to false so that newly submitted URL Metrics only ever include the known properties. 'additionalProperties' => true, ); @@ -364,7 +364,7 @@ public static function get_json_schema(): array { protected static function extend_schema_with_optional_properties( array $properties_schema, array $additional_properties, string $filter_name ): array { $doing_it_wrong = static function ( string $message ) use ( $filter_name ): void { _doing_it_wrong( - esc_html( "Filter: '{$filter_name}'" ), + esc_html( "Filter: '$filter_name'" ), esc_html( $message ), 'Optimization Detective 0.6.0' ); diff --git a/plugins/optimization-detective/deprecated.php b/plugins/optimization-detective/deprecated.php index 1f81d4189d..40e67d8f84 100644 --- a/plugins/optimization-detective/deprecated.php +++ b/plugins/optimization-detective/deprecated.php @@ -5,6 +5,8 @@ * @package optimization-detective * * @since 1.0.0 + * + * @noinspection PhpUnused */ // @codeCoverageIgnoreStart diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index ce9157ae90..c38c9efacd 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -27,14 +27,14 @@ */ /** - * Window reference to reduce size when script is minified. + * Window reference to reduce size when the script is minified. * * @type {Window} */ const win = window; /** - * Document reference to reduce size when script is minified. + * Document reference to reduce size when the script is minified. * * @type {Document} */ @@ -184,7 +184,7 @@ function createLogger( /** * Attempts to get the extension name (i.e. slug for plugin or theme) from the script module URL. * - * If extraction of the slug fails then the entire URL is returned. + * If extraction of the slug fails, then the entire URL is returned. * * @param {string} scriptModuleUrl - Script module URL. * @return {string} Derived extension name. @@ -244,7 +244,7 @@ async function getAlreadySubmittedSessionStorageKey( urlMetricGroupStatus, { warn, error } ) { - if ( ! window.crypto || ! window.crypto.subtle ) { + if ( ! win.crypto || ! win.crypto.subtle ) { warn( 'Unable to generate sessionStorage key for already-submitted URL since crypto is not available, likely due to to the page not being served via HTTPS.' ); @@ -509,7 +509,7 @@ function debounceCompressUrlMetric() { */ /** - * Detects the LCP element, loaded images, client viewport and store for future optimizations. + * Detects the LCP element, loaded images, client viewport, and store for future optimizations. * * @param {Object} args - Args. * @param {string[]} args.extensionModuleUrls - URLs for extension script modules to import. @@ -580,7 +580,7 @@ export default async function detect( { return; } - if ( document.visibilityState === 'hidden' && ! document.prerendering ) { + if ( doc.visibilityState === 'hidden' && ! doc.prerendering ) { log( 'Page opened in background tab so URL Metric is not collected.' ); return; } @@ -669,9 +669,9 @@ export default async function detect( { return; } - // Keep track of whether the window resized. If it resized, we abort sending the URLMetric. + // Keep track of whether the window resized. If it was resized, we abort sending the URLMetric. let didWindowResize = false; - window.addEventListener( + win.addEventListener( 'resize', () => { didWindowResize = true; @@ -704,10 +704,10 @@ export default async function detect( { const breadcrumbedElementsMap = new Map( [ ...breadcrumbedElements ].map( /** - * @param {HTMLElement} element - * @return {[HTMLElement, string]} Tuple of element and its XPath. + * @param {Element} element + * @return {[Element, string]} Tuple of an element and its XPath. */ - ( element ) => [ element, element.dataset.odXpath ] + ( element ) => [ element, element.getAttribute( 'data-od-xpath' ) ] ) ); @@ -724,7 +724,7 @@ export default async function detect( { } } - // Wait for the intersection observer to report back on the initially-visible elements. + // Wait for the intersection observer to report back on the initially visible elements. // Note that the first callback will include _all_ observed entries per . if ( breadcrumbedElementsMap.size > 0 ) { await new Promise( ( resolve ) => { @@ -756,7 +756,7 @@ export default async function detect( { /** @type {(LCPMetric|LCPMetricWithAttribution)[]} */ const lcpMetricCandidates = []; - // Obtain at least one LCP candidate. More may be reported before the page finishes loading. + // Get at least one LCP candidate. More may be reported before the page finishes loading. await new Promise( ( resolve ) => { onLCP( /** @@ -769,7 +769,7 @@ export default async function detect( { resolve(); }, { - // This avoids needing to click to finalize LCP candidate. While this is helpful for testing, it also + // This avoids needing to click to finalize the LCP candidate. While this is helpful for testing, it also // ensures that we always get an LCP candidate reported. Otherwise, the callback may never fire if the // user never does a click or keydown, per . reportAllChanges: true, @@ -777,7 +777,7 @@ export default async function detect( { ); } ); - // Stop observing initial viewport. + // Stop observing the initial viewport. disconnectIntersectionObserver(); urlMetric = { @@ -927,7 +927,7 @@ export default async function detect( { doc.addEventListener( 'visibilitychange', () => { - if ( document.visibilityState === 'hidden' ) { + if ( doc.visibilityState === 'hidden' ) { // TODO: This will fire even when switching tabs. resolve(); } @@ -936,7 +936,7 @@ export default async function detect( { ); } ); - // Only proceed with submitting the URL Metric if viewport stayed the same size. Changing the viewport size (e.g. due + // Only proceed with submitting the URL Metric if the viewport stayed the same size. Changing the viewport size (e.g. due // to resizing a window or changing the orientation of a device) will result in unexpected metrics being collected. if ( didWindowResize ) { log( 'Aborting URL Metric collection due to viewport size change.' ); @@ -1009,7 +1009,7 @@ export default async function detect( { } /* - * Now prepare the URL Metric to be sent as JSON request body. + * Now prepare the URL Metric to be sent in the JSON request body. */ const maxBodyLengthKiB = 64; diff --git a/plugins/optimization-detective/detection.php b/plugins/optimization-detective/detection.php index 648fe0cced..efad149448 100644 --- a/plugins/optimization-detective/detection.php +++ b/plugins/optimization-detective/detection.php @@ -13,7 +13,7 @@ // @codeCoverageIgnoreEnd /** - * Obtains the ID for a post related to this response so that page caches can be told to invalidate their cache. + * Gets the ID for a post related to this response so that page caches can be told to invalidate their cache. * * If the queried object for the response is a post, then that post's ID is used. Otherwise, it uses the ID of the first * post in The Loop. @@ -23,15 +23,15 @@ * this ID if the relevant actions are triggered for the post (e.g. clean_post_cache, save_post, transition_post_status). * * Otherwise, if the response is an archive page or the front page where show_on_front=posts (i.e. is_home), then - * there is no singular post object that represents the URL. In this case, we obtain the first post in the main - * loop. By triggering the relevant actions for this post ID, page caches will have their best shot at invalidating + * there is no singular post object that represents the URL. In this case, we get the first post in the main + * loop. By triggering the relevant actions for this post ID, page caches will be more likely able to invalidate * the related URLs. Page caching plugins which leverage surrogate keys will be the most reliable here. Otherwise, * caching plugins may just resort to automatically purging the cache for the homepage whenever any post is edited, * which is better than nothing. * * There should not be any situation by default in which a page optimized with Optimization Detective does not have such * a post available for cache purging. As seen in {@see od_can_optimize_response()}, when such a post ID is not - * available for cache purging then it returns false, as it also does in another case like if is_404(). + * available for cache purging, then it returns false, as it also does in another case like if is_404(). * * @since 0.8.0 * @access private diff --git a/plugins/optimization-detective/docs/extensions.md b/plugins/optimization-detective/docs/extensions.md index 2d1fa901ce..523aae6c83 100644 --- a/plugins/optimization-detective/docs/extensions.md +++ b/plugins/optimization-detective/docs/extensions.md @@ -2,11 +2,11 @@ # Optimization Detective Extensions -The Optimization Detective plugin is a designed as a dependency for other plugins to extend to implement actual optimizations. Here you can find use cases and examples for how Optimization Detective is extended as well as a list of available extensions. +The Optimization Detective plugin is designed as a dependency for other plugins to extend to implement actual optimizations. Here you can find use cases and examples for how Optimization Detective is extended as well as a list of available extensions. ## Use Cases and Examples -As mentioned above, this plugin is a dependency that doesn't provide features on its own. Dependent plugins leverage the collected URL Metrics to apply optimizations. What follows us a running list of the optimizations which are enabled by Optimization Detective, along with a links to the related code used for the implementation: +As mentioned above, this plugin is a dependency that doesn't provide features on its own. Dependent plugins leverage the collected URL Metrics to apply optimizations. What follows is a running list of the optimizations which are enabled by Optimization Detective, along with links to the related code used for the implementation: **[Image Prioritizer](https://wordpress.org/plugins/image-prioritizer/) ([GitHub](https://github.com/WordPress/performance/tree/trunk/plugins/image-prioritizer)):** @@ -17,7 +17,7 @@ As mentioned above, this plugin is a dependency that doesn't provide features on 4. An element with a CSS `background-image` applied with a stylesheet (when the image is from an allowed origin). ([1](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/hooks.php#L14-L16), [2](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-background-image-styled-tag-visitor.php#L82-L83), [3](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-background-image-styled-tag-visitor.php#L135-L203), [4](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/helper.php#L83-L320), [5](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/detect.js)) 5. A `VIDEO` element's `poster` image. ([1](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php#L127-L161)) 2. Ensure `fetchpriority=high` is only added to an `IMG` when it is the LCP element across all responsive breakpoints. ([1](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php#L65-L91), [2](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php#L137-L146)) -3. Add `fetchpriority=low` to `IMG` tags which appear in the initial viewport but are not visible, such as when they are subsequent carousel slides. ([1](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php#L105-L123), [2](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php#L137-L146)) +3. Add `fetchpriority=low` to `IMG` tags which appear in the initial viewport but are not visible, such as when they are following carousel slides. ([1](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php#L105-L123), [2](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php#L137-L146)) 4. Lazy loading: 1. Apply lazy loading to `IMG` tags based on whether they appear in any breakpoint’s initial viewport. ([1](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php#L124-L133)) 2. Implement lazy loading of CSS background images added via inline `style` attributes. ([1](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-background-image-styled-tag-visitor.php#L205-L238), [2](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/helper.php#L365-L380), [3](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/lazy-load-bg-image.js)) @@ -50,5 +50,5 @@ For development and debugging, also on GitHub: * [Optimization Detective Admin UI](https://github.com/westonruter/od-admin-ui): Provides an admin UI to inspect URL Metrics from the Optimization Detective plugin. * [Optimization Detective Debug Helper](https://github.com/swissspidy/od-debug-helper/): Makes data from Optimization Detective visible on the front end through the admin bar. * [Optimization Detective Store Query Vars](https://github.com/westonruter/od-store-query-vars): Stores the Query Vars with a URL Metric in the Optimization Detective plugin. This is useful for debugging URL Metrics, in particular what the slug was computed from. -* [Optimization Detective Store User Agent](https://github.com/westonruter/od-store-user-agent): Stores the User Agent with a URL Metric in the Optimization Detective plugin. This is useful for debugging URL Metrics, in particular to understand what device has a given viewport dimensions. +* [Optimization Detective Store User Agent](https://github.com/westonruter/od-store-user-agent): Stores the User Agent with a URL Metric in the Optimization Detective plugin. This is useful for debugging URL Metrics, in particular to understand what device has a given viewport's dimensions. * [Optimization Detective Dev Mode](https://github.com/westonruter/od-dev-mode): Adds filters to facilitate development of the Optimization Detective plugin. diff --git a/plugins/optimization-detective/docs/hooks.md b/plugins/optimization-detective/docs/hooks.md index d49369473a..54f9b15f0c 100644 --- a/plugins/optimization-detective/docs/hooks.md +++ b/plugins/optimization-detective/docs/hooks.md @@ -9,7 +9,7 @@ Fires when the Optimization Detective is initializing. This action is useful for loading extension code that depends on Optimization Detective to be running. The version -of the plugin is passed as the sole argument so that if the required version is not present, the callback can short circuit. +of the plugin is passed as the sole argument so that if the required version is not present, the callback can short-circuit. Example: @@ -115,7 +115,7 @@ It is important to note that this action fires _after_ the entire template has b other words, it will fire after the `wp_footer` action. This action runs before any of the registered tag visitors have been invoked in the current response. It is useful for -an extension to gather the required information from the currently-stored URL Metrics for tag visitors to later leverage. +an extension to gather the required information from the currently stored URL Metrics for tag visitors to later leverage. See [example](https://github.com/WordPress/performance/pull/1921) from the Image Prioritizer plugin where it can be used to determine what the common external LCP background-image is for each viewport group up front so that this doesn't have to be computed when a tag visitor is invoked. @@ -187,10 +187,10 @@ The additional attribution data is made available to client-side extension scrip Filters the breakpoint max widths to group URL Metrics for various viewports. Each number represents the maximum width (inclusive) for a given breakpoint. So if there is one number, 480, then this means there will be two viewport groupings, one for 0\<=480, and another \>480. If instead there are the two breakpoints defined, 480 and 782, then this means there will be three viewport groups of URL Metrics, one for 0\<=480 (i.e. mobile), another 481\<=782 (i.e. phablet/tablet), and another \>782 (i.e. desktop). -This array may be empty in which case there are no responsive breakpoints and all URL Metrics are collected in a single group. +This array may be empty, in which case there are no responsive breakpoints, and all URL Metrics are collected in a single group. A breakpoint must be greater than zero or else a usage warning will occur. -These default breakpoints are reused from Gutenberg which appear to be used the most in media queries that affect frontend styles. +These default breakpoints are reused from Gutenberg, which appear to be used the most in media queries that affect frontend styles. ### Filter: `od_can_optimize_response` (default: boolean condition, see below) @@ -198,21 +198,35 @@ Filters whether the current response can be optimized. By default, detection and 1. It’s not a search template (`is_search()`). 2. It’s not a post embed template (`is_embed()`). -3. It’s not the Customizer preview (`is_customize_preview()`) -4. It’s not the response to a `POST` request. -5. There is at least one queried post on the page. This is used to facilitate the purging of page caches after a new URL Metric is stored. +3. It’s not a preview (`is_preview()`). +4. It’s not the Customizer preview (`is_customize_preview()`). +5. It’s not the response to a `POST` request. +6. There is at least one queried post on the page. This is used to facilitate the purging of page caches after a new URL Metric is stored. -To force every response to be optimized regardless of the conditions above, you can do: +The filter now receives an additional `$disabled_flags` parameter that contains information about which specific conditions are preventing optimization. This allows for more granular control. + +For example, to enable optimization specifically for search pages: ```php -add_filter( 'od_can_optimize_response', '__return_true' ); +add_filter( 'od_can_optimize_response', function( $can_optimize, array $disabled_flags ): bool { + if ( ! $can_optimize && $disabled_flags['is_search'] ) { + unset( $disabled_flags['is_search'] ); + $can_optimize = count( array_filter( $disabled_flags ) ) === 0; + } + return $can_optimize; +}, 10, 2 ); ``` +Note that this filter cannot override some conditions. Even if the filter returns `true`, optimization will still be disabled when: + +1. The REST API for storing URL Metrics is not available. +2. The URL has the `optimization_detective_disabled` query parameter. + ### Filter: `od_url_metrics_breakpoint_sample_size` (default: 3) Filters the sample size for a breakpoint's URL Metrics on a given URL. -The filtered value must be greater than zero; otherwise it will be ignored and a usage warning will result. +The filtered value must be greater than zero; otherwise it will be ignored, and a usage warning will result. You can increase the sample size if you want better guarantees that the applied optimizations will be accurate. During development, it may be helpful to reduce the sample size to 1 (along with setting the `od_url_metric_storage_lock_ttl` and `od_url_metric_freshness_ttl` filters below) so that you don't have to keep reloading the page to collect new URL Metrics to flush out stale ones during active development: @@ -226,7 +240,7 @@ add_filter( 'od_url_metrics_breakpoint_sample_size', function (): int { Filters how long the current IP is locked from submitting another URL metric storage REST API request. -Filtering the TTL to zero will disable any URL Metric storage locking. This is useful, for example, to disable locking when a user is logged-in with code like the following: +Filtering the TTL to zero will disable any URL Metric storage locking. This is useful, for example, to disable locking when a user is logged in with code like the following: ```php add_filter( 'od_metrics_storage_lock_ttl', function ( int $ttl ): int { @@ -236,7 +250,7 @@ add_filter( 'od_metrics_storage_lock_ttl', function ( int $ttl ): int { By default, the TTL is zero (0) for authorized users and sixty (60) for everyone else. Whether the current user is authorized is determined by whether the user has the `od_store_url_metric_now` capability. This custom capability by default maps to the `manage_options` primitive capability via the `user_has_cap` filter. -During development this is useful to set to zero so you can quickly collect new URL Metrics by reloading the page without having to wait for the storage lock to release: +During development this is useful to set to zero, so you can quickly collect new URL Metrics by reloading the page without having to wait for the storage lock to release: ```php add_filter( 'od_metrics_storage_lock_ttl', function ( int $ttl ): int { @@ -274,7 +288,7 @@ add_filter( 'od_url_metric_freshness_ttl', '__return_zero' ); Filters the minimum allowed viewport aspect ratio for URL Metrics. -The 0.4 value is intended to accommodate the phone with the greatest known aspect ratio at 21:9 when rotated 90 degrees to 9:21 (0.429). During development when you have the DevTools console open on the right, the viewport aspect ratio will be smaller than normal. In this case, you may want to set this to 0: +The 0.4 value is intended to accommodate the phone with the greatest known aspect ratio at 21:9 when rotated 90 degrees to 9:21 (0.429). During development, when you have the DevTools console open on the right, the viewport aspect ratio will be smaller than normal. In this case, you may want to set this to 0: ```php add_filter( 'od_minimum_viewport_aspect_ratio', static function (): int { @@ -298,13 +312,15 @@ add_filter( 'od_maximum_viewport_aspect_ratio', static function (): int { ### Filter: `od_template_output_buffer` (default: the HTML response) -Filters the template output buffer prior to sending to the client. This filter is added to implement [\#43258](https://core.trac.wordpress.org/ticket/43258) in WordPress core. +Filters the template output buffer before sending it to the client. + +This filter is added to implement [\#43258](https://core.trac.wordpress.org/ticket/43258) in WordPress core. ### Filter: `od_url_metric_schema_element_item_additional_properties` (default: empty array) Filters additional schema properties which should be allowed for an element's item in a URL Metric. -For example to add a `resizedBoundingClientRect` property: +For example, to add a `resizedBoundingClientRect` property: ```php append_head_html()` and `$processor->append_body_html()`. These are useful, for example, to inject additional styles and scripts into the page. The Optimization Detective subclass does implement some methods which are otherwise only available on `WP_HTML_Processor`: * `expects_closer()`: Whether the tag expects a closing tag. -* `get_breadcrumbs()`: Computes the HTML breadcrumbs for the currently-matched node. +* `get_breadcrumbs()`: Computes the HTML breadcrumbs for the currently matched node. * `get_current_depth()`: Returns the nesting depth of the current location in the document. In addition to these, the following methods are also implemented which are useful for tag visitors: @@ -440,13 +440,13 @@ Note that by default the [“standard” build](https://github.com/GoogleChrome/ When the `WP_DEBUG` constant is enabled, additional logging for Optimization Detective is added to the browser console. -During the development of Optimization Detective extensions it’s recommended that you install the [Development Mode](https://github.com/westonruter/od-dev-mode) plugin which will: +During the development of Optimization Detective extensions, it’s recommended that you install the [Development Mode](https://github.com/westonruter/od-dev-mode) plugin which will: * Zero out the storage lock and freshness TTLs to allow new URL Metrics to be collected with each page load. * Reduce the sample size from 3 to 1. * Disable aspect ratio constraints to allow URL Metrics to be collected when DevTools is open, for example. -Then to actually inspect the contents of the URL Metrics which are collected, it’s recommended you install the [Admin UI](https://github.com/westonruter/od-admin-ui) plugin. +Then, to actually inspect the contents of the URL Metrics which are collected, it’s recommended you install the [Admin UI](https://github.com/westonruter/od-admin-ui) plugin. # Highlighted Extensions @@ -519,7 +519,7 @@ After: > Optimizes the performance of embeds through lazy loading, preconnecting, and reserving space to reduce layout shifts. -Embeds are very resource intensive components on a page, both in terms of their network usage and in their CPU load. The page load time suffers when out-of-viewport embeds compete to load alongside assets displayed in the initial viewport. This can be seen here in this profile of a page in which embeds from YouTube, Twitter, and TikTok are on the page but aren’t in the initial viewport: +Embeds are very resource-intensive components on a page, both in terms of their network usage and in their CPU load. The page load time suffers when out-of-viewport embeds compete to load alongside assets displayed in the initial viewport. This can be seen here in this profile of a page in which embeds from YouTube, Twitter, and TikTok are on the page but aren’t in the initial viewport: ![Lazy-loading embeds: before. Profile of a page with embeds from YouTube, Twitter, and TikTok which are all outside the viewport, showing significant main thread activity and 26 megabytes downloaded over the network.](images/lazy-loading-embeds-before.png) @@ -540,11 +540,11 @@ The reduction in main thread work is dramatic: Additionally, there is a 99% reduction in the number of bytes downloaded over the network, from 26.3 MB to 152 kB. The `load` event also goes from firing at 1.15 seconds down to 245 ms. -The other major performance optimization implemented by Embed Optimizer is the reduction in CLS by reserving space for embeds that resize when loading. This was discussed above as part of Client-side Extensions but here is the impact on CLS for loading a tweet: +The other major performance optimization implemented by Embed Optimizer is the reduction in CLS by reserving space for embeds that resize when loading. This was discussed above as part of Client-side Extensions, but here is the impact on CLS for loading a tweet: | Before | After | |:---------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------| | ![Tweet embed showing significant layout shift once it loads](images/tweet-embed-before.gif) | ![Tweet embed showing now layout shift once it loads due to the space being reserved](images/tweet-embed-after.gif) | | CLS 0.15 ⚠️ | CLS 0.00 ✅ | -To see how these optimizations were implemented in Image Prioritizer and Embed Optimizer, refer to the previously-mentioned [reference](https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/extensions.md#use-cases-and-examples). The same docs page also includes a [list of extension plugins](https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/extensions.md#extension-plugins), some of which are experimental and others which are helpful for development and debugging. +To see how these optimizations were implemented in Image Prioritizer and Embed Optimizer, refer to the previously mentioned [reference](https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/extensions.md#use-cases-and-examples). The same docs page also includes a [list of extension plugins](https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/extensions.md#extension-plugins), some of which are experimental and others which are helpful for development and debugging. diff --git a/plugins/optimization-detective/helper.php b/plugins/optimization-detective/helper.php index 5d701f13a4..75f985102a 100644 --- a/plugins/optimization-detective/helper.php +++ b/plugins/optimization-detective/helper.php @@ -61,7 +61,122 @@ function od_generate_media_query( ?int $minimum_viewport_width, ?int $maximum_vi } /** - * Displays the HTML generator meta tag for the Optimization Detective plugin. + * Gets the reasons why Optimization Detective is disabled for the current response. + * + * @since n.e.x.t + * @access private + * + * @return array{ + * is_search?: string, + * is_embed?: string, + * is_preview?: string, + * is_customize_preview?: string, + * non_get_request?: string, + * no_cache_purge_post_id?: string, + * filter_disabled?: string, + * rest_api_unavailable?: string, + * query_param_disabled?: string + * } Array of disabled reason codes and their messages. + */ +function od_get_disabled_reasons(): array { + $disabled_flags = array( + 'is_search' => false, + 'is_embed' => false, + 'is_preview' => false, + 'is_customize_preview' => false, + 'non_get_request' => false, + 'no_cache_purge_post_id' => false, + ); + + // Disable the search template since there is no predictability in whether posts in the loop will have featured images assigned or not. If a + // theme template for search results doesn't even show featured images, then this wouldn't be an issue. + if ( is_search() ) { + $disabled_flags['is_search'] = true; + } + + // Avoid optimizing embed responses because the Post Embed iframes include a sandbox attribute with the value of + // "allow-scripts" but without "allow-same-origin". This can result in an error in the console: + // > Access to script at '.../detect.js?ver=0.4.1' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. + // So it's better to just avoid attempting to optimize Post Embed responses (which don't need optimization anyway). + if ( is_embed() ) { + $disabled_flags['is_embed'] = true; + } + + // Skip posts that aren't published yet. + if ( is_preview() ) { + $disabled_flags['is_preview'] = true; + } + + // Disable in Customizer preview since injection of inline-editing controls can interfere with XPath. Optimization is also not necessary in this context. + if ( is_customize_preview() ) { + $disabled_flags['is_customize_preview'] = true; + } + + // Disable for POST responses since they cannot, by definition, be cached. + if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'GET' !== $_SERVER['REQUEST_METHOD'] ) { + $disabled_flags['non_get_request'] = true; + } + + // Disable when there is no post ID available for cache purging. Page caching plugins can only reliably be told to invalidate a cached page when a post is available to trigger + // the relevant actions on. + if ( null === od_get_cache_purge_post_id() ) { + $disabled_flags['no_cache_purge_post_id'] = true; + } + + // Check if any flags are set to true. + $has_disabled_flags = count( array_filter( $disabled_flags ) ) > 0; + + /** + * Filters whether the current response can be optimized. + * + * @since 0.1.0 + * @since n.e.x.t Added $disabled_flags parameter + * @link https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/hooks.md#:~:text=Filter%3A%20od_can_optimize_response + * + * @param bool $can_optimize Whether response can be optimized. + * @param array{ + * is_search: bool, + * is_embed: bool, + * is_preview: bool, + * is_customize_preview: bool, + * non_get_request: bool, + * no_cache_purge_post_id: bool + * } $disabled_flags Flags indicating which conditions are disabling optimization. + */ + $can_optimize = (bool) apply_filters( 'od_can_optimize_response', ! $has_disabled_flags, $disabled_flags ); + + $reasons = array(); + if ( ! $can_optimize ) { + $reason_messages = array( + 'is_search' => __( 'Page is not optimized because it is a search results page.', 'optimization-detective' ), + 'is_embed' => __( 'Page is not optimized because it is an embed.', 'optimization-detective' ), + 'is_preview' => __( 'Page is not optimized because it is a preview.', 'optimization-detective' ), + 'is_customize_preview' => __( 'Page is not optimized because it is a customize preview.', 'optimization-detective' ), + 'non_get_request' => __( 'Page is not optimized because it is not a GET request.', 'optimization-detective' ), + 'no_cache_purge_post_id' => __( 'Page is not optimized because there is no post ID available for cache purging.', 'optimization-detective' ), + ); + + $reasons = wp_array_slice_assoc( $reason_messages, array_keys( array_filter( $disabled_flags ) ) ); + + // If no technical reasons but optimization still disabled, it's because of the filter. + if ( 0 === count( $reasons ) ) { + $reasons['filter_disabled'] = __( 'Page is not optimized because the od_can_optimize_response filter returned false.', 'optimization-detective' ); + } + } + + if ( od_is_rest_api_unavailable() && ! ( wp_get_environment_type() === 'local' && ! function_exists( 'tests_add_filter' ) ) ) { + $reasons['rest_api_unavailable'] = __( 'Page is not optimized because the REST API for storing URL Metrics is not available.', 'optimization-detective' ); + } + + if ( isset( $_GET['optimization_detective_disabled'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $reasons['query_param_disabled'] = __( 'Page is not optimized because the URL has the optimization_detective_disabled query parameter.', 'optimization-detective' ); + } + + return $reasons; +} + +/** + * Displays the HTML generator META tag for the Optimization Detective plugin. * * See {@see 'wp_head'}. * @@ -72,9 +187,11 @@ function od_render_generator_meta_tag(): void { // Use the plugin slug as it is immutable. $content = 'optimization-detective ' . OPTIMIZATION_DETECTIVE_VERSION; - // Indicate that the plugin will not be doing anything because the REST API is unavailable. - if ( od_is_rest_api_unavailable() ) { - $content .= '; rest_api_unavailable'; + // Add any reasons why Optimization Detective is disabled. + $disabled_reasons = od_get_disabled_reasons(); + if ( count( $disabled_reasons ) > 0 ) { + $flags = array_keys( $disabled_reasons ); + $content .= '; ' . implode( '; ', $flags ); } echo '' . "\n"; @@ -86,7 +203,7 @@ function od_render_generator_meta_tag(): void { * @since 0.9.0 * @access private * - * @param string $src_path Source path, relative to plugin root. + * @param string $src_path Source path, relative to the plugin root. * @param string|null $min_path Minified path. If not supplied, then '.min' is injected before the file extension in the source path. * @return string URL to script or stylesheet. * diff --git a/plugins/optimization-detective/optimization.php b/plugins/optimization-detective/optimization.php index 25c9c7bfe3..35e0afc72c 100644 --- a/plugins/optimization-detective/optimization.php +++ b/plugins/optimization-detective/optimization.php @@ -17,7 +17,7 @@ * * This is to implement #43258 in core. * - * This is a hack which would eventually be replaced with something like this in wp-includes/template-loader.php: + * This is a hack that would eventually be replaced with something like this in wp-includes/template-loader.php: * * $template = apply_filters( 'template_include', $template ); * + ob_start( 'wp_template_output_buffer_callback' ); @@ -40,7 +40,7 @@ function od_buffer_output( $passthrough ) { * response as an HTML document, this would result in broken HTML processing. * * If this ends up being problematic, then PHP_OUTPUT_HANDLER_FLUSHABLE could be added to the $flags and the - * output buffer callback could check if the phase is PHP_OUTPUT_HANDLER_FLUSH and abort any subsequent + * output buffer callback could check if the phase is PHP_OUTPUT_HANDLER_FLUSH and abort any later * processing while also emitting a _doing_it_wrong(). * * The output buffer needs to be removable because WordPress calls wp_ob_end_flush_all() and then calls @@ -52,13 +52,13 @@ function od_buffer_output( $passthrough ) { ob_start( static function ( string $output, ?int $phase ): string { - // When the output is being cleaned (e.g. pending template is replaced with error page), do not send it through the filter. + // When the output is being cleaned (e.g. the pending template is replaced with an error page), do not send it through the filter. if ( ( $phase & PHP_OUTPUT_HANDLER_CLEAN ) !== 0 ) { return $output; } /** - * Filters the template output buffer prior to sending to the client. + * Filters the template output buffer before sending it to the client. * * @since 0.1.0 * @link https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/hooks.md#:~:text=Filter%3A%20od_template_output_buffer @@ -81,32 +81,13 @@ static function ( string $output, ?int $phase ): string { * @access private */ function od_maybe_add_template_output_buffer_filter(): void { - $conditions = array( - array( - 'test' => od_can_optimize_response(), - 'reason' => __( 'Page is not optimized because od_can_optimize_response() returned false. This can be overridden with the od_can_optimize_response filter.', 'optimization-detective' ), - ), - array( - 'test' => ! od_is_rest_api_unavailable() || ( wp_get_environment_type() === 'local' && ! function_exists( 'tests_add_filter' ) ), - 'reason' => __( 'Page is not optimized because the REST API for storing URL Metrics is not available.', 'optimization-detective' ), - ), - array( - 'test' => ! isset( $_GET['optimization_detective_disabled'] ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended - 'reason' => __( 'Page is not optimized because the URL has the optimization_detective_disabled query parameter.', 'optimization-detective' ), - ), - ); - $reasons = array(); - foreach ( $conditions as $condition ) { - if ( ! $condition['test'] ) { - $reasons[] = $condition['reason']; - } - } - if ( count( $reasons ) > 0 ) { + $disabled_reasons = od_get_disabled_reasons(); + if ( count( $disabled_reasons ) > 0 ) { if ( WP_DEBUG ) { add_action( 'wp_print_footer_scripts', - static function () use ( $reasons ): void { - od_print_disabled_reasons( $reasons ); + static function () use ( $disabled_reasons ): void { + od_print_disabled_reasons( array_values( $disabled_reasons ) ); } ); } @@ -141,7 +122,7 @@ function od_print_disabled_reasons( array $reasons ): void { wp_print_inline_script_tag( sprintf( 'console.info( %s );', - (string) wp_json_encode( '[Optimization Detective] ' . $reason ) + wp_json_encode( '[Optimization Detective] ' . $reason ) ), array( 'type' => 'module' ) ); @@ -159,35 +140,7 @@ function od_print_disabled_reasons( array $reasons ): void { * @return bool Whether response can be optimized. */ function od_can_optimize_response(): bool { - $able = ! ( - // Since there is no predictability in whether posts in the loop will have featured images assigned or not. If a - // theme template for search results doesn't even show featured images, then this wouldn't be an issue. - is_search() || - // Avoid optimizing embed responses because the Post Embed iframes include a sandbox attribute with the value of - // "allow-scripts" but without "allow-same-origin". This can result in an error in the console: - // > Access to script at '.../detect.js?ver=0.4.1' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. - // So it's better to just avoid attempting to optimize Post Embed responses (which don't need optimization anyway). - is_embed() || - // Skip posts that aren't published yet. - is_preview() || - // Since injection of inline-editing controls interfere with breadcrumbs, while also just not necessary in this context. - is_customize_preview() || - // Since the images detected in the response body of a POST request cannot, by definition, be cached. - ( isset( $_SERVER['REQUEST_METHOD'] ) && 'GET' !== $_SERVER['REQUEST_METHOD'] ) || - // Page caching plugins can only reliably be told to invalidate a cached page when a post is available to trigger - // the relevant actions on. - null === od_get_cache_purge_post_id() - ); - - /** - * Filters whether the current response can be optimized. - * - * @since 0.1.0 - * @link https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/hooks.md#:~:text=Filter%3A%20od_can_optimize_response - * - * @param bool $able Whether response can be optimized. - */ - return (bool) apply_filters( 'od_can_optimize_response', $able ); + return count( od_get_disabled_reasons() ) === 0; } /** diff --git a/plugins/optimization-detective/site-health.php b/plugins/optimization-detective/site-health.php index d3ad5f228e..4e953e1ecc 100644 --- a/plugins/optimization-detective/site-health.php +++ b/plugins/optimization-detective/site-health.php @@ -66,7 +66,7 @@ function od_test_rest_api_availability(): array { * then at this point the check will be performed at {@see od_maybe_run_rest_api_health_check()}. In practice, this will * happen immediately after the user activates a plugin since the user is redirected back to the plugin list table in * the admin. The reason for storing the negative unavailable state as opposed to the positive available state is that - * when an option does not exist then `get_option()` returns `false` which is the same falsy value as the stored `'0'`. + * when an option does not exist, then `get_option()` returns `false` which is the same falsy value as the stored `'0'`. * * @since 1.0.0 * @access private @@ -90,7 +90,7 @@ function od_compose_site_health_result( $response ): array { $common_description_html = '

' . wp_kses( sprintf( /* translators: %s is the REST API endpoint */ - __( 'To collect URL Metrics from visitors the REST API must be available to unauthenticated users. Specifically, visitors must be able to perform a POST request to the %s endpoint.', 'optimization-detective' ), + __( 'To collect URL Metrics from visitors, the REST API must be available to unauthenticated users. Specifically, visitors must be able to perform a POST request to the %s endpoint.', 'optimization-detective' ), '/' . OD_REST_URL_Metrics_Store_Endpoint::ROUTE_NAMESPACE . OD_REST_URL_Metrics_Store_Endpoint::ROUTE_BASE ), array( 'code' => array() ) diff --git a/plugins/optimization-detective/storage/class-od-rest-url-metrics-store-endpoint.php b/plugins/optimization-detective/storage/class-od-rest-url-metrics-store-endpoint.php index 65b7e9a71b..9b6520289e 100644 --- a/plugins/optimization-detective/storage/class-od-rest-url-metrics-store-endpoint.php +++ b/plugins/optimization-detective/storage/class-od-rest-url-metrics-store-endpoint.php @@ -126,7 +126,7 @@ public function store_permissions_check() { } /** - * Determines if the HTTP origin is an authorized one. + * Determines if the HTTP origin is authorized. * * Note that `is_allowed_http_origin()` is not used directly because the underlying `get_allowed_http_origins()` does * not account for the URL port (although there is a to-do comment committed in core to address this). Additionally, @@ -157,7 +157,7 @@ protected static function is_allowed_http_origin( string $origin ): bool { * @return WP_REST_Response|WP_Error Response. */ public function handle_rest_request( WP_REST_Request $request ) { - // Block cross-origin storage requests since by definition URL Metrics data can only be sourced from the frontend of the site. + // Block cross-origin storage requests since, by definition, URL Metrics data can only be sourced from the frontend of the site. $origin = $request->get_header( 'origin' ); if ( null === $origin || ! self::is_allowed_http_origin( $origin ) ) { return new WP_Error( @@ -222,7 +222,7 @@ public function handle_rest_request( WP_REST_Request $request ) { return new WP_Error( 'rest_invalid_param', sprintf( - /* translators: %s is exception message */ + /* translators: %s is the exception message */ __( 'Failed to validate URL Metric: %s', 'optimization-detective' ), $e->getMessage() ), diff --git a/plugins/optimization-detective/storage/class-od-storage-lock.php b/plugins/optimization-detective/storage/class-od-storage-lock.php index 7eca1ff83b..129a74aad8 100644 --- a/plugins/optimization-detective/storage/class-od-storage-lock.php +++ b/plugins/optimization-detective/storage/class-od-storage-lock.php @@ -79,7 +79,7 @@ public static function get_ttl(): int { } /** - * Gets transient key for locking URL Metric storage (for the current IP). + * Gets the transient key for locking URL Metric storage (for the current IP). * * @since 0.1.0 * diff --git a/plugins/optimization-detective/storage/class-od-url-metric-store-request-context.php b/plugins/optimization-detective/storage/class-od-url-metric-store-request-context.php index a43c811e2d..da773e58f2 100644 --- a/plugins/optimization-detective/storage/class-od-url-metric-store-request-context.php +++ b/plugins/optimization-detective/storage/class-od-url-metric-store-request-context.php @@ -116,7 +116,7 @@ public function __get( string $name ) { esc_html( __CLASS__ . '::$' . $name ), esc_html( sprintf( - /* translators: %s is class member variable name */ + /* translators: %s is the class member variable name */ __( 'Use %s instead.', 'optimization-detective' ), __CLASS__ . '::$url_metrics_id' ) @@ -128,7 +128,7 @@ public function __get( string $name ) { throw new Error( esc_html( sprintf( - /* translators: %s is class member variable name */ + /* translators: %s is the class member variable name */ __( 'Unknown property %s.', 'optimization-detective' ), __CLASS__ . '::$' . $name ) diff --git a/plugins/optimization-detective/storage/class-od-url-metrics-post-type.php b/plugins/optimization-detective/storage/class-od-url-metrics-post-type.php index 4c419be669..85876fe05a 100644 --- a/plugins/optimization-detective/storage/class-od-url-metrics-post-type.php +++ b/plugins/optimization-detective/storage/class-od-url-metrics-post-type.php @@ -58,7 +58,7 @@ public static function add_hooks(): void { /** * Registers post type for URL Metrics storage. * - * This the configuration for this post type is similar to the oembed_cache in core. + * The configuration for this post type is similar to the oembed_cache in core. * * @since 0.1.0 */ @@ -152,7 +152,7 @@ public static function get_url_metrics_from_post( WP_Post $post ): array { } elseif ( ! is_array( $url_metrics_data ) ) { $trigger_error( sprintf( - /* translators: %s is post type slug */ + /* translators: %s is the post type slug */ __( 'Contents of %s post type was not a JSON array.', 'optimization-detective' ), self::SLUG ), diff --git a/plugins/optimization-detective/storage/data.php b/plugins/optimization-detective/storage/data.php index b42536e9d9..36d2f4c31a 100644 --- a/plugins/optimization-detective/storage/data.php +++ b/plugins/optimization-detective/storage/data.php @@ -15,7 +15,7 @@ /** * Gets the freshness age (TTL) for a given URL Metric. * - * When a URL Metric expires it is eligible to be replaced by a newer one if its viewport lies within the same breakpoint. + * When a URL Metric expires, it is eligible to be replaced by a newer one if its viewport lies within the same breakpoint. * * @since 0.1.0 * @access private @@ -67,7 +67,7 @@ function od_get_normalized_query_vars(): array { * Get the URL for the current request. * * This is essentially the REQUEST_URI prefixed by the scheme and host for the home URL. - * This is needed in particular due to subdirectory installs. + * This is needed in particular due to subdirectory installations. * * @since 0.1.1 * @access private @@ -135,7 +135,7 @@ function od_get_current_theme_template() { global $template, $_wp_current_template_id; if ( wp_is_block_theme() && isset( $_wp_current_template_id ) ) { - $block_template = get_block_template( $_wp_current_template_id, 'wp_template' ); + $block_template = get_block_template( $_wp_current_template_id ); if ( $block_template instanceof WP_Block_Template ) { return $block_template; } @@ -337,7 +337,7 @@ function od_get_maximum_viewport_aspect_ratio(): float { * * Each number represents the maximum width (inclusive) for a given breakpoint. So if there is one number, 480, then * this means there will be two viewport groupings, one for 0<=480, and another >480. If instead there were three - * provided breakpoints (320, 480, 576) then this means there will be four groups: + * provided breakpoints (320, 480, 576), then this means there will be four groups: * * 1. 0-320 (small smartphone) * 2. 321-480 (normal smartphone) @@ -352,7 +352,7 @@ function od_get_maximum_viewport_aspect_ratio(): float { * * These breakpoints appear to be used the most in media queries that affect frontend styles. * - * This array may be empty in which case there are no responsive breakpoints and all URL Metrics are collected in a + * This array may be empty, in which case there are no responsive breakpoints, and all URL Metrics are collected in a * single group. * * @since 0.1.0 diff --git a/plugins/optimization-detective/tests/test-helper.php b/plugins/optimization-detective/tests/test-helper.php index 4509624fa3..59ae425c51 100644 --- a/plugins/optimization-detective/tests/test-helper.php +++ b/plugins/optimization-detective/tests/test-helper.php @@ -84,7 +84,7 @@ public function test_od_generate_media_query( ?int $min_width, ?int $max_width, } /** - * Test printing the meta generator tag. + * Test printing the META generator tag. * * @covers ::od_render_generator_meta_tag */ @@ -98,9 +98,23 @@ public function test_od_render_generator_meta_tag(): void { } /** - * Test printing the meta generator tag when the REST API is not available. + * Test META generator tag when query parameter is present. * * @covers ::od_render_generator_meta_tag + * @covers ::od_get_disabled_reasons + */ + public function test_od_render_generator_meta_tag_query_param_disabled(): void { + $_GET['optimization_detective_disabled'] = '1'; + $tag = get_echo( 'od_render_generator_meta_tag' ); + $this->assertStringContainsString( '; query_param_disabled', $tag ); + unset( $_GET['optimization_detective_disabled'] ); + } + + /** + * Test printing the META generator tag when the REST API is not available. + * + * @covers ::od_render_generator_meta_tag + * @covers ::od_get_disabled_reasons */ public function test_od_render_generator_meta_tag_rest_api_unavailable(): void { update_option( 'od_rest_api_unavailable', '1' ); diff --git a/plugins/optimization-detective/tests/test-optimization.php b/plugins/optimization-detective/tests/test-optimization.php index d4789d208f..b27f43efdc 100644 --- a/plugins/optimization-detective/tests/test-optimization.php +++ b/plugins/optimization-detective/tests/test-optimization.php @@ -50,7 +50,7 @@ public function test_od_buffer_output(): void { $original = 'Hello World!'; $expected = '¡Hola Mundo!'; - // In order to test, a wrapping output buffer is required because ob_get_clean() does not invoke the output + // To test, a wrapping output buffer is required because ob_get_clean() does not invoke the output // buffer callback. See . ob_start(); @@ -88,7 +88,7 @@ public function test_od_buffer_with_cleaning_and_attempted_flushing(): void { $template_middle = ', the middle'; $template_end = ', and the end!'; - // In order to test, a wrapping output buffer is required because ob_get_clean() does not invoke the output + // To test, a wrapping output buffer is required because ob_get_clean() does not invoke the output // buffer callback. See . $initial_level = ob_get_level(); $this->assertTrue( ob_start() ); @@ -177,6 +177,34 @@ public function data_provider_test_od_maybe_add_template_output_buffer_filter(): }, 'expected_has_filter' => true, ), + 'search_enabled_by_filter_using_flags' => array( + 'set_up' => function (): string { + // This is needed because otherwise no_cache_purge_post_id will be true. + self::factory()->post->create( array( 'post_title' => 'foo' ) ); + + add_filter( + 'od_can_optimize_response', + function ( $can_optimize, array $disabled_flags ): bool { + $expected_keys = array( 'is_search', 'is_embed', 'is_preview', 'is_customize_preview', 'non_get_request', 'no_cache_purge_post_id' ); + $this->assertCount( count( $expected_keys ), $disabled_flags ); + foreach ( $expected_keys as $key ) { + $this->assertArrayHasKey( $key, $disabled_flags ); + $this->assertIsBool( $disabled_flags[ $key ] ); + } + + if ( ! $can_optimize && $disabled_flags['is_search'] ) { + unset( $disabled_flags['is_search'] ); + $can_optimize = count( array_filter( $disabled_flags ) ) === 0; + } + return $can_optimize; + }, + 10, + 2 + ); + return home_url( '/?s=foo' ); + }, + 'expected_has_filter' => true, + ), 'home_disabled_by_get_param' => array( 'set_up' => static function (): string { return home_url( '/?optimization_detective_disabled=1' ); @@ -199,7 +227,7 @@ public function data_provider_test_od_maybe_add_template_output_buffer_filter(): * @dataProvider data_provider_test_od_maybe_add_template_output_buffer_filter * * @covers ::od_maybe_add_template_output_buffer_filter - * @covers ::od_can_optimize_response + * @covers ::od_get_disabled_reasons * @covers ::od_is_rest_api_unavailable */ public function test_od_maybe_add_template_output_buffer_filter( Closure $set_up, bool $expected_has_filter ): void { @@ -281,6 +309,13 @@ public function data_provider_test_od_can_optimize_response(): array { }, 'expected' => false, ), + 'post_embed_as_anonymous' => array( + 'set_up' => static function (): string { + $post_id = self::factory()->post->create( array( 'post_title' => 'Hello' ) ); + return (string) get_post_embed_url( $post_id ); + }, + 'expected' => false, + ), 'home_customizer_preview_as_anonymous' => array( 'set_up' => static function (): string { global $wp_customize; @@ -341,6 +376,7 @@ public function data_provider_test_od_can_optimize_response(): array { * Test od_can_optimize_response(). * * @covers ::od_can_optimize_response + * @covers ::od_get_disabled_reasons * @covers ::od_get_cache_purge_post_id * * @dataProvider data_provider_test_od_can_optimize_response @@ -353,6 +389,27 @@ public function test_od_can_optimize_response( Closure $set_up, bool $expected ) $url = $set_up(); $this->go_to( $url ); $this->assertSame( $expected, od_can_optimize_response() ); + $disabled_reasons = od_get_disabled_reasons(); + $possible_keys = array( + 'is_search', + 'is_embed', + 'is_preview', + 'is_customize_preview', + 'non_get_request', + 'no_cache_purge_post_id', + 'filter_disabled', + 'rest_api_unavailable', + 'query_param_disabled', + ); + foreach ( $disabled_reasons as $key => $reason ) { + $this->assertContains( $key, $possible_keys ); + $this->assertIsString( $reason ); + } + if ( $expected ) { + $this->assertCount( 0, $disabled_reasons ); + } else { + $this->assertNotCount( 0, $disabled_reasons ); + } } /** diff --git a/plugins/optimization-detective/uninstall.php b/plugins/optimization-detective/uninstall.php index d7f657d5d0..3400386724 100644 --- a/plugins/optimization-detective/uninstall.php +++ b/plugins/optimization-detective/uninstall.php @@ -26,8 +26,8 @@ $od_delete_site_data(); /* - * For a multisite, delete the URL Metrics for all other sites (however limited to 100 sites to avoid memory limit or - * timeout problems in large scale networks). + * For a multisite install, delete the URL Metrics for all other sites (however, this is limited to 100 sites to avoid memory limit + * and timeout problems in large scale networks). */ if ( is_multisite() ) { $site_ids = get_sites( diff --git a/plugins/view-transitions/hooks.php b/plugins/view-transitions/hooks.php index df3903f708..7c8981cca0 100644 --- a/plugins/view-transitions/hooks.php +++ b/plugins/view-transitions/hooks.php @@ -29,4 +29,5 @@ function plvt_render_generator(): void { * Filters related to the View Transitions functionality. */ add_action( 'after_setup_theme', 'plvt_polyfill_theme_support', PHP_INT_MAX ); +add_action( 'init', 'plvt_sanitize_view_transitions_theme_support', 1 ); add_action( 'wp_enqueue_scripts', 'plvt_load_view_transitions' ); diff --git a/plugins/view-transitions/includes/functions.php b/plugins/view-transitions/includes/functions.php new file mode 100644 index 0000000000..7a57830199 --- /dev/null +++ b/plugins/view-transitions/includes/functions.php @@ -0,0 +1,53 @@ + $_wp_theme_features Theme support features added and their arguments. + */ +function plvt_sanitize_view_transitions_theme_support(): void { + global $_wp_theme_features; + + if ( ! isset( $_wp_theme_features['view-transitions'] ) ) { + return; + } + + $args = $_wp_theme_features['view-transitions']; + + $defaults = array( + 'post-selector' => '.wp-block-post.post, article.post, body.single main', + 'global-transition-names' => array( + 'header' => 'header', + 'main' => 'main', + ), + 'post-transition-names' => array( + '.wp-block-post-title, .entry-title' => 'post-title', + '.wp-post-image' => 'post-thumbnail', + '.wp-block-post-content, .entry-content' => 'post-content', + ), + ); + + // If no specific `$args` were provided, simply use the defaults. + if ( true === $args ) { + $args = $defaults; + } else { + /* + * By default, `add_theme_support()` will take all function parameters as `$args`, but for the + * 'view-transitions' feature, only a single associative array of arguments is relevant, which is expected as + * the sole (optional) parameter. + */ + if ( count( $args ) === 1 && isset( $args[0] ) && is_array( $args[0] ) ) { + $args = $args[0]; + } + + $args = wp_parse_args( $args, $defaults ); + + // Enforce correct types. + if ( ! is_array( $args['global-transition-names'] ) ) { + $args['global-transition-names'] = array(); + } + if ( ! is_array( $args['post-transition-names'] ) ) { + $args['post-transition-names'] = array(); + } + } + + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $_wp_theme_features['view-transitions'] = $args; +} + /** * Loads view transitions based on the current configuration. * @@ -42,4 +103,44 @@ function plvt_load_view_transitions(): void { wp_register_style( 'wp-view-transitions', false, array(), null ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion wp_add_inline_style( 'wp-view-transitions', $stylesheet ); wp_enqueue_style( 'wp-view-transitions' ); + + $theme_support = get_theme_support( 'view-transitions' ); + + /* + * No point in loading the script if no specific view transition names are configured. + */ + if ( + ( ! is_array( $theme_support['global-transition-names'] ) || count( $theme_support['global-transition-names'] ) === 0 ) && + ( ! is_array( $theme_support['post-transition-names'] ) || count( $theme_support['post-transition-names'] ) === 0 ) + ) { + return; + } + + $config = array( + 'postSelector' => $theme_support['post-selector'], + 'globalTransitionNames' => $theme_support['global-transition-names'], + 'postTransitionNames' => $theme_support['post-transition-names'], + ); + + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + $src_script = file_get_contents( plvt_get_asset_path( 'js/view-transitions.js' ) ); + if ( false === $src_script || '' === $src_script ) { + // This clause should never be entered, but is needed to please PHPStan. Can't hurt to be safe. + return; + } + + $init_script = sprintf( + 'plvtInitViewTransitions( %s )', + wp_json_encode( $config, JSON_FORCE_OBJECT ) + ); + + /* + * This must be in the , not in the footer. + * This is because the pagereveal event listener must be added before the first rAF occurs since that is when the event fires. See . + * An inline script is used to avoid an extra request. + */ + wp_register_script( 'wp-view-transitions', false, array(), null, array() ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion + wp_add_inline_script( 'wp-view-transitions', $src_script ); + wp_add_inline_script( 'wp-view-transitions', $init_script ); + wp_enqueue_script( 'wp-view-transitions' ); } diff --git a/plugins/view-transitions/js/types.ts b/plugins/view-transitions/js/types.ts new file mode 100644 index 0000000000..678ca40a52 --- /dev/null +++ b/plugins/view-transitions/js/types.ts @@ -0,0 +1,21 @@ +export type ViewTransitionsConfig = { + postSelector?: string; + globalTransitionNames?: Record< string, string >; + postTransitionNames?: Record< string, string >; +}; + +export type InitViewTransitionsFunction = ( + config: ViewTransitionsConfig +) => void; + +declare global { + interface Window { + plvtInitViewTransitions?: InitViewTransitionsFunction; + navigation?: { + activation: NavigationActivation; + }; + } +} + +export type PageSwapListenerFunction = ( event: PageSwapEvent ) => void; +export type PageRevealListenerFunction = ( event: PageRevealEvent ) => void; diff --git a/plugins/view-transitions/js/view-transitions.js b/plugins/view-transitions/js/view-transitions.js new file mode 100644 index 0000000000..07185097a3 --- /dev/null +++ b/plugins/view-transitions/js/view-transitions.js @@ -0,0 +1,197 @@ +/** + * @typedef {import("./types.ts").ViewTransitionsConfig} ViewTransitionsConfig + * @typedef {import("./types.ts").InitViewTransitionsFunction} InitViewTransitionsFunction + * @typedef {import("./types.ts").PageSwapListenerFunction} PageSwapListenerFunction + * @typedef {import("./types.ts").PageRevealListenerFunction} PageRevealListenerFunction + */ + +/** + * Initializes view transitions for the current URL. + * + * @type {InitViewTransitionsFunction} + * @param {ViewTransitionsConfig} config - The view transitions configuration. + */ +window.plvtInitViewTransitions = ( config ) => { + if ( ! window.navigation || ! ( 'CSSViewTransitionRule' in window ) ) { + window.console.warn( + 'View transitions not loaded as the browser is lacking support.' + ); + return; + } + + /** + * Gets all view transition entries relevant for a view transition. + * + * @param {Element} bodyElement The body element. + * @param {Element|null} articleElement The post element relevant for the view transition, if any. + * @return {Array[]} View transition entries with each one containing the element and its view transition name. + */ + const getViewTransitionEntries = ( bodyElement, articleElement ) => { + const globalEntries = Object.entries( + config.globalTransitionNames || {} + ).map( ( [ selector, name ] ) => { + const element = bodyElement.querySelector( selector ); + return [ element, name ]; + } ); + + const postEntries = articleElement + ? Object.entries( config.postTransitionNames || {} ).map( + ( [ selector, name ] ) => { + const element = + articleElement.querySelector( selector ); + return [ element, name ]; + } + ) + : []; + + return [ ...globalEntries, ...postEntries ]; + }; + + /** + * Temporarily sets view transition names for the given entries until the view transition has been completed. + * + * @param {Array[]} entries View transition entries as received from `getViewTransitionEntries()`. + * @param {Promise} vtPromise Promise that resolves after the view transition has been completed. + * @return {Promise} Promise that resolves after the view transition names were reset. + */ + const setTemporaryViewTransitionNames = async ( entries, vtPromise ) => { + for ( const [ element, name ] of entries ) { + if ( ! element ) { + continue; + } + element.style.viewTransitionName = name; + } + + await vtPromise; + + for ( const [ element ] of entries ) { + if ( ! element ) { + continue; + } + element.style.viewTransitionName = ''; + } + }; + + /** + * Appends a selector to another selector. + * + * This supports selectors which technically include multiple selectors (separated by comma). + * + * @param {string} selectors Main selector. + * @param {string} append Selector to append to the main selector. + * @return {string} Combined selector. + */ + const appendSelectors = ( selectors, append ) => { + return selectors + .split( ',' ) + .map( ( subselector ) => subselector.trim() + ' ' + append ) + .join( ',' ); + }; + + /** + * Gets a post element (the first on the page, in case there are multiple). + * + * @return {Element|null} Post element, or null if none is found. + */ + const getArticle = () => { + if ( ! config.postSelector ) { + return null; + } + return document.querySelector( config.postSelector ); + }; + + /** + * Gets the post element for a specific post URL. + * + * @param {string} url Post URL (permalink) to find post element. + * @return {Element|null} Post element, or null if none is found. + */ + const getArticleForUrl = ( url ) => { + if ( ! config.postSelector ) { + return null; + } + const postLinkSelector = appendSelectors( + config.postSelector, + 'a[href="' + url + '"]' + ); + const articleLink = document.querySelector( postLinkSelector ); + if ( ! articleLink ) { + return null; + } + return articleLink.closest( config.postSelector ); + }; + + /** + * Customizes view transition behavior on the URL that is being navigated from. + * + * @type {PageSwapListenerFunction} + * @param {PageSwapEvent} event - Event fired as the previous URL is about to unload. + */ + window.addEventListener( + 'pageswap', + ( /** @type {PageSwapEvent} */ event ) => { + if ( event.viewTransition ) { + let viewTransitionEntries; + if ( document.body.classList.contains( 'single' ) ) { + viewTransitionEntries = getViewTransitionEntries( + document.body, + getArticle() + ); + } else if ( + document.body.classList.contains( 'home' ) || + document.body.classList.contains( 'archive' ) + ) { + viewTransitionEntries = getViewTransitionEntries( + document.body, + getArticleForUrl( event.activation.entry.url ) + ); + } + if ( viewTransitionEntries ) { + setTemporaryViewTransitionNames( + viewTransitionEntries, + event.viewTransition.finished + ); + } + } + } + ); + + /** + * Customizes view transition behavior on the URL that is being navigated to. + * + * @type {PageRevealListenerFunction} + * @param {PageRevealEvent} event - Event fired as the new URL being navigated to is loaded. + */ + window.addEventListener( + 'pagereveal', + ( /** @type {PageRevealEvent} */ event ) => { + if ( event.viewTransition ) { + let viewTransitionEntries; + if ( document.body.classList.contains( 'single' ) ) { + viewTransitionEntries = getViewTransitionEntries( + document.body, + getArticle() + ); + } else if ( + document.body.classList.contains( 'home' ) || + document.body.classList.contains( 'archive' ) + ) { + viewTransitionEntries = getViewTransitionEntries( + document.body, + window.navigation.activation.from + ? getArticleForUrl( + window.navigation.activation.from.url + ) + : null + ); + } + if ( viewTransitionEntries ) { + setTemporaryViewTransitionNames( + viewTransitionEntries, + event.viewTransition.ready + ); + } + } + } + ); +}; diff --git a/plugins/view-transitions/readme.txt b/plugins/view-transitions/readme.txt index 7fb27b817f..55115c2944 100644 --- a/plugins/view-transitions/readme.txt +++ b/plugins/view-transitions/readme.txt @@ -2,7 +2,7 @@ Contributors: wordpressdotorg Tested up to: 6.8 -Stable tag: 1.4.0 +Stable tag: 1.0.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html Tags: performance, view transitions, smooth transitions, animations diff --git a/plugins/view-transitions/tests/test-hooks.php b/plugins/view-transitions/tests/test-hooks.php index 505d4f3c77..4483052a4d 100644 --- a/plugins/view-transitions/tests/test-hooks.php +++ b/plugins/view-transitions/tests/test-hooks.php @@ -11,6 +11,7 @@ class Test_ViewTransitions_Hooks extends WP_UnitTestCase { public function test_hooks(): void { $this->assertSame( 10, has_action( 'wp_head', 'plvt_render_generator' ) ); $this->assertSame( PHP_INT_MAX, has_action( 'after_setup_theme', 'plvt_polyfill_theme_support' ) ); + $this->assertSame( 1, has_action( 'init', 'plvt_sanitize_view_transitions_theme_support' ) ); $this->assertSame( 10, has_action( 'wp_enqueue_scripts', 'plvt_load_view_transitions' ) ); } diff --git a/plugins/view-transitions/tests/test-theme.php b/plugins/view-transitions/tests/test-theme.php index 5e43560cf1..f5faa36da2 100644 --- a/plugins/view-transitions/tests/test-theme.php +++ b/plugins/view-transitions/tests/test-theme.php @@ -36,6 +36,7 @@ public function test_plvt_load_view_transitions(): void { // Test that with theme support it registers and enqueues the style. add_theme_support( 'view-transitions' ); + plvt_sanitize_view_transitions_theme_support(); // This must be called to sanitize the arguments (normally on 'init'). plvt_load_view_transitions(); $this->assertTrue( wp_style_is( 'wp-view-transitions', 'registered' ) ); $this->assertTrue( wp_style_is( 'wp-view-transitions', 'enqueued' ) ); diff --git a/plugins/view-transitions/view-transitions.php b/plugins/view-transitions/view-transitions.php index 0e508f30f5..03ba7434ad 100644 --- a/plugins/view-transitions/view-transitions.php +++ b/plugins/view-transitions/view-transitions.php @@ -27,6 +27,7 @@ define( 'VIEW_TRANSITIONS_VERSION', '1.0.0' ); +require_once __DIR__ . '/includes/functions.php'; require_once __DIR__ . '/includes/theme.php'; require_once __DIR__ . '/hooks.php'; // @codeCoverageIgnoreEnd diff --git a/webpack.config.js b/webpack.config.js index 5981f9bf76..7f6f84a21a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -22,6 +22,13 @@ const { */ const defaultConfig = require( '@wordpress/scripts/config/webpack.config' ); +/* + * Temporary workaround because 'view-transitions' should not be added to `plugins.json` just yet, since it is not + * ready to be released. + * TODO: Remove this workaround once the plugin is added to `plugins.json`. + */ +standalonePlugins.push( 'view-transitions' ); + const defaultBuildConfig = { entry: {}, output: { @@ -40,6 +47,7 @@ const pluginsWithBuild = [ 'embed-optimizer', 'image-prioritizer', 'optimization-detective', + 'view-transitions', 'web-worker-offloading', ]; @@ -217,6 +225,39 @@ const optimizationDetective = ( env ) => { }; }; +/** + * Webpack Config: View Transitions + * + * @param {*} env Webpack environment + * @return {Object} Webpack configuration + */ +const viewTransitions = ( env ) => { + if ( env.plugin && env.plugin !== 'view-transitions' ) { + return defaultBuildConfig; + } + + const destination = path.resolve( __dirname, 'plugins/view-transitions' ); + + return { + ...sharedConfig, + name: 'view-transitions', + plugins: [ + new CopyWebpackPlugin( { + patterns: [ + { + from: `${ destination }/js/view-transitions.js`, + to: `${ destination }/js/view-transitions.min.js`, + }, + ], + } ), + new WebpackBar( { + name: 'Building View Transitions Assets', + color: '#2196f3', + } ), + ], + }; +}; + /** * Webpack Config: Web Worker Offloading * @@ -347,6 +388,7 @@ module.exports = [ embedOptimizer, imagePrioritizer, optimizationDetective, + viewTransitions, webWorkerOffloading, buildPlugin, ];