From 6fee7ca136bf196246c6babc9f5dc0aac8f77322 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Fri, 21 Aug 2026 17:21:16 +0200 Subject: [PATCH] feat(quality): parse every shipped language and run its unit suite Four parse checks join the `lint-*` family, each using the language's own parser rather than a reimplementation: `php -l`, `ruby -c`, `sqlfluff parse`, and stylelint with an empty rule set (it raises CssSyntaxError whatever the rules say, so the check stays syntax-only and grows no style opinion). Three unit runners join `test-unit`: node:test, PHPUnit, minitest -- the first and third ship with their runtime, so only PHPUnit adds a dependency. Provisioning is best effort and the lint script decides whether it can run. A machine without PHP must not fail the gate, but it must not look like success either: the skip names how many files went unchecked. The first version of these scripts read `git ls-files`, which inside the compose container returns nothing (git refuses the bind-mounted repo as "dubious ownership") and reported "No SQL files found." over 52 files -- green, and a lie. They use `find` now, as the surrounding lint scripts already did. sqlfluff needed two corrections before it parsed anything real. `%(name)s` is a psycopg parameter marker, not SQL, so the placeholder templater is configured rather than the files exempted; and the dialect follows each role's declared engine (`meta/services.yml`), which is postgres for six roles and mysql for svc-db-mariadb and pixelfed. Recipes stay in scripts, not in the Makefile: the targets delegate, matching every other target in the file. Co-Authored-By: Claude Opus 5 (1M context) --- .sqlfluff | 11 + .stylelintrc.json | 3 + Makefile | 58 +- aliases | 8 + composer.json | 15 + package-lock.json | 1441 ++++++++++++++++- package.json | 3 +- phpunit.xml | 19 + roles/svc-db-mariadb/files/sql/.sqlfluff | 6 + roles/web-app-pixelfed/files/sql/.sqlfluff | 6 + scripts/lint/css.sh | 35 + scripts/lint/php.sh | 39 + scripts/lint/ruby.sh | 39 + scripts/lint/sql.sh | 42 + scripts/tests/unit/javascript.sh | 24 + scripts/tests/unit/php.sh | 35 + scripts/tests/unit/ruby.sh | 28 + .../sys-front-inj-logout/files/logout.test.js | 140 ++ .../files/logout-panel.test.js | 216 +++ utils/install/lint/__main__.py | 27 +- utils/install/lint/php.py | 24 + utils/install/lint/ruby.py | 24 + utils/install/lint/sqlfluff.py | 24 + utils/install/lint/stylelint.py | 28 + utils/install/system_pkg.py | 8 + 25 files changed, 2297 insertions(+), 6 deletions(-) create mode 100644 .sqlfluff create mode 100644 .stylelintrc.json create mode 100644 composer.json create mode 100644 phpunit.xml create mode 100644 roles/svc-db-mariadb/files/sql/.sqlfluff create mode 100644 roles/web-app-pixelfed/files/sql/.sqlfluff create mode 100755 scripts/lint/css.sh create mode 100755 scripts/lint/php.sh create mode 100755 scripts/lint/ruby.sh create mode 100755 scripts/lint/sql.sh create mode 100755 scripts/tests/unit/javascript.sh create mode 100755 scripts/tests/unit/php.sh create mode 100755 scripts/tests/unit/ruby.sh create mode 100644 tests/unit/javascript/roles/sys-front-inj-logout/files/logout.test.js create mode 100644 tests/unit/javascript/roles/web-app-keycloak/files/logout-panel.test.js create mode 100644 utils/install/lint/php.py create mode 100644 utils/install/lint/ruby.py create mode 100644 utils/install/lint/sqlfluff.py create mode 100644 utils/install/lint/stylelint.py diff --git a/.sqlfluff b/.sqlfluff new file mode 100644 index 0000000000..21e374f1f4 --- /dev/null +++ b/.sqlfluff @@ -0,0 +1,11 @@ +[sqlfluff] +# Postgres is the majority engine. A role whose meta/services.yml declares +# `mariadb:` instead overrides this with its own .sqlfluff beside the files. +dialect = postgres +templater = placeholder + +# The shipped SQL is executed through psycopg, so `%(name)s` parameter markers +# are the driver's, not SQL. Without this the parser rejects every file that +# takes an argument. +[sqlfluff:templater:placeholder] +param_style = pyformat diff --git a/.stylelintrc.json b/.stylelintrc.json new file mode 100644 index 0000000000..90d8894e28 --- /dev/null +++ b/.stylelintrc.json @@ -0,0 +1,3 @@ +{ + "rules": {} +} diff --git a/Makefile b/Makefile index 3c4f56b1e3..6f4a8cc0fa 100644 --- a/Makefile +++ b/Makefile @@ -384,15 +384,19 @@ kernel-loop-load: lint: install-lint @bash scripts/make/parallel.sh lint-action \ lint-ansible \ + lint-css \ lint-dockerfile \ lint-javascript \ lint-makefile \ lint-markdown \ lint-mermaid \ lint-packages \ + lint-php \ lint-playwright \ lint-python \ - lint-shellcheck + lint-ruby \ + lint-shellcheck \ + lint-sql .PHONY: lint-action # Run the GitHub Actions lint checks. @@ -405,6 +409,12 @@ lint-action: install-lint lint-ansible: install-lint setup @bash scripts/lint/wrapper.sh ansible +.PHONY: lint-css +# Check that every CSS file parses, via stylelint with an empty rule set. +lint-css: install-lint + @bash scripts/install/wrapper.sh css + @bash scripts/lint/wrapper.sh css + .PHONY: lint-dockerfile # Run hadolint over the root Dockerfile. lint-dockerfile: install-lint @@ -438,6 +448,13 @@ lint-packages: install-lint @bash scripts/install/wrapper.sh packages @bash scripts/lint/wrapper.sh packages +.PHONY: lint-php +# Check that every PHP file parses, via `php -l`. +# Note: provisions the PHP CLI explicitly; an absent interpreter is skipped. +lint-php: install-lint + @bash scripts/install/wrapper.sh php + @bash scripts/lint/wrapper.sh php + .PHONY: lint-playwright # Verify every role's Playwright spec parses + resolves its helpers. # Note: stages the spec like test-e2e-playwright does and runs `npx playwright test --list`. @@ -449,11 +466,25 @@ lint-playwright: install-lint lint-python: install-lint @bash scripts/lint/wrapper.sh python +.PHONY: lint-ruby +# Check that every Ruby file parses, via `ruby -c`. +# Note: provisions the Ruby CLI explicitly; an absent interpreter is skipped. +lint-ruby: install-lint + @bash scripts/install/wrapper.sh ruby + @bash scripts/lint/wrapper.sh ruby + .PHONY: lint-shellcheck # Run shellcheck lint checks. lint-shellcheck: install-lint @bash scripts/lint/wrapper.sh shellcheck +.PHONY: lint-sql +# Check that every SQL file parses, via `sqlfluff parse`. +# Note: dialect comes from the nearest .sqlfluff; absent tooling is skipped. +lint-sql: install-lint + @bash scripts/install/wrapper.sh sql + @bash scripts/lint/wrapper.sh sql + .PHONY: meta-list # Print the repository role list. meta-list: @@ -784,12 +815,33 @@ test-signed: echo "✅ HEAD commit signature status: $$status" .PHONY: test-unit -# Run the unit test suite. -test-unit: install +# Run the unit test suite (every language). +test-unit: test-unit-python test-unit-javascript test-unit-php test-unit-ruby + +.PHONY: test-unit-javascript +# Run the JavaScript unit test suite (node:test, no extra dependency). +test-unit-javascript: + @bash scripts/tests/unit/javascript.sh + +.PHONY: test-unit-php +# Run the PHP unit test suite (PHPUnit). +# Note: skips loudly while the suite is empty or PHP is absent; see scripts/tests/unit/php.sh. +test-unit-php: + @bash scripts/tests/unit/php.sh + +.PHONY: test-unit-python +# Run the Python unit test suite. +test-unit-python: install @INFINITO_TEST_TYPE="unit" \ INFINITO_COMPILE=0 \ bash scripts/tests/code/wrapper.sh +.PHONY: test-unit-ruby +# Run the Ruby unit test suite (minitest, stdlib). +# Note: skips loudly while the suite is empty or Ruby is absent; see scripts/tests/unit/ruby.sh. +test-unit-ruby: + @bash scripts/tests/unit/ruby.sh + .PHONY: worktree-down # Stop a branch worktree's stack, release the checkout and free its slot. # Usage: make worktree-down branch= [base=] [force=true] diff --git a/aliases b/aliases index 81e7c4252f..8c0f189773 100644 --- a/aliases +++ b/aliases @@ -44,6 +44,7 @@ alias i8kelo='m kernel-loop-load' alias i8la='m alias' alias i8li='m lint' alias i8lica='m lint-action' +alias i8lics='m lint-css' alias i8lido='m lint-dockerfile' alias i8lija='m lint-javascript' alias i8lima='m lint-makefile' @@ -52,8 +53,11 @@ alias i8lime='m lint-mermaid' alias i8lina='m lint-ansible' alias i8lipa='m lint-packages' alias i8lipa2='m lint-playwright' +alias i8liph='m lint-php' alias i8lipo='m lint-python' +alias i8liru='m lint-ruby' alias i8lise='m lint-shellcheck' +alias i8lisq='m lint-sql' alias i8maal='i8source && python3 -m cli.meta.ci.query --modes auto --matrix' # complexity matrix of every deployable row, in CI query order alias i8maco='i8source && python3 -m cli.meta.ci.query --modes compose --matrix' # complexity matrix in compose query order alias i8maho='i8source && python3 -m cli.meta.ci.query --modes host --matrix' # complexity matrix in host query order @@ -138,6 +142,10 @@ alias i8tema='m test-main-merged' alias i8temesi='m test-merge-signed' alias i8teni='m test-integration' alias i8tenu='m test-unit' +alias i8tenuja='m test-unit-javascript' +alias i8tenuph='m test-unit-php' +alias i8tenupo='m test-unit-python' +alias i8tenuru='m test-unit-ruby' alias i8tepe='m test-performance' alias i8tesi='m test-signed' alias i8texe='m test-external' diff --git a/composer.json b/composer.json new file mode 100644 index 0000000000..f18dac2f6e --- /dev/null +++ b/composer.json @@ -0,0 +1,15 @@ +{ + "name": "infinito-nexus/core", + "description": "PHPUnit dependencies for the PHP shipped in roles/*/files/php/.", + "type": "project", + "license": "MIT", + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "config": { + "sort-packages": true + }, + "scripts": { + "test": "phpunit" + } +} diff --git a/package-lock.json b/package-lock.json index 481f6b1d42..734cd86a71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,220 @@ "@playwright/test": "^1.62.1", "eslint": "^10.8.1", "eslint-plugin-playwright": "^2.10.5", - "globals": "^17.9.0" + "globals": "^17.9.0", + "stylelint": "^16.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/memory/node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/memory/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.8.tgz", + "integrity": "sha512-CpMLjAvwQg3BL5S0IeqsZNMH7EQrEWi0kLKOC13ZBF0ZwERiLWlibNPJr8G1kdU3Ms/r2KiNrF81pUh2HwAHdg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz", + "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@dual-bundle/import-meta-resolve": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.2.1.tgz", + "integrity": "sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/JounQin" } }, "node_modules/@eslint-community/eslint-utils": { @@ -209,6 +422,51 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@playwright/test": { "version": "1.62.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", @@ -286,6 +544,59 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -309,6 +620,107 @@ "node": "20 || >=22" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/cacheable/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colord": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/colord/-/colord-2.10.0.tgz", + "integrity": "sha512-AidJptpBJmjTclAp9BkLwJi0T93fo5epJnbaZslpg6QVzpHjAiveF55mE9AcUJiGMqRHgMDY8soMsQtuNYMHfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -324,6 +736,43 @@ "node": ">= 8" } }, + "node_modules/css-functions-list": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/css-functions-list/-/css-functions-list-3.3.3.tgz", + "integrity": "sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -349,6 +798,46 @@ "dev": true, "license": "MIT" }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -540,6 +1029,36 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -554,6 +1073,43 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -567,6 +1123,19 @@ "node": ">=16.0.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -633,6 +1202,47 @@ "node": ">=10.13.0" } }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/globals": { "version": "17.9.0", "resolved": "https://registry.npmjs.org/globals/-/globals-17.9.0.tgz", @@ -646,6 +1256,77 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globjoin": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/globjoin/-/globjoin-0.1.4.tgz", + "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -656,6 +1337,33 @@ "node": ">= 4" } }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -666,6 +1374,20 @@ "node": ">=0.8.19" } }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -676,6 +1398,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -689,6 +1421,26 @@ "node": ">=0.10.0" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/is-plain-object/-/is-plain-object-5.1.0.tgz", + "integrity": "sha512-bUi/yjmtKYcRVUtWRGr0UA6xEFh2I6zWUwMrUXB3s7bmYCaZ8a+0ZsTRkrawh/mzlSD1Y0Ph8bp/U+TvBpWDNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -696,6 +1448,36 @@ "dev": true, "license": "ISC" }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -703,6 +1485,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -727,6 +1516,23 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/known-css-properties": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/known-css-properties/-/known-css-properties-0.37.0.tgz", + "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==", + "dev": true, + "license": "MIT" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -741,6 +1547,13 @@ "node": ">= 0.8.0" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -757,6 +1570,68 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/mathml-tag-names": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", + "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/meow": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -780,6 +1655,25 @@ "dev": true, "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -787,6 +1681,16 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -837,6 +1741,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -857,6 +1793,36 @@ "node": ">=8" } }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/playwright": { "version": "1.62.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", @@ -889,6 +1855,90 @@ "node": ">=20" } }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-resolve-nested-selector": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz", + "integrity": "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", + "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -909,6 +1959,102 @@ "node": ">=6" } }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -932,6 +2078,278 @@ "node": ">=8" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/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_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint": { + "version": "16.26.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/stylelint/-/stylelint-16.26.1.tgz", + "integrity": "sha512-v20V59/crfc8sVTAtge0mdafI3AdnzQ2KsWe6v523L4OA1bJO02S7MO2oyXDCS6iWb9ckIPnqAFVItqSBQr7jw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-syntax-patches-for-csstree": "^1.0.19", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3", + "@csstools/selector-specificity": "^5.0.0", + "@dual-bundle/import-meta-resolve": "^4.2.1", + "balanced-match": "^2.0.0", + "colord": "^2.9.3", + "cosmiconfig": "^9.0.0", + "css-functions-list": "^3.2.3", + "css-tree": "^3.1.0", + "debug": "^4.4.3", + "fast-glob": "^3.3.3", + "fastest-levenshtein": "^1.0.16", + "file-entry-cache": "^11.1.1", + "global-modules": "^2.0.0", + "globby": "^11.1.0", + "globjoin": "^0.1.4", + "html-tags": "^3.3.1", + "ignore": "^7.0.5", + "imurmurhash": "^0.1.4", + "is-plain-object": "^5.0.0", + "known-css-properties": "^0.37.0", + "mathml-tag-names": "^2.1.3", + "meow": "^13.2.0", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.5.6", + "postcss-resolve-nested-selector": "^0.1.6", + "postcss-safe-parser": "^7.0.1", + "postcss-selector-parser": "^7.1.0", + "postcss-value-parser": "^4.2.0", + "resolve-from": "^5.0.0", + "string-width": "^4.2.3", + "supports-hyperlinks": "^3.2.0", + "svg-tags": "^1.0.0", + "table": "^6.9.0", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "stylelint": "bin/stylelint.mjs" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/stylelint/node_modules/balanced-match": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/balanced-match/-/balanced-match-2.0.0.tgz", + "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stylelint/node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/stylelint/node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/stylelint/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/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/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -955,6 +2373,13 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -981,6 +2406,20 @@ "node": ">=0.10.0" } }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/repository/npm-proxy/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 6a6e87039d..5376f71873 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "@playwright/test": "^1.62.1", "eslint": "^10.8.1", "eslint-plugin-playwright": "^2.10.5", - "globals": "^17.9.0" + "globals": "^17.9.0", + "stylelint": "^16.0.0" }, "overrides": { "brace-expansion": "^5.0.9" diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000000..c4dd29ac38 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,19 @@ + + + + + tests/unit/php + + + + + roles + + + diff --git a/roles/svc-db-mariadb/files/sql/.sqlfluff b/roles/svc-db-mariadb/files/sql/.sqlfluff new file mode 100644 index 0000000000..5b702f70c2 --- /dev/null +++ b/roles/svc-db-mariadb/files/sql/.sqlfluff @@ -0,0 +1,6 @@ +[sqlfluff] +dialect = mysql +templater = placeholder + +[sqlfluff:templater:placeholder] +param_style = pyformat diff --git a/roles/web-app-pixelfed/files/sql/.sqlfluff b/roles/web-app-pixelfed/files/sql/.sqlfluff new file mode 100644 index 0000000000..5b702f70c2 --- /dev/null +++ b/roles/web-app-pixelfed/files/sql/.sqlfluff @@ -0,0 +1,6 @@ +[sqlfluff] +dialect = mysql +templater = placeholder + +[sqlfluff:templater:placeholder] +param_style = pyformat diff --git a/scripts/lint/css.sh b/scripts/lint/css.sh new file mode 100755 index 0000000000..3c8ed92d15 --- /dev/null +++ b/scripts/lint/css.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# shellcheck shell=bash +# +# stylelint reports a CssSyntaxError whatever the rule set says, so the empty +# config in .stylelintrc.json is enough to make this a parse check and keeps it +# from turning into a style opinion nobody signed up for. +# +# File discovery uses find, not git ls-files: inside the compose container git +# refuses the bind-mounted repo with "detected dubious ownership", which makes +# git ls-files print nothing and turns this lint into a silent no-op. + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)" + +cd "${REPO_ROOT}" + +mapfile -t css_files < <( + find . -type f -name '*.css' \ + -not -path './node_modules/*' \ + -not -path './.git/*' | + sed 's|^\./||' | sort +) +if [[ "${#css_files[@]}" -eq 0 ]]; then + printf 'No CSS files found.\n' + exit 0 +fi + +if [[ ! -d node_modules/stylelint ]]; then + printf 'SKIP: stylelint is not installed; %d file(s) left unchecked.\n' "${#css_files[@]}" + exit 0 +fi + +npx --no-install stylelint "${css_files[@]}" diff --git a/scripts/lint/php.sh b/scripts/lint/php.sh new file mode 100755 index 0000000000..d41f77122d --- /dev/null +++ b/scripts/lint/php.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# shellcheck shell=bash +# +# File discovery uses find, not git ls-files: inside the compose container git +# refuses the bind-mounted repo with "detected dubious ownership", which makes +# git ls-files print nothing and turns this lint into a silent no-op. + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)" + +cd "${REPO_ROOT}" + +mapfile -t php_files < <( + find . -type f -name '*.php' \ + -not -path './node_modules/*' \ + -not -path './vendor/*' \ + -not -path './.git/*' | + sed 's|^\./||' | sort +) +if [[ "${#php_files[@]}" -eq 0 ]]; then + printf 'No PHP files found.\n' + exit 0 +fi + +if ! command -v php >/dev/null 2>&1; then + printf 'SKIP: php is not installed; %d file(s) left unchecked.\n' "${#php_files[@]}" + exit 0 +fi + +failed=0 +for file in "${php_files[@]}"; do + if ! php -l "${file}"; then + failed=1 + fi +done + +exit "${failed}" diff --git a/scripts/lint/ruby.sh b/scripts/lint/ruby.sh new file mode 100755 index 0000000000..ce9d5503d5 --- /dev/null +++ b/scripts/lint/ruby.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# shellcheck shell=bash +# +# File discovery uses find, not git ls-files: inside the compose container git +# refuses the bind-mounted repo with "detected dubious ownership", which makes +# git ls-files print nothing and turns this lint into a silent no-op. + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)" + +cd "${REPO_ROOT}" + +mapfile -t ruby_files < <( + find . -type f -name '*.rb' \ + -not -path './node_modules/*' \ + -not -path './.git/*' | + sed 's|^\./||' | sort +) +if [[ "${#ruby_files[@]}" -eq 0 ]]; then + printf 'No Ruby files found.\n' + exit 0 +fi + +if ! command -v ruby >/dev/null 2>&1; then + printf 'SKIP: ruby is not installed; %d file(s) left unchecked.\n' "${#ruby_files[@]}" + exit 0 +fi + +failed=0 +for file in "${ruby_files[@]}"; do + if ! ruby -c "${file}" >/dev/null; then + printf '%s: does not parse\n' "${file}" >&2 + failed=1 + fi +done + +exit "${failed}" diff --git a/scripts/lint/sql.sh b/scripts/lint/sql.sh new file mode 100755 index 0000000000..c49aa57e01 --- /dev/null +++ b/scripts/lint/sql.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# shellcheck shell=bash +# +# Dialect comes from the nearest .sqlfluff: the repo root sets postgres, and a +# directory whose SQL targets another engine overrides it beside the files. +# +# File discovery uses find, not git ls-files: inside the compose container git +# refuses the bind-mounted repo with "detected dubious ownership", which makes +# git ls-files print nothing and turns this lint into a silent no-op. + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)" + +cd "${REPO_ROOT}" + +mapfile -t sql_files < <( + find . -type f -name '*.sql' \ + -not -path './node_modules/*' \ + -not -path './.git/*' | + sed 's|^\./||' | sort +) +if [[ "${#sql_files[@]}" -eq 0 ]]; then + printf 'No SQL files found.\n' + exit 0 +fi + +if ! command -v sqlfluff >/dev/null 2>&1; then + printf 'SKIP: sqlfluff is not installed; %d file(s) left unchecked.\n' "${#sql_files[@]}" + exit 0 +fi + +failed=0 +for file in "${sql_files[@]}"; do + if ! sqlfluff parse "${file}" >/dev/null; then + printf '%s: does not parse\n' "${file}" >&2 + failed=1 + fi +done + +exit "${failed}" diff --git a/scripts/tests/unit/javascript.sh b/scripts/tests/unit/javascript.sh new file mode 100755 index 0000000000..0098327297 --- /dev/null +++ b/scripts/tests/unit/javascript.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# shellcheck shell=bash +# +# Runs the JavaScript unit suite through node:test, Node's built-in runner. +# +# The glob is quoted so node expands it, not the shell: passing the bare +# directory makes the runner resolve test files as module specifiers and fail +# with MODULE_NOT_FOUND. + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../../.." && pwd)" + +cd "${REPO_ROOT}" + +suite="tests/unit/javascript" + +if [[ -z "$(find "${suite}" -name '*.test.js' 2>/dev/null)" ]]; then + printf 'SKIP test-unit-javascript: no JavaScript unit tests in %s\n' "${suite}" + exit 0 +fi + +exec node --test "${suite}/**/*.test.js" diff --git a/scripts/tests/unit/php.sh b/scripts/tests/unit/php.sh new file mode 100755 index 0000000000..2a206f465f --- /dev/null +++ b/scripts/tests/unit/php.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# shellcheck shell=bash +# +# Runs the PHP unit suite through PHPUnit. +# +# Every shipped PHP file carries a mirrored-unit-test exemption today, so the +# suite is empty and this skips rather than letting PHPUnit fail on "no tests +# executed". An absent interpreter or vendor tree skips loudly for the same +# reason: a machine without PHP must not fail the whole gate. + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../../.." && pwd)" + +cd "${REPO_ROOT}" + +suite="tests/unit/php" + +if [[ -z "$(find "${suite}" -name '*Test.php' 2>/dev/null)" ]]; then + printf 'SKIP test-unit-php: no PHP unit tests in %s (see lint mirrored-unit-test)\n' "${suite}" + exit 0 +fi + +if ! command -v php >/dev/null 2>&1; then + printf 'SKIP test-unit-php: php is not installed on this machine\n' + exit 0 +fi + +if [[ ! -x vendor/bin/phpunit ]]; then + printf "SKIP test-unit-php: run 'composer install' first\n" + exit 0 +fi + +exec vendor/bin/phpunit diff --git a/scripts/tests/unit/ruby.sh b/scripts/tests/unit/ruby.sh new file mode 100755 index 0000000000..bff739ad80 --- /dev/null +++ b/scripts/tests/unit/ruby.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# shellcheck shell=bash +# +# Runs the Ruby unit suite through minitest, which ships with Ruby itself. +# +# minitest/autorun has no directory runner, so the suite files are required +# explicitly and minitest's at_exit hook runs them. + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../../.." && pwd)" + +cd "${REPO_ROOT}" + +suite="tests/unit/ruby" + +if [[ -z "$(find "${suite}" -name '*_test.rb' 2>/dev/null)" ]]; then + printf 'SKIP test-unit-ruby: no Ruby unit tests in %s (see lint mirrored-unit-test)\n' "${suite}" + exit 0 +fi + +if ! command -v ruby >/dev/null 2>&1; then + printf 'SKIP test-unit-ruby: ruby is not installed on this machine\n' + exit 0 +fi + +exec ruby -I"${suite}" -e 'Dir.glob("'"${suite}"'/**/*_test.rb").each { |f| require File.expand_path(f) }' diff --git a/tests/unit/javascript/roles/sys-front-inj-logout/files/logout.test.js b/tests/unit/javascript/roles/sys-front-inj-logout/files/logout.test.js new file mode 100644 index 0000000000..585a1076bf --- /dev/null +++ b/tests/unit/javascript/roles/sys-front-inj-logout/files/logout.test.js @@ -0,0 +1,140 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const vm = require("node:vm"); + +const SOURCE = path.join( + __dirname, + "..", "..", "..", "..", "..", "..", + "roles", "sys-front-inj-logout", "files", "javascript", "logout.js", +); + +const LOGOUT_URL = "https://logout.example.org/"; + +function element(tagName, attributes = {}, text = "") { + const el = { + tagName, + dataset: {}, + style: {}, + listeners: [], + innerText: text, + attributes: [], + getAttribute(name) { + const found = el.attributes.find((a) => a.name === name); + return found ? found.value : null; + }, + setAttribute(name, value) { + const found = el.attributes.find((a) => a.name === name); + if (found) { + found.value = value; + return; + } + el.attributes.push({ name, value }); + }, + hasAttribute(name) { + return el.attributes.some((a) => a.name === name); + }, + addEventListener(type, handler, options) { + el.listeners.push({ type, handler, options }); + }, + querySelectorAll() { + return []; + }, + }; + + for (const [name, value] of Object.entries(attributes)) { + el.setAttribute(name, value); + } + el.id = el.getAttribute("id") || ""; + el.className = el.getAttribute("class") || ""; + return el; +} + +function patch(elements) { + const sandbox = { + console: { debug() {} }, + location: { href: "" }, + Element: class Element {}, + MutationObserver: class MutationObserver { + observe() {} + }, + document: { + body: {}, + querySelectorAll: () => elements, + }, + }; + sandbox.window = sandbox; + vm.createContext(sandbox); + vm.runInContext(fs.readFileSync(SOURCE, "utf8"), sandbox); + sandbox.initLogoutPatch(LOGOUT_URL, false); + return sandbox; +} + +test("an anchor whose text reads 'Log out' is redirected", () => { + const link = element("A", { href: "/account/exit" }, "Log out"); + patch([link]); + + assert.equal(link.getAttribute("href"), LOGOUT_URL); + assert.equal(link.dataset._logoutHandled, "true"); + assert.equal(link.listeners.length, 1); + assert.equal(link.listeners[0].options.capture, true); +}); + +test("a click on a patched element is intercepted, not followed", () => { + const link = element("A", { id: "logout-link" }, "Bye"); + const sandbox = patch([link]); + + let prevented = false; + link.listeners[0].handler({ preventDefault: () => { prevented = true; } }); + + assert.equal(prevented, true); + assert.equal(sandbox.window.location.href, LOGOUT_URL); +}); + +test("an href pointing at /logout is NOT enough to match", () => { + const link = element("A", { href: "/logout" }, "My profile"); + patch([link]); + + assert.equal(link.getAttribute("href"), "/logout"); + assert.equal(link.dataset._logoutHandled, undefined); +}); + +test("a non-interactive tag is never patched, whatever it says", () => { + const div = element("DIV", { id: "logout" }, "Log out"); + patch([div]); + + assert.equal(div.dataset._logoutHandled, undefined); + assert.equal(div.listeners.length, 0); +}); + +test("a button carrying formaction has that formaction rewritten", () => { + const button = element("BUTTON", { formaction: "/session/end", title: "Logout" }); + patch([button]); + + assert.equal(button.getAttribute("formaction"), LOGOUT_URL); +}); + +test("a data-* attribute value only counts on data-/aria- names", () => { + const matched = element("BUTTON", { "data-action": "logout" }, ""); + const ignored = element("BUTTON", { "value": "logout" }, ""); + patch([matched, ignored]); + + assert.equal(matched.dataset._logoutHandled, "true"); + assert.equal(ignored.dataset._logoutHandled, undefined); +}); + +test("an element already handled is not bound a second time", () => { + const link = element("A", { id: "logout" }, ""); + const sandbox = patch([link]); + sandbox.initLogoutPatch(LOGOUT_URL, false); + + assert.equal(link.listeners.length, 1); +}); + +test("a text block over 1000 characters is ignored as a match source", () => { + const link = element("A", {}, `${"x".repeat(1000)} log out`); + patch([link]); + + assert.equal(link.dataset._logoutHandled, undefined); +}); diff --git a/tests/unit/javascript/roles/web-app-keycloak/files/logout-panel.test.js b/tests/unit/javascript/roles/web-app-keycloak/files/logout-panel.test.js new file mode 100644 index 0000000000..e1f1083023 --- /dev/null +++ b/tests/unit/javascript/roles/web-app-keycloak/files/logout-panel.test.js @@ -0,0 +1,216 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); + +const PROJECT_ROOT = path.resolve(__dirname, "../../../../../.."); +const PANEL = path.join( + PROJECT_ROOT, + "roles/web-app-keycloak/files/javascript/logout-panel.js", +); +const CATALOGUE = path.join( + PROJECT_ROOT, + "roles/web-app-keycloak/files/logout_i18n.yml", +); +const ORIGIN = "https://logout.example.test"; +const DOMAINS = ["https://shop.example.test", "https://cloud.example.test"]; + +/** + * Read the catalogue without a YAML dependency. + * + * The file is generated, flat and two levels deep, so a line reader is enough + * and keeps this suite free of a parser it would otherwise need for one file. + */ +function readCatalogue() { + const out = {}; + let current = null; + for (const line of fs.readFileSync(CATALOGUE, "utf8").split("\n")) { + const top = line.match(/^([a-z]{2}):\s*$/); + if (top) { + current = {}; + out[top[1]] = current; + continue; + } + const entry = line.match(/^ {2}([a-z_]+):\s*(.*)$/); + if (entry && current) { + let value = entry[2].trim(); + if ( + (value.startsWith("'") && value.endsWith("'")) || + (value.startsWith('"') && value.endsWith('"')) + ) { + value = value.slice(1, -1).replace(/''/g, "'"); + } + current[entry[1]] = value; + } + } + return out; +} + +function element() { + const node = { + children: [], + style: { cssText: "" }, + attrs: {}, + id: "", + title: "", + href: "", + setAttribute(key, value) { + this.attrs[key] = value; + }, + appendChild(child) { + this.children.push(child); + return child; + }, + }; + let text = ""; + Object.defineProperty(node, "textContent", { + get: () => text, + set(value) { + text = value; + node.children.length = 0; + }, + enumerable: true, + }); + return node; +} + +/** Load the panel against a stubbed page and return handles for assertions. */ +function mount({ lang = "en", hostname = "auth.example.test", path: pathname = "/realms/x/protocol/openid-connect/logout" } = {}) { + const body = element(); + const listeners = {}; + const timers = {}; + let next = 0; + + global.window = { + __INFINITO_LOGOUT__: { origin: ORIGIN, i18n: readCatalogue() }, + location: { pathname, hostname }, + addEventListener(type, fn) { + listeners[type] = listeners[type] || []; + if (!listeners[type].includes(fn)) listeners[type].push(fn); + }, + removeEventListener(type, fn) { + listeners[type] = (listeners[type] || []).filter((f) => f !== fn); + }, + }; + global.document = { + readyState: "complete", + createElement: element, + querySelector: () => null, + getElementById: () => null, + documentElement: { lang }, + body, + addEventListener() {}, + }; + Object.defineProperty(globalThis, "navigator", { + value: { language: "en" }, + configurable: true, + writable: true, + }); + global.setTimeout = (fn, ms) => { + next += 1; + timers[next] = { fn, ms }; + return next; + }; + global.clearTimeout = (id) => { + if (id && timers[id]) timers[id].cleared = true; + }; + + // eslint-disable-next-line no-eval + eval(fs.readFileSync(PANEL, "utf8")); + + const post = (payload) => + (listeners.message || []).forEach((fn) => + fn({ origin: ORIGIN, data: { source: "universal-logout", ...payload } }), + ); + const panel = () => body.children[0]; + return { + post, + panel, + status: () => panel().children[0].textContent, + hint: () => panel().children[1].textContent, + counter: () => panel().children[2].textContent, + rows: () => panel().children[3].children, + armed: () => (listeners.beforeunload || []).length, + live: (ms) => + Object.values(timers).filter((t) => t.ms === ms && !t.cleared), + }; +} + +test("the catalogue reader sees every language", () => { + const catalogue = readCatalogue(); + assert.equal(Object.keys(catalogue).length, 30); + assert.equal(catalogue.de.dir, "ltr"); + assert.equal(catalogue.ar.dir, "rtl"); +}); + +test("a clean sweep ends by telling the visitor they may leave", () => { + const page = mount(); + page.post({ type: "start", domains: DOMAINS }); + assert.match(page.status(), /Signing you out/); + assert.equal(page.armed(), 1); + + page.post({ type: "host", host: DOMAINS[0], ok: true }); + page.post({ type: "host", host: DOMAINS[1], ok: true }); + page.post({ type: "done", total: 2, failed: 0 }); + + assert.match(page.status(), /Signed out everywhere/); + assert.match(page.hint(), /You can close this page now/); + assert.equal(page.counter(), "2 of 2 services signed out"); + assert.equal(page.armed(), 0, "the guard is released once the sweep is done"); +}); + +test("a failing host keeps a way out on screen", () => { + const page = mount(); + page.post({ type: "start", domains: DOMAINS }); + page.post({ type: "host", host: DOMAINS[1], ok: false }); + + const right = page.rows()[1].children[1]; + assert.match(right.children[0].textContent, /failed/); + assert.equal(right.children[1].href, `${DOMAINS[1]}/logout?manual=1`); +}); + +test("the panel speaks the language Keycloak chose", () => { + const page = mount({ lang: "de" }); + page.post({ type: "start", domains: DOMAINS }); + assert.match(page.status(), /Sie werden von Ihren Diensten abgemeldet/); + + page.post({ type: "host", host: DOMAINS[0], ok: true }); + assert.equal(page.counter(), "1 von 2 Diensten abgemeldet"); +}); + +test("right-to-left languages flip the panel", () => { + const page = mount({ lang: "ar" }); + page.post({ type: "start", domains: DOMAINS }); + assert.equal(page.panel().attrs.dir, "rtl"); +}); + +test("an unknown language falls back to English", () => { + const page = mount({ lang: "klingon" }); + page.post({ type: "start", domains: DOMAINS }); + assert.match(page.status(), /Signing you out/); +}); + +test("onion sessions are told to expect a longer wait", () => { + const page = mount({ hostname: "abcdefghij.onion" }); + page.post({ type: "start", domains: DOMAINS }); + assert.match(page.hint(), /Over Tor this can take a while/); +}); + +test("a sweep that stalls says so instead of spinning forever", () => { + const page = mount(); + page.post({ type: "start", domains: DOMAINS }); + page.post({ type: "host", host: DOMAINS[0], ok: true }); + + const [sweep] = page.live(60000); + assert.ok(sweep, "the stall timer is armed on start"); + sweep.fn(); + + assert.match(page.status(), /Could not confirm every sign-out/); + assert.doesNotMatch(page.status(), /Signed out everywhere/); + assert.equal(page.armed(), 0, "the guard fails open"); +}); + +test("the panel stays off every other Keycloak page", () => { + const page = mount({ path: "/realms/x/account" }); + assert.equal(page.armed(), 0); +}); diff --git a/utils/install/lint/__main__.py b/utils/install/lint/__main__.py index 5869d4c536..dc7b97c90c 100644 --- a/utils/install/lint/__main__.py +++ b/utils/install/lint/__main__.py @@ -23,10 +23,14 @@ from utils.install.lint import ( mbake, mermaid_cli, packages, + php, playwright, + ruby, ruff, shellcheck, shfmt, + sqlfluff, + stylelint, ) from utils.install.lint.ansible import collections as ansible_collections from utils.install.lint.ansible import commands as ansible_commands @@ -101,6 +105,22 @@ def _install_packages_tools() -> None: packages.ensure() +def _install_php_tools() -> None: + php.ensure() + + +def _install_ruby_tools() -> None: + ruby.ensure() + + +def _install_sql_tools() -> None: + sqlfluff.ensure() + + +def _install_css_tools() -> None: + stylelint.ensure() + + _GROUP_FN_NAMES = { "action": "_install_action_tools", "ansible": "_install_ansible_tools", @@ -113,6 +133,10 @@ _GROUP_FN_NAMES = { "javascript": "_install_javascript_tools", "playwright": "_install_playwright_tools", "packages": "_install_packages_tools", + "php": "_install_php_tools", + "ruby": "_install_ruby_tools", + "sql": "_install_sql_tools", + "css": "_install_css_tools", } @@ -160,7 +184,8 @@ def _dispatch(group: str) -> None: if fn_name is None: raise RuntimeError( "Usage: python -m utils.install.lint " - "[all|action|ansible|python|shellcheck|dockerfile|markdown|mermaid|makefile|javascript|playwright|packages]..." + "[all|action|ansible|python|shellcheck|dockerfile|markdown|mermaid|makefile|" + "javascript|playwright|packages|php|ruby|sql|css]..." ) globals()[fn_name]() diff --git a/utils/install/lint/php.py b/utils/install/lint/php.py new file mode 100644 index 0000000000..c286b04acd --- /dev/null +++ b/utils/install/lint/php.py @@ -0,0 +1,24 @@ +"""Install the PHP CLI via system package manager (parser for lint-php). + +Provisioning is best effort: a host without PHP, or one that cannot install it, +must not fail the whole gate. scripts/lint/php.sh skips loudly instead. +""" + +from __future__ import annotations + +from utils.install.primitives import log, warn, which +from utils.install.system_pkg import install_command_via_pkg + + +def ensure() -> None: + if which("php"): + return + + log("Missing command 'php'. Attempting system package installation.") + try: + install_command_via_pkg("php") + except RuntimeError as exc: + warn(f"[install-lint] php could not be installed: {exc}") + + if not which("php"): + warn("[install-lint] php is unavailable; lint-php will skip.") diff --git a/utils/install/lint/ruby.py b/utils/install/lint/ruby.py new file mode 100644 index 0000000000..ad31b526f2 --- /dev/null +++ b/utils/install/lint/ruby.py @@ -0,0 +1,24 @@ +"""Install the Ruby CLI via system package manager (parser for lint-ruby). + +Provisioning is best effort: a host without Ruby, or one that cannot install it, +must not fail the whole gate. scripts/lint/ruby.sh skips loudly instead. +""" + +from __future__ import annotations + +from utils.install.primitives import log, warn, which +from utils.install.system_pkg import install_command_via_pkg + + +def ensure() -> None: + if which("ruby"): + return + + log("Missing command 'ruby'. Attempting system package installation.") + try: + install_command_via_pkg("ruby") + except RuntimeError as exc: + warn(f"[install-lint] ruby could not be installed: {exc}") + + if not which("ruby"): + warn("[install-lint] ruby is unavailable; lint-ruby will skip.") diff --git a/utils/install/lint/sqlfluff.py b/utils/install/lint/sqlfluff.py new file mode 100644 index 0000000000..add3cf4366 --- /dev/null +++ b/utils/install/lint/sqlfluff.py @@ -0,0 +1,24 @@ +"""Install sqlfluff via pip (parser for lint-sql). + +Provisioning is best effort: a host that cannot reach PyPI must not fail the +whole gate. scripts/lint/sql.sh skips loudly instead. +""" + +from __future__ import annotations + +from utils.install.pip import install_pip_pkg +from utils.install.primitives import log, warn, which + + +def ensure() -> None: + if which("sqlfluff"): + return + + log("Missing command 'sqlfluff'. Installing via pip.") + try: + install_pip_pkg("sqlfluff") + except (RuntimeError, OSError) as exc: + warn(f"[install-lint] sqlfluff could not be installed: {exc}") + + if not which("sqlfluff"): + warn("[install-lint] sqlfluff is unavailable; lint-sql will skip.") diff --git a/utils/install/lint/stylelint.py b/utils/install/lint/stylelint.py new file mode 100644 index 0000000000..6e5293403c --- /dev/null +++ b/utils/install/lint/stylelint.py @@ -0,0 +1,28 @@ +"""Install stylelint (locally in repo root via npm; parser for lint-css). + +Provisioning is best effort: a host that cannot reach the npm registry must not +fail the whole gate. scripts/lint/css.sh skips loudly instead. +""" + +from __future__ import annotations + +from pathlib import Path + +from utils.cache import PROJECT_ROOT +from utils.install.npm import npm_install_local_in_repo +from utils.install.primitives import log, warn + + +def ensure() -> None: + repo_root = Path(PROJECT_ROOT) + if (repo_root / "node_modules" / "stylelint").is_dir(): + return + + log("Missing local 'stylelint'. Installing via npm (in repo root).") + try: + npm_install_local_in_repo(str(repo_root)) + except (RuntimeError, OSError) as exc: + warn(f"[install-lint] stylelint could not be installed: {exc}") + + if not (repo_root / "node_modules" / "stylelint").is_dir(): + warn("[install-lint] stylelint is unavailable; lint-css will skip.") diff --git a/utils/install/system_pkg.py b/utils/install/system_pkg.py index 8407869c38..3886d874ea 100644 --- a/utils/install/system_pkg.py +++ b/utils/install/system_pkg.py @@ -102,6 +102,14 @@ _COMMAND_PACKAGES: dict[str, dict[str, list[str]]] = { "shfmt": {m: ["shfmt"] for m in _SUPPORTED}, "shellcheck": {m: ["shellcheck"] for m in _SUPPORTED}, "unzip": {m: ["unzip"] for m in _SUPPORTED}, + "php": { + "pacman": ["php"], + "apt-get": ["php-cli", "php"], + "dnf": ["php-cli", "php"], + "yum": ["php-cli", "php"], + "brew": ["php"], + }, + "ruby": {m: ["ruby"] for m in _SUPPORTED}, "npm": { "pacman": ["npm", "nodejs"], "apt-get": ["npm", "nodejs"],