diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 782ab92..848443f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -7,7 +7,7 @@ body: attributes: label: Affected area options: - - Composer scripts (CollectScopingStubs, ScopePhpDependencies) + - Composer scripts (GlobalSymbols, ScopePhpDependencies) - php-scoper base config (scoper-base) - PHPCS ruleset (phpcs.dist.xml) - PHPStan config (phpstan.dist.neon) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 853ab5c..6b12ab4 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -7,7 +7,7 @@ body: attributes: label: Target area options: - - Composer scripts (CollectScopingStubs, ScopePhpDependencies) + - Composer scripts (GlobalSymbols, ScopePhpDependencies) - php-scoper base config (scoper-base) - PHPCS ruleset - PHPStan config diff --git a/AGENTS.md b/AGENTS.md index 566bcbe..9be8963 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,14 +9,14 @@ wordpress-configs/ ├── php/ │ ├── quality-assurance/ # the shared PHPCS profiles and base, the PHPStan profile and base, opt-in path discovery │ ├── php-scoper/ # scoper-base.inc.php and per-library recipes//.inc.php -│ ├── composer/ # ScopePhpDependencies, ScopedPackages, CollectScopingStubs and their AST visitors +│ ├── composer/ # ScopePhpDependencies, ScopedPackages, GlobalSymbols, Manifest and their AST visitors │ ├── phpstan/ # GlobalTagVisitor, which the PHPStan profile registers │ └── release/ # the manifest and lock of the WP-CLI commands reusable-release.yml installs, which Dependabot keeps current ├── node/ # tsconfig, ESLint, Stylelint, PostCSS and Playwright baselines ├── docs/ # workflows.md (input reference) and scripts-contract.md (scripts, files, floors) ├── tests/ │ ├── Unit/ # PHPUnit tests that run the real tools -│ ├── Support/ # RunsWorkflowSteps, which runs a workflow step's script +│ ├── Support/ # RunsWorkflowStepsTrait, which runs a workflow step's script, and RecordsInstalledPackagesTrait │ └── fixtures/ # projects and captured tool output the tests use ├── phpcs.dist.xml # self-lint: the production profile minus WordPress-runtime sniffs ├── phpcs.tests.dist.xml # self-lint for tests/ @@ -57,3 +57,4 @@ zizmor --config .github/zizmor.yml .github/ - `reusable-playwright-e2e.yml` and a full `reusable-release.yml` run have no target here: CI checks them with actionlint and zizmor, and the unit tests run the release's assertion steps. - `symfony/filesystem` and `symfony/finder` stay a major behind their latest release, because `humbug/php-scoper` caps them. - The release's PHP-floor check reads a strict `>` lower bound as inclusive. +- `reusable-phpunit.yml`'s wp-env branch runs in no job here: every consumer's Integration lane runs it, and a pull request that moves a consumer's pin catches a break. diff --git a/README.md b/README.md index 355bcb1..f52ee35 100644 --- a/README.md +++ b/README.md @@ -170,20 +170,25 @@ Create `scoper.inc.php` at the project root and name the packages to scope: return ( require __DIR__ . '/vendor/ahegyes/wordpress-configs/php/php-scoper/scoper-base.inc.php' )( __DIR__, 'dompdf/dompdf', 'php-di/php-di' ); ``` -A name is a package name or an `fnmatch()` pattern, such as `'your-vendor/your-library-*'`. The runtime dependencies of each named package follow from Composer's `installed.json`, through `require` and never `require-dev`, so naming a library scopes what it needs. A pattern matches every installed package, development tools included, so keep it as narrow as the packages it means. A name that starts with `!` keeps the packages it matches unscoped, such as a polyfill whose global functions must stay reachable (`'!symfony/polyfill-php80'`), and the symbols they declare stay global too. Keep the scoped packages in `require-dev`, so a production install never loads their unprefixed copies, and require an unscoped package in `require`, so a production install still ships it. +A name is a package name or an `fnmatch()` pattern, such as `'your-vendor/your-library-*'`. The runtime dependencies of each named package follow from Composer's `installed.json`, through `require` and never `require-dev`, so naming a library scopes what it needs; a requirement that another package or the project itself replaces or provides, as Composer records in `installed.php`, counts as met. A pattern matches every installed package, development tools included, so keep it as narrow as the packages it means. A name that starts with `!` keeps the packages it matches unscoped, such as a polyfill whose global functions must stay reachable (`'!symfony/polyfill-php80'`), and the symbols they declare stay global too. Keep the scoped packages in `require-dev`, so a production install never loads their unprefixed copies, and require an unscoped package in `require`, so a production install still ships it. A scope run: -- copies every file of each scoped package to `vendor-prefixed///`, after emptying `vendor-prefixed/` and refusing one that is a symbolic link; +- scopes every file of each scoped package into `vendor-prefixed.tmp/` and, once php-scoper succeeds, moves it to `vendor-prefixed///` in place of the previous build, refusing a `vendor-prefixed/` that is a symbolic link, so a run php-scoper fails keeps the previous build; - leaves global the WordPress symbols of `sniccowp/php-scoper-wordpress-excludes`, Action Scheduler (`as_*` and `ActionScheduler*`), every symbol of the stubs files the project or a scoped package declares under `extra.scoping-stubs`, and their namespaces; -- prefixes the strings that start with a scoped package's own namespace, which libraries use to build class names; +- prefixes the strings that start with a scoped package's own namespace, which libraries use to build class names, unless they name a namespace the run leaves global or a symbol in one; - applies the recipe of each scoped package that has one in `php/php-scoper/recipes//.inc.php`; -- rewrites the text domain a scoped package declares under `extra.scoping-text-domain` to the project's `extra.text-domain`; -- fails, naming each one, when the scoped code references a prefixed name it does not declare: a host symbol no stubs file declares, a dependency outside the scoped packages, or a string php-scoper took for a class name; +- rewrites each string literal of the text domain a scoped package declares under `extra.scoping-text-domain` to the project's `extra.text-domain`, unless the two are the same; +- fails, naming each one, when the scoped code references a prefixed name it does not declare as the kind of symbol the reference needs: a host symbol no stubs file declares, a dependency outside the scoped packages, a string php-scoper took for a class name, or the class of a `Class::MEMBER` string; +- fails, naming its namespace, when the scoped code declares a class, interface, trait or enum outside the prefix, which a copy the host or another plugin loads can replace, other than the polyfill of a PHP class; - passes a `function_exists()` check of the prefixed name of a function it leaves global, PHP's own included, which php-scoper adds to every guard of that function and which is false while nothing declares the name; - writes `vendor-prefixed/scoper-autoload.php`, a class map over each scoped package's `psr-4`, `psr-0` and `classmap` paths that honors `exclude-from-classmap`, followed by its `files` entries. -The run also fails when `extra.scoping-prefix` is missing or empty, when a name matches no installed package, when a scoped package requires one that is not installed, when a declared stubs file is missing, when a scoped package declares a text domain and the project declares none, when a declared text domain survives outside a string literal of its own, and when a recipe's target is gone. +The run also fails when `extra.scoping-prefix` is missing or empty, when a name matches no installed package, when a scoped package requires one that is not installed, when a declared stubs file is missing, when a scoped file, a declared stubs file or a dropped package's file does not parse, when a scoped package declares a text domain and the project declares none, when a declared text domain survives outside a string literal of its own, and when a recipe's target is gone. + +A package that reaches a dependency only through a metapackage includes its `files` entries before that dependency's, because the autoloader orders them by each package's direct `require`. + +The run supports none of php-scoper's `expose-*` options, since each plugin keeps its own prefixed copy of what it scopes: the namespace-string patcher prefixes the global name in the alias php-scoper writes for an exposed class, so the class stays unexposed. The project or a scoped package declares the stubs files of the host symbols the scoped code calls, each as `./path/to/file.php` relative to its `composer.json`, as `vendor/package` for that package's `.php`, or as `vendor/package:path/to/file.php`: diff --git a/composer.lock b/composer.lock index d9a5e6e..317573f 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "composer/class-map-generator", - "version": "1.8.0", + "version": "1.9.0", "source": { "type": "git", "url": "https://github.com/composer/class-map-generator.git", - "reference": "c0efbeb2f3fd7e23c7f180a59765c0b48a31fbc4" + "reference": "5fa48e0e1992ea8a786df3ceca80625c9a643b75" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/class-map-generator/zipball/c0efbeb2f3fd7e23c7f180a59765c0b48a31fbc4", - "reference": "c0efbeb2f3fd7e23c7f180a59765c0b48a31fbc4", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/5fa48e0e1992ea8a786df3ceca80625c9a643b75", + "reference": "5fa48e0e1992ea8a786df3ceca80625c9a643b75", "shasum": "" }, "require": { @@ -61,7 +61,7 @@ ], "support": { "issues": "https://github.com/composer/class-map-generator/issues", - "source": "https://github.com/composer/class-map-generator/tree/1.8.0" + "source": "https://github.com/composer/class-map-generator/tree/1.9.0" }, "funding": [ { @@ -73,7 +73,8 @@ "type": "github" } ], - "time": "2026-09-25T15:24:19+00:00" + "time": "2026-11-03T10:10:19+00:00", + "published-time": "2026-11-03T10:12:49+00:00" }, { "name": "composer/pcre", @@ -149,7 +150,8 @@ "type": "github" } ], - "time": "2026-06-07T11:47:49+00:00" + "time": "2026-06-07T11:47:49+00:00", + "published-time": "2026-06-11T12:12:37+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", @@ -245,7 +247,8 @@ "type": "thanks_dev" } ], - "time": "2026-05-06T08:26:05+00:00" + "time": "2026-05-06T08:26:05+00:00", + "published-time": "2026-05-06T12:56:03+00:00" }, { "name": "johnbillion/wp-compat", @@ -320,7 +323,8 @@ "type": "github" } ], - "time": "2026-10-21T20:54:48+00:00" + "time": "2026-10-21T20:54:48+00:00", + "published-time": "2026-10-21T20:54:55+00:00" }, { "name": "nikic/php-parser", @@ -377,7 +381,8 @@ "issues": "https://github.com/nikic/PHP-Parser/issues", "source": "https://github.com/nikic/PHP-Parser/tree/v5.9.0" }, - "time": "2026-09-13T18:51:52+00:00" + "time": "2026-09-13T18:51:52+00:00", + "published-time": "2026-09-13T18:53:16+00:00" }, { "name": "php-stubs/wordpress-stubs", @@ -429,7 +434,8 @@ "issues": "https://github.com/php-stubs/wordpress-stubs/issues", "source": "https://github.com/php-stubs/wordpress-stubs/tree/v7.1.2" }, - "time": "2026-10-06T06:59:21+00:00" + "time": "2026-10-06T06:59:21+00:00", + "published-time": "2026-10-06T07:32:21+00:00" }, { "name": "phpcompatibility/php-compatibility", @@ -519,7 +525,8 @@ "type": "thanks_dev" } ], - "time": "2025-11-28T11:36:33+00:00" + "time": "2025-11-28T11:36:33+00:00", + "published-time": "2025-11-28T11:41:33+00:00" }, { "name": "phpcompatibility/phpcompatibility-paragonie", @@ -590,7 +597,8 @@ "type": "thanks_dev" } ], - "time": "2025-11-29T13:09:49+00:00" + "time": "2025-11-29T13:09:49+00:00", + "published-time": "2025-11-29T13:22:17+00:00" }, { "name": "phpcompatibility/phpcompatibility-wp", @@ -657,7 +665,8 @@ "type": "thanks_dev" } ], - "time": "2025-12-16T13:35:20+00:00" + "time": "2025-12-16T13:35:20+00:00", + "published-time": "2025-12-16T13:47:29+00:00" }, { "name": "phpcsstandards/phpcsextra", @@ -739,7 +748,8 @@ "type": "thanks_dev" } ], - "time": "2026-07-27T11:13:17+00:00" + "time": "2026-07-27T11:13:17+00:00", + "published-time": "2026-07-27T11:19:59+00:00" }, { "name": "phpcsstandards/phpcsutils", @@ -832,7 +842,8 @@ "type": "thanks_dev" } ], - "time": "2026-07-27T10:28:41+00:00" + "time": "2026-07-27T10:28:41+00:00", + "published-time": "2026-07-27T10:36:31+00:00" }, { "name": "phpstan/extension-installer", @@ -880,15 +891,16 @@ "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" + "time": "2024-09-04T20:21:43+00:00", + "published-time": "2024-09-04T20:22:27+00:00" }, { "name": "phpstan/phpstan", - "version": "2.3.4", + "version": "2.3.5", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f20ce6f3c82556cfcfc086b152445a12d1d64fff", - "reference": "f20ce6f3c82556cfcfc086b152445a12d1d64fff", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/645a1bd6bead15fc1b8474137467961d0fa30a79", + "reference": "645a1bd6bead15fc1b8474137467961d0fa30a79", "shasum": "" }, "require": { @@ -944,7 +956,8 @@ "type": "github" } ], - "time": "2026-10-28T22:38:53+00:00" + "time": "2026-11-03T12:14:22+00:00", + "published-time": "2026-11-03T12:14:36+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -995,20 +1008,21 @@ "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.5" }, - "time": "2026-07-22T06:50:43+00:00" + "time": "2026-07-22T06:50:43+00:00", + "published-time": "2026-07-27T15:18:00+00:00" }, { "name": "phpstan/phpstan-strict-rules", - "version": "2.1.0", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "24ec51589f896f048d0d36de373d993b5cf71786" + "reference": "fe4a0afae773eedf178fab17ed9361c0061d2804" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/24ec51589f896f048d0d36de373d993b5cf71786", - "reference": "24ec51589f896f048d0d36de373d993b5cf71786", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/fe4a0afae773eedf178fab17ed9361c0061d2804", + "reference": "fe4a0afae773eedf178fab17ed9361c0061d2804", "shasum": "" }, "require": { @@ -1045,9 +1059,10 @@ ], "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.1.0" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.1.1" }, - "time": "2026-10-01T21:30:37+00:00" + "time": "2026-11-02T08:16:15+00:00", + "published-time": "2026-11-02T10:55:07+00:00" }, { "name": "roave/security-advisories", @@ -1055,12 +1070,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "7fd2aa4f9a2f0db3e04724273d5121c1d16a6c66" + "reference": "7252ec7fc650313ca2c1ecc1e556da38d93acd56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/7fd2aa4f9a2f0db3e04724273d5121c1d16a6c66", - "reference": "7fd2aa4f9a2f0db3e04724273d5121c1d16a6c66", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/7252ec7fc650313ca2c1ecc1e556da38d93acd56", + "reference": "7252ec7fc650313ca2c1ecc1e556da38d93acd56", "shasum": "" }, "conflict": { @@ -1068,6 +1083,7 @@ "adaptcms/adaptcms": "<=1.3", "adawolfa/isdoc": "<1.4.3|>=1.5,<1.5.1|>=1.6,<1.6.1", "admidio/admidio": "<=5.0.11", + "adminneo-org/adminneo": "<5.5.1", "adodb/adodb-php": "<=5.22.9", "aheinze/cockpit": "<2.2", "aimeos/ai-admin-graphql": ">=2022.04.1,<2022.10.10|>=2023.04.1,<2023.10.6|>=2024.04.1,<2024.07.2", @@ -1264,7 +1280,7 @@ "doctrine/dbal": ">=2,<2.0.8|>=2.1,<2.1.2|>=3,<3.1.4", "doctrine/doctrine-bundle": "<1.5.2", "doctrine/doctrine-module": "<0.7.2", - "doctrine/mongodb-odm": "<1.0.2", + "doctrine/mongodb-odm": "<1.0.2|>=2.12,<2.17.3", "doctrine/mongodb-odm-bundle": "<3.0.1", "doctrine/orm": ">=1,<1.2.4|>=2,<2.4.8|>=2.5,<2.5.1|>=2.8.3,<2.8.4", "dolibarr/dolibarr": "<=23.0.2", @@ -1327,7 +1343,7 @@ "erusev/parsedown": "<1.7.2", "ether/logs": "<3.0.4", "evolutioncms/evolution": "<=3.2.3", - "evoweb/sf-register": "<13.2.4|>=14,<14.0.2", + "evoweb/sf-register": "<13.2.5|>=14,<14.0.3", "exceedone/exment": "<4.4.3|>=5,<5.0.3", "exceedone/laravel-admin": "<2.2.3|==3", "ezsystems/demobundle": ">=5.4,<5.4.6.1-dev", @@ -1595,6 +1611,7 @@ "mckenziearts/livewire-markdown-editor": "<1.3", "mcp/sdk": ">=0.5,<0.7.1", "mdanter/ecc": "<2", + "mediaessenz/mail": "<3.12.5|>=4,<4.2", "mediawiki/abuse-filter": "<1.39.9|>=1.40,<1.41.3|>=1.42,<1.42.2", "mediawiki/cargo": "<3.8.3", "mediawiki/core": "<1.39.5|==1.40", @@ -1784,6 +1801,7 @@ "prestashop/ps_emailsubscription": "<2.6.1", "prestashop/ps_facetedsearch": "<4.0.4", "prestashop/ps_linklist": "<3.1", + "prinsfrank/pdfparser": "<2.9|>=3,<3.9|>=4,<4.3", "privatebin/privatebin": "<=2.0.4", "processwire/processwire": "<=3.0.255", "propel/propel": ">=2.0.0.0-alpha1,<2.0.0.0-alpha8", @@ -1849,11 +1867,11 @@ "shuchkin/simplexlsx": ">=1.0.12,<1.1.13", "silverstripe-australia/advancedreports": ">=1,<=2", "silverstripe/admin": "<1.13.19|>=2,<2.1.8", - "silverstripe/assets": "<2.4.5|>=3,<3.1.3", + "silverstripe/assets": "<2.4.6|>=3,<3.2.4", "silverstripe/cms": "<6.2.1", "silverstripe/comments": ">=1.3,<3.1.1", "silverstripe/forum": "<0.6.2|>=0.7,<0.7.4", - "silverstripe/framework": "<6.2.2", + "silverstripe/framework": "<6.2.13", "silverstripe/graphql": ">=2,<2.0.5|>=3,<3.8.2|>=4,<4.3.7|>=5,<5.1.3", "silverstripe/hybridsessions": ">=1,<2.4.1|>=2.5,<2.5.1", "silverstripe/mfa": "<6", @@ -1864,7 +1882,7 @@ "silverstripe/silverstripe-omnipay": "<2.5.2|>=3,<3.0.2|>=3.1,<3.1.4|>=3.2,<3.2.1", "silverstripe/subsites": ">=2,<2.6.1", "silverstripe/taxonomy": ">=1.3,<1.3.1|>=2,<2.0.1", - "silverstripe/userforms": "<6.4.9|>=7,<7.0.7|>=7.1,<7.1.1", + "silverstripe/userforms": "<7.1.4", "silverstripe/versioned": "<3.2.1", "silverstripe/versioned-admin": ">=1,<1.11.1", "simogeo/filemanager": "<=2.5", @@ -1930,25 +1948,27 @@ "sylius/paypal-plugin": "<1.6.2|>=1.7,<1.7.2|>=2,<2.0.2", "sylius/resource-bundle": ">=1,<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4", "sylius/sylius": "<1.14.21|>=2,<2.1.17|>=2.2,<2.2.12|>=2.3,<2.3.2", - "symbiote/silverstripe-advancedworkflow": "<6.4.5|>=7,<7.1.3|>=7.2,<7.2.1", + "symbiote/silverstripe-advancedworkflow": "<6.4.6|>=7,<7.2.4", "symbiote/silverstripe-multivaluefield": ">=3,<3.1", "symbiote/silverstripe-queuedjobs": ">=3,<3.0.2|>=3.1,<3.1.4|>=4,<4.0.7|>=4.1,<4.1.2|>=4.2,<4.2.4|>=4.3,<4.3.3|>=4.4,<4.4.3|>=4.5,<4.5.1|>=4.6,<4.6.4", "symbiote/silverstripe-seed": "<6.0.3", "symbiote/silverstripe-versionedfiles": "<=2.0.3", "symfont/process": ">=0", "symfony/cache": "<5.4.52|>=6,<6.4.40|>=7,<7.4.12|>=8,<8.0.12", + "symfony/config": ">=2,<5.4.56|>=6,<6.4.50|>=7,<7.4.23|>=8,<8.1.11", "symfony/dependency-injection": ">=2,<2.0.17|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", "symfony/dom-crawler": "<5.4.52|>=6,<6.4.40|>=7,<7.4.12|>=8,<8.0.12", "symfony/error-handler": ">=4.4,<4.4.4|>=5,<5.0.4", "symfony/expression-language": ">=2.4,<5.4.54|>=6,<6.4.48|>=7,<7.4.21|>=8,<8.1.9", + "symfony/filesystem": ">=5.4,<5.4.56|>=6,<6.4.50|>=7,<7.4.23|>=8,<8.1.11", "symfony/form": ">=2.3,<2.3.35|>=2.4,<2.6.12|>=2.7,<2.7.50|>=2.8,<2.8.49|>=3,<3.4.20|>=4,<4.0.15|>=4.1,<4.1.9|>=4.2,<4.2.1", - "symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7|>=5.3.14,<5.3.15|>=5.4.3,<5.4.4|>=6.0.3,<6.0.4", - "symfony/html-sanitizer": ">=6.1,<6.4.41|>=7,<7.4.13|>=8,<8.0.13", - "symfony/http-client": ">=4.3,<5.4.53|>=6,<6.4.15|>=7,<7.1.8", - "symfony/http-foundation": "<5.4.54|>=6,<6.4.46|>=7,<7.4.19|>=8,<8.1.7", - "symfony/http-kernel": ">=2,<4.4.50|>=5,<5.4.20|>=6,<6.0.20|>=6.1,<6.1.12|>=6.2,<6.2.6|>=7.4,<7.4.12|>=8,<8.0.12", + "symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7|>=5.3.14,<5.3.15|>=5.4.3,<5.4.4|>=6.0.3,<6.0.4|>=7.1,<7.4.23|>=8,<8.1.11", + "symfony/html-sanitizer": ">=6.1,<6.4.50|>=7,<7.4.23|>=8,<8.1.11", + "symfony/http-client": ">=4.3,<5.4.56|>=6,<6.4.50|>=7,<7.4.23|>=8,<8.1.11", + "symfony/http-foundation": "<5.4.56|>=6,<6.4.50|>=7,<7.4.23|>=8,<8.1.11", + "symfony/http-kernel": ">=2,<5.4.56|>=6,<6.4.50|>=7,<7.4.23|>=8,<8.1.11", "symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13", - "symfony/json-path": ">=7.3,<7.4.12|>=8,<8.0.12", + "symfony/json-path": ">=7.3,<7.4.23|>=8,<8.1.11", "symfony/lox24-notifier": ">=7.1,<7.4.12|>=8,<8.0.12", "symfony/mailer": "<5.4.52|>=6,<6.4.40|>=7,<7.4.12|>=8,<8.0.12", "symfony/mailjet-mailer": ">=6.4,<6.4.40|>=7,<7.4.12|>=8,<8.0.12", @@ -1963,16 +1983,16 @@ "symfony/polyfill-php55": ">=1,<1.10", "symfony/process": "<5.4.51|>=6,<6.4.33|>=7,<7.1.7|>=7.3,<7.3.11|>=7.4,<7.4.5|>=8,<8.0.5", "symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", - "symfony/routing": "<5.4.53|>=6,<6.4.41|>=7,<7.4.13|>=8,<8.0.13", + "symfony/routing": "<5.4.56|>=6,<6.4.50|>=7,<7.4.23|>=8,<8.1.11", "symfony/runtime": ">=5.3,<5.4.52|>=6,<6.4.40|>=7,<7.4.12|>=8,<8.0.12", "symfony/security": ">=2,<2.7.51|>=2.8,<3.4.49|>=4,<4.4.24|>=5,<5.2.8", "symfony/security-bundle": ">=2,<4.4.50|>=5,<5.4.20|>=6,<6.0.20|>=6.1,<6.1.12|>=6.2,<6.4.10|>=7,<7.0.10|>=7.1,<7.1.3", "symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<3.4.49|>=4,<4.4.24|>=5,<5.2.9", "symfony/security-csrf": ">=2.4,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", "symfony/security-guard": ">=2.8,<3.4.48|>=4,<4.4.23|>=5,<5.2.8", - "symfony/security-http": "<5.4.53|>=6,<6.4.41|>=7,<7.4.13|>=8,<8.0.13", - "symfony/serializer": ">=2,<2.0.11|>=4.1,<4.4.35|>=5,<5.3.12", - "symfony/symfony": "<5.4.55|>=6,<6.4.49|>=7,<7.4.22|>=8,<8.1.10", + "symfony/security-http": "<5.4.56|>=6,<6.4.50|>=7,<7.4.23|>=8,<8.1.11", + "symfony/serializer": ">=2,<5.4.56|>=6,<6.4.50|>=7,<7.4.23|>=8,<8.1.11", + "symfony/symfony": "<5.4.56|>=6,<6.4.50|>=7,<7.4.23|>=8,<8.1.11", "symfony/translation": ">=2,<2.0.17", "symfony/twig-bridge": ">=2,<4.4.51|>=5,<5.4.31|>=6,<6.3.8|>=6.4.24,<6.4.40", "symfony/twilio-notifier": ">=6.4,<6.4.40|>=7,<7.4.12|>=8,<8.0.12", @@ -1981,11 +2001,11 @@ "symfony/ux-live-component": "<2.36|>=3,<3.1", "symfony/ux-toolkit": ">=2.32,<2.36.1|>=3,<3.2", "symfony/ux-twig-component": "<2.25.1", - "symfony/validator": "<5.4.43|>=6,<6.4.11|>=7,<7.1.4", + "symfony/validator": "<5.4.56|>=6,<6.4.50|>=7,<7.4.23|>=8,<8.1.11", "symfony/var-exporter": ">=4.2,<4.2.12|>=4.3,<4.3.8", "symfony/web-profiler-bundle": ">=2,<2.3.19|>=2.4,<2.4.9|>=2.5,<2.5.4|>=7.2.9,<7.4.12|>=8,<8.0.12", "symfony/webhook": ">=6.3,<6.3.8", - "symfony/yaml": "<5.4.54|>=6,<6.4.45|>=7,<7.4.18|>=8,<8.1.6", + "symfony/yaml": "<5.4.56|>=6,<6.4.50|>=7,<7.4.23|>=8,<8.1.11", "symphonycms/symphony-2": "<2.6.4", "syssy/syssy-typo3-extension": "<3.0.6", "t3/dce": "<0.11.5|>=2.2,<2.6.2", @@ -2216,26 +2236,27 @@ "type": "tidelift" } ], - "time": "2026-10-29T16:37:57+00:00" + "time": "2026-11-05T19:32:52+00:00", + "published-time": "2026-11-05T19:32:58+00:00" }, { "name": "sniccowp/php-scoper-wordpress-excludes", - "version": "7.1.0", + "version": "7.1.2", "source": { "type": "git", "url": "https://github.com/snicco/php-scoper-wordpress-excludes.git", - "reference": "d8ad8aae8a051a4f332150f37a7bf96df1d11d0a" + "reference": "3dfa46a15362197a7ebc423d4dc80db2f5b3cd35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/snicco/php-scoper-wordpress-excludes/zipball/d8ad8aae8a051a4f332150f37a7bf96df1d11d0a", - "reference": "d8ad8aae8a051a4f332150f37a7bf96df1d11d0a", + "url": "https://api.github.com/repos/snicco/php-scoper-wordpress-excludes/zipball/3dfa46a15362197a7ebc423d4dc80db2f5b3cd35", + "reference": "3dfa46a15362197a7ebc423d4dc80db2f5b3cd35", "shasum": "" }, "require-dev": { "php": "^7.4", "php-stubs/wordpress-globals": "0.2.0", - "php-stubs/wordpress-stubs": "7.1.0", + "php-stubs/wordpress-stubs": "7.1.2", "sniccowp/php-scoper-excludes": "dev-master" }, "type": "library", @@ -2261,9 +2282,10 @@ ], "support": { "issues": "https://github.com/snicco/php-scoper-wordpress-excludes/issues", - "source": "https://github.com/snicco/php-scoper-wordpress-excludes/tree/7.1.0" + "source": "https://github.com/snicco/php-scoper-wordpress-excludes/tree/7.1.2" }, - "time": "2026-08-28T02:50:05+00:00" + "time": "2026-10-07T00:26:15+00:00", + "published-time": "2026-11-01T10:37:09+00:00" }, { "name": "squizlabs/php_codesniffer", @@ -2342,7 +2364,8 @@ "type": "thanks_dev" } ], - "time": "2026-08-06T00:17:32+00:00" + "time": "2026-08-06T00:17:32+00:00", + "published-time": "2026-08-06T01:05:02+00:00" }, { "name": "swissspidy/phpstan-no-private", @@ -2394,20 +2417,21 @@ "issues": "https://github.com/swissspidy/phpstan-no-private/issues", "source": "https://github.com/swissspidy/phpstan-no-private/tree/v1.0.0" }, - "time": "2024-11-11T11:04:45+00:00" + "time": "2024-11-11T11:04:45+00:00", + "published-time": "2024-11-11T11:08:03+00:00" }, { "name": "symfony/filesystem", - "version": "v7.4.21", + "version": "v7.4.23", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "fd0e3462be6d32a0d130e8423e4940a7688162d8" + "reference": "53d609da66578dad5a39fe290e8f924ec8f05e93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/fd0e3462be6d32a0d130e8423e4940a7688162d8", - "reference": "fd0e3462be6d32a0d130e8423e4940a7688162d8", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/53d609da66578dad5a39fe290e8f924ec8f05e93", + "reference": "53d609da66578dad5a39fe290e8f924ec8f05e93", "shasum": "" }, "require": { @@ -2444,7 +2468,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.21" + "source": "https://github.com/symfony/filesystem/tree/v7.4.23" }, "funding": [ { @@ -2464,20 +2488,21 @@ "type": "tidelift" } ], - "time": "2026-10-06T20:05:19+00:00" + "time": "2026-11-04T08:27:22+00:00", + "published-time": "2026-11-04T11:28:00+00:00" }, { "name": "symfony/finder", - "version": "v7.4.22", + "version": "v7.4.23", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "e13420a8e0a76bdde1e7be09f923e62ad13739f2" + "reference": "c723f990aefe8c1053b18374faf4231eba84f518" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/e13420a8e0a76bdde1e7be09f923e62ad13739f2", - "reference": "e13420a8e0a76bdde1e7be09f923e62ad13739f2", + "url": "https://api.github.com/repos/symfony/finder/zipball/c723f990aefe8c1053b18374faf4231eba84f518", + "reference": "c723f990aefe8c1053b18374faf4231eba84f518", "shasum": "" }, "require": { @@ -2512,7 +2537,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.22" + "source": "https://github.com/symfony/finder/tree/v7.4.23" }, "funding": [ { @@ -2532,7 +2557,8 @@ "type": "tidelift" } ], - "time": "2026-10-19T16:56:54+00:00" + "time": "2026-11-02T10:56:49+00:00", + "published-time": "2026-11-04T11:28:12+00:00" }, { "name": "symfony/polyfill-ctype", @@ -2615,7 +2641,8 @@ "type": "tidelift" } ], - "time": "2026-04-10T16:19:22+00:00" + "time": "2026-04-10T16:19:22+00:00", + "published-time": "2026-04-26T14:44:50+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -2700,7 +2727,8 @@ "type": "tidelift" } ], - "time": "2026-10-28T09:35:49+00:00" + "time": "2026-10-28T09:35:49+00:00", + "published-time": "2026-10-28T10:07:45+00:00" }, { "name": "szepeviktor/phpstan-wordpress", @@ -2763,7 +2791,8 @@ "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v2.0.4" }, - "time": "2026-05-22T16:22:09+00:00" + "time": "2026-05-22T16:22:09+00:00", + "published-time": "2026-08-28T06:48:49+00:00" }, { "name": "wp-coding-standards/wpcs", @@ -2829,7 +2858,8 @@ "type": "custom" } ], - "time": "2026-07-27T11:53:23+00:00" + "time": "2026-07-27T11:53:23+00:00", + "published-time": "2026-07-27T11:55:52+00:00" }, { "name": "wp-hooks/wordpress-core", @@ -2912,7 +2942,8 @@ "type": "github" } ], - "time": "2026-08-20T19:54:29+00:00" + "time": "2026-08-20T19:54:29+00:00", + "published-time": "2026-08-20T19:54:42+00:00" } ], "packages-dev": [ @@ -3002,7 +3033,8 @@ "type": "patreon" } ], - "time": "2024-02-09T13:06:12+00:00" + "time": "2024-02-09T13:06:12+00:00", + "published-time": "2024-02-09T13:06:43+00:00" }, { "name": "composer/ca-bundle", @@ -3074,20 +3106,21 @@ "type": "github" } ], - "time": "2026-10-29T14:10:42+00:00" + "time": "2026-10-29T14:10:42+00:00", + "published-time": "2026-10-29T14:45:12+00:00" }, { "name": "composer/composer", - "version": "2.11.0", + "version": "2.11.1", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "e52c65bed1de6aa272cdde4b7725ca6653d7b06a" + "reference": "472b418967419c6124edd91dc6310a3bec091dae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/e52c65bed1de6aa272cdde4b7725ca6653d7b06a", - "reference": "e52c65bed1de6aa272cdde4b7725ca6653d7b06a", + "url": "https://api.github.com/repos/composer/composer/zipball/472b418967419c6124edd91dc6310a3bec091dae", + "reference": "472b418967419c6124edd91dc6310a3bec091dae", "shasum": "" }, "require": { @@ -3177,7 +3210,7 @@ "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", "security": "https://github.com/composer/composer/security/policy", - "source": "https://github.com/composer/composer/tree/2.11.0" + "source": "https://github.com/composer/composer/tree/2.11.1" }, "funding": [ { @@ -3189,7 +3222,8 @@ "type": "github" } ], - "time": "2026-10-29T09:23:20+00:00" + "time": "2026-10-30T13:52:10+00:00", + "published-time": "2026-10-30T14:03:09+00:00" }, { "name": "composer/metadata-minifier", @@ -3254,7 +3288,8 @@ "type": "github" } ], - "time": "2026-06-30T11:34:59+00:00" + "time": "2026-06-30T11:34:59+00:00", + "published-time": "2026-06-30T11:37:18+00:00" }, { "name": "composer/semver", @@ -3331,7 +3366,8 @@ "type": "github" } ], - "time": "2026-10-29T09:13:34+00:00" + "time": "2026-10-29T09:13:34+00:00", + "published-time": "2026-10-29T09:14:11+00:00" }, { "name": "composer/spdx-licenses", @@ -3407,7 +3443,8 @@ "type": "github" } ], - "time": "2026-04-08T20:18:39+00:00" + "time": "2026-04-08T20:18:39+00:00", + "published-time": "2026-04-08T20:19:56+00:00" }, { "name": "composer/xdebug-handler", @@ -3473,7 +3510,8 @@ "type": "tidelift" } ], - "time": "2024-05-06T16:37:16+00:00" + "time": "2024-05-06T16:37:16+00:00", + "published-time": "2024-05-06T17:01:42+00:00" }, { "name": "dompdf/dompdf", @@ -3537,7 +3575,8 @@ "issues": "https://github.com/dompdf/dompdf/issues", "source": "https://github.com/dompdf/dompdf/tree/v3.1.6" }, - "time": "2026-07-20T12:29:38+00:00" + "time": "2026-07-20T12:29:38+00:00", + "published-time": "2026-07-20T12:35:08+00:00" }, { "name": "dompdf/php-font-lib", @@ -3582,7 +3621,8 @@ "issues": "https://github.com/dompdf/php-font-lib/issues", "source": "https://github.com/dompdf/php-font-lib/tree/1.0.2" }, - "time": "2026-01-20T14:10:26+00:00" + "time": "2026-01-20T14:10:26+00:00", + "published-time": "2026-01-20T14:14:50+00:00" }, { "name": "dompdf/php-svg-lib", @@ -3628,7 +3668,8 @@ "issues": "https://github.com/dompdf/php-svg-lib/issues", "source": "https://github.com/dompdf/php-svg-lib/tree/1.0.2" }, - "time": "2026-01-02T16:01:13+00:00" + "time": "2026-01-02T16:01:13+00:00", + "published-time": "2026-01-02T16:02:22+00:00" }, { "name": "fidry/console", @@ -3714,7 +3755,8 @@ "type": "github" } ], - "time": "2025-02-14T11:06:15+00:00" + "time": "2025-02-14T11:06:15+00:00", + "published-time": "2025-02-14T11:09:06+00:00" }, { "name": "fidry/cpu-core-counter", @@ -3776,7 +3818,8 @@ "type": "github" } ], - "time": "2026-09-29T22:11:39+00:00" + "time": "2026-09-29T22:11:39+00:00", + "published-time": "2026-09-29T22:12:43+00:00" }, { "name": "fidry/filesystem", @@ -3842,7 +3885,8 @@ "type": "github" } ], - "time": "2025-02-13T23:05:19+00:00" + "time": "2025-02-13T23:05:19+00:00", + "published-time": "2025-02-13T23:09:29+00:00" }, { "name": "humbug/php-scoper", @@ -3928,7 +3972,8 @@ "issues": "https://github.com/humbug/php-scoper/issues", "source": "https://github.com/humbug/php-scoper/tree/0.18.20" }, - "time": "2026-10-08T08:21:05+00:00" + "time": "2026-10-08T08:21:05+00:00", + "published-time": "2026-10-08T08:29:48+00:00" }, { "name": "infection/abstract-testframework-adapter", @@ -3985,7 +4030,8 @@ "type": "open_collective" } ], - "time": "2026-05-28T19:10:29+00:00" + "time": "2026-05-28T19:10:29+00:00", + "published-time": "2026-05-28T19:12:08+00:00" }, { "name": "infection/extension-installer", @@ -4051,7 +4097,8 @@ "type": "open_collective" } ], - "time": "2021-10-20T22:08:34+00:00" + "time": "2021-10-20T22:08:34+00:00", + "published-time": "2022-01-17T16:32:15+00:00" }, { "name": "infection/include-interceptor", @@ -4107,20 +4154,21 @@ "type": "open_collective" } ], - "time": "2024-05-03T21:48:06+00:00" + "time": "2024-05-03T21:48:06+00:00", + "published-time": "2026-05-06T22:52:27+00:00" }, { "name": "infection/infection", - "version": "0.35.8", + "version": "0.35.9", "source": { "type": "git", "url": "https://github.com/infection/infection.git", - "reference": "642f7974a73713e7a2219baea0cd9b0856afba4b" + "reference": "3ca3e072dbb578257c636e81b947fe050bc61c69" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/infection/infection/zipball/642f7974a73713e7a2219baea0cd9b0856afba4b", - "reference": "642f7974a73713e7a2219baea0cd9b0856afba4b", + "url": "https://api.github.com/repos/infection/infection/zipball/3ca3e072dbb578257c636e81b947fe050bc61c69", + "reference": "3ca3e072dbb578257c636e81b947fe050bc61c69", "shasum": "" }, "require": { @@ -4235,7 +4283,7 @@ ], "support": { "issues": "https://github.com/infection/infection/issues", - "source": "https://github.com/infection/infection/tree/0.35.8" + "source": "https://github.com/infection/infection/tree/0.35.9" }, "funding": [ { @@ -4247,7 +4295,8 @@ "type": "open_collective" } ], - "time": "2026-10-14T09:21:58+00:00" + "time": "2026-11-02T17:30:39+00:00", + "published-time": "2026-11-02T17:39:26+00:00" }, { "name": "infection/mutator", @@ -4300,7 +4349,8 @@ "type": "open_collective" } ], - "time": "2025-04-29T08:19:52+00:00" + "time": "2025-04-29T08:19:52+00:00", + "published-time": "2025-04-29T08:21:00+00:00" }, { "name": "jetbrains/phpstorm-stubs", @@ -4344,7 +4394,8 @@ "stubs", "type" ], - "time": "2026-06-12T13:19:10+00:00" + "time": "2026-06-12T13:19:10+00:00", + "published-time": "2026-07-22T11:41:13+00:00" }, { "name": "justinrainbow/json-schema", @@ -4409,7 +4460,8 @@ "issues": "https://github.com/jsonrainbow/json-schema/issues", "source": "https://github.com/jsonrainbow/json-schema/tree/6.14.0" }, - "time": "2026-10-24T08:53:34+00:00" + "time": "2026-10-24T08:53:34+00:00", + "published-time": "2026-10-24T08:53:38+00:00" }, { "name": "laravel/serializable-closure", @@ -4470,7 +4522,8 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2026-09-22T14:32:34+00:00" + "time": "2026-09-22T14:32:34+00:00", + "published-time": "2026-09-22T14:33:13+00:00" }, { "name": "marc-mabe/php-enum", @@ -4543,7 +4596,8 @@ "issues": "https://github.com/marc-mabe/php-enum/issues", "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.2" }, - "time": "2025-09-14T11:18:39+00:00" + "time": "2025-09-14T11:18:39+00:00", + "published-time": "2025-09-14T11:32:17+00:00" }, { "name": "masterminds/html5", @@ -4611,7 +4665,8 @@ "issues": "https://github.com/Masterminds/html5-php/issues", "source": "https://github.com/Masterminds/html5-php/tree/2.12.0" }, - "time": "2026-10-25T08:57:58+00:00" + "time": "2026-10-25T08:57:58+00:00", + "published-time": "2026-10-25T08:58:48+00:00" }, { "name": "myclabs/deep-copy", @@ -4671,7 +4726,8 @@ "type": "github" } ], - "time": "2026-08-11T10:17:44+00:00" + "time": "2026-08-11T10:17:44+00:00", + "published-time": "2026-08-11T10:29:20+00:00" }, { "name": "ondram/ci-detector", @@ -4749,7 +4805,8 @@ "issues": "https://github.com/OndraM/ci-detector/issues", "source": "https://github.com/OndraM/ci-detector/tree/4.2.0" }, - "time": "2024-03-12T13:22:30+00:00" + "time": "2024-03-12T13:22:30+00:00", + "published-time": "2024-03-12T13:23:38+00:00" }, { "name": "phar-io/manifest", @@ -4816,7 +4873,8 @@ "type": "github" } ], - "time": "2024-03-03T12:33:53+00:00" + "time": "2024-03-03T12:33:53+00:00", + "published-time": "2024-03-03T12:39:44+00:00" }, { "name": "phar-io/version", @@ -4867,7 +4925,8 @@ "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" + "time": "2022-02-21T01:04:05+00:00", + "published-time": "2022-02-21T01:04:30+00:00" }, { "name": "php-di/invoker", @@ -4922,7 +4981,8 @@ "type": "github" } ], - "time": "2025-08-30T10:22:22+00:00" + "time": "2025-08-30T10:22:22+00:00", + "published-time": "2025-08-30T10:22:43+00:00" }, { "name": "php-di/php-di", @@ -4995,20 +5055,21 @@ "type": "tidelift" } ], - "time": "2025-08-16T11:10:48+00:00" + "time": "2025-08-16T11:10:48+00:00", + "published-time": "2025-08-16T11:11:08+00:00" }, { "name": "php-stubs/woocommerce-stubs", - "version": "v11.2.0", + "version": "v11.2.2", "source": { "type": "git", "url": "https://github.com/php-stubs/woocommerce-stubs.git", - "reference": "fb590c1c2a26c47aa5da2c6c51a707130e8a389c" + "reference": "d5c232c183c09689d75c177b01bef60e6dd53639" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/woocommerce-stubs/zipball/fb590c1c2a26c47aa5da2c6c51a707130e8a389c", - "reference": "fb590c1c2a26c47aa5da2c6c51a707130e8a389c", + "url": "https://api.github.com/repos/php-stubs/woocommerce-stubs/zipball/d5c232c183c09689d75c177b01bef60e6dd53639", + "reference": "d5c232c183c09689d75c177b01bef60e6dd53639", "shasum": "" }, "require": { @@ -5037,22 +5098,23 @@ ], "support": { "issues": "https://github.com/php-stubs/woocommerce-stubs/issues", - "source": "https://github.com/php-stubs/woocommerce-stubs/tree/v11.2.0" + "source": "https://github.com/php-stubs/woocommerce-stubs/tree/v11.2.2" }, - "time": "2026-10-07T19:02:59+00:00" + "time": "2026-11-04T21:47:17+00:00", + "published-time": "2026-11-04T21:48:23+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "14.5.0", + "version": "14.5.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "1db2f40af7a6538742e4a634b71ee556d6219be7" + "reference": "4063552c678ce3beb4b7f131a5b7354a19ff3e2a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1db2f40af7a6538742e4a634b71ee556d6219be7", - "reference": "1db2f40af7a6538742e4a634b71ee556d6219be7", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4063552c678ce3beb4b7f131a5b7354a19ff3e2a", + "reference": "4063552c678ce3beb4b7f131a5b7354a19ff3e2a", "shasum": "" }, "require": { @@ -5108,7 +5170,7 @@ "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/14.5.0" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/14.5.1" }, "funding": [ { @@ -5128,7 +5190,8 @@ "type": "tidelift" } ], - "time": "2026-10-30T08:11:15+00:00" + "time": "2026-11-02T11:47:37+00:00", + "published-time": "2026-11-02T11:49:08+00:00" }, { "name": "phpunit/php-file-iterator", @@ -5201,7 +5264,8 @@ "type": "tidelift" } ], - "time": "2026-08-25T14:47:43+00:00" + "time": "2026-08-25T14:47:43+00:00", + "published-time": "2026-08-25T14:48:49+00:00" }, { "name": "phpunit/php-invoker", @@ -5277,7 +5341,8 @@ "type": "tidelift" } ], - "time": "2026-02-06T04:34:47+00:00" + "time": "2026-02-06T04:34:47+00:00", + "published-time": "2026-02-06T04:35:29+00:00" }, { "name": "phpunit/php-text-template", @@ -5349,7 +5414,8 @@ "type": "tidelift" } ], - "time": "2026-10-03T04:54:24+00:00" + "time": "2026-10-03T04:54:24+00:00", + "published-time": "2026-10-03T04:54:39+00:00" }, { "name": "phpunit/php-timer", @@ -5421,20 +5487,21 @@ "type": "tidelift" } ], - "time": "2026-02-06T04:37:53+00:00" + "time": "2026-02-06T04:37:53+00:00", + "published-time": "2026-02-06T04:38:39+00:00" }, { "name": "phpunit/phpunit", - "version": "13.4.5", + "version": "13.4.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "941457a4f147a60fe2074d40e1fce0d08ea8d930" + "reference": "4821376960d0e1e5a7b7b700955ebcab33ecd71b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/941457a4f147a60fe2074d40e1fce0d08ea8d930", - "reference": "941457a4f147a60fe2074d40e1fce0d08ea8d930", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4821376960d0e1e5a7b7b700955ebcab33ecd71b", + "reference": "4821376960d0e1e5a7b7b700955ebcab33ecd71b", "shasum": "" }, "require": { @@ -5448,7 +5515,7 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.4.1", - "phpunit/php-code-coverage": "^14.4.1", + "phpunit/php-code-coverage": "^14.5.0", "phpunit/php-file-iterator": "^7.0.2", "phpunit/php-invoker": "^7.0.0", "phpunit/php-text-template": "^6.0.1", @@ -5505,7 +5572,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/13.4.5" + "source": "https://github.com/sebastianbergmann/phpunit/tree/13.4.6" }, "funding": [ { @@ -5513,7 +5580,8 @@ "type": "other" } ], - "time": "2026-10-27T04:16:22+00:00" + "time": "2026-11-02T07:12:28+00:00", + "published-time": "2026-11-02T07:13:08+00:00" }, { "name": "psr/clock", @@ -5561,7 +5629,8 @@ "issues": "https://github.com/php-fig/clock/issues", "source": "https://github.com/php-fig/clock/tree/1.0.0" }, - "time": "2022-11-25T14:36:26+00:00" + "time": "2022-11-25T14:36:26+00:00", + "published-time": "2022-11-25T14:59:22+00:00" }, { "name": "psr/container", @@ -5614,7 +5683,8 @@ "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" + "time": "2021-11-05T16:47:00+00:00", + "published-time": "2021-11-05T16:53:27+00:00" }, { "name": "psr/event-dispatcher", @@ -5664,7 +5734,8 @@ "issues": "https://github.com/php-fig/event-dispatcher/issues", "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" }, - "time": "2019-01-08T18:20:26+00:00" + "time": "2019-01-08T18:20:26+00:00", + "published-time": "2019-03-25T15:31:44+00:00" }, { "name": "psr/log", @@ -5714,7 +5785,8 @@ "support": { "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2024-09-11T13:17:53+00:00" + "time": "2024-09-11T13:17:53+00:00", + "published-time": "2024-09-11T13:21:14+00:00" }, { "name": "react/promise", @@ -5787,7 +5859,8 @@ "type": "open_collective" } ], - "time": "2025-08-19T18:57:03+00:00" + "time": "2025-08-19T18:57:03+00:00", + "published-time": "2025-08-19T19:13:36+00:00" }, { "name": "sabberworm/php-css-parser", @@ -5865,7 +5938,8 @@ "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.5.0" }, - "time": "2026-09-20T15:02:00+00:00" + "time": "2026-09-20T15:02:00+00:00", + "published-time": "2026-09-20T15:20:25+00:00" }, { "name": "sanmai/di-container", @@ -5944,20 +6018,21 @@ "type": "github" } ], - "time": "2026-08-11T06:42:18+00:00" + "time": "2026-08-11T06:42:18+00:00", + "published-time": "2026-08-11T06:44:32+00:00" }, { "name": "sanmai/duoclock", - "version": "0.1.3", + "version": "0.1.4", "source": { "type": "git", "url": "https://github.com/sanmai/DuoClock.git", - "reference": "47461e3ff65b7308635047831a55615652e7be1a" + "reference": "9fa79ec9445c8a43e3e6f5deefdebe920085f284" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sanmai/DuoClock/zipball/47461e3ff65b7308635047831a55615652e7be1a", - "reference": "47461e3ff65b7308635047831a55615652e7be1a", + "url": "https://api.github.com/repos/sanmai/DuoClock/zipball/9fa79ec9445c8a43e3e6f5deefdebe920085f284", + "reference": "9fa79ec9445c8a43e3e6f5deefdebe920085f284", "shasum": "" }, "require": { @@ -5999,7 +6074,7 @@ "description": "PHP time mocking for tests - PSR-20 clock with mockable sleep(), time(), and TimeSpy for PHPUnit testing", "support": { "issues": "https://github.com/sanmai/DuoClock/issues", - "source": "https://github.com/sanmai/DuoClock/tree/0.1.3" + "source": "https://github.com/sanmai/DuoClock/tree/0.1.4" }, "funding": [ { @@ -6007,7 +6082,8 @@ "type": "github" } ], - "time": "2025-12-26T06:12:34+00:00" + "time": "2026-11-06T08:04:12+00:00", + "published-time": "2026-11-06T08:04:52+00:00" }, { "name": "sanmai/later", @@ -6071,7 +6147,8 @@ "type": "github" } ], - "time": "2026-06-29T07:24:33+00:00" + "time": "2026-06-29T07:24:33+00:00", + "published-time": "2026-07-29T07:33:40+00:00" }, { "name": "sanmai/pipeline", @@ -6138,7 +6215,8 @@ "type": "github" } ], - "time": "2026-09-30T13:46:33+00:00" + "time": "2026-09-30T13:46:33+00:00", + "published-time": "2026-09-30T13:48:53+00:00" }, { "name": "sebastian/cli-parser", @@ -6207,7 +6285,8 @@ "type": "tidelift" } ], - "time": "2026-08-01T04:27:14+00:00" + "time": "2026-08-01T04:27:14+00:00", + "published-time": "2026-08-01T04:29:01+00:00" }, { "name": "sebastian/comparator", @@ -6299,7 +6378,8 @@ "type": "tidelift" } ], - "time": "2026-08-07T07:23:13+00:00" + "time": "2026-08-07T07:23:13+00:00", + "published-time": "2026-08-07T07:23:29+00:00" }, { "name": "sebastian/complexity", @@ -6369,7 +6449,8 @@ "type": "tidelift" } ], - "time": "2026-02-06T04:41:32+00:00" + "time": "2026-02-06T04:41:32+00:00", + "published-time": "2026-02-06T04:41:49+00:00" }, { "name": "sebastian/diff", @@ -6448,7 +6529,8 @@ "type": "tidelift" } ], - "time": "2026-08-25T15:38:55+00:00" + "time": "2026-08-25T15:38:55+00:00", + "published-time": "2026-08-25T15:39:44+00:00" }, { "name": "sebastian/environment", @@ -6524,7 +6606,8 @@ "type": "tidelift" } ], - "time": "2026-10-01T12:37:56+00:00" + "time": "2026-10-01T12:37:56+00:00", + "published-time": "2026-10-01T12:38:48+00:00" }, { "name": "sebastian/exporter", @@ -6614,7 +6697,8 @@ "type": "tidelift" } ], - "time": "2026-08-07T07:22:06+00:00" + "time": "2026-08-07T07:22:06+00:00", + "published-time": "2026-08-07T07:23:47+00:00" }, { "name": "sebastian/file-filter", @@ -6683,7 +6767,8 @@ "type": "tidelift" } ], - "time": "2026-04-22T07:20:04+00:00" + "time": "2026-04-22T07:20:04+00:00", + "published-time": "2026-04-22T07:20:25+00:00" }, { "name": "sebastian/git-state", @@ -6752,7 +6837,8 @@ "type": "tidelift" } ], - "time": "2026-10-21T05:59:52+00:00" + "time": "2026-10-21T05:59:52+00:00", + "published-time": "2026-10-21T06:01:28+00:00" }, { "name": "sebastian/global-state", @@ -6826,7 +6912,8 @@ "type": "tidelift" } ], - "time": "2026-06-01T15:11:33+00:00" + "time": "2026-06-01T15:11:33+00:00", + "published-time": "2026-06-01T15:20:45+00:00" }, { "name": "sebastian/lines-of-code", @@ -6896,7 +6983,8 @@ "type": "tidelift" } ], - "time": "2026-07-09T08:42:34+00:00" + "time": "2026-07-09T08:42:34+00:00", + "published-time": "2026-07-09T08:44:06+00:00" }, { "name": "sebastian/object-enumerator", @@ -6965,7 +7053,8 @@ "type": "tidelift" } ], - "time": "2026-08-13T07:05:05+00:00" + "time": "2026-08-13T07:05:05+00:00", + "published-time": "2026-08-13T07:07:58+00:00" }, { "name": "sebastian/object-reflector", @@ -7033,7 +7122,8 @@ "type": "tidelift" } ], - "time": "2026-08-13T06:34:36+00:00" + "time": "2026-08-13T06:34:36+00:00", + "published-time": "2026-08-13T06:39:22+00:00" }, { "name": "sebastian/recursion-context", @@ -7109,7 +7199,8 @@ "type": "tidelift" } ], - "time": "2026-08-03T05:58:12+00:00" + "time": "2026-08-03T05:58:12+00:00", + "published-time": "2026-08-03T05:59:59+00:00" }, { "name": "sebastian/type", @@ -7178,7 +7269,8 @@ "type": "tidelift" } ], - "time": "2026-08-10T08:00:57+00:00" + "time": "2026-08-10T08:00:57+00:00", + "published-time": "2026-08-10T08:01:43+00:00" }, { "name": "sebastian/version", @@ -7244,20 +7336,21 @@ "type": "tidelift" } ], - "time": "2026-10-04T05:51:14+00:00" + "time": "2026-10-04T05:51:14+00:00", + "published-time": "2026-10-04T05:52:22+00:00" }, { "name": "seld/jsonlint", - "version": "1.12.1", + "version": "1.12.2", "source": { "type": "git", "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "9a90eb5d32d5a500296bf43f946d60246444d5f7" + "reference": "4882a0459c88772e4afa3a73ee81b7824490aa9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9a90eb5d32d5a500296bf43f946d60246444d5f7", - "reference": "9a90eb5d32d5a500296bf43f946d60246444d5f7", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/4882a0459c88772e4afa3a73ee81b7824490aa9c", + "reference": "4882a0459c88772e4afa3a73ee81b7824490aa9c", "shasum": "" }, "require": { @@ -7296,7 +7389,7 @@ ], "support": { "issues": "https://github.com/Seldaek/jsonlint/issues", - "source": "https://github.com/Seldaek/jsonlint/tree/1.12.1" + "source": "https://github.com/Seldaek/jsonlint/tree/1.12.2" }, "funding": [ { @@ -7308,7 +7401,8 @@ "type": "tidelift" } ], - "time": "2026-06-12T11:32:29+00:00" + "time": "2026-11-03T10:16:18+00:00", + "published-time": "2026-11-03T10:20:37+00:00" }, { "name": "seld/phar-utils", @@ -7356,7 +7450,8 @@ "issues": "https://github.com/Seldaek/phar-utils/issues", "source": "https://github.com/Seldaek/phar-utils/tree/1.2.3" }, - "time": "2026-10-01T08:16:23+00:00" + "time": "2026-10-01T08:16:23+00:00", + "published-time": "2026-10-01T08:17:41+00:00" }, { "name": "seld/signal-handler", @@ -7417,7 +7512,8 @@ "issues": "https://github.com/Seldaek/signal-handler/issues", "source": "https://github.com/Seldaek/signal-handler/tree/2.0.2" }, - "time": "2023-09-03T09:24:00+00:00" + "time": "2023-09-03T09:24:00+00:00", + "published-time": "2023-09-03T09:24:36+00:00" }, { "name": "staabm/side-effects-detector", @@ -7469,20 +7565,21 @@ "type": "github" } ], - "time": "2024-10-20T05:08:20+00:00" + "time": "2024-10-20T05:08:20+00:00", + "published-time": "2024-10-20T05:09:39+00:00" }, { "name": "symfony/console", - "version": "v7.4.22", + "version": "v7.4.23", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "8012f842110b414fd1cc641adb0ed8423353b0bb" + "reference": "81957c2440c05c962fe8291f318de6e85f8db330" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/8012f842110b414fd1cc641adb0ed8423353b0bb", - "reference": "8012f842110b414fd1cc641adb0ed8423353b0bb", + "url": "https://api.github.com/repos/symfony/console/zipball/81957c2440c05c962fe8291f318de6e85f8db330", + "reference": "81957c2440c05c962fe8291f318de6e85f8db330", "shasum": "" }, "require": { @@ -7547,7 +7644,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.22" + "source": "https://github.com/symfony/console/tree/v7.4.23" }, "funding": [ { @@ -7567,20 +7664,21 @@ "type": "tidelift" } ], - "time": "2026-10-28T11:08:27+00:00" + "time": "2026-11-03T21:47:07+00:00", + "published-time": "2026-11-04T11:27:31+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.7.1", + "version": "v3.8.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" + "reference": "feb1edf4e546af3b85c5caebe8359a20db230ca3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", - "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/feb1edf4e546af3b85c5caebe8359a20db230ca3", + "reference": "feb1edf4e546af3b85c5caebe8359a20db230ca3", "shasum": "" }, "require": { @@ -7593,7 +7691,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.7-dev" + "dev-main": "3.8-dev" } }, "autoload": { @@ -7618,7 +7716,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.8.1" }, "funding": [ { @@ -7638,20 +7736,21 @@ "type": "tidelift" } ], - "time": "2026-06-05T06:23:12+00:00" + "time": "2026-09-16T14:15:10+00:00", + "published-time": "2026-11-04T11:25:34+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.7.1", + "version": "v3.8.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e" + "reference": "658e29751424da3ef4758c60f769e59d1b2981eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c7de7a00ffb67842132da02ea92988a39ccd9f4e", - "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/658e29751424da3ef4758c60f769e59d1b2981eb", + "reference": "658e29751424da3ef4758c60f769e59d1b2981eb", "shasum": "" }, "require": { @@ -7665,7 +7764,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.7-dev" + "dev-main": "3.8-dev" } }, "autoload": { @@ -7698,7 +7797,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.1" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.8.1" }, "funding": [ { @@ -7718,7 +7817,8 @@ "type": "tidelift" } ], - "time": "2026-06-05T06:23:12+00:00" + "time": "2026-09-16T14:15:10+00:00", + "published-time": "2026-11-04T11:25:36+00:00" }, { "name": "symfony/polyfill-iconv", @@ -7802,7 +7902,8 @@ "type": "tidelift" } ], - "time": "2026-09-29T06:25:14+00:00" + "time": "2026-09-29T06:25:14+00:00", + "published-time": "2026-09-30T10:23:39+00:00" }, { "name": "symfony/polyfill-intl-grapheme", @@ -7884,7 +7985,8 @@ "type": "tidelift" } ], - "time": "2026-10-15T09:18:53+00:00" + "time": "2026-10-15T09:18:53+00:00", + "published-time": "2026-10-28T10:07:44+00:00" }, { "name": "symfony/polyfill-intl-normalizer", @@ -7969,7 +8071,8 @@ "type": "tidelift" } ], - "time": "2026-09-23T22:02:09+00:00" + "time": "2026-09-23T22:02:09+00:00", + "published-time": "2026-09-30T10:23:47+00:00" }, { "name": "symfony/polyfill-php73", @@ -8049,7 +8152,8 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-09-09T11:45:10+00:00", + "published-time": "2026-04-26T14:44:49+00:00" }, { "name": "symfony/polyfill-php80", @@ -8133,7 +8237,8 @@ "type": "tidelift" } ], - "time": "2026-09-25T15:50:05+00:00" + "time": "2026-09-25T15:50:05+00:00", + "published-time": "2026-09-30T10:23:35+00:00" }, { "name": "symfony/polyfill-php81", @@ -8213,7 +8318,8 @@ "type": "tidelift" } ], - "time": "2026-05-26T12:45:58+00:00" + "time": "2026-05-26T12:45:58+00:00", + "published-time": "2026-05-26T13:40:34+00:00" }, { "name": "symfony/polyfill-php84", @@ -8293,7 +8399,8 @@ "type": "tidelift" } ], - "time": "2026-09-23T22:01:09+00:00" + "time": "2026-09-23T22:01:09+00:00", + "published-time": "2026-09-30T10:23:33+00:00" }, { "name": "symfony/polyfill-php85", @@ -8373,7 +8480,8 @@ "type": "tidelift" } ], - "time": "2026-09-25T15:46:47+00:00" + "time": "2026-09-25T15:46:47+00:00", + "published-time": "2026-09-30T10:23:32+00:00" }, { "name": "symfony/process", @@ -8438,20 +8546,21 @@ "type": "tidelift" } ], - "time": "2026-10-25T14:40:25+00:00" + "time": "2026-10-25T14:40:25+00:00", + "published-time": "2026-10-28T18:25:20+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.7.3", + "version": "v3.8.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257" + "reference": "052852a96fd5e5ebdb617dba22031e41a3300fb5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", - "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/052852a96fd5e5ebdb617dba22031e41a3300fb5", + "reference": "052852a96fd5e5ebdb617dba22031e41a3300fb5", "shasum": "" }, "require": { @@ -8469,7 +8578,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.7-dev" + "dev-main": "3.8-dev" } }, "autoload": { @@ -8505,7 +8614,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.7.3" + "source": "https://github.com/symfony/service-contracts/tree/v3.8.1" }, "funding": [ { @@ -8525,7 +8634,8 @@ "type": "tidelift" } ], - "time": "2026-07-27T15:39:01+00:00" + "time": "2026-10-20T17:38:25+00:00", + "published-time": "2026-11-04T11:25:33+00:00" }, { "name": "symfony/string", @@ -8615,7 +8725,8 @@ "type": "tidelift" } ], - "time": "2026-10-23T17:06:53+00:00" + "time": "2026-10-23T17:06:53+00:00", + "published-time": "2026-10-28T18:24:27+00:00" }, { "name": "symfony/var-dumper", @@ -8702,20 +8813,21 @@ "type": "tidelift" } ], - "time": "2026-10-25T21:04:04+00:00" + "time": "2026-10-25T21:04:04+00:00", + "published-time": "2026-10-28T18:20:00+00:00" }, { "name": "symfony/yaml", - "version": "v8.1.10", + "version": "v8.1.11", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "9de4ff9874dd18b738b9f248398c3e36ef15a1d2" + "reference": "e8266f55160f674f290a5f567cb55e11e21f921b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/9de4ff9874dd18b738b9f248398c3e36ef15a1d2", - "reference": "9de4ff9874dd18b738b9f248398c3e36ef15a1d2", + "url": "https://api.github.com/repos/symfony/yaml/zipball/e8266f55160f674f290a5f567cb55e11e21f921b", + "reference": "e8266f55160f674f290a5f567cb55e11e21f921b", "shasum": "" }, "require": { @@ -8758,7 +8870,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v8.1.10" + "source": "https://github.com/symfony/yaml/tree/v8.1.11" }, "funding": [ { @@ -8778,7 +8890,8 @@ "type": "tidelift" } ], - "time": "2026-10-18T14:56:39+00:00" + "time": "2026-11-04T08:39:20+00:00", + "published-time": "2026-11-04T11:31:03+00:00" }, { "name": "thecodingmachine/safe", @@ -8921,7 +9034,8 @@ "type": "github" } ], - "time": "2026-02-04T18:08:13+00:00" + "time": "2026-02-04T18:08:13+00:00", + "published-time": "2026-02-15T15:48:29+00:00" }, { "name": "webmozart/assert", @@ -8979,7 +9093,8 @@ "issues": "https://github.com/webmozarts/assert/issues", "source": "https://github.com/webmozarts/assert/tree/1.12.1" }, - "time": "2025-10-29T15:56:20+00:00" + "time": "2025-10-29T15:56:20+00:00", + "published-time": "2025-10-29T15:56:42+00:00" } ], "aliases": [], @@ -8995,5 +9110,5 @@ "ext-json": "*" }, "platform-dev": {}, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.11.0" } diff --git a/infection.json b/infection.json index 574d330..dc7392a 100644 --- a/infection.json +++ b/infection.json @@ -4,7 +4,6 @@ "directories": [ "php/composer", "php/php-scoper", - "php/phpstan", "php/quality-assurance" ] }, diff --git a/package-lock.json b/package-lock.json index 0987b56..ebc353a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -97,14 +97,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", - "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "version": "7.29.12", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.12.tgz", + "integrity": "sha512-2W76mhjrpQFO1lK5e8IvGYB2D0YZ7S2R1Z2BjO2ZUYuW+amcpK/z60U6ZZmzskwoa7sJ6XohP/Wg8H5KxXI/IA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.8", - "@babel/types": "^7.29.8", + "@babel/parser": "^7.29.12", + "@babel/types": "^7.29.11", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -154,9 +154,9 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", - "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "version": "7.29.12", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.12.tgz", + "integrity": "sha512-ACxGoWMV866q/QlZHeCOaQZAR5QclKsIvkAmlDemqjm6+6Bynijs1mH068BzH2N2e+wSuDmBWY87fqKfvhi1Lw==", "dev": true, "license": "MIT", "dependencies": { @@ -165,7 +165,7 @@ "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", - "@babel/traverse": "^7.29.7", + "@babel/traverse": "^7.29.12", "semver": "^6.3.1" }, "engines": { @@ -419,9 +419,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.11", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.11.tgz", - "integrity": "sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==", + "version": "7.29.12", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.12.tgz", + "integrity": "sha512-WJ7JBp/Q4+z/ImydtI7jS6NVco5++632ExBemSX+Ysb+G1lCwQ5BVo3SY+jGGOcZXI4ZccbZSUSJLNBW6eEpJg==", "dev": true, "license": "MIT", "dependencies": { @@ -698,9 +698,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.29.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.11.tgz", - "integrity": "sha512-iARF4jjHSM0v8DImCACPXrwEltBP3DgqDpXTvjAMBAVpyEY0LiGss78wvI+KtmW+IJNEvZ5sOhPDCsOsXsIybA==", + "version": "7.29.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.12.tgz", + "integrity": "sha512-nEreZBofjfd6FG621xe9Rk3+tMGBXf1nAuFrR6NUGF67fkQbSQf3NrbRuhgWRn+zb0qcy9XVok1GVFI/t9wWVQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1803,18 +1803,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.29.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz", - "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==", + "version": "7.29.12", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.12.tgz", + "integrity": "sha512-MscXe/NAZcOkYEsRajHZFt3ynsb22PUi6CTrp5U7+RYycR9By84mn9mFN54YgfITiqK4hFx3Ju3tZSyzfpS5ew==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.8", + "@babel/generator": "^7.29.12", "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.9", + "@babel/parser": "^7.29.12", "@babel/template": "^7.29.7", - "@babel/types": "^7.29.8", + "@babel/types": "^7.29.11", "debug": "^4.3.1" }, "engines": { @@ -1944,9 +1944,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.15.tgz", - "integrity": "sha512-J0u7HkVl2nzSlhsiTOp4AmwcUQ3D+mGEEKfBy/7To5/y7F2OHwyLrXfrhR0SMgr4p5Lo+eaMVSeai24zUcBIxA==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.16.tgz", + "integrity": "sha512-LsDbJPP/B8cPasUJyTA5S99F7C4G93ngW8hUluXz8CFeOaaRnBPtBiIlvgcPYU0HmHNz8CXg+1vwFrIB4F6Dgw==", "dev": true, "funding": [ { @@ -3110,125 +3110,370 @@ } }, "node_modules/@opentelemetry/api": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", - "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.10.0.tgz", + "integrity": "sha512-lMkyes8iHrG+mk6WH7X70MdCLI2RS0FwNBY1PyQ8UemaijE2m5mv5A0Niy3HtbYTFrgkUEQjFMXmSAk2hQDuPQ==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8.0.0" } }, - "node_modules/@opentelemetry/api-logs": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz", - "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==", + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.152.0.tgz", + "integrity": "sha512-393VtirINkae4XHBTkq/yQxhOwEr4V+r0et37P2fp+sYZSdYgZTRDPo5Um8vBh0QjMZ+RmaMy4PP0qwr4cTMwA==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@opentelemetry/core": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.12.0.tgz", - "integrity": "sha512-1HsSAuvT4/my0QrXWsyXFdWjKaPeCHsismKZgEEIb8NK13LP6DrMRlsUbvD/0F9mDWK3xDom9AMv3IscxgOnOQ==", + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.152.0.tgz", + "integrity": "sha512-iYFcExATJNZ0rZj0o02THbD1Nc9jAW0nVgIQPee6ejbSASpbZXlIMw8jxtpzaG14JQTWwGSZ6U5dEtRJXE6V3Q==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@opentelemetry/instrumentation": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.220.0.tgz", - "integrity": "sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==", + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.152.0.tgz", + "integrity": "sha512-fnU+DfUzi5/rFy0LzmipYRz06PAUUkOS8kCJQ3Eg6oqlunC1XZXoffFqxmdL51RmcbYtUG3P8DVG1U5fckoa2A==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.220.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@opentelemetry/resources": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.12.0.tgz", - "integrity": "sha512-+rSRoOdln6NhXTVcBdHSlZxJDNzwxadeweK2ltZO3tVtfp6aklABc06xclDk6ovRR+EiGgIcSbh/6NgjFhnb3A==", + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.152.0.tgz", + "integrity": "sha512-Y3U2okryWTs7hDld6UhjJwLNf8/do+x4g4C+1U5SiGwrTLwbO83WJYJA8+l9iFSilWBgnfpd/+U76tfeNxUHzA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@opentelemetry/sdk-trace": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.12.0.tgz", - "integrity": "sha512-ImVuRQ6faOsTi1Wna88s2avgrrf/iDgXPigIeynkUhjstMuoQUDOJXgkez2j1mlU4btjXcXBGa+q7qKFYR3GsA==", + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.152.0.tgz", + "integrity": "sha512-XkhkRrrPffEw+ZLv/d/5pEni0buxinm/E/N6fLbJCunD1DBbeCTtVP23ESV+4CWCWdSz397asks1ViGslpmWiQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.0", - "@opentelemetry/resources": "2.12.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.12.0.tgz", - "integrity": "sha512-JOERgZCxRM06FEJ5kEWxlSRChah6MmXDx3KwCruqEv/iEyLkTokCSemf2y1w8oIHtfrr8EVTpzebq/31vn2Cpg==", - "deprecated": "Use @opentelemetry/sdk-trace package", + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.152.0.tgz", + "integrity": "sha512-MLzsRRbdfmdzo6JEhbC1yKU9ItfvM8tEf18HM32jCiNYZEz6J3bRBRl3cTkKR0vBdrk9ZVlcARXtwHccs+epDA==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.0", - "@opentelemetry/resources": "2.12.0", - "@opentelemetry/sdk-trace": "2.12.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", - "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.152.0.tgz", + "integrity": "sha512-HHerTEJz7Iy10Ji6BGsk4gqG6PU0UzFuBu91jIEOX89FnrI5ECWSEr7BLpkYQhp7U0QFtMnBhNfQKI0hYZK78w==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14" + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.152.0.tgz", + "integrity": "sha512-9Bb8GyzZ2uSLzRDZ27usaKokr07mxiBe2dDF5b6bd2janmaJTT1gL51UfP7AU30yFiXrJvcRyFKREAgKwZYs0w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.152.0.tgz", + "integrity": "sha512-om7iDMUroI74A/lfRRWY9/Ev3VWrVsrUZ4tyvyh8raBey/sdcfyAD7tYWc+iykAiotnmftyKTpUKFr1QnaBrAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.152.0.tgz", + "integrity": "sha512-Lf1OL2lMTqwDMFC6jnRb30B+WYHKV0XT+bPHcFM6xHtNVwxpobiTzUIvKH6a0LWpvfwyru8Yu6zCGHqqFHH+gg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.152.0.tgz", + "integrity": "sha512-BYWSMQK+YJ9ZILStPcmmiSVq8KEG9Qdq/c4BHPC3rUEq/fEDCVjUIwdj5ZzjgZ9P9is9PKu0tcAXzq1ZXg4urQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.152.0.tgz", + "integrity": "sha512-d2LqHNNNqdH185sRK5dPKWZu201JrNu9/CRFuVEuT+ht7n/QaVblEMWT9MALQPrDWYvhG10P+NSKJFquDWZ09w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.152.0.tgz", + "integrity": "sha512-8gBeHt5rA/8+pKhHl+zLggfa3L/V/L2at96p7ty6pBE7id6lFO19nFUc15u2PcjC4OYeYK465MKy1fhhbpFIGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.152.0.tgz", + "integrity": "sha512-ltgrSF6L+yPTVsIdaAyqKq3zTqzj/1VlNbxWZuxAe8flR3sWdyNsKvdXukoO8/ucJbLickhmieRBge+o/e8Ugw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.152.0.tgz", + "integrity": "sha512-LcMp6BvrBnewSfdkLHoSqidhoAq4syKOTBV5TYHWd+2VnGkfD/nPRRVIzTBt/Lkbt7pfOFDGtkiTntFS0p6sQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.152.0.tgz", + "integrity": "sha512-jbhoMCslVxzRCRNQ8TSHxjYdSKRvk8O6ciLVDkahJvmyHDzBqba/bMQHM6ULKj/rgHNNbSzlIiakdx8SEfkJzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.152.0.tgz", + "integrity": "sha512-JZYVlhv36mr/nertnRB7vchJECOBb1Lz6lltalILsB+o9M/is+H6BEDRLQKdqA4JKs6NjJszo46+iM5sfwj6jQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.152.0.tgz", + "integrity": "sha512-1X9oganvVsunRH0lH10UU05N+H9atpX6WsOybByUjwnfcd3h/0tYwvuv16XuHglrIUOnxV2TodbLVEwwafd64g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.152.0.tgz", + "integrity": "sha512-sw0M80/dn9rcpu+Rqqph/C+MjvDDce6MlXmFjsNQtPEymha0eJy3ml6+foSUTqFfpDIDyHZZ4eoJ43IoMe8bJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz", + "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" } }, "node_modules/@parcel/watcher": { @@ -3552,14 +3797,13 @@ } }, "node_modules/@paulirish/trace_engine": { - "version": "0.0.65", - "resolved": "https://registry.npmjs.org/@paulirish/trace_engine/-/trace_engine-0.0.65.tgz", - "integrity": "sha512-Qsm6F5C8xf6ZzQXbQc2+wcpe6sggfs/gvc/ytqSurdvYg3kyW0ECHCqE0CWBKZpqgjVfPNX9c7SCS3r2nEIRGg==", + "version": "0.0.67", + "resolved": "https://registry.npmjs.org/@paulirish/trace_engine/-/trace_engine-0.0.67.tgz", + "integrity": "sha512-AWZSKQDwbWKIO//xGvM8isSyDgEDZWDq12SbMQMxl6eE9ZgWaoc9n/2d7REjwcRANyA1Zjo1rVfJiSRFFGKUww==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "legacy-javascript": "latest", - "third-party-web": "latest" + "third-party-web": "*" } }, "node_modules/@peculiar/asn1-cms": { @@ -3874,163 +4118,64 @@ } } }, - "node_modules/@puppeteer/browsers/node_modules/ansi-regex": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", - "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@puppeteer/browsers/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@puppeteer/browsers/node_modules/cliui": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", - "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@puppeteer/browsers/node_modules/cliui/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@puppeteer/browsers/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "dev": true, "license": "MIT" }, - "node_modules/@puppeteer/browsers/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "node_modules/@sentry/bundler-plugins": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/@sentry/bundler-plugins/-/bundler-plugins-11.10.0.tgz", + "integrity": "sha512-+ngJ485rIYll5uOhWSvSO+Vx4S24MGe5KcLKlUZghr+BXK+ib32uBOZNG294wGyeauLTDvdjncLEYwiaBnP/5w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.2.2" + "@sentry/core": "11.10.0", + "dotenv": "^17.4.2", + "glob": "^13.0.6", + "magic-string": "~0.30.8 || ^1.0.0", + "oxc-parser": "^0.152.0", + "sentry": "^0.45.0", + "supports-color": "^8.1.1" }, "engines": { - "node": ">=12" + "node": ">=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0" }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@puppeteer/browsers/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" + "peerDependencies": { + "rollup": ">=3.2.0", + "webpack": ">=5.1.0" }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "peerDependenciesMeta": { + "rollup": { + "optional": true + }, + "webpack": { + "optional": true + } } }, - "node_modules/@puppeteer/browsers/node_modules/wrap-ansi/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "node_modules/@sentry/bundler-plugins/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=18" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@puppeteer/browsers/node_modules/yargs": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", - "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^9.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "string-width": "^8.2.1", - "y18n": "^5.0.5", - "yargs-parser": "^22.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, - "node_modules/@puppeteer/browsers/node_modules/yargs-parser": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", - "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, "node_modules/@sentry/conventions": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@sentry/conventions/-/conventions-0.16.0.tgz", - "integrity": "sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==", + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@sentry/conventions/-/conventions-0.29.0.tgz", + "integrity": "sha512-kYq9/HJ/l6MYP/srIc20dy1W4mWyJfCF0OWFF+RhBGZwyr/wCmtabRFKyw7kqM+DTwZXQOjSTSZUcJhDowcInQ==", "dev": true, "license": "MIT", "engines": { @@ -4038,110 +4183,81 @@ } }, "node_modules/@sentry/core": { - "version": "10.76.2", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.76.2.tgz", - "integrity": "sha512-vndrQalRiwYatTkjlLWvyf69KocXIb9TKJ4A9oiyWguL8PJYtqIRTlRe4OpkpjLrxS18fvRdV0jEm29c/9p3hQ==", + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-11.10.0.tgz", + "integrity": "sha512-l/OtTo1v+TQgZtHG2Q5N4IX+gFdAE1tZ27K/P21UCzJglWk54TPEm+d9T2lulPqMK06kZcLwDgRAhE2GO4YS5w==", "dev": true, "license": "MIT", "dependencies": { - "@sentry/conventions": "^0.16.0" + "@sentry/conventions": "^0.29.0" }, "engines": { - "node": ">=18" + "node": ">=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0" } }, "node_modules/@sentry/node": { - "version": "10.76.2", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.76.2.tgz", - "integrity": "sha512-LSwGGE/GceTbkDHRoQQr3lq/dpxxA6RdooBbuGTJ/S6BdqKX2kw2FdV7Se9eJzZJJJ5AxCZum37+ejYS7N2pJg==", + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-11.10.0.tgz", + "integrity": "sha512-ZsndrSn1NjL9AslpUdxz/kYPcv/i+plVZoyPe1ivlr5XPUpocB8pUL5noYbIUl96Wd/yGvPBw6zqO8d69rRKQQ==", "dev": true, "license": "MIT", "dependencies": { "@opentelemetry/api": "^1.9.1", - "@opentelemetry/instrumentation": "^0.220.0", - "@opentelemetry/sdk-trace-base": "^2.9.0", - "@sentry/conventions": "^0.16.0", - "@sentry/core": "10.76.2", - "@sentry/node-core": "10.76.2", - "@sentry/opentelemetry": "10.76.2", - "@sentry/server-utils": "10.76.2", - "import-in-the-middle": "^3.0.0" + "@sentry/bundler-plugins": "11.10.0", + "@sentry/conventions": "^0.29.0", + "@sentry/core": "11.10.0", + "@sentry/opentelemetry": "11.10.0", + "@sentry/server-runtime-injection": "11.10.0", + "@sentry/server-utils": "11.10.0" }, "engines": { - "node": ">=18" + "node": ">=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0" } }, - "node_modules/@sentry/node-core": { - "version": "10.76.2", - "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.76.2.tgz", - "integrity": "sha512-TrgAc/SvU5kQfjw8pz6I8LLIUiblOSf9BPPNkTbS0Dzcqa4yQLVbmsbS3rvGT7ot64m9Vjzn327ZRy63MlI6/w==", + "node_modules/@sentry/opentelemetry": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-11.10.0.tgz", + "integrity": "sha512-YMV4ZgaRlW7QPCcMScVpSBvYOdvDlY+alqRT3KOTcmPKFZtf8O2hXHWFhl/M33kKDEfqOk2XtEVeHs3mZFOCaw==", "dev": true, "license": "MIT", "dependencies": { - "@sentry/conventions": "^0.16.0", - "@sentry/core": "10.76.2", - "@sentry/opentelemetry": "10.76.2", - "import-in-the-middle": "^3.0.0" + "@sentry/conventions": "^0.29.0", + "@sentry/core": "11.10.0" }, "engines": { - "node": ">=18" + "node": ">=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/core": "^1.30.1 || ^2.1.0", - "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", - "@opentelemetry/instrumentation": ">=0.57.1 <1", - "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@opentelemetry/core": { - "optional": true - }, - "@opentelemetry/exporter-trace-otlp-http": { - "optional": true - }, - "@opentelemetry/instrumentation": { - "optional": true - }, - "@opentelemetry/sdk-trace-base": { - "optional": true - } + "@opentelemetry/api": "^1.9.0" } }, - "node_modules/@sentry/opentelemetry": { - "version": "10.76.2", - "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.76.2.tgz", - "integrity": "sha512-deqWrKJGaku3nhiZchg34z8W9v6kUwtzoQ4cmEq6CeT8/4X93X4YfeJgoh1dhbswFRpwor4pQ6kj0KttagKtfA==", + "node_modules/@sentry/server-runtime-injection": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/@sentry/server-runtime-injection/-/server-runtime-injection-11.10.0.tgz", + "integrity": "sha512-1nc+HNPldrPEov16sL5m2cOLVgNJ+FsR7QtaWGOOv8qYFJkSp95lyc2qzLnaG2L72UzSMOv10peGRioAKrrZQQ==", "dev": true, "license": "MIT", "dependencies": { - "@sentry/conventions": "^0.16.0", - "@sentry/core": "10.76.2" + "@sentry/core": "11.10.0", + "@sentry/server-utils": "11.10.0" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/core": "^1.30.1 || ^2.1.0", - "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" + "node": ">=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0" } }, "node_modules/@sentry/server-utils": { - "version": "10.76.2", - "resolved": "https://registry.npmjs.org/@sentry/server-utils/-/server-utils-10.76.2.tgz", - "integrity": "sha512-wogj6Trj8fAu+aD7HbpvHEBlyANMOvvO9MP5NFJaiQbprLKugtCdE6rFTxDhofXvRo4nswL9m4Cvysg3k5KcvQ==", + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/@sentry/server-utils/-/server-utils-11.10.0.tgz", + "integrity": "sha512-m/MVCvPzQJJZZCTFlVPCrTuoncT7SR80+QKg1K7ul25AsL0byABr5KsHCbXVpOYXLVz04Omk+pOOR5Cv2IAlrA==", "dev": true, "license": "MIT", "dependencies": { - "@sentry/conventions": "^0.16.0", - "@sentry/core": "10.76.2" + "@opentelemetry/api": "^1.9.1", + "@sentry/conventions": "^0.29.0", + "@sentry/core": "11.10.0" }, "engines": { - "node": ">=18" + "node": ">=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0" } }, "node_modules/@sindresorhus/base62": { @@ -4627,9 +4743,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.10.tgz", + "integrity": "sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==", "dev": true, "license": "MIT" }, @@ -4708,9 +4824,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.6.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", - "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", + "version": "26.6.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.6.tgz", + "integrity": "sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==", "dev": true, "license": "MIT", "dependencies": { @@ -6861,9 +6977,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.6.1", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.6.1.tgz", - "integrity": "sha512-cL1Qz6ADZhcEbny/8HPfe99J6HhNoYtpX2LFLIbhgGE7Q1hlQVkYFdetDN7Id3KiQxhDrHwzlHr/YQCnZ8+xSA==", + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.6.2.tgz", + "integrity": "sha512-VOw9/yGTGwPPgzFcODtcv6zhEPIeIWkRSe10L2ri2f5vZ7ID7lCt6jZzRF5/ubgOZ+szYydmEF5AYsA5jxX1tA==", "funding": [ { "type": "opencollective", @@ -7013,9 +7129,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.11.33", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.33.tgz", - "integrity": "sha512-yiVKDX+qCMRKBE084lXvq9EcwFUhKZ2nU8TK1EQQuOgPNR/lmbGxXOeDdlGUqC2Zz11Cs9rk1d/8NfgBmlkt0w==", + "version": "2.11.34", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.34.tgz", + "integrity": "sha512-Aqr8mA1kIvyJ3UY5YAY7ib8LSo1DmDMbMnVqGPtsKW1LxaFQFwn9Eb3yvq2fUKJsRvaXjnAV7R5osqETapzNiA==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -7111,9 +7227,9 @@ } }, "node_modules/browserslist": { - "version": "4.29.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.4.tgz", - "integrity": "sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA==", + "version": "4.29.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.5.tgz", + "integrity": "sha512-khTmKu5NGYTotqcYo8Bew2k4RAE9PuXRSFRXG2lQBrCEQHyeQ/vX6gKMxfokwywBsLiXZMnHsmWMG1pnO8J7uQ==", "funding": [ { "type": "opencollective", @@ -7130,10 +7246,10 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.11.29", - "caniuse-lite": "^1.0.30001816", - "electron-to-chromium": "^1.5.459", - "node-releases": "^2.0.59", + "baseline-browser-mapping": "^2.11.33", + "caniuse-lite": "^1.0.30001819", + "electron-to-chromium": "^1.5.464", + "node-releases": "^2.0.60", "update-browserslist-db": "^1.3.4" }, "bin": { @@ -7336,9 +7452,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001819", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001819.tgz", - "integrity": "sha512-LFUqIIAwpzsIuevnKnsR9qnStBcgzPShTX61lBwN9uFkkf8o8zLiebEjbW8AYtZiF+1YKDWfdt/pWDtMuG8Xwg==", + "version": "1.0.30001820", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001820.tgz", + "integrity": "sha512-gfWD6MxqVCqAOwmODfZf7DchWvlrA+SRdNX7ceLfg3xRqe5ssjjpF7EgslC/0tiLiNTajdaNN/q8c/5u/7Gz8w==", "funding": [ { "type": "opencollective", @@ -7545,48 +7661,73 @@ "devtools-protocol": "*" } }, - "node_modules/cjs-module-lexer": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.3.0.tgz", - "integrity": "sha512-lmNyBzi6iYiqyrExTctfmjd8tInx+dHu9PBomTfH5RXS53c27JMFJgzNv5TyAnm3PgGf+MLy0+Ws41/HlVxdKQ==", - "dev": true, - "license": "MIT" - }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, "license": "ISC", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.5.0.tgz", + "integrity": "sha512-zhvbbbdt1kbw7EJ9OQoxmQcuaNR9P5kTj7e/YdmOSU10aHWbycofJxA6jKfoEy3vlLdctOF9ZHaxLtmSm998wQ==", + "dev": true, + "license": "MIT", "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "dev": true, "license": "MIT" }, "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/clone-deep": { @@ -7751,19 +7892,20 @@ "license": "MIT" }, "node_modules/configstore": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-7.1.0.tgz", - "integrity": "sha512-N4oog6YJWbR9kGyXvS7jEykLDXIE2C0ILYqNBZBp9iwiJpoCBWYsuAdW6PPFn6w06jjnC+3JstVvWHO4cZqvRg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-8.0.0.tgz", + "integrity": "sha512-U51WPZg+o6FDFhBCV6yYFEspGMCoHvCgJSGFw9AwsW2u75gYBPoDlYca5XfDs4TdMUu3/y0R6FFdvvgyH31mKQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "atomically": "^2.0.3", - "dot-prop": "^9.0.0", + "atomically": "^2.1.0", + "dot-prop": "^10.1.0", "graceful-fs": "^4.2.11", + "is-safe-filename": "^0.1.0", "xdg-basedir": "^5.1.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -8408,13 +8550,16 @@ } }, "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/define-properties": { @@ -8492,9 +8637,9 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1687809", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", - "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", + "version": "0.0.1727723", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1727723.tgz", + "integrity": "sha512-laxmpDN+zItbpy5klVbHqBsZ38O2rC6VwI9W5daM11Pc3Dj2gruc2wwnHW48CB8wYiWHALDkA6vRg92U2cfdLA==", "dev": true, "license": "BSD-3-Clause" }, @@ -8608,21 +8753,34 @@ } }, "node_modules/dot-prop": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", - "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-10.2.0.tgz", + "integrity": "sha512-BTJ9aZYL3vCfZlZOBLy9v8TUqWGQ0pzFnygKwFZt5udj6viBoFIBviKPUoZLDCPn1FoXffv6McQFDenrm5Krfw==", "dev": true, "license": "MIT", "dependencies": { - "type-fest": "^4.18.2" + "type-fest": "^5.0.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -8653,9 +8811,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.464", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.464.tgz", - "integrity": "sha512-nRa65aFge40viPAKxMKULIY8tcXKwKboJWNn4RoKwyWXJ6SP7BnrDGKUTzc64818jIciH0PK7LSF8QzZes19GA==", + "version": "1.5.468", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.468.tgz", + "integrity": "sha512-8XdEp3sIqO+PzmVPxhzqHq/3+yCxp44m27cm2WY1sRn3ZhwKyKmqSi2hx6gEhmnOXmh8NUuYE00z6Onzn5m6DA==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -8676,13 +8834,17 @@ } }, "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.1.tgz", + "integrity": "sha512-Ev/hIyePLZtpL98x562lvPtenrUfGr0lfaf44HFZ+hDEwso7ccV9QAZrEVLg0hxNAzygaAFxy2dYaAKKM0G06A==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/encoding": { @@ -8918,9 +9080,9 @@ } }, "node_modules/es-module-lexer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-3.0.4.tgz", - "integrity": "sha512-BldNY5mwkVu+cQ5Ymn3e6hB9Sx8XHx4ZevWbiUyZbfKxJxYO27Hf0wfzkxIaD5CAJMaGl3YspKELj0ff/7nCMA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", "dev": true, "license": "MIT" }, @@ -10379,22 +10541,18 @@ } }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -10430,37 +10588,6 @@ "tslib": "2" } }, - "node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", - "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/global-modules": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", @@ -11017,21 +11144,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-in-the-middle": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.5.3.tgz", - "integrity": "sha512-usm/IiY63VtOyx/dm7uEoZV+EsanCDmwRUroQZIkFLGu1NVXZ8mZqIOhaN4QIJ7EsbMTuQbJXwCySCx+ozMA8g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "cjs-module-lexer": "^2.2.0", - "es-module-lexer": "^3.0.3", - "module-details-from-path": "^1.0.4" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -11715,6 +11827,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-safe-filename": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-safe-filename/-/is-safe-filename-0.1.1.tgz", + "integrity": "sha512-4SrR7AdnY11LHfDKTZY1u6Ga3RuxZdl3YKWWShO5iyuG5h8QS4GD2tOb04peBJ5I7pXbR+CGBNEhTcwK+FzN3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-set": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", @@ -12191,13 +12316,6 @@ "shell-quote": "^1.10.0" } }, - "node_modules/legacy-javascript": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/legacy-javascript/-/legacy-javascript-0.0.4.tgz", - "integrity": "sha512-UTmeKBVxnfJcpmpOvzzNnLsQT99BXHpPlc/ZKddIRmWmrR0fw6335I3lq4+sJoP60UTbBNaYsGYVPc65YB1x3A==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -12213,40 +12331,34 @@ } }, "node_modules/lighthouse": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/lighthouse/-/lighthouse-13.5.0.tgz", - "integrity": "sha512-w/yYIwg7mP9pCwkcbAXRztnvKHua2dJnZaNptPLa1cEw+akr6lbsIDiUOLsQwvaiAUr0xvxdmwvAzl3pj3Np0A==", + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/lighthouse/-/lighthouse-13.6.0.tgz", + "integrity": "sha512-2/HUPUpQyGm01VEgYCUjSlNZY7Zkhk1v3QI4c7UTbnaoRB7HF0qYMPzzaiidNf5iqw5wnmtm9LpGawKKqCfLmg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@paulirish/trace_engine": "0.0.65", - "@sentry/node": "^10.0.0", - "ajv": "^8.20.0", + "@paulirish/trace_engine": "0.0.67", + "@sentry/node": "^11.8.0", "ajv-formats": "^3.0.1", - "axe-core": "^4.13.0", - "chrome-launcher": "^1.2.1", - "configstore": "^7.0.0", + "axe-core": "4.14.0", + "chrome-launcher": "^1.2.2", + "configstore": "^8.0.0", "csp_evaluator": "1.1.8", - "devtools-protocol": "0.0.1687809", + "devtools-protocol": "0.0.1727723", "enquirer": "^2.3.6", "http-link-header": "^1.1.1", "intl-messageformat": "^10.5.3", - "jpeg-js": "^0.4.4", "js-library-detector": "^6.7.0", "lighthouse-logger": "^2.0.2", "lighthouse-stack-packs": "1.12.3", - "lodash-es": "^4.17.21", - "lookup-closest-locale": "6.2.0", - "open": "^8.4.0", - "puppeteer-core": "^25.9.0", + "open": "^11.0.4", + "puppeteer-core": "^25.16.0", "robots-parser": "^3.0.1", "speedline-core": "^1.4.3", "third-party-web": "^0.30.0", - "tldts-icann": "^7.4.13", - "web-features": "^3.39.0", - "ws": "^7.0.0", - "yargs": "^17.3.1", - "yargs-parser": "^21.0.0" + "tldts-icann": "^7.4.19", + "yargs": "^18.2.0", + "yargs-parser": "^22.0.0" }, "bin": { "chrome-debug": "core/scripts/manual-chrome-launcher.js", @@ -12275,30 +12387,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/lighthouse/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/lighthouse/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, "node_modules/lilconfig": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.4.tgz", @@ -12370,13 +12458,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash-es": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", - "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -12430,13 +12511,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lookup-closest-locale": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/lookup-closest-locale/-/lookup-closest-locale-6.2.0.tgz", - "integrity": "sha512-/c2kL+Vnp1jnV6K6RpDTHK3dgg0Tu2VVp+elEiJpjfS1UyY7AjOYHohRug6wT0OpoX2qFgNORndE9RqesfVxWQ==", - "dev": true, - "license": "MIT" - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -13523,6 +13597,16 @@ } } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", @@ -13540,13 +13624,6 @@ "node": ">=18.0.0" } }, - "node_modules/module-details-from-path": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", - "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", - "dev": true, - "license": "MIT" - }, "node_modules/mousetrap": { "version": "1.6.5", "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz", @@ -13693,9 +13770,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.60", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.60.tgz", - "integrity": "sha512-OmO8+GM1JUZqXET720VrJCwkjUCXmCMQm0Z3UDDRbfNLHS4NEAvY60j5jiFv0tkKw2PNNlqOLhahVmwJIOLDIQ==", + "version": "2.0.62", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.62.tgz", + "integrity": "sha512-A0Eq+qVDQI97IbAD6g/HriIuv256el9N8EwF4+xqJ49pwrdRYpdCSmLk0sO4I3fnS6hpYE5v22B73Bk+WArEoQ==", "license": "MIT", "engines": { "node": ">=18" @@ -13877,6 +13954,59 @@ "node": ">=10" } }, + "node_modules/npm-packlist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/npm-packlist/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/npm-packlist/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm-packlist/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -14065,18 +14195,21 @@ } }, "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.4.tgz", + "integrity": "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==", "dev": true, "license": "MIT", "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" + "default-browser": "^5.5.1", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.2.1", + "wsl-utils": "^1.0.0" }, "engines": { - "node": ">=12" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -14129,6 +14262,43 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oxc-parser": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.152.0.tgz", + "integrity": "sha512-ZztsZgaCWmPeKYMdoTT5e0oV/oxD1NeLWNR3kAtr/G707cUPYaaozRlVDHLCGnszHJRszN9mJwmPNpqgwloC0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.152.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.152.0", + "@oxc-parser/binding-android-arm64": "0.152.0", + "@oxc-parser/binding-darwin-arm64": "0.152.0", + "@oxc-parser/binding-darwin-x64": "0.152.0", + "@oxc-parser/binding-freebsd-x64": "0.152.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.152.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.152.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.152.0", + "@oxc-parser/binding-linux-arm64-musl": "0.152.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.152.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.152.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.152.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.152.0", + "@oxc-parser/binding-linux-x64-gnu": "0.152.0", + "@oxc-parser/binding-linux-x64-musl": "0.152.0", + "@oxc-parser/binding-openharmony-arm64": "0.152.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.152.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.152.0", + "@oxc-parser/binding-win32-x64-msvc": "0.152.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -14335,6 +14505,33 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/path-to-regexp": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", @@ -14556,9 +14753,9 @@ } }, "node_modules/postcss": { - "version": "8.5.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", - "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", + "version": "8.5.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz", + "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==", "funding": [ { "type": "opencollective", @@ -15401,28 +15598,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/puppeteer-core/node_modules/ws": { - "version": "8.22.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", - "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/pvtsutils": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", @@ -15464,9 +15639,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.16.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.1.tgz", - "integrity": "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==", + "version": "6.17.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.17.1.tgz", + "integrity": "sha512-jd3COkaKVgGUW+ZHyEHCG9bH7RoaaiWuMgzsIBjUMVjLAaWGWpoUAVwljVvBHxRu/2qEHnadLGZrkutYUBPQJA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -15891,19 +16066,9 @@ "node_modules/requestidlecallback": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/requestidlecallback/-/requestidlecallback-0.3.0.tgz", - "integrity": "sha512-TWHFkT7S9p7IxLC5A1hYmAYQx2Eb9w1skrXmQ+dS1URyvR8tenMLl4lHbqEOUnpEYxNKpkVMXUgknVpBZWXXfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "integrity": "sha512-TWHFkT7S9p7IxLC5A1hYmAYQx2Eb9w1skrXmQ+dS1URyvR8tenMLl4lHbqEOUnpEYxNKpkVMXUgknVpBZWXXfQ==", + "dev": true, + "license": "MIT" }, "node_modules/require-from-string": { "version": "2.0.2", @@ -15915,20 +16080,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-in-the-middle": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", - "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "module-details-from-path": "^1.0.3" - }, - "engines": { - "node": ">=9.3.0 || >=8.10.0 <9.0.0" - } - }, "node_modules/requireindex": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", @@ -16220,9 +16371,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.106.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.106.1.tgz", - "integrity": "sha512-kD+Gi66vycZcbJt+42NC4z4PXNMcCajVrJPhqoKKXy5cOBMmJQt052CxIBcRjwYXeImHYOpu/ydWWGZQIenl7A==", + "version": "1.107.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.107.0.tgz", + "integrity": "sha512-+whPilvVNoAD57xvdIPLQT0gXo4RD1lHYw+Uw8zYHlojaeaMDxK14tc0JmEsz/XMns48h3lpypisMP+3E8eHQA==", "dev": true, "license": "MIT", "dependencies": { @@ -16460,6 +16611,19 @@ "upper-case-first": "^2.0.2" } }, + "node_modules/sentry": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/sentry/-/sentry-0.45.0.tgz", + "integrity": "sha512-kgmEbLlC22lL7uneeUm9Sjz65rp5cvwYOMTxXbZCmBum4sbVre/lLT3zTV3kwXHJ9/BM8ZS0/GhZe9Cl1t6WyA==", + "dev": true, + "license": "FSL-1.1-Apache-2.0", + "bin": { + "sentry": "dist/bin.cjs" + }, + "engines": { + "node": ">=20.0" + } + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -17051,9 +17215,9 @@ } }, "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", - "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.5.0.tgz", + "integrity": "sha512-zhvbbbdt1kbw7EJ9OQoxmQcuaNR9P5kTj7e/YdmOSU10aHWbycofJxA6jKfoEy3vlLdctOF9ZHaxLtmSm998wQ==", "dev": true, "license": "MIT", "engines": { @@ -17431,9 +17595,9 @@ } }, "node_modules/stylelint/node_modules/ansi-regex": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", - "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.5.0.tgz", + "integrity": "sha512-zhvbbbdt1kbw7EJ9OQoxmQcuaNR9P5kTj7e/YdmOSU10aHWbycofJxA6jKfoEy3vlLdctOF9ZHaxLtmSm998wQ==", "dev": true, "license": "MIT", "engines": { @@ -17804,6 +17968,19 @@ "node": ">=8" } }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tannin": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/tannin/-/tannin-1.2.0.tgz", @@ -17995,9 +18172,9 @@ } }, "node_modules/tldts-core": { - "version": "7.4.19", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.19.tgz", - "integrity": "sha512-goNXV6Ho9TIOfpJZanTd6CKG5DyYgYAL+k+22T0SLh8l6+KHIfBUPFuePuSZv05UttIaaLmI9tb7TmGIdK5AkQ==", + "version": "7.4.20", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.20.tgz", + "integrity": "sha512-C630TWXZCZmLq0F2bW4QqwnaaxCQ/svpAqvJwnC4JvgEM5ohhBnGsEzTLSOXcN+xjz+kEaHS5uoWIw2klnqd0A==", "dev": true, "license": "MIT" }, @@ -18168,13 +18345,16 @@ } }, "node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.10.0.tgz", + "integrity": "sha512-NoSdpq/WEiAg5sjmBkmV/hfxv6HJH4NqPNrqjtSO5CwRmpsDfaf4begxW34KdJykH/l1yHtwBWQkCRdoXO8mPA==", "dev": true, "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, "engines": { - "node": ">=16" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -18655,13 +18835,6 @@ "node": ">=10.13.0" } }, - "node_modules/web-features": { - "version": "3.44.0", - "resolved": "https://registry.npmjs.org/web-features/-/web-features-3.44.0.tgz", - "integrity": "sha512-K/+YJI7uVE/b4G8SU67/F/4j81/ajJ6MfBCZyuQaqyuPlVXF8cd6dLqBznXIWJI9nwnvuY2dapKK08OHzTjYww==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/web-vitals": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", @@ -18754,6 +18927,28 @@ "node": ">= 10" } }, + "node_modules/webpack-bundle-analyzer/node_modules/ws": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/webpack-cli": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", @@ -18811,9 +19006,9 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-8.5.0.tgz", - "integrity": "sha512-+rXF28E5GKr7VnLH5qJaGIKLNPkthlA0VhqBFp/K6mjjCzOd6q34vYc2/3/ysRTjxmN01S98r1LwTTpWXDzWWA==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-8.6.0.tgz", + "integrity": "sha512-gDtuNFvzUys4RSkLAjXFCSbKz/MYIrCR9oXcIOnv4vFTmGsArddZoCb9LKlKWZO/JVL0COgK6hFBGPOt6EBg2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -18851,29 +19046,6 @@ "node": ">= 0.6" } }, - "node_modules/webpack-dev-middleware/node_modules/ws": { - "version": "8.22.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", - "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/webpack-dev-server": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-6.1.0.tgz", @@ -18928,62 +19100,6 @@ } } }, - "node_modules/webpack-dev-server/node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/webpack-dev-server/node_modules/open": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/open/-/open-11.0.4.tgz", - "integrity": "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "default-browser": "^5.5.1", - "define-lazy-prop": "^3.0.0", - "is-in-ssh": "^1.0.0", - "is-inside-container": "^1.0.0", - "powershell-utils": "^0.2.1", - "wsl-utils": "^1.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.22.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", - "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/webpack-merge": { "version": "5.10.0", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", @@ -19000,22 +19116,15 @@ } }, "node_modules/webpack-sources": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.6.0.tgz", - "integrity": "sha512-EIMmPVvNI0CYXyRPp34F2Qk7W37/BZVZIofS6LAUnrNjCVahS+hO4mxwFJDjQjODTGZDtVzLYoN2+1dYkhk9qA==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.7.0.tgz", + "integrity": "sha512-PdUhbCycwEgiS1hB/FBDksWKg/ashAIuJKqwdbPn8/dFVgU5LxQiBvG0js6VRRIzPujMrStYCczywp82P0KWng==", "dev": true, "license": "MIT", "engines": { "node": ">=10.13.0" } }, - "node_modules/webpack/node_modules/es-module-lexer": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", - "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", - "dev": true, - "license": "MIT" - }, "node_modules/webpack/node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -19156,43 +19265,88 @@ } }, "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.5.0.tgz", + "integrity": "sha512-zhvbbbdt1kbw7EJ9OQoxmQcuaNR9P5kTj7e/YdmOSU10aHWbycofJxA6jKfoEy3vlLdctOF9ZHaxLtmSm998wQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "dev": true, "license": "MIT" }, "node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/wrappy": { @@ -19216,17 +19370,17 @@ } }, "node_modules/ws": { - "version": "7.5.13", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", - "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.3.0" + "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -19311,54 +19465,31 @@ } }, "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", "dev": true, "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, "license": "ISC", "engines": { - "node": ">=12" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yocto-queue": { diff --git a/php/composer/CollectScopingStubs.php b/php/composer/GlobalSymbols.php similarity index 71% rename from php/composer/CollectScopingStubs.php rename to php/composer/GlobalSymbols.php index 77a1441..57ac3f5 100644 --- a/php/composer/CollectScopingStubs.php +++ b/php/composer/GlobalSymbols.php @@ -3,7 +3,8 @@ namespace DeepWebSolutions\Config\Composer; use Composer\InstalledVersions; -use DeepWebSolutions\Config\Composer\Internal\StubSymbolCollector; +use DeepWebSolutions\Config\Composer\Visitors\StubSymbolCollector; +use PhpParser\Error; use PhpParser\NodeTraverser; use PhpParser\NodeVisitor\NameResolver; use PhpParser\ParserFactory; @@ -11,8 +12,19 @@ /** * Collects the WordPress, Action Scheduler, stub-declared and dropped-package symbols that scoping leaves global. + * + * @internal */ -final class CollectScopingStubs { +final class GlobalSymbols { + // region MAGIC METHODS + + /** + * Constructor. + */ + protected function __construct() {} + + // endregion + // region METHODS /** @@ -22,7 +34,7 @@ final class CollectScopingStubs { * @param list $package_dirs The directories of the packages to scope. * @param list $dropped_dirs The directories of the packages that stay unscoped. * - * @throws \RuntimeException Thrown when a declared stubs file is missing or a file cannot be read. + * @throws \RuntimeException Thrown when a declared stubs file is missing, or a file cannot be read or does not parse. * * @return array{classes: list, functions: list, constants: list, namespaces: list} */ @@ -34,10 +46,27 @@ public static function collect( string $project_dir, array $package_dirs, array 'constants' => self::read_list( $wordpress . 'constants.json' ), ); + // A manifest declares each stubs file as ./path.php relative to itself, as vendor/package for that package's conventional package.php file, or as vendor/package:path.php. $files = array(); foreach ( array( $project_dir, ...$package_dirs ) as $dir ) { - \array_push( $files, ...self::declared_stubs( $dir ) ); + foreach ( (array) ( Manifest::extra( Manifest::read( $dir . '/composer.json' ) )['scoping-stubs'] ?? array() ) as $entry ) { + $entry = \is_string( $entry ) ? $entry : ''; + if ( \str_starts_with( $entry, './' ) ) { + $path = $dir . \substr( $entry, 1 ); + } else { + $parts = \explode( ':', $entry, 2 ); + $install = InstalledVersions::isInstalled( $parts[0] ) ? InstalledVersions::getInstallPath( $parts[0] ) : null; + $path = \is_null( $install ) ? '' : $install . '/' . ( $parts[1] ?? \basename( $parts[0] ) . '.php' ); + } + + if ( ! \is_file( $path ) ) { + throw new \RuntimeException( \sprintf( "'%s/composer.json' declares the scoping stubs %s, which do not exist.", $dir, \var_export( $entry, true ) ) ); + } + + $files[] = $path; + } } + foreach ( $dropped_dirs as $dir ) { foreach ( Finder::create()->files()->in( $dir )->exclude( array( 'tests', 'Tests' ) )->name( '*.php' ) as $file ) { $files[] = $file->getPathname(); @@ -46,8 +75,14 @@ public static function collect( string $project_dir, array $package_dirs, array $parser = new ParserFactory()->createForNewestSupportedVersion(); foreach ( \array_unique( $files ) as $file ) { + try { + $statements = $parser->parse( self::read( $file ) ) ?? array(); + } catch ( Error $error ) { + throw new \RuntimeException( "Could not parse '$file', whose symbols the run keeps global: " . $error->getMessage(), 0, $error ); + } + $collector = new StubSymbolCollector(); - new NodeTraverser( new NameResolver(), $collector )->traverse( $parser->parse( self::read( $file ) ) ?? array() ); + new NodeTraverser( new NameResolver(), $collector )->traverse( $statements ); $symbols['classes'] = array( ...$symbols['classes'], ...$collector->classes ); $symbols['functions'] = array( ...$symbols['functions'], ...$collector->functions ); @@ -62,8 +97,8 @@ public static function collect( string $project_dir, array $package_dirs, array $namespaces[ \preg_quote( \ltrim( \substr( $symbol, 0, $separator ), '\\' ), '/' ) ] = true; } } - $symbols['namespaces'] = array() === $namespaces ? array() : array( '/^(?:' . \implode( '|', \array_keys( $namespaces ) ) . ')$/i' ); + $symbols['namespaces'] = array() === $namespaces ? array() : array( '/^(?:' . \implode( '|', \array_keys( $namespaces ) ) . ')$/i' ); return $symbols; } @@ -71,38 +106,6 @@ public static function collect( string $project_dir, array $package_dirs, array // region HELPERS - /** - * Returns the stubs files a manifest declares: ./path.php relative to the manifest, vendor/package for that package's conventional package.php file, or vendor/package:path.php. - * - * @param string $dir The directory of the manifest. - * - * @throws \RuntimeException Thrown when a declared stubs file is missing or the manifest cannot be read. - * - * @return list - */ - protected static function declared_stubs( string $dir ): array { - $manifest = \json_decode( self::read( $dir . '/composer.json' ), true, flags: \JSON_THROW_ON_ERROR ); - $extra = \is_array( $manifest ) && isset( $manifest['extra'] ) && \is_array( $manifest['extra'] ) ? $manifest['extra'] : array(); - - $stubs_files = array(); - foreach ( (array) ( $extra['scoping-stubs'] ?? array() ) as $entry ) { - $entry = \is_string( $entry ) ? $entry : ''; - if ( \str_starts_with( $entry, './' ) ) { - $path = $dir . \substr( $entry, 1 ); - } else { - $parts = \explode( ':', $entry, 2 ); - $install = InstalledVersions::isInstalled( $parts[0] ) ? InstalledVersions::getInstallPath( $parts[0] ) : null; - $path = \is_null( $install ) ? '' : $install . '/' . ( $parts[1] ?? \basename( $parts[0] ) . '.php' ); - } - if ( ! \is_file( $path ) ) { - throw new \RuntimeException( \sprintf( "'%s/composer.json' declares the scoping stubs %s, which do not exist.", $dir, \var_export( $entry, true ) ) ); - } - $stubs_files[] = $path; - } - - return $stubs_files; - } - /** * Returns the strings of a JSON list file. * diff --git a/php/composer/Internal/ScopedSymbolCollector.php b/php/composer/Internal/ScopedSymbolCollector.php deleted file mode 100644 index 0e994fb..0000000 --- a/php/composer/Internal/ScopedSymbolCollector.php +++ /dev/null @@ -1,153 +0,0 @@ - - */ - public array $declared = array(); - - /** - * The prefixed names the code references, each with its location and whether a function_exists() call checks it. - * - * @var list - */ - public array $references = array(); - - /** - * The file the next traversal reads. - * - * @var string - */ - public string $file = ''; - - // endregion - - // region MAGIC METHODS - - /** - * Constructor. - * - * @param string $prefix The scoping prefix, without leading or trailing backslashes. - * @param Parser $parser The parser that reads the PHP code php-scoper scopes inside nowdoc strings. - */ - public function __construct( - protected readonly string $prefix, - protected readonly Parser $parser - ) {} - - // endregion - - // region INHERITED METHODS - - /** - * {@inheritDoc} - */ - #[\Override] - public function enterNode( Node $node ): int|null { - if ( $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\Function_ ) { - $this->declare( $node->namespacedName ); - } elseif ( $node instanceof Node\Stmt\Const_ ) { - foreach ( $node->consts as $const ) { - $this->declare( $const->namespacedName ); - } - } elseif ( $node instanceof Node\Scalar\String_ ) { - $this->string( $node ); - } elseif ( $node instanceof Node\Name && ! $node->getAttribute( 'parent' ) instanceof Node\Stmt\Namespace_ ) { - $use = $node->getAttribute( 'parent' ) instanceof Node\UseItem ? $node->getAttribute( 'parent' )->getAttribute( 'parent' ) : null; - $this->reference( $use instanceof Node\Stmt\GroupUse ? $use->prefix->toString() . '\\' . $node->toString() : $node->toString(), $node ); - } - - return null; - } - - /** - * {@inheritDoc} - */ - #[\Override] - public function leaveNode( Node $node ): null { - // The names in a call's arguments are resolved only once the traversal has visited them. - if ( $node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && \in_array( $node->name->toLowerString(), array( 'define', 'class_alias' ), true ) ) { - $argument = $node->args[ 'define' === $node->name->toLowerString() ? 0 : 1 ] ?? null; - if ( $argument instanceof Node\Arg && $argument->value instanceof Node\Scalar\String_ ) { - $this->declare( new Node\Name( \ltrim( $argument->value->value, '\\' ) ) ); - } elseif ( $argument instanceof Node\Arg && $argument->value instanceof Node\Expr\ClassConstFetch && $argument->value->class instanceof Node\Name ) { - $this->declare( $argument->value->class ); - } - } - - return null; - } - - // endregion - - // region HELPERS - - /** - * Records a declared symbol. - * - * @param Node\Name|null $name The resolved name of the symbol, or null for an anonymous class. - * - * @return void - */ - protected function declare( ?Node\Name $name ): void { - if ( ! \is_null( $name ) ) { - $this->declared[ $name->toLowerString() ] = $name->toLowerString(); - } - } - - /** - * Records a string literal shaped like a symbol name, and reads the PHP code of a nowdoc string that opens with a PHP tag, which php-scoper scopes like a file. - * - * @param Node\Scalar\String_ $node The string literal. - * - * @return void - */ - protected function string( Node\Scalar\String_ $node ): void { - if ( Node\Scalar\String_::KIND_NOWDOC === $node->getAttribute( 'kind' ) && \str_starts_with( $node->value, 'traverse( $this->parser->parse( $node->value ) ?? array() ); - } elseif ( 1 === \preg_match( '/^\\\\?([A-Za-z_\x80-\xff][\w\x80-\xff\\\\]*)$/', $node->value, $match ) ) { - $this->reference( $match[1], $node ); - } - } - - /** - * Records a name that carries the prefix, with its location and whether a function_exists() call checks it. - * - * @param string $name The name. - * @param Node $node The node that references it. - * - * @return void - */ - protected function reference( string $name, Node $node ): void { - if ( 0 === \stripos( $name, $this->prefix . '\\' ) ) { - $call = $node->getAttribute( 'parent' ) instanceof Node\Arg ? $node->getAttribute( 'parent' )->getAttribute( 'parent' ) : null; - $this->references[] = array( - 'name' => $name, - 'location' => $this->file . ':' . $node->getStartLine(), - 'checked' => $call instanceof Node\Expr\FuncCall && $call->name instanceof Node\Name && 'function_exists' === $call->name->toString(), - ); - } - } - - // endregion -} diff --git a/php/composer/Internal/StubSymbolCollector.php b/php/composer/Internal/StubSymbolCollector.php deleted file mode 100644 index 3f1d83e..0000000 --- a/php/composer/Internal/StubSymbolCollector.php +++ /dev/null @@ -1,132 +0,0 @@ - - */ - public array $classes = array(); - - /** - * The function names the stubs declare. - * - * @var list - */ - public array $functions = array(); - - /** - * The constant names the stubs declare, through const statements and define() calls. - * - * @var list - */ - public array $constants = array(); - - // endregion - - // region INHERITED METHODS - - /** - * {@inheritDoc} - */ - #[\Override] - public function enterNode( Node $node ): int|null { - switch ( \get_class( $node ) ) { - case Node\Stmt\Class_::class: - case Node\Stmt\Interface_::class: - case Node\Stmt\Trait_::class: - case Node\Stmt\Enum_::class: - if ( null !== $node->namespacedName ) { - $this->collect( $this->classes, $node->namespacedName->toString() ); - } - return NodeVisitor::DONT_TRAVERSE_CHILDREN; - case Node\Stmt\Function_::class: - if ( null !== $node->namespacedName ) { - $this->collect( $this->functions, $node->namespacedName->toString() ); - } - break; - case Node\Stmt\Const_::class: - foreach ( $node->consts as $const ) { - if ( null !== $const->namespacedName ) { - $this->collect( $this->constants, $const->namespacedName->toString() ); - } - } - break; - case Node\Expr\FuncCall::class: - if ( ! $node->name instanceof Node\Name ) { - break; - } - // A define() name and a class_alias() alias are real symbols, so scoped code that references them must keep them unprefixed. - $function = $node->name->toString(); - if ( 'define' === $function ) { - $this->collect_string_argument( $this->constants, $node, 0 ); - } elseif ( 'class_alias' === $function ) { - $this->collect_string_argument( $this->classes, $node, 1 ); - } - break; - } - - return null; - } - - // endregion - - // region HELPERS - - /** - * Adds a symbol when it has the name shape php-scoper's exclusion lists accept. - * - * @param list $symbols The symbol list to append to. - * @param string $symbol The symbol name. - * - * @return void - */ - protected function collect( array &$symbols, string $symbol ): void { - if ( 1 === \preg_match( self::SYMBOL_NAME_REGEX, $symbol ) ) { - $symbols[] = $symbol; - } - } - - /** - * Adds the string literal a function call passes at a position, when there is one. - * - * @param list $symbols The symbol list to append to. - * @param Node\Expr\FuncCall $node The function call. - * @param int $index The zero-based argument position. - * - * @return void - */ - protected function collect_string_argument( array &$symbols, Node\Expr\FuncCall $node, int $index ): void { - if ( - isset( $node->args[ $index ] ) - && $node->args[ $index ] instanceof Node\Arg - && $node->args[ $index ]->value instanceof Node\Scalar\String_ - ) { - $this->collect( $symbols, $node->args[ $index ]->value->value ); - } - } - - // endregion -} diff --git a/php/composer/Manifest.php b/php/composer/Manifest.php new file mode 100644 index 0000000..a835f83 --- /dev/null +++ b/php/composer/Manifest.php @@ -0,0 +1,72 @@ + + */ + public static function read( string $file ): array { + $contents = \is_file( $file ) ? \file_get_contents( $file ) : false; + if ( false === $contents ) { + throw new \RuntimeException( "Could not read '$file'." ); + } + + $decoded = \json_decode( $contents, true, flags: \JSON_THROW_ON_ERROR ); + return \is_array( $decoded ) ? $decoded : array(); + } + + /** + * Returns the extra entries a decoded manifest declares. + * + * @param array $manifest The decoded manifest. + * + * @return array + */ + public static function extra( array $manifest ): array { + return isset( $manifest['extra'] ) && \is_array( $manifest['extra'] ) ? $manifest['extra'] : array(); + } + + /** + * Returns the scoping prefix a project's manifest declares under extra.scoping-prefix, without leading or trailing backslashes. + * + * @param string $project_dir The root directory of the project. + * + * @throws \RuntimeException Thrown when the manifest cannot be read or declares no scoping prefix. + * + * @return string + */ + public static function scoping_prefix( string $project_dir ): string { + $prefix = self::extra( self::read( $project_dir . '/composer.json' ) )['scoping-prefix'] ?? null; + $prefix = \is_string( $prefix ) ? \trim( $prefix, '\\' ) : ''; + if ( '' === $prefix ) { + throw new \RuntimeException( "'$project_dir/composer.json' declares no extra.scoping-prefix." ); + } + + return $prefix; + } + + // endregion +} diff --git a/php/composer/ScopePhpDependencies.php b/php/composer/ScopePhpDependencies.php index 257a674..6bf5844 100644 --- a/php/composer/ScopePhpDependencies.php +++ b/php/composer/ScopePhpDependencies.php @@ -6,9 +6,10 @@ use Composer\Factory; use Composer\InstalledVersions; use Composer\Script\Event; -use DeepWebSolutions\Config\Composer\Internal\ScopedSymbolCollector; +use DeepWebSolutions\Config\Composer\Visitors\ScopedSymbolCollector; use Humbug\PhpScoper\Configuration\RegexChecker; use JetBrains\PHPStormStub\PhpStormStubsMap; +use PhpParser\Error; use PhpParser\NodeTraverser; use PhpParser\NodeVisitor\NameResolver; use PhpParser\NodeVisitor\ParentConnectingVisitor; @@ -20,6 +21,15 @@ * Scopes a project's dependencies into its vendor-prefixed directory with php-scoper and writes their autoloader. */ final class ScopePhpDependencies { + // region MAGIC METHODS + + /** + * Constructor. + */ + protected function __construct() {} + + // endregion + // region METHODS /** @@ -31,7 +41,7 @@ final class ScopePhpDependencies { * * @return void */ - public static function postAutoloadDump( Event $event ): void { + public static function postAutoloadDump( Event $event ): void { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid -- Consumer-facing Composer callback name. if ( $event->isDevMode() ) { self::scope( \dirname( Factory::getComposerFile() ) ); } @@ -40,24 +50,24 @@ public static function postAutoloadDump( Event $event ): void { /** * Scopes a project's dependencies into its vendor-prefixed directory with the project's scoper.inc.php, writes vendor-prefixed/scoper-autoload.php, then checks that the scoped code declares every prefixed name it references. * + * @internal + * * @param string $project_dir The root directory of the project. * - * @throws \RuntimeException Thrown when the project, its scoping prefix or php-scoper is missing, vendor-prefixed is a symbolic link, php-scoper fails, a prefixed name is declared by no scoped package, or a scoped package manifest cannot be read. + * @throws \RuntimeException Thrown when the project, its scoping prefix or php-scoper is missing, vendor-prefixed is a symbolic link, php-scoper fails, a scoped file does not parse, a scoped package declares a class outside the prefix, a prefixed name is declared by no scoped package, or a scoped package manifest cannot be read. * * @return void */ public static function scope( string $project_dir ): void { $project_dir = \realpath( $project_dir ) ?: throw new \RuntimeException( "The project directory '$project_dir' does not exist." ); - $prefix = self::extra( $project_dir . '/composer.json' )['scoping-prefix'] ?? null; - $prefix = \is_string( $prefix ) ? \trim( $prefix, '\\' ) : ''; - if ( '' === $prefix ) { - throw new \RuntimeException( "'$project_dir/composer.json' declares no extra.scoping-prefix." ); - } + $prefix = Manifest::scoping_prefix( $project_dir ); - $output_dir = $project_dir . '/vendor-prefixed'; + $output_dir = $project_dir . '/vendor-prefixed'; + $scoping_dir = $output_dir . '.tmp'; if ( \is_link( $output_dir ) ) { throw new \RuntimeException( "Refusing to replace '$output_dir', which is a symbolic link." ); } + if ( ! InstalledVersions::isInstalled( 'humbug/php-scoper' ) ) { throw new \RuntimeException( 'Install humbug/php-scoper to scope dependencies.' ); } @@ -68,211 +78,200 @@ public static function scope( string $project_dir ): void { } // Loading the config parses the declared stubs files, and large ones exceed PHP's default memory limit. - $command = array( \PHP_BINARY, '-d', 'memory_limit=1G', ( InstalledVersions::getInstallPath( 'humbug/php-scoper' ) ?? '' ) . '/bin/php-scoper', 'add-prefix', '--working-dir=' . $project_dir, '--config=' . $project_dir . '/scoper.inc.php', '--output-dir=' . $output_dir, '--force', '--no-interaction' ); + $command = array( + \PHP_BINARY, + '-d', + 'memory_limit=1G', + ( InstalledVersions::getInstallPath( 'humbug/php-scoper' ) ?? '' ) . '/bin/php-scoper', + 'add-prefix', + '--working-dir=' . $project_dir, + '--config=' . $project_dir . '/scoper.inc.php', + '--output-dir=' . $scoping_dir, + '--force', + '--no-interaction', + ); + // php-scoper's console wraps what it prints at the terminal width, which would split the paths and symbols in a failure message. $process = \proc_open( \implode( ' ', \array_map( 'escapeshellarg', $command ) ) . ' 2>&1', array( 1 => array( 'pipe', 'w' ) ), $pipes, null, array( 'COLUMNS' => '4096' ) + \getenv() ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_proc_open -- Runs in Composer, never on a WordPress server. if ( false === $process ) { throw new \RuntimeException( 'Could not start php-scoper.' ); } + $output = \stream_get_contents( $pipes[1] ); if ( 0 !== \proc_close( $process ) ) { + $filesystem->remove( $scoping_dir ); // php-scoper removes its output when it throws, but not when a fatal error or a signal ends it. throw new \RuntimeException( "php-scoper failed:\n" . $output ); } // php-scoper writes every file below the deepest directory its inputs share, which drops the vendor directory of a single-vendor closure. - $manifests = \glob( $output_dir . '/*/composer.json' ); + $manifests = \glob( $scoping_dir . '/*/composer.json' ); if ( false === $manifests ) { - throw new \RuntimeException( "Could not list '$output_dir'." ); + throw new \RuntimeException( "Could not list '$scoping_dir'." ); } + $dropped = match ( true ) { - \is_file( $output_dir . '/composer.json' ) => self::package_name( $output_dir . '/composer.json' ), - array() !== $manifests => \dirname( self::package_name( \current( $manifests ) ) ), - default => '', + \is_file( $scoping_dir . '/composer.json' ) => self::package_name( $scoping_dir . '/composer.json' ), + array() !== $manifests => \dirname( self::package_name( \current( $manifests ) ) ), + default => '', }; - if ( '' !== $dropped ) { - $filesystem->rename( $output_dir, $output_dir . '.nesting' ); - $filesystem->mkdir( \dirname( $output_dir . '/' . $dropped ) ); - $filesystem->rename( $output_dir . '.nesting', $output_dir . '/' . $dropped ); - } - - // The autoloader goes first, so a project whose scoped code fails the check still loads. - $filesystem->dumpFile( $output_dir . '/scoper-autoload.php', self::autoload( $output_dir ) ); - - // The functions the run leaves global are those the config excludes and those PHP declares, whose map Composer's script autoloader does not load. The config runs in a closure, so the variables it sets stay out of this method. - $config = ( static fn (): mixed => require $project_dir . '/scoper.inc.php' )(); - $stubs_path = InstalledVersions::getInstallPath( 'jetbrains/phpstorm-stubs' ) ?? ''; - require_once $stubs_path . '/PhpStormStubsMap.php'; - self::verify( $output_dir, $prefix, array( ...( \is_array( $config ) ? self::strings( $config['exclude-functions'] ?? array() ) : array() ), ...\array_keys( PhpStormStubsMap::FUNCTIONS ) ) ); - } - - // endregion - - // region HELPERS - - /** - * Fails when the scoped code references a prefixed name it does not declare, which would fail at runtime: a host symbol no stubs file declares, a dependency outside the scoped packages, or a string php-scoper took for a class name. - * - * A function_exists() check of the prefixed copy of a function the run leaves global passes, because php-scoper adds one to every function_exists() guard of that function, and it is false while nothing declares the copy. - * - * @param string $output_dir The vendor-prefixed directory. - * @param string $prefix The scoping prefix. - * @param list $global_functions The names and patterns of the functions the run leaves global. - * - * @throws \RuntimeException Thrown when a prefixed name is declared by no scoped package. - * - * @return void - */ - protected static function verify( string $output_dir, string $prefix, array $global_functions ): void { - $parser = new ParserFactory()->createForNewestSupportedVersion(); - $collector = new ScopedSymbolCollector( $prefix, $parser ); - foreach ( Finder::create()->files()->in( $output_dir )->name( '*.php' )->sortByName() as $file ) { - $collector->file = $file->getPathname(); - new NodeTraverser( new NameResolver(), new ParentConnectingVisitor(), $collector )->traverse( $parser->parse( $file->getContents() ) ?? array() ); - } - $namespaces = array(); - foreach ( $collector->declared as $name ) { - $namespace = ''; - foreach ( \array_slice( \explode( '\\', $name ), 0, -1 ) as $segment ) { - $namespace = \ltrim( "$namespace\\$segment", '\\' ); - $namespaces[ $namespace ] = $namespace; - } - } - - // php-scoper reads the entries its regex checker accepts as patterns, which it makes case-insensitive, and the rest as names it compares without case. - $regex_checker = new RegexChecker(); - $names = array(); - $patterns = array(); - foreach ( $global_functions as $function ) { - if ( $regex_checker->isRegexLike( $function ) ) { - $patterns[] = $function . 'i'; - } else { - $names[ \strtolower( \trim( $function, '\\ ' ) ) ] = $function; - } - } + // The build replaces the previous one only once php-scoper succeeds, so a failed run keeps the project loading. + $filesystem->remove( $output_dir ); + $filesystem->mkdir( \dirname( $output_dir . '/' . $dropped ) ); + $filesystem->rename( $scoping_dir, \rtrim( $output_dir . '/' . $dropped, '/' ) ); - $undeclared = array(); - foreach ( $collector->references as $reference ) { - $reference_name = \rtrim( $reference['name'], '\\' ); - $name = \strtolower( $reference_name ); - $function = \ltrim( \substr( $reference_name, \strlen( $prefix ) + 1 ), '\\' ); - $global = $reference['checked'] && ( isset( $names[ \strtolower( $function ) ] ) || \array_any( $patterns, static fn ( string $pattern ): bool => 1 === \preg_match( $pattern, $function ) ) ); - if ( ! $global && ! isset( $collector->declared[ $name ] ) && ! isset( $namespaces[ $name ] ) ) { - $undeclared[] = $reference['name'] . ' in ' . $reference['location']; - } - } - if ( array() !== $undeclared ) { - throw new \RuntimeException( "No scoped package declares these prefixed names, which would fail at runtime. Declare the host's stubs under extra.scoping-stubs, scope the package that declares the name, or exclude it in scoper.inc.php:\n" . \implode( "\n", \array_unique( $undeclared ) ) ); - } - } - - /** - * Returns the autoloader of the scoped packages: a class map over each package's psr-4, psr-0 and classmap paths, which serves PEAR-style psr-0 classes too, followed by its files entries. - * - * @param string $output_dir The vendor-prefixed directory. - * - * @throws \RuntimeException Thrown when no scoped package manifest exists or one cannot be read. - * - * @return string - */ - protected static function autoload( string $output_dir ): string { - $manifests = \glob( $output_dir . '/*/*/composer.json' ); - if ( false === $manifests || array() === $manifests ) { + // The autoloader goes first, so a project whose scoped code fails the check still loads. + $package_manifests = \glob( $output_dir . '/*/*/composer.json' ); + if ( false === $package_manifests || array() === $package_manifests ) { throw new \RuntimeException( "No scoped package exists in '$output_dir'." ); } $classmap = array(); $includes = array(); $requires = array(); - foreach ( $manifests as $manifest ) { + foreach ( $package_manifests as $manifest ) { $package_dir = \dirname( $manifest ); - $relative = \substr( $package_dir, \strlen( $output_dir ) ); - $decoded = self::manifest( $manifest ); + $decoded = Manifest::read( $manifest ); $autoload = isset( $decoded['autoload'] ) && \is_array( $decoded['autoload'] ) ? $decoded['autoload'] : array(); + // A class map over the psr-4, psr-0 and classmap paths serves PEAR-style psr-0 classes too. $paths = self::strings( $autoload['classmap'] ?? array() ); foreach ( array( 'psr-4', 'psr-0' ) as $standard ) { foreach ( (array) ( $autoload[ $standard ] ?? array() ) as $namespace_paths ) { \array_push( $paths, ...self::strings( $namespace_paths ) ); } } - $excluded = self::excluded( $package_dir, self::strings( $autoload['exclude-from-classmap'] ?? array() ) ); + + $excluded = null; + $excluded_paths = self::strings( $autoload['exclude-from-classmap'] ?? array() ); + if ( array() !== $excluded_paths ) { + // Composer's exclude-from-classmap paths take * and ** wildcards. + $excluded = '#^' . \preg_quote( \strtr( $package_dir, '\\', '/' ), '#' ) . '/(?:' . \implode( + '|', + \array_map( + static fn ( string $path ): string => \strtr( + \preg_quote( \trim( (string) \preg_replace( '#^(?:\./)+#', '', \strtr( $path, '\\', '/' ) ), '/' ), '#' ), + array( + '\\*\\*' => '.+?', + '\\*' => '[^/]+?', + ) + ), + $excluded_paths + ) + ) . ')(?:$|/)#'; + } + $generator = new ClassMapGenerator(); - foreach ( $paths as $path ) { - if ( \str_contains( $path, '*' ) || \file_exists( "$package_dir/$path" ) ) { // Composer skips an autoload path a package does not ship. - $generator->scanPaths( "$package_dir/$path", $excluded ); + foreach ( $paths as $autoload_path ) { + if ( \str_contains( $autoload_path, '*' ) || \file_exists( "$package_dir/$autoload_path" ) ) { // Composer skips an autoload path a package does not ship. + $generator->scanPaths( "$package_dir/$autoload_path", $excluded ); } } + foreach ( $generator->getClassMap()->getMap() as $class => $file ) { $classmap[ $class ] = \substr( $file, \strlen( $output_dir ) ); } - $name = self::package_name( $manifest ); - $requires[ $name ] = \array_map( 'strval', \array_keys( (array) ( $decoded['require'] ?? array() ) ) ); - $includes[ $name ] = ''; - foreach ( self::strings( $autoload['files'] ?? array() ) as $path ) { - $includes[ $name ] .= \sprintf( "require_once __DIR__ . %s;\n", \var_export( "$relative/$path", true ) ); + + $package_name = self::package_name( $manifest ); + $relative = \substr( $package_dir, \strlen( $output_dir ) ); + $requires[ $package_name ] = \array_map( 'strval', \array_keys( (array) ( $decoded['require'] ?? array() ) ) ); + $includes[ $package_name ] = ''; + foreach ( self::strings( $autoload['files'] ?? array() ) as $file_entry ) { + $includes[ $package_name ] .= \sprintf( "require_once __DIR__ . %s;\n", \var_export( "$relative/$file_entry", true ) ); } } // Composer includes a package's files after those of the packages it requires, which they may call when loaded. $files = ''; while ( array() !== $includes ) { - $ready = \array_filter( \array_keys( $includes ), static fn ( string $name ): bool => array() === \array_intersect( $requires[ $name ], \array_keys( $includes ) ) ); - $name = \current( $ready ) ?: \array_key_first( $includes ); // Packages that require each other keep their name order. - $files .= $includes[ $name ]; - unset( $includes[ $name ] ); + $ready = \array_filter( \array_keys( $includes ), static fn ( string $package_name ): bool => array() === \array_intersect( $requires[ $package_name ], \array_keys( $includes ) ) ); + $package_name = \current( $ready ) ?: \array_key_first( $includes ); // Packages that require each other keep their name order. + $files .= $includes[ $package_name ]; + unset( $includes[ $package_name ] ); } \ksort( $classmap ); $classes = ''; - foreach ( $classmap as $class => $file ) { - $classes .= \sprintf( "\t%s => __DIR__ . %s,\n", \var_export( $class, true ), \var_export( $file, true ) ); + foreach ( $classmap as $class_name => $class_file ) { + $classes .= \sprintf( "\t%s => __DIR__ . %s,\n", \var_export( $class_name, true ), \var_export( $class_file, true ) ); } - return "addClassMap( array(\n$classes) );\n\$loader->register();\n\n$files"; - } + $filesystem->dumpFile( $output_dir . '/scoper-autoload.php', "addClassMap( array(\n$classes) );\n\$loader->register();\n\n$files" ); - /** - * Returns the pattern of a package's exclude-from-classmap paths, with Composer's * and ** wildcards, or null when it excludes none. - * - * @param string $package_dir The directory of the scoped package. - * @param list $paths The paths, relative to the package. - * - * @return non-empty-string|null - */ - protected static function excluded( string $package_dir, array $paths ): ?string { - if ( array() === $paths ) { - return null; + // The functions the run leaves global are those the config excludes and those PHP declares, whose map Composer's script autoloader does not load. + $config = ( static fn (): mixed => require $project_dir . '/scoper.inc.php' )(); + $stubs_path = InstalledVersions::getInstallPath( 'jetbrains/phpstorm-stubs' ) ?? ''; + require_once $stubs_path . '/PhpStormStubsMap.php'; + $global_functions = array( ...( \is_array( $config ) ? self::strings( $config['exclude-functions'] ?? array() ) : array() ), ...\array_keys( PhpStormStubsMap::FUNCTIONS ) ); + + $parser = new ParserFactory()->createForNewestSupportedVersion(); + $collector = new ScopedSymbolCollector( $prefix, $parser ); + foreach ( Finder::create()->files()->in( $output_dir )->name( '*.php' )->sortByName() as $scoped_file ) { + $collector->file = $scoped_file->getPathname(); + try { + $statements = $parser->parse( $scoped_file->getContents() ) ?? array(); + } catch ( Error $error ) { + throw new \RuntimeException( "Could not parse the scoped file '" . $scoped_file->getPathname() . "': " . $error->getMessage(), 0, $error ); + } + + new NodeTraverser( new NameResolver(), new ParentConnectingVisitor(), $collector )->traverse( $statements ); } - $patterns = \array_map( - static fn ( string $path ): string => \strtr( - \preg_quote( \trim( (string) \preg_replace( '#^(?:\./)+#', '', \strtr( $path, '\\', '/' ) ), '/' ), '#' ), - array( - '\\*\\*' => '.+?', - '\\*' => '[^/]+?', - ) - ), - $paths - ); - return '#^' . \preg_quote( \strtr( $package_dir, '\\', '/' ), '#' ) . '/(?:' . \implode( '|', $patterns ) . ')(?:$|/)#'; - } + // A class outside the prefix collides with the copy the host or another plugin loads, but for the polyfill of a PHP class, which php-scoper aliases to its global name. + $php_classes = \array_change_key_case( PhpStormStubsMap::CLASSES ); + $outside = array(); + foreach ( $collector->declared['class'] as $key => $class ) { + if ( 0 !== \stripos( $class, $prefix . '\\' ) && ! isset( $php_classes[ $key ] ) ) { + $separator = \strrpos( $class, '\\' ); + $outside[] = false === $separator ? 'the global namespace' : \substr( $class, 0, $separator ); + } + } - /** - * Returns the extra entries a manifest declares. - * - * @param string $file The path of the manifest. - * - * @throws \RuntimeException Thrown when the manifest cannot be read. - * - * @return array - */ - protected static function extra( string $file ): array { - $extra = self::manifest( $file )['extra'] ?? array(); + if ( array() !== $outside ) { + throw new \RuntimeException( "The scoped packages declare classes in these namespaces outside the prefix, where the copy the host or another plugin loads can replace them. Drop the package that declares one with !vendor/package, or stop excluding the namespace in scoper.inc.php:\n" . \implode( "\n", \array_unique( $outside ) ) ); + } + + $namespaces = array(); + foreach ( \array_merge( ...\array_values( $collector->declared ) ) as $name ) { + $namespace = ''; + foreach ( \array_slice( \explode( '\\', $name ), 0, -1 ) as $segment ) { + $namespace = \ltrim( "$namespace\\$segment", '\\' ); + $namespaces[ \strtolower( $namespace ) ] = $namespace; + } + } - return \is_array( $extra ) ? $extra : array(); + // php-scoper reads the entries its regex checker accepts as patterns, which it makes case-insensitive, and the rest as names it compares without case. + $regex_checker = new RegexChecker(); + $names = array(); + $patterns = array(); + foreach ( $global_functions as $function ) { + if ( $regex_checker->isRegexLike( $function ) ) { + $patterns[] = $function . 'i'; + } else { + $names[ \strtolower( \trim( $function, '\\ ' ) ) ] = $function; + } + } + + // A function_exists() check of the prefixed copy of a function the run leaves global passes, because php-scoper adds one to every function_exists() guard of that function, and it is false while nothing declares the copy. + $undeclared = array(); + foreach ( $collector->references as $reference ) { + $function = \ltrim( \substr( \rtrim( $reference['name'], '\\' ), \strlen( $prefix ) + 1 ), '\\' ); + $global = $reference['checked'] && ( isset( $names[ \strtolower( $function ) ] ) || \array_any( $patterns, static fn ( string $pattern ): bool => 1 === \preg_match( $pattern, $function ) ) ); + $declared = \array_any( $reference['keys'], static fn ( string $key, string $kind ): bool => 'namespace' === $kind ? isset( $namespaces[ $key ] ) : isset( $collector->declared[ $kind ][ $key ] ) ); + if ( ! $global && ! $declared ) { + $undeclared[] = $reference['name'] . ' in ' . $reference['location']; + } + } + + if ( array() !== $undeclared ) { + throw new \RuntimeException( "No scoped package declares these prefixed names, which would fail at runtime. Declare the host's stubs under extra.scoping-stubs, scope the package that declares the name, or exclude it in scoper.inc.php:\n" . \implode( "\n", \array_unique( $undeclared ) ) ); + } } + // endregion + + // region HELPERS + /** * Returns the name a scoped package manifest declares. * @@ -283,7 +282,7 @@ protected static function extra( string $file ): array { * @return string */ protected static function package_name( string $file ): string { - $name = self::manifest( $file )['name'] ?? null; + $name = Manifest::read( $file )['name'] ?? null; if ( ! \is_string( $name ) || ! \str_contains( $name, '/' ) ) { throw new \RuntimeException( "'$file' declares no package name." ); } @@ -291,25 +290,6 @@ protected static function package_name( string $file ): string { return $name; } - /** - * Returns the decoded contents of a package manifest. - * - * @param string $file The path of the manifest. - * - * @throws \RuntimeException Thrown when the manifest cannot be read. - * - * @return array - */ - protected static function manifest( string $file ): array { - $contents = \file_get_contents( $file ); - if ( false === $contents ) { - throw new \RuntimeException( "Could not read '$file'." ); - } - $decoded = \json_decode( $contents, true, flags: \JSON_THROW_ON_ERROR ); - - return \is_array( $decoded ) ? $decoded : array(); - } - /** * Returns the strings of a value that holds one string or a list of them. * diff --git a/php/composer/ScopedPackages.php b/php/composer/ScopedPackages.php index dbb7305..1eafb91 100644 --- a/php/composer/ScopedPackages.php +++ b/php/composer/ScopedPackages.php @@ -4,8 +4,19 @@ /** * Resolves the packages a project scopes from the names it gives and from Composer's record of the installed packages. + * + * @internal */ final class ScopedPackages { + // region MAGIC METHODS + + /** + * Constructor. + */ + protected function __construct() {} + + // endregion + // region METHODS /** @@ -16,7 +27,7 @@ final class ScopedPackages { * @param string $vendor_dir The project's Composer vendor directory. * @param list $names The packages to scope, as names or fnmatch() patterns, where a name that starts with ! drops the packages it matches. * - * @throws \RuntimeException Thrown when no name is given, installed.json is unreadable, a name matches no installed package, a dependency is not installed, or a scoped package is a symbolic link. + * @throws \RuntimeException Thrown when no name is given, installed.json or installed.php is unreadable, a name matches no installed package, a dependency is not installed, or a scoped package is a symbolic link. * * @return array{scoped: array, dropped: array} */ @@ -25,18 +36,34 @@ public static function resolve( string $vendor_dir, array $names ): array { throw new \RuntimeException( 'Name at least one package to scope.' ); } - $installed = self::installed( $vendor_dir ); + $installed_file = $vendor_dir . '/composer/installed.json'; + if ( ! \is_file( $installed_file ) ) { + throw new \RuntimeException( "Could not read '$installed_file'. Install the project's dependencies first." ); + } + + $recorded = Manifest::read( $installed_file ); + if ( ! \is_array( $recorded['packages'] ?? null ) ) { + throw new \RuntimeException( "'$installed_file' lists no packages." ); + } + + $installed = array(); $replacers = array(); - $provided = array(); - foreach ( $installed as $name => $package ) { - foreach ( \array_keys( (array) ( $package['replace'] ?? array() ) ) as $replaced ) { - $replacers[ (string) $replaced ][] = $name; - } - foreach ( \array_keys( (array) ( $package['provide'] ?? array() ) ) as $virtual ) { - $provided[ (string) $virtual ] = true; + foreach ( $recorded['packages'] as $package ) { + if ( \is_array( $package ) && \is_string( $package['name'] ?? null ) ) { + $installed[ $package['name'] ] = $package; + foreach ( \array_keys( (array) ( $package['replace'] ?? array() ) ) as $replaced ) { + $replacers[ (string) $replaced ][] = $package['name']; + } } } + // Composer records every name a package or the root replaces or provides, so a requirement no installed package fills is met when it is recorded there. + $versions_file = $vendor_dir . '/composer/installed.php'; + $versions = \is_file( $versions_file ) ? ( static fn (): mixed => require $versions_file )() : null; + if ( ! \is_array( $versions ) || ! \is_array( $versions['versions'] ?? null ) ) { + throw new \RuntimeException( "Could not read '$versions_file'. Install the project's dependencies first." ); + } + $included = array(); $dropped = array(); foreach ( $names as $name ) { @@ -45,6 +72,7 @@ public static function resolve( string $vendor_dir, array $names ): array { if ( array() === $matches ) { throw new \RuntimeException( "'$pattern' matches no package installed in '$vendor_dir'." ); } + if ( \str_starts_with( $name, '!' ) ) { \array_push( $dropped, ...$matches ); } else { @@ -60,6 +88,7 @@ public static function resolve( string $vendor_dir, array $names ): array { if ( isset( $scoped[ $name ] ) || 'composer-plugin' === ( $package['type'] ?? '' ) ) { // Composer plugins run at build time and never ship. continue; } + $scoped[ $name ] = $package; foreach ( \array_keys( (array) ( $package['require'] ?? array() ) ) as $required ) { @@ -67,14 +96,16 @@ public static function resolve( string $vendor_dir, array $names ): array { if ( ! \str_contains( $required, '/' ) || \in_array( $required, $dropped, true ) ) { // Platform requirements such as php and ext-json carry no vendor. continue; } + $candidates = isset( $installed[ $required ] ) ? array( $required ) : \array_diff( $replacers[ $required ] ?? array(), $dropped ); - // A requirement another package only provides is a virtual package, such as an interface implementation the project picks itself. - if ( array() === $candidates && ! isset( $provided[ $required ] ) && ! isset( $replacers[ $required ] ) ) { + if ( array() === $candidates && ! isset( $versions['versions'][ $required ] ) ) { throw new \RuntimeException( "'$name' requires '$required', which is not installed in '$vendor_dir'." ); } + \array_push( $queue, ...$candidates ); } } + \ksort( $scoped ); $directories = array( @@ -86,6 +117,7 @@ public static function resolve( string $vendor_dir, array $names ): array { $directories['scoped'][ $name ] = self::directory( $vendor_dir, $name, $package, true ); } } + foreach ( \array_unique( $dropped ) as $name ) { if ( 'metapackage' !== ( $installed[ $name ]['type'] ?? '' ) ) { $directories['dropped'][ $name ] = self::directory( $vendor_dir, $name, $installed[ $name ], false ); @@ -99,36 +131,6 @@ public static function resolve( string $vendor_dir, array $names ): array { // region HELPERS - /** - * Returns the packages Composer recorded in the vendor directory's installed.json, keyed by name. - * - * @param string $vendor_dir The project's Composer vendor directory. - * - * @throws \RuntimeException Thrown when installed.json is missing or lists no packages. - * - * @return array> - */ - protected static function installed( string $vendor_dir ): array { - $file = $vendor_dir . '/composer/installed.json'; - $contents = \is_file( $file ) ? \file_get_contents( $file ) : false; - if ( false === $contents ) { - throw new \RuntimeException( "Could not read '$file'. Install the project's dependencies first." ); - } - $decoded = \json_decode( $contents, true, flags: \JSON_THROW_ON_ERROR ); - if ( ! \is_array( $decoded ) || ! \is_array( $decoded['packages'] ?? null ) ) { - throw new \RuntimeException( "'$file' lists no packages." ); - } - - $installed = array(); - foreach ( $decoded['packages'] as $package ) { - if ( \is_array( $package ) && \is_string( $package['name'] ?? null ) ) { - $installed[ $package['name'] ] = $package; - } - } - - return $installed; - } - /** * Returns the real directory a package is installed in. * diff --git a/php/composer/Visitors/ScopedSymbolCollector.php b/php/composer/Visitors/ScopedSymbolCollector.php new file mode 100644 index 0000000..38d22cd --- /dev/null +++ b/php/composer/Visitors/ScopedSymbolCollector.php @@ -0,0 +1,193 @@ +, function: array, constant: array} + */ + protected(set) array $declared = array( + 'class' => array(), + 'function' => array(), + 'constant' => array(), + ); + + /** + * The prefixed names the code references, each with its location, its lookup key for each kind of symbol it can name, and whether a function_exists() call checks it. + * + * @var list, checked: bool}> + */ + protected(set) array $references = array(); + + /** + * The file the next traversal reads. + * + * @var string + */ + public string $file = ''; + + // endregion + + // region MAGIC METHODS + + /** + * Constructor. + * + * @param string $prefix The scoping prefix, without leading or trailing backslashes. + * @param Parser $parser The parser that reads the PHP code php-scoper scopes inside nowdoc strings. + */ + public function __construct( + protected readonly string $prefix, + protected readonly Parser $parser + ) {} + + // endregion + + // region INHERITED METHODS + + /** + * {@inheritDoc} + */ + #[\Override] + public function enterNode( Node $node ): int|null { + $parent = $node->getAttribute( 'parent' ); + if ( $node instanceof Node\Stmt\ClassLike ) { + $this->declare( 'class', $node->namespacedName?->toString() ); + } elseif ( $node instanceof Node\Stmt\Function_ ) { + $this->declare( 'function', $node->namespacedName?->toString() ); + } elseif ( $node instanceof Node\Stmt\Const_ ) { + foreach ( $node->consts as $const ) { + $this->declare( 'constant', $const->namespacedName?->toString() ); + } + } elseif ( $node instanceof Node\Scalar\String_ ) { + // php-scoper scopes the PHP code of a nowdoc that opens with a PHP tag like a file, and a string that names a symbol or a class constant like a reference. + $code = \ltrim( $node->value ); + if ( Node\Scalar\String_::KIND_NOWDOC === $node->getAttribute( 'kind' ) && \str_starts_with( $code, 'parser->parse( $code ) ?? array(); + } catch ( Error ) { + return null; // php-scoper leaves a nowdoc that does not parse as PHP unscoped. + } + + // php-scoper parses the code as written, so behind leading whitespace it scopes only code without a namespace, which it puts in the prefix namespace. + $first = $statements[0] ?? null; + if ( $code !== $node->value && ! ( $first instanceof Node\Stmt\Namespace_ && $this->prefix === $first->name?->toString() ) ) { + return null; + } + + new NodeTraverser( new NameResolver(), new ParentConnectingVisitor(), $this )->traverse( $statements ); + } else { + $parts = \explode( '::', $node->value, 2 ); + if ( 1 === \preg_match( StubSymbolCollector::SYMBOL_NAME_REGEX, $parts[0] ) ) { + $this->reference( \ltrim( $parts[0], '\\' ), $node, isset( $parts[1] ) ? array( 'class' ) : array( 'class', 'function', 'constant', 'namespace' ) ); + } + } + } elseif ( $node instanceof Node\Name && ! $parent instanceof Node\Stmt\Namespace_ ) { + $use = $parent instanceof Node\UseItem ? $parent->getAttribute( 'parent' ) : null; + $type = $parent instanceof Node\UseItem && ( $use instanceof Node\Stmt\Use_ || $use instanceof Node\Stmt\GroupUse ) ? ( $parent->type ?: $use->type ) : null; + $kinds = match ( true ) { + $parent instanceof Node\Expr\FuncCall, Node\Stmt\Use_::TYPE_FUNCTION === $type => array( 'function' ), + $parent instanceof Node\Expr\ConstFetch, Node\Stmt\Use_::TYPE_CONSTANT === $type => array( 'constant' ), + $parent instanceof Node\Stmt\GroupUse => array( 'namespace' ), + // An import can name a namespace, which the code then extends. + $parent instanceof Node\UseItem => array( 'class', 'namespace' ), + default => array( 'class' ), + }; + $this->reference( $use instanceof Node\Stmt\GroupUse ? $use->prefix->toString() . '\\' . $node->toString() : $node->toString(), $node, $kinds ); + } + + return null; + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function leaveNode( Node $node ): null { + // The names in a call's arguments are resolved only once the traversal has visited them. + $declaration = $node instanceof Node\Expr\FuncCall ? StubSymbolCollector::declared_by_call( $node ) : null; + if ( ! \is_null( $declaration ) ) { + $this->declare( $declaration[0], $declaration[1] ); + } + + return null; + } + + // endregion + + // region HELPERS + + /** + * Records a declared symbol. + * + * @param 'class'|'function'|'constant' $kind The kind of symbol. + * @param string|null $name The name of the symbol, or null for an anonymous class. + * + * @return void + */ + protected function declare( string $kind, ?string $name ): void { + if ( ! \is_null( $name ) ) { + $this->declared[ $kind ][ self::key( $kind, $name ) ] = $name; + } + } + + /** + * Records a name that carries the prefix, with its location, its lookup key for each kind of symbol it can name, and whether a function_exists() call checks it. + * + * @param string $name The name. + * @param Node $node The node that references it. + * @param non-empty-list<'class'|'function'|'constant'|'namespace'> $kinds The kinds of symbol the name can stand for where it appears. + * + * @return void + */ + protected function reference( string $name, Node $node, array $kinds ): void { + if ( 0 === \stripos( $name, $this->prefix . '\\' ) ) { + $call = $node->getAttribute( 'parent' ) instanceof Node\Arg ? $node->getAttribute( 'parent' )->getAttribute( 'parent' ) : null; + $this->references[] = array( + 'name' => $name, + 'location' => $this->file . ':' . $node->getStartLine(), + 'keys' => \array_combine( $kinds, \array_map( static fn ( string $kind ): string => self::key( $kind, $name ), $kinds ) ), + 'checked' => $call instanceof Node\Expr\FuncCall && $call->name instanceof Node\Name && 'function_exists' === $call->name->toString(), + ); + } + } + + /** + * Returns the key a symbol of a kind is looked up by: its lowercase name, but a constant keeps the case of its name below a lowercase namespace, as PHP compares them. + * + * @param string $kind The kind of symbol: class, function, constant or namespace. + * @param string $name The name of the symbol. + * + * @return string + */ + protected static function key( string $kind, string $name ): string { + $name = \trim( $name, '\\' ); + $separator = \strrpos( $name, '\\' ); + if ( 'constant' !== $kind ) { + return \strtolower( $name ); + } + + return false === $separator ? $name : \strtolower( \substr( $name, 0, $separator ) ) . \substr( $name, $separator ); + } + + // endregion +} diff --git a/php/composer/Visitors/StubSymbolCollector.php b/php/composer/Visitors/StubSymbolCollector.php new file mode 100644 index 0000000..816473d --- /dev/null +++ b/php/composer/Visitors/StubSymbolCollector.php @@ -0,0 +1,147 @@ + + */ + protected(set) array $classes = array(); + + /** + * The function names the stubs declare. + * + * @var list + */ + protected(set) array $functions = array(); + + /** + * The constant names the stubs declare, through const statements and define() calls. + * + * @var list + */ + protected(set) array $constants = array(); + + // endregion + + // region INHERITED METHODS + + /** + * {@inheritDoc} + */ + #[\Override] + public function enterNode( Node $node ): int|null { + switch ( \get_class( $node ) ) { + case Node\Stmt\Class_::class: + case Node\Stmt\Interface_::class: + case Node\Stmt\Trait_::class: + case Node\Stmt\Enum_::class: + if ( ! \is_null( $node->namespacedName ) ) { + $this->collect( $this->classes, $node->namespacedName->toString() ); + } + return NodeVisitor::DONT_TRAVERSE_CHILDREN; + case Node\Stmt\Function_::class: + if ( ! \is_null( $node->namespacedName ) ) { + $this->collect( $this->functions, $node->namespacedName->toString() ); + } + break; + case Node\Stmt\Const_::class: + foreach ( $node->consts as $const ) { + if ( ! \is_null( $const->namespacedName ) ) { + $this->collect( $this->constants, $const->namespacedName->toString() ); + } + } + break; + } + + return null; + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function leaveNode( Node $node ): null { + // A define() name and a class_alias() alias are real symbols, so scoped code that references them must keep them unprefixed. + $declaration = $node instanceof Node\Expr\FuncCall ? self::declared_by_call( $node ) : null; + if ( 'class' === ( $declaration[0] ?? null ) ) { + $this->collect( $this->classes, $declaration[1] ); + } elseif ( 'constant' === ( $declaration[0] ?? null ) ) { + $this->collect( $this->constants, $declaration[1] ); + } + + return null; + } + + // endregion + + // region METHODS + + /** + * Returns the kind and name of the symbol a define() or class_alias() call declares, or null when the call declares none by a string literal or a ::class constant. + * + * The call's arguments must have been visited, since a NameResolver resolves a ::class constant's class only when the traversal reaches it. + * + * @param Node\Expr\FuncCall $node The function call. + * + * @return array{'class'|'constant', string}|null + */ + public static function declared_by_call( Node\Expr\FuncCall $node ): ?array { + $function = $node->name instanceof Node\Name ? $node->name->toLowerString() : ''; + $argument = match ( $function ) { + 'define' => $node->args[0] ?? null, + 'class_alias' => $node->args[1] ?? null, + default => null, + }; + $value = $argument instanceof Node\Arg ? $argument->value : null; + $name = match ( true ) { + $value instanceof Node\Scalar\String_ => \ltrim( $value->value, '\\' ), + $value instanceof Node\Expr\ClassConstFetch && $value->class instanceof Node\Name && $value->name instanceof Node\Identifier && 'class' === $value->name->toLowerString() => $value->class->toString(), + default => null, + }; + + return \is_null( $name ) ? null : array( 'define' === $function ? 'constant' : 'class', $name ); + } + + // endregion + + // region HELPERS + + /** + * Adds a symbol when it has the name shape php-scoper's exclusion lists accept. + * + * @param list $symbols The symbol list to append to. + * @param string $symbol The symbol name. + * + * @return void + */ + protected function collect( array &$symbols, string $symbol ): void { + if ( 1 === \preg_match( self::SYMBOL_NAME_REGEX, $symbol ) ) { + $symbols[] = $symbol; + } + } + + // endregion +} diff --git a/php/php-scoper/scoper-base.inc.php b/php/php-scoper/scoper-base.inc.php index 231f456..de90421 100644 --- a/php/php-scoper/scoper-base.inc.php +++ b/php/php-scoper/scoper-base.inc.php @@ -5,7 +5,8 @@ * @package DeepWebSolutions\Config */ -use DeepWebSolutions\Config\Composer\CollectScopingStubs; +use DeepWebSolutions\Config\Composer\GlobalSymbols; +use DeepWebSolutions\Config\Composer\Manifest; use DeepWebSolutions\Config\Composer\ScopedPackages; use Symfony\Component\Finder\Finder; @@ -20,18 +21,8 @@ * @return array */ return static function ( string $project_dir, string ...$names ): array { - $manifest = static function ( string $file ): array { - $decoded = json_decode( (string) file_get_contents( $file ), true, flags: JSON_THROW_ON_ERROR ); - - return is_array( $decoded ) ? $decoded : array(); - }; - $extra = static fn ( array $decoded ): array => isset( $decoded['extra'] ) && is_array( $decoded['extra'] ) ? $decoded['extra'] : array(); - - $project = $manifest( "$project_dir/composer.json" ); - $prefix = $extra( $project )['scoping-prefix'] ?? null; - if ( ! is_string( $prefix ) || '' === trim( $prefix, '\\' ) ) { - throw new RuntimeException( "'$project_dir/composer.json' declares no extra.scoping-prefix." ); - } + $project = Manifest::read( "$project_dir/composer.json" ); + $prefix = Manifest::scoping_prefix( $project_dir ); $vendor = is_array( $project['config'] ?? null ) && is_string( $project['config']['vendor-dir'] ?? null ) ? $project['config']['vendor-dir'] : 'vendor'; $vendor_dir = str_starts_with( $vendor, '/' ) ? $vendor : "$project_dir/$vendor"; @@ -39,7 +30,8 @@ if ( array() === $packages['scoped'] ) { throw new RuntimeException( "No package to scope is installed in '$vendor_dir'." ); // Without finders, php-scoper scopes the whole working directory. } - $symbols = CollectScopingStubs::collect( $project_dir, array_values( $packages['scoped'] ), array_values( $packages['dropped'] ) ); + + $symbols = GlobalSymbols::collect( $project_dir, array_values( $packages['scoped'] ), array_values( $packages['dropped'] ) ); $exclude_files = array(); $exclude_namespaces = array(); @@ -47,12 +39,13 @@ $roots = array(); $domains = array(); foreach ( $packages['scoped'] as $name => $dir ) { - $package = $manifest( "$dir/composer.json" ); - $domain = $extra( $package )['scoping-text-domain'] ?? null; - $autoload = isset( $package['autoload'] ) && is_array( $package['autoload'] ) ? $package['autoload'] : array(); + $package = Manifest::read( "$dir/composer.json" ); + $domain = Manifest::extra( $package )['scoping-text-domain'] ?? null; if ( ! is_null( $domain ) ) { $domains[ $dir ] = is_string( $domain ) && '' !== $domain ? $domain : throw new RuntimeException( "'$dir/composer.json' declares an empty extra.scoping-text-domain." ); } + + $autoload = isset( $package['autoload'] ) && is_array( $package['autoload'] ) ? $package['autoload'] : array(); foreach ( array( 'psr-4', 'psr-0' ) as $standard ) { foreach ( array_keys( (array) ( $autoload[ $standard ] ?? array() ) ) as $namespace ) { if ( str_contains( (string) $namespace, '\\' ) ) { @@ -65,6 +58,7 @@ if ( ! is_file( $recipe_file ) ) { continue; } + /** * A recipe returns php-scoper settings for one package, with paths relative to it. * @@ -77,20 +71,25 @@ throw new RuntimeException( "The '$name' recipe targets '$path', which the installed package no longer ships." ); } } + foreach ( $recipe['exclude-files'] ?? array() as $excluded_file ) { $exclude_files[] = "$dir/$excluded_file"; } + array_push( $exclude_namespaces, ...( $recipe['exclude-namespaces'] ?? array() ) ); + foreach ( $recipe['replacements'] ?? array() as $target => $replacements ) { $patchers[] = static function ( string $file_path, string $prefix, string $contents ) use ( $name, $dir, $target, $replacements ): string { if ( "$dir/$target" !== $file_path ) { return $contents; } + foreach ( $replacements as $search => $replace ) { $search = str_replace( '{prefix}', $prefix, (string) $search ); if ( ! str_contains( $contents, $search ) ) { throw new RuntimeException( "The '$name' recipe expects '$search' in '$target', which the installed package no longer contains." ); } + $contents = str_replace( $search, str_replace( '{prefix}', $prefix, $replace ), $contents ); } @@ -99,19 +98,23 @@ } } - $text_domain = $extra( $project )['text-domain'] ?? null; + $text_domain = Manifest::extra( $project )['text-domain'] ?? null; if ( array() !== $domains && ( ! is_string( $text_domain ) || '' === $text_domain ) ) { throw new RuntimeException( "'$project_dir/composer.json' declares no extra.text-domain, which the strings of the scoped packages take." ); } + $domains = array_filter( $domains, static fn ( string $domain ): bool => $text_domain !== $domain ); + $exclude_namespaces = array( ...$exclude_namespaces, ...$symbols['namespaces'] ); + // Libraries build class names in strings php-scoper cannot see, and a string that starts with a scoped package's own namespace names one of its classes. if ( array() !== $roots ) { $separated = static fn ( string $name ): string => implode( '\\\\{1,2}', array_map( static fn ( string $segment ): string => preg_quote( $segment, '/' ), explode( '\\', $name ) ) ); $own = '/^(\\\\{0,2})((?:' . implode( '|', array_map( $separated, array_unique( $roots ) ) ) . ')(\\\\{1,2}))/'; - $patchers[] = static function ( string $file_path, string $prefix, string $contents ) use ( $own, $separated ): string { + $patchers[] = static function ( string $file_path, string $prefix, string $contents ) use ( $own, $separated, $exclude_namespaces ): string { if ( ! str_ends_with( $file_path, '.php' ) ) { return $contents; } + $scoped = '/^\\\\{0,2}' . $separated( $prefix ) . '\\\\{1,2}/'; $tokens = PhpToken::tokenize( $contents ); foreach ( $tokens as $index => $token ) { @@ -119,31 +122,50 @@ if ( ! $starts_string ) { continue; } + $opening = $token->is( T_CONSTANT_ENCAPSED_STRING ) ? strcspn( $token->text, '\'"' ) + 1 : 0; $body = substr( $token->text, $opening ); - if ( 1 !== preg_match( $scoped, $body ) && 1 === preg_match( $own, $body, $match ) ) { - // Double-quoted strings and heredocs read escape sequences, so a prefix segment such as Vendor\name takes doubled backslashes there. - $escapes = $token->is( T_CONSTANT_ENCAPSED_STRING ) ? str_contains( substr( $token->text, 0, $opening ), '"' ) : ! str_contains( $tokens[ $index - 1 ]->text, "'" ); - $separator = $escapes ? '\\\\' : $match[3]; - $token->text = substr( $token->text, 0, $opening ) . $match[1] . str_replace( '\\', $separator, $prefix ) . $separator . substr( $body, strlen( $match[1] ) ); + if ( 1 === preg_match( $scoped, $body ) || 1 !== preg_match( $own, $body, $match ) ) { + continue; } + + // A string that names a namespace php-scoper keeps global, or a symbol in one, names the host's symbol, and a name the string follows with a separator is a namespace. + $named = 1 === preg_match( '/^\\\\{0,2}((?:[\w\x80-\xff]+\\\\{1,2})*[\w\x80-\xff]+)(\\\\?)/', $body, $name_match ) ? (string) preg_replace( '/\\\\{2}/', '\\', $name_match[1] ) : ''; + $namespace = '' === ( $name_match[2] ?? '' ) ? substr( $named, 0, (int) strrpos( $named, '\\' ) ) : $named; + if ( array_any( $exclude_namespaces, static fn ( string $pattern ): bool => 1 === preg_match( $pattern, $named ) || 1 === preg_match( $pattern, $namespace ) ) ) { + continue; + } + + // Double-quoted strings and heredocs read escape sequences, so a prefix segment such as Vendor\name takes doubled backslashes there. + $escapes = $token->is( T_CONSTANT_ENCAPSED_STRING ) ? str_contains( substr( $token->text, 0, $opening ), '"' ) : ! str_contains( $tokens[ $index - 1 ]->text, "'" ); + $separator = $escapes ? '\\\\' : $match[3]; + $token->text = substr( $token->text, 0, $opening ) . $match[1] . str_replace( '\\', $separator, $prefix ) . $separator . substr( $body, strlen( $match[1] ) ); } - return implode( '', array_map( static fn ( PhpToken $token ): string => $token->text, $tokens ) ); + return implode( '', array_column( $tokens, 'text' ) ); }; } if ( array() !== $domains ) { - $patchers[] = static function ( string $file_path, string $prefix, string $contents ) use ( $domains, $text_domain ): string { - foreach ( $domains as $dir => $domain ) { - if ( ! str_starts_with( $file_path, "$dir/" ) || ! str_ends_with( $file_path, '.php' ) ) { - continue; + $rewrite = static function ( string $code, string $domain, string $file_path ) use ( &$rewrite, $text_domain ): string { + $tokens = PhpToken::tokenize( $code ); + foreach ( $tokens as $index => $token ) { + if ( $token->is( T_CONSTANT_ENCAPSED_STRING ) && in_array( $token->text, array( "'$domain'", "\"$domain\"" ), true ) ) { + $token->text = var_export( $text_domain, true ); + } elseif ( $token->is( T_ENCAPSED_AND_WHITESPACE ) && $index > 0 && $tokens[ $index - 1 ]->is( T_START_HEREDOC ) && str_contains( $tokens[ $index - 1 ]->text, "'" ) && str_starts_with( ltrim( $token->text ), 'text = $rewrite( $token->text, $domain, $file_path ); + } elseif ( ! $token->is( array( T_COMMENT, T_DOC_COMMENT, T_INLINE_HTML ) ) && 1 === preg_match( '/(?text ) ) { + throw new RuntimeException( "'$file_path' still contains the text domain '$domain' after scoping, outside a string literal of its own." ); } - $contents = str_replace( array( "'$domain'", "\"$domain\"" ), var_export( $text_domain, true ), $contents ); - foreach ( PhpToken::tokenize( $contents ) as $token ) { - if ( ! $token->is( array( T_COMMENT, T_DOC_COMMENT, T_INLINE_HTML ) ) && 1 === preg_match( '/(?text ) ) { - throw new RuntimeException( "'$file_path' still contains the text domain '$domain' after scoping, outside a string literal of its own." ); - } + } + + return implode( '', array_column( $tokens, 'text' ) ); + }; + $patchers[] = static function ( string $file_path, string $prefix, string $contents ) use ( $domains, $rewrite ): string { + foreach ( $domains as $dir => $domain ) { + if ( str_starts_with( $file_path, "$dir/" ) && str_ends_with( $file_path, '.php' ) ) { + $contents = $rewrite( $contents, $domain, $file_path ); } } @@ -152,11 +174,11 @@ } return array( - 'prefix' => trim( $prefix, '\\' ), + 'prefix' => $prefix, // A package's tests run against development tools that are never scoped, so they would only ship dead, unresolvable code. 'finders' => array( Finder::create()->files()->in( array_values( $packages['scoped'] ) )->exclude( array( 'tests', 'Tests' ) ) ), 'exclude-files' => $exclude_files, - 'exclude-namespaces' => array( ...$exclude_namespaces, ...$symbols['namespaces'] ), + 'exclude-namespaces' => $exclude_namespaces, 'exclude-classes' => $symbols['classes'], 'exclude-functions' => $symbols['functions'], 'exclude-constants' => $symbols['constants'], diff --git a/php/phpstan/GlobalTagVisitor.php b/php/phpstan/GlobalTagVisitor.php index aa77faf..0214958 100644 --- a/php/phpstan/GlobalTagVisitor.php +++ b/php/phpstan/GlobalTagVisitor.php @@ -13,6 +13,8 @@ * Gives each global statement the types that the @global tags of its function's or method's docblock declare, as WordPress's documentation standard writes them. * * PHPStan types a global as mixed but reads an inline @var on the statement, so the visitor attaches one; a global no tag declares stays mixed, and a statement with its own docblock keeps it. + * + * @internal */ final class GlobalTagVisitor extends NodeVisitorAbstract { // region FIELDS AND CONSTANTS @@ -35,8 +37,8 @@ final class GlobalTagVisitor extends NodeVisitorAbstract { * @param PhpDocParser $parser PHPStan's PHPDoc parser. */ public function __construct( - protected Lexer $lexer, - protected PhpDocParser $parser + protected readonly Lexer $lexer, + protected readonly PhpDocParser $parser ) {} // endregion @@ -60,7 +62,7 @@ public function enterNode( Node $node ): null { if ( $node instanceof Node\FunctionLike ) { $types = array(); $doc = $node->getDocComment()?->getText(); - if ( null !== $doc ) { + if ( ! \is_null( $doc ) ) { // The parser reads @global as an unknown tag, so each one is parsed with @var's grammar of a type and a variable. $tokens = new TokenIterator( $this->lexer->tokenize( (string) \preg_replace( '/@global(?=\s)/', '@var', $doc ) ) ); foreach ( $this->parser->parse( $tokens )->getVarTagValues() as $tag ) { @@ -69,8 +71,9 @@ public function enterNode( Node $node ): null { } } } + $this->scopes[] = $types; - } elseif ( $node instanceof Node\Stmt\Global_ && null === $node->getDocComment() ) { + } elseif ( $node instanceof Node\Stmt\Global_ && \is_null( $node->getDocComment() ) ) { $types = \end( $this->scopes ) ?: array(); $lines = array(); foreach ( $node->vars as $var ) { @@ -78,6 +81,7 @@ public function enterNode( Node $node ): null { $lines[] = " * @var {$types[ $var->name ]} \${$var->name}"; } } + if ( array() !== $lines ) { $node->setDocComment( new Doc( "/**\n" . \implode( "\n", $lines ) . "\n */" ) ); } diff --git a/php/quality-assurance/phpcs.base.dist.xml b/php/quality-assurance/phpcs.base.dist.xml index 7223d58..e68077d 100644 --- a/php/quality-assurance/phpcs.base.dist.xml +++ b/php/quality-assurance/phpcs.base.dist.xml @@ -36,6 +36,10 @@ + + + + @@ -62,18 +66,8 @@ - - - 0 - - - - - 0 - - - + 0 diff --git a/phpcs.dist.xml b/phpcs.dist.xml index 5843c9e..0ef6fbc 100644 --- a/phpcs.dist.xml +++ b/phpcs.dist.xml @@ -1,18 +1,13 @@ - Lints this repo's own PHP. Excludes WP-runtime sniffs from the shared ruleset since this code is Composer-time tooling, not WP code. - - + - + - - + diff --git a/phpcs.tests.dist.xml b/phpcs.tests.dist.xml index 099560e..80c9086 100644 --- a/phpcs.tests.dist.xml +++ b/phpcs.tests.dist.xml @@ -1,17 +1,11 @@ - Lints this repo's own tests. - - + (?-i)*/tests/fixtures/* - - - - + diff --git a/tests/README.md b/tests/README.md index 16cef0e..3048e40 100644 --- a/tests/README.md +++ b/tests/README.md @@ -5,10 +5,11 @@ The tests prove that the shared configs, the scoping pipeline and the reusable w ## Tiers - **Unit** (`tests/Unit/`, `composer test`). PHPUnit tests that run PHPCS, PHPStan, php-scoper and workflow step scripts on fixtures copied to a temporary directory: - - `PhpcsProfilesTest` scans files with both PHPCS profiles: the case-sensitive vendor exclusions, the `index.php` stub waiver, the full `WordPress` standard, and the two ways a project lowers the PHP and WordPress floors. + - `PhpcsProfilesTest` scans files with both PHPCS profiles: the files each profile scans and the tooling, generated and case-sensitive vendor paths it skips, the `index.php` stub waiver, slash-separated hook names, generic docblock types over their native hints, the full `WordPress` standard, and the two ways a project lowers the PHP and WordPress floors. - `PhpstanConfigTest` analyzes fixtures with the shared PHPStan rules: level 10 findings, nested `vendor/` directories scanned but not analyzed, and the types `@global` tags declare. - `ScopePhpDependenciesTest` scopes `fixtures/scoping-project/` end to end and checks the output, the generated autoloader, the recipes, the `function_exists()` checks php-scoper adds to guards of global functions, and every failure the scope run reports. Its dompdf render test is in the `slow` group. - - `PhpunitWorkflowTest`, `PluginCheckWorkflowTest`, `ReleaseWorkflowTest` and `SupplyChainAuditWorkflowTest` run steps of `reusable-phpunit.yml`, `reusable-plugin-check.yml`, `reusable-release.yml` and `reusable-supply-chain-audit.yml`. `Support/RunsWorkflowSteps.php` loads a workflow with `symfony/yaml`, finds a step by its job and `name`, and runs its script with `bash -e`, as GitHub Actions does for a step that names no shell. Renaming a tested step breaks its test. + - `ScopedPackagesTest` resolves the packages to scope from inline packages through each link Composer records: a replaced dependency, a provided virtual package, a Composer plugin, a metapackage, and a requirement the root replaces or provides. `Support/RecordsInstalledPackagesTrait.php` writes `installed.json` and `installed.php` for both tests, as Composer does. + - `PhpunitWorkflowTest`, `PluginCheckWorkflowTest`, `ReleaseWorkflowTest` and `SupplyChainAuditWorkflowTest` run steps of `reusable-phpunit.yml`, `reusable-plugin-check.yml`, `reusable-release.yml` and `reusable-supply-chain-audit.yml`. `Support/RunsWorkflowStepsTrait.php` loads a workflow with `symfony/yaml`, finds a step by its job and `name`, and runs its script with `bash -e`, as GitHub Actions does for a step that names no shell. Renaming a tested step breaks its test. - **CI smokes** (`.github/workflows/quality.yml`). Checks that run a shared config as a consumer would: - the PHPCS production profile explains itself with `phpcs -e` and scans `fixtures/plugin-stub/`; - `npm run lint:config` loads each Node baseline against the installed toolchain through `fixtures/node-config/smoke.mjs`; @@ -18,11 +19,11 @@ The tests prove that the shared configs, the scoping pipeline and the reusable w ## Mutation testing -Mutation testing is opt-in. `composer test:unit:mutation` runs Infection over `php/` with the configuration in `infection.json`, skipping the `slow` group, and reports the mutation score; no gate or floor enforces it. `composer test:all` runs the unit suite and then Infection, and `composer quality-check:all` runs the PHP lint before both. +Mutation testing is opt-in. `composer test:unit:mutation` runs Infection over the directories PHPUnit's coverage source lists, with the configuration in `infection.json`, skipping the `slow` group, and reports the mutation score; no gate or floor enforces it. `php/phpstan/` stays out, because only a PHPStan subprocess runs its visitor, which `PhpstanConfigTest` pins. `composer test:all` runs the unit suite and then Infection, and `composer quality-check:all` runs the PHP lint before both. ## Not covered here -This repository's own CI runs the PHP lint, PHP syntax, PHPUnit, scripts lint, supply-chain audit, block.json, Plugin Check, workflow checks and CodeQL workflows. `reusable-playwright-e2e.yml` and `reusable-release.yml` need a plugin to act on, so CI checks them only with actionlint and zizmor, and the unit tests run the release's assertion steps; a full run of either happens first in a consumer's CI. +This repository's own CI runs the PHP lint, PHP syntax, PHPUnit, scripts lint, supply-chain audit, block.json, Plugin Check, workflow checks and CodeQL workflows. `reusable-playwright-e2e.yml` and `reusable-release.yml` need a plugin to act on, so CI checks them only with actionlint and zizmor, and the unit tests run the release's assertion steps; a full run of either happens first in a consumer's CI. `reusable-phpunit.yml`'s wp-env branch runs in no job here either: every consumer's Integration lane runs it, and a pull request that moves a consumer's pin catches a break. ## Fixtures diff --git a/tests/Support/RecordsInstalledPackagesTrait.php b/tests/Support/RecordsInstalledPackagesTrait.php new file mode 100644 index 0000000..a28a312 --- /dev/null +++ b/tests/Support/RecordsInstalledPackagesTrait.php @@ -0,0 +1,44 @@ + $root + * @param list> $packages + */ + protected static function record_installed_packages( string $vendor_dir, array $root, array $packages ): void { + $installed_json = array( + 'packages' => $packages, + 'dev' => true, + 'dev-package-names' => array(), + ); + \file_put_contents( $vendor_dir . '/composer/installed.json', \json_encode( $installed_json, \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES ) ); + + // Composer records the root and every installed package, and every name one of them replaces or provides. + $records = array( + 'replace' => 'replaced', + 'provide' => 'provided', + ); + $versions = array(); + foreach ( array( $root, ...$packages ) as $package ) { + self::assertIsString( $package['name'] ); + $versions[ $package['name'] ]['dev_requirement'] = false; + foreach ( $records as $link => $record ) { + foreach ( (array) ( $package[ $link ] ?? array() ) as $target => $constraint ) { + $versions[ $target ]['dev_requirement'] = false; + $versions[ $target ][ $record ][] = $constraint; + } + } + } + + $installed_php = array( + 'root' => array( + 'name' => $root['name'], + 'dev' => true, + ), + 'versions' => $versions, + ); + \file_put_contents( $vendor_dir . '/composer/installed.php', 'work_dir, \FilesystemIterator::SKIP_DOTS ), - \RecursiveIteratorIterator::CHILD_FIRST - ); - foreach ( $entries as $entry ) { - self::assertInstanceOf( \SplFileInfo::class, $entry ); - $entry->isDir() ? \rmdir( $entry->getPathname() ) : \unlink( $entry->getPathname() ); - } - \rmdir( $this->work_dir ); + new Filesystem()->remove( $this->work_dir ); } /** @@ -39,19 +31,24 @@ protected function write_project( array $files ): void { if ( ! \is_dir( \dirname( $file ) ) ) { \mkdir( \dirname( $file ), 0777, true ); } + \file_put_contents( $file, $contents ); } } - protected function write_executable( string $name, string $script ): void { - \file_put_contents( $this->work_dir . '/bin/' . $name, $script ); - \chmod( $this->work_dir . '/bin/' . $name, 0755 ); + protected function write_executable( string $path, string $script ): void { + if ( ! \is_dir( \dirname( $this->work_dir . '/' . $path ) ) ) { + \mkdir( \dirname( $this->work_dir . '/' . $path ), 0777, true ); + } + + \file_put_contents( $this->work_dir . '/' . $path, $script ); + \chmod( $this->work_dir . '/' . $path, 0755 ); } /** * @param array $env * - * @return array{status: int, output: string, outputs: array} + * @return array{status: int, output: string, outputs: array, env: array} */ protected function run_step( string $workflow, string $job, string $name, array $env ): array { $definition = Yaml::parseFile( __DIR__ . '/../../.github/workflows/' . $workflow ); @@ -67,11 +64,15 @@ protected function run_step( string $workflow, string $job, string $name, array $script = $step['run'] ?? null; } } + self::assertIsString( $script, "No run step named '$name' in job '$job' of $workflow." ); $log = $this->work_dir . '/step.log'; \file_put_contents( $this->work_dir . '/step.sh', $script ); - \file_put_contents( $this->work_dir . '/github-output', '' ); + foreach ( array( 'github-output', 'github-env', 'github-path' ) as $file ) { + \file_put_contents( $this->work_dir . '/' . $file, '' ); + } + \file_put_contents( $log, '' ); $process = \proc_open( array( 'bash', '-e', $this->work_dir . '/step.sh' ), // GitHub Actions runs a step that names no shell this way, so a script fails here exactly as it fails in CI. @@ -86,22 +87,31 @@ protected function run_step( string $workflow, string $job, string $name, array 'HOME' => $this->work_dir, 'GITHUB_OUTPUT' => $this->work_dir . '/github-output', 'GITHUB_ENV' => $this->work_dir . '/github-env', + 'GITHUB_PATH' => $this->work_dir . '/github-path', ) ); self::assertIsResource( $process ); $status = \proc_close( $process ); $output = (string) \file_get_contents( $log ); - $outputs = array(); - foreach ( \file( $this->work_dir . '/github-output', \FILE_IGNORE_NEW_LINES | \FILE_SKIP_EMPTY_LINES ) ?: array() as $line ) { - $pair = \explode( '=', $line, 2 ); - $outputs[ $pair[0] ] = $pair[1] ?? ''; + $files = array( + 'outputs' => 'github-output', + 'env' => 'github-env', + ); + $pairs = array(); + foreach ( $files as $key => $file ) { + $pairs[ $key ] = array(); + foreach ( \file( $this->work_dir . '/' . $file, \FILE_IGNORE_NEW_LINES | \FILE_SKIP_EMPTY_LINES ) ?: array() as $line ) { + $pair = \explode( '=', $line, 2 ); + $pairs[ $key ][ $pair[0] ] = $pair[1] ?? ''; + } } return array( 'status' => $status, 'output' => $output, - 'outputs' => $outputs, + 'outputs' => $pairs['outputs'], + 'env' => $pairs['env'], ); } } diff --git a/tests/Unit/PhpcsProfilesTest.php b/tests/Unit/PhpcsProfilesTest.php index 8a4c14e..9101130 100644 --- a/tests/Unit/PhpcsProfilesTest.php +++ b/tests/Unit/PhpcsProfilesTest.php @@ -4,9 +4,9 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use Symfony\Component\Filesystem\Filesystem; final class PhpcsProfilesTest extends TestCase { - protected const string RULESET_DIR = __DIR__ . '/../../php/quality-assurance'; protected string $scan_dir; @@ -19,15 +19,7 @@ protected function setUp(): void { #[\Override] protected function tearDown(): void { - $entries = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator( $this->scan_dir, \FilesystemIterator::SKIP_DOTS ), - \RecursiveIteratorIterator::CHILD_FIRST - ); - foreach ( $entries as $entry ) { - self::assertInstanceOf( \SplFileInfo::class, $entry ); - $entry->isDir() ? \rmdir( $entry->getPathname() ) : \unlink( $entry->getPathname() ); - } - \rmdir( $this->scan_dir ); + new Filesystem()->remove( $this->scan_dir ); } /** @@ -50,6 +42,47 @@ public function test_profile_excludes_vendor_directories_case_sensitively( strin self::assertSame( array( 'src/Vendor/x.php' ), \array_keys( $this->scan( self::RULESET_DIR . '/' . $profile ) ) ); } + public function test_production_profile_scans_a_project_s_own_files_and_skips_tooling_and_generated_ones(): void { + $scanned = array( 'includes/class-asset.php', 'patterns/example.php', 'src/Asset.php', 'src/Bin/Command.php', 'src/ScriptAsset.php', 'src/Search/Index.php', 'src/Tests/Harness.php', 'src/Vendor/Payout.php' ); + $skipped = array( 'bin/tool.php', 'blocks/build/blocks-manifest.php', 'build/index.asset.php', 'languages/acme-de_DE.l10n.php', 'node_modules/lib.php', 'tests/lib.php', 'vendor/lib.php', 'vendor-prefixed/acme/lib.php' ); + foreach ( array( ...$scanned, ...$skipped ) as $file ) { + if ( ! \is_dir( \dirname( $this->scan_dir . '/' . $file ) ) ) { + \mkdir( \dirname( $this->scan_dir . '/' . $file ), 0777, true ); + } + + \file_put_contents( $this->scan_dir . '/' . $file, "scan( self::RULESET_DIR . '/phpcs.dist.xml' ) ); + \sort( $reported ); + + self::assertSame( $scanned, $reported ); + } + + public function test_tests_profile_scans_a_tests_directory_the_production_profile_skips(): void { + \mkdir( $this->scan_dir . '/tests' ); + \file_put_contents( $this->scan_dir . '/tests/x.php', "scan( self::RULESET_DIR . '/phpcs.tests.dist.xml' ) ) ); + self::assertSame( array(), $this->scan( self::RULESET_DIR . '/phpcs.dist.xml' ) ); + } + + public function test_production_profile_accepts_slashes_between_hook_name_levels(): void { + \file_put_contents( $this->scan_dir . '/slashes.php', "scan_dir . '/hyphens.php', "scan( self::RULESET_DIR . '/phpcs.dist.xml' ); + + self::assertArrayNotHasKey( 'slashes.php', $sources ); + self::assertArrayHasKey( 'WordPress.NamingConventions.ValidHookName.UseUnderscores', $sources['hyphens.php'] ?? array() ); + } + + public function test_production_profile_accepts_a_generic_type_over_its_erased_native_hint(): void { + \file_put_contents( $this->scan_dir . '/generic.php', " \$class The class.\n *\n * @return string\n */\nfunction acme_probe( string \$class ): string {\n\treturn \$class;\n}\n" ); + + self::assertArrayNotHasKey( 'Squiz.Commenting.FunctionComment.IncorrectTypeHint', $this->scan( self::RULESET_DIR . '/phpcs.dist.xml' )['generic.php'] ?? array() ); + } + public function test_production_profile_skips_docblock_sniffs_in_patterns_directories(): void { foreach ( array( 'patterns', 'src/Patterns' ) as $dir ) { \mkdir( $this->scan_dir . '/' . $dir, 0777, true ); diff --git a/tests/Unit/PhpstanConfigTest.php b/tests/Unit/PhpstanConfigTest.php index 06a16fb..20c004a 100644 --- a/tests/Unit/PhpstanConfigTest.php +++ b/tests/Unit/PhpstanConfigTest.php @@ -3,9 +3,9 @@ namespace DeepWebSolutions\Config\Tests\Unit; use PHPUnit\Framework\TestCase; +use Symfony\Component\Filesystem\Filesystem; final class PhpstanConfigTest extends TestCase { - protected const string FIXTURE_DIR = __DIR__ . '/../fixtures/phpstan'; public function test_shared_rules_report_implicit_mixed_and_missing_overrides(): void { @@ -25,6 +25,7 @@ public function test_the_path_discovery_adds_a_plugin_s_conventional_files_and_s foreach ( array( 'src', 'includes', 'blocks', 'vendor-prefixed/acme/library', 'docs' ) as $dir ) { \mkdir( "$project/$dir", 0777, true ); } + foreach ( array( 'functions.php', 'uninstall.php', 'footprint.php' ) as $file ) { \file_put_contents( "$project/$file", "remove( $project ); } self::assertSame( diff --git a/tests/Unit/PhpunitWorkflowTest.php b/tests/Unit/PhpunitWorkflowTest.php index d0f60ee..ff93b7b 100644 --- a/tests/Unit/PhpunitWorkflowTest.php +++ b/tests/Unit/PhpunitWorkflowTest.php @@ -2,38 +2,37 @@ namespace DeepWebSolutions\Config\Tests\Unit; -use DeepWebSolutions\Config\Tests\Support\RunsWorkflowSteps; +use DeepWebSolutions\Config\Tests\Support\RunsWorkflowStepsTrait; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; final class PhpunitWorkflowTest extends TestCase { - - use RunsWorkflowSteps; + use RunsWorkflowStepsTrait; /** - * @return array, string, string}> + * @return array, string, array}> */ public static function provide_configs(): array { return array( - 'no-config' => array( array(), '', "WP_ENV_PHP_VERSION=8.5\n" ), - 'unpinned' => array( array( '.wp-env.json' => '{ "core": null }' ), '', "WP_ENV_PHP_VERSION=8.5\n" ), - 'pinned' => array( array( '.wp-env.json' => '{ "phpVersion": "7.4" }' ), '', '' ), + 'no-config' => array( array(), '', array( 'WP_ENV_PHP_VERSION' => '8.5' ) ), + 'unpinned' => array( array( '.wp-env.json' => '{ "core": null }' ), '', array( 'WP_ENV_PHP_VERSION' => '8.5' ) ), + 'pinned' => array( array( '.wp-env.json' => '{ "phpVersion": "7.4" }' ), '', array() ), 'pinned-in-the-override' => array( array( '.wp-env.json' => '{}', '.wp-env.override.json' => '{ "phpVersion": "7.4" }', ), '', - '', + array(), ), - 'pinned-for-one-environment' => array( array( '.wp-env.json' => '{ "env": { "tests": { "phpVersion": "7.4" } } }' ), '', '' ), + 'pinned-for-one-environment' => array( array( '.wp-env.json' => '{ "env": { "tests": { "phpVersion": "7.4" } } }' ), '', array() ), 'pinned-in-the-named-config' => array( array( '.wp-env.json' => '{}', '.wp-env.php74.json' => '{ "phpVersion": "7.4" }', ), '.wp-env.php74.json', - '', + array(), ), 'pinned-beside-the-named-override' => array( array( @@ -41,7 +40,7 @@ public static function provide_configs(): array { '.wp-env.php74.override.json' => '{ "phpVersion": "7.4" }', ), '.wp-env.php74.json', - '', + array(), ), 'unpinned-in-the-named-config' => array( array( @@ -49,16 +48,17 @@ public static function provide_configs(): array { '.wp-env.tests.json' => '{}', ), '.wp-env.tests.json', - "WP_ENV_PHP_VERSION=8.5\n", + array( 'WP_ENV_PHP_VERSION' => '8.5' ), ), ); } /** * @param array $files + * @param array $exported */ #[DataProvider( 'provide_configs' )] - public function test_the_site_runs_on_php_version_only_where_the_wp_env_config_pins_no_php( array $files, string $config_file, string $exported ): void { + public function test_the_site_runs_on_php_version_only_where_the_wp_env_config_pins_no_php( array $files, string $config_file, array $exported ): void { $this->write_project( $files ); $result = $this->run_step( @@ -72,6 +72,6 @@ public function test_the_site_runs_on_php_version_only_where_the_wp_env_config_p ); self::assertSame( 0, $result['status'], $result['output'] ); - self::assertSame( $exported, \is_file( $this->work_dir . '/github-env' ) ? (string) \file_get_contents( $this->work_dir . '/github-env' ) : '' ); + self::assertSame( $exported, $result['env'] ); } } diff --git a/tests/Unit/PluginCheckWorkflowTest.php b/tests/Unit/PluginCheckWorkflowTest.php index 86b2e42..ff9b8dc 100644 --- a/tests/Unit/PluginCheckWorkflowTest.php +++ b/tests/Unit/PluginCheckWorkflowTest.php @@ -2,13 +2,12 @@ namespace DeepWebSolutions\Config\Tests\Unit; -use DeepWebSolutions\Config\Tests\Support\RunsWorkflowSteps; +use DeepWebSolutions\Config\Tests\Support\RunsWorkflowStepsTrait; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; final class PluginCheckWorkflowTest extends TestCase { - - use RunsWorkflowSteps; + use RunsWorkflowStepsTrait; /** * @return array> @@ -76,7 +75,7 @@ protected static function in_each_workflow( array $rows ): array { } /** - * @return array{status: int, output: string, outputs: array} + * @return array{status: int, output: string, outputs: array, env: array} */ protected function resolve( string $workflow, string $job, string $wp_org, string $exclude_checks ): array { return $this->run_step( diff --git a/tests/Unit/ReleaseToolsTest.php b/tests/Unit/ReleaseToolsTest.php index 19f918e..19fd300 100644 --- a/tests/Unit/ReleaseToolsTest.php +++ b/tests/Unit/ReleaseToolsTest.php @@ -5,7 +5,6 @@ use PHPUnit\Framework\TestCase; final class ReleaseToolsTest extends TestCase { - protected const string DIR = __DIR__ . '/../../php/release'; public function test_the_lock_is_current_with_its_manifest(): void { @@ -23,6 +22,7 @@ public function test_the_lock_holds_every_command_the_release_runs_and_nothing_f foreach ( array( 'wp-cli/wp-cli', 'wp-cli/i18n-command', 'wp-cli/dist-archive-command' ) as $package ) { self::assertContains( $package, $names ); } + self::assertSame( array(), $lock['packages-dev'] ?? null ); } } diff --git a/tests/Unit/ReleaseWorkflowTest.php b/tests/Unit/ReleaseWorkflowTest.php index 7a0d86c..09b4150 100644 --- a/tests/Unit/ReleaseWorkflowTest.php +++ b/tests/Unit/ReleaseWorkflowTest.php @@ -2,13 +2,12 @@ namespace DeepWebSolutions\Config\Tests\Unit; -use DeepWebSolutions\Config\Tests\Support\RunsWorkflowSteps; +use DeepWebSolutions\Config\Tests\Support\RunsWorkflowStepsTrait; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; final class ReleaseWorkflowTest extends TestCase { - - use RunsWorkflowSteps; + use RunsWorkflowStepsTrait; /** * @return array, ?string, string}> @@ -48,17 +47,16 @@ public function test_the_main_plugin_file_is_the_one_root_php_file_with_a_plugin $this->write_project( $files ); $result = $this->run_build_step( 'Find the main plugin file', array() ); - $env = \is_file( $this->work_dir . '/github-env' ) ? (string) \file_get_contents( $this->work_dir . '/github-env' ) : ''; if ( \is_null( $expected ) ) { self::assertSame( 1, $result['status'], $result['output'] ); self::assertMatchesRegularExpression( '/^::error::.*' . \preg_quote( $named, '/' ) . '/m', $result['output'] ); - self::assertSame( '', $env ); + self::assertSame( array(), $result['env'] ); return; } self::assertSame( 0, $result['status'], $result['output'] ); - self::assertSame( "ENTRY_FILE=$expected\n", $env ); + self::assertSame( array( 'ENTRY_FILE' => $expected ), $result['env'] ); } /** @@ -81,6 +79,7 @@ public function test_versions_must_agree( string $tag_version, string $wp_org, ? if ( ! \is_null( $stable_tag ) ) { $files['readme.txt'] = "=== Acme Plugin ===\nStable tag: $stable_tag\n"; } + $this->write_project( $files ); $result = $this->run_build_step( @@ -255,6 +254,7 @@ public function test_headers_must_cover_the_shipped_packages( array $headers, ar foreach ( $scoped as $name => $manifest ) { $files[ "vendor-prefixed/$name/composer.json" ] = (string) \json_encode( array( 'name' => $name ) + $manifest ); } + $this->write_project( $files ); $result = $this->run_build_step( 'Verify the headers cover the shipped packages', array() ); @@ -307,17 +307,16 @@ public static function provide_archive_entries(): array { 'hidden-tests-ruleset' => array( '.phpcs.tests.xml', 1 ), 'dist-suffixed-ruleset' => array( 'phpcs.xml.dist', 1 ), 'dist-named-phpunit' => array( 'phpunit.dist.xml', 1 ), + 'node-modules' => array( 'node_modules/x', 1 ), + 'secret-env-file' => array( '.env', 1 ), + 'nested-auth-json' => array( 'nested/auth.json', 1 ), + 'bundled-certificate' => array( 'vendor/acme/ca.pem', 0 ), ); } #[DataProvider( 'provide_archive_entries' )] public function test_archive_must_not_ship_development_files( string $entry, int $expected_status ): void { - $stage = $this->work_dir . '/stage/acme-plugin'; - \mkdir( \dirname( "$stage/$entry" ), 0777, true ); - \file_put_contents( "$stage/acme-plugin.php", "work_dir . '/project/acme-plugin.zip' ) ), $output, $zip_status ); - self::assertSame( 0, $zip_status ); + $this->write_archive( array( 'acme-plugin.php', $entry ) ); $result = $this->run_build_step( 'Assert forbidden entries absent', array() ); @@ -343,15 +342,7 @@ public static function provide_scoped_archives(): array { */ #[DataProvider( 'provide_scoped_archives' )] public function test_the_archive_must_ship_every_scoped_package( array $archived, bool $scoped, int $expected_status, string $named ): void { - $stage = $this->work_dir . '/stage/acme-plugin'; - foreach ( $archived as $file ) { - if ( ! \is_dir( \dirname( "$stage/$file" ) ) ) { - \mkdir( \dirname( "$stage/$file" ), 0777, true ); - } - \file_put_contents( "$stage/$file", "work_dir . '/project/acme-plugin.zip' ) ), $output, $zip_status ); - self::assertSame( 0, $zip_status ); + $this->write_archive( $archived ); if ( $scoped ) { $this->write_project( array( 'vendor-prefixed/acme/library/composer.json' => '{"name": "acme/library"}' ) ); } @@ -365,23 +356,33 @@ public function test_the_archive_must_ship_every_scoped_package( array $archived } /** - * @return array + * @return array>|null, int, string}> */ public static function provide_provenance_runs(): array { $required = '[".github/workflows/tests.yml", ".github/workflows/quality.yml"]'; + $tests = self::workflow_run( '.github/workflows/tests.yml' ); return array( - 'every-required-workflow-passed' => array( $required, ".github/workflows/quality.yml\n.github/workflows/tests.yml\n", 0, '' ), - 'one-required-workflow-missing' => array( $required, ".github/workflows/tests.yml\n", 1, '.github/workflows/quality.yml' ), - 'required-workflows-not-a-list' => array( '".github/workflows/tests.yml"', '', 1, 'non-empty JSON array' ), - 'actions-api-unavailable' => array( $required, 'FAIL', 1, 'Unable to list' ), + 'every-required-workflow-passed' => array( $required, array( $tests, self::workflow_run( '.github/workflows/quality.yml' ) ), 0, '' ), + 'one-required-workflow-missing' => array( $required, array( $tests ), 1, '.github/workflows/quality.yml' ), + 'pull-request-run' => array( $required, array( $tests, self::workflow_run( '.github/workflows/quality.yml', array( 'event' => 'pull_request' ) ) ), 1, '.github/workflows/quality.yml' ), + 'failed-run' => array( $required, array( $tests, self::workflow_run( '.github/workflows/quality.yml', array( 'conclusion' => 'failure' ) ) ), 1, '.github/workflows/quality.yml' ), + 'run-of-another-commit' => array( $required, array( $tests, self::workflow_run( '.github/workflows/quality.yml', array( 'head_sha' => 'def456' ) ) ), 1, '.github/workflows/quality.yml' ), + 'required-workflows-not-a-list' => array( '".github/workflows/tests.yml"', array(), 1, 'non-empty JSON array' ), + 'actions-api-unavailable' => array( $required, null, 1, 'Unable to list' ), ); } + /** + * @param list>|null $runs + */ #[DataProvider( 'provide_provenance_runs' )] - public function test_a_release_needs_a_successful_push_run_of_every_required_workflow( string $required, string $runs, int $expected_status, string $named ): void { - $this->write_executable( 'gh', "#!/usr/bin/env bash\nif [ \"\$(cat \"\$HOME/runs\")\" = FAIL ]; then exit 1; fi\ncat \"\$HOME/runs\"\n" ); - \file_put_contents( $this->work_dir . '/runs', $runs ); + public function test_a_release_needs_a_successful_push_run_of_every_required_workflow( string $required, ?array $runs, int $expected_status, string $named ): void { + // Like the runs API, the stub answers only the runs of the head_sha it is asked for, and it filters them through the step's own --jq. + $this->write_executable( 'bin/gh', "#!/usr/bin/env bash\nprintf '%s\\n' \"\$@\" > \"\$HOME/gh-arguments\"\nif [ ! -f \"\$HOME/runs.json\" ]; then exit 1; fi\nwhile [ \$# -gt 0 ]; do\n\tcase \"\$1\" in\n\t\t--raw-field) case \"\$2\" in head_sha=*) sha=\"\${2#head_sha=}\" ;; esac; shift 2 ;;\n\t\t--jq) filter=\"\$2\"; shift 2 ;;\n\t\t*) shift ;;\n\tesac\ndone\njq --arg sha \"\$sha\" '.workflow_runs |= map( select( .head_sha == \$sha ) )' \"\$HOME/runs.json\" | jq -r \"\$filter\"\n" ); + if ( ! \is_null( $runs ) ) { + \file_put_contents( $this->work_dir . '/runs.json', (string) \json_encode( array( 'workflow_runs' => $runs ) ) ); + } $result = $this->run_step( 'reusable-release.yml', @@ -399,6 +400,49 @@ public function test_a_release_needs_a_successful_push_run_of_every_required_wor if ( '' !== $named ) { self::assertMatchesRegularExpression( '/^::error::.*' . \preg_quote( $named, '/' ) . '/m', $result['output'] ); } + + if ( \is_file( $this->work_dir . '/gh-arguments' ) ) { + $arguments = \file( $this->work_dir . '/gh-arguments', \FILE_IGNORE_NEW_LINES ) ?: array(); + self::assertContains( 'repos/acme/acme-plugin/actions/runs', $arguments ); + self::assertContains( 'head_sha=abc123', $arguments ); + self::assertContains( 'status=success', $arguments ); + } + } + + /** + * @return array + */ + public static function provide_release_inputs(): array { + return array( + 'version-tag' => array( 'tag', 'v1.2.3', '.', 'true', 'acme-plugin', 0, '1.2.3' ), + 'tag-with-a-leading-zero' => array( 'tag', 'v1.02.3', '.', 'true', 'acme-plugin', 1, 'no leading zeros' ), + 'branch-with-publish' => array( 'branch', 'trunk', '.', 'true', 'acme-plugin', 1, 'is not a tag' ), + 'branch-without-publish' => array( 'branch', 'trunk', 'plugins/acme-plugin', 'false', 'acme-plugin', 0, '' ), + 'dot-dot-path-segment' => array( 'tag', 'v1.2.3', 'plugins/../acme-plugin', 'true', 'acme-plugin', 1, 'must not contain a .. path segment' ), + 'absolute-path' => array( 'tag', 'v1.2.3', '/tmp/acme-plugin', 'true', 'acme-plugin', 1, 'must be relative' ), + 'slug-out-of-pattern' => array( 'tag', 'v1.2.3', '.', 'true', 'Acme_Plugin', 1, 'plugin-slug must match' ), + ); + } + + #[DataProvider( 'provide_release_inputs' )] + public function test_a_release_runs_from_a_version_tag_with_a_safe_slug_and_path( string $ref_type, string $ref_name, string $project_path, string $publish, string $slug, int $expected_status, string $expected ): void { + $result = $this->run_build_step( + 'Enforce release inputs', + array( + 'REF_TYPE' => $ref_type, + 'REF_NAME' => $ref_name, + 'PROJECT_PATH' => $project_path, + 'PUBLISH' => $publish, + 'PLUGIN_SLUG' => $slug, + ) + ); + + self::assertSame( $expected_status, $result['status'], $result['output'] ); + if ( 1 === $expected_status ) { + self::assertMatchesRegularExpression( '/^::error::.*' . \preg_quote( $expected, '/' ) . '/m', $result['output'] ); + } else { + self::assertSame( array( 'tag-version' => $expected ), $result['outputs'] ); + } } /** @@ -416,10 +460,11 @@ public static function provide_changelogs(): array { #[DataProvider( 'provide_changelogs' )] public function test_the_release_notes_are_the_version_s_changelog_section( ?string $changelog, int $expected_status, string $notes ): void { - $this->write_executable( 'gh', "#!/usr/bin/env bash\nprintf '%s\\n' \"\$@\" > \"\$HOME/gh-arguments\"\n" ); + $this->write_executable( 'bin/gh', "#!/usr/bin/env bash\nprintf '%s\\n' \"\$@\" > \"\$HOME/gh-arguments\"\n" ); if ( ! \is_null( $changelog ) ) { $this->write_project( array( 'CHANGELOG.md' => $changelog ) ); } + \mkdir( $this->work_dir . '/runner-temp' ); $result = $this->run_step( @@ -456,15 +501,12 @@ public function test_the_release_notes_are_the_version_s_changelog_section( ?str } public function test_required_files_are_found_in_a_large_archive(): void { - $stage = $this->work_dir . '/stage/acme-plugin'; - \mkdir( "$stage/assets", 0777, true ); - \file_put_contents( "$stage/main.php", "work_dir . '/project/acme-plugin.zip' ); - \exec( \sprintf( 'cd %s && zip -q %s acme-plugin/main.php && zip -qr %s acme-plugin/assets', \escapeshellarg( \dirname( $stage ) ), $zip, $zip ), $output, $zip_status ); - self::assertSame( 0, $zip_status ); + + $this->write_archive( $files ); $result = $this->run_build_step( 'Assert required files present', array( 'WP_ORG' => 'false' ) ); @@ -483,12 +525,7 @@ public static function provide_archived_main_files(): array { #[DataProvider( 'provide_archived_main_files' )] public function test_the_archive_must_ship_the_main_plugin_file( string $archived, int $expected_status ): void { - $stage = $this->work_dir . '/stage/acme-plugin'; - \mkdir( $stage, 0777, true ); - \file_put_contents( "$stage/$archived", "work_dir . '/project/acme-plugin.zip' ); - \exec( \sprintf( 'cd %s && zip -q %s %s', \escapeshellarg( \dirname( $stage ) ), $zip, \escapeshellarg( "acme-plugin/$archived" ) ), $output, $zip_status ); - self::assertSame( 0, $zip_status ); + $this->write_archive( array( $archived ) ); $result = $this->run_build_step( 'Assert required files present', array( 'WP_ORG' => 'false' ) ); @@ -525,7 +562,7 @@ public function test_a_plugin_that_does_not_require_configs_gets_no_release_tool } public function test_the_archive_command_runs_from_the_release_tools(): void { - $this->write_executable( 'wp', "#!/usr/bin/env bash\necho \"wp \$*\" >> \"\$HOME/calls\"\n" ); + $this->write_executable( 'bin/wp', "#!/usr/bin/env bash\necho \"wp \$*\" >> \"\$HOME/calls\"\n" ); $result = $this->run_build_step( 'Create plugin archive', array() ); @@ -545,8 +582,7 @@ public static function provide_wp_env_config_files(): array { #[DataProvider( 'provide_wp_env_config_files' )] public function test_wp_env_gets_a_config_flag_only_for_a_named_file( string $config_file, string $expected_arguments ): void { - $this->write_project( array( 'node_modules/.bin/wp-env' => "#!/usr/bin/env bash\necho \"\$*\" > wp-env-arguments\n" ) ); - \chmod( $this->work_dir . '/project/node_modules/.bin/wp-env', 0755 ); + $this->write_executable( 'project/node_modules/.bin/wp-env', "#!/usr/bin/env bash\necho \"\$*\" > wp-env-arguments\n" ); $result = $this->run_step( 'reusable-release.yml', 'test', 'Start wp-env', array( 'WP_ENV_CONFIG_FILE' => $config_file ) ); @@ -568,8 +604,43 @@ public function test_artifact_redirect_defaults_to_the_standard_config(): void { ) ); + $mapping = array( 'wp-content/plugins/acme-plugin' => '/built/acme-plugin' ); + self::assertSame( 0, $result['status'], $result['output'] ); + self::assertJsonStringEqualsJsonString( + (string) \json_encode( + array( + 'core' => null, + 'mappings' => $mapping, + 'env' => array( + 'development' => array( 'mappings' => $mapping ), + 'tests' => array( 'mappings' => $mapping ), + ), + ) + ), + (string) \file_get_contents( $this->work_dir . '/project/.wp-env.override.json' ) + ); + } + + /** + * @return array + */ + public static function provide_wp_env_ports(): array { + return array( + 'development-port' => array( '{ "port": 8811, "env": { "development": { "port": 8822 } } }', '8822' ), + 'root-port' => array( '{ "port": 8811 }', '8811' ), + 'no-port' => array( '{}', '8888' ), + ); + } + + #[DataProvider( 'provide_wp_env_ports' )] + public function test_the_site_is_requested_on_the_config_s_development_port( string $config, string $port ): void { + $this->write_project( array( '.wp-env.json' => $config ) ); + $this->write_executable( 'bin/curl', "#!/usr/bin/env bash\nprintf '%s\\n' \"\$@\" > \"\$HOME/curl-arguments\"\n" ); + + $result = $this->run_step( 'reusable-release.yml', 'test', 'Assert the site serves', array( 'WP_ENV_CONFIG_FILE' => '' ) ); + self::assertSame( 0, $result['status'], $result['output'] ); - self::assertStringContainsString( '/built/acme-plugin', (string) \file_get_contents( $this->work_dir . '/project/.wp-env.override.json' ) ); + self::assertContains( "http://localhost:$port/", \file( $this->work_dir . '/curl-arguments', \FILE_IGNORE_NEW_LINES ) ?: array() ); } /** @@ -585,9 +656,8 @@ public static function provide_expected_versions(): array { #[DataProvider( 'provide_expected_versions' )] public function test_the_active_plugin_must_carry_the_built_version( string $expected_version, int $expected_status ): void { // Like wp-env's own `run`, the stub consumes the first `--` and appends the arguments after it to the command. - $this->write_project( array( 'node_modules/.bin/wp-env' => "#!/usr/bin/env bash\nuntil [ \"\$1\" = run ]; do shift; done\nshift 2\ncommand=()\nfor argument in \"\$@\"; do\n\tif [ -z \"\$dashed\" ] && [ \"\$argument\" = -- ]; then dashed=1; continue; fi\n\tcommand+=( \"\$argument\" )\ndone\nexec \"\${command[@]}\"\n" ) ); - \chmod( $this->work_dir . '/project/node_modules/.bin/wp-env', 0755 ); - $this->write_executable( 'wp', "#!/usr/bin/env bash\ncase \"\$*\" in\n\t'plugin activate acme-plugin') ;;\n\t'plugin get acme-plugin --field=version') echo 1.2.3 ;;\n\t*) echo \"Error: The '\$3' plugin could not be found.\" >&2; exit 1 ;;\nesac\n" ); + $this->write_executable( 'project/node_modules/.bin/wp-env', "#!/usr/bin/env bash\nuntil [ \"\$1\" = run ]; do shift; done\nshift 2\ncommand=()\nfor argument in \"\$@\"; do\n\tif [ -z \"\$dashed\" ] && [ \"\$argument\" = -- ]; then dashed=1; continue; fi\n\tcommand+=( \"\$argument\" )\ndone\nexec \"\${command[@]}\"\n" ); + $this->write_executable( 'bin/wp', "#!/usr/bin/env bash\ncase \"\$*\" in\n\t'plugin activate acme-plugin') ;;\n\t'plugin get acme-plugin --field=version') echo 1.2.3 ;;\n\t*) echo \"Error: The '\$3' plugin could not be found.\" >&2; exit 1 ;;\nesac\n" ); $result = $this->run_step( 'reusable-release.yml', @@ -605,25 +675,55 @@ public function test_the_active_plugin_must_carry_the_built_version( string $exp self::assertStringContainsString( "Active plugin version: 1.2.3 (expected $expected_version)", $result['output'] ); } + /** + * @param list $files + */ + protected function write_archive( array $files ): void { + $stage = $this->work_dir . '/stage/acme-plugin'; + foreach ( $files as $file ) { + if ( ! \is_dir( \dirname( "$stage/$file" ) ) ) { + \mkdir( \dirname( "$stage/$file" ), 0777, true ); + } + + \file_put_contents( "$stage/$file", "work_dir . '/stage/entries', \implode( "\n", \array_map( static fn ( string $file ): string => "acme-plugin/$file", $files ) ) . "\n" ); + \exec( \sprintf( 'cd %s && zip -q %s -@ < entries', \escapeshellarg( $this->work_dir . '/stage' ), \escapeshellarg( $this->work_dir . '/project/acme-plugin.zip' ) ), $output, $zip_status ); + self::assertSame( 0, $zip_status, \implode( "\n", $output ) ); + } + + /** + * @param array $overrides + * + * @return array + */ + protected static function workflow_run( string $path, array $overrides = array() ): array { + return $overrides + array( + 'path' => $path, + 'event' => 'push', + 'conclusion' => 'success', + 'head_sha' => 'abc123', + ); + } + // Answers `composer config vendor-dir` as Composer's default does, and records every other call. protected function write_fake_composer(): void { - $this->write_executable( 'composer', "#!/usr/bin/env bash\nif [ \"\$1\" = config ]; then echo vendor; exit 0; fi\necho \"composer \$*\" >> \"\$HOME/calls\"\n" ); + $this->write_executable( 'bin/composer', "#!/usr/bin/env bash\nif [ \"\$1\" = config ]; then echo vendor; exit 0; fi\necho \"composer \$*\" >> \"\$HOME/calls\"\n" ); } /** * @return array */ protected function release_tools_env(): array { - return array( - 'RUNNER_TEMP' => $this->work_dir . '/runner-temp', - 'GITHUB_PATH' => $this->work_dir . '/github-path', - ); + return array( 'RUNNER_TEMP' => $this->work_dir . '/runner-temp' ); } /** * @param array $env * - * @return array{status: int, output: string, outputs: array} + * @return array{status: int, output: string, outputs: array, env: array} */ protected function run_build_step( string $name, array $env ): array { return $this->run_step( diff --git a/tests/Unit/ScopePhpDependenciesTest.php b/tests/Unit/ScopePhpDependenciesTest.php index 9bc4bf9..52bb18b 100644 --- a/tests/Unit/ScopePhpDependenciesTest.php +++ b/tests/Unit/ScopePhpDependenciesTest.php @@ -3,12 +3,16 @@ namespace DeepWebSolutions\Config\Tests\Unit; use DeepWebSolutions\Config\Composer\ScopePhpDependencies; +use DeepWebSolutions\Config\Tests\Support\RecordsInstalledPackagesTrait; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; use Symfony\Component\Filesystem\Filesystem; +#[CoversClass( ScopePhpDependencies::class )] final class ScopePhpDependenciesTest extends TestCase { + use RecordsInstalledPackagesTrait; protected const string FIXTURE_DIR = __DIR__ . '/../fixtures/scoping-project'; @@ -85,20 +89,34 @@ public function test_non_php_files_ship_with_their_package(): void { } public function test_scoped_autoload_loads_prefixed_classes_and_files_without_global_aliases(): void { - $script = \sprintf( - 'namespace Automattic\WooCommerce\Admin\Settings { abstract class SettingsSection {} } namespace { require %s; require %s; echo json_encode( array( interface_exists( %s ), class_exists( %s ), class_exists( %s ), class_exists( %s ), class_exists( %s ), function_exists( %s ), function_exists( %s ), get_class( %s::format( "Csv" ) ), class_exists( "LegacyRenderer", false ), class_exists( "AcmeLegacy_Config", false ) ) ); }', - \var_export( self::VENDOR_DIR . '/composer/ClassLoader.php', true ), - \var_export( self::scoped_project() . '/vendor-prefixed/scoper-autoload.php', true ), - \var_export( 'Acme\Scoped\Psr\Log\LoggerInterface', true ), - \var_export( 'Acme\Scoped\LegacyRenderer', true ), - \var_export( 'Acme\Scoped\LegacyCanvas', true ), - \var_export( 'Acme\Scoped\AcmeLegacy_Config', true ), - \var_export( 'Acme\Scoped\DI\ContainerBuilder', true ), - \var_export( 'Acme\Scoped\DI\create', true ), - \var_export( 'Acme\Scoped\Acme\Commerce\get_label_prefix', true ), - '\Acme\Scoped\Acme\Commerce\SettingsSection' - ); - \exec( \escapeshellarg( \PHP_BINARY ) . ' -r ' . \escapeshellarg( $script ) . ' 2>&1', $output, $exit_code ); + $project = self::scoped_project(); + $probe = <<<'PHP' + &1', $output, $exit_code ); self::assertSame( 0, $exit_code, \implode( "\n", $output ) ); self::assertSame( '[true,true,true,true,true,true,true,"Acme\\\\Scoped\\\\Acme\\\\Commerce\\\\Formats\\\\Csv",false,false]', \implode( "\n", $output ) ); @@ -226,6 +244,7 @@ public function test_scope_run_fails_without_a_text_domain_when_a_package_declar self::assertStringStartsWith( "php-scoper failed:\n", $exception->getMessage() ); self::assertStringContainsString( 'extra.text-domain', $exception->getMessage() ); } + self::assertFileExists( $project . '/vendor-prefixed/scoper-autoload.php' ); } @@ -248,6 +267,20 @@ public function test_a_project_that_scopes_no_declaring_package_needs_no_text_do self::assertSame( array( 'acme/legacy' ), self::package_dirs( $project . '/vendor-prefixed' ) ); } + public function test_a_php_scoper_run_that_dies_leaves_no_partial_build(): void { + $project = self::make_project( array( 'acme/commerce' ) ); + \file_put_contents( $project . '/scoper.inc.php', \str_replace( 'return ( require', '$config = ( require', self::read( $project . '/scoper.inc.php' ) ) . "\$config['patchers'][] = static function ( string \$file_path, string \$prefix, string \$contents ): string {\n\tif ( str_ends_with( \$file_path, 'SettingsSection.php' ) ) {\n\t\texit( 1 );\n\t}\n\n\treturn \$contents;\n};\n\nreturn \$config;\n" ); + + try { + ScopePhpDependencies::scope( $project ); + self::fail( 'The scope run did not fail.' ); + } catch ( \RuntimeException $exception ) { + self::assertStringStartsWith( "php-scoper failed:\n", $exception->getMessage() ); + } + + self::assertDirectoryDoesNotExist( $project . '/vendor-prefixed.tmp' ); + } + public function test_a_failed_scope_run_keeps_the_previous_build(): void { $project = self::make_project( array( 'acme/commerce' ) ); ScopePhpDependencies::scope( $project ); @@ -260,6 +293,7 @@ public function test_a_failed_scope_run_keeps_the_previous_build(): void { } catch ( \RuntimeException $exception ) { self::assertStringContainsString( 'extra.text-domain', $exception->getMessage() ); } + self::assertSame( $autoload, self::read( $project . '/vendor-prefixed/scoper-autoload.php' ) ); self::assertContains( 'acme/commerce', self::package_dirs( $project . '/vendor-prefixed' ) ); } @@ -299,6 +333,7 @@ public function test_scope_run_fails_on_a_prefixed_name_the_scoped_code_does_not self::assertStringStartsWith( 'No scoped package declares these prefixed names', $exception->getMessage() ); self::assertMatchesRegularExpression( '/\n' . \preg_quote( $name . ' in ', '/' ) . '.+SettingsSection\.php:\d+$/m', $exception->getMessage() ); } + self::assertStringContainsString( "'Acme\\\\Scoped\\\\Acme\\\\Commerce\\\\SettingsSection'", self::read( $project . '/vendor-prefixed/scoper-autoload.php' ) ); } @@ -319,16 +354,176 @@ public static function provide_undeclared_prefixed_names(): array { 'class-named-like-a-function' => array( $return, "\t\tnew \\Date();\n" . $return, 'Acme\Scoped\Date' ), 'callable-of-a-php-function' => array( $return, "\t\t\\add_action( 'init', 'Brotli\\compress' );\n" . $return, 'Acme\Scoped\Brotli\compress' ), 'uppercased-function-exists' => array( $return, "\t\t\\FUNCTION_EXISTS( 'Brotli\\compress' );\n" . $return, 'Acme\Scoped\Brotli\compress' ), + 'nowdoc-after-a-blank-line' => array( $return, "\t\t\$code = <<<'PHP'\n\n\t\t\t array( $return, "\t\t\\constant( 'Automattic\\\\WooCommerce\\\\Future\\\\Feature::FLAG' );\n" . $return, 'Acme\Scoped\Automattic\WooCommerce\Future\Feature' ), + 'function-taken-for-a-class' => array( $return, "\t\tnew \\Acme\\Commerce\\get_label_prefix();\n" . $return, 'Acme\Scoped\Acme\Commerce\get_label_prefix' ), + 'namespace-taken-for-a-class' => array( $return, "\t\tnew \\Acme\\Commerce\\Formats();\n" . $return, 'Acme\Scoped\Acme\Commerce\Formats' ), + 'constant-in-another-case' => array( $return, "\t\t\$label = \\Acme\\Commerce\\Label;\n" . $return, 'Acme\Scoped\Acme\Commerce\Label' ), ); } - public function test_scope_run_fails_when_a_declared_text_domain_survives_the_rewrite(): void { + public function test_scope_run_fails_when_a_declared_text_domain_survives_the_rewrite_and_keeps_the_previous_build(): void { $project = self::make_project( array( 'acme/commerce' ) ); - $section = $project . '/vendor' . self::SECTION; + ScopePhpDependencies::scope( $project ); + $autoload = self::read( $project . '/vendor-prefixed/scoper-autoload.php' ); + $packages = self::package_dirs( $project . '/vendor-prefixed' ); + $section = $project . '/vendor' . self::SECTION; \file_put_contents( $section, \str_replace( "\t\treturn \\__(", "\t\t\$credit = 'Translated by acme-commerce';\n\t\treturn \\__(", self::read( $section ) ) ); + try { + ScopePhpDependencies::scope( $project ); + self::fail( 'The scope run did not fail.' ); + } catch ( \RuntimeException $exception ) { + self::assertStringContainsString( "still contains the text domain 'acme-commerce'", $exception->getMessage() ); + } + + self::assertSame( $autoload, self::read( $project . '/vendor-prefixed/scoper-autoload.php' ) ); + self::assertSame( $packages, self::package_dirs( $project . '/vendor-prefixed' ) ); + } + + public function test_scope_run_reports_a_text_domain_quoted_inside_another_string(): void { + $project = self::make_project( array( 'acme/commerce' ) ); + $section = $project . '/vendor' . self::SECTION; + \file_put_contents( $section, \str_replace( "\t\treturn \\__(", "\t\t\$hint = 'Choose \"acme-commerce\" here';\n\t\treturn \\__(", self::read( $section ) ) ); + $this->expectException( \RuntimeException::class ); - $this->expectExceptionMessageIsOrContains( 'acme-commerce' ); + $this->expectExceptionMessageIsOrContains( "still contains the text domain 'acme-commerce'" ); + + ScopePhpDependencies::scope( $project ); + } + + public function test_a_double_quoted_text_domain_takes_the_project_s(): void { + $project = self::make_project( array( 'acme/commerce' ) ); + $section = $project . '/vendor' . self::SECTION; + \file_put_contents( $section, \str_replace( "\t\treturn \\__(", "\t\t\$other = \\__( 'Other', \"acme-commerce\" );\n\t\treturn \\__(", self::read( $section ) ) ); + + ScopePhpDependencies::scope( $project ); + + self::assertStringContainsString( "\\__('Other', 'acme-text-domain')", self::read( $project . '/vendor-prefixed' . self::SECTION ) ); + } + + public function test_a_text_domain_in_the_php_code_of_a_nowdoc_takes_the_project_s(): void { + $project = self::make_project( array( 'acme/commerce' ) ); + $section = $project . '/vendor' . self::SECTION; + \file_put_contents( $section, \str_replace( "\t\treturn \\__(", "\t\t\$template = <<<'PHP'\n\t\t\t 'acme-commerce' ) ); + + ScopePhpDependencies::scope( $project ); + + self::assertStringContainsString( "\\__('Commerce', 'acme-commerce')", self::read( $project . '/vendor-prefixed' . self::SECTION ) ); + } + + public function test_a_host_class_under_a_scoped_package_s_namespace_stays_global_in_strings(): void { + $project = self::make_project( array( 'acme/commerce' ), array( 'scoping-stubs' => array( './stubs/host-plugin.php', './stubs/gateway.php' ) ) ); + \file_put_contents( $project . '/stubs/gateway.php', " array( './stubs/host-plugin.php', './stubs/acme.php' ) ) ); + \file_put_contents( $project . '/stubs/acme.php', " array( './stubs/host-plugin.php', './stubs/calls.php' ) ) ); + \file_put_contents( $project . '/stubs/calls.php', " + */ + public static function provide_symbols_stubs_declare_through_calls(): array { + return array( + 'constant-of-an-uppercase-define' => array( "DEFINE( 'Host\\\\Plugin\\\\FLAG', 1 );", '$flag = \\Host\\Plugin\\FLAG' ), + 'alias-named-by-a-class-constant' => array( "class Host_Plugin_Real {}\nclass_alias( 'Host_Plugin_Real', \\Host\\Plugin\\Alias::class );", '$alias = new \\Host\\Plugin\\Alias()' ), + ); + } + + public function test_scope_run_fails_when_the_scoped_code_declares_a_class_outside_the_prefix(): void { + $project = self::make_project( array( 'acme/commerce' ) ); + \file_put_contents( $project . '/vendor/acme/commerce/lib/Mailer.php', "getMessage() ); + self::assertMatchesRegularExpression( '/\nPHPMailer\\\\PHPMailer$/m', $exception->getMessage() ); + } + } + + public function test_a_polyfill_of_a_php_class_passes_the_check(): void { + $project = self::make_project( array( 'acme/commerce' ) ); + \file_put_contents( $project . '/vendor/acme/commerce/lib/Polyfill.php', "expectException( \RuntimeException::class ); + $this->expectExceptionMessageMatches( '#acme/commerce/lib/Broken\.php#' ); + + ScopePhpDependencies::scope( $project ); + } + + public function test_scope_run_names_a_stubs_file_it_cannot_parse(): void { + $project = self::make_project( array( 'acme/commerce' ), array( 'scoping-stubs' => array( './stubs/host-plugin.php', './stubs/broken.php' ) ) ); + \file_put_contents( $project . '/stubs/broken.php', "expectException( \RuntimeException::class ); + $this->expectExceptionMessageMatches( "#Could not parse '[^']*/stubs/broken\.php'#" ); ScopePhpDependencies::scope( $project ); } @@ -352,6 +547,7 @@ public function test_scope_run_fails_before_php_scoper_without_a_scoping_prefix( } catch ( \RuntimeException $exception ) { self::assertStringContainsString( 'extra.scoping-prefix', $exception->getMessage() ); } + self::assertDirectoryDoesNotExist( $project . '/vendor-prefixed' ); } @@ -410,6 +606,7 @@ public function test_scope_run_refuses_an_output_directory_that_resolves_elsewhe } catch ( \RuntimeException $exception ) { self::assertStringContainsString( 'vendor-prefixed', $exception->getMessage() ); } + self::assertFileExists( $project . '/src/Plugin.php' ); } @@ -488,6 +685,7 @@ protected static function make_project( array $names = array( 'acme/commerce', ' if ( 'vendor' !== $vendor ) { $manifest['config'] = array( 'vendor-dir' => $vendor ); } + \file_put_contents( $project . '/composer.json', \json_encode( $manifest, \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES ) ); $locked = array(); @@ -511,16 +709,12 @@ protected static function make_project( array $names = array( 'acme/commerce', ' $source = self::VENDOR_DIR . '/' . $name; $package = $locked[ $name ]; } + new Filesystem()->mirror( $source, $project . '/' . $vendor . '/' . $name ); $packages[] = array( 'install-path' => '../' . $name ) + $package; } - $installed_json = array( - 'packages' => $packages, - 'dev' => true, - 'dev-package-names' => array(), - ); - \file_put_contents( $project . '/' . $vendor . '/composer/installed.json', \json_encode( $installed_json, \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES ) ); + self::record_installed_packages( $project . '/' . $vendor, $manifest, $packages ); return $project; } diff --git a/tests/Unit/ScopedPackagesTest.php b/tests/Unit/ScopedPackagesTest.php new file mode 100644 index 0000000..680017d --- /dev/null +++ b/tests/Unit/ScopedPackagesTest.php @@ -0,0 +1,130 @@ +remove( \dirname( $this->vendor_dir ) ); + } + + /** + * @param list> $packages + * @param array $root_links + * @param list $scoped + */ + #[DataProvider( 'provide_links_composer_resolved' )] + public function test_dependencies_follow_the_links_composer_resolved( array $packages, array $root_links, array $scoped ): void { + $this->install( $packages, $root_links ); + + self::assertSame( $scoped, \array_keys( ScopedPackages::resolve( $this->vendor_dir, array( 'acme/app' ) )['scoped'] ) ); + } + + /** + * @return array>, array, list}> + */ + public static function provide_links_composer_resolved(): array { + return array( + 'replaced-dependency' => array( + array( + self::package( 'acme/app', array( 'require' => array( 'acme/upstream' => '*' ) ) ), + self::package( 'acme/fork', array( 'replace' => array( 'acme/upstream' => '*' ) ) ), + ), + array(), + array( 'acme/app', 'acme/fork' ), + ), + 'provided-virtual-package' => array( + array( + self::package( 'acme/app', array( 'require' => array( 'acme/logger-implementation' => '*' ) ) ), + self::package( 'acme/adapter', array( 'provide' => array( 'acme/logger-implementation' => '1.0' ) ) ), + ), + array(), + array( 'acme/app' ), + ), + 'composer-plugin-dependency' => array( + array( + self::package( 'acme/app', array( 'require' => array( 'acme/installer' => '*' ) ) ), + self::package( 'acme/installer', array( 'type' => 'composer-plugin' ) ), + ), + array(), + array( 'acme/app' ), + ), + 'metapackage-dependency' => array( + array( + self::package( 'acme/app', array( 'require' => array( 'acme/bundle' => '*' ) ) ), + self::package( + 'acme/bundle', + array( + 'type' => 'metapackage', + 'require' => array( 'acme/loader' => '*' ), + ) + ), + self::package( 'acme/loader' ), + ), + array(), + array( 'acme/app', 'acme/loader' ), + ), + 'dependency-the-root-replaces' => array( + array( self::package( 'acme/app', array( 'require' => array( 'acme/polyfill' => '*' ) ) ) ), + array( 'replace' => array( 'acme/polyfill' => '*' ) ), + array( 'acme/app' ), + ), + 'virtual-the-root-provides' => array( + array( self::package( 'acme/app', array( 'require' => array( 'acme/cache-implementation' => '*' ) ) ) ), + array( 'provide' => array( 'acme/cache-implementation' => '1.0' ) ), + array( 'acme/app' ), + ), + ); + } + + public function test_a_dependency_nothing_installs_replaces_or_provides_fails(): void { + $this->install( array( self::package( 'acme/app', array( 'require' => array( 'acme/absent' => '*' ) ) ) ), array() ); + + $this->expectException( \RuntimeException::class ); + $this->expectExceptionMessageIsOrContains( "'acme/app' requires 'acme/absent', which is not installed" ); + + ScopedPackages::resolve( $this->vendor_dir, array( 'acme/app' ) ); + } + + /** + * @param list> $packages + * @param array $root_links + */ + protected function install( array $packages, array $root_links ): void { + $this->vendor_dir = ( \realpath( \sys_get_temp_dir() ) ?: \sys_get_temp_dir() ) . '/wp-configs-packages ' . \uniqid() . '/vendor'; + \mkdir( $this->vendor_dir . '/composer', 0777, true ); + foreach ( $packages as $package ) { + self::assertIsString( $package['name'] ); + \mkdir( $this->vendor_dir . '/' . $package['name'], 0777, true ); + } + + self::record_installed_packages( $this->vendor_dir, array( 'name' => 'acme/project' ) + $root_links, $packages ); + } + + /** + * @param array $links + * + * @return array + */ + protected static function package( string $name, array $links = array() ): array { + return $links + array( + 'name' => $name, + 'version' => '1.0.0', + 'type' => 'library', + 'install-path' => '../' . $name, + ); + } +} diff --git a/tests/Unit/SupplyChainAuditWorkflowTest.php b/tests/Unit/SupplyChainAuditWorkflowTest.php index 8c4fb65..a555568 100644 --- a/tests/Unit/SupplyChainAuditWorkflowTest.php +++ b/tests/Unit/SupplyChainAuditWorkflowTest.php @@ -2,13 +2,12 @@ namespace DeepWebSolutions\Config\Tests\Unit; -use DeepWebSolutions\Config\Tests\Support\RunsWorkflowSteps; +use DeepWebSolutions\Config\Tests\Support\RunsWorkflowStepsTrait; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; final class SupplyChainAuditWorkflowTest extends TestCase { - - use RunsWorkflowSteps; + use RunsWorkflowStepsTrait; /** * @return array @@ -50,11 +49,11 @@ public function test_failing_mode_fails_on_advisories(): void { } /** - * @return array{status: int, output: string, outputs: array} + * @return array{status: int, output: string, outputs: array, env: array} */ protected function audit( ?string $fixture, string $fail_on_findings ): array { $report = \is_null( $fixture ) ? '/dev/null' : __DIR__ . '/../fixtures/composer-audit/' . $fixture; - $this->write_executable( 'composer', "#!/usr/bin/env bash\ncat " . \escapeshellarg( $report ) . "\nexit 1\n" ); + $this->write_executable( 'bin/composer', "#!/usr/bin/env bash\ncat " . \escapeshellarg( $report ) . "\nexit 1\n" ); return $this->run_step( 'reusable-supply-chain-audit.yml', diff --git a/tests/fixtures/node-config/smoke.mjs b/tests/fixtures/node-config/smoke.mjs index b8c276f..a3c3a64 100644 --- a/tests/fixtures/node-config/smoke.mjs +++ b/tests/fixtures/node-config/smoke.mjs @@ -5,7 +5,7 @@ */ import { execFileSync } from 'node:child_process'; -import { readFileSync } from 'node:fs'; +import { existsSync, readFileSync, rmSync } from 'node:fs'; import { createRequire } from 'node:module'; import { resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; @@ -81,6 +81,24 @@ probes.push( [ throw new Error( `${ name } is linted as a script` ); } } + + const e2e = await eslint.calculateConfigForFile( + resolve( 'tests/e2e/probe.js' ) + ); + if ( + ! Object.keys( e2e?.rules ?? {} ).some( ( rule ) => + rule.startsWith( 'playwright/' ) + ) + ) { + throw new Error( + 'tests/e2e/probe.js does not get the Playwright rules' + ); + } + for ( const path of [ 'build/x.js', 'vendor/x.js' ] ) { + if ( ! ( await eslint.isPathIgnored( resolve( path ) ) ) ) { + throw new Error( `${ path } is linted` ); + } + } }, ] ); @@ -111,6 +129,9 @@ probes.push( [ if ( config.rules[ 'selector-class-pattern' ] ) { throw new Error( 'selector-class-pattern is not turned off' ); } + if ( ! config.rules[ 'font-weight-notation' ] ) { + throw new Error( '@wordpress/stylelint-config is not extended' ); + } }, ] ); @@ -149,12 +170,36 @@ probes.push( [ probes.push( [ 'tsconfig.base.json', - () => - execFileSync( - './node_modules/.bin/tsc', - [ '--noEmit', '-p', 'tests/fixtures/node-config/tsconfig.json' ], - { stdio: 'pipe' } - ), + () => { + const project = [ '-p', 'tests/fixtures/node-config/tsconfig.json' ]; + // The base's noEmit stands in for the flag, so compiling the probe writes no JavaScript. + execFileSync( './node_modules/.bin/tsc', project, { stdio: 'pipe' } ); + if ( existsSync( 'tests/fixtures/node-config/probe.js' ) ) { + rmSync( 'tests/fixtures/node-config/probe.js' ); + throw new Error( 'tsc emitted probe.js' ); + } + const options = JSON.parse( + execFileSync( + './node_modules/.bin/tsc', + [ '--showConfig', ...project ], + { stdio: 'pipe' } + ) + ).compilerOptions; + const deltas = { + jsx: 'react-jsx', + noImplicitReturns: true, + noFallthroughCasesInSwitch: true, + skipLibCheck: true, + isolatedModules: true, + resolveJsonModule: true, + noEmit: true, + }; + for ( const [ option, value ] of Object.entries( deltas ) ) { + if ( value !== options[ option ] ) { + throw new Error( `${ option } is ${ options[ option ] }` ); + } + } + }, ] ); probes.push( [