diff --git a/.distignore b/.distignore
index 359ffd6..fa1a3aa 100644
--- a/.distignore
+++ b/.distignore
@@ -7,6 +7,8 @@
.DS_Store
Thumbs.db
.phpcs-cache
+.phpunit.cache
+.phpunit.result.cache
composer.json
composer.lock
package.json
@@ -14,6 +16,8 @@ package-lock.json
bun.lock
phpcs.xml
phpstan.neon.dist
+phpunit.xml
+phpunit.xml.dist
AGENTS.md
CLAUDE.md
CONTRIBUTING.md
@@ -30,5 +34,6 @@ release
scripts
src
stubs
+tests
vendor
node_modules
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bfb445e..11eb6fa 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -19,4 +19,5 @@ jobs:
bun-install: true
php-analyze-command: composer analyze
bun-lint-command: bun run lint
+ test-command: bun run test
build-command: bun run build
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 6365d80..186b9c1 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -25,6 +25,7 @@ jobs:
bun-install: true
php-analyze-command: composer analyze
bun-lint-command: bun run lint
+ test-command: bun run test
build-command: bun run build
i18n-command: bun run i18n
required-artifacts: |
diff --git a/.gitignore b/.gitignore
index 5d8f4c7..51a5781 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ vendor/
build/
release/
.phpcs-cache
+.phpunit.cache/
diff --git a/.hooks/pre-commit b/.hooks/pre-commit
index 7114c85..de93b12 100755
--- a/.hooks/pre-commit
+++ b/.hooks/pre-commit
@@ -2,7 +2,7 @@
set -eu
staged_files="$(git diff --cached --name-only --diff-filter=ACMR)"
-js_files="$(printf '%s\n' "$staged_files" | grep -E '^src/.*\.(js|jsx)$' || true)"
+js_files="$(printf '%s\n' "$staged_files" | grep -E '^(src/.*\.(js|jsx)|scripts/.*\.m?js)$' || true)"
scss_files="$(printf '%s\n' "$staged_files" | grep -E '^src/.*\.scss$' || true)"
if printf '%s\n' "$staged_files" | grep -E '\.php$' >/dev/null 2>&1; then
diff --git a/AGENTS.md b/AGENTS.md
index 2b48b2a..8385f7b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -32,4 +32,4 @@ Agent guidance for this repository. Keep this file focused on non-obvious block,
- Build and lint commands are defined in `package.json` and `composer.json`; prefer those scripts.
- `stubs/wpvdb-search.stub.php` is for static analysis only. Do not load it at runtime.
- If adding another block, check the source map generator, release the required artifacts, and add a block registration fallback in the same change.
-- Unit test coverage is still backlog work. When adding it, start with block rendering, search argument mapping, and i18n source map generation.
+- Unit tests cover block rendering and the i18n source map generator. Extend those tests when render output, search argument mapping, or source map behavior changes.
diff --git a/README.md b/README.md
index a37207f..765c41b 100644
--- a/README.md
+++ b/README.md
@@ -46,6 +46,7 @@ Run the local checks:
```bash
bun run lint
bun run analyze
+bun run test
```
The main branch maintenance workflow regenerates translation files and commits them when strings change. This plugin also has block editor JavaScript, so the i18n task rebuilds `languages/source-map.json` and refreshes the hashed JSON files WordPress uses for script translations. Release workflows regenerate translations before staging the zip and fail if generated files are out of date.
diff --git a/composer.json b/composer.json
index 6ca43fe..335fa71 100644
--- a/composer.json
+++ b/composer.json
@@ -21,6 +21,7 @@
"automattic/vipwpcs": "^3.0",
"dealerdirect/phpcodesniffer-composer-installer": "*",
"phpcompatibility/phpcompatibility-wp": "*",
+ "phpunit/phpunit": "^12.0",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.1",
"szepeviktor/phpstan-wordpress": "^2.0",
@@ -30,6 +31,7 @@
"scripts": {
"lint": "./vendor/bin/phpcs",
"analyze": "./vendor/bin/phpstan analyze --memory-limit=512M",
+ "test": "./vendor/bin/phpunit",
"fix": "./vendor/bin/phpcbf",
"post-install-cmd": [
"vendor/bin/cghooks add --no-lock"
diff --git a/composer.lock b/composer.lock
index a8d3588..9ee85bb 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "2ab8d3a9a238d56ee5ae1dc6cffba7e4",
+ "content-hash": "2475d6c81296d45b1507078d6acac0a3",
"packages": [],
"packages-dev": [
{
@@ -230,6 +230,242 @@
],
"time": "2026-05-06T08:26:05+00:00"
},
+ {
+ "name": "myclabs/deep-copy",
+ "version": "1.13.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "doctrine/collections": "<1.6.8",
+ "doctrine/common": "<2.13.3 || >=3 <3.2.2"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.6.8",
+ "doctrine/common": "^2.13.3 || ^3.2.2",
+ "phpspec/prophecy": "^1.10",
+ "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/DeepCopy/deep_copy.php"
+ ],
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Create deep copies (clones) of your objects",
+ "keywords": [
+ "clone",
+ "copy",
+ "duplicate",
+ "object",
+ "object graph"
+ ],
+ "support": {
+ "issues": "https://github.com/myclabs/DeepCopy/issues",
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-01T08:46:24+00:00"
+ },
+ {
+ "name": "nikic/php-parser",
+ "version": "v5.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/PHP-Parser.git",
+ "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82",
+ "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "ext-json": "*",
+ "ext-tokenizer": "*",
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "ircmaxell/php-yacc": "^0.0.7",
+ "phpunit/phpunit": "^9.0"
+ },
+ "bin": [
+ "bin/php-parse"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PhpParser\\": "lib/PhpParser"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Nikita Popov"
+ }
+ ],
+ "description": "A PHP parser written in PHP",
+ "keywords": [
+ "parser",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/nikic/PHP-Parser/issues",
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0"
+ },
+ "time": "2025-12-06T11:56:16+00:00"
+ },
+ {
+ "name": "phar-io/manifest",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/manifest.git",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-phar": "*",
+ "ext-xmlwriter": "*",
+ "phar-io/version": "^3.0.1",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+ "support": {
+ "issues": "https://github.com/phar-io/manifest/issues",
+ "source": "https://github.com/phar-io/manifest/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-03T12:33:53+00:00"
+ },
+ {
+ "name": "phar-io/version",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/version.git",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Library for handling version information and constraints",
+ "support": {
+ "issues": "https://github.com/phar-io/version/issues",
+ "source": "https://github.com/phar-io/version/tree/3.2.1"
+ },
+ "time": "2022-02-21T01:04:05+00:00"
+ },
{
"name": "php-stubs/wordpress-stubs",
"version": "v6.9.1",
@@ -559,270 +795,1602 @@
},
"funding": [
{
- "url": "https://github.com/PHPCSStandards",
- "type": "github"
- },
- {
- "url": "https://github.com/jrfnl",
+ "url": "https://github.com/PHPCSStandards",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/jrfnl",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/php_codesniffer",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/phpcsstandards",
+ "type": "thanks_dev"
+ }
+ ],
+ "time": "2025-11-12T23:06:57+00:00"
+ },
+ {
+ "name": "phpcsstandards/phpcsutils",
+ "version": "1.2.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPCSStandards/PHPCSUtils.git",
+ "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/c216317e96c8b3f5932808f9b0f1f7a14e3bbf55",
+ "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55",
+ "shasum": ""
+ },
+ "require": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0",
+ "php": ">=5.4",
+ "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1"
+ },
+ "require-dev": {
+ "ext-filter": "*",
+ "php-parallel-lint/php-console-highlighter": "^1.0",
+ "php-parallel-lint/php-parallel-lint": "^1.4.0",
+ "phpcsstandards/phpcsdevcs": "^1.2.0",
+ "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0"
+ },
+ "type": "phpcodesniffer-standard",
+ "extra": {
+ "branch-alias": {
+ "dev-stable": "1.x-dev",
+ "dev-develop": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "PHPCSUtils/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-3.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Juliette Reinders Folmer",
+ "homepage": "https://github.com/jrfnl",
+ "role": "lead"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors"
+ }
+ ],
+ "description": "A suite of utility functions for use with PHP_CodeSniffer",
+ "homepage": "https://phpcsutils.com/",
+ "keywords": [
+ "PHP_CodeSniffer",
+ "phpcbf",
+ "phpcodesniffer-standard",
+ "phpcs",
+ "phpcs3",
+ "phpcs4",
+ "standards",
+ "static analysis",
+ "tokens",
+ "utility"
+ ],
+ "support": {
+ "docs": "https://phpcsutils.com/",
+ "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues",
+ "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy",
+ "source": "https://github.com/PHPCSStandards/PHPCSUtils"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/PHPCSStandards",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/jrfnl",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/php_codesniffer",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/phpcsstandards",
+ "type": "thanks_dev"
+ }
+ ],
+ "time": "2025-12-08T14:27:58+00:00"
+ },
+ {
+ "name": "phpstan/extension-installer",
+ "version": "1.4.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/extension-installer.git",
+ "reference": "85e90b3942d06b2326fba0403ec24fe912372936"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/85e90b3942d06b2326fba0403ec24fe912372936",
+ "reference": "85e90b3942d06b2326fba0403ec24fe912372936",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^2.0",
+ "php": "^7.2 || ^8.0",
+ "phpstan/phpstan": "^1.9.0 || ^2.0"
+ },
+ "require-dev": {
+ "composer/composer": "^2.0",
+ "php-parallel-lint/php-parallel-lint": "^1.2.0",
+ "phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "PHPStan\\ExtensionInstaller\\Plugin"
+ },
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\ExtensionInstaller\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Composer plugin for automatic installation of PHPStan extensions",
+ "keywords": [
+ "dev",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/phpstan/extension-installer/issues",
+ "source": "https://github.com/phpstan/extension-installer/tree/1.4.3"
+ },
+ "time": "2024-09-04T20:21:43+00:00"
+ },
+ {
+ "name": "phpstan/phpstan",
+ "version": "2.1.54",
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8be50c3992107dc837b17da4d140fbbdf9a5c5bd",
+ "reference": "8be50c3992107dc837b17da4d140fbbdf9a5c5bd",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4|^8.0"
+ },
+ "conflict": {
+ "phpstan/phpstan-shim": "*"
+ },
+ "bin": [
+ "phpstan",
+ "phpstan.phar"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPStan - PHP Static Analysis Tool",
+ "keywords": [
+ "dev",
+ "static analysis"
+ ],
+ "support": {
+ "docs": "https://phpstan.org/user-guide/getting-started",
+ "forum": "https://github.com/phpstan/phpstan/discussions",
+ "issues": "https://github.com/phpstan/phpstan/issues",
+ "security": "https://github.com/phpstan/phpstan/security/policy",
+ "source": "https://github.com/phpstan/phpstan-src"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/ondrejmirtes",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/phpstan",
+ "type": "github"
+ }
+ ],
+ "time": "2026-04-29T13:31:09+00:00"
+ },
+ {
+ "name": "phpunit/php-code-coverage",
+ "version": "12.5.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+ "reference": "876099a072646c7745f673d7aeab5382c4439691"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/876099a072646c7745f673d7aeab5382c4439691",
+ "reference": "876099a072646c7745f673d7aeab5382c4439691",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-xmlwriter": "*",
+ "nikic/php-parser": "^5.7.0",
+ "php": ">=8.3",
+ "phpunit/php-text-template": "^5.0",
+ "sebastian/complexity": "^5.0",
+ "sebastian/environment": "^8.0.3",
+ "sebastian/lines-of-code": "^4.0",
+ "sebastian/version": "^6.0",
+ "theseer/tokenizer": "^2.0.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.5.1"
+ },
+ "suggest": {
+ "ext-pcov": "PHP extension that provides line coverage",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "12.5.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+ "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+ "keywords": [
+ "coverage",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+ "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-04-15T08:23:17+00:00"
+ },
+ {
+ "name": "phpunit/php-file-iterator",
+ "version": "6.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+ "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5",
+ "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+ "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+ "keywords": [
+ "filesystem",
+ "iterator"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+ "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-02-02T14:04:18+00:00"
+ },
+ {
+ "name": "phpunit/php-invoker",
+ "version": "6.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-invoker.git",
+ "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406",
+ "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.3"
+ },
+ "require-dev": {
+ "ext-pcntl": "*",
+ "phpunit/phpunit": "^12.0"
+ },
+ "suggest": {
+ "ext-pcntl": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Invoke callables with a timeout",
+ "homepage": "https://github.com/sebastianbergmann/php-invoker/",
+ "keywords": [
+ "process"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-invoker/issues",
+ "security": "https://github.com/sebastianbergmann/php-invoker/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2025-02-07T04:58:58+00:00"
+ },
+ {
+ "name": "phpunit/php-text-template",
+ "version": "5.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-text-template.git",
+ "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53",
+ "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Simple template engine.",
+ "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+ "keywords": [
+ "template"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+ "security": "https://github.com/sebastianbergmann/php-text-template/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2025-02-07T04:59:16+00:00"
+ },
+ {
+ "name": "phpunit/php-timer",
+ "version": "8.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-timer.git",
+ "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc",
+ "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "8.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Utility class for timing",
+ "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "keywords": [
+ "timer"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+ "security": "https://github.com/sebastianbergmann/php-timer/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2025-02-07T04:59:38+00:00"
+ },
+ {
+ "name": "phpunit/phpunit",
+ "version": "12.5.25",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "792c2980442dfce319226b88fa845b8b6de3b333"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/792c2980442dfce319226b88fa845b8b6de3b333",
+ "reference": "792c2980442dfce319226b88fa845b8b6de3b333",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-xml": "*",
+ "ext-xmlwriter": "*",
+ "myclabs/deep-copy": "^1.13.4",
+ "phar-io/manifest": "^2.0.4",
+ "phar-io/version": "^3.2.1",
+ "php": ">=8.3",
+ "phpunit/php-code-coverage": "^12.5.6",
+ "phpunit/php-file-iterator": "^6.0.1",
+ "phpunit/php-invoker": "^6.0.0",
+ "phpunit/php-text-template": "^5.0.0",
+ "phpunit/php-timer": "^8.0.0",
+ "sebastian/cli-parser": "^4.2.0",
+ "sebastian/comparator": "^7.1.6",
+ "sebastian/diff": "^7.0.0",
+ "sebastian/environment": "^8.1.0",
+ "sebastian/exporter": "^7.0.2",
+ "sebastian/global-state": "^8.0.2",
+ "sebastian/object-enumerator": "^7.0.0",
+ "sebastian/recursion-context": "^7.0.1",
+ "sebastian/type": "^6.0.3",
+ "sebastian/version": "^6.0.0",
+ "staabm/side-effects-detector": "^1.0.5"
+ },
+ "bin": [
+ "phpunit"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "12.5-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Framework/Assert/Functions.php"
+ ],
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "The PHP Unit Testing framework.",
+ "homepage": "https://phpunit.de/",
+ "keywords": [
+ "phpunit",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+ "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.25"
+ },
+ "funding": [
+ {
+ "url": "https://phpunit.de/sponsoring.html",
+ "type": "other"
+ }
+ ],
+ "time": "2026-05-13T03:56:57+00:00"
+ },
+ {
+ "name": "psr/container",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/container.git",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Container\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common Container Interface (PHP FIG PSR-11)",
+ "homepage": "https://github.com/php-fig/container",
+ "keywords": [
+ "PSR-11",
+ "container",
+ "container-interface",
+ "container-interop",
+ "psr"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/container/issues",
+ "source": "https://github.com/php-fig/container/tree/2.0.2"
+ },
+ "time": "2021-11-05T16:47:00+00:00"
+ },
+ {
+ "name": "sebastian/cli-parser",
+ "version": "4.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/cli-parser.git",
+ "reference": "7d05781b13f7dec9043a629a21d086ed74582a15"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/7d05781b13f7dec9043a629a21d086ed74582a15",
+ "reference": "7d05781b13f7dec9043a629a21d086ed74582a15",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.5.25"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "4.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for parsing CLI options",
+ "homepage": "https://github.com/sebastianbergmann/cli-parser",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
+ "security": "https://github.com/sebastianbergmann/cli-parser/security/policy",
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-17T05:29:34+00:00"
+ },
+ {
+ "name": "sebastian/comparator",
+ "version": "7.1.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/comparator.git",
+ "reference": "c769009dee98f494e0edc3fd4f4087501688f11e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/c769009dee98f494e0edc3fd4f4087501688f11e",
+ "reference": "c769009dee98f494e0edc3fd4f4087501688f11e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-mbstring": "*",
+ "php": ">=8.3",
+ "sebastian/diff": "^7.0",
+ "sebastian/exporter": "^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.2"
+ },
+ "suggest": {
+ "ext-bcmath": "For comparing BcMath\\Number objects"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "7.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ }
+ ],
+ "description": "Provides the functionality to compare PHP values for equality",
+ "homepage": "https://github.com/sebastianbergmann/comparator",
+ "keywords": [
+ "comparator",
+ "compare",
+ "equality"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/comparator/issues",
+ "security": "https://github.com/sebastianbergmann/comparator/security/policy",
+ "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-04-14T08:23:15+00:00"
+ },
+ {
+ "name": "sebastian/complexity",
+ "version": "5.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/complexity.git",
+ "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb",
+ "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^5.0",
+ "php": ">=8.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for calculating the complexity of PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/complexity",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/complexity/issues",
+ "security": "https://github.com/sebastianbergmann/complexity/security/policy",
+ "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2025-02-07T04:55:25+00:00"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "7.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git",
+ "reference": "7ab1ea946c012266ca32390913653d844ecd085f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f",
+ "reference": "7ab1ea946c012266ca32390913653d844ecd085f",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.0",
+ "symfony/process": "^7.2"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "7.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ }
+ ],
+ "description": "Diff implementation",
+ "homepage": "https://github.com/sebastianbergmann/diff",
+ "keywords": [
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/diff/issues",
+ "security": "https://github.com/sebastianbergmann/diff/security/policy",
+ "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2025-02-07T04:55:46+00:00"
+ },
+ {
+ "name": "sebastian/environment",
+ "version": "8.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/environment.git",
+ "reference": "b121608b28a13f721e76ffbbd386d08eff58f3f6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/b121608b28a13f721e76ffbbd386d08eff58f3f6",
+ "reference": "b121608b28a13f721e76ffbbd386d08eff58f3f6",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.0"
+ },
+ "suggest": {
+ "ext-posix": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "8.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides functionality to handle HHVM/PHP environments",
+ "homepage": "https://github.com/sebastianbergmann/environment",
+ "keywords": [
+ "Xdebug",
+ "environment",
+ "hhvm"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/environment/issues",
+ "security": "https://github.com/sebastianbergmann/environment/security/policy",
+ "source": "https://github.com/sebastianbergmann/environment/tree/8.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/environment",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-04-15T12:13:01+00:00"
+ },
+ {
+ "name": "sebastian/exporter",
+ "version": "7.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/exporter.git",
+ "reference": "016951ae10980765e4e7aee491eb288c64e505b7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7",
+ "reference": "016951ae10980765e4e7aee491eb288c64e505b7",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": ">=8.3",
+ "sebastian/recursion-context": "^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "7.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Provides the functionality to export PHP variables for visualization",
+ "homepage": "https://www.github.com/sebastianbergmann/exporter",
+ "keywords": [
+ "export",
+ "exporter"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/exporter/issues",
+ "security": "https://github.com/sebastianbergmann/exporter/security/policy",
+ "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-09-24T06:16:11+00:00"
+ },
+ {
+ "name": "sebastian/global-state",
+ "version": "8.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/global-state.git",
+ "reference": "ef1377171613d09edd25b7816f05be8313f9115d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ef1377171613d09edd25b7816f05be8313f9115d",
+ "reference": "ef1377171613d09edd25b7816f05be8313f9115d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.3",
+ "sebastian/object-reflector": "^5.0",
+ "sebastian/recursion-context": "^7.0"
+ },
+ "require-dev": {
+ "ext-dom": "*",
+ "phpunit/phpunit": "^12.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "8.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Snapshotting of global state",
+ "homepage": "https://www.github.com/sebastianbergmann/global-state",
+ "keywords": [
+ "global state"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/global-state/issues",
+ "security": "https://github.com/sebastianbergmann/global-state/security/policy",
+ "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-29T11:29:25+00:00"
+ },
+ {
+ "name": "sebastian/lines-of-code",
+ "version": "4.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/lines-of-code.git",
+ "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f",
+ "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^5.0",
+ "php": ">=8.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for counting the lines of code in PHP source code",
+ "homepage": "https://github.com/sebastianbergmann/lines-of-code",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
+ "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy",
+ "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2025-02-07T04:57:28+00:00"
+ },
+ {
+ "name": "sebastian/object-enumerator",
+ "version": "7.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+ "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894",
+ "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.3",
+ "sebastian/object-reflector": "^5.0",
+ "sebastian/recursion-context": "^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "7.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+ "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
+ "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy",
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2025-02-07T04:57:48+00:00"
+ },
+ {
+ "name": "sebastian/object-reflector",
+ "version": "5.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-reflector.git",
+ "reference": "4bfa827c969c98be1e527abd576533293c634f6a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a",
+ "reference": "4bfa827c969c98be1e527abd576533293c634f6a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Allows reflection of object attributes, including inherited and non-public ones",
+ "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
+ "security": "https://github.com/sebastianbergmann/object-reflector/security/policy",
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
"type": "github"
- },
- {
- "url": "https://opencollective.com/php_codesniffer",
- "type": "open_collective"
- },
- {
- "url": "https://thanks.dev/u/gh/phpcsstandards",
- "type": "thanks_dev"
}
],
- "time": "2025-11-12T23:06:57+00:00"
+ "time": "2025-02-07T04:58:17+00:00"
},
{
- "name": "phpcsstandards/phpcsutils",
- "version": "1.2.2",
+ "name": "sebastian/recursion-context",
+ "version": "7.0.1",
"source": {
"type": "git",
- "url": "https://github.com/PHPCSStandards/PHPCSUtils.git",
- "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55"
+ "url": "https://github.com/sebastianbergmann/recursion-context.git",
+ "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/c216317e96c8b3f5932808f9b0f1f7a14e3bbf55",
- "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c",
+ "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c",
"shasum": ""
},
"require": {
- "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0",
- "php": ">=5.4",
- "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1"
+ "php": ">=8.3"
},
"require-dev": {
- "ext-filter": "*",
- "php-parallel-lint/php-console-highlighter": "^1.0",
- "php-parallel-lint/php-parallel-lint": "^1.4.0",
- "phpcsstandards/phpcsdevcs": "^1.2.0",
- "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0"
+ "phpunit/phpunit": "^12.0"
},
- "type": "phpcodesniffer-standard",
+ "type": "library",
"extra": {
"branch-alias": {
- "dev-stable": "1.x-dev",
- "dev-develop": "1.x-dev"
+ "dev-main": "7.0-dev"
}
},
"autoload": {
"classmap": [
- "PHPCSUtils/"
+ "src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "LGPL-3.0-or-later"
+ "BSD-3-Clause"
],
"authors": [
{
- "name": "Juliette Reinders Folmer",
- "homepage": "https://github.com/jrfnl",
- "role": "lead"
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
},
{
- "name": "Contributors",
- "homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors"
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
}
],
- "description": "A suite of utility functions for use with PHP_CodeSniffer",
- "homepage": "https://phpcsutils.com/",
- "keywords": [
- "PHP_CodeSniffer",
- "phpcbf",
- "phpcodesniffer-standard",
- "phpcs",
- "phpcs3",
- "phpcs4",
- "standards",
- "static analysis",
- "tokens",
- "utility"
- ],
+ "description": "Provides functionality to recursively process PHP variables",
+ "homepage": "https://github.com/sebastianbergmann/recursion-context",
"support": {
- "docs": "https://phpcsutils.com/",
- "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues",
- "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy",
- "source": "https://github.com/PHPCSStandards/PHPCSUtils"
+ "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+ "security": "https://github.com/sebastianbergmann/recursion-context/security/policy",
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1"
},
"funding": [
{
- "url": "https://github.com/PHPCSStandards",
+ "url": "https://github.com/sebastianbergmann",
"type": "github"
},
{
- "url": "https://github.com/jrfnl",
- "type": "github"
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
},
{
- "url": "https://opencollective.com/php_codesniffer",
- "type": "open_collective"
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
},
{
- "url": "https://thanks.dev/u/gh/phpcsstandards",
- "type": "thanks_dev"
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context",
+ "type": "tidelift"
}
],
- "time": "2025-12-08T14:27:58+00:00"
+ "time": "2025-08-13T04:44:59+00:00"
},
{
- "name": "phpstan/extension-installer",
- "version": "1.4.3",
+ "name": "sebastian/type",
+ "version": "6.0.3",
"source": {
"type": "git",
- "url": "https://github.com/phpstan/extension-installer.git",
- "reference": "85e90b3942d06b2326fba0403ec24fe912372936"
+ "url": "https://github.com/sebastianbergmann/type.git",
+ "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/85e90b3942d06b2326fba0403ec24fe912372936",
- "reference": "85e90b3942d06b2326fba0403ec24fe912372936",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d",
+ "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d",
"shasum": ""
},
"require": {
- "composer-plugin-api": "^2.0",
- "php": "^7.2 || ^8.0",
- "phpstan/phpstan": "^1.9.0 || ^2.0"
+ "php": ">=8.3"
},
"require-dev": {
- "composer/composer": "^2.0",
- "php-parallel-lint/php-parallel-lint": "^1.2.0",
- "phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0"
+ "phpunit/phpunit": "^12.0"
},
- "type": "composer-plugin",
+ "type": "library",
"extra": {
- "class": "PHPStan\\ExtensionInstaller\\Plugin"
- },
- "autoload": {
- "psr-4": {
- "PHPStan\\ExtensionInstaller\\": "src/"
+ "branch-alias": {
+ "dev-main": "6.0-dev"
}
},
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "description": "Composer plugin for automatic installation of PHPStan extensions",
- "keywords": [
- "dev",
- "static analysis"
- ],
- "support": {
- "issues": "https://github.com/phpstan/extension-installer/issues",
- "source": "https://github.com/phpstan/extension-installer/tree/1.4.3"
- },
- "time": "2024-09-04T20:21:43+00:00"
- },
- {
- "name": "phpstan/phpstan",
- "version": "2.1.54",
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8be50c3992107dc837b17da4d140fbbdf9a5c5bd",
- "reference": "8be50c3992107dc837b17da4d140fbbdf9a5c5bd",
- "shasum": ""
- },
- "require": {
- "php": "^7.4|^8.0"
- },
- "conflict": {
- "phpstan/phpstan-shim": "*"
- },
- "bin": [
- "phpstan",
- "phpstan.phar"
- ],
- "type": "library",
"autoload": {
- "files": [
- "bootstrap.php"
+ "classmap": [
+ "src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "MIT"
+ "BSD-3-Clause"
],
- "description": "PHPStan - PHP Static Analysis Tool",
- "keywords": [
- "dev",
- "static analysis"
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
],
+ "description": "Collection of value objects that represent the types of the PHP type system",
+ "homepage": "https://github.com/sebastianbergmann/type",
"support": {
- "docs": "https://phpstan.org/user-guide/getting-started",
- "forum": "https://github.com/phpstan/phpstan/discussions",
- "issues": "https://github.com/phpstan/phpstan/issues",
- "security": "https://github.com/phpstan/phpstan/security/policy",
- "source": "https://github.com/phpstan/phpstan-src"
+ "issues": "https://github.com/sebastianbergmann/type/issues",
+ "security": "https://github.com/sebastianbergmann/type/security/policy",
+ "source": "https://github.com/sebastianbergmann/type/tree/6.0.3"
},
"funding": [
{
- "url": "https://github.com/ondrejmirtes",
+ "url": "https://github.com/sebastianbergmann",
"type": "github"
},
{
- "url": "https://github.com/phpstan",
- "type": "github"
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/type",
+ "type": "tidelift"
}
],
- "time": "2026-04-29T13:31:09+00:00"
+ "time": "2025-08-09T06:57:12+00:00"
},
{
- "name": "psr/container",
- "version": "2.0.2",
+ "name": "sebastian/version",
+ "version": "6.0.0",
"source": {
"type": "git",
- "url": "https://github.com/php-fig/container.git",
- "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
+ "url": "https://github.com/sebastianbergmann/version.git",
+ "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
- "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c",
+ "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c",
"shasum": ""
},
"require": {
- "php": ">=7.4.0"
+ "php": ">=8.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0.x-dev"
+ "dev-main": "6.0-dev"
}
},
"autoload": {
- "psr-4": {
- "Psr\\Container\\": "src/"
- }
+ "classmap": [
+ "src/"
+ ]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "MIT"
+ "BSD-3-Clause"
],
"authors": [
{
- "name": "PHP-FIG",
- "homepage": "https://www.php-fig.org/"
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
}
],
- "description": "Common Container Interface (PHP FIG PSR-11)",
- "homepage": "https://github.com/php-fig/container",
- "keywords": [
- "PSR-11",
- "container",
- "container-interface",
- "container-interop",
- "psr"
- ],
+ "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+ "homepage": "https://github.com/sebastianbergmann/version",
"support": {
- "issues": "https://github.com/php-fig/container/issues",
- "source": "https://github.com/php-fig/container/tree/2.0.2"
+ "issues": "https://github.com/sebastianbergmann/version/issues",
+ "security": "https://github.com/sebastianbergmann/version/security/policy",
+ "source": "https://github.com/sebastianbergmann/version/tree/6.0.0"
},
- "time": "2021-11-05T16:47:00+00:00"
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2025-02-07T05:00:38+00:00"
},
{
"name": "sirbrillig/phpcs-variable-analysis",
@@ -959,6 +2527,58 @@
],
"time": "2025-11-04T16:30:35+00:00"
},
+ {
+ "name": "staabm/side-effects-detector",
+ "version": "1.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/staabm/side-effects-detector.git",
+ "reference": "d8334211a140ce329c13726d4a715adbddd0a163"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163",
+ "reference": "d8334211a140ce329c13726d4a715adbddd0a163",
+ "shasum": ""
+ },
+ "require": {
+ "ext-tokenizer": "*",
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/extension-installer": "^1.4.3",
+ "phpstan/phpstan": "^1.12.6",
+ "phpunit/phpunit": "^9.6.21",
+ "symfony/var-dumper": "^5.4.43",
+ "tomasvotruba/type-coverage": "1.0.0",
+ "tomasvotruba/unused-public": "1.0.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "lib/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "A static analysis tool to detect side effects in PHP code",
+ "keywords": [
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/staabm/side-effects-detector/issues",
+ "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/staabm",
+ "type": "github"
+ }
+ ],
+ "time": "2024-10-20T05:08:20+00:00"
+ },
{
"name": "symfony/console",
"version": "v7.4.11",
@@ -1704,6 +3324,56 @@
},
"time": "2025-09-14T02:58:22+00:00"
},
+ {
+ "name": "theseer/tokenizer",
+ "version": "2.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theseer/tokenizer.git",
+ "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4",
+ "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": "^8.1"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+ "support": {
+ "issues": "https://github.com/theseer/tokenizer/issues",
+ "source": "https://github.com/theseer/tokenizer/tree/2.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2025-12-08T11:19:18+00:00"
+ },
{
"name": "wp-coding-standards/wpcs",
"version": "3.3.0",
diff --git a/package.json b/package.json
index 331ea4c..4525764 100644
--- a/package.json
+++ b/package.json
@@ -21,14 +21,15 @@
"i18n:map": "node scripts/generate-i18n-source-map.mjs",
"i18n:json": "wp i18n make-json languages languages --pretty-print --no-purge --use-map=languages/source-map.json",
"lint": "bun run lint:js && bun run lint:scss && bun run lint:php",
- "lint:js": "wp-scripts lint-js 'src/**/*.{js,jsx}'",
+ "lint:js": "wp-scripts lint-js 'src/**/*.{js,jsx}' 'scripts/**/*.js' 'scripts/**/*.mjs'",
"lint:js:staged": "wp-scripts lint-js",
"lint:scss": "wp-scripts lint-style 'src/**/*.scss' --customSyntax postcss-scss",
"lint:scss:staged": "wp-scripts lint-style --customSyntax postcss-scss",
"lint:php": "composer lint",
"analyze": "composer analyze",
+ "test": "composer test && wp-scripts test-unit-js",
"fix": "bun run fix:js && bun run fix:scss",
- "fix:js": "wp-scripts lint-js --fix 'src/**/*.{js,jsx}'",
+ "fix:js": "wp-scripts lint-js --fix 'src/**/*.{js,jsx}' 'scripts/**/*.js' 'scripts/**/*.mjs'",
"fix:scss": "wp-scripts lint-style 'src/**/*.scss' --customSyntax postcss-scss --fix"
},
"devDependencies": {
diff --git a/phpcs.xml b/phpcs.xml
index 3b0f265..6d2e7d5 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -15,6 +15,10 @@
*/editor.asset.php
+ */tests/php/Unit/*Test.php
+
+
+ */tests/php/Unit/*Test.php
@@ -33,4 +37,5 @@
*/vendor/*
*/release/*
*/stubs/*
+ */tests/php/bootstrap.php
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..3025208
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,13 @@
+
+
+
+
+ tests/php/Unit
+
+
+
diff --git a/scripts/generate-i18n-source-map.mjs b/scripts/generate-i18n-source-map.mjs
index 5ff3771..e30aa30 100644
--- a/scripts/generate-i18n-source-map.mjs
+++ b/scripts/generate-i18n-source-map.mjs
@@ -1,5 +1,12 @@
-import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
-import { dirname, join } from 'node:path';
+import {
+ existsSync,
+ mkdirSync,
+ readdirSync,
+ readFileSync,
+ unlinkSync,
+ writeFileSync,
+} from 'node:fs';
+import { dirname, join, posix } from 'node:path';
const sourceRoot = 'src';
const buildRoot = 'build';
@@ -29,7 +36,8 @@ for ( const blockName of readdirSync( sourceRoot ).sort() ) {
for ( const field of scriptFields ) {
for ( const scriptPath of normalizeScripts( blockJson[ field ] ) ) {
- map[ join( sourceRoot, blockName, scriptPath ) ] = join( buildRoot, blockName, scriptPath );
+ map[ sourceMapPath( sourceRoot, blockName, scriptPath ) ] =
+ sourceMapPath( buildRoot, blockName, scriptPath );
}
}
}
@@ -46,17 +54,31 @@ function normalizeScripts( value ) {
const values = Array.isArray( value ) ? value : [ value ];
return values
- .filter( ( item ) => typeof item === 'string' && item.startsWith( 'file:./' ) && item.endsWith( '.js' ) )
+ .filter(
+ ( item ) =>
+ typeof item === 'string' &&
+ item.startsWith( 'file:./' ) &&
+ item.endsWith( '.js' )
+ )
.map( ( item ) => item.replace( 'file:./', '' ) );
}
+function sourceMapPath( ...parts ) {
+ return posix.join(
+ ...parts.map( ( part ) => part.replaceAll( '\\', '/' ) )
+ );
+}
+
function cleanJsonFiles() {
if ( ! existsSync( languagesRoot ) ) {
return;
}
for ( const fileName of readdirSync( languagesRoot ) ) {
- if ( fileName.startsWith( jsonPrefix ) && fileName.endsWith( '.json' ) ) {
+ if (
+ fileName.startsWith( jsonPrefix ) &&
+ fileName.endsWith( '.json' )
+ ) {
unlinkSync( join( languagesRoot, fileName ) );
}
}
diff --git a/scripts/generate-i18n-source-map.test.js b/scripts/generate-i18n-source-map.test.js
new file mode 100644
index 0000000..d4b41f7
--- /dev/null
+++ b/scripts/generate-i18n-source-map.test.js
@@ -0,0 +1,150 @@
+import { execFileSync } from 'node:child_process';
+import {
+ mkdirSync,
+ mkdtempSync,
+ readFileSync,
+ rmSync,
+ existsSync,
+ writeFileSync,
+} from 'node:fs';
+import { tmpdir } from 'node:os';
+import { dirname, join, resolve } from 'node:path';
+
+const fixtures = [];
+const scriptPath = resolve( 'scripts/generate-i18n-source-map.mjs' );
+
+afterAll( () => {
+ for ( const fixture of fixtures ) {
+ rmSync( fixture, { recursive: true, force: true } );
+ }
+} );
+
+test( 'generates source maps for all block script fields', () => {
+ const root = createFixture( {
+ 'src/example/block.json': JSON.stringify( {
+ name: 'wpvdb-blocks/example',
+ editorScript: 'file:./index.js',
+ editorScriptModule: 'file:./editor-module.js',
+ script: [ 'wp-element', 'file:./frontend.js' ],
+ scriptModule: 'file:./frontend-module.js',
+ viewScript: 'file:./view.js',
+ viewScriptModule: 'file:./view-module.js',
+ } ),
+ 'languages/wpvdb-blocks-old.json': '{}',
+ 'languages/other-domain-old.json': '{}',
+ } );
+
+ execFileSync( process.execPath, [ scriptPath ], { cwd: root } );
+
+ const map = JSON.parse(
+ readFileSync( join( root, 'languages/source-map.json' ), 'utf8' )
+ );
+
+ expect( map ).toEqual(
+ expect.objectContaining( {
+ 'src/example/editor-module.js': 'build/example/editor-module.js',
+ 'src/example/frontend-module.js':
+ 'build/example/frontend-module.js',
+ 'src/example/frontend.js': 'build/example/frontend.js',
+ 'src/example/index.js': 'build/example/index.js',
+ 'src/example/view-module.js': 'build/example/view-module.js',
+ 'src/example/view.js': 'build/example/view.js',
+ } )
+ );
+ expect(
+ existsSync( join( root, 'languages/wpvdb-blocks-old.json' ) )
+ ).toBe( false );
+ expect(
+ readFileSync( join( root, 'languages/other-domain-old.json' ), 'utf8' )
+ ).toBe( '{}' );
+} );
+
+test( 'is deterministic across consecutive runs', () => {
+ const root = createFixture( {
+ 'src/second/block.json': JSON.stringify( {
+ name: 'wpvdb-blocks/second',
+ editorScript: 'file:./index.js',
+ } ),
+ 'src/first/block.json': JSON.stringify( {
+ name: 'wpvdb-blocks/first',
+ editorScript: 'file:./index.js',
+ } ),
+ 'languages/wpvdb-blocks-stale.json': '{}',
+ } );
+
+ execFileSync( process.execPath, [ scriptPath ], { cwd: root } );
+ const firstRun = readFileSync(
+ join( root, 'languages/source-map.json' ),
+ 'utf8'
+ );
+
+ writeFileSync( join( root, 'languages/wpvdb-blocks-stale.json' ), '{}' );
+ execFileSync( process.execPath, [ scriptPath ], { cwd: root } );
+ const secondRun = readFileSync(
+ join( root, 'languages/source-map.json' ),
+ 'utf8'
+ );
+
+ expect( secondRun ).toBe( firstRun );
+ expect(
+ existsSync( join( root, 'languages/wpvdb-blocks-stale.json' ) )
+ ).toBe( false );
+} );
+
+test( 'ignores non file script handles and non JavaScript assets', () => {
+ const root = createFixture( {
+ 'src/example/block.json': JSON.stringify( {
+ name: 'wpvdb-blocks/example',
+ editorScript: [ 'wp-element', 'file:./index.js' ],
+ script: 'file:./frontend.css',
+ viewScript: 'https://example.test/view.js',
+ viewScriptModule: 'file:./view-module.js',
+ } ),
+ } );
+
+ execFileSync( process.execPath, [ scriptPath ], { cwd: root } );
+
+ const map = JSON.parse(
+ readFileSync( join( root, 'languages/source-map.json' ), 'utf8' )
+ );
+
+ expect( map ).toEqual( {
+ 'src/example/index.js': 'build/example/index.js',
+ 'src/example/view-module.js': 'build/example/view-module.js',
+ } );
+} );
+
+test( 'creates languages directory and skips folders without block metadata', () => {
+ const root = createFixture( {
+ 'src/example/block.json': JSON.stringify( {
+ name: 'wpvdb-blocks/example',
+ editorScript: 'file:./index.js',
+ } ),
+ 'src/ignored/readme.txt': 'No block metadata here.',
+ } );
+
+ execFileSync( process.execPath, [ scriptPath ], { cwd: root } );
+
+ const map = JSON.parse(
+ readFileSync( join( root, 'languages/source-map.json' ), 'utf8' )
+ );
+
+ expect( map ).toEqual(
+ expect.objectContaining( {
+ 'src/example/index.js': 'build/example/index.js',
+ } )
+ );
+} );
+
+function createFixture( files ) {
+ const root = mkdtempSync( join( tmpdir(), 'wpvdb-blocks-test-' ) );
+ fixtures.push( root );
+
+ for ( const [ relativePath, contents ] of Object.entries( files ) ) {
+ const path = join( root, relativePath );
+ mkdirSync( dirname( path ), { recursive: true } );
+ writeFileSync( path, contents );
+ }
+
+ return root;
+}
diff --git a/tests/php/Unit/RelatedArticlesBlockTest.php b/tests/php/Unit/RelatedArticlesBlockTest.php
new file mode 100644
index 0000000..0d4b308
--- /dev/null
+++ b/tests/php/Unit/RelatedArticlesBlockTest.php
@@ -0,0 +1,149 @@
+ [] ];
+ Search::$last_call = [];
+ }
+
+ /**
+ * Test rendering uses context, clamps limits, and escapes markup.
+ *
+ * @covers \WPVDB_Blocks\Related_Articles_Block::render
+ */
+ public function test_render_uses_context_post_clamps_limit_and_escapes_markup(): void {
+ $GLOBALS['wpvdb_blocks_test']['post_fields'][42] = [
+ 'post_excerpt' => 'Useful context for the related article.',
+ 'post_content' => '',
+ ];
+ Search::$next_related = [
+ 'results' => [
+ [
+ 'post_id' => 42,
+ 'title' => ' Related title',
+ 'link' => 'https://example.test/article?x=',
+ 'date' => '2026-05-17T00:00:00+00:00',
+ ],
+ ],
+ ];
+
+ $html = Related_Articles_Block::render(
+ [
+ 'title' => 'Read next',
+ 'limit' => 99,
+ 'showExcerpt' => true,
+ ],
+ '',
+ new WP_Block( [ 'postId' => 7 ] )
+ );
+
+ self::assertSame( 7, Search::$last_call['post_id'], 'Render should use the block context post ID.' );
+ self::assertSame( Related_Articles_Block::MAX_LIMIT, Search::$last_call['limit'], 'Render should clamp the configured limit.' );
+ self::assertSame(
+ [
+ 'collapse_by_post' => true,
+ 'fields' => [
+ 'post_id',
+ 'title',
+ 'link',
+ 'date',
+ 'distance',
+ 'similarity',
+ 'matched_chunks',
+ ],
+ ],
+ Search::$last_call['args'],
+ 'Render should request the expected related article fields.'
+ );
+ self::assertStringContainsString( '<em>Read next</em>', $html, 'Block titles should be escaped.' );
+ self::assertStringContainsString( '<script>Bad</script> Related title', $html, 'Related article titles should be escaped.' );
+ self::assertStringContainsString( 'href="https://example.test/article?x=bad"', $html, 'Related article URLs should be sanitized.' );
+ self::assertStringContainsString( 'Useful context for the related article.', $html, 'Related article excerpts should be rendered.' );
+ self::assertStringContainsString( 'May 17, 2026', $html, 'Related article dates should be formatted.' );
+ self::assertStringNotContainsString( '', $html, 'Raw script tags should not be rendered.' );
+ }
+
+ /**
+ * Test excerpt visibility can be disabled.
+ *
+ * @covers \WPVDB_Blocks\Related_Articles_Block::render
+ */
+ public function test_render_can_hide_excerpt(): void {
+ $GLOBALS['wpvdb_blocks_test']['post_fields'][42] = [
+ 'post_excerpt' => 'Visible only when excerpts are enabled.',
+ 'post_content' => '',
+ ];
+ Search::$next_related = [
+ 'results' => [
+ [
+ 'post_id' => 42,
+ 'title' => 'Related title',
+ 'link' => 'https://example.test/article',
+ 'date' => '',
+ ],
+ ],
+ ];
+
+ $html = Related_Articles_Block::render(
+ [
+ 'showExcerpt' => false,
+ ],
+ '',
+ new WP_Block( [ 'postId' => 7 ] )
+ );
+
+ self::assertStringContainsString( 'href="https://example.test/article"', $html, 'Related articles should still render when excerpts are disabled.' );
+ self::assertStringContainsString( 'Related title', $html, 'Related article titles should still render when excerpts are disabled.' );
+ self::assertStringNotContainsString( 'Visible only when excerpts are enabled.', $html, 'Disabled excerpts should not render.' );
+ }
+
+ /**
+ * Test missing context shows an editor notice.
+ *
+ * @covers \WPVDB_Blocks\Related_Articles_Block::render
+ */
+ public function test_render_returns_admin_notice_for_missing_post_context(): void {
+ $html = Related_Articles_Block::render( [] );
+
+ self::assertStringContainsString( 'Select a post to preview related articles.', $html, 'Missing post context should render an editor notice.' );
+ self::assertStringContainsString( 'wp-block-wpvdb-blocks-related-articles--notice', $html, 'Missing post context should render notice markup.' );
+ }
+
+ /**
+ * Test anonymous frontend viewers do not see editor notices.
+ *
+ * @covers \WPVDB_Blocks\Related_Articles_Block::render
+ */
+ public function test_render_suppresses_notice_for_anonymous_frontend_viewers(): void {
+ $GLOBALS['wpvdb_blocks_test']['is_admin'] = false;
+ $GLOBALS['wpvdb_blocks_test']['can_edit_posts'] = false;
+
+ self::assertSame( '', Related_Articles_Block::render( [] ), 'Anonymous frontend viewers should not see editor notices.' );
+ }
+}
diff --git a/tests/php/bootstrap.php b/tests/php/bootstrap.php
new file mode 100644
index 0000000..ba39521
--- /dev/null
+++ b/tests/php/bootstrap.php
@@ -0,0 +1,167 @@
+ 0,
+ 'is_admin' => true,
+ 'can_edit_posts' => true,
+ 'post_fields' => [],
+ 'date_format' => 'F j, Y',
+ ];
+
+ if ( ! class_exists( 'WP_Error' ) ) {
+ class WP_Error {
+ public function __construct( private readonly string $code = '' ) {}
+
+ public function get_error_code(): string {
+ return $this->code;
+ }
+ }
+ }
+
+ if ( ! class_exists( 'WP_Block' ) ) {
+ class WP_Block {
+ /**
+ * @param array $context Block context.
+ */
+ public function __construct( public array $context = [] ) {}
+ }
+ }
+
+ function __( string $text, string $domain = 'default' ): string {
+ unset( $domain );
+ return $text;
+ }
+
+ function is_wp_error( mixed $value ): bool {
+ return $value instanceof \WP_Error;
+ }
+
+ function absint( mixed $value ): int {
+ return max( 0, (int) $value );
+ }
+
+ function get_the_ID(): int {
+ return (int) $GLOBALS['wpvdb_blocks_test']['current_post_id'];
+ }
+
+ function is_admin(): bool {
+ return (bool) $GLOBALS['wpvdb_blocks_test']['is_admin'];
+ }
+
+ function current_user_can( string $capability, mixed ...$args ): bool {
+ unset( $capability, $args );
+ return (bool) $GLOBALS['wpvdb_blocks_test']['can_edit_posts'];
+ }
+
+ function get_block_wrapper_attributes( array $attributes = [] ): string {
+ $class = wp_get_block_default_classname( \WPVDB_Blocks\Related_Articles_Block::NAME );
+ if ( ! empty( $attributes['class'] ) ) {
+ $class .= ' ' . (string) $attributes['class'];
+ }
+
+ return 'class="' . esc_attr( trim( $class ) ) . '"';
+ }
+
+ function wp_get_block_default_classname( string $name ): string {
+ return 'wp-block-' . sanitize_html_class( str_replace( '/', '-', $name ) );
+ }
+
+ function sanitize_html_class( string $class ): string {
+ return preg_replace( '/[^A-Za-z0-9_-]/', '', $class ) ?? '';
+ }
+
+ function esc_attr( mixed $value ): string {
+ return htmlspecialchars( (string) $value, ENT_QUOTES, 'UTF-8' );
+ }
+
+ function esc_url( mixed $value ): string {
+ $url = trim( (string) $value );
+ $url = str_replace( [ "\r", "\n", "\t" ], '', $url );
+ $url = preg_replace( '/[^a-z0-9-~+_.?#=!&;,\/:%@$|*\'()\[\]\x80-\xff]/i', '', $url ) ?? '';
+
+ $scheme = parse_url( $url, PHP_URL_SCHEME );
+ if ( is_string( $scheme ) && ! in_array( strtolower( $scheme ), [ 'http', 'https', 'mailto' ], true ) ) {
+ return '';
+ }
+
+ return htmlspecialchars( $url, ENT_QUOTES, 'UTF-8' );
+ }
+
+ function esc_html( mixed $value ): string {
+ return htmlspecialchars( (string) $value, ENT_QUOTES, 'UTF-8' );
+ }
+
+ function post_password_required( int $post_id ): bool {
+ unset( $post_id );
+ return false;
+ }
+
+ function get_post_field( string $field, int $post_id ): mixed {
+ return $GLOBALS['wpvdb_blocks_test']['post_fields'][ $post_id ][ $field ] ?? '';
+ }
+
+ function wp_strip_all_tags( string $text ): string {
+ return strip_tags( $text );
+ }
+
+ function wp_trim_words( string $text, int $num_words = 55, ?string $more = null ): string {
+ $words = preg_split( '/\s+/', trim( $text ) );
+ $words = false === $words ? [] : array_values( array_filter( $words, static fn ( string $word ): bool => '' !== $word ) );
+
+ if ( count( $words ) <= $num_words ) {
+ return implode( ' ', $words );
+ }
+
+ return implode( ' ', array_slice( $words, 0, $num_words ) ) . ( $more ?? '...' );
+ }
+
+ function wp_date( string $format, int $timestamp ): string {
+ return ( new \DateTimeImmutable( '@' . $timestamp ) )
+ ->setTimezone( new \DateTimeZone( 'UTC' ) )
+ ->format( $format );
+ }
+
+ function get_option( string $name ): mixed {
+ if ( 'date_format' === $name ) {
+ return $GLOBALS['wpvdb_blocks_test']['date_format'];
+ }
+
+ return null;
+ }
+}
+
+namespace WPVDB_Search {
+ class Search {
+ public static mixed $next_related = [
+ 'results' => [],
+ ];
+
+ /**
+ * @var array
+ */
+ public static array $last_call = [];
+
+ /**
+ * @param array $args Related args.
+ * @return array|\WP_Error
+ */
+ public static function related_to_post( int $post_id, int $limit = 5, array $args = [] ): array|\WP_Error {
+ self::$last_call = [
+ 'post_id' => $post_id,
+ 'limit' => $limit,
+ 'args' => $args,
+ ];
+
+ return self::$next_related;
+ }
+ }
+}
+
+namespace {
+ require_once dirname( __DIR__, 2 ) . '/includes/class-related-articles-block.php';
+}